Docker report (HellRa1SeR, 2.7.8): pasting YouTube cookies threw
`ERROR: unsupported browser: "custom"`. the 'Paste cookies.txt' dropdown value is the sentinel
'custom', and youtube_client built `cookiesfrombrowser=('custom',)` from it — yt-dlp rejects
that since 'custom' isn't a browser. (server/Docker users have no local browser, so pasted
cookies.txt is the ONLY way to authenticate yt-dlp — e.g. for private 'Liked Music'.)
the correct precedence already existed in core.youtube_cookies.build_youtube_cookie_opts
('custom' -> cookiefile, browser name -> cookiesfrombrowser, falsy -> anonymous) and the web
layer used it — but youtube_client never got migrated and kept 5 inline cookiesfrombrowser
sites. route all of them through a shared _resolve_cookie_opts() that delegates to the tested
helper (reads youtube.cookies_browser + youtube.cookies_file, checks the file exists). also:
the no-cookies download retry now drops cookiefile too (not just cookiesfrombrowser), and
removed 3 now-dead config_manager imports.
3 regression tests (custom->cookiefile, browser unchanged, custom+missing-file->anonymous);
97 youtube tests green.
132 lines
5.8 KiB
Python
132 lines
5.8 KiB
Python
"""Settings → YouTube cookie options: browser store vs a pasted cookies.txt.
|
|
|
|
#902: syncing a YouTube *Music* "Liked Music" playlist (list=LM) needs auth, and on
|
|
a server/Docker box there's no local browser for cookiesfrombrowser to read — so we
|
|
let users paste a cookies.txt (yt-dlp cookiefile). These pin the precedence (so the
|
|
two cookie sources can never both be emitted), the paste validation (junk must not be
|
|
written out and break yt-dlp), and the fail-safe write (a blank save never wipes a
|
|
saved file).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.youtube_cookies import (
|
|
PASTE_MODE,
|
|
build_youtube_cookie_opts,
|
|
looks_like_cookiefile,
|
|
write_pasted_cookiefile,
|
|
)
|
|
|
|
NETSCAPE = (
|
|
"# Netscape HTTP Cookie File\n"
|
|
".youtube.com\tTRUE\t/\tTRUE\t1999999999\tLOGIN_INFO\tsecretvalue\n"
|
|
".youtube.com\tTRUE\t/\tTRUE\t1999999999\tSID\tanother\n"
|
|
)
|
|
|
|
|
|
# ── precedence (pure opts) ──────────────────────────────────────────────────
|
|
|
|
def test_empty_mode_is_anonymous():
|
|
assert build_youtube_cookie_opts("") == {}
|
|
assert build_youtube_cookie_opts(None) == {}
|
|
|
|
|
|
def test_browser_mode_uses_cookiesfrombrowser():
|
|
assert build_youtube_cookie_opts("firefox") == {"cookiesfrombrowser": ("firefox",)}
|
|
|
|
|
|
def test_paste_mode_uses_cookiefile_when_present():
|
|
opts = build_youtube_cookie_opts(PASTE_MODE, "/cfg/youtube_cookies.txt", cookiefile_exists=True)
|
|
assert opts == {"cookiefile": "/cfg/youtube_cookies.txt"}
|
|
|
|
|
|
def test_paste_mode_without_a_real_file_is_anonymous_not_broken():
|
|
# stale/missing path must NOT become a cookiefile arg yt-dlp would choke on
|
|
assert build_youtube_cookie_opts(PASTE_MODE, "/cfg/gone.txt", cookiefile_exists=False) == {}
|
|
assert build_youtube_cookie_opts(PASTE_MODE, "", cookiefile_exists=True) == {}
|
|
|
|
|
|
def test_sources_are_mutually_exclusive():
|
|
# a browser name is never PASTE_MODE, so cookiefile + cookiesfrombrowser can't co-occur
|
|
for mode in ("chrome", "firefox", PASTE_MODE, ""):
|
|
opts = build_youtube_cookie_opts(mode, "/x.txt", cookiefile_exists=True)
|
|
assert not ("cookiefile" in opts and "cookiesfrombrowser" in opts)
|
|
|
|
|
|
# ── paste validation ────────────────────────────────────────────────────────
|
|
|
|
def test_accepts_netscape_header_and_cookie_rows():
|
|
assert looks_like_cookiefile(NETSCAPE) is True
|
|
# no header but a valid tab-separated cookie row still counts
|
|
assert looks_like_cookiefile(".youtube.com\tTRUE\t/\tTRUE\t123\tSID\tv") is True
|
|
|
|
|
|
def test_rejects_junk_paste():
|
|
assert looks_like_cookiefile("") is False
|
|
assert looks_like_cookiefile(" ") is False
|
|
assert looks_like_cookiefile(None) is False
|
|
assert looks_like_cookiefile("https://music.youtube.com/playlist?list=LM") is False
|
|
assert looks_like_cookiefile('{"cookies": []}') is False
|
|
assert looks_like_cookiefile("# Netscape HTTP Cookie File\n# only comments\n") is False
|
|
|
|
|
|
# ── fail-safe write ─────────────────────────────────────────────────────────
|
|
|
|
def test_write_persists_valid_cookiefile(tmp_path):
|
|
dest = tmp_path / "youtube_cookies.txt"
|
|
out = write_pasted_cookiefile(NETSCAPE, str(dest))
|
|
assert out == str(dest)
|
|
assert dest.read_text().startswith("# Netscape HTTP Cookie File")
|
|
|
|
|
|
def test_write_appends_trailing_newline(tmp_path):
|
|
dest = tmp_path / "c.txt"
|
|
write_pasted_cookiefile(NETSCAPE.rstrip("\n"), str(dest))
|
|
assert dest.read_text().endswith("\n")
|
|
|
|
|
|
def test_write_refuses_junk_and_leaves_no_file(tmp_path):
|
|
dest = tmp_path / "c.txt"
|
|
assert write_pasted_cookiefile("not a cookie file", str(dest)) == ""
|
|
assert not dest.exists()
|
|
|
|
|
|
def test_write_refuses_junk_without_clobbering_existing(tmp_path):
|
|
# a blank/garbage save must NOT wipe a previously-saved cookie file
|
|
dest = tmp_path / "c.txt"
|
|
write_pasted_cookiefile(NETSCAPE, str(dest))
|
|
before = dest.read_text()
|
|
assert write_pasted_cookiefile("", str(dest)) == ""
|
|
assert dest.read_text() == before
|
|
|
|
|
|
# ── regression: youtube_client must USE the helper, not pass 'custom' as a browser ──
|
|
# (Docker bug: pasted cookies threw yt-dlp 'unsupported browser: "custom"' because the
|
|
# client built cookiesfrombrowser=('custom',) instead of a cookiefile.)
|
|
|
|
def test_resolve_cookie_opts_routes_custom_to_cookiefile(monkeypatch, tmp_path):
|
|
import core.youtube_client as yt
|
|
cookiefile = tmp_path / "youtube_cookies.txt"
|
|
cookiefile.write_text(".youtube.com\tTRUE\t/\tTRUE\t123\tSID\tv\n")
|
|
cfg = {'youtube.cookies_browser': 'custom', 'youtube.cookies_file': str(cookiefile)}
|
|
monkeypatch.setattr('config.settings.config_manager.get',
|
|
lambda k, d=None: cfg.get(k, d))
|
|
opts = yt._resolve_cookie_opts()
|
|
assert opts == {'cookiefile': str(cookiefile)}
|
|
assert 'cookiesfrombrowser' not in opts # never the bogus browser arg
|
|
|
|
|
|
def test_resolve_cookie_opts_browser_mode_unchanged(monkeypatch):
|
|
import core.youtube_client as yt
|
|
cfg = {'youtube.cookies_browser': 'firefox', 'youtube.cookies_file': ''}
|
|
monkeypatch.setattr('config.settings.config_manager.get',
|
|
lambda k, d=None: cfg.get(k, d))
|
|
assert yt._resolve_cookie_opts() == {'cookiesfrombrowser': ('firefox',)}
|
|
|
|
|
|
def test_resolve_cookie_opts_custom_missing_file_is_anonymous(monkeypatch):
|
|
import core.youtube_client as yt
|
|
cfg = {'youtube.cookies_browser': 'custom', 'youtube.cookies_file': '/nope/gone.txt'}
|
|
monkeypatch.setattr('config.settings.config_manager.get',
|
|
lambda k, d=None: cfg.get(k, d))
|
|
assert yt._resolve_cookie_opts() == {} # not a broken cookiefile arg
|