diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index abb42b0f..9e59c669 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -495,6 +495,12 @@ class dl: help="Only use CDM, or only use Key Vaults for retrieval of Decryption Keys.", ) @click.option("--no-proxy", is_flag=True, default=False, help="Force disable all proxy use.") + @click.option( + "--no-proxy-download", + is_flag=True, + default=False, + help="Bypass proxy for segment downloads only. Manifest, license, and auth still use proxy.", + ) @click.option("--no-folder", is_flag=True, default=False, help="Disable folder creation for TV Shows.") @click.option( "--no-source", is_flag=True, default=False, help="Disable the source tag from the output file name and path." @@ -1032,6 +1038,7 @@ class dl: export: bool, cdm_only: Optional[bool], no_proxy: bool, + no_proxy_download: bool, no_folder: bool, no_source: bool, no_mux: bool, @@ -1448,6 +1455,9 @@ class dl: console.log("Skipped chapters as --no-chapters was used...") title.tracks.chapters = [] + if no_proxy_download and any(service.session.proxies.values()): + console.log("Bypassing proxy for downloads as --no-proxy-download was used...") + tracks_label = "Getting Remote Tracks..." if self.is_remote else "Getting Tracks..." with console.status(tracks_label, spinner="dots"): try: @@ -2070,6 +2080,7 @@ class dl: pool.submit( track.download, session=track.session or service.session, + no_proxy_download=no_proxy_download, prepare_drm=partial( partial(self.prepare_drm, table=download_table), track=track, diff --git a/unshackle/core/api/routes.py b/unshackle/core/api/routes.py index d466b403..5c10938a 100644 --- a/unshackle/core/api/routes.py +++ b/unshackle/core/api/routes.py @@ -1327,26 +1327,24 @@ def setup_swagger(app: web.Application) -> None: ) # Add routes with OpenAPI documentation - swagger.add_routes( - [ - web.get("/api/health", health), - web.get("/api/services", services), - web.post("/api/search", search), - web.post("/api/list-titles", list_titles), - web.post("/api/list-tracks", list_tracks), - web.post("/api/download", download), - web.get("/api/download/jobs", download_jobs), - web.get("/api/download/jobs/{job_id}", download_job_detail), - web.delete("/api/download/jobs/{job_id}", cancel_download_job), - # Remote-DL session endpoints - web.post("/api/session/create", session_create), - web.get("/api/session/{session_id}/titles", session_titles), - web.post("/api/session/{session_id}/tracks", session_tracks), - web.post("/api/session/{session_id}/segments", session_segments), - web.post("/api/session/{session_id}/license", session_license), - web.get("/api/session/{session_id}/prompt", session_prompt_get), - web.post("/api/session/{session_id}/prompt", session_prompt_submit), - web.get("/api/session/{session_id}", session_info), - web.delete("/api/session/{session_id}", session_delete), - ] - ) + swagger.add_routes([ + web.get("/api/health", health), + web.get("/api/services", services), + web.post("/api/search", search), + web.post("/api/list-titles", list_titles), + web.post("/api/list-tracks", list_tracks), + web.post("/api/download", download), + web.get("/api/download/jobs", download_jobs), + web.get("/api/download/jobs/{job_id}", download_job_detail), + web.delete("/api/download/jobs/{job_id}", cancel_download_job), + # Remote-DL session endpoints + web.post("/api/session/create", session_create), + web.get("/api/session/{session_id}/titles", session_titles), + web.post("/api/session/{session_id}/tracks", session_tracks), + web.post("/api/session/{session_id}/segments", session_segments), + web.post("/api/session/{session_id}/license", session_license), + web.get("/api/session/{session_id}/prompt", session_prompt_get), + web.post("/api/session/{session_id}/prompt", session_prompt_submit), + web.get("/api/session/{session_id}", session_info), + web.delete("/api/session/{session_id}", session_delete), + ]) diff --git a/unshackle/core/tracks/subtitle.py b/unshackle/core/tracks/subtitle.py index 929e6bf1..280e34d4 100644 --- a/unshackle/core/tracks/subtitle.py +++ b/unshackle/core/tracks/subtitle.py @@ -221,8 +221,9 @@ class Subtitle(Track): progress: Optional[partial] = None, *, cdm: Optional[object] = None, + no_proxy_download: bool = False, ): - super().download(session, prepare_drm, max_workers, progress, cdm=cdm) + super().download(session, prepare_drm, max_workers, progress, cdm=cdm, no_proxy_download=no_proxy_download) if not self.path: return @@ -460,17 +461,15 @@ class Subtitle(Track): content_lines.append(lines[i]) i += 1 - cues.append( - { - "start_ms": Subtitle._parse_vtt_time(start_str), - "end_ms": Subtitle._parse_vtt_time(end_str), - "start_str": start_str, - "end_str": end_str, - "line_pos": line_pos, - "content": "\n".join(content_lines), - "settings": settings, - } - ) + cues.append({ + "start_ms": Subtitle._parse_vtt_time(start_str), + "end_ms": Subtitle._parse_vtt_time(end_str), + "start_str": start_str, + "end_str": end_str, + "line_pos": line_pos, + "content": "\n".join(content_lines), + "settings": settings, + }) else: i += 1 @@ -495,23 +494,19 @@ class Subtitle(Track): group.sort(key=lambda x: x["line_pos"]) # Use the earliest start time from the group earliest = min(group, key=lambda x: x["start_ms"]) - merged_cues.append( - { - "start_str": earliest["start_str"], - "end_str": group[0]["end_str"], - "content": "\n".join(c["content"] for c in group), - "settings": "", - } - ) + merged_cues.append({ + "start_str": earliest["start_str"], + "end_str": group[0]["end_str"], + "content": "\n".join(c["content"] for c in group), + "settings": "", + }) else: - merged_cues.append( - { - "start_str": current["start_str"], - "end_str": current["end_str"], - "content": current["content"], - "settings": current["settings"], - } - ) + merged_cues.append({ + "start_str": current["start_str"], + "end_str": current["end_str"], + "content": current["content"], + "settings": current["settings"], + }) i = j if len(group) > 1 else i + 1 @@ -1150,16 +1145,14 @@ class Subtitle(Track): if cue_box.type == b"sttg": layout = Layout(webvtt_positioning=cue_box.settings) elif cue_box.type == b"payl": - nodes.extend( - [ - node - for line in cue_box.cue_text.split("\n") - for node in [ - CaptionNode.create_text(WebVTTReader()._decode(line)), - CaptionNode.create_break(), - ] + nodes.extend([ + node + for line in cue_box.cue_text.split("\n") + for node in [ + CaptionNode.create_text(WebVTTReader()._decode(line)), + CaptionNode.create_break(), ] - ) + ]) nodes.pop() if nodes: diff --git a/unshackle/core/tracks/track.py b/unshackle/core/tracks/track.py index 2fe430ca..4198c9bc 100644 --- a/unshackle/core/tracks/track.py +++ b/unshackle/core/tracks/track.py @@ -28,6 +28,25 @@ from unshackle.core.utilities import get_boxes, try_ensure_utf8 from unshackle.core.utils.subprocess import ffprobe +def direct_session(session: Union[Session, "RnetSession"]) -> Session: + """Vanilla requests.Session with copied headers/cookies, no proxy.""" + new = Session() + headers = getattr(session, "headers", None) + if headers is not None: + try: + new.headers.update(dict(headers)) + except Exception: + pass + cookies = getattr(session, "cookies", None) + if cookies is not None: + jar = getattr(cookies, "jar", None) + try: + new.cookies.update(jar if jar is not None else cookies) + except Exception: + pass + return new + + class Track: class Descriptor(Enum): URL = 1 # Direct URL, nothing fancy @@ -188,6 +207,7 @@ class Track: progress: Optional[partial] = None, *, cdm: Optional[object] = None, + no_proxy_download: bool = False, ): """Download and optionally Decrypt this Track.""" from unshackle.core.manifests import DASH, HLS, ISM @@ -203,6 +223,11 @@ class Track: proxy = next(iter(session.proxies.values()), None) + dl_session = session + if no_proxy_download and proxy: + dl_session = direct_session(session) + proxy = None + track_type = self.__class__.__name__ save_path = config.directories.temp / f"{track_type}_{self.id}.mp4" if track_type == "Subtitle": @@ -237,7 +262,7 @@ class Track: save_path=save_path, save_dir=save_dir, progress=progress, - session=session, + session=dl_session, proxy=proxy, max_workers=max_workers, license_widevine=prepare_drm, @@ -249,7 +274,7 @@ class Track: save_path=save_path, save_dir=save_dir, progress=progress, - session=session, + session=dl_session, proxy=proxy, max_workers=max_workers, license_widevine=prepare_drm, @@ -261,7 +286,7 @@ class Track: save_path=save_path, save_dir=save_dir, progress=progress, - session=session, + session=dl_session, proxy=proxy, max_workers=max_workers, license_widevine=prepare_drm, @@ -320,11 +345,11 @@ class Track: urls=self.url, output_dir=save_path.parent, filename=save_path.name, - headers=session.headers, - cookies=session.cookies, + headers=dl_session.headers, + cookies=dl_session.cookies, proxy=proxy, max_workers=max_workers, - session=session, + session=dl_session, ): file_downloaded = status_update.get("file_downloaded") if not file_downloaded: @@ -662,29 +687,25 @@ class Track: if hasattr(self, "data") and self.data.get("audio_language"): audio_lang = self.data["audio_language"] audio_name = self.data.get("audio_language_name", audio_lang) - args.extend( - [ - "-metadata:s:a:0", - f"language={audio_lang}", - "-metadata:s:a:0", - f"title={audio_name}", - "-metadata:s:a:0", - f"handler_name={audio_name}", - ] - ) + args.extend([ + "-metadata:s:a:0", + f"language={audio_lang}", + "-metadata:s:a:0", + f"title={audio_name}", + "-metadata:s:a:0", + f"handler_name={audio_name}", + ]) - args.extend( - [ - # Following are very important! - "-map_metadata", - "-1", # don't transfer metadata to output file - "-fflags", - "bitexact", # only have minimal tag data, reproducible mux - "-codec", - "copy", - str(output_path), - ] - ) + args.extend([ + # Following are very important! + "-map_metadata", + "-1", # don't transfer metadata to output file + "-fflags", + "bitexact", # only have minimal tag data, reproducible mux + "-codec", + "copy", + str(output_path), + ]) subprocess.run( args,