Files
OqeeRewind/utils/times.py
√(noham)² ae5035c6a8 optimize DASH probing and segment download paths
- Switch bruteforce probes from GET to HEAD to avoid downloading
  full segment bodies (only the status code is needed)
- Cap bruteforce concurrency with a semaphore (default 100) instead of issuing one task per tick, and cancel leftover tasks on early exit
- Reuse a single aiohttp session / event loop for the manifest mode's AVC/HEVC/audio bruteforces to keep TCP connections alive
- Hoist the per-request segment headers into a module-level constant
- Use a set for the already-downloaded resume check in save_segments
- Replace read()-into-memory copies with shutil.copyfileobj in   merge_segments and decrypt
2026-08-13 17:40:06 +02:00

148 lines
4.4 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")
TIMESCALE = 90000
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, concurrency=100, session=None):
"""Bruteforce segments to find valid ticks.
Args:
track_id: The track identifier.
date: The base tick to start probing from.
batch_size: Number of ticks to probe per batch.
concurrency: Maximum number of in-flight requests at once.
session: Optional shared aiohttp session. A new one is created if omitted.
Returns:
List of valid ticks found (empty if none).
"""
valid_ticks = []
total_requests = 288000
logger.debug("Starting bruteforce for %s near (%s) %s", track_id, date, convert_sec_to_date(convert_ticks_to_sec(date, TIMESCALE)))
start_time = time.time()
owns_session = session is None
if owns_session:
session = aiohttp.ClientSession()
try:
semaphore = asyncio.Semaphore(concurrency)
async def probe(t):
async with semaphore:
return await fetch_segment(session, t + date, track_id)
for batch_start in range(0, total_requests, batch_size):
batch_end = min(batch_start + batch_size, total_requests)
tasks = [probe(t) 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)
for task in tasks:
task.cancel()
break
except KeyboardInterrupt:
logger.error("Interrupted by user (Ctrl+C)")
finally:
if owns_session:
await session.close()
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