mirror of
https://github.com/NohamR/OqeeRewind.git
synced 2026-08-26 02:29:53 +00:00
- 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
148 lines
4.4 KiB
Python
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
|