emby-cli 0.7.1__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {emby_cli-0.7.1 → emby_cli-0.7.2}/AGENTS.md +4 -2
- {emby_cli-0.7.1 → emby_cli-0.7.2}/CHANGELOG.md +9 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/PKG-INFO +2 -1
- {emby_cli-0.7.1 → emby_cli-0.7.2}/README.md +1 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/_version.py +2 -2
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/cli.py +4 -1
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/item.py +1 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/item_ops.py +91 -17
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_item_ops.py +61 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/.env.example +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/.github/workflows/ci.yml +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/.github/workflows/publish.yml +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/.gitignore +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/LICENSE +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/pyproject.toml +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/__init__.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/__main__.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/api/__init__.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/api/collections.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/api/items.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/api/libraries.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/auth_cache.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/client.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/collection_ops.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/__init__.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/collection.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/config.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/help.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/info.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/library.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/login.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/logout.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/commands/version.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/constants.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/credentials.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/data_cache.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/detail_output.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/download_ops.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/library_ops.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/media_sort.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/output.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/resolve.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/types.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/util.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/src/emby_cli/version.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_auth_cache.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_auth_expired.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_client.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_client_collection.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_client_item.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_client_library.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_client_retry.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_collection.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_collection_ops.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_config.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_credentials.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_data_cache.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_download_one.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_download_ops.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_info.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_item.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_item_performance.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_library.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_library_ops.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_logout.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_media_sort.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_output.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_parser.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_resolve.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_resolve_table.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_resolve_titles.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_session_auth.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_urllib3_warning.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_util.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_validate_args.py +0 -0
- {emby_cli-0.7.1 → emby_cli-0.7.2}/tests/test_version.py +0 -0
|
@@ -352,8 +352,10 @@ Read-only entity commands (`item list`, `item search`, `item show`, `item play`)
|
|
|
352
352
|
Supports `--player`, `--wait`, `--pick-best-item`, `--no-cache`, `--no-parse-query`,
|
|
353
353
|
and comma-separated `--id`. Parent `--id` may appear before the subcommand.
|
|
354
354
|
QUERY defaults to title-line resolution (`resolve_title_items`).
|
|
355
|
-
- `item download` resolves one item by QUERY or `--id` (CSV supported)
|
|
356
|
-
|
|
355
|
+
- `item download` resolves one item by QUERY or `--id` (CSV supported). A
|
|
356
|
+
`--id` that refers to a `Series` downloads all episodes in season/episode
|
|
357
|
+
order. Downloads via `item_ops` (`download_items`, `download_item_ids`,
|
|
358
|
+
`download_from_file`).
|
|
357
359
|
Supports `--output`, `--method`, `--force`, `--throttle`, `--pick-best-item`,
|
|
358
360
|
`--dry-run`, `--from-file`, `--no-parse-query`, and optional `--mirror-path`.
|
|
359
361
|
QUERY / `--from-file` default to title-line resolution (`resolve_title_items`).
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.2
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `item download --id` expands a `Series` ID into all episodes (sorted by
|
|
10
|
+
season/episode, then Id) and downloads them sequentially. Duplicate
|
|
11
|
+
SxxExx versions follow the same rules as title-line downloads
|
|
12
|
+
(`--pick-best-item` or fail with a disambiguation table).
|
|
13
|
+
|
|
5
14
|
## 0.7.1
|
|
6
15
|
|
|
7
16
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: emby-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: CLI to list, search, play, and download/backup original media from an Emby server via its REST API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/soukron/emby-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/soukron/emby-cli
|
|
@@ -120,6 +120,7 @@ Preview a large operation first, then choose a destination:
|
|
|
120
120
|
```bash
|
|
121
121
|
emby-cli item download "breaking bad S01E01" --dry-run
|
|
122
122
|
emby-cli item download --id 123456 --output ./downloads
|
|
123
|
+
emby-cli item download --id 123456 --pick-best-item # series: one file per SxxExx
|
|
123
124
|
emby-cli item download --from-file titles.txt --dry-run
|
|
124
125
|
emby-cli library download "peliculas 4k" --output ./downloads
|
|
125
126
|
```
|
|
@@ -85,6 +85,7 @@ Preview a large operation first, then choose a destination:
|
|
|
85
85
|
```bash
|
|
86
86
|
emby-cli item download "breaking bad S01E01" --dry-run
|
|
87
87
|
emby-cli item download --id 123456 --output ./downloads
|
|
88
|
+
emby-cli item download --id 123456 --pick-best-item # series: one file per SxxExx
|
|
88
89
|
emby-cli item download --from-file titles.txt --dry-run
|
|
89
90
|
emby-cli library download "peliculas 4k" --output ./downloads
|
|
90
91
|
```
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.7.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 7,
|
|
21
|
+
__version__ = version = '0.7.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -540,7 +540,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
540
540
|
|
|
541
541
|
it_download = it_sub.add_parser("download", help="Download media items")
|
|
542
542
|
it_download.add_argument("query", nargs="?", metavar="QUERY")
|
|
543
|
-
it_download.add_argument(
|
|
543
|
+
it_download.add_argument(
|
|
544
|
+
"--id",
|
|
545
|
+
help="Media item ID (comma-separated). Series IDs download all episodes",
|
|
546
|
+
)
|
|
544
547
|
it_download.add_argument(
|
|
545
548
|
"--type",
|
|
546
549
|
dest="item_type",
|
|
@@ -248,6 +248,7 @@ def _cmd_download(client: EmbyClient, args: argparse.Namespace) -> None:
|
|
|
248
248
|
raw_type=_text(args, "item_type"),
|
|
249
249
|
mirror_path=opts.mirror_path,
|
|
250
250
|
path_strip=opts.path_strip,
|
|
251
|
+
pick_best=pick_best,
|
|
251
252
|
)
|
|
252
253
|
print_done(stats)
|
|
253
254
|
raise SystemExit(stats.exit_code())
|
|
@@ -18,9 +18,10 @@ import requests
|
|
|
18
18
|
|
|
19
19
|
from emby_cli.client import EmbyClient
|
|
20
20
|
from emby_cli.constants import SEARCH_ITEM_TYPES, SHOW_ITEM_FIELDS
|
|
21
|
-
from emby_cli.media_sort import sort_media_items
|
|
21
|
+
from emby_cli.media_sort import sort_key_id, sort_media_items
|
|
22
22
|
from emby_cli.output import Stats, print_done, print_download, print_error, print_skip
|
|
23
23
|
from emby_cli.resolve import (
|
|
24
|
+
_pick_episode_versions,
|
|
24
25
|
classify_resolution,
|
|
25
26
|
item_label,
|
|
26
27
|
item_video_width,
|
|
@@ -454,6 +455,59 @@ class DownloadOpts:
|
|
|
454
455
|
)
|
|
455
456
|
|
|
456
457
|
|
|
458
|
+
def sort_episodes(episodes: list[dict]) -> list[dict]:
|
|
459
|
+
"""Sort episodes by season, episode number, then Emby Id."""
|
|
460
|
+
|
|
461
|
+
def key(ep: dict) -> tuple:
|
|
462
|
+
season = ep.get("ParentIndexNumber")
|
|
463
|
+
number = ep.get("IndexNumber")
|
|
464
|
+
if season is not None and number is not None:
|
|
465
|
+
return (0, int(season), int(number), sort_key_id(ep))
|
|
466
|
+
if season is not None:
|
|
467
|
+
return (1, int(season), 0, sort_key_id(ep))
|
|
468
|
+
if number is not None:
|
|
469
|
+
return (2, 0, int(number), sort_key_id(ep))
|
|
470
|
+
return (3, 0, 0, sort_key_id(ep))
|
|
471
|
+
|
|
472
|
+
return sorted(episodes, key=key)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def expand_item_for_download(
|
|
476
|
+
client: EmbyClient,
|
|
477
|
+
item: dict,
|
|
478
|
+
*,
|
|
479
|
+
pick_best: bool = False,
|
|
480
|
+
) -> list[dict] | None:
|
|
481
|
+
"""Return download targets for *item* (one row, or all episodes for Series)."""
|
|
482
|
+
if item.get("Type") != "Series":
|
|
483
|
+
return [item]
|
|
484
|
+
|
|
485
|
+
name = item.get("Name") or item.get("Id")
|
|
486
|
+
try:
|
|
487
|
+
episodes = client.get_show_episodes(item["Id"], season=None)
|
|
488
|
+
except (requests.RequestException, RuntimeError) as exc:
|
|
489
|
+
print_error(f"listing episodes for series '{name}': {exc}")
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
if not episodes:
|
|
493
|
+
print(f"Series '{name}': no episodes found.")
|
|
494
|
+
return []
|
|
495
|
+
|
|
496
|
+
series_name = item.get("Name")
|
|
497
|
+
enriched: list[dict] = []
|
|
498
|
+
for ep in episodes:
|
|
499
|
+
row = dict(ep)
|
|
500
|
+
if series_name and not row.get("SeriesName"):
|
|
501
|
+
row["SeriesName"] = series_name
|
|
502
|
+
enriched.append(row)
|
|
503
|
+
|
|
504
|
+
picked = _pick_episode_versions(sort_episodes(enriched), pick_best=pick_best)
|
|
505
|
+
if picked is None:
|
|
506
|
+
return None
|
|
507
|
+
print(f"Series '{name}': {len(picked)} episode(s)")
|
|
508
|
+
return picked
|
|
509
|
+
|
|
510
|
+
|
|
457
511
|
def resolve_rate(item: dict, throttle: float) -> float | None:
|
|
458
512
|
if not throttle:
|
|
459
513
|
return None
|
|
@@ -584,6 +638,7 @@ def download_item_ids(
|
|
|
584
638
|
raw_type: str | None = None,
|
|
585
639
|
mirror_path: bool = False,
|
|
586
640
|
path_strip: str | None = None,
|
|
641
|
+
pick_best: bool = False,
|
|
587
642
|
) -> Stats:
|
|
588
643
|
"""Download comma-separated item IDs."""
|
|
589
644
|
item_ids = [part.strip() for part in item_id.split(",") if part.strip()]
|
|
@@ -605,7 +660,14 @@ def download_item_ids(
|
|
|
605
660
|
)
|
|
606
661
|
stats.error += 1
|
|
607
662
|
continue
|
|
608
|
-
|
|
663
|
+
targets = expand_item_for_download(client, item, pick_best=pick_best)
|
|
664
|
+
if targets is None:
|
|
665
|
+
stats.error += 1
|
|
666
|
+
continue
|
|
667
|
+
if not targets and item.get("Type") == "Series":
|
|
668
|
+
stats.error += 1
|
|
669
|
+
continue
|
|
670
|
+
items.extend(targets)
|
|
609
671
|
if items:
|
|
610
672
|
got = download_items(
|
|
611
673
|
client,
|
|
@@ -673,9 +735,21 @@ def download_from_file(
|
|
|
673
735
|
totals.error += 1
|
|
674
736
|
continue
|
|
675
737
|
|
|
676
|
-
|
|
738
|
+
targets = expand_item_for_download(client, item, pick_best=pick_best)
|
|
739
|
+
if targets is None:
|
|
740
|
+
elapsed = time.monotonic() - line_t0
|
|
741
|
+
line_stats.append((label, "ERROR", elapsed, "ambiguous episode versions"))
|
|
742
|
+
totals.error += 1
|
|
743
|
+
continue
|
|
744
|
+
if not targets:
|
|
745
|
+
elapsed = time.monotonic() - line_t0
|
|
746
|
+
line_stats.append((label, "ERROR", elapsed, "no episodes"))
|
|
747
|
+
totals.error += 1
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
got = download_items(
|
|
677
751
|
client,
|
|
678
|
-
|
|
752
|
+
targets,
|
|
679
753
|
output,
|
|
680
754
|
method=method,
|
|
681
755
|
force=force,
|
|
@@ -685,20 +759,20 @@ def download_from_file(
|
|
|
685
759
|
path_strip=path_strip,
|
|
686
760
|
)
|
|
687
761
|
elapsed = time.monotonic() - line_t0
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
"
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
762
|
+
if got.error:
|
|
763
|
+
status = "ERROR"
|
|
764
|
+
elif dry_run and got.ok:
|
|
765
|
+
status = "DRY RUN"
|
|
766
|
+
elif got.skip and not got.ok:
|
|
767
|
+
status = "SKIPPED"
|
|
768
|
+
elif got.skip:
|
|
769
|
+
status = "PARTIAL"
|
|
770
|
+
else:
|
|
771
|
+
status = "OK"
|
|
695
772
|
line_stats.append((label, status, elapsed, item.get("Name", "")))
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
totals.skip += 1
|
|
700
|
-
elif result == "error":
|
|
701
|
-
totals.error += 1
|
|
773
|
+
totals.ok += got.ok
|
|
774
|
+
totals.skip += got.skip
|
|
775
|
+
totals.error += got.error
|
|
702
776
|
continue
|
|
703
777
|
|
|
704
778
|
label = raw_line
|
|
@@ -14,8 +14,10 @@ from emby_cli.item_ops import (
|
|
|
14
14
|
ItemListingQuery,
|
|
15
15
|
ItemResolutionError,
|
|
16
16
|
build_item_listing_query,
|
|
17
|
+
download_item_ids,
|
|
17
18
|
emby_search_term_for_strict_query,
|
|
18
19
|
episodes_for_title_line,
|
|
20
|
+
expand_item_for_download,
|
|
19
21
|
fetch_item_listing,
|
|
20
22
|
item_types_for_api,
|
|
21
23
|
matches_strict_name_query,
|
|
@@ -24,8 +26,10 @@ from emby_cli.item_ops import (
|
|
|
24
26
|
play_url,
|
|
25
27
|
playable_items_for_parent,
|
|
26
28
|
resolve_item,
|
|
29
|
+
sort_episodes,
|
|
27
30
|
split_listing_search_query,
|
|
28
31
|
)
|
|
32
|
+
from emby_cli.output import Stats
|
|
29
33
|
|
|
30
34
|
ITEMS = [
|
|
31
35
|
{"Id": "100", "Name": "The Matrix", "Type": "Movie", "ProductionYear": 1999},
|
|
@@ -339,3 +343,60 @@ def test_resolve_item_by_id_prefix_uses_catalog():
|
|
|
339
343
|
assert resolve_item(client, item_id="20", use_cache=False) == ITEMS[1]
|
|
340
344
|
get.assert_called_once_with("20", fields=SHOW_ITEM_FIELDS, use_cache=False)
|
|
341
345
|
search.assert_called_once_with("", item_types="Movie,Episode,Audio,Video", use_cache=False)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_sort_episodes_orders_by_season_and_number():
|
|
349
|
+
episodes = [
|
|
350
|
+
{"Id": "3", "ParentIndexNumber": 2, "IndexNumber": 1},
|
|
351
|
+
{"Id": "1", "ParentIndexNumber": 1, "IndexNumber": 2},
|
|
352
|
+
{"Id": "2", "ParentIndexNumber": 1, "IndexNumber": 1},
|
|
353
|
+
]
|
|
354
|
+
ordered = sort_episodes(episodes)
|
|
355
|
+
assert [ep["Id"] for ep in ordered] == ["2", "1", "3"]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_expand_item_for_download_series(capsys):
|
|
359
|
+
client = MagicMock()
|
|
360
|
+
series = {"Id": "s1", "Name": "Show", "Type": "Series"}
|
|
361
|
+
client.get_show_episodes.return_value = [
|
|
362
|
+
{"Id": "e2", "Type": "Episode", "ParentIndexNumber": 1, "IndexNumber": 2, "Name": "B"},
|
|
363
|
+
{"Id": "e1", "Type": "Episode", "ParentIndexNumber": 1, "IndexNumber": 1, "Name": "A"},
|
|
364
|
+
]
|
|
365
|
+
targets = expand_item_for_download(client, series)
|
|
366
|
+
assert targets is not None
|
|
367
|
+
assert [ep["Id"] for ep in targets] == ["e1", "e2"]
|
|
368
|
+
assert targets[0]["SeriesName"] == "Show"
|
|
369
|
+
assert "2 episode(s)" in capsys.readouterr().out
|
|
370
|
+
client.get_show_episodes.assert_called_once_with("s1", season=None)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_expand_item_for_download_movie_passthrough():
|
|
374
|
+
client = MagicMock()
|
|
375
|
+
movie = {"Id": "m1", "Name": "Film", "Type": "Movie"}
|
|
376
|
+
assert expand_item_for_download(client, movie) == [movie]
|
|
377
|
+
client.get_show_episodes.assert_not_called()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_download_item_ids_expands_series(tmp_path, capsys):
|
|
381
|
+
client = MagicMock()
|
|
382
|
+
series = {"Id": "s1", "Name": "Show", "Type": "Series"}
|
|
383
|
+
episodes = [
|
|
384
|
+
{"Id": "e1", "Type": "Episode", "ParentIndexNumber": 1, "IndexNumber": 1, "Name": "A"},
|
|
385
|
+
]
|
|
386
|
+
client.get_item_info.return_value = series
|
|
387
|
+
client.get_show_episodes.return_value = episodes
|
|
388
|
+
with patch("emby_cli.item_ops.download_items") as download_items:
|
|
389
|
+
download_items.return_value = Stats(ok=1)
|
|
390
|
+
stats = download_item_ids(
|
|
391
|
+
client,
|
|
392
|
+
"s1",
|
|
393
|
+
tmp_path,
|
|
394
|
+
method="download",
|
|
395
|
+
force=False,
|
|
396
|
+
throttle=0,
|
|
397
|
+
dry_run=True,
|
|
398
|
+
)
|
|
399
|
+
assert stats.ok == 1
|
|
400
|
+
passed_items = download_items.call_args.args[1]
|
|
401
|
+
assert len(passed_items) == 1
|
|
402
|
+
assert passed_items[0]["Id"] == "e1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|