feat(dl): add --no-proxy-download flag

Bypass proxy for segment downloads only. Manifest, license, and auth still use proxy. Faster when CDN is unrestricted but manifest is region-locked.
This commit is contained in:
imSp4rky
2026-05-03 13:18:53 -06:00
parent 21754ad37e
commit d5237371e5
4 changed files with 111 additions and 88 deletions
+11
View File
@@ -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,
+21 -23
View File
@@ -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),
])
+30 -37
View File
@@ -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:
+49 -28
View File
@@ -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,