From 9567540e598e859fc3bd6f0d6edcaffc52d8f186 Mon Sep 17 00:00:00 2001 From: VineFeeder Date: Wed, 1 Jul 2026 09:12:10 +0100 Subject: [PATCH] upstream updates --- packages/envied/pyproject.toml | 2 +- packages/envied/src/envied/CHANGELOG.md | 34 ++++++++++++ packages/envied/src/envied/commands/dl.py | 52 +++++-------------- packages/envied/src/envied/commands/import.py | 4 +- packages/envied/src/envied/commands/serve.py | 2 +- packages/envied/src/envied/core/__init__.py | 2 +- packages/envied/src/envied/core/__main__.py | 5 +- .../src/envied/core/cdm/custom_remote_cdm.py | 2 +- .../core/cdm/decrypt_labs_remote_cdm.py | 2 +- .../envied/core/cdm/monalisa/monalisa_cdm.py | 2 +- packages/envied/src/envied/core/config.py | 14 ++--- .../envied/src/envied/core/drm/widevine.py | 2 +- .../envied/src/envied/core/providers/_base.py | 2 +- .../envied/src/envied/core/proxies/gluetun.py | 14 ++--- .../envied/src/envied/core/remote_service.py | 10 ++-- .../envied/src/envied/core/tracks/subtitle.py | 9 +--- .../envied/src/envied/core/tracks/tracks.py | 26 ++++++++++ .../src/envied/services/CRAV/__init__.py | 2 +- .../src/envied/services/MGMP/config.yaml | 2 +- .../envied/src/envied/services/VM/__init__.py | 10 ++-- packages/envied/src/envied/vaults/API.py | 2 +- packages/envied/src/envied/vaults/HTTP.py | 2 +- packages/envied/src/unshackle/__init__.py | 2 +- packages/vinefeeder/src/vinefeeder/pretty.py | 2 +- .../src/vinefeeder/services/TVNZ/__init__.py | 2 +- uv.lock | 2 +- vaults/API.py | 2 +- vaults/HTTP.py | 2 +- 28 files changed, 122 insertions(+), 92 deletions(-) diff --git a/packages/envied/pyproject.toml b/packages/envied/pyproject.toml index 6cee6da..8a4f384 100644 --- a/packages/envied/pyproject.toml +++ b/packages/envied/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "uv_build" [project] name = "envied" -version = "5.1.0" +version = "5.3.0" description = "Modular Movie, TV, and Music Archival Software." authors = [{ name = "rlaphoenix and others" }] requires-python = ">=3.10,<=3.14" diff --git a/packages/envied/src/envied/CHANGELOG.md b/packages/envied/src/envied/CHANGELOG.md index 954da95..8a1bbce 100644 --- a/packages/envied/src/envied/CHANGELOG.md +++ b/packages/envied/src/envied/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this project will be documented in this file. + +## 5.3.0 01-07-2026 + +Repository: unshackle-dl/unshackle · Tag: 5.3.0 · Commit: d4a5826 · Released by: imSp4rky +Features + + --merge-video: merge video language variants into one MKV per (resolution, range, codec) group + Native ExpressVPN HTTPS proxy provider (full OAuth PKCE flow, no external tools) + Proton VPN proxy provider with country/city/server-number selection, plus ProtonVPN TV-code login + Load service plugins from git repos (clone + TTL-pull cache, refresh-services command, dirty-clone guard) + Python 3.14 support (supported range widened to 3.11-3.14) + Automatic Firefox cookie and localStorage extraction + Nested directories in folder templates (/ treated as path separator) + air_date for date-based episode naming (daily/sports content named by date instead of SxxExx) + Per-vault network timeout for remote API/HTTP vaults + Richer API job progress: active-track segment counts, transfer speed, title description/date/cover_url + +Bug Fixes + + drm: don't switch to a mismatched CDM type during licensing; raise a clear error instead + config: match per-service cdm keys case-insensitively + dl: apply per-service decryption tool override before decrypt + hls: stream segment merges to avoid OOM on large (multi-GB) tracks + console: pause active live/spinner contexts during terminal input so prompts stay visible + output: treat both / and \ as folder separators on any OS + env: only resolve Path items in directory lists (git repo specs pass through) + +Performance & Changes + + perf(hls): rename single decrypted range instead of re-copying a full track + perf(tracks): skip redundant DV/VUI bitstream passes when VUI already matches range + api: collapse route registration, dedup handlers, remove dead serve/remote code paths + + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/packages/envied/src/envied/commands/dl.py b/packages/envied/src/envied/commands/dl.py index dae48b7..a0342ec 100644 --- a/packages/envied/src/envied/commands/dl.py +++ b/packages/envied/src/envied/commands/dl.py @@ -514,7 +514,7 @@ class dl: type=str, default=None, hidden=True, - help="Internal: path to an export JSON to reconstruct a download from (used by 'unshackle import').", + help="Internal: path to an export JSON to reconstruct a download from (used by 'envied.import').", ) @click.option( "--cdm-only/--vaults-only", @@ -571,7 +571,7 @@ class dl: is_flag=True, default=False, is_eager=True, - help="Use a remote unshackle server instead of local service code.", + help="Use a remote envied.server instead of local service code.", ) @click.option( "--server", @@ -614,7 +614,7 @@ class dl: raise click.ClickException( "No 'output_template' configured in your envied.yaml.\n" "Please add an 'output_template' section with movies, series, and songs templates.\n" - "See unshackle-example.yaml for examples." + "See envied.example.yaml for examples." ) self.service = Services.get_tag(ctx.invoked_subcommand) @@ -1697,24 +1697,12 @@ class dl: has_hybrid = any(r == Video.Range.HYBRID for r in range_) non_hybrid_ranges = [r for r in range_ if r != Video.Range.HYBRID] - # DV is both a hybrid ingredient (lowest track) and, when explicitly - # requested, a standalone deliverable (best track per resolution). - dv_is_deliverable = Video.Range.DV in non_hybrid_ranges - if quality: missing_resolutions = [] if has_hybrid: - hybrid_candidate_tracks = [ - v - for v in title.tracks.videos - if v.range in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - ] - non_hybrid_tracks = [ - v - for v in title.tracks.videos - if v.range not in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - or (dv_is_deliverable and v.range == Video.Range.DV) - ] + hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos( + title.tracks.videos, non_hybrid_ranges + ) hybrid_filter = title.tracks.select_hybrid(hybrid_candidate_tracks, quality, worst=worst) hybrid_selected = list(filter(hybrid_filter, hybrid_candidate_tracks)) @@ -1757,17 +1745,9 @@ class dl: pre_hybrid_videos: list[Video] = list(title.tracks.videos) if has_hybrid else [] if has_hybrid: # Apply hybrid selection for HYBRID tracks - hybrid_candidate_tracks = [ - v - for v in title.tracks.videos - if v.range in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - ] - non_hybrid_tracks = [ - v - for v in title.tracks.videos - if v.range not in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - or (dv_is_deliverable and v.range == Video.Range.DV) - ] + hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos( + title.tracks.videos, non_hybrid_ranges + ) if not quality: best_resolution = max((v.height for v in hybrid_candidate_tracks), default=None) @@ -1811,14 +1791,10 @@ class dl: title.tracks.videos = Tracks.merge_video_selections(hybrid_selected, non_hybrid_selected) - # Flag the lowest DV track as ingredient-only so mux skips it standalone, - # unless it is itself the chosen DV deliverable (single DV rendition). - selected_dv = [v for v in title.tracks.videos if v.range == Video.Range.DV] - if selected_dv: - ingredient_dv = min(selected_dv, key=lambda v: v.height) - deliverable_dv = [v for v in non_hybrid_selected if v.range == Video.Range.DV] - if not (dv_is_deliverable and ingredient_dv in deliverable_dv): - ingredient_dv.hybrid_base_only = True + # Flag tracks selected only as hybrid ingredients (the HDR base and/or + # the lowest DV) so the standalone mux loop skips them. Tracks also + # picked as explicit deliverables stay unflagged. + Tracks.flag_hybrid_ingredients(hybrid_selected, non_hybrid_selected) else: selected_videos: list[Video] = [] if video_multi_lang: @@ -2792,7 +2768,7 @@ class dl: return meta def write_export(self, export: Path, title: Title_T, track: AnyTrack, drm: Any) -> None: - """Write a shareable v2 export usable by ``unshackle import``. + """Write a shareable v2 export usable by ``envied.import``. Carries no session/cookies/dl-flags. Region (country code) is stored only when the export used ``--proxy``, as an import geofence. Each track records only the licensed diff --git a/packages/envied/src/envied/commands/import.py b/packages/envied/src/envied/commands/import.py index b6a58d3..98b65a7 100644 --- a/packages/envied/src/envied/commands/import.py +++ b/packages/envied/src/envied/commands/import.py @@ -27,7 +27,7 @@ class ImportCommand: Re-fetches the manifest, injects the stored keys, then downloads/decrypts/muxes as a normal `dl` run. Any `dl` options after the file are forwarded verbatim: - unshackle import export.json -r HDR10 --proxy US + envied.import export.json -r HDR10 --proxy US """ if not export_file.is_file(): raise click.ClickException(f"Export file not found: {export_file}") @@ -48,7 +48,7 @@ class ImportCommand: raise click.ClickException("Export file is missing the 'service' tag.") args = [*dl_args, "--import", str(export_file), service_tag] - dl.cli.main(args=args, prog_name="unshackle dl", standalone_mode=False) + dl.cli.main(args=args, prog_name="envied.dl", standalone_mode=False) globals()["import"] = ImportCommand diff --git a/packages/envied/src/envied/commands/serve.py b/packages/envied/src/envied/commands/serve.py index b41ee49..6ca5b0d 100644 --- a/packages/envied/src/envied/commands/serve.py +++ b/packages/envied/src/envied/commands/serve.py @@ -65,7 +65,7 @@ def serve( \b You may serve with Caddy at the same time with --caddy. You can use Caddy as a reverse-proxy to serve with HTTPS. The config used will be the Caddyfile - next to the unshackle config. + next to the envied.config. \b DEVICE CONFIGURATION: diff --git a/packages/envied/src/envied/core/__init__.py b/packages/envied/src/envied/core/__init__.py index 0d72820..f575288 100644 --- a/packages/envied/src/envied/core/__init__.py +++ b/packages/envied/src/envied/core/__init__.py @@ -1 +1 @@ -__version__ = "5.1.0" +__version__ = "5.3.0" diff --git a/packages/envied/src/envied/core/__main__.py b/packages/envied/src/envied/core/__main__.py index cef083d..494e007 100644 --- a/packages/envied/src/envied/core/__main__.py +++ b/packages/envied/src/envied/core/__main__.py @@ -23,7 +23,7 @@ from envied.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.""" + """envied.Modular Movie, TV, and Music Archival Software.""" debug_logging_enabled = debug or config.debug logging.basicConfig( @@ -57,7 +57,7 @@ def main(version: bool, debug: bool) -> None: r"░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░▀▀░" + "\n" , style="ascii.art", ), - Text(" and more than unshackled...", style = "ascii.art"), + Text(" and more than envied....", style = "ascii.art"), f"\nv [repr.number]{__version__}[/] - https://github.com/vinefeeder/envied", ), (1, 11, 1, 10), @@ -66,6 +66,7 @@ def main(version: bool, debug: bool) -> None: justify="center", ) + @atexit.register diff --git a/packages/envied/src/envied/core/cdm/custom_remote_cdm.py b/packages/envied/src/envied/core/cdm/custom_remote_cdm.py index b2bbe45..be3c902 100644 --- a/packages/envied/src/envied/core/cdm/custom_remote_cdm.py +++ b/packages/envied/src/envied/core/cdm/custom_remote_cdm.py @@ -219,7 +219,7 @@ class CustomRemoteCDM: # HTTP session setup self._http_session = Session() self._http_session.headers.update( - {"Content-Type": "application/json", "User-Agent": f"unshackle-custom-cdm/{__version__}"} + {"Content-Type": "application/json", "User-Agent": f"envied.custom-cdm/{__version__}"} ) # Apply custom headers from auth config diff --git a/packages/envied/src/envied/core/cdm/decrypt_labs_remote_cdm.py b/packages/envied/src/envied/core/cdm/decrypt_labs_remote_cdm.py index 230a116..ac8cdbc 100644 --- a/packages/envied/src/envied/core/cdm/decrypt_labs_remote_cdm.py +++ b/packages/envied/src/envied/core/cdm/decrypt_labs_remote_cdm.py @@ -151,7 +151,7 @@ class DecryptLabsRemoteCDM: { "decrypt-labs-api-key": self.secret, "Content-Type": "application/json", - "User-Agent": f"unshackle-decrypt-labs-cdm/{__version__}", + "User-Agent": f"envied.decrypt-labs-cdm/{__version__}", } ) diff --git a/packages/envied/src/envied/core/cdm/monalisa/monalisa_cdm.py b/packages/envied/src/envied/core/cdm/monalisa/monalisa_cdm.py index eeeb78e..7b885a3 100644 --- a/packages/envied/src/envied/core/cdm/monalisa/monalisa_cdm.py +++ b/packages/envied/src/envied/core/cdm/monalisa/monalisa_cdm.py @@ -95,7 +95,7 @@ class MonaLisaCDM: @staticmethod def get_worker_path() -> Optional[Path]: - """Get ML-Worker binary path from the unshackle binaries system.""" + """Get ML-Worker binary path from the envied.binaries system.""" if binaries.ML_Worker: return Path(binaries.ML_Worker) return None diff --git a/packages/envied/src/envied/core/config.py b/packages/envied/src/envied/core/config.py index a02814a..7309f9c 100644 --- a/packages/envied/src/envied/core/config.py +++ b/packages/envied/src/envied/core/config.py @@ -12,7 +12,7 @@ from appdirs import AppDirs class Config: class _Directories: # default directories, do not modify here, set via config - app_dirs = AppDirs("unshackle", False) + app_dirs = AppDirs("envied", False) core_dir = Path(__file__).resolve().parent namespace_dir = core_dir.parent commands = namespace_dir / "commands" @@ -33,8 +33,8 @@ class Config: class _Filenames: # default filenames, do not modify here, set via config - log = "unshackle_{name}_{time}.log" # Directories.logs - debug_log = "unshackle_debug_{service}_{time}.jsonl" # Directories.logs + log = "envied.{name}_{time}.log" # Directories.logs + debug_log = "envied.debug_{service}_{time}.jsonl" # Directories.logs config = "config.yaml" # Directories.services / tag root_config = "envied.yaml" # Directories.user_configs chapters = "Chapters_{title}_{random}.txt" # Directories.temp @@ -112,7 +112,7 @@ class Config: raise SystemExit( "ERROR: The 'scene_naming' option has been removed.\n" "Please configure 'output_template' in your envied.yaml instead.\n" - "See unshackle-example.yaml for examples." + "See envied.example.yaml for examples." ) if self.output_template: @@ -231,11 +231,11 @@ class Config: # noinspection PyProtectedMember POSSIBLE_CONFIG_PATHS = ( - # The unshackle Namespace Folder (e.g., %appdata%/Python/Python311/site-packages/unshackle) + # The envied.Namespace Folder (e.g., %appdata%/Python/Python311/site-packages/envied. Config._Directories.namespace_dir / Config._Filenames.root_config, - # The Parent Folder to the unshackle Namespace Folder (e.g., %appdata%/Python/Python311/site-packages) + # The Parent Folder to the envied.Namespace Folder (e.g., %appdata%/Python/Python311/site-packages) Config._Directories.namespace_dir.parent / Config._Filenames.root_config, - # The AppDirs User Config Folder (e.g., ~/.config/unshackle on Linux, %LOCALAPPDATA%\unshackle on Windows) + # The AppDirs User Config Folder (e.g., ~/.config/envied.on Linux, %LOCALAPPDATA%\envied.on Windows) Path(Config._Directories.app_dirs.user_config_dir) / Config._Filenames.root_config, ) diff --git a/packages/envied/src/envied/core/drm/widevine.py b/packages/envied/src/envied/core/drm/widevine.py index 008e9d9..f4d7a82 100644 --- a/packages/envied/src/envied/core/drm/widevine.py +++ b/packages/envied/src/envied/core/drm/widevine.py @@ -348,7 +348,7 @@ class Widevine: try: arguments = [ - f"input={path},stream=0,output={output_path},output_format=mp4", + f"input={path},stream=0,output={output_path},output_format=MP4", "--enable_raw_key_decryption", "--keys", ",".join( diff --git a/packages/envied/src/envied/core/providers/_base.py b/packages/envied/src/envied/core/providers/_base.py index 6222786..431b551 100644 --- a/packages/envied/src/envied/core/providers/_base.py +++ b/packages/envied/src/envied/core/providers/_base.py @@ -12,7 +12,7 @@ from requests.adapters import HTTPAdapter, Retry log = logging.getLogger("METADATA") -HEADERS = {"User-Agent": "unshackle-tags/1.0"} +HEADERS = {"User-Agent": "envied.tags/1.0"} STRIP_RE = re.compile(r"[^a-z0-9]+", re.I) YEAR_RE = re.compile(r"\s*\(?[12][0-9]{3}\)?$") diff --git a/packages/envied/src/envied/core/proxies/gluetun.py b/packages/envied/src/envied/core/proxies/gluetun.py index f121cc6..f5331ce 100644 --- a/packages/envied/src/envied/core/proxies/gluetun.py +++ b/packages/envied/src/envied/core/proxies/gluetun.py @@ -81,7 +81,7 @@ class Gluetun(Proxy): # Global settings (optional) base_port: 8888 auto_cleanup: true - container_prefix: "unshackle-gluetun" + container_prefix: "envied.gluetun" Usage: --proxy gluetun:windscribe:us @@ -166,7 +166,7 @@ class Gluetun(Proxy): providers: Optional[dict] = None, base_port: int = 8888, auto_cleanup: bool = True, - container_prefix: str = "unshackle-gluetun", + container_prefix: str = "envied.gluetun", auth_user: Optional[str] = None, auth_password: Optional[str] = None, verify_ip: bool = True, @@ -187,7 +187,7 @@ class Gluetun(Proxy): } base_port: Starting port for HTTP proxies (default: 8888) auto_cleanup: Automatically remove stopped containers (default: True) - container_prefix: Docker container name prefix (default: "unshackle-gluetun") + container_prefix: Docker container name prefix (default: "envied.gluetun") auth_user: Optional HTTP proxy authentication username auth_password: Optional HTTP proxy authentication password verify_ip: Automatically verify IP and region after connection (default: True) @@ -272,7 +272,7 @@ class Gluetun(Proxy): debug_logger = get_debug_logger() # Check if container already exists (in memory OR in Docker) - # This handles multiple concurrent Unshackle sessions + # This handles multiple concurrent envied.sessions if query_key in self.active_containers: container = self.active_containers[query_key] if self._is_container_running(container["container_name"]): @@ -778,7 +778,7 @@ class Gluetun(Proxy): # Avoid exposing credentials in process listings by using --env-file instead of many "-e KEY=VALUE". env_file_path: str | None = None try: - fd, env_file_path = tempfile.mkstemp(prefix=f"unshackle-{container_name}-", suffix=".env") + fd, env_file_path = tempfile.mkstemp(prefix=f"envied.{container_name}-", suffix=".env") try: # Best-effort restrictive permissions. if os.name != "nt": @@ -921,7 +921,7 @@ class Gluetun(Proxy): """ Check if a container exists in Docker and get its info. - This handles multiple Unshackle sessions - if another session already + This handles multiple envied.sessions - if another session already created the container, we'll reuse it instead of trying to create a duplicate. Args: @@ -955,7 +955,7 @@ class Gluetun(Proxy): port = int(port_match.group(1)) # Extract provider and region from container name - # Format: unshackle-gluetun-provider-region + # Format: envied.gluetun-provider-region name_pattern = f"{self.container_prefix}-(.+)-([^-]+)$" name_match = re.match(name_pattern, container_name) if not name_match: diff --git a/packages/envied/src/envied/core/remote_service.py b/packages/envied/src/envied/core/remote_service.py index e754f1a..e53ad8b 100644 --- a/packages/envied/src/envied/core/remote_service.py +++ b/packages/envied/src/envied/core/remote_service.py @@ -1,7 +1,7 @@ """Remote service adapter for envied. Implements the Service interface by proxying authenticate, get_titles, -get_tracks, get_chapters, and license methods to a remote unshackle server. +get_tracks, get_chapters, and license methods to a remote envied.server. Everything else (track selection, download, decrypt, mux) runs locally. """ @@ -36,7 +36,7 @@ log = logging.getLogger("remote_service") class RemoteClient: - """HTTP client for the unshackle serve API.""" + """HTTP client for the envied.serve API.""" def __init__(self, server_url: str, api_key: str) -> None: self.server_url = server_url.rstrip("/") @@ -49,7 +49,7 @@ class RemoteClient: from envied.core import __version__ self._session = requests.Session() - self._session.headers["User-Agent"] = f"unshackle/{__version__}" + self._session.headers["User-Agent"] = f"envied.{__version__}" if self.api_key: self._session.headers["X-Secret-Key"] = self.api_key return self._session @@ -59,7 +59,7 @@ class RemoteClient: try: resp = getattr(self.session, method)(url, json=data, timeout=120 if method == "post" else 30) except requests.ConnectionError: - log.error(f"Could not connect to remote server at {self.server_url}. Is it running? (unshackle serve)") + log.error(f"Could not connect to remote server at {self.server_url}. Is it running? (envied.serve)") raise SystemExit(1) except requests.Timeout: log.error(f"Request to remote server timed out: {endpoint}") @@ -366,7 +366,7 @@ def _resolve_proxy(proxy_arg: Optional[str]) -> Optional[str]: class RemoteService: - """Service adapter that proxies to a remote unshackle server. + """Service adapter that proxies to a remote envied.server. Implements the same interface dl.py's result() expects without subclassing Service (avoids proxy/geofence setup in __init__). diff --git a/packages/envied/src/envied/core/tracks/subtitle.py b/packages/envied/src/envied/core/tracks/subtitle.py index 4b625a6..7135692 100644 --- a/packages/envied/src/envied/core/tracks/subtitle.py +++ b/packages/envied/src/envied/core/tracks/subtitle.py @@ -861,20 +861,13 @@ class Subtitle(Track): Subtitle.Codec.SAMI: "sami", Subtitle.Codec.MicroDVD: "microdvd", }.get(codec, codec.name.lower()) - '''sub_edit_args = [ + sub_edit_args = [ str(binaries.SubtitleEdit), "/convert", str(self.path), sub_edit_format, f"/outputfilename:{output_path.name}", "/encoding:utf8", - ]''' - ## Angela edited this to remove the /convert argument because it was causing issues with SubtitleEdit - sub_edit_args = [ - binaries.SubtitleEdit, - self.path, sub_edit_format, - f"/outputfilename:{output_path.name}", - "/encoding:utf8" ] if codec == Subtitle.Codec.SubRip: sub_edit_args.append("/ConvertColorsToDialog") diff --git a/packages/envied/src/envied/core/tracks/tracks.py b/packages/envied/src/envied/core/tracks/tracks.py index f14d47c..bbc0003 100644 --- a/packages/envied/src/envied/core/tracks/tracks.py +++ b/packages/envied/src/envied/core/tracks/tracks.py @@ -339,6 +339,32 @@ class Tracks: merged.append(video) return merged + @staticmethod + def partition_hybrid_videos( + videos: list[Video], non_hybrid_ranges: list[Video.Range] + ) -> tuple[list[Video], list[Video]]: + """Split videos into hybrid-ingredient candidates and the standalone-deliverable pool. + + HDR10/HDR10+/DV tracks are hybrid ingredients; they only enter the standalone + pool when their range was explicitly requested alongside HYBRID, so e.g. + `-r HYBRID` muxes only the hybrid while `-r HYBRID,HDR10P` also delivers HDR10+. + """ + ingredient_ranges = (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) + hybrid_candidates = [v for v in videos if v.range in ingredient_ranges] + non_hybrid = [v for v in videos if v.range not in ingredient_ranges or v.range in non_hybrid_ranges] + return hybrid_candidates, non_hybrid + + @staticmethod + def flag_hybrid_ingredients(hybrid_selected: list[Video], non_hybrid_selected: list[Video]) -> None: + """Mark tracks selected only as hybrid ingredients so the standalone mux loop skips them. + + A track that was also picked as an explicit deliverable (same track in both + selections) stays unflagged and is muxed standalone alongside the hybrid. + """ + for video in hybrid_selected: + if video not in non_hybrid_selected: + video.hybrid_base_only = True + def select_hybrid(self, tracks, quality, worst: bool = False): # Prefer HDR10+ over HDR10 as the base layer (preserves dynamic metadata) base_ranges = (Video.Range.HDR10P, Video.Range.HDR10) diff --git a/packages/envied/src/envied/services/CRAV/__init__.py b/packages/envied/src/envied/services/CRAV/__init__.py index 3bec33c..bd74410 100644 --- a/packages/envied/src/envied/services/CRAV/__init__.py +++ b/packages/envied/src/envied/services/CRAV/__init__.py @@ -42,7 +42,7 @@ class CRAV(Service): \b Notes: - Subtitles are not always consistent, so both external (VTT) and internal (WVTT) subtitles are added. - If you experience incomplete subtitles, try using a newer version of Unshackle (v5.0.0+). + If you experience incomplete subtitles, try using a newer version of envied.(v5.0.0+). - Authentication will look for master profile first, then first profile with adult maturity scope. - Account pins are currently not supported. """ diff --git a/packages/envied/src/envied/services/MGMP/config.yaml b/packages/envied/src/envied/services/MGMP/config.yaml index 4184c85..3e3d949 100644 --- a/packages/envied/src/envied/services/MGMP/config.yaml +++ b/packages/envied/src/envied/services/MGMP/config.yaml @@ -6,7 +6,7 @@ endpoints: headers: Accept: application/json - User-Agent: okhttp/5.1.0 + User-Agent: okhttp/5..0 client: apikey: 592c205299de839aa3948643addfe0bc diff --git a/packages/envied/src/envied/services/VM/__init__.py b/packages/envied/src/envied/services/VM/__init__.py index 403e2bf..9343ba1 100644 --- a/packages/envied/src/envied/services/VM/__init__.py +++ b/packages/envied/src/envied/services/VM/__init__.py @@ -9,11 +9,11 @@ from urllib.parse import urljoin, urlparse import click from requests import Request -from unshackle.core.manifests import DASH -from unshackle.core.search_result import SearchResult -from unshackle.core.service import Service -from unshackle.core.titles import Episode, Series, Title_T, Titles_T -from unshackle.core.tracks import Chapters, Tracks +from envied.core.manifests import DASH +from envied.core.search_result import SearchResult +from envied.core.service import Service +from envied.core.titles import Episode, Series, Title_T, Titles_T +from envied.core.tracks import Chapters, Tracks class VM(Service): diff --git a/packages/envied/src/envied/vaults/API.py b/packages/envied/src/envied/vaults/API.py index 438b0c5..75bc699 100644 --- a/packages/envied/src/envied/vaults/API.py +++ b/packages/envied/src/envied/vaults/API.py @@ -14,7 +14,7 @@ class API(Vault): super().__init__(name, no_push) self.uri = uri.rstrip("/") self.session = Session() - self.session.headers.update({"User-Agent": f"unshackle v{__version__}"}) + self.session.headers.update({"User-Agent": f"envied.v{__version__}"}) self.session.headers.update({"Authorization": f"Bearer {token}"}) def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]: diff --git a/packages/envied/src/envied/vaults/HTTP.py b/packages/envied/src/envied/vaults/HTTP.py index 6c38079..cd09b8f 100644 --- a/packages/envied/src/envied/vaults/HTTP.py +++ b/packages/envied/src/envied/vaults/HTTP.py @@ -57,7 +57,7 @@ class HTTP(Vault): self.api_mode = api_mode.lower() self.current_title = None self.session = Session() - self.session.headers.update({"User-Agent": f"unshackle v{__version__}"}) + self.session.headers.update({"User-Agent": f"envied.v{__version__}"}) self.api_session_id = None if self.api_mode == "decrypt_labs": diff --git a/packages/envied/src/unshackle/__init__.py b/packages/envied/src/unshackle/__init__.py index d99c9de..208bf3e 100644 --- a/packages/envied/src/unshackle/__init__.py +++ b/packages/envied/src/unshackle/__init__.py @@ -1,5 +1,5 @@ """ -Compatibility shim for precompiled services expecting `unshackle`. +Compatibility shim for precompiled services expecting `envied.. """ from envied import * # noqa: F401,F403 \ No newline at end of file diff --git a/packages/vinefeeder/src/vinefeeder/pretty.py b/packages/vinefeeder/src/vinefeeder/pretty.py index 6b03b4e..a5fe379 100644 --- a/packages/vinefeeder/src/vinefeeder/pretty.py +++ b/packages/vinefeeder/src/vinefeeder/pretty.py @@ -39,7 +39,7 @@ def pretty_print(): f"[{catppuccin_mocha['pink']}]░█▀▀░█▀█░█░█░▀█▀░█▀▀░█▀▄" + "\n" f"[{catppuccin_mocha['pink']}]░█▀▀░█░█░▀▄▀░░█░░█▀▀░█░█" + "\n" f"[{catppuccin_mocha['pink']}]░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░▀▀░" + "\n\n" - f"[{catppuccin_mocha['pink']}]It's more than unshackled...[/]" + "\n" + f"[{catppuccin_mocha['pink']}]It's more than envied....[/]" + "\n" diff --git a/packages/vinefeeder/src/vinefeeder/services/TVNZ/__init__.py b/packages/vinefeeder/src/vinefeeder/services/TVNZ/__init__.py index b4904d3..c9eea76 100644 --- a/packages/vinefeeder/src/vinefeeder/services/TVNZ/__init__.py +++ b/packages/vinefeeder/src/vinefeeder/services/TVNZ/__init__.py @@ -7,7 +7,7 @@ from typing import Any from urllib.parse import urlparse import click -from unshackle.core import service +from envied.core import service import yaml from beaupy import select_multiple from rich.console import Console diff --git a/uv.lock b/uv.lock index 846680f..19c18f9 100644 --- a/uv.lock +++ b/uv.lock @@ -690,7 +690,7 @@ wheels = [ [[package]] name = "envied" -version = "5.1.0" +version = "5.3.0" source = { editable = "packages/envied" } dependencies = [ { name = "aiohttp" }, diff --git a/vaults/API.py b/vaults/API.py index 5049c1d..8145ffa 100644 --- a/vaults/API.py +++ b/vaults/API.py @@ -14,7 +14,7 @@ class API(Vault): super().__init__(name, no_push) self.uri = uri.rstrip("/") self.session = Session() - self.session.headers.update({"User-Agent": f"unshackle v{__version__}"}) + self.session.headers.update({"User-Agent": f"envied.v{__version__}"}) self.session.headers.update({"Authorization": f"Bearer {token}"}) def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]: diff --git a/vaults/HTTP.py b/vaults/HTTP.py index 6c38079..cd09b8f 100644 --- a/vaults/HTTP.py +++ b/vaults/HTTP.py @@ -57,7 +57,7 @@ class HTTP(Vault): self.api_mode = api_mode.lower() self.current_title = None self.session = Session() - self.session.headers.update({"User-Agent": f"unshackle v{__version__}"}) + self.session.headers.update({"User-Agent": f"envied.v{__version__}"}) self.api_session_id = None if self.api_mode == "decrypt_labs":