forked from kenzuya/unshackle
fix(hls): finalize n_m3u8dl_re outputs
- Add a small helper to move N_m3u8DL-RE final outputs into the expected temp path (preserve actual suffix) and keep subtitle codec consistent with the produced file. - Skip generic HLS segment merging when N_m3u8DL-RE is in use to avoid mixing in sidecar files and reduce Windows file-lock issues. - Harden segmented WebVTT merging to avoid IndexError when caption segment indexes exceed the provided duration list.
This commit is contained in:
@@ -225,6 +225,39 @@ class HLS:
|
|||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _finalize_n_m3u8dl_re_output(*, track: AnyTrack, save_dir: Path, save_path: Path) -> Path:
|
||||||
|
"""
|
||||||
|
Finalize output from N_m3u8DL-RE.
|
||||||
|
|
||||||
|
We call N_m3u8DL-RE with `--save-name track.id`, so the final file should be `{track.id}.*` under `save_dir`.
|
||||||
|
This moves that output to `save_path` (preserving the real suffix) and, for subtitles, updates `track.codec`
|
||||||
|
to match the produced file extension.
|
||||||
|
"""
|
||||||
|
matches = [p for p in save_dir.rglob(f"{track.id}.*") if p.is_file()]
|
||||||
|
if not matches:
|
||||||
|
raise FileNotFoundError(f"No output files produced by N_m3u8DL-RE for save-name={track.id} in: {save_dir}")
|
||||||
|
|
||||||
|
primary = max(matches, key=lambda p: p.stat().st_size)
|
||||||
|
|
||||||
|
final_save_path = save_path.with_suffix(primary.suffix) if primary.suffix else save_path
|
||||||
|
|
||||||
|
final_save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if primary.absolute() != final_save_path.absolute():
|
||||||
|
final_save_path.unlink(missing_ok=True)
|
||||||
|
shutil.move(str(primary), str(final_save_path))
|
||||||
|
|
||||||
|
if isinstance(track, Subtitle):
|
||||||
|
ext = final_save_path.suffix.lower().lstrip(".")
|
||||||
|
try:
|
||||||
|
track.codec = Subtitle.Codec.from_mime(ext)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
shutil.rmtree(save_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
return final_save_path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def download_track(
|
def download_track(
|
||||||
track: AnyTrack,
|
track: AnyTrack,
|
||||||
@@ -420,7 +453,13 @@ class HLS:
|
|||||||
for control_file in segment_save_dir.glob("*.aria2__temp"):
|
for control_file in segment_save_dir.glob("*.aria2__temp"):
|
||||||
control_file.unlink()
|
control_file.unlink()
|
||||||
|
|
||||||
if not skip_merge:
|
if skip_merge:
|
||||||
|
final_save_path = HLS._finalize_n_m3u8dl_re_output(track=track, save_dir=save_dir, save_path=save_path)
|
||||||
|
progress(downloaded="Downloaded")
|
||||||
|
track.path = final_save_path
|
||||||
|
events.emit(events.Types.TRACK_DOWNLOADED, track=track)
|
||||||
|
return
|
||||||
|
|
||||||
progress(total=total_segments, completed=0, downloaded="Merging")
|
progress(total=total_segments, completed=0, downloaded="Merging")
|
||||||
|
|
||||||
name_len = len(str(total_segments))
|
name_len = len(str(total_segments))
|
||||||
@@ -483,9 +522,7 @@ class HLS:
|
|||||||
range_len = (last_segment_i - first_segment_i) + 1
|
range_len = (last_segment_i - first_segment_i) + 1
|
||||||
|
|
||||||
segment_range = f"{str(first_segment_i).zfill(name_len)}-{str(last_segment_i).zfill(name_len)}"
|
segment_range = f"{str(first_segment_i).zfill(name_len)}-{str(last_segment_i).zfill(name_len)}"
|
||||||
merged_path = (
|
merged_path = segment_save_dir / f"{segment_range}{get_extension(master.segments[last_segment_i].uri)}"
|
||||||
segment_save_dir / f"{segment_range}{get_extension(master.segments[last_segment_i].uri)}"
|
|
||||||
)
|
|
||||||
decrypted_path = segment_save_dir / f"{merged_path.stem}_decrypted{merged_path.suffix}"
|
decrypted_path = segment_save_dir / f"{merged_path.stem}_decrypted{merged_path.suffix}"
|
||||||
|
|
||||||
files = [
|
files = [
|
||||||
@@ -597,7 +634,11 @@ class HLS:
|
|||||||
if segment_keys:
|
if segment_keys:
|
||||||
if cdm:
|
if cdm:
|
||||||
cdm_segment_keys = HLS.filter_keys_for_cdm(segment_keys, cdm)
|
cdm_segment_keys = HLS.filter_keys_for_cdm(segment_keys, cdm)
|
||||||
key = HLS.get_supported_key(cdm_segment_keys) if cdm_segment_keys else HLS.get_supported_key(segment_keys)
|
key = (
|
||||||
|
HLS.get_supported_key(cdm_segment_keys)
|
||||||
|
if cdm_segment_keys
|
||||||
|
else HLS.get_supported_key(segment_keys)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
key = HLS.get_supported_key(segment_keys)
|
key = HLS.get_supported_key(segment_keys)
|
||||||
if encryption_data and encryption_data[0] != key and i != 0 and segment not in unwanted_segments:
|
if encryption_data and encryption_data[0] != key and i != 0 and segment not in unwanted_segments:
|
||||||
|
|||||||
@@ -168,6 +168,16 @@ def merge_segmented_webvtt(vtt_raw: str, segment_durations: Optional[list[int]]
|
|||||||
duplicate_index: list[int] = []
|
duplicate_index: list[int] = []
|
||||||
captions = vtt.get_captions(lang)
|
captions = vtt.get_captions(lang)
|
||||||
|
|
||||||
|
# Some providers can produce "segment_index" values that are
|
||||||
|
# outside the provided segment_durations list after normalization/merge.
|
||||||
|
# This used to crash with IndexError and abort the entire download.
|
||||||
|
if segment_durations and captions:
|
||||||
|
max_idx = max(getattr(c, "segment_index", 0) for c in captions)
|
||||||
|
if max_idx >= len(segment_durations):
|
||||||
|
# Pad with the last known duration (or 0 if empty) so indexing is safe.
|
||||||
|
pad_val = segment_durations[-1] if segment_durations else 0
|
||||||
|
segment_durations = segment_durations + [pad_val] * (max_idx - len(segment_durations) + 1)
|
||||||
|
|
||||||
if captions[0].segment_index == 0:
|
if captions[0].segment_index == 0:
|
||||||
first_segment_mpegts = captions[0].mpegts
|
first_segment_mpegts = captions[0].mpegts
|
||||||
else:
|
else:
|
||||||
@@ -179,6 +189,9 @@ def merge_segmented_webvtt(vtt_raw: str, segment_durations: Optional[list[int]]
|
|||||||
# calculate the timestamp from SegmentTemplate/SegmentList duration.
|
# calculate the timestamp from SegmentTemplate/SegmentList duration.
|
||||||
likely_dash = first_segment_mpegts == 0 and caption.mpegts == 0
|
likely_dash = first_segment_mpegts == 0 and caption.mpegts == 0
|
||||||
if likely_dash and segment_durations:
|
if likely_dash and segment_durations:
|
||||||
|
# Defensive: segment_index can still be out of range if captions are malformed.
|
||||||
|
if caption.segment_index < 0 or caption.segment_index >= len(segment_durations):
|
||||||
|
continue
|
||||||
duration = segment_durations[caption.segment_index]
|
duration = segment_durations[caption.segment_index]
|
||||||
caption.mpegts = MPEG_TIMESCALE * (duration / timescale)
|
caption.mpegts = MPEG_TIMESCALE * (duration / timescale)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user