From affc97c5279fc30c94af60ce1a5a8fe97bc8959c Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 22 Sep 2026 11:58:10 +0000 Subject: [PATCH] Choix de la langue VF/VOSTFR sur la fiche titre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Episode : nouveau champ version (vf/vostfr) - French-Manga : épisodes listés pour chaque langue disponible (VOSTFR puis VF) au lieu d'un choix automatique, fragment #ep=- déjà résolu par extract_embed_links - Vostfree : langue déduite du titre h1 ou du slug de la fiche - UI : filtre VOSTFR/VF/Tout (défaut VOSTFR), badge par épisode, téléchargement de saison limité à la langue filtrée, compteur d'épisodes unique, version incluse dans le nom de fichier --- app/scrapers/base.py | 2 +- app/scrapers/sources/french_manga.py | 31 ++++++++++---------- app/scrapers/sources/vostfree.py | 12 ++++++++ app/static/css/style.css | 27 +++++++++++++++++ app/templates/title.html | 43 ++++++++++++++++++++++++---- 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/app/scrapers/base.py b/app/scrapers/base.py index f2459b3..058ce48 100644 --- a/app/scrapers/base.py +++ b/app/scrapers/base.py @@ -38,7 +38,7 @@ class Episode: title: str | None url: str # page de l'épisode chez la source season: int = 1 - + version: str | None = None # langue ("vf" / "vostfr") quand la source la distingue @dataclass class TitleDetails: diff --git a/app/scrapers/sources/french_manga.py b/app/scrapers/sources/french_manga.py index 096327f..28a3090 100644 --- a/app/scrapers/sources/french_manga.py +++ b/app/scrapers/sources/french_manga.py @@ -259,29 +259,28 @@ class FrenchMangaScraper(SourceScraper): if season_match: season = int(season_match.group(1)) - numbers: set[float] = set() + info = data.get("info") or {} + episodes: list[Episode] = [] for version in versions: + numbers: set[float] = set() for number_text in data.get(version) or {}: try: numbers.add(float(number_text)) except ValueError: logger.warning("french_manga : numéro d'épisode invalide %r", number_text) - - info = data.get("info") or {} - episodes: list[Episode] = [] - for number in sorted(numbers): - number_text = str(int(number)) if number.is_integer() else str(number) - episode_info = info.get(number_text) or info.get(str(int(number))) - label = episode_info.get("title") if isinstance(episode_info, dict) else None - version = next((v for v in versions if number_text in (data.get(v) or {})), versions[0]) - episodes.append( - Episode( - number=number, - title=label, - url=f"{page_url}#{config['episodes']['fragment_prefix']}={version}-{number_text}", - season=season, + for number in sorted(numbers): + number_text = str(int(number)) if number.is_integer() else str(number) + episode_info = info.get(number_text) or info.get(str(int(number))) + label = episode_info.get("title") if isinstance(episode_info, dict) else None + episodes.append( + Episode( + number=number, + title=label, + url=f"{page_url}#{config['episodes']['fragment_prefix']}={version}-{number_text}", + season=season, + version=version, + ) ) - ) if not episodes: raise ScrapeError(f"french_manga : aucun épisode trouvé pour {source_id}") return episodes diff --git a/app/scrapers/sources/vostfree.py b/app/scrapers/sources/vostfree.py index b874f9c..52c6385 100644 --- a/app/scrapers/sources/vostfree.py +++ b/app/scrapers/sources/vostfree.py @@ -85,6 +85,7 @@ _SLUG_RE = re.compile(r"([^/]+)\.html?$") _NUMBER_RE = re.compile(r"(\d+(?:[.,]\d+)?)") _SEASON_RE = re.compile(r"Saison\s*:?\s*(\d+)", re.IGNORECASE) _SAFE_FRAGMENT_RE = re.compile(r"^[A-Za-z0-9_-]+$") +_VERSION_RE = re.compile(r"\b(vostfr|vf)\b", re.IGNORECASE) def _merged_config() -> dict: @@ -246,8 +247,18 @@ class VostfreeScraper(SourceScraper): return int(season_match.group(1)) return 1 + def _detect_version(self, soup: BeautifulSoup, page_url: str) -> str | None: + config = _merged_config() + title_el = soup.select_one(config["details"]["title"]) + for text in (self._text(title_el), page_url): + match = _VERSION_RE.search(text) + if match: + return match.group(1).lower() + return None + def _parse_episodes(self, soup: BeautifulSoup, page_url: str, season: int) -> list[Episode]: config = _merged_config() + version = self._detect_version(soup, page_url) options = soup.select(config["episodes"]["option"]) entries: list[tuple[str, str]] = [] seen_ids: set[str] = set() @@ -274,6 +285,7 @@ class VostfreeScraper(SourceScraper): title=label or f"Episode {index}", url=f"{page_url}#{button_id}" if button_id else page_url, season=season, + version=version, ) ) return episodes diff --git a/app/static/css/style.css b/app/static/css/style.css index 6637f51..bc6138a 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -676,3 +676,30 @@ button { font-family: inherit; } .update-banner .spin { display: inline-block; animation: update-spin 1s linear infinite; } @keyframes update-spin { to { transform: rotate(360deg); } } +/* ------------------------------------------------------------ filtre VF/VOSTFR */ + +.version-filter { + display: inline-flex; + gap: 0.2rem; + background: var(--surface-2); + padding: 0.2rem; + border-radius: 99px; + margin-left: 0.8rem; + vertical-align: middle; +} +.version-tab { + border: none; + background: transparent; + color: var(--text-dim); + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 0.28rem 0.75rem; + border-radius: 99px; + cursor: pointer; + transition: all 0.15s; +} +.version-tab:hover { color: var(--text); } +.version-tab.active { background: var(--accent); color: #fff; } +.badge-version { background: rgba(90, 200, 250, 0.14); color: #7ec8f5; } diff --git a/app/templates/title.html b/app/templates/title.html index 666306d..c7c700b 100644 --- a/app/templates/title.html +++ b/app/templates/title.html @@ -28,7 +28,7 @@
★ /10 Année : - épisode(s) + épisode(s)
-

Épisodes

+

Épisodes + + + + +

-