Import from URL now gets past YouTube's "Sign in to confirm you're not a bot": a run that downloads nothing for that reason retries once with yt-dlp's --cookies-from-browser (Firefox first), and a browser that worked is remembered in this machine's config.json so later imports send the cookies from the start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
5.8 KiB
Python
155 lines
5.8 KiB
Python
"""Round 59: YouTube's "Sign in to confirm you're not a bot".
|
||
|
||
YouTube flags a network, not a program, so `song` hits it too. The cure is
|
||
the browser's YouTube cookies: a run that downloads nothing because of the
|
||
check is retried once with --cookies-from-browser, and the browser is then
|
||
remembered in this machine's config.json.
|
||
"""
|
||
import os
|
||
import stat
|
||
import sys
|
||
|
||
import pytest
|
||
|
||
from lintunes import config, url_import
|
||
from lintunes.url_import import UrlImportWorker, build_command, is_bot_check
|
||
|
||
BOT_LINE = ("[youtube] m-7NEY4p5s0: Sign in to confirm you’re not a bot. "
|
||
"Use --cookies-from-browser or --cookies for the authentication.")
|
||
|
||
# Refuses anonymous requests the way YouTube does; lets cookies through.
|
||
# Each run appends its argv to $FAKE_YTDLP_LOG so tests can count attempts.
|
||
FAKE_YTDLP = '''#!/usr/bin/env python3
|
||
import os, sys
|
||
args = sys.argv[1:]
|
||
with open(os.environ["FAKE_YTDLP_LOG"], "a") as log:
|
||
log.write(" ".join(args) + "\\n")
|
||
if "--cookies-from-browser" not in args or os.environ.get("FAKE_ALWAYS_BOT"):
|
||
print("ERROR: " + os.environ["FAKE_BOT_LINE"], file=sys.stderr, flush=True)
|
||
sys.exit(1)
|
||
dest = args[args.index("-P") + 1]
|
||
path = os.path.join(dest, "Song [m-7NEY4p5s0].mp3")
|
||
open(path, "wb").write(b"ID3fake")
|
||
print("LTSTART 1\\t1\\tSong", flush=True)
|
||
print(f"LTFILE {path}", flush=True)
|
||
'''
|
||
|
||
|
||
@pytest.fixture
|
||
def fake(tmp_path, monkeypatch):
|
||
bin_dir = tmp_path / "bin"
|
||
bin_dir.mkdir()
|
||
script = bin_dir / "yt-dlp"
|
||
script.write_text(FAKE_YTDLP.replace("/usr/bin/env python3", sys.executable))
|
||
script.chmod(script.stat().st_mode | stat.S_IEXEC)
|
||
log = tmp_path / "argv.log"
|
||
monkeypatch.setenv("PATH", f"{bin_dir}{os.pathsep}{os.environ['PATH']}")
|
||
monkeypatch.setenv("FAKE_YTDLP_LOG", str(log))
|
||
monkeypatch.setenv("FAKE_BOT_LINE", BOT_LINE)
|
||
monkeypatch.setattr(url_import, "default_cookies_browser",
|
||
lambda: "firefox")
|
||
dest = tmp_path / "dl"
|
||
dest.mkdir()
|
||
return dest, lambda: (log.read_text().splitlines()
|
||
if log.exists() else [])
|
||
|
||
|
||
def _run(dest, **kw):
|
||
worker = UrlImportWorker("https://www.youtube.com/watch?v=m-7NEY4p5s0",
|
||
**kw)
|
||
worker.temp_dir = dest
|
||
events = []
|
||
worker.downloaded.connect(lambda p: events.append(("file", p)))
|
||
worker.finished.connect(lambda d: events.append(("finished", d)))
|
||
worker.failed.connect(lambda m: events.append(("failed", m)))
|
||
worker.cookies_used.connect(lambda b: events.append(("cookies", b)))
|
||
worker._run()
|
||
return events
|
||
|
||
|
||
def test_cookies_go_before_the_url_and_only_when_asked(tmp_path):
|
||
plain = build_command("https://x.test/v", tmp_path)
|
||
assert "--cookies-from-browser" not in plain
|
||
cmd = build_command("https://x.test/v", tmp_path, "firefox")
|
||
i = cmd.index("--cookies-from-browser")
|
||
assert cmd[i + 1] == "firefox"
|
||
assert i < cmd.index("--") and cmd[-1] == "https://x.test/v"
|
||
assert cmd[1:4] == url_import.SONG_ARGS
|
||
|
||
|
||
def test_is_bot_check():
|
||
assert is_bot_check(BOT_LINE)
|
||
assert is_bot_check(BOT_LINE.replace("’", "'"))
|
||
assert not is_bot_check("[generic] Unsupported URL: https://x.test")
|
||
assert not is_bot_check("")
|
||
|
||
|
||
def test_a_bot_check_retries_with_the_browsers_cookies(fake):
|
||
dest, argvs = fake
|
||
events = _run(dest)
|
||
assert len(argvs()) == 2
|
||
assert "--cookies-from-browser" not in argvs()[0]
|
||
assert "--cookies-from-browser firefox" in argvs()[1]
|
||
assert events == [("file", str(dest / "Song [m-7NEY4p5s0].mp3")),
|
||
("cookies", "firefox"),
|
||
("finished", {"downloaded": 1, "errors": [],
|
||
"cancelled": False})]
|
||
|
||
|
||
def test_a_remembered_browser_is_used_from_the_start(fake):
|
||
dest, argvs = fake
|
||
events = _run(dest, cookies_browser="firefox")
|
||
assert len(argvs()) == 1
|
||
assert "--cookies-from-browser firefox" in argvs()[0]
|
||
assert ("cookies", "firefox") not in events # nothing new to remember
|
||
assert events[-1][0] == "finished"
|
||
|
||
|
||
def test_cookies_that_dont_help_fail_with_advice(fake, monkeypatch):
|
||
dest, argvs = fake
|
||
monkeypatch.setenv("FAKE_ALWAYS_BOT", "1")
|
||
events = _run(dest)
|
||
assert len(argvs()) == 2 # one retry, not a loop
|
||
[(kind, message)] = events
|
||
assert kind == "failed"
|
||
assert "Sign in to YouTube in Firefox" in message
|
||
assert BOT_LINE[:40] in message
|
||
|
||
|
||
def test_no_browser_means_no_retry(fake, monkeypatch):
|
||
dest, argvs = fake
|
||
monkeypatch.setattr(url_import, "default_cookies_browser", lambda: None)
|
||
events = _run(dest)
|
||
assert len(argvs()) == 1
|
||
assert events[0][0] == "failed"
|
||
assert "your browser" in events[0][1]
|
||
|
||
|
||
def test_a_cancelled_run_is_not_retried(fake):
|
||
dest, argvs = fake
|
||
worker = UrlImportWorker("https://x.test/v")
|
||
worker.temp_dir = dest
|
||
worker._cancel.set()
|
||
worker._run()
|
||
assert len(argvs()) <= 1
|
||
|
||
|
||
def test_default_browser_prefers_firefox(tmp_path, monkeypatch):
|
||
monkeypatch.setattr(url_import.Path, "home", lambda: tmp_path)
|
||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / ".config"))
|
||
assert url_import.default_cookies_browser() is None
|
||
(tmp_path / ".config" / "chromium").mkdir(parents=True)
|
||
assert url_import.default_cookies_browser() == "chromium"
|
||
(tmp_path / ".mozilla" / "firefox").mkdir(parents=True)
|
||
assert url_import.default_cookies_browser() == "firefox"
|
||
|
||
|
||
def test_the_window_remembers_the_browser_in_config_json(
|
||
qapp, tmp_path, monkeypatch):
|
||
from lintunes.gui.main_window import MainWindow
|
||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
||
config.save_config({"music_root": "/somewhere"})
|
||
MainWindow._remember_cookies_browser(None, "firefox")
|
||
assert config.load_config() == {"music_root": "/somewhere",
|
||
url_import.COOKIES_KEY: "firefox"}
|