From 29d0828c33e72fbaa16e7eef23188e291eb0f33b Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Wed, 19 Aug 2026 13:45:58 -0600 Subject: [PATCH] style: clean up prose --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +-- .github/ISSUE_TEMPLATE/feature_request.md | 4 +-- docs/reference/configuration/directories.md | 36 +++++++++---------- mkdocs.yml | 2 +- tests/core/test_click_types.py | 2 +- tests/core/test_export.py | 2 +- tests/core/test_ism_init.py | 4 +-- tests/core/test_service_dl_overrides.py | 2 +- tests/core/test_service_repo.py | 2 +- tests/remote/README.md | 18 +++++----- .../remote/e2e/fixtures/fixtures-example.yaml | 30 ++++++++-------- tests/remote/e2e/test_live_download.py | 2 +- tests/remote/e2e/test_live_quality.py | 6 ++-- tests/remote/unit/test_download_manager.py | 2 +- tests/remote/unit/test_errors.py | 4 +-- tests/remote/unit/test_routes.py | 2 +- tests/tracks/test_hybrid_selection.py | 14 ++++---- unshackle/core/__main__.py | 4 +-- unshackle/core/cdm/loader.py | 2 +- unshackle/core/downloaders/requests.py | 2 +- unshackle/core/drm/playready_errors.py | 1 - unshackle/core/manifests/dash.py | 2 +- unshackle/core/manifests/hls.py | 2 +- unshackle/core/manifests/ism.py | 2 +- unshackle/core/manifests/ism_init.py | 10 +++--- unshackle/core/providers/__init__.py | 4 +-- unshackle/core/providers/tvdb.py | 2 +- unshackle/core/proxies/proton.py | 2 +- unshackle/core/remote_service.py | 2 +- unshackle/core/service_repo.py | 6 ++-- unshackle/core/session.py | 4 +-- unshackle/core/tracks/dv_fixup.py | 2 +- unshackle/core/tracks/subtitle_convert.py | 10 +++--- unshackle/core/tracks/track.py | 2 +- unshackle/core/tracks/video.py | 4 +-- unshackle/core/utilities.py | 2 +- unshackle/core/utils/bitrate.py | 8 ++--- unshackle/core/utils/ip_info.py | 6 ++-- unshackle/services/MUSIC_EXAMPLE/__init__.py | 2 +- unshackle/unshackle-example.yaml | 2 +- 40 files changed, 109 insertions(+), 110 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4721aee5..0570b4cb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: Bug report about: Create a report to help us improve title: "" labels: "" -assignees: Sp5rky +assignees: imSp4rky --- **Describe the bug** @@ -57,7 +57,7 @@ Add any other context about the problem here. --- -**⚠️ Important:** +**Important:** - **DO NOT include service-specific implementation code** unless you have explicit rights to share it - **DO NOT share credentials, API keys, WVD files, or authentication tokens** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2f3d79e1..32605565 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,7 +3,7 @@ name: Feature request about: Suggest an idea for this project title: "" labels: "" -assignees: Sp5rky +assignees: imSp4rky --- **Feature Category** @@ -49,5 +49,5 @@ Add any other context or screenshots about the feature request here. --- -**⚠️ Note:** +**Note:** This project focuses on the core framework and tooling. Service-specific feature requests should focus on what the framework should support, not specific service implementations. diff --git a/docs/reference/configuration/directories.md b/docs/reference/configuration/directories.md index 363fec81..ec132f66 100644 --- a/docs/reference/configuration/directories.md +++ b/docs/reference/configuration/directories.md @@ -16,24 +16,24 @@ directories: | Key | Type | Default | Overridable | Purpose | |-----|------|---------|:-----------:|---------| -| `downloads` | path | `/downloads` | ✅ | Default output directory for finished files. | -| `temp` | path | `/temp` | ✅ | Temporary working files during download/decrypt/mux. | -| `cache` | path | `/cache` | ✅ | Generic cache, title cache, and the update-check store. | -| `cookies` | path | `/cookies` | ✅ | Per-service cookie files (and VPN cookie files). | -| `logs` | path | `/logs` | ✅ | Log files. | -| `exports` | path | `/exports` | ✅ | Export JSON files. | -| `wvds` | path | `/WVDs` | ✅ | Widevine device files (`.wvd`). | -| `prds` | path | `/PRDs` | ✅ | PlayReady device files (`.prd`). | -| `dcsl` | path | `/DCSL` | ✅ | DCSL data. | -| `commands` | path | `unshackle/commands` | ✅ | CLI command modules. | -| `services` | list \| path | `[unshackle/services]` | ✅ | Service search paths and/or remote repo specs (see below). | -| `vaults` | path | `unshackle/vaults` | ✅ | Vault backend modules. | -| `fonts` | path | `unshackle/fonts` | ✅ | Bundled fonts. | -| `user_configs` | path | `unshackle/` | ❌ protected | Where `unshackle.yaml` lives. | -| `data` | path | `unshackle/` | ❌ protected | Base for the data subdirectories above. | -| `core_dir` | path | `unshackle/core` | ❌ protected | Package core. | -| `namespace_dir` | path | `unshackle/` | ❌ protected | Package root. | -| `app_dirs` | - | `AppDirs("unshackle", False)` | ❌ protected | Internal AppDirs instance. | +| `downloads` | path | `/downloads` | Yes | Default output directory for finished files. | +| `temp` | path | `/temp` | Yes | Temporary working files during download/decrypt/mux. | +| `cache` | path | `/cache` | Yes | Generic cache, title cache, and the update-check store. | +| `cookies` | path | `/cookies` | Yes | Per-service cookie files (and VPN cookie files). | +| `logs` | path | `/logs` | Yes | Log files. | +| `exports` | path | `/exports` | Yes | Export JSON files. | +| `wvds` | path | `/WVDs` | Yes | Widevine device files (`.wvd`). | +| `prds` | path | `/PRDs` | Yes | PlayReady device files (`.prd`). | +| `dcsl` | path | `/DCSL` | Yes | DCSL data. | +| `commands` | path | `unshackle/commands` | Yes | CLI command modules. | +| `services` | list \| path | `[unshackle/services]` | Yes | Service search paths and/or remote repo specs (see below). | +| `vaults` | path | `unshackle/vaults` | Yes | Vault backend modules. | +| `fonts` | path | `unshackle/fonts` | Yes | Bundled fonts. | +| `user_configs` | path | `unshackle/` | No protected | Where `unshackle.yaml` lives. | +| `data` | path | `unshackle/` | No protected | Base for the data subdirectories above. | +| `core_dir` | path | `unshackle/core` | No protected | Package core. | +| `namespace_dir` | path | `unshackle/` | No protected | Package root. | +| `app_dirs` | - | `AppDirs("unshackle", False)` | No protected | Internal AppDirs instance. | !!! note "The `services` directory is special" `services` may be a **list**, and each entry can be either a local directory or a diff --git a/mkdocs.yml b/mkdocs.yml index d796e069..95f24827 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: unshackle -site_description: Movie, TV, and Music archival software — user guide and REST API reference +site_description: Movie, TV, and Music archival software. User guide and REST API reference site_url: https://docs.unshackle.dev/ repo_url: https://github.com/unshackle-dl/unshackle repo_name: unshackle-dl/unshackle diff --git a/tests/core/test_click_types.py b/tests/core/test_click_types.py index eee5de87..8bd063d7 100644 --- a/tests/core/test_click_types.py +++ b/tests/core/test_click_types.py @@ -1,4 +1,4 @@ -"""Tests for SubtitleCodecChoice — notably the ``original`` keep-source sentinel that +"""Tests for SubtitleCodecChoice, notably the ``original`` keep-source sentinel that services set via the ``sub_format`` override (must not be rejected as an invalid codec).""" from __future__ import annotations diff --git a/tests/core/test_export.py b/tests/core/test_export.py index d393e0a0..4522e627 100644 --- a/tests/core/test_export.py +++ b/tests/core/test_export.py @@ -1,4 +1,4 @@ -"""Tests for ``dl.write_export`` — the ``--export`` JSON sidecar. +"""Tests for ``dl.write_export``, the ``--export`` JSON sidecar. Regression: DRM-free tracks never pass through ``prepare_drm``, so ``write_export`` must accept ``drm=None`` (and DRM systems without ``to_dict``/``content_keys`` such diff --git a/tests/core/test_ism_init.py b/tests/core/test_ism_init.py index 4d149468..02dbf59f 100644 --- a/tests/core/test_ism_init.py +++ b/tests/core/test_ism_init.py @@ -61,7 +61,7 @@ VIDEO_HEVC_SDR_CPD = ( ) # Real Dolby Vision (dvhe, DoViProfile "stn") CodecPrivateData from a Smooth # manifest: NALs arrive SPS,PPS,VPS (VPS last) and the VUI colour triple is -# Unspecified (2,2,2) — DV is signalled by FourCC only, never by CICP. +# Unspecified (2,2,2): DV is signalled by FourCC only, never by CICP. VIDEO_HEVC_DV_CPD = ( "00000001420101022000000300B00000030000030096A001E020021C4D9457B91CAF016E0404042800001F480002EE0401F4E1" "15EE7E0001312D00002FAF0C80000000014401C1ACBE0EC90000000140010C01FFFF022000000300B00000030000030096" @@ -295,7 +295,7 @@ def test_ec3_without_dolby_guid_builds_bare_entry(): def test_aac_codec_private_data_synthesis_matches_real_manifest(): - # 48 kHz stereo AAC-LC must produce 0x1190 — the exact ASC real manifests carry. + # 48 kHz stereo AAC-LC must produce 0x1190, the exact ASC real manifests carry. assert synthesize_aac_codec_private_data("AACL", 48000, 2).hex() == "1190" diff --git a/tests/core/test_service_dl_overrides.py b/tests/core/test_service_dl_overrides.py index 1d5184ce..19d46a61 100644 --- a/tests/core/test_service_dl_overrides.py +++ b/tests/core/test_service_dl_overrides.py @@ -1,4 +1,4 @@ -"""Tests for ``apply_service_dl_overrides`` — precedence: CLI/env > service dl > global dl > defaults.""" +"""Tests for ``apply_service_dl_overrides``. Precedence: CLI/env > service dl > global dl > defaults.""" from __future__ import annotations diff --git a/tests/core/test_service_repo.py b/tests/core/test_service_repo.py index 5d90d3b9..6a692e8c 100644 --- a/tests/core/test_service_repo.py +++ b/tests/core/test_service_repo.py @@ -246,7 +246,7 @@ def test_changed_services_up_to_date(monkeypatch): @pytest.mark.parametrize("order", [["a", "b"], ["b", "a"]]) def test_collision_first_source_wins(tmp_path, order): - """Discovery dedupes by tag honoring list order — the FIRST source to define a tag wins, + """Discovery dedupes by tag honoring list order: the FIRST source to define a tag wins, whether it's a local dir or a repo clone; later duplicates are shadowed.""" dirs = {} for name in ("a", "b"): diff --git a/tests/remote/README.md b/tests/remote/README.md index 5f547277..2f041917 100644 --- a/tests/remote/README.md +++ b/tests/remote/README.md @@ -2,10 +2,10 @@ Test suite for the unshackle remote-services subsystem: -- `unshackle/core/remote_service.py` — `RemoteClient`, `RemoteService`, helpers -- `unshackle/core/api/` — routes, handlers, session_store, input_bridge, +- `unshackle/core/remote_service.py`: `RemoteClient`, `RemoteService`, helpers +- `unshackle/core/api/`: routes, handlers, session_store, input_bridge, download_manager, errors, compression -- `unshackle/commands/serve.py` — CLI surface +- `unshackle/commands/serve.py`: CLI surface Two tiers: @@ -38,7 +38,7 @@ The suite can either: `--server-url http://host:port`. Spawn mode is controlled by `--spawn-serve {auto, always, never}` (default -`auto` — spawn only when `--server-url` is empty). +`auto` spawns only when `--server-url` is empty). ### Auto-spawn (recommended) @@ -78,7 +78,7 @@ services you have access to. Schema (see the example file for full docs): services: EXAMPLE: title_url: "..." # required - series_url: "..." # optional — overrides movie target when set + series_url: "..." # optional, overrides movie target when set target_season: 1 target_episode: 1 search_query: "..." @@ -114,9 +114,9 @@ uv run pytest tests/remote -v --live ## Markers -- `unit` — fast, mocked (default) -- `live` — needs `--live` -- `slow` — hits real services; combine with `--live` +- `unit`: fast, mocked (default) +- `live`: needs `--live` +- `slow`: hits real services; combine with `--live` Run only fast unit tests: @@ -127,4 +127,4 @@ uv run pytest tests/remote -m "unit and not slow" ## Adding a new service to e2e 1. Add a block under `services:` in your local `fixtures.yaml`. -2. No new Python needed — every e2e test is parametrized over the YAML. +2. No new Python needed; every e2e test is parametrized over the YAML. diff --git a/tests/remote/e2e/fixtures/fixtures-example.yaml b/tests/remote/e2e/fixtures/fixtures-example.yaml index f1eb48e3..2c5961da 100644 --- a/tests/remote/e2e/fixtures/fixtures-example.yaml +++ b/tests/remote/e2e/fixtures/fixtures-example.yaml @@ -8,39 +8,39 @@ # unshackle (matches the service directory name, e.g. EXAMPLE). The keys # below are read by the e2e tests: # -# title_url (required) — movie URL/slug/ID accepted by the service -# series_url (optional) — series URL/slug/ID; when set, the +# title_url (required): movie URL/slug/ID accepted by the service +# series_url (optional): series URL/slug/ID; when set, the # quality + license + download tests # target target_season/target_episode # instead of the movie # target_season (optional, default 1) # target_episode (optional, default 1) -# search_query (optional) — string to send to /api/search -# requires_auth (optional, default true) — informational -# server_cdm (optional, default false) — informational -# drm (optional) — informational ("widevine" / "playready") +# search_query (optional): string to send to /api/search +# requires_auth (optional, default true): informational +# server_cdm (optional, default false): informational +# drm (optional): informational ("widevine" / "playready") # -# expected_quality (optional) — when present, the quality test +# expected_quality (optional): when present, the quality test # asserts these limits against the # discovered video tracks -# min_height (optional) — max video.height >= this -# min_codecs (optional) — codec names that must be present +# min_height (optional): max video.height >= this +# min_codecs (optional): codec names that must be present # (AVC, HEVC, VC1, VP8, VP9, AV1) -# min_ranges (optional) — range names that must be present +# min_ranges (optional): range names that must be present # (SDR, HDR10, HDR10P, DV, HLG, HYBRID) -# min_track_count (optional) — at least this many video tracks +# min_track_count (optional): at least this many video tracks # -# runs_download_test (optional, default false) — when true, the +# runs_download_test (optional, default false): when true, the # download smoke test # runs for this service -# runs_license_test (optional, default false) — when true, the +# runs_license_test (optional, default false): when true, the # license test runs for # this service -# license_drm (optional, default "widevine") — DRM type the +# license_drm (optional, default "widevine"): DRM type the # license test # requests ("widevine" # or "playready") -# license_quality (optional, default 1080) — pixel height of the +# license_quality (optional, default 1080): pixel height of the # track the license test # targets diff --git a/tests/remote/e2e/test_live_download.py b/tests/remote/e2e/test_live_download.py index cb8dcc35..febe24e8 100644 --- a/tests/remote/e2e/test_live_download.py +++ b/tests/remote/e2e/test_live_download.py @@ -7,7 +7,7 @@ For any fixture service with ``runs_download_test: true``, this test: 4. Fetches the manifest URL with the resolved headers. 5. Asserts the body is non-empty and looks like DASH/HLS. -It does NOT decrypt or mux — that requires a full local CDM. It proves +It does NOT decrypt or mux; that requires a full local CDM. It proves the end-to-end pipeline up to CDN reachability for the selected quality. """ diff --git a/tests/remote/e2e/test_live_quality.py b/tests/remote/e2e/test_live_quality.py index 52b59911..d7031fd4 100644 --- a/tests/remote/e2e/test_live_quality.py +++ b/tests/remote/e2e/test_live_quality.py @@ -22,7 +22,7 @@ def create_session(http_session, server_url: str, service: str, title_id: str) - payload = { "service": service, "title_id": title_id, - # Quality knobs — send enum names the server's session_create parser accepts. + # Quality knobs: send enum names the server's session_create parser accepts. "range_": ["SDR", "HDR10", "DV"], "vcodec": ["AVC", "HEVC"], "best_available": True, @@ -115,13 +115,13 @@ def test_track_quality_meets_expected(http_session, server_url: str, service_cas tracks = r.json() video = tracks.get("video") or [] summary = summarize(video) - # Print summary even when test passes — discovery aid. + # Print summary even when the test passes, as a discovery aid. with capsys.disabled(): print(f"\n[{service}] target={kind} '{target.get('name')}' -> {summary}") expected = conf.get("expected_quality") or {} if not expected: - pytest.skip(f"{service}: no expected_quality in fixtures.yaml — discovered={summary}") + pytest.skip(f"{service}: no expected_quality in fixtures.yaml; discovered={summary}") if "min_height" in expected: assert summary["max_height"] >= expected["min_height"], ( diff --git a/tests/remote/unit/test_download_manager.py b/tests/remote/unit/test_download_manager.py index deb3d377..b8accba0 100644 --- a/tests/remote/unit/test_download_manager.py +++ b/tests/remote/unit/test_download_manager.py @@ -1,7 +1,7 @@ """Unit tests for DownloadJob + DownloadQueueManager state machine. These tests focus on the queue manager's data layer (create/get/list/cancel/ -cleanup/serialize) — they do not exercise the actual subprocess download path. +cleanup/serialize); they do not exercise the actual subprocess download path. """ from __future__ import annotations diff --git a/tests/remote/unit/test_errors.py b/tests/remote/unit/test_errors.py index e8f18b0e..96e2f127 100644 --- a/tests/remote/unit/test_errors.py +++ b/tests/remote/unit/test_errors.py @@ -47,7 +47,7 @@ def test_api_error_default_http_status_per_code() -> None: APIErrorCode.NETWORK_ERROR: 503, APIErrorCode.SERVICE_UNAVAILABLE: 503, } - # Lock that the cases above are the *complete* live set — a new/removed code trips this. + # Lock that the cases above are the *complete* live set; a new/removed code trips this. assert {c.name for c in cases} == {c.name for c in APIErrorCode} for code, expected in cases.items(): assert APIError(code, "x").http_status == expected, code @@ -142,7 +142,7 @@ def test_handle_api_exception_categorizes_generic() -> None: assert body["error_code"] == "NETWORK_ERROR" -# Locks the FULL observable response — (http status, error_code, retryable) — that +# Locks the FULL observable response (http status, error_code, retryable) that # a client sees for a generic (non-APIError) exception flowing through the # handle_api_exception funnel that every route's `except Exception` path calls. # This is the equivalence reference: after the categorize_exception collapse, diff --git a/tests/remote/unit/test_routes.py b/tests/remote/unit/test_routes.py index 39498f97..cee0b95b 100644 --- a/tests/remote/unit/test_routes.py +++ b/tests/remote/unit/test_routes.py @@ -190,5 +190,5 @@ async def test_auth_middleware_accepts_known_key(make_app, aiohttp_client) -> No app["config"]["users"]["good-key"] = {"devices": []} client = await aiohttp_client(app) resp = await client.get("/api/session/nonexistent", headers={"X-Secret-Key": "good-key"}) - # Auth passed; handler then 404s the session — anything other than 401 is fine here. + # Auth passed; handler then 404s the session; anything other than 401 is fine here. assert resp.status != 401 diff --git a/tests/tracks/test_hybrid_selection.py b/tests/tracks/test_hybrid_selection.py index a3b49806..61ad1d75 100644 --- a/tests/tracks/test_hybrid_selection.py +++ b/tests/tracks/test_hybrid_selection.py @@ -2,14 +2,14 @@ Covers the selection primitives that back `-r ...,DV,HYBRID` downloads: -- ``Tracks.select_hybrid`` — picks the HDR base layer per resolution and the +- ``Tracks.select_hybrid``: picks the HDR base layer per resolution and the single lowest DV track used as a hybrid ingredient. -- ``Tracks.merge_video_selections`` — de-duplicates the ingredient/deliverable +- ``Tracks.merge_video_selections``: de-duplicates the ingredient/deliverable overlap so a DV track that is chosen as both is not muxed/downloaded twice. -- ``Tracks.partition_hybrid_videos`` — splits the ladder into hybrid-ingredient +- ``Tracks.partition_hybrid_videos``: splits the ladder into hybrid-ingredient candidates and the standalone-deliverable pool; HDR10/HDR10+/DV only enter the pool when their range was explicitly requested alongside HYBRID. -- ``Tracks.flag_hybrid_ingredients`` — marks ingredient-only tracks with +- ``Tracks.flag_hybrid_ingredients``: marks ingredient-only tracks with ``hybrid_base_only`` so the standalone mux loop skips them. The remaining ``dl`` glue (the Cartesian deliverable product and the mux loop) @@ -169,7 +169,7 @@ def flagged(tracks: list[Video]) -> set[str]: def test_flag_hybrid_only_flags_base_and_ingredient_dv(ladder: list[Video]) -> None: """`-r HYBRID`: no deliverables, so the base and the ingredient DV are both - skipped by the standalone mux loop — only the hybrid output remains.""" + skipped by the standalone mux loop; only the hybrid output remains.""" hybrid_selected = list(filter(Tracks().select_hybrid(ladder, [1080]), ladder)) Tracks.flag_hybrid_ingredients(hybrid_selected, []) assert flagged(ladder) == {"hdr10p-1080", "dv-360"} @@ -177,7 +177,7 @@ def test_flag_hybrid_only_flags_base_and_ingredient_dv(ladder: list[Video]) -> N def test_flag_hybrid_plus_hdr10p_keeps_base_deliverable(ladder: list[Video]) -> None: """`-r HYBRID,HDR10P`: the base is also an explicit deliverable, only the - ingredient DV is skipped — hybrid + standalone HDR10+ are muxed.""" + ingredient DV is skipped; hybrid + standalone HDR10+ are muxed.""" hybrid_selected = list(filter(Tracks().select_hybrid(ladder, [1080]), ladder)) base = next(t for t in ladder if t.id == "hdr10p-1080") Tracks.flag_hybrid_ingredients(hybrid_selected, [base]) @@ -186,7 +186,7 @@ def test_flag_hybrid_plus_hdr10p_keeps_base_deliverable(ladder: list[Video]) -> def test_flag_hybrid_plus_hdr10p_and_dv_keeps_both_deliverables(ladder: list[Video]) -> None: """`-r HYBRID,HDR10P,DV`: best DV is a deliverable, lowest DV stays - ingredient-only — hybrid + HDR10+ + DV are muxed.""" + ingredient-only; hybrid + HDR10+ + DV are muxed.""" hybrid_selected = list(filter(Tracks().select_hybrid(ladder, [1080]), ladder)) base = next(t for t in ladder if t.id == "hdr10p-1080") best_dv = next(t for t in ladder if t.id == "dv-1080") diff --git a/unshackle/core/__main__.py b/unshackle/core/__main__.py index 7c30742e..cbe4b480 100644 --- a/unshackle/core/__main__.py +++ b/unshackle/core/__main__.py @@ -23,7 +23,7 @@ from unshackle.core.utilities import close_debug_logger, init_debug_logger @click.option("-v", "--version", is_flag=True, default=False, help="Print version information.") @click.option("-d", "--debug", is_flag=True, default=False, help="Enable DEBUG level logs and JSON debug logging.") def main(version: bool, debug: bool) -> None: - """unshackle—Modular Movie, TV, and Music Archival Software.""" + """unshackle: Modular Movie, TV, and Music Archival Software.""" debug_logging_enabled = debug or config.debug logging.basicConfig( @@ -80,7 +80,7 @@ def main(version: bool, debug: bool) -> None: latest_version = UpdateChecker.check_for_updates_sync(__version__) if latest_version: console.print( - f"\n[yellow]⚠️ Update available![/yellow] " + f"\n[yellow]Update available![/yellow] " f"Current: {__version__} → Latest: [green]{latest_version}[/green]", justify="center", ) diff --git a/unshackle/core/cdm/loader.py b/unshackle/core/cdm/loader.py index cf97672d..ec342518 100644 --- a/unshackle/core/cdm/loader.py +++ b/unshackle/core/cdm/loader.py @@ -2,7 +2,7 @@ Instantiates a CDM object (local or remote) given a resolved device name. Name resolution (quality-based, profile-based, DRM-type) is the caller's -responsibility — this module only handles the instantiation step. +responsibility; this module only handles the instantiation step. """ from __future__ import annotations diff --git a/unshackle/core/downloaders/requests.py b/unshackle/core/downloaders/requests.py index 6db09326..3ad133b6 100644 --- a/unshackle/core/downloaders/requests.py +++ b/unshackle/core/downloaders/requests.py @@ -640,7 +640,7 @@ def download( RnetSession preserves TLS fingerprinting for services that need it. segmented: If downloads are segments or parts of one bigger file. part_offset: Byte offset to write at within a pre-allocated file. When set - (with `part_end`), enables part mode for parallel ranged downloads — + (with `part_end`), enables part mode for parallel ranged downloads: no truncate, no skip-if-exists, no control file; emits only `advance` events; retries resume mid-part via Range. part_end: Inclusive end byte of the part. Required when `part_offset` is set. diff --git a/unshackle/core/drm/playready_errors.py b/unshackle/core/drm/playready_errors.py index 9890ea89..335341fa 100644 --- a/unshackle/core/drm/playready_errors.py +++ b/unshackle/core/drm/playready_errors.py @@ -872,7 +872,6 @@ def describe(code: int) -> str | None: if __name__ == "__main__": - # ponytail: one check, signed and unsigned forms resolve to the same entry assert describe(0x8004C065) == describe(-2147172251) assert "DEVCERT_REVOKED" in describe(-2147172251) assert describe(-1021) is None # non-DRM_E service code diff --git a/unshackle/core/manifests/dash.py b/unshackle/core/manifests/dash.py index 758e5259..51f28afb 100644 --- a/unshackle/core/manifests/dash.py +++ b/unshackle/core/manifests/dash.py @@ -1177,7 +1177,7 @@ class DASH: drm.append(PlayReady(pssh=pr_pssh, kid=kid, pssh_b64=pr_pssh_b64)) elif urn == ClearKeyCENC.urn: - # W3C EME ClearKey (org.w3.clearkey) — match the scheme UUID alone, + # W3C EME ClearKey (org.w3.clearkey): match the scheme UUID alone, # value="ClearKey1.0" is spec'd (DASH-IF CCP) but not required in the wild kid_attr = protection.get("default_KID") or protection.get("{urn:mpeg:cenc:2013}default_KID") kid = None diff --git a/unshackle/core/manifests/hls.py b/unshackle/core/manifests/hls.py index 9a831ed8..7f49994a 100644 --- a/unshackle/core/manifests/hls.py +++ b/unshackle/core/manifests/hls.py @@ -355,7 +355,7 @@ class HLS: seg_uri = urljoin(variant_url, variant.segments[0].uri) - # Download only the first 8KB — SPS is always near the start of the first TS packet + # Download only the first 8KB: SPS is always near the start of the first TS packet res = session.get(seg_uri, headers={"Range": "bytes=0-8191"}) data = res.content diff --git a/unshackle/core/manifests/ism.py b/unshackle/core/manifests/ism.py index c8c8cade..91de896b 100644 --- a/unshackle/core/manifests/ism.py +++ b/unshackle/core/manifests/ism.py @@ -215,7 +215,7 @@ class ISM: iv_size=iv_size, ) except (NotImplementedError, ValueError, struct.error) as e: - # Unsupported codec, malformed CodecPrivateData or out-of-range field — + # Unsupported codec, malformed CodecPrivateData or out-of-range field: # fall back to raw concatenation rather than aborting the download. log_event( "manifest_ism_init_unsupported", diff --git a/unshackle/core/manifests/ism_init.py b/unshackle/core/manifests/ism_init.py index fd8e8df9..13fb1183 100644 --- a/unshackle/core/manifests/ism_init.py +++ b/unshackle/core/manifests/ism_init.py @@ -46,7 +46,7 @@ TRACK_IN_MOVIE = 0x2 TRACK_IN_PREVIEW = 0x4 SELF_CONTAINED = 0x1 -# Fixed creation/modification time — deterministic output (no wall clock). +# Fixed creation/modification time for deterministic output (no wall clock). EPOCH = 0 NAL_START_CODE = b"\x00\x00\x00\x01" @@ -96,7 +96,7 @@ def split_nal_units(codec_private_data: bytes) -> list[bytes]: def remove_emulation_prevention(data: bytes) -> bytes: """Strip H.26x emulation-prevention bytes (the 0x03 in any 00 00 03 run). - The byte after a consumed escape is data — even another 0x03 — so the scan + The byte after a consumed escape is data, even another 0x03, so the scan must skip past it rather than re-examine (a naive trailing-window check over-strips consecutive escapes and shifts every later bit position). """ @@ -611,10 +611,10 @@ def build_dec3(codec_private_data: bytes) -> Optional[bytes]: """Build a dec3 (EC-3 specific) box from Smooth EC-3 CodecPrivateData. Smooth EC-3 CodecPrivateData ([MS-SSTR] AudioTag 65534) serializes a - WAVEFORMATEXTENSIBLE — sometimes the full structure, sometimes only its - extension (samples-per-block + channel mask + DD+ SubFormat GUID) — with + WAVEFORMATEXTENSIBLE, sometimes the full structure, sometimes only its + extension (samples-per-block + channel mask + DD+ SubFormat GUID), with the raw dec3 payload (ETSI TS 102 366 F.6) after the GUID. Returns None - when the GUID is absent — decoders still sync from EC-3 frames in mdat. + when the GUID is absent; decoders still sync from EC-3 frames in mdat. """ guid_at = codec_private_data.find(DOLBY_DIGITAL_PLUS_GUID) if guid_at != -1 and len(codec_private_data) > guid_at + 16: diff --git a/unshackle/core/providers/__init__.py b/unshackle/core/providers/__init__.py index 61835a52..84762e8b 100644 --- a/unshackle/core/providers/__init__.py +++ b/unshackle/core/providers/__init__.py @@ -431,7 +431,7 @@ def validate_enrichments( tmdb_votes[provider_name] = ext.tmdb_id if len(set(tmdb_votes.values())) <= 1: - return enrichments # all agree or only one voted — no conflict + return enrichments # all agree or only one voted, no conflict value_counts = Counter(tmdb_votes.values()) most_common_val, most_common_count = value_counts.most_common(1)[0] @@ -439,7 +439,7 @@ def validate_enrichments( if most_common_count > 1: anchor_tmdb_id = most_common_val else: - # No majority — pick the most authoritative provider + # No majority, so pick the most authoritative provider best_provider = min( tmdb_votes.keys(), key=lambda name: authority.get(name, 99), diff --git a/unshackle/core/providers/tvdb.py b/unshackle/core/providers/tvdb.py index 592eb0a9..efa9bb25 100644 --- a/unshackle/core/providers/tvdb.py +++ b/unshackle/core/providers/tvdb.py @@ -206,7 +206,7 @@ class TVDBProvider(MetadataProvider): return cached episodes: list[dict] = [] - for page in range(20): # ponytail: hard page cap, 500 eps/page covers any real series + for page in range(20): data = self.api_get(f"/series/{tvdb_id}/episodes/{order}", {"page": page}) if data is None and page: # a partial listing would renumber episodes wrongly diff --git a/unshackle/core/proxies/proton.py b/unshackle/core/proxies/proton.py index c5e9d11a..834362a0 100644 --- a/unshackle/core/proxies/proton.py +++ b/unshackle/core/proxies/proton.py @@ -24,7 +24,7 @@ class ProtonVPN(Proxy): Proton VPN HTTPS proxy provider. Authenticates via TV login (a self-sustaining session cached at /vpn/protonvpn.json, the - only refreshable kind) or an exported account.proton.me cookie session (AUTH-, access only — + only refreshable kind) or an exported account.proton.me cookie session (AUTH-, access only, re-export on expiry). Resolves a server by country/city, mints short-lived proxy credentials, and returns an authenticated HTTPS proxy URL (https://user:pass@server:4443). diff --git a/unshackle/core/remote_service.py b/unshackle/core/remote_service.py index 450b2ac2..bd934bd2 100644 --- a/unshackle/core/remote_service.py +++ b/unshackle/core/remote_service.py @@ -736,7 +736,7 @@ class RemoteService: def create_drm_stub(drm_type: str, kid_hexes: list[str]) -> Any: """Create a DRM object stub matching the type the server actually used. - For server_cdm mode, this is only used for display — keys are already + For server_cdm mode, this is only used for display; keys are already resolved. We build a minimal DRM object that holds content_keys. """ from uuid import UUID diff --git a/unshackle/core/service_repo.py b/unshackle/core/service_repo.py index 6ec919ef..79be54b4 100644 --- a/unshackle/core/service_repo.py +++ b/unshackle/core/service_repo.py @@ -95,12 +95,12 @@ def resolve_service_repo(spec: str, *, ttl: int = DEFAULT_TTL, force: bool = Fal force_sync(dest, stamp) elif is_stale(stamp, ttl): pull(dest, stamp) - return dest # ponytail: git IS the cache layer — no bespoke fetch/integrity code + return dest def clone(url: str, branch: Optional[str], dest: Path, stamp: Path) -> Optional[Path]: if not binaries.Git: - log.error("git not found on PATH — cannot fetch service repo %s", url) + log.error("git not found on PATH; cannot fetch service repo %s", url) return None dest.parent.mkdir(parents=True, exist_ok=True) args = [str(binaries.Git), "clone", "--depth", "1"] @@ -122,7 +122,7 @@ def pull(dest: Path, stamp: Path) -> None: if not binaries.Git: write_stamp(stamp) return - # never clobber local edits — refuse to refresh a dirty clone and let the caller exit cleanly + # never clobber local edits: refuse to refresh a dirty clone and let the caller exit cleanly if is_dirty(dest): raise DirtyServiceRepo(dest) try: diff --git a/unshackle/core/session.py b/unshackle/core/session.py index 8faddb8d..bc5ce7cf 100644 --- a/unshackle/core/session.py +++ b/unshackle/core/session.py @@ -755,11 +755,11 @@ class RnetSession: return self.request(method, url, **send_kwargs) def mount(self, prefix: str, adapter: Any) -> None: - """No-op — rnet handles TLS and connection pooling natively.""" + """No-op; rnet handles TLS and connection pooling natively.""" pass def close(self) -> None: - """No-op — rnet manages its own resources.""" + """No-op; rnet manages its own resources.""" pass diff --git a/unshackle/core/tracks/dv_fixup.py b/unshackle/core/tracks/dv_fixup.py index f2a90add..e10fc4c7 100644 --- a/unshackle/core/tracks/dv_fixup.py +++ b/unshackle/core/tracks/dv_fixup.py @@ -6,7 +6,7 @@ hvc1, with DV advertised only via SUPPLEMENTAL-CODECS. The fMP4 carries valid DV but the container does not signal DV, so muxing the MP4 directly produces an MKV that mediainfo and DV-capable TVs see as plain HDR10/HDR10+. -The RPU is already valid — only the container's DV signaling is lost. Demuxing the +The RPU is already valid; only the container's DV signaling is lost. Demuxing the elementary HEVC stream (ffmpeg -c:v copy) exposes the in-stream RPU to mkvmerge, which then signals DV in the muxed MKV. No dovi_tool extract/inject round-trip is needed. """ diff --git a/unshackle/core/tracks/subtitle_convert.py b/unshackle/core/tracks/subtitle_convert.py index fca380d2..c02c1055 100644 --- a/unshackle/core/tracks/subtitle_convert.py +++ b/unshackle/core/tracks/subtitle_convert.py @@ -8,7 +8,7 @@ requested pair and orders them by rank; ``run_conversion`` tries each in turn (a fallback chain) until one succeeds. The public entry point stays ``Subtitle.convert`` / ``Subtitle.strip_hearing_impaired`` in -subtitle.py — this module only holds the selection + conversion logic so subtitle.py keeps +subtitle.py; this module only holds the selection + conversion logic so subtitle.py keeps the codec enum, ``parse``, sanitizers and cue helpers (the collaborators backends reuse). """ @@ -113,7 +113,7 @@ def subtitleedit_args( # Styled SubStation formats flattened to SRT lose positioning/colours/italics. -# Never performed automatically — only when the user explicitly forces a target format. +# Never performed automatically, only when the user explicitly forces a target format. LOSSY_DOWNCONVERTS: frozenset[tuple[Codec, Codec]] = frozenset( { (Codec.SubStationAlpha, Codec.SubRip), @@ -188,7 +188,7 @@ class SubtitleEditBackend: class Pysubs2Backend: - """pysubs2 — pure Python, broad format support, best fidelity for SSA/ASS (native style model).""" + """pysubs2: pure Python, broad format support, best fidelity for SSA/ASS (native style model).""" name = "pysubs2" formats = frozenset(PYSUBS2_FORMATS) @@ -209,7 +209,7 @@ class Pysubs2Backend: class SubbyBackend: - """subby — purpose-built for streaming subs. WebVTT/fVTT/SAMI -> SRT + CommonIssuesFixer cleanup.""" + """subby: purpose-built for streaming subs. WebVTT/fVTT/SAMI -> SRT + CommonIssuesFixer cleanup.""" name = "subby" reads = frozenset({Codec.WebVTT, Codec.fVTT, Codec.SAMI}) @@ -248,7 +248,7 @@ class SubbyBackend: class PycaptionBackend: - """pycaption — last resort. Note: flattens positioning/italics (devine #39), so ranked last.""" + """pycaption: last resort. Flattens positioning/italics (devine #39), so ranked last.""" name = "pycaption" reads = frozenset({Codec.SubRip, Codec.TimedTextMarkupLang, Codec.WebVTT, Codec.SAMI, Codec.fTTML, Codec.fVTT}) diff --git a/unshackle/core/tracks/track.py b/unshackle/core/tracks/track.py index 48ccd857..08b7726a 100644 --- a/unshackle/core/tracks/track.py +++ b/unshackle/core/tracks/track.py @@ -667,7 +667,7 @@ class Track: def base_kwargs_from_dict(data: dict[str, Any]) -> dict[str, Any]: """Build the shared Track constructor kwargs from a ``to_dict()`` payload. - DRM is not reconstructed here — ``to_dict`` does not serialise it, and the import + DRM is not reconstructed here: ``to_dict`` does not serialise it, and the import flow attaches the licensed DRM + content keys separately. """ return { diff --git a/unshackle/core/tracks/video.py b/unshackle/core/tracks/video.py index 0c297d97..0fbef2bc 100644 --- a/unshackle/core/tracks/video.py +++ b/unshackle/core/tracks/video.py @@ -433,7 +433,7 @@ class Video(Track): if not vui: return None - # Skip the rewrite when the bitstream VUI already matches the target — avoids a full + # Skip the rewrite when the bitstream VUI already matches the target, which avoids a full # bitstream pass on services that already ship correct colour metadata expected = { Video.Range.HDR10: ("bt2020", "smpte2084", "bt2020nc"), @@ -602,7 +602,7 @@ class Video(Track): does it actually call ccextractor. Even though there is a possibility of more than one c608 box, only one - can actually be extracted. Not only that but it's very possible this + can actually be extracted. It is also very possible this needs to be done before any decryption as the decryption may destroy some of the metadata. diff --git a/unshackle/core/utilities.py b/unshackle/core/utilities.py index b953e4ea..a3698d1a 100644 --- a/unshackle/core/utilities.py +++ b/unshackle/core/utilities.py @@ -1162,7 +1162,7 @@ def get_debug_logger() -> Optional[DebugLogger]: def log_event(operation: str, *, level: str = "DEBUG", message: str = "", **kwargs: Any) -> None: """Emit a single structured debug-log entry. No-op when debug logging is disabled. - The canonical one-shot logging primitive — replaces the + The canonical one-shot logging primitive. Replaces the ``if dl := get_debug_logger(): dl.log(...)`` guard boilerplate. To add logging to a new feature, call ``log_event("my_feature_event", message="...", context={...})``. """ diff --git a/unshackle/core/utils/bitrate.py b/unshackle/core/utils/bitrate.py index d725c40f..c85ad748 100644 --- a/unshackle/core/utils/bitrate.py +++ b/unshackle/core/utils/bitrate.py @@ -59,7 +59,7 @@ def measure_real_bitrate( Single-file tracks are measured exactly. Segmented tracks probe up to ``samples`` segments spread across the track and extrapolate; byte-range segments need no request. Returns bits/sec, or ``None`` if it cannot be - measured. Never raises — a probe failure must not abort a download. + measured. Never raises; a probe failure must not abort a download. """ from unshackle.core.tracks.track import Track @@ -74,7 +74,7 @@ def measure_real_bitrate( # Descriptor.URL: a single file. Some services parse a DASH # manifest then collapse each representation to its single BaseURL and # flip the descriptor to URL, leaving the manifest (and its duration) in - # track.data — recover the duration from there, else probe the file. + # track.data: recover the duration from there, else probe the file. segments = extract_url(track, session, log=log) if not segments: log.debug(f"{track.id}: cannot measure real bitrate (no known duration)") @@ -130,7 +130,7 @@ def apply_real_bitrates( declared-bitrate tracks per group are probed, in parallel. Each group is then extended downward: while the lowest probed bitrate in a group sits below the next unprobed track's declared bitrate (so that track could outrank a probed - one), the next track is probed too — until the probed set is safely above the + one), the next track is probed too, until the probed set is safely above the rest. Unprobed tracks keep their manifest-declared bitrate. """ groups: defaultdict[Hashable, list["Track"]] = defaultdict(list) @@ -182,7 +182,7 @@ def dedupe(segments: list[Segment]) -> list[Segment]: Collapse segments that address the same bytes so each object is measured once. Manifests sometimes wrap a single file in several segment entries sharing one - URL — with no byte range (a ``SegmentTemplate`` whose media pattern has no + URL, with no byte range (a ``SegmentTemplate`` whose media pattern has no ``$Number$``) or with the same range. Each resolves to the whole file, so counting them all would multiply the size by the segment count. Segments sharing the same ``(url, byte_range)`` are merged into one entry whose duration diff --git a/unshackle/core/utils/ip_info.py b/unshackle/core/utils/ip_info.py index 6feec23e..ea75d7ee 100644 --- a/unshackle/core/utils/ip_info.py +++ b/unshackle/core/utils/ip_info.py @@ -97,7 +97,7 @@ def lookup_session(source: Optional[requests.Session]) -> requests.Session: Build a plain, retry-free requests session for IP geolocation. Geolocation needs no TLS fingerprinting, so we skip the impersonated rnet - session and the base session's urllib3 retry loop — both retry 429 internally, + session and the base session's urllib3 retry loop. Both retry 429 internally, which hides the response and defeats fast provider handover. With a bare session a 429 comes straight back so we can move to the next provider immediately. Only the proxy is carried over so proxied lookups still report the proxy's exit IP. @@ -139,7 +139,7 @@ def fetch_ipinfo(session: requests.Session) -> Optional[dict]: def fetch_ip_api_in(session: requests.Session) -> Optional[dict]: - """ip-api.in has no /me endpoint — resolve IP via ipify first, then look it up.""" + """ip-api.in has no /me endpoint, so resolve IP via ipify first, then look it up.""" ip_resp = session.get("https://api.ipify.org", timeout=REQUEST_TIMEOUT) if ip_resp.status_code == 429: raise RateLimited() @@ -198,7 +198,7 @@ def get_ip_info( returned info reflects the proxy's exit IP. Auth headers for ipinfo are sent per-request; never mutated onto session.headers. cached: When True, read/write a 24h Cacher-backed entry. Use only for - local IP lookups — never with a proxied session. + local IP lookups, never with a proxied session. """ cache = None if cached: diff --git a/unshackle/services/MUSIC_EXAMPLE/__init__.py b/unshackle/services/MUSIC_EXAMPLE/__init__.py index b4629d15..fcd1f844 100644 --- a/unshackle/services/MUSIC_EXAMPLE/__init__.py +++ b/unshackle/services/MUSIC_EXAMPLE/__init__.py @@ -75,7 +75,7 @@ class MUSIC_EXAMPLE(Service): # GEOFENCE: regions required; the framework warns/blocks if proxy region mismatches. GEOFENCE = ("US", "UK") # TITLE_RE: named groups (?P<...>) parsed in get_titles(). Accepts URL or bare id. - # A music catalogue is addressed by several kinds of thing, not just one, so `kind` + # A music catalogue is addressed by several kinds of thing, so `kind` # is part of the identifier and decides which endpoint get_titles() asks. TITLE_RE = ( r"^(?:https?://(?:www\.)?musicdomain\.com/)?(?Ptrack|album|playlist|artist)/(?P[^/?#]+)" diff --git a/unshackle/unshackle-example.yaml b/unshackle/unshackle-example.yaml index 92ae015f..b8c9a1b6 100644 --- a/unshackle/unshackle-example.yaml +++ b/unshackle/unshackle-example.yaml @@ -3,7 +3,7 @@ # This is a MINIMAL starter. Every option has a sane default, so unshackle runs # even with most of this file empty or removed. # -# The full, canonical configuration reference lives in docs/ — start at +# The full, canonical configuration reference lives in docs/. Start at # docs/README.md. Each section below points to the doc that documents it. # Group or username appended to download filenames after a dash.