mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
feat(dl): add --latest-episodes N to download the N newest episodes
Split episodes count once. Exposed as latest_episodes on the REST API. Closes #159.
This commit is contained in:
@@ -429,6 +429,7 @@ Make a download job. It requires `service` and `title_id`. Every other field is
|
|||||||
| `no_atmos` | boolean | `false` | Exclude Atmos tracks. |
|
| `no_atmos` | boolean | `false` | Exclude Atmos tracks. |
|
||||||
| `wanted` | string / string[] | `[]` | Episode/season selectors, as a list or as one comma-separated string. Accepts the part form, `"S01E01.2"`, and the air-date form, `"2026-08-11"` or `"2026-08-01:2026-08-31"`. For a music release, a selector is a track number, `"1-5"` or `"1,3,7"`, or `"{disc}x{track}"` such as `"2x3"`. |
|
| `wanted` | string / string[] | `[]` | Episode/season selectors, as a list or as one comma-separated string. Accepts the part form, `"S01E01.2"`, and the air-date form, `"2026-08-11"` or `"2026-08-01:2026-08-31"`. For a music release, a selector is a track number, `"1-5"` or `"1,3,7"`, or `"{disc}x{track}"` such as `"2x3"`. |
|
||||||
| `latest_episode` | boolean | `false` | Only the newest episode. |
|
| `latest_episode` | boolean | `false` | Only the newest episode. |
|
||||||
|
| `latest_episodes` | integer | `null` | Only the N newest episodes. A positive integer. When set, it overrides `latest_episode`. |
|
||||||
| `lang` / `v_lang` / `a_lang` / `s_lang` | string[] | `["orig"]` / `[]` / `[]` / `["all"]` | Language filters. |
|
| `lang` / `v_lang` / `a_lang` / `s_lang` | string[] | `["orig"]` / `[]` / `[]` / `["all"]` | Language filters. |
|
||||||
| `require_audio` | string[] | `[]` | Audio languages that must exist. The job fails if one is missing, even with `best_available`. |
|
| `require_audio` | string[] | `[]` | Audio languages that must exist. The job fails if one is missing, even with `best_available`. |
|
||||||
| `require_video` | string[] | `[]` | Video languages that must exist. The job fails if one is missing, even with `best_available`. |
|
| `require_video` | string[] | `[]` | Video languages that must exist. The job fails if one is missing, even with `best_available`. |
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ unshackle honours the special language tokens `orig`, `all`, and `best` everywhe
|
|||||||
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). For a [split episode](downloading.md#split-episodes), `.N` picks one part (`S01E01.2`), a range must stay inside the episode (`S01E01.1-S01E01.3`), and `S01E01` on its own takes every part. For a [dated episode](downloading.md#daily-and-date-based-content), a token can also be an ISO air date (`2026-08-11`) or a date range with a colon (`2026-08-01:2026-08-31`). For a [music release](downloading.md#music-tracks), a token is a track number (`1-5`, `1,3,7`), or `{disc}x{track}` (`2x3`) when the release has more than one disc. |
|
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). For a [split episode](downloading.md#split-episodes), `.N` picks one part (`S01E01.2`), a range must stay inside the episode (`S01E01.1-S01E01.3`), and `S01E01` on its own takes every part. For a [dated episode](downloading.md#daily-and-date-based-content), a token can also be an ISO air date (`2026-08-11`) or a date range with a colon (`2026-08-01:2026-08-31`). For a [music release](downloading.md#music-tracks), a token is a track number (`1-5`, `1,3,7`), or `{disc}x{track}` (`2x3`) when the release has more than one disc. |
|
||||||
| `--select-titles` | Interactively select what to download: episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
|
| `--select-titles` | Interactively select what to download: episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
|
||||||
| `--latest-episode` | Download only the single most recent episode. |
|
| `--latest-episode` | Download only the single most recent episode. |
|
||||||
|
| `--latest-episodes N` | Download only the N most recent episodes. |
|
||||||
| `--list-titles` | List titles only; do not download. |
|
| `--list-titles` | List titles only; do not download. |
|
||||||
|
|
||||||
### Track-type inclusion / exclusion
|
### Track-type inclusion / exclusion
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ with `--skip-dl`.
|
|||||||
2. **Authenticate**: sign in to the service with your profile's cookies/credentials.
|
2. **Authenticate**: sign in to the service with your profile's cookies/credentials.
|
||||||
3. **Fetch titles**: retrieve the movie, episode list, or album (cached unless
|
3. **Fetch titles**: retrieve the movie, episode list, or album (cached unless
|
||||||
`--no-cache`/`--reset-cache`).
|
`--no-cache`/`--reset-cache`).
|
||||||
4. **Filter titles**: apply `--wanted`, `--latest-episode`, or `--select-titles`.
|
4. **Filter titles**: apply `--wanted`, `--latest-episode`, `--latest-episodes`, or `--select-titles`.
|
||||||
5. **Get tracks**: parse the manifest into video, audio, subtitle, and chapter tracks.
|
5. **Get tracks**: parse the manifest into video, audio, subtitle, and chapter tracks.
|
||||||
6. **Select tracks**: narrow each type down using your quality, codec, range, language,
|
6. **Select tracks**: narrow each type down using your quality, codec, range, language,
|
||||||
and bitrate flags.
|
and bitrate flags.
|
||||||
@@ -65,7 +65,7 @@ with `--skip-dl`.
|
|||||||
`mkvmerge`, and move the finished file to your downloads directory.
|
`mkvmerge`, and move the finished file to your downloads directory.
|
||||||
|
|
||||||
`--list` prints the tracks a title exposes and stops before selection. `--list-titles`
|
`--list` prints the tracks a title exposes and stops before selection. `--list-titles`
|
||||||
prints every title the service returned and stops before `--wanted`/`--latest-episode`
|
prints every title the service returned and stops before `--wanted`/`--latest-episode`/`--latest-episodes`
|
||||||
filtering. `--skip-dl` runs the license step but skips the actual segment download.
|
filtering. `--skip-dl` runs the license step but skips the actual segment download.
|
||||||
|
|
||||||
## Choosing quality
|
## Choosing quality
|
||||||
@@ -600,12 +600,19 @@ when you want a part on its own.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--select-titles` | Interactively pick episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
|
| `--select-titles` | Interactively pick episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
|
||||||
| `--latest-episode` | Download only the single most recent episode. |
|
| `--latest-episode` | Download only the single most recent episode. |
|
||||||
| `--list-titles` | List every title the service returned, then stop. `-w`/`--latest-episode` are not applied to this listing. |
|
| `--latest-episodes N` | Download only the N most recent episodes. The newest episodes can come from more than one season. A [split episode](#split-episodes) counts as one episode, and all its parts download. |
|
||||||
|
| `--list-titles` | List every title the service returned, then stop. `-w`, `--latest-episode` and `--latest-episodes` are not applied to this listing. |
|
||||||
|
|
||||||
|
When `--latest-episode` or `--latest-episodes` is set, `-w` has no effect.
|
||||||
|
|
||||||
```shell title="Grab just the newest episode"
|
```shell title="Grab just the newest episode"
|
||||||
unshackle dl --latest-episode EXAMPLE 81234567
|
unshackle dl --latest-episode EXAMPLE 81234567
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```shell title="Grab the three newest episodes"
|
||||||
|
unshackle dl --latest-episodes 3 EXAMPLE 81234567
|
||||||
|
```
|
||||||
|
|
||||||
## Including and excluding track types
|
## Including and excluding track types
|
||||||
|
|
||||||
You can restrict a download to certain track categories, either positively (only these)
|
You can restrict a download to certain track categories, either positively (only these)
|
||||||
@@ -653,7 +660,7 @@ Before committing to a long download, inspect what unshackle *would* do:
|
|||||||
| Flag | Effect |
|
| Flag | Effect |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--list` | List the tracks the service exposes for each title, then stop. No selection, no download. |
|
| `--list` | List the tracks the service exposes for each title, then stop. No selection, no download. |
|
||||||
| `--list-titles` | List every title the service returned, then stop. `-w`/`--latest-episode` are not applied to this listing. |
|
| `--list-titles` | List every title the service returned, then stop. `-w`, `--latest-episode` and `--latest-episodes` are not applied to this listing. |
|
||||||
| `--skip-dl` | Skip downloading but still acquire the decryption keys. |
|
| `--skip-dl` | Skip downloading but still acquire the decryption keys. |
|
||||||
|
|
||||||
```shell title="See the track selection without downloading"
|
```shell title="See the track selection without downloading"
|
||||||
@@ -1002,6 +1009,7 @@ authoritative list.
|
|||||||
| `--daily` | | Fill missing air dates from TVDB during `--enrich`. |
|
| `--daily` | | Fill missing air dates from TVDB during `--enrich`. |
|
||||||
| `--select-titles` | | Interactively pick episodes or films. |
|
| `--select-titles` | | Interactively pick episodes or films. |
|
||||||
| `--latest-episode` | | Only the newest episode. |
|
| `--latest-episode` | | Only the newest episode. |
|
||||||
|
| `--latest-episodes N` | | Only the N newest episodes. |
|
||||||
| `--video-only` / `--audio-only` / `--subs-only` | `-V` / `-A` / `-S` | Restrict track types. |
|
| `--video-only` / `--audio-only` / `--subs-only` | `-V` / `-A` / `-S` | Restrict track types. |
|
||||||
| `--no-video` / `--no-audio` / `--no-subs` / `--no-chapters` / `--no-attachments` | `-nv` / `-na` / `-ns` / `-nc` / `-nt` | Skip track types. |
|
| `--no-video` / `--no-audio` / `--no-subs` / `--no-chapters` / `--no-attachments` | `-nv` / `-na` / `-ns` / `-nc` / `-nt` | Skip track types. |
|
||||||
| `--worst` | | Lowest bitrate within `-q`. |
|
| `--worst` | | Lowest bitrate within `-q`. |
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Selection for --latest-episode / --latest-episodes and its REST API validation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from unshackle.commands.dl import latest_episode_keys
|
||||||
|
from unshackle.core.api.handlers import validate_download_parameters
|
||||||
|
from unshackle.core.titles import Movie, Movies, Series
|
||||||
|
from unshackle.core.titles.episode import Episode
|
||||||
|
|
||||||
|
|
||||||
|
class Svc:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def episode(season: int, number: int, part: Optional[int] = None) -> Episode:
|
||||||
|
return Episode(id_=f"ep-{season}x{number}.{part}", service=Svc, title="T", season=season, number=number, part=part)
|
||||||
|
|
||||||
|
|
||||||
|
def series() -> Series:
|
||||||
|
# out of order on purpose: the Series sorts itself
|
||||||
|
return Series(episode(s, n) for s, n in [(2, 1), (1, 1), (2, 3), (1, 2), (2, 2)])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("latest_episode", "latest_episodes", "expected"),
|
||||||
|
[
|
||||||
|
(False, None, []),
|
||||||
|
(True, None, [(2, 3)]),
|
||||||
|
(False, 3, [(2, 1), (2, 2), (2, 3)]),
|
||||||
|
(True, 2, [(2, 2), (2, 3)]),
|
||||||
|
(False, 99, [(1, 1), (1, 2), (2, 1), (2, 2), (2, 3)]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_the_newest_episodes_are_picked_across_seasons(
|
||||||
|
latest_episode: bool, latest_episodes: Optional[int], expected: list[tuple[int, int]]
|
||||||
|
) -> None:
|
||||||
|
assert latest_episode_keys(series(), latest_episode, latest_episodes) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_split_episode_takes_one_slot() -> None:
|
||||||
|
titles = Series([episode(1, 4), episode(1, 5, part=1), episode(1, 5, part=2)])
|
||||||
|
assert latest_episode_keys(titles, False, 2) == [(1, 4), (1, 5)]
|
||||||
|
assert latest_episode_keys(titles, True, None) == [(1, 5)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_titles_that_are_not_a_series_are_left_alone() -> None:
|
||||||
|
movies = Movies([Movie(id_="movie-1", service=Svc, name="M")])
|
||||||
|
assert latest_episode_keys(movies, True, 3) == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", [0, -1, True, "3", 1.5])
|
||||||
|
def test_a_non_positive_or_non_integer_count_is_refused(value: object) -> None:
|
||||||
|
assert validate_download_parameters({"latest_episodes": value}) == "latest_episodes must be a positive integer"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_positive_count_is_accepted() -> None:
|
||||||
|
assert validate_download_parameters({"latest_episodes": 3}) is None
|
||||||
@@ -227,6 +227,37 @@ def test_perform_download_puts_the_lang_selection_in_the_service_ctx(
|
|||||||
assert (p["lang"], p["v_lang"], p["a_lang"], [c.name for c in p["acodec"]], p["forced_subs"]) == expected
|
assert (p["lang"], p["v_lang"], p["a_lang"], [c.name for c in p["acodec"]], p["forced_subs"]) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_download_passes_latest_episodes_to_dl_result(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
import click
|
||||||
|
|
||||||
|
import unshackle.commands.dl as dl_module
|
||||||
|
from unshackle.core.api import download_manager, handlers
|
||||||
|
from unshackle.core.services import Services
|
||||||
|
|
||||||
|
class FakeDl:
|
||||||
|
cli = click.Command("dl")
|
||||||
|
|
||||||
|
def __init__(self, ctx: click.Context, **kwargs: object) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def result(self, **kwargs: object) -> None:
|
||||||
|
raise _CapturedCtx(kwargs)
|
||||||
|
|
||||||
|
class FakeService:
|
||||||
|
def __init__(self, ctx: click.Context) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setattr(dl_module, "dl", FakeDl)
|
||||||
|
monkeypatch.setattr(Services, "get_path", staticmethod(lambda s: tmp_path))
|
||||||
|
monkeypatch.setattr(Services, "load", staticmethod(lambda s: FakeService))
|
||||||
|
monkeypatch.setattr(handlers, "load_full_cdm", lambda *a: None)
|
||||||
|
|
||||||
|
with pytest.raises(_CapturedCtx) as exc:
|
||||||
|
download_manager.perform_download("job-1", "EXAMPLE", "t1", {"latest_episodes": 3})
|
||||||
|
|
||||||
|
assert exc.value.args[0]["latest_episodes"] == 3
|
||||||
|
|
||||||
|
|
||||||
def test_worker_relays_a_prompt_and_reads_the_answer(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_worker_relays_a_prompt_and_reads_the_answer(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|||||||
+27
-19
@@ -214,6 +214,18 @@ def title_wanted(candidate: Any, wanted: Collection[str]) -> bool:
|
|||||||
return bool(candidate.matches_wanted(wanted))
|
return bool(candidate.matches_wanted(wanted))
|
||||||
|
|
||||||
|
|
||||||
|
def latest_episode_keys(titles: Any, latest_episode: bool, latest_episodes: Optional[int]) -> list[tuple[int, int]]:
|
||||||
|
"""The (season, number) of each episode ``--latest-episode(s)`` keeps, oldest first.
|
||||||
|
|
||||||
|
Counts distinct episodes, not Series entries, so a split episode takes one slot and all its parts download.
|
||||||
|
"""
|
||||||
|
count = latest_episodes or (1 if latest_episode else 0)
|
||||||
|
if not count or not isinstance(titles, Series):
|
||||||
|
return []
|
||||||
|
# a Series keeps itself sorted, so the newest episodes are last
|
||||||
|
return list(dict.fromkeys((ep.season, ep.number) for ep in titles))[-count:]
|
||||||
|
|
||||||
|
|
||||||
def server_url(server_name: Optional[str]) -> str:
|
def server_url(server_name: Optional[str]) -> str:
|
||||||
"""The configured URL of a remote server, or an empty string when the config cannot be read."""
|
"""The configured URL of a remote server, or an empty string when the config cannot be read."""
|
||||||
from unshackle.core.remote_service import resolve_server
|
from unshackle.core.remote_service import resolve_server
|
||||||
@@ -644,6 +656,12 @@ class dl:
|
|||||||
default=False,
|
default=False,
|
||||||
help="Download only the single most recent episode available.",
|
help="Download only the single most recent episode available.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--latest-episodes",
|
||||||
|
type=click.IntRange(min=1),
|
||||||
|
default=None,
|
||||||
|
help="Download only the N most recent episodes available.",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-vl",
|
"-vl",
|
||||||
"--v-lang",
|
"--v-lang",
|
||||||
@@ -1431,6 +1449,7 @@ class dl:
|
|||||||
select_titles: bool,
|
select_titles: bool,
|
||||||
wanted: list[str],
|
wanted: list[str],
|
||||||
latest_episode: bool,
|
latest_episode: bool,
|
||||||
|
latest_episodes: Optional[int],
|
||||||
lang: list[str],
|
lang: list[str],
|
||||||
v_lang: list[str],
|
v_lang: list[str],
|
||||||
a_lang: list[str],
|
a_lang: list[str],
|
||||||
@@ -1898,13 +1917,12 @@ class dl:
|
|||||||
count = len(titles)
|
count = len(titles)
|
||||||
console.print(Padding(f"[text]Total selected: {count}[/]", (0, 5)))
|
console.print(Padding(f"[text]Total selected: {count}[/]", (0, 5)))
|
||||||
|
|
||||||
latest_episode_id = None
|
latest_keys = latest_episode_keys(titles, latest_episode, latest_episodes)
|
||||||
if latest_episode and isinstance(titles, Series) and len(titles) > 0:
|
if latest_keys:
|
||||||
# Series is already sorted by (season, number, year)
|
picked = ", ".join(f"S{season:02}E{number:02}" for season, number in latest_keys)
|
||||||
# The last episode in the sorted list is the latest
|
self.log.info(f"Latest episode mode: Selecting {picked}")
|
||||||
latest_ep = titles[-1]
|
# replaces -w; a base SxE key selects every part of a split episode
|
||||||
latest_episode_id = f"{latest_ep.season}x{latest_ep.number}"
|
wanted = [f"{season}x{number}" for season, number in latest_keys]
|
||||||
self.log.info(f"Latest episode mode: Selecting S{latest_ep.season:02}E{latest_ep.number:02}")
|
|
||||||
|
|
||||||
def select_best_audio(
|
def select_best_audio(
|
||||||
audio: list[Audio],
|
audio: list[Audio],
|
||||||
@@ -1955,15 +1973,9 @@ class dl:
|
|||||||
|
|
||||||
base_selection = (v_lang, a_lang, s_lang, range_)
|
base_selection = (v_lang, a_lang, s_lang, range_)
|
||||||
|
|
||||||
def post_script_queued(candidate: Any) -> bool:
|
|
||||||
"""Mirror of the filters the loop below applies, so the season counter matches it."""
|
|
||||||
if isinstance(candidate, Episode) and latest_episode and latest_episode_id:
|
|
||||||
return f"{candidate.season}x{candidate.number}" == latest_episode_id
|
|
||||||
return title_wanted(candidate, wanted)
|
|
||||||
|
|
||||||
post_script_pending: dict[Any, int] = {}
|
post_script_pending: dict[Any, int] = {}
|
||||||
for candidate in titles:
|
for candidate in titles:
|
||||||
if post_script_queued(candidate):
|
if title_wanted(candidate, wanted):
|
||||||
key = post_script_group(candidate)
|
key = post_script_group(candidate)
|
||||||
post_script_pending[key] = post_script_pending.get(key, 0) + 1
|
post_script_pending[key] = post_script_pending.get(key, 0) + 1
|
||||||
post_script_last: dict[Any, dict[Path, dict[str, str]]] = {}
|
post_script_last: dict[Any, dict[Path, dict[str, str]]] = {}
|
||||||
@@ -1972,11 +1984,7 @@ class dl:
|
|||||||
|
|
||||||
for i, title in enumerate(titles):
|
for i, title in enumerate(titles):
|
||||||
v_lang, a_lang, s_lang, range_ = base_selection
|
v_lang, a_lang, s_lang, range_ = base_selection
|
||||||
if isinstance(title, Episode) and latest_episode and latest_episode_id:
|
if not title_wanted(title, wanted):
|
||||||
# If --latest-episode is set, only process the latest episode
|
|
||||||
if f"{title.season}x{title.number}" != latest_episode_id:
|
|
||||||
continue
|
|
||||||
elif not title_wanted(title, wanted):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if progress_sink:
|
if progress_sink:
|
||||||
|
|||||||
@@ -591,6 +591,7 @@ def perform_download(
|
|||||||
select_titles=False,
|
select_titles=False,
|
||||||
wanted=params.get("wanted", []),
|
wanted=params.get("wanted", []),
|
||||||
latest_episode=params.get("latest_episode", False),
|
latest_episode=params.get("latest_episode", False),
|
||||||
|
latest_episodes=params.get("latest_episodes"),
|
||||||
lang=ctx.params["lang"],
|
lang=ctx.params["lang"],
|
||||||
v_lang=ctx.params["v_lang"],
|
v_lang=ctx.params["v_lang"],
|
||||||
a_lang=ctx.params["a_lang"],
|
a_lang=ctx.params["a_lang"],
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ DEFAULT_DOWNLOAD_PARAMS = {
|
|||||||
"no_atmos": False,
|
"no_atmos": False,
|
||||||
"wanted": [],
|
"wanted": [],
|
||||||
"latest_episode": False,
|
"latest_episode": False,
|
||||||
|
"latest_episodes": None,
|
||||||
"lang": ["orig"],
|
"lang": ["orig"],
|
||||||
"v_lang": [],
|
"v_lang": [],
|
||||||
"a_lang": [],
|
"a_lang": [],
|
||||||
@@ -1871,7 +1872,7 @@ def validate_download_parameters(data: Dict[str, Any]) -> Optional[str]:
|
|||||||
if not isinstance(data["downloads"], int) or data["downloads"] <= 0:
|
if not isinstance(data["downloads"], int) or data["downloads"] <= 0:
|
||||||
return "downloads must be a positive integer"
|
return "downloads must be a positive integer"
|
||||||
|
|
||||||
for name in ("tmdb_id", "tvdb_id"):
|
for name in ("tmdb_id", "tvdb_id", "latest_episodes"):
|
||||||
if data.get(name) is not None:
|
if data.get(name) is not None:
|
||||||
value = data[name]
|
value = data[name]
|
||||||
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
||||||
|
|||||||
@@ -853,6 +853,10 @@ async def download(request: web.Request) -> web.Response:
|
|||||||
latest_episode:
|
latest_episode:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Download only the single most recent episode (default - false)
|
description: Download only the single most recent episode (default - false)
|
||||||
|
latest_episodes:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
description: Download only the N most recent episodes; overrides latest_episode (default - null)
|
||||||
lang:
|
lang:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
Reference in New Issue
Block a user