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 asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime
|
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
|
_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
|
_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
|
@dataclass
|
||||||
class Release:
|
class Release:
|
||||||
@@ -149,7 +155,13 @@ class TorznabService:
|
|||||||
# ------------------------------------------------------------ recherche
|
# ------------------------------------------------------------ recherche
|
||||||
|
|
||||||
async def tvsearch(self, q: str, season: int | None = None, ep: int | None = None) -> list[Release]:
|
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] = []
|
releases: list[Release] = []
|
||||||
for source in await self._enabled_sources():
|
for source in await self._enabled_sources():
|
||||||
outcomes = await self._search_source(source, q)
|
outcomes = await self._search_source(source, q)
|
||||||
@@ -159,12 +171,21 @@ class TorznabService:
|
|||||||
except (ScrapeError, TimeoutError):
|
except (ScrapeError, TimeoutError):
|
||||||
continue
|
continue
|
||||||
releases.extend(
|
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:
|
if len(releases) >= _MAX_RELEASES:
|
||||||
return releases[:_MAX_RELEASES]
|
return releases[:_MAX_RELEASES]
|
||||||
return 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]:
|
async def search(self, q: str) -> list[Release]:
|
||||||
"""Recherche libre : tous les épisodes des fiches trouvées."""
|
"""Recherche libre : tous les épisodes des fiches trouvées."""
|
||||||
return await self.tvsearch(q)
|
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
|
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):
|
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.
|
# Sans q : flux RSS des nouveautés (RSS sync / test Sonarr), pas une erreur 400.
|
||||||
async def fake_latest():
|
async def fake_latest():
|
||||||
|
|||||||
Reference in New Issue
Block a user