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
This commit is contained in:
√(noham)²
2026-08-13 17:40:06 +02:00
parent a57f0a74a1
commit ae5035c6a8
4 changed files with 114 additions and 91 deletions

View File

@@ -55,45 +55,66 @@ def future(rep, base, duration):
return base + rep * duration
async def bruteforce(track_id, date, batch_size=20000):
"""Bruteforce segments to find valid ticks."""
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:
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)
]
semaphore = asyncio.Semaphore(concurrency)
results = []
for coro in tqdm(
asyncio.as_completed(tasks),
total=len(tasks),
desc="Bruteforce",
unit="req",
):
result = await coro
results.append(result)
async def probe(t):
async with semaphore:
return await fetch_segment(session, t + date, track_id)
valid_ticks.extend(
[r for r in results if r and not isinstance(r, Exception)]
)
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)]
# Stop if we found valid ticks
if valid_ticks:
logger.debug("Found valid ticks: %s, stopping bruteforce.", valid_ticks)
break
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)