docs: Update CLAUDE.md with three-tier architecture and new providers

- Added new video players: Vidzy, LuLuvid, Uqload
- Added new anime site: French-Manga
- Added new series sites category with FS7
- Updated documentation to reflect three-tier architecture (anime sites → series sites → video players)
- Added BaseSeriesSite interface documentation
- Added "Adding New Series Site" section
- Updated test organization with test_french_manga.py

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
root
2026-01-25 10:34:39 +00:00
parent 3afad41d46
commit 4d280b5239
16 changed files with 1507 additions and 53 deletions
@@ -9,6 +9,9 @@ from .lpayer import LpayerDownloader
from .unfichier import UnFichierDownloader
from .uptobox import UptoboxDownloader
from .rapidfile import RapidFileDownloader
from .vidzy import VidzyDownloader
from .luluv import LuLuvidDownloader
from .uqload import UqloadDownloader
__all__ = [
"BaseVideoPlayer",
@@ -20,6 +23,9 @@ __all__ = [
"UnFichierDownloader",
"UptoboxDownloader",
"RapidFileDownloader",
"VidzyDownloader",
"LuLuvidDownloader",
"UqloadDownloader",
]
@@ -34,6 +40,9 @@ def get_video_player(url: str) -> BaseVideoPlayer:
UnFichierDownloader(),
UptoboxDownloader(),
RapidFileDownloader(),
VidzyDownloader(),
LuLuvidDownloader(),
UqloadDownloader(),
]
for player in players:
+112
View File
@@ -0,0 +1,112 @@
"""LuLuvid video hosting service downloader"""
import logging
from typing import Optional
from .base import BaseVideoPlayer
from bs4 import BeautifulSoup
from app.utils import sanitize_filename
logger = logging.getLogger(__name__)
class LuLuvidDownloader(BaseVideoPlayer):
"""
Downloader for LuLuvid video hosting service.
LuLuvid is a video hosting platform used by various anime streaming sites.
"""
def can_handle(self, url: str) -> bool:
"""Check if this downloader can handle the given URL"""
return "luluv" in url.lower() or "luluvid" in url.lower()
async def get_download_link(
self,
url: str,
target_filename: Optional[str] = None
) -> tuple[str, str]:
"""
Extract direct download link and filename from LuLuvid URL.
Args:
url: The LuLuvid video player URL
target_filename: Optional filename override
Returns:
Tuple of (download_url, filename)
"""
try:
logger.info(f"Fetching LuLuvid URL: {url}")
# Fetch the page
response = await self.client.get(url)
response.raise_for_status()
html = response.text
soup = BeautifulSoup(html, 'lxml')
# Method 1: Look for video source in <video> tag
video_tag = soup.find('video')
if video_tag and video_tag.get('src'):
download_url = video_tag['src']
logger.info(f"Found video source from <video> tag")
else:
# Method 2: Look for source in <source> tag
source_tag = soup.find('source')
if source_tag and source_tag.get('src'):
download_url = source_tag['src']
logger.info(f"Found video source from <source> tag")
else:
# Method 3: Look for video URL in JavaScript
# LuLuvid often stores the video URL in a JavaScript variable
scripts = soup.find_all('script')
for script in scripts:
if script.string:
# Look for patterns like 'file:"URL"' or 'source:"URL"'
import re
patterns = [
r'file\s*:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
r'source\s*:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
r'videoUrl\s*:\s*["\']([^"\']+)["\']',
r'"url"\s*:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
r'["\']src["\']\s*:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
]
for pattern in patterns:
match = re.search(pattern, script.string)
if match:
download_url = match.group(1)
logger.info(f"Found video source from JavaScript")
break
if 'download_url' in locals():
break
if 'download_url' not in locals():
raise ValueError("Could not find video URL in page")
# Ensure URL is absolute
if not download_url.startswith('http'):
if download_url.startswith('//'):
download_url = 'https:' + download_url
else:
from urllib.parse import urljoin
download_url = urljoin(url, download_url)
# Generate filename
if target_filename:
filename = sanitize_filename(target_filename)
else:
# Try to extract filename from URL
filename = download_url.split('/')[-1].split('?')[0]
if not filename or len(filename) < 5:
filename = "luluv_video.mp4"
filename = sanitize_filename(filename)
# Ensure .mp4 extension
if not filename.endswith('.mp4'):
filename += '.mp4'
logger.info(f"Successfully extracted LuLuvid download link: {filename}")
return download_url, filename
except Exception as e:
logger.error(f"Error extracting LuLuvid download link: {e}")
raise ValueError(f"Failed to extract download link from LuLuvid: {str(e)}")
+110
View File
@@ -0,0 +1,110 @@
"""Uqload video hosting service downloader"""
import logging
import re
from typing import Optional
from .base import BaseVideoPlayer
from bs4 import BeautifulSoup
from app.utils import sanitize_filename
logger = logging.getLogger(__name__)
class UqloadDownloader(BaseVideoPlayer):
"""
Downloader for Uqload video hosting service.
Uqload is a video hosting platform used by French Stream and other streaming sites.
"""
def can_handle(self, url: str) -> bool:
"""Check if this downloader can handle the given URL"""
return "uqload" in url.lower()
async def get_download_link(
self,
url: str,
target_filename: Optional[str] = None
) -> tuple[str, str]:
"""
Extract direct download link and filename from Uqload URL.
Args:
url: The Uqload video player URL
target_filename: Optional filename override
Returns:
Tuple of (download_url, filename)
"""
try:
logger.info(f"Fetching Uqload URL: {url}")
# Fetch the page
response = await self.client.get(url)
response.raise_for_status()
html = response.text
# Method 1: Look for video URL in JavaScript
# Uqload stores the video URL in a JavaScript variable like: sources: ["URL"]
patterns = [
r'sources:\s*\["([^"]+\.mp4[^"]*)"\]',
r'sources:\s*\[["\']([^"\']+\.mp4[^"\']*)["\']\]',
r'"sources":\s*\["([^"]+\.mp4[^"]*)"\]',
r'file:\s*"([^"]+\.mp4[^"]*)"',
r'file:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
r'"file"\s*:\s*"([^"]+\.mp4[^"]*)"',
]
for pattern in patterns:
match = re.search(pattern, html)
if match:
download_url = match.group(1)
# Clean up any escape characters
download_url = download_url.replace('\\/', '/')
logger.info(f"Found video source from JavaScript pattern: {pattern[:20]}...")
break
else:
# Method 2: Try parsing with BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
# Look for video tag
video_tag = soup.find('video')
if video_tag and video_tag.get('src'):
download_url = video_tag['src']
logger.info(f"Found video source from <video> tag")
else:
# Look for source tag
source_tag = soup.find('source')
if source_tag and source_tag.get('src'):
download_url = source_tag['src']
logger.info(f"Found video source from <source> tag")
else:
raise ValueError("Could not find video URL in Uqload page")
# Ensure URL is absolute
if not download_url.startswith('http'):
if download_url.startswith('//'):
download_url = 'https:' + download_url
else:
from urllib.parse import urljoin
download_url = urljoin(url, download_url)
# Generate filename
if target_filename:
filename = sanitize_filename(target_filename)
else:
# Try to extract filename from URL
filename = download_url.split('/')[-1].split('?')[0]
if not filename or len(filename) < 5:
filename = "uqload_video.mp4"
filename = sanitize_filename(filename)
# Ensure .mp4 extension
if not filename.endswith('.mp4'):
filename += '.mp4'
logger.info(f"Successfully extracted Uqload download link: {filename}")
return download_url, filename
except Exception as e:
logger.error(f"Error extracting Uqload download link: {e}")
raise ValueError(f"Failed to extract download link from Uqload: {str(e)}")
+111
View File
@@ -0,0 +1,111 @@
"""Vidzy video hosting service downloader"""
import logging
from typing import Optional
from .base import BaseVideoPlayer
from bs4 import BeautifulSoup
from app.utils import sanitize_filename
logger = logging.getLogger(__name__)
class VidzyDownloader(BaseVideoPlayer):
"""
Downloader for Vidzy video hosting service.
Vidzy is a video hosting platform used by various anime streaming sites.
"""
def can_handle(self, url: str) -> bool:
"""Check if this downloader can handle the given URL"""
return "vidzy" in url.lower()
async def get_download_link(
self,
url: str,
target_filename: Optional[str] = None
) -> tuple[str, str]:
"""
Extract direct download link and filename from Vidzy URL.
Args:
url: The Vidzy video player URL
target_filename: Optional filename override
Returns:
Tuple of (download_url, filename)
"""
try:
logger.info(f"Fetching Vidzy URL: {url}")
# Fetch the page
response = await self.client.get(url)
response.raise_for_status()
html = response.text
soup = BeautifulSoup(html, 'lxml')
# Method 1: Look for video source in <video> tag
video_tag = soup.find('video')
if video_tag and video_tag.get('src'):
download_url = video_tag['src']
logger.info(f"Found video source from <video> tag")
else:
# Method 2: Look for source in <source> tag
source_tag = soup.find('source')
if source_tag and source_tag.get('src'):
download_url = source_tag['src']
logger.info(f"Found video source from <source> tag")
else:
# Method 3: Look for video URL in JavaScript
# Vidzy often stores the video URL in a JavaScript variable
scripts = soup.find_all('script')
for script in scripts:
if script.string:
# Look for patterns like 'file:"URL"' or 'file: "URL"'
import re
patterns = [
r'file\s*:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
r'source\s*:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
r'videoUrl\s*:\s*["\']([^"\']+)["\']',
r'"url"\s*:\s*["\']([^"\']+\.mp4[^"\']*)["\']',
]
for pattern in patterns:
match = re.search(pattern, script.string)
if match:
download_url = match.group(1)
logger.info(f"Found video source from JavaScript")
break
if 'download_url' in locals():
break
if 'download_url' not in locals():
raise ValueError("Could not find video URL in page")
# Ensure URL is absolute
if not download_url.startswith('http'):
if download_url.startswith('//'):
download_url = 'https:' + download_url
else:
from urllib.parse import urljoin
download_url = urljoin(url, download_url)
# Generate filename
if target_filename:
filename = sanitize_filename(target_filename)
else:
# Try to extract filename from URL
filename = download_url.split('/')[-1].split('?')[0]
if not filename or len(filename) < 5:
filename = "vidzy_video.mp4"
filename = sanitize_filename(filename)
# Ensure .mp4 extension
if not filename.endswith('.mp4'):
filename += '.mp4'
logger.info(f"Successfully extracted Vidzy download link: {filename}")
return download_url, filename
except Exception as e:
logger.error(f"Error extracting Vidzy download link: {e}")
raise ValueError(f"Failed to extract download link from Vidzy: {str(e)}")