diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index a3de0c4..a83890c 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -1137,7 +1137,10 @@ class dl: }, ) - with console.status("Authenticating with Remote Service..." if self.is_remote else "Authenticating with Service...", spinner="dots"): + with console.status( + "Authenticating with Remote Service..." if self.is_remote else "Authenticating with Service...", + spinner="dots", + ): try: cookies = self.get_cookie_jar(self.service, self.profile) credential = self.get_credentials(self.service, self.profile) @@ -1162,7 +1165,9 @@ class dl: ) raise - with console.status("Fetching Remote Title Metadata..." if self.is_remote else "Fetching Title Metadata...", spinner="dots"): + with console.status( + "Fetching Remote Title Metadata..." if self.is_remote else "Fetching Title Metadata...", spinner="dots" + ): try: titles = service.get_titles_cached() if not titles: @@ -1513,9 +1518,9 @@ class dl: title.tracks.add(non_sdh_sub) events.subscribe( events.Types.TRACK_MULTIPLEX, - lambda track, sub_id=non_sdh_sub.id: (track.strip_hearing_impaired()) - if track.id == sub_id - else None, + lambda track, sub_id=non_sdh_sub.id: ( + (track.strip_hearing_impaired()) if track.id == sub_id else None + ), ) with console.status("Sorting tracks by language and bitrate...", spinner="dots"): @@ -1596,14 +1601,14 @@ class dl: lambda x: x.bitrate and vbitrate_min <= x.bitrate // 1000 <= vbitrate_max ) if not title.tracks.videos: - self.log.error( - f"No Video Track in {vbitrate_min}-{vbitrate_max}kbps range..." - ) + self.log.error(f"No Video Track in {vbitrate_min}-{vbitrate_max}kbps range...") sys.exit(1) effective_video_lang = v_lang or lang video_languages = [lang for lang in effective_video_lang if lang != "best"] - video_multi_lang = "best" in effective_video_lang or "all" in effective_video_lang or len(video_languages) > 1 + video_multi_lang = ( + "best" in effective_video_lang or "all" in effective_video_lang or len(video_languages) > 1 + ) if video_languages and "all" not in video_languages: processed_video_lang = [] for language in video_languages: @@ -1715,9 +1720,7 @@ class dl: if non_hybrid_ranges and non_hybrid_tracks: # Include language dimension when multiple video languages were requested if video_multi_lang: - non_hybrid_langs = list( - dict.fromkeys(str(v.language) for v in non_hybrid_tracks) - ) + non_hybrid_langs = list(dict.fromkeys(str(v.language) for v in non_hybrid_tracks)) else: non_hybrid_langs = [None] for resolution, color_range, codec, vlang in product( @@ -1743,9 +1746,7 @@ class dl: else: selected_videos: list[Video] = [] if video_multi_lang: - unique_video_langs = list( - dict.fromkeys(str(v.language) for v in title.tracks.videos) - ) + unique_video_langs = list(dict.fromkeys(str(v.language) for v in title.tracks.videos)) else: unique_video_langs = [None] for resolution, color_range, codec, vlang in product( @@ -1754,11 +1755,7 @@ class dl: candidates = [ t for t in title.tracks.videos - if ( - not resolution - or t.height == resolution - or int(t.width * (9 / 16)) == resolution - ) + if (not resolution or t.height == resolution or int(t.width * (9 / 16)) == resolution) and (not color_range or t.range == color_range) and (not codec or t.codec == codec) and (vlang is None or str(t.language) == vlang) @@ -1875,9 +1872,7 @@ class dl: lambda x: x.bitrate and abitrate_min <= x.bitrate // 1000 <= abitrate_max ) if not title.tracks.audio: - self.log.error( - f"No Audio Track in {abitrate_min}-{abitrate_max}kbps range..." - ) + self.log.error(f"No Audio Track in {abitrate_min}-{abitrate_max}kbps range...") sys.exit(1) audio_languages = a_lang or lang if audio_languages: @@ -2259,13 +2254,9 @@ class dl: if cc: cc.cc = True title.tracks.add(cc) - self.log.info( - f"Extracted a Closed Caption from Video track {video_track_n + 1}" - ) + self.log.info(f"Extracted a Closed Caption from Video track {video_track_n + 1}") else: - self.log.info( - f"No Closed Captions were found in Video track {video_track_n + 1}" - ) + self.log.info(f"No Closed Captions were found in Video track {video_track_n + 1}") except EnvironmentError: self.log.error( "Cannot extract Closed Captions as the ccextractor executable was not found..." @@ -2327,7 +2318,9 @@ class dl: def enqueue_mux_tasks(task_description: str, base_tracks: Tracks) -> None: if merge_audio or not base_tracks.audio: - task_id = progress.add_task(f"{task_description}...", total=None, start=False, downloaded="") + task_id = progress.add_task( + f"{task_description}...", total=None, start=False, downloaded="" + ) multiplex_tasks.append((task_id, base_tracks, None)) return @@ -2665,9 +2658,7 @@ class dl: title_data["subtitles"] = subs section_order = ["manifest", "video", "audio", "subtitles", "other"] - keys[str(title)] = { - k: title_data[k] for k in section_order if k in title_data - } + keys[str(title)] = {k: title_data[k] for k in section_order if k in title_data} export.write_text(jsonpickle.dumps(keys, indent=4), encoding="utf8") @@ -2699,7 +2690,9 @@ class dl: return svc = getattr(self, "_remote_service", None) server_drm_type = getattr(svc, "_server_cdm_type", None) if svc else None - drm_name = {"widevine": "Widevine", "playready": "PlayReady"}.get(server_drm_type or "", drm.__class__.__name__) + drm_name = {"widevine": "Widevine", "playready": "PlayReady"}.get( + server_drm_type or "", drm.__class__.__name__ + ) with self.DRM_TABLE_LOCK: pssh_str = "" expected_class = "PlayReady" if server_drm_type == "playready" else "Widevine" @@ -2727,10 +2720,7 @@ class dl: for kid, key in drm.content_keys.items(): if kid not in all_kids: cek_tree.add(f"[text2]{kid.hex}:{key}") - if not any( - isinstance(x, Tree) and x.label == cek_tree.label - for x in table.columns[0].cells - ): + if not any(isinstance(x, Tree) and x.label == cek_tree.label for x in table.columns[0].cells): table.add_row(cek_tree) return diff --git a/unshackle/commands/search.py b/unshackle/commands/search.py index 450f263..a2767eb 100644 --- a/unshackle/commands/search.py +++ b/unshackle/commands/search.py @@ -86,7 +86,9 @@ def search(ctx: click.Context, no_proxy: bool, profile: Optional[str] = None, pr # requesting proxy from a specific proxy provider requested_provider, proxy = proxy.split(":", maxsplit=1) # Match simple region codes (us, ca, uk1) or provider:region format (nordvpn:ca, windscribe:us) - if re.match(r"^[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE) or re.match(r"^[a-z]+:[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE): + if re.match(r"^[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE) or re.match( + r"^[a-z]+:[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE + ): proxy = proxy.lower() with console.status(f"Getting a Proxy to {proxy}...", spinner="dots"): if requested_provider: diff --git a/unshackle/commands/serve.py b/unshackle/commands/serve.py index 46144c4..5741759 100644 --- a/unshackle/commands/serve.py +++ b/unshackle/commands/serve.py @@ -147,7 +147,7 @@ def serve( if global_services: log.info(f"Global service allowlist: {', '.join(global_services)}") users = config.serve.get("users", {}) - for user_key, user_cfg in (users.items() if isinstance(users, dict) else []): + for user_key, user_cfg in users.items() if isinstance(users, dict) else []: user_services = user_cfg.get("services") if isinstance(user_cfg, dict) else None if user_services: username = user_cfg.get("username", user_key[:8] + "...") @@ -165,6 +165,7 @@ def serve( # Start session cleanup loop for remote-dl sessions from unshackle.core.api.session_store import get_session_store + session_store = get_session_store() async def start_session_cleanup(_app: web.Application) -> None: @@ -233,9 +234,13 @@ def serve( if serve_playready_flag and request.path.startswith("/playready"): from pyplayready import __version__ as pyplayready_version - response.headers["Server"] = f"https://git.gay/ready-dl/pyplayready serve v{pyplayready_version}" + + response.headers["Server"] = ( + f"https://git.gay/ready-dl/pyplayready serve v{pyplayready_version}" + ) return response + return serve_authentication if no_key: @@ -249,6 +254,7 @@ def serve( # Start session cleanup loop for remote-dl sessions from unshackle.core.api.session_store import get_session_store + session_store = get_session_store() async def start_session_cleanup(_app: web.Application) -> None: @@ -292,6 +298,7 @@ def serve( async def playready_ping(_: web.Request) -> web.Response: from pyplayready import __version__ as pyplayready_version + response = web.json_response({"message": "OK"}) response.headers["Server"] = f"https://git.gay/ready-dl/pyplayready serve v{pyplayready_version}" return response diff --git a/unshackle/core/api/handlers.py b/unshackle/core/api/handlers.py index f2c7403..9739132 100644 --- a/unshackle/core/api/handlers.py +++ b/unshackle/core/api/handlers.py @@ -612,7 +612,9 @@ async def list_titles_handler(data: Dict[str, Any], request: Optional[web.Reques service_kwargs[param_name] = False else: # Log warning for unknown required parameters - log.warning(f"Unknown required parameter '{_sanitize_log(param_name)}' for service {_sanitize_log(normalized_service)}") + log.warning( + f"Unknown required parameter '{_sanitize_log(param_name)}' for service {_sanitize_log(normalized_service)}" + ) # Filter out any parameters that the service doesn't accept filtered_kwargs = {} @@ -764,7 +766,9 @@ async def list_tracks_handler(data: Dict[str, Any], request: Optional[web.Reques service_kwargs[param_name] = False else: # Log warning for unknown required parameters - log.warning(f"Unknown required parameter '{_sanitize_log(param_name)}' for service {_sanitize_log(normalized_service)}") + log.warning( + f"Unknown required parameter '{_sanitize_log(param_name)}' for service {_sanitize_log(normalized_service)}" + ) # Filter out any parameters that the service doesn't accept filtered_kwargs = {} diff --git a/unshackle/core/cdm/loader.py b/unshackle/core/cdm/loader.py index f5aaba0..7f0a7f4 100644 --- a/unshackle/core/cdm/loader.py +++ b/unshackle/core/cdm/loader.py @@ -60,8 +60,7 @@ def _load_remote_cdm( cdm_api["secret"] = config.decrypt_labs_api_key else: raise ValueError( - f"No secret provided for DecryptLabs CDM '{cdm_name}' and no global " - "decrypt_labs_api_key configured" + f"No secret provided for DecryptLabs CDM '{cdm_name}' and no global decrypt_labs_api_key configured" ) return DecryptLabsRemoteCDM(service_name=service_name, vaults=vaults, **cdm_api) diff --git a/unshackle/core/downloaders/requests.py b/unshackle/core/downloaders/requests.py index 65a61e8..3db3ea9 100644 --- a/unshackle/core/downloaders/requests.py +++ b/unshackle/core/downloaders/requests.py @@ -20,8 +20,8 @@ RETRY_WAIT = 2 PROGRESS_WINDOW = 2 # Adaptive chunk sizing — benchmarked optimal range -MIN_CHUNK = 524_288 # 512KB -MAX_CHUNK = 4_194_304 # 4MB +MIN_CHUNK = 524_288 # 512KB +MAX_CHUNK = 4_194_304 # 4MB DEFAULT_CHUNK = 524_288 # 512KB SPEED_ROLLING_WINDOW = 10 # seconds of history to keep for speed calculation @@ -41,6 +41,7 @@ def _is_requests_session(session: Any) -> bool: def _is_rnet_session(session: Any) -> bool: """Check if the session is an RnetSession (uses resp.stream()).""" from unshackle.core.session import RnetSession + return isinstance(session, RnetSession) diff --git a/unshackle/core/drm/playready.py b/unshackle/core/drm/playready.py index 7907100..4b444a9 100644 --- a/unshackle/core/drm/playready.py +++ b/unshackle/core/drm/playready.py @@ -179,9 +179,9 @@ class PlayReady: pssh_boxes.extend( Box.parse(base64.b64decode(x.uri.split(",")[-1])) for x in (master.session_keys or master.keys) - if x and x.keyformat and x.keyformat.lower() in { - f"urn:uuid:{PSSH.SYSTEM_ID}", "com.microsoft.playready" - } + if x + and x.keyformat + and x.keyformat.lower() in {f"urn:uuid:{PSSH.SYSTEM_ID}", "com.microsoft.playready"} ) init_data = track.get_init_segment(session=session) @@ -360,10 +360,7 @@ class PlayReady: # but the real KID for the license server. Add zero-KID fallback entries so # mp4decrypt can match when the file's default KID is all zeros. zero_kid = "00" * 16 - existing_kids = { - kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") - for kid in self.content_keys - } + existing_kids = {kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") for kid in self.content_keys} if zero_kid not in existing_kids: for key in self.content_keys.values(): key_hex = key if isinstance(key, str) else key.hex() @@ -378,7 +375,7 @@ class PlayReady: ] try: - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8') + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8") except subprocess.CalledProcessError as e: error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}" raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg) diff --git a/unshackle/core/drm/widevine.py b/unshackle/core/drm/widevine.py index 9f3bc2e..09596aa 100644 --- a/unshackle/core/drm/widevine.py +++ b/unshackle/core/drm/widevine.py @@ -296,10 +296,7 @@ class Widevine: # but the real KID for the license server. Add zero-KID fallback entries so # mp4decrypt can match when the file's default KID is all zeros. zero_kid = "00" * 16 - existing_kids = { - kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") - for kid in self.content_keys - } + existing_kids = {kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") for kid in self.content_keys} if zero_kid not in existing_kids: for key in self.content_keys.values(): key_hex = key if isinstance(key, str) else key.hex() @@ -314,7 +311,7 @@ class Widevine: ] try: - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8') + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8") except subprocess.CalledProcessError as e: error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}" raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg) diff --git a/unshackle/core/manifests/dash.py b/unshackle/core/manifests/dash.py index b9cdaf4..2902fec 100644 --- a/unshackle/core/manifests/dash.py +++ b/unshackle/core/manifests/dash.py @@ -486,7 +486,7 @@ class DASH: "save_dir": str(save_dir), "save_path": str(save_path), "downloader": "requests", - }, + }, ) raise FileNotFoundError(error_msg) @@ -505,7 +505,7 @@ class DASH: "segments_found": len(segments_to_merge), "segment_files": [f.name for f in segments_to_merge[:10]], # Limit to first 10 "downloader": "requests", - }, + }, ) if not segments_to_merge: @@ -523,7 +523,7 @@ class DASH: "save_dir": str(save_dir), "directory_contents": [str(p) for p in all_contents], "downloader": "requests", - }, + }, ) raise FileNotFoundError(error_msg) @@ -577,8 +577,8 @@ class DASH: # Find the sidx box in the data offset = 0 while offset < len(data) - 8: - box_size = struct.unpack(">I", data[offset:offset + 4])[0] - if box_size < 8 or data[offset + 4:offset + 8] != b"sidx": + box_size = struct.unpack(">I", data[offset : offset + 4])[0] + if box_size < 8 or data[offset + 4 : offset + 8] != b"sidx": offset += max(box_size, 8) continue @@ -589,14 +589,14 @@ class DASH: pos += 4 # timescale if version == 0: - first_offset = struct.unpack(">I", data[pos + 4:pos + 8])[0] + first_offset = struct.unpack(">I", data[pos + 4 : pos + 8])[0] pos += 8 else: - first_offset = struct.unpack(">Q", data[pos + 8:pos + 16])[0] + first_offset = struct.unpack(">Q", data[pos + 8 : pos + 16])[0] pos += 16 pos += 2 # reserved - reference_count = struct.unpack(">H", data[pos:pos + 2])[0] + reference_count = struct.unpack(">H", data[pos : pos + 2])[0] pos += 2 idx_end = int(index_range.split("-")[1]) @@ -604,7 +604,7 @@ class DASH: segments = [] for _ in range(reference_count): - ref_size = struct.unpack(">I", data[pos:pos + 4])[0] & 0x7FFFFFFF + ref_size = struct.unpack(">I", data[pos : pos + 4])[0] & 0x7FFFFFFF pos += 12 # ref_info + subseg_duration + SAP fields seg_end = current_offset + ref_size - 1 segments.append(f"{current_offset}-{seg_end}") diff --git a/unshackle/core/manifests/hls.py b/unshackle/core/manifests/hls.py index 08ad646..a381ac8 100644 --- a/unshackle/core/manifests/hls.py +++ b/unshackle/core/manifests/hls.py @@ -116,12 +116,14 @@ class HLS: cc_by_group_id: dict[str, list[dict[str, Any]]] = {} for media in self.manifest.media: if media.type == "CLOSED-CAPTIONS": - cc_by_group_id.setdefault(media.group_id, []).append({ - "language": media.language, - "name": media.name, - "instream_id": media.instream_id, - "characteristics": media.characteristics, - }) + cc_by_group_id.setdefault(media.group_id, []).append( + { + "language": media.language, + "name": media.name, + "instream_id": media.instream_id, + "characteristics": media.characteristics, + } + ) tracks = Tracks() for playlist in self.manifest.playlists: @@ -313,8 +315,8 @@ class HLS: # H.264: NAL type 7 (SPS), identified by byte & 0x1F == 7 # H.265: NAL type 33 (SPS), identified by (byte >> 1) & 0x3F == 33 for i in range(len(data) - 4): - start3 = data[i:i + 3] == b"\x00\x00\x01" - start4 = data[i:i + 4] == b"\x00\x00\x00\x01" + start3 = data[i : i + 3] == b"\x00\x00\x01" + start4 = data[i : i + 4] == b"\x00\x00\x00\x01" if not start3 and not start4: continue offset = i + (4 if start4 else 3) @@ -327,7 +329,7 @@ class HLS: # H.264 SPS (NAL type 7) if h264_type == 7: - sps = data[offset:offset + 64] + sps = data[offset : offset + 64] if len(sps) < 5: continue try: @@ -385,7 +387,7 @@ class HLS: # H.265 SPS (NAL type 33) elif h265_type == 33: - sps = data[offset:offset + 128] + sps = data[offset : offset + 128] if len(sps) < 10: continue try: @@ -506,9 +508,7 @@ class HLS: sys.exit(1) playlist_text = response.text else: - raise TypeError( - f"Expected response to be a requests.Response or rnet.Response, not {type(response)}" - ) + raise TypeError(f"Expected response to be a requests.Response or rnet.Response, not {type(response)}") master = m3u8.loads(playlist_text, uri=track.url) @@ -821,9 +821,7 @@ class HLS: res.raise_for_status() init_content = res.content else: - raise TypeError( - f"Expected response to be requests.Response or rnet.Response, not {type(res)}" - ) + raise TypeError(f"Expected response to be requests.Response or rnet.Response, not {type(res)}") map_data = (segment.init_section, init_content) @@ -910,7 +908,7 @@ class HLS: "segments_found": len(segments_to_merge), "segment_files": [f.name for f in segments_to_merge[:10]], # Limit to first 10 "downloader": "requests", - }, + }, ) if not segments_to_merge: @@ -928,7 +926,7 @@ class HLS: "save_dir_exists": save_dir.exists(), "directory_contents": [str(p) for p in all_contents], "downloader": "requests", - }, + }, ) raise FileNotFoundError(error_msg) diff --git a/unshackle/core/manifests/ism.py b/unshackle/core/manifests/ism.py index 778a625..c385bc0 100644 --- a/unshackle/core/manifests/ism.py +++ b/unshackle/core/manifests/ism.py @@ -293,7 +293,7 @@ class ISM: "downloader": "requests", "has_drm": bool(session_drm), "drm_type": session_drm.__class__.__name__ if session_drm else None, - "save_path": str(save_path), + "save_path": str(save_path), }, ) @@ -321,7 +321,7 @@ class ISM: "save_dir": str(save_dir), "save_path": str(save_path), "downloader": "requests", - }, + }, ) raise FileNotFoundError(error_msg) @@ -340,7 +340,7 @@ class ISM: "segments_found": len(segments_to_merge), "segment_files": [f.name for f in segments_to_merge[:10]], # Limit to first 10 "downloader": "requests", - }, + }, ) if not segments_to_merge: @@ -357,7 +357,7 @@ class ISM: "save_dir": str(save_dir), "directory_contents": [str(p) for p in all_contents], "downloader": "requests", - }, + }, ) raise FileNotFoundError(error_msg) diff --git a/unshackle/core/proxies/windscribevpn.py b/unshackle/core/proxies/windscribevpn.py index 2233cca..c136b63 100644 --- a/unshackle/core/proxies/windscribevpn.py +++ b/unshackle/core/proxies/windscribevpn.py @@ -83,7 +83,7 @@ class WindscribeVPN(Proxy): if not hostname: return None - hostname = hostname.split(':')[0] + hostname = hostname.split(":")[0] return f"https://{self.username}:{self.password}@{hostname}:443" def get_specific_server(self, country_code: str, server_num: str) -> Optional[str]: diff --git a/unshackle/core/session.py b/unshackle/core/session.py index 733fe8c..2529934 100644 --- a/unshackle/core/session.py +++ b/unshackle/core/session.py @@ -41,6 +41,7 @@ def _resolve_impersonate(browser: str) -> rnet.Impersonate: f"See rnet.Impersonate for all available presets." ) + # Map string method names to rnet.Method enum _METHOD_MAP: dict[str, rnet.Method] = { "GET": rnet.Method.GET, @@ -167,6 +168,7 @@ class RnetResponse: def json(self, **kwargs: Any) -> Any: import json as _json + return _json.loads(self.content) def raise_for_status(self) -> None: @@ -319,8 +321,9 @@ class RnetCookieAdapter(MutableMapping): self._flat[name] = value self._set_cookie_on_client("https://localhost", name, value) - def get(self, name: str, default: Optional[str] = None, domain: Optional[str] = None, - path: Optional[str] = None) -> Optional[str]: + def get( + self, name: str, default: Optional[str] = None, domain: Optional[str] = None, path: Optional[str] = None + ) -> Optional[str]: if domain and domain in self._cookies: return self._cookies[domain].get(name, default) return self._flat.get(name, default) @@ -364,8 +367,7 @@ class RnetCookieAdapter(MutableMapping): return dict(self._cookies.get(domain, {})) return dict(self._flat) - def clear(self, domain: Optional[str] = None, path: Optional[str] = None, - name: Optional[str] = None) -> None: + def clear(self, domain: Optional[str] = None, path: Optional[str] = None, name: Optional[str] = None) -> None: """Remove cookies (requests RequestsCookieJar compat). - ``clear()`` removes all cookies. @@ -527,7 +529,9 @@ class RnetSession: return url parsed = urlparse(url) separator = "&" if parsed.query else "" - query = parsed.query + separator + urlencode(params, doseq=True) if parsed.query else urlencode(params, doseq=True) + query = ( + parsed.query + separator + urlencode(params, doseq=True) if parsed.query else urlencode(params, doseq=True) + ) return urlunparse(parsed._replace(query=query)) def get_sleep_time(self, response: Optional[RnetResponse], attempt: int) -> Optional[float]: diff --git a/unshackle/core/titles/episode.py b/unshackle/core/titles/episode.py index 814c381..020f369 100644 --- a/unshackle/core/titles/episode.py +++ b/unshackle/core/titles/episode.py @@ -103,33 +103,33 @@ class Episode(Title): if config.folder_template: formatter = TemplateFormatter(config.folder_template) context = self._build_template_context(media_info, show_service) - context['season'] = f"S{self.season:02}" + context["season"] = f"S{self.season:02}" folder_name = formatter.format(context) - separators = re.sub(r'\{[^}]*\}', '', config.folder_template) + separators = re.sub(r"\{[^}]*\}", "", config.folder_template) spacer = "." if "." in separators and " " not in separators else " " return sanitize_filename(folder_name, spacer) series_template = config.output_template.get("series") if series_template: derived_template = series_template - derived_template = re.sub(r'\{episode\}', '', derived_template) - derived_template = re.sub(r'\{episode_name\?\}', '', derived_template) - derived_template = re.sub(r'\{episode_name\}', '', derived_template) - derived_template = re.sub(r'\{season_episode\}', '{season}', derived_template) + derived_template = re.sub(r"\{episode\}", "", derived_template) + derived_template = re.sub(r"\{episode_name\?\}", "", derived_template) + derived_template = re.sub(r"\{episode_name\}", "", derived_template) + derived_template = re.sub(r"\{season_episode\}", "{season}", derived_template) - derived_template = re.sub(r'\.{2,}', '.', derived_template) - derived_template = re.sub(r'\s{2,}', ' ', derived_template) - derived_template = re.sub(r'^[\.\s]+|[\.\s]+$', '', derived_template) + derived_template = re.sub(r"\.{2,}", ".", derived_template) + derived_template = re.sub(r"\s{2,}", " ", derived_template) + derived_template = re.sub(r"^[\.\s]+|[\.\s]+$", "", derived_template) formatter = TemplateFormatter(derived_template) context = self._build_template_context(media_info, show_service) - context['season'] = f"S{self.season:02}" + context["season"] = f"S{self.season:02}" folder_name = formatter.format(context) - separators = re.sub(r'\{[^}]*\}', '', derived_template) + separators = re.sub(r"\{[^}]*\}", "", derived_template) spacer = "." if "." in separators and " " not in separators else " " return sanitize_filename(folder_name, spacer) else: diff --git a/unshackle/core/titles/movie.py b/unshackle/core/titles/movie.py index d170b99..fcde150 100644 --- a/unshackle/core/titles/movie.py +++ b/unshackle/core/titles/movie.py @@ -65,7 +65,7 @@ class Movie(Title): context = self._build_template_context(media_info, show_service) folder_name = formatter.format(context) - separators = re.sub(r'\{[^}]*\}', '', config.folder_template) + separators = re.sub(r"\{[^}]*\}", "", config.folder_template) spacer = "." if "." in separators and " " not in separators else " " return sanitize_filename(folder_name, spacer) name = f"{self.name}" diff --git a/unshackle/core/titles/song.py b/unshackle/core/titles/song.py index 3e1712a..402d5a6 100644 --- a/unshackle/core/titles/song.py +++ b/unshackle/core/titles/song.py @@ -100,7 +100,7 @@ class Song(Title): context = self._build_template_context(media_info, show_service) folder_name = formatter.format(context) - separators = re.sub(r'\{[^}]*\}', '', config.folder_template) + separators = re.sub(r"\{[^}]*\}", "", config.folder_template) spacer = "." if "." in separators and " " not in separators else " " return sanitize_filename(folder_name, spacer) name = f"{self.artist} - {self.album}" diff --git a/unshackle/core/tracks/attachment.py b/unshackle/core/tracks/attachment.py index 01f6acc..00a82e2 100644 --- a/unshackle/core/tracks/attachment.py +++ b/unshackle/core/tracks/attachment.py @@ -86,9 +86,7 @@ class Attachment: raise ValueError(f"Failed to download attachment from URL: {e}") if path is not None and not isinstance(path, (str, Path)): - raise ValueError( - f"Invalid attachment path type: expected str or Path, got {type(path).__name__}." - ) + raise ValueError(f"Invalid attachment path type: expected str or Path, got {type(path).__name__}.") if path is not None: path = Path(path) diff --git a/unshackle/core/tracks/hybrid.py b/unshackle/core/tracks/hybrid.py index 4a66866..54756d8 100644 --- a/unshackle/core/tracks/hybrid.py +++ b/unshackle/core/tracks/hybrid.py @@ -113,9 +113,7 @@ class Hybrid: # Edit L6 with actual luminance values from RPU, then L5 active area self.level_6() - base_video = next( - (v for v in videos if v.range in (Video.Range.HDR10, Video.Range.HDR10P)), None - ) + base_video = next((v for v in videos if v.range in (Video.Range.HDR10, Video.Range.HDR10P)), None) if base_video and base_video.path: self.level_5(base_video.path) @@ -423,7 +421,10 @@ class Hybrid: level="ERROR", operation="hybrid_level5", message="Failed editing RPU Level 5 values", - context={"returncode": result.returncode, "stderr": (result.stderr or b"").decode(errors="replace")}, + context={ + "returncode": result.returncode, + "stderr": (result.stderr or b"").decode(errors="replace"), + }, ) Path.unlink(config.directories.temp / "RPU_L5.bin", missing_ok=True) raise ValueError("Failed editing RPU Level 5 values") @@ -433,7 +434,10 @@ class Hybrid: level="DEBUG", operation="hybrid_level5", message="Edited RPU Level 5 active area", - context={"crop": {"left": left, "right": right, "top": top, "bottom": bottom}, "samples": len(crop_results)}, + context={ + "crop": {"left": left, "right": right, "top": top, "bottom": bottom}, + "samples": len(crop_results), + }, success=True, ) self.rpu_file = "RPU_L5.bin" @@ -524,7 +528,10 @@ class Hybrid: level="ERROR", operation="hybrid_level6", message="Failed editing RPU Level 6 values", - context={"returncode": result.returncode, "stderr": (result.stderr or b"").decode(errors="replace")}, + context={ + "returncode": result.returncode, + "stderr": (result.stderr or b"").decode(errors="replace"), + }, ) Path.unlink(config.directories.temp / "RPU_L6.bin", missing_ok=True) raise ValueError("Failed editing RPU Level 6 values") @@ -587,7 +594,12 @@ class Hybrid: level="DEBUG", operation="hybrid_inject_rpu", message=f"Injected Dolby Vision metadata into {self.hdr_type} stream", - context={"hdr_type": self.hdr_type, "rpu_file": self.rpu_file, "output": self.hevc_file, "drop_hdr10plus": self.hdr10plus_to_dv}, + context={ + "hdr_type": self.hdr_type, + "rpu_file": self.rpu_file, + "output": self.hevc_file, + "drop_hdr10plus": self.hdr10plus_to_dv, + }, success=True, ) @@ -660,10 +672,14 @@ class Hybrid: result = subprocess.run( [ ffprobe_bin, - "-v", "error", - "-select_streams", "v:0", - "-show_entries", "stream_side_data=max_luminance,min_luminance,max_content,max_average", - "-of", "json", + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream_side_data=max_luminance,min_luminance,max_content,max_average", + "-of", + "json", str(config.directories.temp / "HDR10.hevc"), ], stdout=subprocess.PIPE, diff --git a/unshackle/core/tracks/tracks.py b/unshackle/core/tracks/tracks.py index 412b2a6..76136ff 100644 --- a/unshackle/core/tracks/tracks.py +++ b/unshackle/core/tracks/tracks.py @@ -322,9 +322,7 @@ class Tracks: base_tracks = [] for range_type in base_ranges: base_tracks = [ - v - for v in tracks - if v.range == range_type and (v.height in quality or int(v.width * 9 / 16) in quality) + v for v in tracks if v.range == range_type and (v.height in quality or int(v.width * 9 / 16) in quality) ] if base_tracks: break