diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index 3aa36d0f..a0840edd 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -189,7 +189,7 @@ order the service used, then renumbers the episodes into the order you asked for | `--cdm-only` / `--vaults-only` | Use only the CDM, or only Key Vaults, for key acquisition. | | `--cdm ` | Use the named CDM device from the `cdm` config mapping for this run, ignoring the service/default mapping. | | `--skip-dl` | Skip the download but still retrieve keys. | -| `--export` | Export track info and keys to a JSON file in the exports directory. | +| `--export` | Export track info and keys to a `mediaexport` JSON file in the exports directory. | ### Network & proxy @@ -263,7 +263,7 @@ unshackle forwards any `dl` options after the file verbatim, so you can override unshackle import export.json -r HDR10 --proxy US ``` -The export file must be a valid v2 export from a current version of unshackle, made with `dl --export`, and must contain a `service` tag. +The export file can be a `mediaexport` file from `dl --export`, an export from an older version of unshackle, or an export from unidl. The file must name the service it came from. --- diff --git a/tests/core/test_import_command.py b/tests/core/test_import_command.py new file mode 100644 index 00000000..480e11a1 --- /dev/null +++ b/tests/core/test_import_command.py @@ -0,0 +1,93 @@ +"""Tests for the ``unshackle import`` command: it reads every export format and forwards to ``dl``.""" + +from __future__ import annotations + +import importlib +import json +from pathlib import Path +from typing import Any + +import pytest +from click.testing import CliRunner + +from unshackle.commands.dl import dl + +KID = "00000000000000000000000000000001" +KEY = "aa" * 16 + +import_cli = importlib.import_module("unshackle.commands.import").ImportCommand.cli + +MEDIAEXPORT = { + "kind": "mediaexport", + "version": 1, + "service": {"tag": "EXAMPLE", "name": "Example"}, + "titles": [ + { + "id": "movie-1", + "kind": "movie", + "title": "Example Movie", + "manifests": [{"url": "https://example.test/m.mpd", "type": "dash", "role": "primary"}], + "keys": {KID: KEY}, + } + ], +} + +UNSHACKLE_V2 = { + "version": 2, + "service": "EXAMPLE", + "titles": { + "movie-1": { + "meta": {"type": "movie", "name": "Example Movie"}, + "manifest_url": "https://example.test/m.mpd", + "manifest_type": "DASH", + "tracks": {"v1": {"id": "v1", "url": "https://example.test/m.mpd", "keys": {KID: KEY}}}, + } + }, +} + +UNIDL_V1 = { + "kind": "unidl-export", + "version": 1, + "service": "EXAMPLE", + "titles": [ + { + "title": {"id": "movie-1", "kind": "movie", "name": "Example Movie"}, + "manifest_url": "https://example.test/m.mpd", + "keys": [f"{KID}:{KEY}"], + } + ], +} + + +@pytest.fixture +def forwarded(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]: + """The argument lists the import command sends to ``dl``.""" + calls: list[list[str]] = [] + + def fake_main(*, args: list[str], **_: Any) -> None: + calls.append(list(args)) + + monkeypatch.setattr(dl.cli, "main", fake_main) + return calls + + +@pytest.mark.parametrize("raw", [MEDIAEXPORT, UNSHACKLE_V2, UNIDL_V1], ids=["mediaexport", "unshackle-v2", "unidl-v1"]) +def test_import_forwards_service_tag(tmp_path: Path, forwarded: list[list[str]], raw: dict[str, Any]) -> None: + export = tmp_path / "export.json" + export.write_text(json.dumps(raw), encoding="utf-8") + + result = CliRunner().invoke(import_cli, [str(export), "-r", "HDR10"]) + + assert result.exit_code == 0, result.output + assert forwarded == [["-r", "HDR10", "--import", str(export), "EXAMPLE"]] + + +def test_import_rejects_a_file_that_is_not_an_export(tmp_path: Path, forwarded: list[list[str]]) -> None: + export = tmp_path / "export.json" + export.write_text(json.dumps({"hello": "world"}), encoding="utf-8") + + result = CliRunner().invoke(import_cli, [str(export)]) + + assert result.exit_code != 0 + assert "is not a usable export file" in result.output + assert forwarded == [] diff --git a/unshackle/commands/import.py b/unshackle/commands/import.py index ec803bcc..3aa69653 100644 --- a/unshackle/commands/import.py +++ b/unshackle/commands/import.py @@ -1,10 +1,9 @@ from __future__ import annotations -import json from pathlib import Path -from typing import Any import click +import mediaexport from unshackle.commands.dl import dl from unshackle.core.constants import context_settings @@ -33,19 +32,13 @@ class ImportCommand: raise click.ClickException(f"Export file not found: {export_file}") try: - data: dict[str, Any] = json.loads(export_file.read_text(encoding="utf8")) - except json.JSONDecodeError as e: - raise click.ClickException(f"Export file is not valid JSON: {e}") + doc = mediaexport.read(export_file) + except mediaexport.ExportError as e: + raise click.ClickException(f"{export_file} is not a usable export file: {e}") - if data.get("version") != 2: - raise click.ClickException( - f"Unsupported export version {data.get('version')!r}. " - "Re-create the export with a current build of unshackle." - ) - - service_tag = data.get("service") + service_tag = doc.service_tag if not service_tag: - raise click.ClickException("Export file is missing the 'service' tag.") + raise click.ClickException("Export file does not name the service it came from.") args = [*dl_args, "--import", str(export_file), service_tag] dl.cli.main(args=args, prog_name="unshackle dl", standalone_mode=False)