Compatibilité Sonarr : flux RSS Torznab, catégories qBittorrent, save_path, TLS auto-signé
- torznab : tvsearch sans q sert les nouveautés (RSS sync + test Sonarr) au lieu d'une erreur 400 — ajoute TorznabService.latest_releases() - qbit : POST /torrents/createCategory + GET /torrents/categories stateful (Sonarr crée sa catégorie tv-sonarr puis la revérifie) - qbit : /app/preferences expose save_path=/downloads (health Sonarr) - sonarr : verify=False sur httpx — reverse proxy swizzin en certificat auto-signé (usage LAN)
This commit is contained in:
+12
-2
@@ -88,7 +88,7 @@ async def webapi_version() -> Response:
|
|||||||
|
|
||||||
@router.get("/api/v2/app/preferences", dependencies=[Depends(_require_sid)])
|
@router.get("/api/v2/app/preferences", dependencies=[Depends(_require_sid)])
|
||||||
async def app_preferences() -> dict:
|
async def app_preferences() -> dict:
|
||||||
return {}
|
return {"save_path": "/downloads"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v2/transfer/info", dependencies=[Depends(_require_sid)])
|
@router.get("/api/v2/transfer/info", dependencies=[Depends(_require_sid)])
|
||||||
@@ -180,9 +180,19 @@ async def torrents_properties(hash: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_categories: dict[str, dict] = {}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v2/torrents/categories", dependencies=[Depends(_require_sid)])
|
@router.get("/api/v2/torrents/categories", dependencies=[Depends(_require_sid)])
|
||||||
async def torrents_categories() -> dict:
|
async def torrents_categories() -> dict:
|
||||||
return {}
|
return _categories
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v2/torrents/createCategory", dependencies=[Depends(_require_sid)])
|
||||||
|
async def torrents_create_category(category: str = Form(""), savePath: str = Form("")) -> Response:
|
||||||
|
if category:
|
||||||
|
_categories[category] = {"name": category, "savePath": savePath}
|
||||||
|
return _plain("Ok.")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v2/torrents/tags", dependencies=[Depends(_require_sid)])
|
@router.get("/api/v2/torrents/tags", dependencies=[Depends(_require_sid)])
|
||||||
|
|||||||
@@ -58,10 +58,9 @@ async def torznab_api(
|
|||||||
|
|
||||||
if t in ("tvsearch", "search"):
|
if t in ("tvsearch", "search"):
|
||||||
if not q or len(q) < 2:
|
if not q or len(q) < 2:
|
||||||
|
releases = await torznab.latest_releases()
|
||||||
return Response(
|
return Response(
|
||||||
torznab.error_xml(200, "Paramètre q requis"),
|
torznab.results_xml(base, key, releases), media_type="application/rss+xml"
|
||||||
media_type="application/xml",
|
|
||||||
status_code=400,
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
releases = await torznab.tvsearch(q, season=season, ep=ep)
|
releases = await torznab.tvsearch(q, season=season, ep=ep)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class SonarrService:
|
|||||||
base_url=url,
|
base_url=url,
|
||||||
timeout=_REQUEST_TIMEOUT,
|
timeout=_REQUEST_TIMEOUT,
|
||||||
headers={"X-Api-Key": apikey},
|
headers={"X-Api-Key": apikey},
|
||||||
|
verify=False, # reverse proxy swizzin : certificat auto-signé (LAN uniquement)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _get(self, path: str, params: dict | None = None) -> dict | list | None:
|
async def _get(self, path: str, params: dict | None = None) -> dict | list | None:
|
||||||
|
|||||||
@@ -169,6 +169,31 @@ class TorznabService:
|
|||||||
"""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)
|
||||||
|
|
||||||
|
async def latest_releases(self) -> list[Release]:
|
||||||
|
"""Flux RSS (Sonarr) : dernier épisode de chaque nouveauté du catalogue."""
|
||||||
|
releases: list[Release] = []
|
||||||
|
for source in await self._enabled_sources():
|
||||||
|
try:
|
||||||
|
latest = await asyncio.wait_for(source.latest(), timeout=_SEARCH_TIMEOUT)
|
||||||
|
except (ScrapeError, TimeoutError) as exc:
|
||||||
|
logger.warning("Torznab : nouveautés %s KO : %s", source.name, exc)
|
||||||
|
continue
|
||||||
|
for result in latest[:_MAX_SERIES_PER_SOURCE]:
|
||||||
|
try:
|
||||||
|
episodes = await self._episodes_of(source, result.source_id)
|
||||||
|
except (ScrapeError, TimeoutError):
|
||||||
|
continue
|
||||||
|
whole = [e for e in episodes if e.number == int(e.number)]
|
||||||
|
if not whole:
|
||||||
|
continue
|
||||||
|
newest = max(whole, key=lambda e: (e.season, e.number))
|
||||||
|
releases.extend(
|
||||||
|
self._releases_for(result.title, source, result.source_id, [newest], None, None)
|
||||||
|
)
|
||||||
|
if len(releases) >= _MAX_RELEASES:
|
||||||
|
return releases[:_MAX_RELEASES]
|
||||||
|
return releases
|
||||||
|
|
||||||
async def _search_source(self, source: SourceScraper, q: str):
|
async def _search_source(self, source: SourceScraper, q: str):
|
||||||
try:
|
try:
|
||||||
return await asyncio.wait_for(source.search(q), timeout=_SEARCH_TIMEOUT)
|
return await asyncio.wait_for(source.search(q), timeout=_SEARCH_TIMEOUT)
|
||||||
|
|||||||
Reference in New Issue
Block a user