695 lines
24 KiB
Python
695 lines
24 KiB
Python
"""streamrip library wrapper (Deezer only for now)."""
|
|
|
|
from pathlib import Path
|
|
|
|
from app.core import settings
|
|
|
|
|
|
def _load_config():
|
|
"""Load streamrip Config with DB paths rewritten to the config dir.
|
|
|
|
The mounted config.toml may contain host-absolute DB paths
|
|
(e.g. /home/lokman/...) that don't exist inside the container.
|
|
Rewriting them to the config dir keeps history working everywhere.
|
|
"""
|
|
from streamrip.config import Config
|
|
|
|
config = Config(settings.CONFIG_PATH)
|
|
cfg_dir = Path(settings.CONFIG_PATH).parent
|
|
try:
|
|
config.session.database.downloads_path = str(cfg_dir / "downloads.db")
|
|
config.session.database.failed_downloads_path = str(
|
|
cfg_dir / "failed_downloads.db"
|
|
)
|
|
except Exception:
|
|
pass
|
|
return config
|
|
|
|
|
|
def _extract_common(item: dict) -> dict:
|
|
cover = (
|
|
item.get("cover_big")
|
|
or item.get("cover_medium")
|
|
or item.get("cover")
|
|
or (item.get("album") or {}).get("cover_big")
|
|
or (item.get("album") or {}).get("cover_medium")
|
|
or item.get("picture_big")
|
|
or item.get("picture_medium")
|
|
)
|
|
preview = item.get("preview")
|
|
duration = item.get("duration")
|
|
explicit = item.get("explicit_lyrics")
|
|
return {"cover": cover, "preview_url": preview, "duration": duration, "explicit": explicit}
|
|
|
|
|
|
def _summarize(media_type: str, item: dict) -> dict:
|
|
from streamrip.metadata import SearchResults
|
|
|
|
summary = SearchResults.from_pages("deezer", media_type, [{"data": [item]}]).results[0]
|
|
base = {"id": summary.id, "desc": summary.summarize()}
|
|
if media_type == "track":
|
|
base.update({"title": summary.name, "artist": summary.artist})
|
|
elif media_type == "album":
|
|
base.update({"title": summary.name, "artist": summary.artist})
|
|
elif media_type == "artist":
|
|
base.update({"title": summary.name, "artist": summary.name})
|
|
elif media_type == "playlist":
|
|
base.update({"title": summary.name, "artist": summary.creator})
|
|
base.update(_extract_common(item))
|
|
base.update({"source": "deezer", "media_type": media_type})
|
|
return base
|
|
|
|
|
|
async def search_deezer(media_type: str, query: str, limit: int = 20) -> list[dict]:
|
|
"""Live search via streamrip DeezerClient. Returns UI-ready dicts."""
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
pages = await client.search(media_type, query, limit=limit)
|
|
finally:
|
|
await main.__aexit__()
|
|
if not pages:
|
|
return []
|
|
items = pages[0].get("data", [])[:limit]
|
|
return [_summarize(media_type, item) for item in items]
|
|
|
|
|
|
def _group_artist_albums(items: list[dict]) -> list[dict]:
|
|
"""Map raw Deezer album items to UI dicts with group album|ep|single."""
|
|
out = []
|
|
for item in items:
|
|
record_type = str(item.get("record_type") or "").lower()
|
|
if record_type == "ep":
|
|
group = "ep"
|
|
elif record_type == "single":
|
|
group = "single"
|
|
else:
|
|
# Deezer marks LPs as "album"; unknown/missing falls back to album
|
|
# only when tracks suggest a full release, else single
|
|
nb = item.get("nb_tracks") or 0
|
|
try:
|
|
nb = int(nb)
|
|
except (TypeError, ValueError):
|
|
nb = 0
|
|
group = "album" if (record_type == "album" or nb > 6) else "single"
|
|
cover = (
|
|
item.get("cover_big")
|
|
or item.get("cover_medium")
|
|
or item.get("cover")
|
|
)
|
|
out.append(
|
|
{
|
|
"id": str(item.get("id")),
|
|
"title": item.get("title") or "Unknown",
|
|
"artist": (item.get("artist") or {}).get("name", "")
|
|
if isinstance(item.get("artist"), dict)
|
|
else str(item.get("artist") or ""),
|
|
"cover": cover,
|
|
"release_date": item.get("release_date") or item.get("record_release_date") or "",
|
|
"nb_tracks": nb,
|
|
"group": group,
|
|
"source": "deezer",
|
|
"media_type": "album",
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
async def get_artist_details(artist_id: str) -> dict:
|
|
"""Artist info + grouped albums + featuring tracks (track-search approx)."""
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
meta = await client.get_metadata(artist_id, "artist")
|
|
finally:
|
|
await main.__aexit__()
|
|
artist = meta if isinstance(meta, dict) else {}
|
|
# get_metadata("artist") returns artist dict with "albums" attached
|
|
albums_raw = artist.get("albums") or []
|
|
if isinstance(albums_raw, dict):
|
|
albums_raw = albums_raw.get("data", [])
|
|
name = artist.get("name") or "Unknown"
|
|
cover = artist.get("picture_big") or artist.get("picture_medium")
|
|
|
|
featuring = await _find_featuring(name)
|
|
|
|
return {
|
|
"id": str(artist_id),
|
|
"name": name,
|
|
"cover": cover,
|
|
"nb_albums": artist.get("nb_album"),
|
|
"albums": _group_artist_albums(albums_raw),
|
|
"featuring": featuring,
|
|
}
|
|
|
|
|
|
def _is_featuring(name: str, item: dict) -> bool:
|
|
"""True if track item is a guest appearance of `name` (not main artist)."""
|
|
lname = name.lower()
|
|
main_artist = item.get("artist")
|
|
aname = (
|
|
main_artist.get("name", "")
|
|
if isinstance(main_artist, dict)
|
|
else str(main_artist or "")
|
|
)
|
|
return aname.lower() != lname
|
|
|
|
|
|
async def _find_featuring(name: str, limit: int = 20) -> list[dict]:
|
|
"""Track-search approximation of guest appearances (labeled as such in UI)."""
|
|
from streamrip.rip.main import Main
|
|
|
|
queries = [name, f"feat. {name}"]
|
|
featuring: list[dict] = []
|
|
seen: set[str] = set()
|
|
try:
|
|
for q in queries:
|
|
if len(featuring) >= limit:
|
|
break
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
pages = await client.search("track", q, limit=50)
|
|
finally:
|
|
await main.__aexit__()
|
|
if not pages:
|
|
continue
|
|
for item in pages[0].get("data", []):
|
|
tid = str(item.get("id"))
|
|
if tid in seen:
|
|
continue
|
|
seen.add(tid)
|
|
if _is_featuring(name, item):
|
|
featuring.append(_summarize("track", item))
|
|
if len(featuring) >= limit:
|
|
break
|
|
except Exception:
|
|
pass
|
|
return featuring
|
|
|
|
|
|
def _map_track_items(items: list[dict]) -> list[dict]:
|
|
"""Map raw Deezer album-track items to UI dicts (track order preserved)."""
|
|
out = []
|
|
for pos, item in enumerate(items, start=1):
|
|
artist = item.get("artist")
|
|
aname = (
|
|
artist.get("name", "")
|
|
if isinstance(artist, dict)
|
|
else str(artist or "")
|
|
)
|
|
album = item.get("album") or {}
|
|
cover = (
|
|
item.get("cover_big")
|
|
or item.get("cover_medium")
|
|
or album.get("cover_big")
|
|
or album.get("cover_medium")
|
|
)
|
|
try:
|
|
track_number = int(item.get("track_position") or pos)
|
|
except (TypeError, ValueError):
|
|
track_number = pos
|
|
out.append(
|
|
{
|
|
"id": str(item.get("id")),
|
|
"title": item.get("title") or "Unknown",
|
|
"artist": aname,
|
|
"track_number": track_number,
|
|
"disk_number": item.get("disk_number") or 1,
|
|
"duration": item.get("duration"),
|
|
"preview_url": item.get("preview"),
|
|
"explicit": item.get("explicit_lyrics"),
|
|
"cover": cover,
|
|
"source": "deezer",
|
|
"media_type": "track",
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
async def _fetch_with_tracks(
|
|
client, media_type: str, item_id: str, methods: tuple[str, str]
|
|
) -> dict:
|
|
"""Fetch item metadata; fall back to embedded tracks if the
|
|
dedicated tracks endpoint has no data (some uploads lack a listing)."""
|
|
import asyncio
|
|
|
|
get_one, get_tracks = methods
|
|
api = client.client.api
|
|
meta = await asyncio.to_thread(getattr(api, get_one), item_id)
|
|
try:
|
|
tracks_resp = await asyncio.to_thread(getattr(api, get_tracks), item_id)
|
|
meta["tracks"] = tracks_resp.get("data", [])
|
|
except Exception:
|
|
embedded = meta.get("tracks")
|
|
if isinstance(embedded, dict):
|
|
meta["tracks"] = embedded.get("data", [])
|
|
elif not isinstance(embedded, list):
|
|
meta["tracks"] = []
|
|
meta["track_total"] = len(meta.get("tracks") or [])
|
|
return meta
|
|
|
|
|
|
async def _fetch_album_meta(client, album_id: str) -> dict:
|
|
return await _fetch_with_tracks(client, "album", album_id, ("get_album", "get_album_tracks"))
|
|
|
|
|
|
async def get_album_details(album_id: str) -> dict:
|
|
"""Album info + track list via streamrip DeezerClient."""
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
meta = await _fetch_album_meta(client, album_id)
|
|
finally:
|
|
await main.__aexit__()
|
|
if not isinstance(meta, dict):
|
|
raise RuntimeError("Empty album metadata")
|
|
artist = meta.get("artist")
|
|
aname = (
|
|
artist.get("name", "")
|
|
if isinstance(artist, dict)
|
|
else str(artist or "")
|
|
)
|
|
tracks_raw = meta.get("tracks") or []
|
|
if isinstance(tracks_raw, dict):
|
|
tracks_raw = tracks_raw.get("data", [])
|
|
return {
|
|
"id": str(meta.get("id") or album_id),
|
|
"title": meta.get("title") or "Unknown",
|
|
"artist": aname,
|
|
"cover": meta.get("cover_big") or meta.get("cover_medium"),
|
|
"release_date": meta.get("release_date") or "",
|
|
"nb_tracks": meta.get("nb_tracks") or len(tracks_raw),
|
|
"tracks": _map_track_items(tracks_raw),
|
|
}
|
|
|
|
|
|
async def get_playlist_details(playlist_id: str) -> dict:
|
|
"""Playlist info + track list via streamrip DeezerClient."""
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
meta = await _fetch_with_tracks(
|
|
client, "playlist", playlist_id, ("get_playlist", "get_playlist_tracks")
|
|
)
|
|
finally:
|
|
await main.__aexit__()
|
|
if not isinstance(meta, dict):
|
|
raise RuntimeError("Empty playlist metadata")
|
|
creator = meta.get("creator") or meta.get("owner") or {}
|
|
cname = (
|
|
creator.get("name", "")
|
|
if isinstance(creator, dict)
|
|
else str(creator or "")
|
|
)
|
|
tracks_raw = meta.get("tracks") or []
|
|
if isinstance(tracks_raw, dict):
|
|
tracks_raw = tracks_raw.get("data", [])
|
|
return {
|
|
"id": str(meta.get("id") or playlist_id),
|
|
"title": meta.get("title") or meta.get("name") or "Unknown",
|
|
"creator": cname,
|
|
"description": meta.get("description") or "",
|
|
"cover": meta.get("picture_big") or meta.get("picture_medium"),
|
|
"nb_tracks": meta.get("nb_tracks") or len(tracks_raw),
|
|
"tracks": _map_track_items(tracks_raw),
|
|
}
|
|
|
|
|
|
async def download_by_url(
|
|
url: str,
|
|
quality: int | None = None,
|
|
codec: str | None = None,
|
|
folder: str | None = None,
|
|
no_db: bool = False,
|
|
on_log=None,
|
|
) -> None:
|
|
"""Resolve + download one item from a Deezer URL. Raises on failure."""
|
|
from streamrip.rip.main import Main
|
|
from streamrip.rip.parse_url import parse_url
|
|
|
|
def log(line: str):
|
|
if on_log:
|
|
on_log(line)
|
|
|
|
if parse_url(url) is None:
|
|
raise ValueError(f"Unable to parse url {url}")
|
|
config = _load_config()
|
|
effective_folder = _apply_job_overrides(config, quality, codec, folder)
|
|
log(f"Target folder: {effective_folder}")
|
|
main = Main(config)
|
|
try:
|
|
if no_db:
|
|
try:
|
|
from streamrip import db as dbmod
|
|
|
|
main.database = dbmod.Database(dbmod.Dummy(), main.database.failed)
|
|
except Exception:
|
|
pass
|
|
log(f"Resolving {url} ...")
|
|
await main.add(url)
|
|
await main.resolve()
|
|
if not main.media:
|
|
raise RuntimeError("Nothing resolved (already downloaded or invalid url)")
|
|
log(f"Downloading {len(main.media)} item(s) ...")
|
|
await main.rip()
|
|
log("Download finished.")
|
|
finally:
|
|
await main.__aexit__()
|
|
|
|
|
|
async def get_related_artists(artist_id: str, limit: int = 12) -> list[dict]:
|
|
"""Similar artists via Deezer related endpoint."""
|
|
import asyncio
|
|
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
api = client.client.api
|
|
resp = await asyncio.to_thread(api.get_artist_related, artist_id, 0, limit)
|
|
finally:
|
|
await main.__aexit__()
|
|
items = resp.get("data", []) if isinstance(resp, dict) else []
|
|
return [_summarize("artist", item) for item in items[:limit]]
|
|
|
|
|
|
async def get_genres() -> list[dict]:
|
|
"""Deezer genre catalog."""
|
|
import asyncio
|
|
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
resp = await asyncio.to_thread(client.client.api.get_genres, 0, 100)
|
|
finally:
|
|
await main.__aexit__()
|
|
items = resp.get("data", []) if isinstance(resp, dict) else []
|
|
return [
|
|
{
|
|
"id": item.get("id"),
|
|
"name": item.get("name") or "Unknown",
|
|
"cover": item.get("picture_big") or item.get("picture_medium"),
|
|
}
|
|
for item in items
|
|
]
|
|
|
|
|
|
async def _playlist_top_tracks(api, genre_id: int, limit: int) -> list[dict]:
|
|
"""Raw track items from the top chart playlists (fallback source)."""
|
|
import asyncio
|
|
|
|
resp = await asyncio.to_thread(api.get_chart_playlists, genre_id, 0, 5)
|
|
playlists = resp.get("data", []) if isinstance(resp, dict) else []
|
|
tracks: list[dict] = []
|
|
seen: set[str] = set()
|
|
for pl in playlists[:3]:
|
|
try:
|
|
tresp = await asyncio.to_thread(api.get_playlist_tracks, pl.get("id"), 0, limit)
|
|
except Exception:
|
|
continue
|
|
for item in tresp.get("data", []) if isinstance(tresp, dict) else []:
|
|
tid = str(item.get("id"))
|
|
if tid in seen:
|
|
continue
|
|
seen.add(tid)
|
|
tracks.append(item)
|
|
if len(tracks) >= limit:
|
|
return tracks
|
|
return tracks
|
|
|
|
|
|
def _artists_from_track_items(items: list[dict]) -> list[dict]:
|
|
"""Unique artist stubs from raw track items (for _summarize)."""
|
|
artists: list[dict] = []
|
|
seen: set[str] = set()
|
|
for item in items:
|
|
artist = item.get("artist")
|
|
if isinstance(artist, dict):
|
|
aid, aname = str(artist.get("id")), artist.get("name") or "Unknown"
|
|
else:
|
|
aid, aname = str(artist or ""), str(artist or "Unknown")
|
|
if not aid or aid in seen:
|
|
continue
|
|
seen.add(aid)
|
|
artists.append({"id": aid, "name": aname})
|
|
return artists
|
|
|
|
|
|
async def _discover_albums_fallback(api, genre_id: int, limit: int, primary: list[dict]) -> list[dict]:
|
|
"""Editorial selection first, chart albums (newest first) as fallback."""
|
|
if primary:
|
|
return primary
|
|
import asyncio
|
|
|
|
resp = await asyncio.to_thread(api.get_chart_albums, genre_id, 0, limit)
|
|
items = resp.get("data", []) if isinstance(resp, dict) else []
|
|
items = sorted(items, key=lambda i: i.get("release_date") or "", reverse=True)
|
|
return items
|
|
|
|
|
|
async def get_discover(kind: str, genre_id: int = 0, limit: int = 20) -> list[dict]:
|
|
"""Charts per kind with fallbacks (several Deezer endpoints are empty)."""
|
|
import asyncio
|
|
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
api = client.client.api
|
|
if kind == "tracks":
|
|
resp = await asyncio.to_thread(api.get_chart_tracks, genre_id, 0, limit)
|
|
items = resp.get("data", [])
|
|
if not items:
|
|
items = await _playlist_top_tracks(api, genre_id, limit)
|
|
return [_summarize("track", i) for i in items[:limit]]
|
|
if kind == "albums":
|
|
resp = await asyncio.to_thread(api.get_chart_albums, genre_id, 0, limit)
|
|
items = resp.get("data", [])
|
|
return [_summarize("album", i) for i in items[:limit]]
|
|
if kind == "artists":
|
|
resp = await asyncio.to_thread(api.get_chart_artists, genre_id, 0, limit)
|
|
items = resp.get("data", [])
|
|
if not items:
|
|
raw = await _playlist_top_tracks(api, genre_id, limit * 2)
|
|
items = _artists_from_track_items(raw)[:limit]
|
|
return [_summarize("artist", i) for i in items[:limit]]
|
|
if kind == "playlists":
|
|
resp = await asyncio.to_thread(api.get_chart_playlists, genre_id, 0, limit)
|
|
items = resp.get("data", [])
|
|
return [_summarize("playlist", i) for i in items[:limit]]
|
|
# kind == "new": editorial selection, chart albums (newest first) as fallback
|
|
resp = await asyncio.to_thread(api.get_editorial_selection, genre_id, 0, limit)
|
|
items = resp.get("data", []) if isinstance(resp, dict) else []
|
|
items = await _discover_albums_fallback(api, genre_id, limit, items)
|
|
return [_summarize("album", i) for i in items[:limit]]
|
|
finally:
|
|
await main.__aexit__()
|
|
|
|
|
|
async def get_genre_radios(genre_id: int) -> list[dict]:
|
|
"""Radio stations for a genre (0 = all)."""
|
|
import asyncio
|
|
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
if genre_id:
|
|
resp = await asyncio.to_thread(client.client.api.get_genre_radios, genre_id, 0, 25)
|
|
else:
|
|
resp = await asyncio.to_thread(client.client.api.get_radios_top, 50)
|
|
finally:
|
|
await main.__aexit__()
|
|
items = resp.get("data", []) if isinstance(resp, dict) else []
|
|
return [
|
|
{
|
|
"id": item.get("id"),
|
|
"title": item.get("title") or "Unknown",
|
|
"cover": item.get("picture_big") or item.get("picture_medium"),
|
|
}
|
|
for item in items
|
|
]
|
|
|
|
|
|
async def get_radio_tracks(radio_id: int, limit: int = 25) -> list[dict]:
|
|
"""Top tracks of a radio station (loadable via batch download)."""
|
|
import asyncio
|
|
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
resp = await asyncio.to_thread(client.client.api.get_radio_tracks, radio_id, limit)
|
|
finally:
|
|
await main.__aexit__()
|
|
items = resp.get("data", []) if isinstance(resp, dict) else []
|
|
return [_summarize("track", i) for i in items[:limit]]
|
|
|
|
|
|
def _apply_job_overrides(config, quality: int | None, codec: str | None, folder: str | None) -> str:
|
|
"""Apply per-job overrides onto a loaded Config.
|
|
|
|
None/empty means 'server default from config.toml'. Returns the
|
|
effective download folder. Single worker -> mutation is safe.
|
|
"""
|
|
try:
|
|
if folder:
|
|
config.session.downloads.folder = folder
|
|
effective_folder = config.session.downloads.folder or settings.DOWNLOAD_DIR
|
|
except Exception:
|
|
effective_folder = folder or settings.DOWNLOAD_DIR
|
|
try:
|
|
if quality is not None:
|
|
config.session.deezer.quality = quality
|
|
except Exception:
|
|
pass
|
|
try:
|
|
if codec:
|
|
config.session.conversion.enabled = True
|
|
config.session.conversion.codec = codec.upper()
|
|
except Exception:
|
|
pass
|
|
return effective_folder
|
|
|
|
|
|
async def search_podcasts(query: str, limit: int = 20) -> list[dict]:
|
|
"""Search Deezer shows/podcasts (listening only, no download support)."""
|
|
import asyncio
|
|
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
resp = await asyncio.to_thread(
|
|
client.client.api.api_call, "search/podcast", {"q": query, "limit": limit}
|
|
)
|
|
finally:
|
|
await main.__aexit__()
|
|
items = resp.get("data", []) if isinstance(resp, dict) else []
|
|
out = []
|
|
for item in items[:limit]:
|
|
out.append(
|
|
{
|
|
"source": "deezer",
|
|
"media_type": "podcast",
|
|
"id": str(item.get("id")),
|
|
"title": item.get("title") or "Unknown",
|
|
"artist": "",
|
|
"desc": (item.get("description") or "")[:120],
|
|
"cover": item.get("picture_big") or item.get("picture_medium"),
|
|
"fans": item.get("fans"),
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
async def get_podcast_details(show_id: str) -> dict:
|
|
"""Show info + episode list (no download support in streamrip)."""
|
|
import asyncio
|
|
|
|
from streamrip.rip.main import Main
|
|
|
|
config = _load_config()
|
|
main = Main(config)
|
|
try:
|
|
client = await main.get_logged_in_client("deezer")
|
|
api = client.client.api
|
|
show, eps = await asyncio.gather(
|
|
asyncio.to_thread(api.api_call, f"podcast/{show_id}", {}),
|
|
asyncio.to_thread(api.api_call, f"podcast/{show_id}/episodes", {"limit": 100}),
|
|
)
|
|
finally:
|
|
await main.__aexit__()
|
|
if not isinstance(show, dict) or show.get("error"):
|
|
raise RuntimeError("Show not found")
|
|
episodes = []
|
|
raw = eps.get("data", []) if isinstance(eps, dict) else []
|
|
for ep in raw:
|
|
episodes.append(
|
|
{
|
|
"id": str(ep.get("id")),
|
|
"title": ep.get("title") or "Unknown",
|
|
"duration": ep.get("duration"),
|
|
"release_date": ep.get("release_date") or "",
|
|
"cover": ep.get("picture_big") or ep.get("picture_medium"),
|
|
}
|
|
)
|
|
return {
|
|
"id": str(show.get("id") or show_id),
|
|
"title": show.get("title") or "Unknown",
|
|
"description": show.get("description") or "",
|
|
"cover": show.get("picture_big") or show.get("picture_medium"),
|
|
"fans": show.get("fans"),
|
|
"link": show.get("link") or f"https://www.deezer.com/show/{show_id}",
|
|
"episodes": episodes,
|
|
}
|
|
|
|
|
|
async def download_by_id(
|
|
source: str,
|
|
media_type: str,
|
|
item_id: str,
|
|
quality: int | None = None,
|
|
codec: str | None = None,
|
|
folder: str | None = None,
|
|
no_db: bool = False,
|
|
on_log=None,
|
|
) -> None:
|
|
"""Resolve + download one item. Raises on failure."""
|
|
from streamrip.rip.main import Main
|
|
|
|
def log(line: str):
|
|
if on_log:
|
|
on_log(line)
|
|
|
|
config = _load_config()
|
|
effective_folder = _apply_job_overrides(config, quality, codec, folder)
|
|
log(f"Target folder: {effective_folder}")
|
|
|
|
main = Main(config)
|
|
try:
|
|
if no_db:
|
|
try:
|
|
from streamrip import db as dbmod
|
|
|
|
main.database = dbmod.Database(dbmod.Dummy(), main.database.failed)
|
|
except Exception:
|
|
pass
|
|
log(f"Resolving {source} {media_type} {item_id} ...")
|
|
await main.add_by_id(source, media_type, item_id)
|
|
await main.resolve()
|
|
if not main.media:
|
|
raise RuntimeError("Nothing resolved (already downloaded or invalid id)")
|
|
log(f"Downloading {len(main.media)} item(s) ...")
|
|
await main.rip()
|
|
log("Download finished.")
|
|
finally:
|
|
await main.__aexit__()
|