mirror of
https://github.com/NohamR/OqeeRewind.git
synced 2026-07-11 22:41:14 +00:00
Implement chunked decryption of .m4s segments (avoid loading entire stream into memory) by changing decrypt() to accept a segment directory, process ~1GB chunks with mp4ff-decrypt, and concatenate results. Add resume support in segment downloader by skipping already downloaded .m4s files and initializing successful counter accordingly. Improve manifest handling: gracefully handle missing AVC/HEVC tracks, bruteforce ticks conditionally, and mark representations for removal when no valid tick is found. Add fallback unfiltered selection in select_track when filtered lookup yields no candidates. Replace naive UTC offset logic with explicit Europe/Paris timezone utilities and adjust related time conversion signatures. Bump project version to 1.0.
146 lines
4.6 KiB
Python
146 lines
4.6 KiB
Python
"""Utility functions for OqeeRewind, including verification, merging, and decryption."""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import shutil
|
|
from utils.logging_config import logger
|
|
|
|
|
|
def verify_mp4ff():
|
|
"""Verify if mp4ff-decrypt is installed and available in PATH."""
|
|
if shutil.which("mp4ff-decrypt") is None:
|
|
logger.error("mp4ff-decrypt is not installed or not in PATH.")
|
|
logger.info("Please install it using:")
|
|
logger.info("go install github.com/Eyevinn/mp4ff/cmd/mp4ff-decrypt@latest")
|
|
sys.exit(1)
|
|
return True
|
|
|
|
|
|
def verify_cmd(path: str) -> bool:
|
|
"""Verify if the file provided at path is valid and exists, otherwise log error and exit."""
|
|
if not os.path.exists(path):
|
|
logger.error("File does not exist: %s", path)
|
|
sys.exit(1)
|
|
if not os.path.isfile(path):
|
|
logger.error("Path is not a file: %s", path)
|
|
sys.exit(1)
|
|
return True
|
|
|
|
|
|
def merge_segments(input_folder: str, track_id: str, output_file: str):
|
|
"""Merge downloaded segments into a single file using ffmpeg."""
|
|
segment_folder = os.path.join(input_folder, f"segments_{track_id}")
|
|
|
|
segment_files = sorted(
|
|
[f for f in os.listdir(segment_folder) if f.endswith(".m4s")],
|
|
key=lambda x: int(x.split(".")[0]),
|
|
)
|
|
with open(output_file, "wb") as outfile:
|
|
for fname in segment_files:
|
|
with open(f"{segment_folder}/{fname}", "rb") as infile:
|
|
outfile.write(infile.read())
|
|
logger.info("Merged segments into %s", output_file)
|
|
|
|
|
|
def decrypt(segment_dir, init_path, output_file, key):
|
|
"""Decrypt segments in chunks of ~1GB to avoid loading entire file in memory.
|
|
|
|
Args:
|
|
segment_dir: Path to the directory containing .m4s segment files.
|
|
init_path: Path to the initialization file.
|
|
output_file: Path to the output decrypted file.
|
|
key: The decryption key in KID:KEY format.
|
|
|
|
Returns:
|
|
True if decryption succeeded, False otherwise.
|
|
"""
|
|
key = key.split(":")[1]
|
|
|
|
segment_files = sorted(
|
|
[f for f in os.listdir(segment_dir) if f.endswith(".m4s")],
|
|
key=lambda x: int(x.split(".")[0]),
|
|
)
|
|
|
|
if not segment_files:
|
|
logger.error("No segment files found in %s", segment_dir)
|
|
return False
|
|
|
|
logger.info(
|
|
"Decrypting %d segments from %s to %s",
|
|
len(segment_files),
|
|
segment_dir,
|
|
output_file,
|
|
)
|
|
|
|
chunk_num = 0
|
|
temp_files = []
|
|
TARGET_SIZE = 1 * 1024 * 1024 * 1024 # 1GB
|
|
|
|
i = 0
|
|
while i < len(segment_files):
|
|
chunk_files = []
|
|
chunk_size = 0
|
|
|
|
while i < len(segment_files) and chunk_size < TARGET_SIZE:
|
|
fname = segment_files[i]
|
|
fpath = os.path.join(segment_dir, fname)
|
|
fsize = os.path.getsize(fpath)
|
|
chunk_files.append(fname)
|
|
chunk_size += fsize
|
|
i += 1
|
|
|
|
logger.debug(
|
|
"Processing chunk %d: %d segments, %.2f MB",
|
|
chunk_num,
|
|
len(chunk_files),
|
|
chunk_size / (1024 * 1024),
|
|
)
|
|
|
|
chunk_merged = os.path.join(segment_dir, f"chunk_{chunk_num}_merged")
|
|
chunk_dec = os.path.join(segment_dir, f"chunk_{chunk_num}_dec")
|
|
temp_files.extend([chunk_merged, chunk_dec])
|
|
|
|
with open(chunk_merged, "wb") as outfile:
|
|
for fname in chunk_files:
|
|
with open(os.path.join(segment_dir, fname), "rb") as infile:
|
|
outfile.write(infile.read())
|
|
|
|
logger.debug("Decrypting chunk %d", chunk_num)
|
|
result = subprocess.run(
|
|
[
|
|
"mp4ff-decrypt",
|
|
"-init",
|
|
init_path,
|
|
"-key",
|
|
key,
|
|
chunk_merged,
|
|
chunk_dec,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
logger.error("Decryption failed for chunk %d: %s", chunk_num, result.stderr)
|
|
for tf in temp_files:
|
|
if os.path.exists(tf):
|
|
os.remove(tf)
|
|
return False
|
|
|
|
chunk_num += 1
|
|
|
|
logger.debug("Concatenating %d decrypted chunks into %s", chunk_num, output_file)
|
|
with open(output_file, "wb") as outfile:
|
|
for c in range(chunk_num):
|
|
chunk_dec = os.path.join(segment_dir, f"chunk_{c}_dec")
|
|
with open(chunk_dec, "rb") as infile:
|
|
outfile.write(infile.read())
|
|
|
|
for tf in temp_files:
|
|
if os.path.exists(tf):
|
|
os.remove(tf)
|
|
|
|
logger.info("Successfully decrypted to %s", output_file)
|
|
return True
|