v0.22.1: not a bot
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>
This commit is contained in:
@@ -279,7 +279,13 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
|||||||
order (`position + songs already inserted`). With no AcoustID key,
|
order (`position + songs already inserted`). With no AcoustID key,
|
||||||
`TrackIdentifier` proposes from the filename alone and never touches fpcalc
|
`TrackIdentifier` proposes from the filename alone and never touches fpcalc
|
||||||
or the network. Only a URL import reaches that path, since the menu route
|
or the network. Only a URL import reaches that path, since the menu route
|
||||||
still insists on setup first.
|
still insists on setup first. YouTube's "Sign in to confirm you're not a
|
||||||
|
bot" is about the *network* (`song` hits it too), and yt-dlp's cure is the
|
||||||
|
browser's cookies: a run that downloads **nothing** for that reason is
|
||||||
|
retried once with `--cookies-from-browser` (`default_cookies_browser`,
|
||||||
|
Firefox first), and a browser that worked is saved as
|
||||||
|
`ytdlp_cookies_browser` in **`config.json`**, not preferences — which
|
||||||
|
browser holds a YouTube login is this machine's business (Round 59).
|
||||||
|
|
||||||
- **`lintunes/mpris.py`** — registers `org.mpris.MediaPlayer2.lintunes` over D-Bus
|
- **`lintunes/mpris.py`** — registers `org.mpris.MediaPlayer2.lintunes` over D-Bus
|
||||||
so the desktop's media keys / now-playing popup control playback. Spacebar and
|
so the desktop's media keys / now-playing popup control playback. Spacebar and
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
||||||
|
|
||||||
__version__ = "0.22.0"
|
__version__ = "0.22.1"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from lintunes.andtunes import sync as andtunes_sync
|
|||||||
from lintunes.art_search import AlbumArtFetcher
|
from lintunes.art_search import AlbumArtFetcher
|
||||||
from lintunes.cast.controller import CastController
|
from lintunes.cast.controller import CastController
|
||||||
from lintunes.eventlog import log_control
|
from lintunes.eventlog import log_control
|
||||||
|
from lintunes.config import load_config, save_config
|
||||||
from lintunes.export import exporter
|
from lintunes.export import exporter
|
||||||
from lintunes.fingerprint import TrackIdentifier, fpcalc_available
|
from lintunes.fingerprint import TrackIdentifier, fpcalc_available
|
||||||
from lintunes.inhibit import INHIBIT_LOGOUT, INHIBIT_SUSPEND, SleepInhibitor
|
from lintunes.inhibit import INHIBIT_LOGOUT, INHIBIT_SUSPEND, SleepInhibitor
|
||||||
@@ -1161,7 +1162,10 @@ class MainWindow(QMainWindow):
|
|||||||
self._url_note = (f"tags proposed from filenames only ({problem})"
|
self._url_note = (f"tags proposed from filenames only ({problem})"
|
||||||
if problem else "")
|
if problem else "")
|
||||||
|
|
||||||
worker = url_import.UrlImportWorker(dialog.url(), self)
|
worker = url_import.UrlImportWorker(
|
||||||
|
dialog.url(), self,
|
||||||
|
cookies_browser=load_config().get(url_import.COOKIES_KEY))
|
||||||
|
worker.cookies_used.connect(self._remember_cookies_browser)
|
||||||
worker.item_started.connect(self._on_url_item_started)
|
worker.item_started.connect(self._on_url_item_started)
|
||||||
worker.progress.connect(self._sync_progress.setValue)
|
worker.progress.connect(self._sync_progress.setValue)
|
||||||
worker.downloaded.connect(self._on_url_downloaded)
|
worker.downloaded.connect(self._on_url_downloaded)
|
||||||
@@ -1176,6 +1180,15 @@ class MainWindow(QMainWindow):
|
|||||||
self._sync_progress.show()
|
self._sync_progress.show()
|
||||||
worker.start()
|
worker.start()
|
||||||
|
|
||||||
|
def _remember_cookies_browser(self, browser: str):
|
||||||
|
"""A bot-checked link got through on this browser's cookies: send
|
||||||
|
them from the start next time. config.json, not preferences — the
|
||||||
|
browser holding a YouTube login is this machine's business."""
|
||||||
|
config = load_config()
|
||||||
|
if config.get(url_import.COOKIES_KEY) != browser:
|
||||||
|
config[url_import.COOKIES_KEY] = browser
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
def _on_url_item_started(self, index: int, count: int, title: str):
|
def _on_url_item_started(self, index: int, count: int, title: str):
|
||||||
short = title if len(title) <= 40 else title[:39] + "…"
|
short = title if len(title) <= 40 else title[:39] + "…"
|
||||||
where = f"{index} of {count} · " if count > 1 else ""
|
where = f"{index} of {count} · " if count > 1 else ""
|
||||||
|
|||||||
+87
-16
@@ -16,6 +16,15 @@ deletes it (``cleanup``) once it has seen ``finished`` or ``failed``: those
|
|||||||
signals are queued after every ``downloaded``, so by then every file has been
|
signals are queued after every ``downloaded``, so by then every file has been
|
||||||
copied. yt-dlp's default filename, ``Title [id].mp3``, is kept on purpose,
|
copied. yt-dlp's default filename, ``Title [id].mp3``, is kept on purpose,
|
||||||
because it's exactly what ``filename_tags.parse_filename`` reads.
|
because it's exactly what ``filename_tags.parse_filename`` reads.
|
||||||
|
|
||||||
|
YouTube sometimes answers an anonymous request with "Sign in to confirm
|
||||||
|
you're not a bot" (it flags the *network*, so cell connections see it
|
||||||
|
most). The cure yt-dlp names is the browser's own YouTube cookies. So a run
|
||||||
|
that downloads nothing because of that check is retried once with
|
||||||
|
``--cookies-from-browser``, and the GUI remembers the browser in this
|
||||||
|
machine's ``config.json`` (``ytdlp_cookies_browser``) so later imports send
|
||||||
|
the cookies from the start. Which browser holds a YouTube login is a fact
|
||||||
|
about this machine, which is why it isn't in the synced preferences.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -40,6 +49,31 @@ FILE = "LTFILE"
|
|||||||
PROGRESS = "LTPROG"
|
PROGRESS = "LTPROG"
|
||||||
|
|
||||||
|
|
||||||
|
# What YouTube says when it wants a signed-in browser. Matched on a fragment
|
||||||
|
# that sidesteps the apostrophe, which yt-dlp prints as a curly ’.
|
||||||
|
BOT_CHECK = "to confirm you"
|
||||||
|
|
||||||
|
# The config.json key naming the browser yt-dlp borrows cookies from.
|
||||||
|
COOKIES_KEY = "ytdlp_cookies_browser"
|
||||||
|
|
||||||
|
|
||||||
|
def is_bot_check(error: str) -> bool:
|
||||||
|
return BOT_CHECK in (error or "") and "bot" in error
|
||||||
|
|
||||||
|
|
||||||
|
def default_cookies_browser() -> str | None:
|
||||||
|
"""The browser whose cookies to borrow: the first one with a profile on
|
||||||
|
this machine, or None when there's nothing to borrow from."""
|
||||||
|
home = Path.home()
|
||||||
|
config = Path(os.environ.get("XDG_CONFIG_HOME", str(home / ".config")))
|
||||||
|
for browser, profile in (("firefox", home / ".mozilla" / "firefox"),
|
||||||
|
("chromium", config / "chromium"),
|
||||||
|
("chrome", config / "google-chrome")):
|
||||||
|
if profile.is_dir():
|
||||||
|
return browser
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def ytdlp_available() -> bool:
|
def ytdlp_available() -> bool:
|
||||||
"""Whether the yt-dlp *binary* is on PATH (the fpcalc_available pattern)."""
|
"""Whether the yt-dlp *binary* is on PATH (the fpcalc_available pattern)."""
|
||||||
return shutil.which("yt-dlp") is not None
|
return shutil.which("yt-dlp") is not None
|
||||||
@@ -52,7 +86,8 @@ def looks_like_url(text: str) -> bool:
|
|||||||
and not any(c.isspace() for c in text))
|
and not any(c.isspace() for c in text))
|
||||||
|
|
||||||
|
|
||||||
def build_command(url: str, dest_dir) -> list[str]:
|
def build_command(url: str, dest_dir,
|
||||||
|
cookies_browser: str | None = None) -> list[str]:
|
||||||
"""yt-dlp with the `song` flags, downloading into ``dest_dir``.
|
"""yt-dlp with the `song` flags, downloading into ``dest_dir``.
|
||||||
|
|
||||||
``before_dl`` names each item as it starts (with its place in a playlist),
|
``before_dl`` names each item as it starts (with its place in a playlist),
|
||||||
@@ -61,8 +96,11 @@ def build_command(url: str, dest_dir) -> list[str]:
|
|||||||
yt-dlp's default already carries on past an unavailable video in a
|
yt-dlp's default already carries on past an unavailable video in a
|
||||||
playlist, so "import everything at the link" needs no extra flag.
|
playlist, so "import everything at the link" needs no extra flag.
|
||||||
"""
|
"""
|
||||||
|
cookies = (["--cookies-from-browser", cookies_browser]
|
||||||
|
if cookies_browser else [])
|
||||||
return [
|
return [
|
||||||
"yt-dlp", *SONG_ARGS,
|
"yt-dlp", *SONG_ARGS,
|
||||||
|
*cookies,
|
||||||
"-P", str(dest_dir),
|
"-P", str(dest_dir),
|
||||||
"--print",
|
"--print",
|
||||||
f"before_dl:{START} %(playlist_index|1)s\t%(n_entries|1)s\t%(title)s",
|
f"before_dl:{START} %(playlist_index|1)s\t%(n_entries|1)s\t%(title)s",
|
||||||
@@ -167,7 +205,8 @@ class UrlImportWorker(QObject):
|
|||||||
|
|
||||||
``finished`` carries {"downloaded": int, "errors": [str], "cancelled":
|
``finished`` carries {"downloaded": int, "errors": [str], "cancelled":
|
||||||
bool}. ``failed`` means nothing downloaded at all, and carries yt-dlp's
|
bool}. ``failed`` means nothing downloaded at all, and carries yt-dlp's
|
||||||
last error line.
|
last error line. ``cookies_used`` names the browser whose cookies got a
|
||||||
|
bot-checked link through, so the caller can remember it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item_started = pyqtSignal(int, int, str) # index, count, title
|
item_started = pyqtSignal(int, int, str) # index, count, title
|
||||||
@@ -175,10 +214,13 @@ class UrlImportWorker(QObject):
|
|||||||
downloaded = pyqtSignal(str) # final path of one mp3
|
downloaded = pyqtSignal(str) # final path of one mp3
|
||||||
finished = pyqtSignal(dict)
|
finished = pyqtSignal(dict)
|
||||||
failed = pyqtSignal(str)
|
failed = pyqtSignal(str)
|
||||||
|
cookies_used = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, url: str, parent=None):
|
def __init__(self, url: str, parent=None, *,
|
||||||
|
cookies_browser: str | None = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._url = url
|
self._url = url
|
||||||
|
self._cookies = cookies_browser
|
||||||
self._busy = False
|
self._busy = False
|
||||||
self._cancel = threading.Event()
|
self._cancel = threading.Event()
|
||||||
self._proc = None
|
self._proc = None
|
||||||
@@ -224,11 +266,50 @@ class UrlImportWorker(QObject):
|
|||||||
self.failed.emit(str(e))
|
self.failed.emit(str(e))
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
|
cookies = self._cookies
|
||||||
|
count, errors, returncode = self._attempt(cookies)
|
||||||
|
if returncode is None: # yt-dlp couldn't even start
|
||||||
|
return
|
||||||
|
if (count == 0 and not self._cancel.is_set() and not cookies
|
||||||
|
and any(is_bot_check(e) for e in errors)):
|
||||||
|
# Nothing landed, so a second pass can't duplicate a song.
|
||||||
|
cookies = default_cookies_browser()
|
||||||
|
if cookies:
|
||||||
|
count, errors, returncode = self._attempt(cookies)
|
||||||
|
if returncode is None:
|
||||||
|
return
|
||||||
|
if count:
|
||||||
|
self.cookies_used.emit(cookies)
|
||||||
|
|
||||||
|
cancelled = self._cancel.is_set()
|
||||||
|
self._busy = False
|
||||||
|
if count == 0 and not cancelled:
|
||||||
|
if errors and is_bot_check(errors[-1]):
|
||||||
|
self.failed.emit(
|
||||||
|
"YouTube wants to see a signed-in browser before it "
|
||||||
|
"will hand this over"
|
||||||
|
+ (f" (tried {cookies}'s cookies)" if cookies else "")
|
||||||
|
+ ".\n\nSign in to YouTube in "
|
||||||
|
+ (cookies.title() if cookies else "your browser")
|
||||||
|
+ " and try again.\n\nyt-dlp said: " + errors[-1])
|
||||||
|
return
|
||||||
|
self.failed.emit(
|
||||||
|
errors[-1] if errors else
|
||||||
|
f"yt-dlp found nothing to download (exit code "
|
||||||
|
f"{returncode})")
|
||||||
|
return
|
||||||
|
self.finished.emit({"downloaded": count, "errors": errors,
|
||||||
|
"cancelled": cancelled})
|
||||||
|
|
||||||
|
def _attempt(self, cookies_browser):
|
||||||
|
"""One yt-dlp run → (songs downloaded, ERROR lines, exit code). The
|
||||||
|
exit code is None when yt-dlp couldn't be started, in which case
|
||||||
|
``failed`` has already been emitted."""
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
count = 0
|
count = 0
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
build_command(self._url, self.temp_dir),
|
build_command(self._url, self.temp_dir, cookies_browser),
|
||||||
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
# One stream: the markers are on stdout, the ERROR: lines
|
# One stream: the markers are on stdout, the ERROR: lines
|
||||||
# on stderr, and one reader can't deadlock on the other.
|
# on stderr, and one reader can't deadlock on the other.
|
||||||
@@ -238,7 +319,7 @@ class UrlImportWorker(QObject):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
self._busy = False
|
self._busy = False
|
||||||
self.failed.emit(f"couldn't run yt-dlp: {e}")
|
self.failed.emit(f"couldn't run yt-dlp: {e}")
|
||||||
return
|
return 0, errors, None
|
||||||
self._proc = proc
|
self._proc = proc
|
||||||
if self._cancel.is_set(): # cancelled before the process existed
|
if self._cancel.is_set(): # cancelled before the process existed
|
||||||
self._terminate()
|
self._terminate()
|
||||||
@@ -258,14 +339,4 @@ class UrlImportWorker(QObject):
|
|||||||
count += 1
|
count += 1
|
||||||
self.downloaded.emit(parsed[1])
|
self.downloaded.emit(parsed[1])
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
return count, errors, proc.returncode
|
||||||
cancelled = self._cancel.is_set()
|
|
||||||
self._busy = False
|
|
||||||
if count == 0 and not cancelled:
|
|
||||||
self.failed.emit(
|
|
||||||
errors[-1] if errors else
|
|
||||||
f"yt-dlp found nothing to download (exit code "
|
|
||||||
f"{proc.returncode})")
|
|
||||||
return
|
|
||||||
self.finished.emit({"downloaded": count, "errors": errors,
|
|
||||||
"cancelled": cancelled})
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""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"}
|
||||||
Reference in New Issue
Block a user