Files
OqeeRewind/utils/times.py
√(noham)² e51a108d28 Chunked decryption, resume & selection fixes
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.
2026-06-17 16:49:49 +02:00

126 lines
3.7 KiB
Python

"""Utility functions for time and tick conversions, and bruteforce operations."""
import asyncio
import datetime
import time
from zoneinfo import ZoneInfo
import aiohttp
from tqdm import tqdm
from utils.stream import fetch_segment
from utils.logging_config import logger
FRANCE_TZ = ZoneInfo("Europe/Paris")
def convert_ticks_to_sec(ticks, timescale):
"""Convert ticks to seconds."""
return ticks / timescale
def convert_sec_to_ticks(seconds, timescale):
"""Convert seconds to ticks."""
return seconds * timescale
def convert_sec_to_date(seconds):
"""Convert UTC seconds to France local datetime."""
dt = datetime.datetime.fromtimestamp(seconds, tz=datetime.UTC).astimezone(
FRANCE_TZ
)
return dt.replace(tzinfo=None)
def convert_date_to_sec(dt):
"""Convert France local datetime to UTC seconds."""
aware = dt.replace(tzinfo=FRANCE_TZ)
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC)
return (aware.astimezone(datetime.UTC) - epoch).total_seconds()
def convert_date_to_ticks(dt, timescale):
"""Convert France local datetime to ticks."""
return int(round(convert_date_to_sec(dt) * timescale))
def past(rep, base, duration):
"""Calculate past tick."""
return base - rep * duration
def future(rep, base, duration):
"""Calculate future tick."""
return base + rep * duration
async def bruteforce(track_id, date, batch_size=20000):
"""Bruteforce segments to find valid ticks."""
valid_ticks = []
total_requests = 288000
logger.debug("Starting bruteforce for %s near %s", track_id, date)
start_time = time.time()
try:
async with aiohttp.ClientSession() as session:
for batch_start in range(0, total_requests, batch_size):
batch_end = min(batch_start + batch_size, total_requests)
tasks = [
fetch_segment(session, t + date, track_id)
for t in range(batch_start, batch_end)
]
results = []
for coro in tqdm(
asyncio.as_completed(tasks),
total=len(tasks),
desc="Bruteforce",
unit="req",
):
result = await coro
results.append(result)
valid_ticks.extend(
[r for r in results if r and not isinstance(r, Exception)]
)
# Stop if we found valid ticks
if valid_ticks:
logger.debug("Found valid ticks: %s, stopping bruteforce.", valid_ticks)
break
except KeyboardInterrupt:
logger.error("Interrupted by user (Ctrl+C)")
elapsed = time.time() - start_time
logger.debug("Completed in %.2fs", elapsed)
logger.debug("Speed: %.2f req/s", total_requests / elapsed if elapsed > 0 else 0)
logger.debug("Total checked: %d", total_requests)
return valid_ticks
def find_nearest_tick_by_hour(base_tick, dt, timescale, duration):
"""Find the nearest tick for a given datetime."""
target_ticks = convert_date_to_ticks(dt, timescale)
diff_ticks = base_tick - target_ticks
rep_estimate = diff_ticks / duration
# Determine if we need to go to past or future
if rep_estimate < 0:
# Target is in the future from base
rep = int(round(abs(rep_estimate)))
nearest_tick = base_tick + rep * duration
else:
# Target is in the past from base
rep = int(round(rep_estimate))
nearest_tick = base_tick - rep * duration
# print(f"Requested datetime: {dt} (offset +{offset_hours}h)")
# print(f"Nearest rep: {rep}")
# print(f"Tick: {nearest_tick}")
return nearest_tick, rep