Torznab : recherche anime Sonarr (numéro absolu) + titre de release aligné sur la requête
docker / build-push (push) Successful in 27s
docker / build-push (push) Successful in 27s
- t=search&q=Titre 02 : le numéro absolu suffixé par Sonarr est extrait et utilisé comme filtre épisode, au lieu de partir tel quel au scraper (chaîne introuvable → résultats parasites) - le titre des releases reprend la requête Sonarr (titre/alias TVDB) plutôt que le titre du scraper : Sonarr reconnaît sa série au lieu de rejeter en « Unknown Series »
This commit is contained in:
+23
-2
@@ -17,6 +17,7 @@ Formats :
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
@@ -48,6 +49,11 @@ _MAX_RELEASES = 100 # borne du flux retourné
|
||||
_SEARCH_TIMEOUT = 40.0 # scraping lent : garde-fou par source
|
||||
_EPISODE_CACHE_TTL = 600.0 # listes d'épisodes re-scrapées au bout de 10 min
|
||||
|
||||
# Recherche anime Sonarr : t=search&q=Titre 02 — le numéro d'épisode ABSOLU
|
||||
# est suffixé au titre (2 à 4 chiffres). Sans décodage, le scraper reçoit une
|
||||
# chaîne introuvable (« Back Street Girls GOKUDOLLS 02 ») et renvoie n'importe quoi.
|
||||
_ANIME_ABS_RE = re.compile(r"^(?P<title>.+?)\s+(?P<abs>\d{2,4})$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Release:
|
||||
@@ -149,7 +155,13 @@ class TorznabService:
|
||||
# ------------------------------------------------------------ recherche
|
||||
|
||||
async def tvsearch(self, q: str, season: int | None = None, ep: int | None = None) -> list[Release]:
|
||||
"""Recherche type Sonarr : série (+ saison/épisode optionnels)."""
|
||||
"""Recherche type Sonarr : série (+ saison/épisode optionnels).
|
||||
|
||||
Le titre publié reprend la requête Sonarr (titre TVDB ou alias) plutôt
|
||||
que le titre du scraper : c'est ce titre-là que Sonarr reconnaît lors
|
||||
du parsing de la release.
|
||||
"""
|
||||
q, ep = self._parse_sonarr_query(q, ep)
|
||||
releases: list[Release] = []
|
||||
for source in await self._enabled_sources():
|
||||
outcomes = await self._search_source(source, q)
|
||||
@@ -159,12 +171,21 @@ class TorznabService:
|
||||
except (ScrapeError, TimeoutError):
|
||||
continue
|
||||
releases.extend(
|
||||
self._releases_for(result.title, source, result.source_id, episodes, season, ep)
|
||||
self._releases_for(q, source, result.source_id, episodes, season, ep)
|
||||
)
|
||||
if len(releases) >= _MAX_RELEASES:
|
||||
return releases[:_MAX_RELEASES]
|
||||
return releases
|
||||
|
||||
@staticmethod
|
||||
def _parse_sonarr_query(q: str, ep: int | None) -> tuple[str, int | None]:
|
||||
"""Extrait le numéro absolu suffixé par Sonarr (« Titre 02 » → ep=2)."""
|
||||
if ep is None:
|
||||
match = _ANIME_ABS_RE.match(q.strip())
|
||||
if match:
|
||||
return match.group("title").strip(), int(match.group("abs"))
|
||||
return q, ep
|
||||
|
||||
async def search(self, q: str) -> list[Release]:
|
||||
"""Recherche libre : tous les épisodes des fiches trouvées."""
|
||||
return await self.tvsearch(q)
|
||||
|
||||
@@ -128,6 +128,37 @@ async def test_torznab_tvsearch_all_episodes(client, apikey, fake_source):
|
||||
assert "Frieren S02E01 VOSTFR WEB-DL" in titles
|
||||
|
||||
|
||||
async def test_torznab_anime_search_absolute_episode(client, apikey, fake_source):
|
||||
# Requête anime Sonarr : t=search&q=Titre 02 (numéro absolu suffixé au titre)
|
||||
r = await client.get(
|
||||
"/torznab/api", params={"t": "search", "q": "Frieren 02", "apikey": apikey}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
root = ET.fromstring(r.text)
|
||||
items = root.findall(".//item")
|
||||
assert len(items) == 1
|
||||
assert items[0].find("title").text == "Frieren S01E02 VOSTFR WEB-DL"
|
||||
|
||||
|
||||
async def test_torznab_release_title_uses_sonarr_query(client, apikey, fake_source):
|
||||
# Le titre publié reprend la requête Sonarr (titre/alias TVDB), pas le titre
|
||||
# du scraper : sinon Sonarr rejette la release en « Unknown Series ».
|
||||
r = await client.get(
|
||||
"/torznab/api",
|
||||
params={
|
||||
"t": "tvsearch",
|
||||
"q": "Frieren: Beyond Journey's End",
|
||||
"season": 1,
|
||||
"ep": 2,
|
||||
"apikey": apikey,
|
||||
},
|
||||
)
|
||||
root = ET.fromstring(r.text)
|
||||
items = root.findall(".//item")
|
||||
assert len(items) == 1
|
||||
assert items[0].find("title").text == "Frieren: Beyond Journey's End S01E02 VOSTFR WEB-DL"
|
||||
|
||||
|
||||
async def test_torznab_search_without_query_serves_latest(client, apikey, fake_source):
|
||||
# Sans q : flux RSS des nouveautés (RSS sync / test Sonarr), pas une erreur 400.
|
||||
async def fake_latest():
|
||||
|
||||
Reference in New Issue
Block a user