fix(dl): prevent missing tracks error when excluding track types

- Define keep_* flags early in the track selection loop to accurately trace which track types (video, audio, subs, chapters) should be kept.
- Guard specific track filtering and validation blocks in dl.py using keep_* variables.
- Prevent fatal 'not found in video/audio/subtitle tracks' errors when using skip options like --no-video (-nv) or --no-subs (-ns).
- Simplify and clean up the trailing track cleanup logic to rely on the pre-determined keep_* flags.
This commit is contained in:
CodeName393
2026-07-12 19:07:27 +09:00
committed by GitHub
parent f42af571a7
commit ccb2a19e7a
+132 -108
View File
@@ -47,8 +47,16 @@ from unshackle.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY,
from unshackle.core.credential import Credential
from unshackle.core.drm import DRM_T, ClearKeyCENC, MonaLisa, PlayReady, Widevine
from unshackle.core.events import events
from unshackle.core.music import (MusicAudioIntegrityError, MusicMetadataResult, MusicPlanner, MusicRenderer,
file_md5, verify_music_audio, write_music_manifest, write_music_metadata)
from unshackle.core.music import (
MusicAudioIntegrityError,
MusicMetadataResult,
MusicPlanner,
MusicRenderer,
file_md5,
verify_music_audio,
write_music_manifest,
write_music_metadata,
)
from unshackle.core.proxies import Basic, ExpressVPN, Gluetun, Hola, NordVPN, ProtonVPN, SurfsharkVPN, WindscribeVPN
from unshackle.core.service import Service
from unshackle.core.services import Services
@@ -59,14 +67,30 @@ from unshackle.core.tracks import Audio, Subtitle, Tracks, Video
from unshackle.core.tracks.attachment import Attachment
from unshackle.core.tracks.dv_fixup import apply_dv_fixup
from unshackle.core.tracks.hybrid import Hybrid
from unshackle.core.utilities import (find_font_with_fallbacks, find_missing_langs, get_debug_logger,
get_system_fonts, init_debug_logger, is_close_match, log_event,
suggest_font_packages, time_elapsed_since)
from unshackle.core.utilities import (
find_font_with_fallbacks,
find_missing_langs,
get_debug_logger,
get_system_fonts,
init_debug_logger,
is_close_match,
log_event,
suggest_font_packages,
time_elapsed_since,
)
from unshackle.core.utils import tags
from unshackle.core.utils.bitrate import apply_real_bitrates
from unshackle.core.utils.click_types import (AUDIO_CODEC_LIST, LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE,
SLOW_DELAY_RANGE, ContextData, MultipleChoice, MultipleVideoCodecChoice,
SubtitleCodecChoice)
from unshackle.core.utils.click_types import (
AUDIO_CODEC_LIST,
LANGUAGE_RANGE,
QUALITY_LIST,
SEASON_RANGE,
SLOW_DELAY_RANGE,
ContextData,
MultipleChoice,
MultipleVideoCodecChoice,
SubtitleCodecChoice,
)
from unshackle.core.utils.collections import ci_get, merge_dict
from unshackle.core.utils.selector import select_multiple
from unshackle.core.utils.subprocess import ffprobe
@@ -842,25 +866,41 @@ class dl:
if name == "shaka_packager":
r = subprocess.run(
[str(binary), "--version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
version = (r.stdout or r.stderr or "").strip()
elif name in ("ffmpeg", "ffprobe"):
r = subprocess.run(
[str(binary), "-version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
version = (r.stdout or "").split("\n")[0].strip()
elif name == "mkvmerge":
r = subprocess.run(
[str(binary), "--version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
version = (r.stdout or "").strip()
elif name == "mp4decrypt":
r = subprocess.run(
[str(binary)],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
output = (r.stdout or "") + (r.stderr or "")
lines = [line.strip() for line in output.split("\n") if line.strip()]
@@ -1303,7 +1343,9 @@ class dl:
else:
vaults_only = not cdm_only
config.decryption = resolve_decryption(config.decryption_map, config.decryption, service.__class__.__name__.upper())
config.decryption = resolve_decryption(
config.decryption_map, config.decryption, service.__class__.__name__.upper()
)
log_event(
"drm_mode_config",
@@ -2134,32 +2176,8 @@ class dl:
events.subscribe(events.Types.TRACK_REPACKED, service.on_track_repacked)
events.subscribe(events.Types.TRACK_MULTIPLEX, service.on_track_multiplex)
if hasattr(service, "NO_SUBTITLES") and service.NO_SUBTITLES:
console.log("Skipping subtitles - service does not support subtitle downloads")
no_subs = True
s_lang = None
title.tracks.subtitles = []
elif no_subs:
console.log("Skipped subtitles as --no-subs was used...")
s_lang = None
title.tracks.subtitles = []
if no_video:
console.log("Skipped video as --no-video was used...")
v_lang = None
title.tracks.videos = []
if no_audio:
console.log("Skipped audio as --no-audio was used...")
a_lang = None
title.tracks.audio = []
if no_chapters:
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...")
# The no_xxx guards are handled later right before track selection.
pass
tracks_label = "Getting Remote Tracks..." if self.is_remote else "Getting Tracks..."
with console.status(tracks_label, spinner="dots"):
@@ -2292,10 +2310,58 @@ class dl:
console.print(Padding(Panel(available_tracks, title="Available Tracks"), (0, 5)))
continue
# Determine which tracks to keep
keep_videos = True
keep_audio = True
keep_subtitles = True
keep_chapters = True
if video_only or audio_only or subs_only or chapters_only:
keep_videos = video_only
keep_audio = audio_only
keep_subtitles = subs_only
keep_chapters = chapters_only
if hasattr(service, "NO_SUBTITLES") and service.NO_SUBTITLES:
console.log("Skipping subtitles - service does not support subtitle downloads")
keep_subtitles = False
s_lang = None
elif no_subs:
console.log("Skipped subtitles as --no-subs was used...")
keep_subtitles = False
s_lang = None
if no_video:
console.log("Skipped video as --no-video was used...")
keep_videos = False
v_lang = None
if no_audio:
console.log("Skipped audio as --no-audio was used...")
keep_audio = False
a_lang = None
if no_chapters:
console.log("Skipped chapters as --no-chapters was used...")
keep_chapters = False
if no_proxy_download and any(service.session.proxies.values()):
console.log("Bypassing proxy for downloads as --no-proxy-download was used...")
# Clear unwanted tracks
if not keep_videos:
title.tracks.videos = []
if not keep_audio:
title.tracks.audio = []
if not keep_subtitles:
title.tracks.subtitles = []
if not keep_chapters:
title.tracks.chapters = []
with console.status("Selecting tracks...", spinner="dots"):
if isinstance(title, (Movie, Episode)):
# filter video tracks
if vcodec:
if keep_videos and vcodec:
title.tracks.select_video(lambda x: x.codec in vcodec)
missing_codecs = [c for c in vcodec if not any(x.codec == c for x in title.tracks.videos)]
for codec in missing_codecs:
@@ -2304,7 +2370,7 @@ class dl:
self.log.error(f"There's no {', '.join(c.name for c in vcodec)} Video Track...")
sys.exit(1)
if range_:
if keep_videos and range_:
# Special handling for HYBRID - don't filter, keep all HDR10 and DV tracks
if Video.Range.HYBRID not in range_:
title.tracks.select_video(lambda x: x.range in range_)
@@ -2315,7 +2381,7 @@ class dl:
self.log.error(f"There's no {', '.join(r.name for r in range_)} Video Track...")
sys.exit(1)
if vbitrate:
if keep_videos and vbitrate:
if any(r == Video.Range.HYBRID for r in range_):
# In HYBRID mode, only apply bitrate filter to non-DV tracks
# DV tracks are kept regardless since they're only used for RPU metadata
@@ -2331,7 +2397,7 @@ class dl:
self.log.error(f"There's no {vbitrate}kbps Video Track...")
sys.exit(1)
if vbitrate_min is not None and vbitrate_max is not None:
if keep_videos and vbitrate_min is not None and vbitrate_max is not None:
title.tracks.select_video(
lambda x: x.bitrate and vbitrate_min <= x.bitrate // 1000 <= vbitrate_max
)
@@ -2344,7 +2410,7 @@ class dl:
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:
if keep_videos and video_languages and "all" not in video_languages:
processed_video_lang = []
for language in video_languages:
if language == "orig":
@@ -2367,9 +2433,7 @@ class dl:
if missing_v_langs:
missing_str = ", ".join(missing_v_langs)
if best_available:
remaining_v_langs = [
tok for tok in processed_video_lang if tok not in missing_v_langs
]
remaining_v_langs = [tok for tok in processed_video_lang if tok not in missing_v_langs]
if remaining_v_langs:
self.log.warning(
f"{missing_str} not found in video tracks, "
@@ -2377,9 +2441,7 @@ class dl:
)
processed_video_lang = remaining_v_langs
else:
self.log.error(
f"{missing_str} not found in video tracks and no fallback available"
)
self.log.error(f"{missing_str} not found in video tracks and no fallback available")
sys.exit(1)
else:
self.log.error(missing_str + " not found in video tracks")
@@ -2394,7 +2456,7 @@ 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]
effective_quality = quality
if quality:
if keep_videos and quality:
missing_resolutions = []
if has_hybrid:
hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos(
@@ -2468,7 +2530,7 @@ class dl:
# choose best track by range and quality
pre_hybrid_videos: list[Video] = list(title.tracks.videos) if has_hybrid else []
if has_hybrid:
if keep_videos and has_hybrid:
# Apply hybrid selection for HYBRID tracks
hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos(
title.tracks.videos, non_hybrid_ranges
@@ -2522,7 +2584,7 @@ class dl:
# 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:
elif keep_videos:
selected_videos: list[Video] = []
if video_multi_lang:
unique_video_langs = list(dict.fromkeys(str(v.language) for v in title.tracks.videos))
@@ -2549,7 +2611,7 @@ class dl:
title.tracks.videos = selected_videos
# validate hybrid mode requirements
if any(r == Video.Range.HYBRID for r in range_):
if keep_videos and any(r == Video.Range.HYBRID for r in range_):
base_tracks = [
v for v in title.tracks.videos if v.range in (Video.Range.HDR10, Video.Range.HDR10P)
]
@@ -2613,7 +2675,7 @@ class dl:
sys.exit(1)
# filter subtitle tracks
if require_subs:
if keep_subtitles and require_subs:
missing_langs = [
lang
for lang in require_subs
@@ -2627,7 +2689,7 @@ class dl:
self.log.info(
f"Required languages found ({', '.join(require_subs)}), downloading all available subtitles"
)
elif s_lang and "all" not in s_lang:
elif keep_subtitles and s_lang and "all" not in s_lang:
from unshackle.core.utilities import is_exact_match
match_func = is_exact_match if exact_lang else is_close_match
@@ -2661,12 +2723,12 @@ class dl:
self.log.error(f"There's no {s_lang} Subtitle Track...")
sys.exit(1)
if not forced_subs:
if keep_subtitles and not forced_subs:
title.tracks.select_subtitles(lambda x: not x.forced)
# filter audio tracks
# might have no audio tracks if part of the video, e.g. transport stream hls
if len(title.tracks.audio) > 0:
if keep_audio and len(title.tracks.audio) > 0:
if not audio_description:
title.tracks.select_audio(lambda x: not x.descriptive) # exclude descriptive audio
if acodec:
@@ -2747,57 +2809,19 @@ class dl:
self.log.error(f"There's no {processed_lang} Audio Track, cannot continue...")
sys.exit(1)
if (
video_only
or audio_only
or subs_only
or chapters_only
or no_subs
or no_audio
or no_chapters
or no_video
):
keep_videos = False
keep_audio = False
keep_subtitles = False
keep_chapters = False
# Reconstruct track set to only include kept tracks
kept_tracks = []
if keep_videos:
kept_tracks.extend(title.tracks.videos)
if keep_audio:
kept_tracks.extend(title.tracks.audio)
if keep_subtitles:
kept_tracks.extend(title.tracks.subtitles)
if keep_chapters:
kept_tracks.extend(title.tracks.chapters)
kept_tracks.extend(title.tracks.attachments)
if video_only or audio_only or subs_only or chapters_only:
if video_only:
keep_videos = True
if audio_only:
keep_audio = True
if subs_only:
keep_subtitles = True
if chapters_only:
keep_chapters = True
else:
keep_videos = True
keep_audio = True
keep_subtitles = True
keep_chapters = True
if no_subs:
keep_subtitles = False
if no_audio:
keep_audio = False
if no_chapters:
keep_chapters = False
if no_video:
keep_videos = False
kept_tracks = []
if keep_videos:
kept_tracks.extend(title.tracks.videos)
if keep_audio:
kept_tracks.extend(title.tracks.audio)
if keep_subtitles:
kept_tracks.extend(title.tracks.subtitles)
if keep_chapters:
kept_tracks.extend(title.tracks.chapters)
kept_tracks.extend(title.tracks.attachments)
title.tracks = Tracks(kept_tracks, manifest_url=title.tracks.manifest_url)
title.tracks = Tracks(kept_tracks, manifest_url=title.tracks.manifest_url)
selected_tracks, tracks_progress_callables = title.tracks.tree(add_progress=True)