"""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"}