Pause discovery metadata calls while a download is active

_meta_run now checks _active_downloads before each background yt-dlp call.
If a download is running it waits (3s poll loop) until the download finishes
before making the next metadata request.

This prevents YouTube from seeing the same session used simultaneously by
a download and a discovery/metadata call, which was causing cookie invalidation
even with private cookie copies.

Downloads still run immediately without waiting for metadata. Background
discovery is the one that yields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 02:12:43 +02:00
parent c3191aa000
commit 0a4dfb845e

View File

@@ -55,10 +55,25 @@ _meta_lock = threading.Lock()
_meta_last_call: float = 0.0 _meta_last_call: float = 0.0
_META_MIN_GAP = 5.0 # seconds between any two metadata requests _META_MIN_GAP = 5.0 # seconds between any two metadata requests
# Active download counter — _meta_run pauses while any download is running so
# background discovery and a concurrent download never share the YouTube session
# at the same time. Downloads set/clear this; _meta_run reads it.
_active_downloads: int = 0
_active_downloads_lock = threading.Lock()
def _meta_run(args: list[str], timeout: int = 60) -> tuple[str, str, int]: def _meta_run(args: list[str], timeout: int = 60) -> tuple[str, str, int]:
global _meta_last_call global _meta_last_call
with _meta_lock: with _meta_lock:
# Pause background metadata calls while a download is active.
# Running both concurrently causes YouTube to see two requests from the
# same session simultaneously, which triggers cookie invalidation.
while True:
with _active_downloads_lock:
if _active_downloads == 0:
break
time.sleep(3)
now = time.monotonic() now = time.monotonic()
wait = _META_MIN_GAP - (now - _meta_last_call) wait = _META_MIN_GAP - (now - _meta_last_call)
if wait > 0: if wait > 0:
@@ -884,63 +899,71 @@ def start_download(
) )
def _run_download(): def _run_download():
with _SEMAPHORE: global _active_downloads
cookie_args = _cookie_args() # Signal to _meta_run that a download is active so it pauses all
print(f"[ytdlp] cookie_args={cookie_args!r}", flush=True) # background discovery/metadata calls for the duration. Running both
cmd = [ # concurrently causes YouTube to see the same session used simultaneously
"yt-dlp", url, # and invalidates cookies.
"-f", fmt, with _active_downloads_lock:
"--merge-output-format", "mp4", _active_downloads += 1
"--no-part", "--no-mtime", try:
"-o", output_template, with _SEMAPHORE:
"--newline", "--progress", "--no-colors", cookie_args = _cookie_args()
*subtitle_args, print(f"[ytdlp] cookie_args={cookie_args!r}", flush=True)
*cookie_args, cmd = [
] "yt-dlp", url,
# Private cookie copy — download runs for minutes; without this, "-f", fmt,
# concurrent metadata calls would write-back to the same cookie file "--merge-output-format", "mp4",
# and corrupt the session. "--no-part", "--no-mtime",
cmd, tmp_cookie_path = _make_private_cookie_copy(cmd) "-o", output_template,
try: "--newline", "--progress", "--no-colors",
process = subprocess.Popen( *subtitle_args,
cmd, *cookie_args,
stdout=subprocess.PIPE, ]
stderr=subprocess.STDOUT, cmd, tmp_cookie_path = _make_private_cookie_copy(cmd)
text=True, try:
) process = subprocess.Popen(
file_path = None cmd,
stream_index = 0 stdout=subprocess.PIPE,
output_lines: list[str] = [] stderr=subprocess.STDOUT,
for line in process.stdout: text=True,
line = line.strip() )
output_lines.append(line) file_path = None
if re.search(r"\[download\] Destination:", line): stream_index = 0
stream_index += 1 output_lines: list[str] = []
m = re.search(r"\[download\]\s+([\d.]+)%", line) for line in process.stdout:
if m: line = line.strip()
pct = float(m.group(1)) output_lines.append(line)
scaled = pct * 0.85 if stream_index <= 1 else 85.0 + pct * 0.10 if re.search(r"\[download\] Destination:", line):
on_progress(download_id, min(scaled, 95.0)) stream_index += 1
m2 = re.search(r"\[(?:download|Merger)\] Destination: (.+)", line) m = re.search(r"\[download\]\s+([\d.]+)%", line)
if m2: if m:
file_path = m2.group(1).strip() pct = float(m.group(1))
scaled = pct * 0.85 if stream_index <= 1 else 85.0 + pct * 0.10
on_progress(download_id, min(scaled, 95.0))
m2 = re.search(r"\[(?:download|Merger)\] Destination: (.+)", line)
if m2:
file_path = m2.group(1).strip()
process.wait() process.wait()
if process.returncode == 0: if process.returncode == 0:
_strip_vtt_cue_settings(video_id) _strip_vtt_cue_settings(video_id)
resolution = detect_resolution(file_path) if file_path else None resolution = detect_resolution(file_path) if file_path else None
on_complete(download_id, file_path, resolution) on_complete(download_id, file_path, resolution)
else: else:
tail = "\n".join(output_lines[-20:]) if output_lines else "(no output)" tail = "\n".join(output_lines[-20:]) if output_lines else "(no output)"
import logging import logging
logging.getLogger(__name__).error("yt-dlp failed (code %d):\n%s", process.returncode, tail) logging.getLogger(__name__).error("yt-dlp failed (code %d):\n%s", process.returncode, tail)
on_error(download_id, f"yt-dlp exited with code {process.returncode}:\n{tail}") on_error(download_id, f"yt-dlp exited with code {process.returncode}:\n{tail}")
finally: finally:
if tmp_cookie_path: if tmp_cookie_path:
try: try:
os.unlink(tmp_cookie_path) os.unlink(tmp_cookie_path)
except Exception: except Exception:
pass pass
finally:
with _active_downloads_lock:
_active_downloads -= 1
thread = threading.Thread(target=_run_download, daemon=True) thread = threading.Thread(target=_run_download, daemon=True)
thread.start() thread.start()