mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-11 19:45:06 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user