Skip to content

Commit 8fec6ed

Browse files
📝 Add docstrings to copilot/add-s-to-catalogue-support
Docstrings generation was requested by @Zzackllack. * #8 (comment) The following files were modified: * `app/api/torznab/api.py` * `app/api/torznab/utils.py` * `app/core/downloader.py` * `app/core/scheduler.py` * `app/db/models.py` * `app/utils/magnet.py` * `app/utils/probe_quality.py` * `app/utils/title_resolver.py` * `tests/test_qbittorrent_torrents.py` * `tests/test_torznab_utils.py`
1 parent be6fde8 commit 8fec6ed

File tree

10 files changed

+181
-82
lines changed

10 files changed

+181
-82
lines changed

app/api/torznab/api.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@
2828

2929
def _default_languages_for_site(site: str) -> List[str]:
3030
"""
31-
Return the default language preference ordering for a catalogue site.
32-
Falls back to AniWorld defaults when a site-specific mapping is not found.
31+
Get the default language preference ordering for a catalogue site.
32+
33+
Parameters:
34+
site (str): Catalogue site key to look up in CATALOG_SITE_CONFIGS.
35+
36+
Returns:
37+
List[str]: Language names in preference order. If the site has no valid mapping, returns the configured aniworld.to defaults (fallbacking to ["German Dub", "German Sub", "English Sub"] if that configuration is absent).
3338
"""
3439
cfg = CATALOG_SITE_CONFIGS.get(site)
3540
if cfg:
@@ -408,4 +413,4 @@ def torznab_api(
408413

409414
xml = ET.tostring(rss, encoding="utf-8", xml_declaration=True).decode("utf-8")
410415
logger.info(f"Returning RSS feed with {count} items.")
411-
return Response(content=xml, media_type="application/rss+xml; charset=utf-8")
416+
return Response(content=xml, media_type="application/rss+xml; charset=utf-8")

app/api/torznab/utils.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ def _normalize_tokens(s: str) -> List[str]:
8484
def _slug_from_query(q: str, site: Optional[str] = None) -> Optional[Tuple[str, str]]:
8585
"""
8686
Resolve a free-text query to the best-matching site and canonical slug.
87-
87+
8888
Parameters:
89-
q (str): The free-text query to resolve (e.g., a title).
90-
site (Optional[str]): If provided, restrict resolution to the specified site identifier.
91-
89+
q (str): The free-text title or query to resolve.
90+
site (Optional[str]): Optional site identifier to restrict resolution to a specific site.
91+
9292
Returns:
93-
Optional[Tuple[str, str]]: A tuple (site, slug) with the site identifier and resolved slug when a match is found, `None` if no match exists.
93+
Optional[Tuple[str, str]]: `(site, slug)` with the site identifier and resolved canonical slug when a match is found, `None` otherwise.
9494
"""
9595
logger.debug(f"Resolving slug from query: '{q}', site filter: {site}")
9696
from app.utils.title_resolver import slug_from_query # type: ignore
@@ -201,4 +201,4 @@ def _build_item(
201201

202202
_add_torznab_attr(item, "seeders", str(seeders))
203203
_add_torznab_attr(item, "peers", str(peers))
204-
_add_torznab_attr(item, "leechers", str(leechers))
204+
_add_torznab_attr(item, "leechers", str(leechers))

app/core/downloader.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,20 @@ def build_episode(
9595
site: str = "aniworld.to",
9696
) -> Episode:
9797
"""
98-
Construct an Episode from a direct link or from slug, season, and episode with the given site.
99-
98+
Construct an Episode from either a direct link or a slug/season/episode triple for the specified site.
99+
100100
Parameters:
101-
link (Optional[str]): Direct episode URL. If provided, it is used and `slug/season/episode` are ignored.
102-
slug (Optional[str]): Series identifier used with `season` and `episode` to build the Episode when `link` is not provided.
103-
season (Optional[int]): Season number used with `slug` and `episode`.
104-
episode (Optional[int]): Episode number used with `slug` and `season`.
105-
site (str): Host site identifier included in the created Episode (default: "aniworld.to").
106-
101+
link (Optional[str]): Direct episode URL; used when provided and takes precedence over slug/season/episode.
102+
slug (Optional[str]): Series identifier used with `season` and `episode` when `link` is not provided.
103+
season (Optional[int]): Season number paired with `slug` and `episode`.
104+
episode (Optional[int]): Episode number paired with `slug` and `season`.
105+
site (str): Host site identifier to attach to the created Episode (default: "aniworld.to").
106+
107107
Returns:
108-
Episode: An Episode constructed from `link` (if present) or from `slug`, `season`, and `episode`.
109-
108+
Episode: The constructed Episode using the provided inputs.
109+
110110
Raises:
111-
ValueError: If neither `link` nor the combination of `slug`, `season`, and `episode` are provided.
111+
ValueError: If neither `link` nor the combination of `slug`, `season`, and `episode` are supplied.
112112
"""
113113
logger.info(
114114
f"Building episode: link={link}, slug={slug}, season={season}, episode={episode}, site={site}"
@@ -179,6 +179,21 @@ def get_direct_url_with_fallback(
179179
language: str,
180180
) -> Tuple[str, str]:
181181

182+
"""
183+
Resolve a direct download URL for an episode, trying a preferred provider first and falling back to the configured provider order.
184+
185+
Parameters:
186+
ep (Episode): Episode object to resolve the direct link for.
187+
preferred (Optional[str]): Provider name to try first; ignored if empty or None.
188+
language (str): Desired language label; will be normalized before use.
189+
190+
Returns:
191+
tuple: (direct_url, provider_name) where `direct_url` is the resolved URL and `provider_name` is the provider that supplied it.
192+
193+
Raises:
194+
LanguageUnavailableError: If the requested language is not offered by the episode or a provider indicates the language is unavailable.
195+
DownloadError: If no provider yields a direct URL after all fallbacks.
196+
"""
182197
language = _normalize_language(language)
183198
logger.info(
184199
f"Getting direct URL with fallback. Preferred: {preferred}, Language: {language}"
@@ -251,6 +266,23 @@ def _ydl_download(
251266
stop_event: Optional[threading.Event] = None,
252267
force_no_proxy: bool = False,
253268
) -> Tuple[Path, Dict[str, Any]]:
269+
"""
270+
Download a media resource via yt-dlp and return the downloaded file path and metadata.
271+
272+
Uses yt-dlp to download the resource at `direct_url` into `dest_dir`, applying an optional filename hint, cookiefile, proxy configuration, progress callbacks, and cancellation via `stop_event`.
273+
274+
Parameters:
275+
direct_url (str): Direct media URL or playlist identifier to pass to yt-dlp.
276+
dest_dir (Path): Directory where the download and temporary files will be stored; created if missing.
277+
title_hint (Optional[str]): Hint for the output filename; sanitized and used in the output template if provided.
278+
cookiefile (Optional[Path]): Path to a cookies file to supply to yt-dlp for authenticated requests.
279+
progress_cb (Optional[callable]): Callback invoked with yt-dlp progress dictionaries as they arrive.
280+
stop_event (Optional[threading.Event]): If set during download, the operation will be cancelled and raise DownloadError("Cancelled").
281+
force_no_proxy (bool): When true, disable any configured proxy for this yt-dlp invocation.
282+
283+
Returns:
284+
Tuple[Path, Dict[str, Any]]: A tuple containing the final downloaded file path and the yt-dlp info dictionary.
285+
"""
254286
logger.info(
255287
f"Starting yt-dlp download: url={direct_url}, dest_dir={dest_dir}, title_hint={title_hint}"
256288
)
@@ -512,4 +544,4 @@ def download_episode(
512544
site=site,
513545
)
514546
logger.success(f"Final file path: {final_path}")
515-
return final_path
547+
return final_path

app/core/scheduler.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,22 +114,24 @@ def _cb(d: dict):
114114

115115
def _run_download(job_id: str, req: dict, stop_event: threading.Event):
116116
"""
117-
Execute a download job: start the episode download, update the job record in the database with progress/result, and handle errors or cancellation.
118-
117+
Run a download task and record its progress and final state in the database.
118+
119+
Executes the episode download described by `req`, updates the job row with lifecycle states
120+
(e.g., "downloading", "completed", "failed", "cancelled"), writes final `result_path` on success,
121+
and removes the job from the in-memory RUNNING registry when finished. If the download is cancelled
122+
or an exception occurs, the job status and message are updated accordingly. An OSError caused by
123+
an unwritable download directory sets the job to "failed" with a directory-specific message.
124+
119125
Parameters:
120126
job_id (str): Identifier of the job being run.
121-
req (dict): Download request containing keys used by the downloader:
122-
- 'slug', 'season', 'episode' (identifiers for the episode)
127+
req (dict): Download request with keys used by the downloader. Recognized keys:
128+
- 'slug', 'season', 'episode' (episode identifiers)
123129
- 'language' (optional)
124130
- 'provider' (optional)
125131
- 'title_hint' (optional)
126132
- 'link' (optional)
127133
- 'site' (optional, defaults to "aniworld.to")
128134
stop_event (threading.Event): Event that, when set, requests cancellation of the download.
129-
130-
Side effects:
131-
- Updates the job row in the database with status, progress, messages, source site, and result path.
132-
- Removes the job from the RUNNING registry when finished.
133135
"""
134136
try:
135137
with Session(engine) as s:
@@ -182,8 +184,24 @@ def _run_download(job_id: str, req: dict, stop_event: threading.Event):
182184

183185
def schedule_download(req: dict) -> str:
184186
"""
185-
req: {slug, season, episode, language, provider?, title_hint?, link?}
186-
returns job_id
187+
Schedule a background download job and return its job identifier.
188+
189+
Parameters:
190+
req (dict): Download request containing:
191+
- slug (str): Content identifier.
192+
- season (int | str): Season number or identifier.
193+
- episode (int | str): Episode number or identifier.
194+
- language (str): Desired audio/subtitle language.
195+
- provider (str, optional): Provider to use.
196+
- title_hint (str, optional): Suggested title for the download destination.
197+
- link (str, optional): Direct link or reference.
198+
- site (str, optional): Source site name; used as the job's source_site (defaults to "aniworld.to").
199+
200+
Returns:
201+
str: The created job's identifier.
202+
203+
Raises:
204+
RuntimeError: If the thread pool executor is unavailable after initialization.
187205
"""
188206
init_executor()
189207
if EXECUTOR is None:
@@ -206,4 +224,4 @@ def cancel_job(job_id: str) -> None:
206224
return
207225
fut, ev = item
208226
ev.set()
209-
fut.cancel()
227+
fut.cancel()

app/db/models.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ def _migrate_episode_availability_table() -> None:
209209

210210

211211
def create_db_and_tables() -> None:
212+
"""
213+
Ensure the application's SQLite database file and ORM tables exist, running any required schema migration first.
214+
215+
Performs any necessary episode availability migration and creates tables defined on the module's private metadata, creating the database file if it does not already exist.
216+
"""
212217
logger.debug("Creating DB and tables if not exist.")
213218
try:
214219
_migrate_episode_availability_table()
@@ -244,6 +249,15 @@ def dispose_engine() -> None:
244249

245250
# --- Jobs CRUD
246251
def create_job(session: Session, *, source_site: Optional[str] = None) -> Job:
252+
"""
253+
Create and persist a new Job record in the database.
254+
255+
Parameters:
256+
source_site (Optional[str]): Optional source site identifier to associate with the job; if omitted the model's default is used.
257+
258+
Returns:
259+
Job: The created Job instance refreshed from the database (includes generated id and timestamps).
260+
"""
247261
logger.debug("Creating new job entry in DB.")
248262
try:
249263
job_kwargs: dict[str, Any] = {}
@@ -394,12 +408,12 @@ def get_availability(
394408
) -> Optional[EpisodeAvailability]:
395409
"""
396410
Retrieve the cached availability record for a specific episode identified by slug, season, episode, language, and site.
397-
411+
398412
Parameters:
399413
site (str): Site identifier to query for (default "aniworld.to").
400-
414+
401415
Returns:
402-
EpisodeAvailability | None: The matching EpisodeAvailability if found, `None` otherwise.
416+
EpisodeAvailability | None: `EpisodeAvailability` if a matching record exists, `None` otherwise.
403417
"""
404418
logger.debug(
405419
f"Fetching availability for {slug} S{season}E{episode} {language} on {site}"
@@ -540,4 +554,4 @@ def delete_client_task(session: Session, hash: str) -> None:
540554
session.commit()
541555
logger.success(f"Deleted client task for hash {hash}")
542556
else:
543-
logger.warning(f"Client task for hash {hash} not found, nothing to delete.")
557+
logger.warning(f"Client task for hash {hash} not found, nothing to delete.")

app/utils/magnet.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111

1212

1313
def _site_prefix(site: str) -> str:
14+
"""
15+
Determine the parameter prefix associated with a site.
16+
17+
Parameters:
18+
site (str): Hostname of the site (e.g., "aniworld.to" or "s.to").
19+
20+
Returns:
21+
str: "aw" for "aniworld.to", "sto" for "s.to", and "aw" for any other site (default). Logs a warning when defaulting.
22+
"""
1423
if site == "aniworld.to":
1524
return "aw"
1625
if site == "s.to":
@@ -20,6 +29,12 @@ def _site_prefix(site: str) -> str:
2029

2130

2231
def _hash_id(slug: str, season: int, episode: int, language: str) -> str:
32+
"""
33+
Compute a deterministic SHA-1 identifier for the specified content.
34+
35+
Returns:
36+
A hexadecimal SHA-1 digest of the string "{slug}|{season}|{episode}|{language}" using UTF-8 encoding.
37+
"""
2338
logger.debug(
2439
f"Hashing ID with slug={slug}, season={season}, episode={episode}, language={language}"
2540
)
@@ -157,4 +172,4 @@ def parse_magnet(magnet: str) -> Dict[str, str]:
157172
raise ValueError(f"missing param: {req}")
158173

159174
logger.success(f"Magnet parsed successfully: {flat}")
160-
return flat
175+
return flat

app/utils/probe_quality.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ def probe_episode_quality_once(
1818
direct_url: str, timeout: float = 6.0
1919
) -> tuple[Optional[int], Optional[str], Dict[str, Any] | None]:
2020
"""
21-
Lädt KEINE Daten. Holt nur Info über Formate/Höhe/Codec.
21+
Retrieve reported video height, video codec, and metadata from a direct media URL without downloading the media.
22+
23+
Parameters:
24+
timeout (float): Socket timeout in seconds used when probing the URL.
25+
26+
Returns:
27+
tuple: A three-item tuple (height, vcodec, info_dict) where `height` is the reported video height in pixels or `None` if unavailable, `vcodec` is the reported video codec string or `None` if unavailable, and `info_dict` is the extracted metadata dictionary from yt-dlp or `None` if extraction failed.
2228
"""
2329
logger.debug(
2430
f"Probing episode quality for URL: {direct_url} with timeout={timeout}"
@@ -115,4 +121,4 @@ def probe_episode_quality(
115121
logger.warning(f"Provider '{prov}' failed: {e}")
116122
continue
117123
logger.error("No provider succeeded for this episode/language.")
118-
return (False, None, None, None, None)
124+
return (False, None, None, None, None)

0 commit comments

Comments
 (0)