mirror of
https://github.com/vinefeeder/TwinVine.git
synced 2026-07-15 18:10:04 +02:00
upstream updates
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "5.1.0"
|
||||
__version__ = "5.3.0"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}\)?$")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__).
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,7 @@ endpoints:
|
||||
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-Agent: okhttp/5.1.0
|
||||
User-Agent: okhttp/5..0
|
||||
|
||||
client:
|
||||
apikey: 592c205299de839aa3948643addfe0bc
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Compatibility shim for precompiled services expecting `unshackle`.
|
||||
Compatibility shim for precompiled services expecting `envied..
|
||||
"""
|
||||
|
||||
from envied import * # noqa: F401,F403
|
||||
Reference in New Issue
Block a user