v0.36.0: a playlist link says so before it downloads
Pasting a song from a YouTube Mix (watch?v=…&list=RD…) imported song after song: yt-dlp takes the whole list by default, and a Mix runs to thousands of rows and loops back on itself, so the linked song came in again and again with nothing on screen saying why. Now anything but a plain song link is listed first (--flat-playlist, streamed into the dialog). One song goes straight through. Several become a checklist: a song-in-a-list link ticks only its song and offers Just This Song without waiting, and a playlist link ticks everything. Rows are de-duplicated by id. The chosen songs download by their own pages, never the list link, and more than one gets a per-song progress window (Hide keeps downloading; the status-bar line brings it back). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+13
-6
@@ -50,7 +50,7 @@ if mode == "fail":
|
||||
src = os.environ.get("FAKE_YTDLP_AUDIO")
|
||||
titles = ["First Song", "Second Song"]
|
||||
for i, title in enumerate(titles, 1):
|
||||
print(f"LTSTART {i}\\t{len(titles)}\\t{title}", flush=True)
|
||||
print(f"LTSTART {i}\\t{len(titles)}\\tid{i}\\t{title}", flush=True)
|
||||
print("[download] some noise yt-dlp prints", flush=True)
|
||||
print("LTPROG 50.0%", flush=True)
|
||||
path = os.path.join(dest, f"{title} [id{i}].mp3")
|
||||
@@ -127,9 +127,10 @@ class TestCommand:
|
||||
assert "-o" not in cmd and "--output" not in cmd
|
||||
|
||||
@pytest.mark.parametrize("line, expected", [
|
||||
("LTSTART 2\t5\tSome Song\n", ("start", 2, 5, "Some Song")),
|
||||
("LTSTART NA\tNA\tSolo\n", ("start", 1, 1, "Solo")),
|
||||
("LTSTART 1\t1\tTab\tin title\n", ("start", 1, 1, "Tab\tin title")),
|
||||
("LTSTART 2\t5\tab1\tSome Song\n", ("start", 2, 5, "ab1", "Some Song")),
|
||||
("LTSTART NA\tNA\tx\tSolo\n", ("start", 1, 1, "x", "Solo")),
|
||||
("LTSTART 1\t1\tx\tTab\tin title\n",
|
||||
("start", 1, 1, "x", "Tab\tin title")),
|
||||
("LTFILE /tmp/x/Song [abc].mp3\n", ("file", "/tmp/x/Song [abc].mp3")),
|
||||
("LTPROG 45.3%\n", ("progress", 45)),
|
||||
("LTPROG 100.0%\r\n", ("progress", 100)),
|
||||
@@ -210,7 +211,8 @@ def _run_worker(url, dest, cancel_on_first=False):
|
||||
worker = UrlImportWorker(url)
|
||||
worker.temp_dir = dest
|
||||
events = []
|
||||
worker.item_started.connect(lambda i, n, t: events.append(("start", i, n, t)))
|
||||
worker.item_started.connect(
|
||||
lambda i, n, vid, t: events.append(("start", i, n, t)))
|
||||
worker.progress.connect(lambda p: events.append(("progress", p)))
|
||||
|
||||
def on_file(path):
|
||||
@@ -330,8 +332,13 @@ class TestWindow:
|
||||
monkeypatch.setenv("FAKE_YTDLP_AUDIO", str(mp3_file))
|
||||
|
||||
class _Dialog:
|
||||
def __init__(self, description, parent=None):
|
||||
cookies_used = None
|
||||
def __init__(self, description, parent=None, **_kw):
|
||||
assert "T2" in description
|
||||
def chosen_entries(self):
|
||||
return []
|
||||
def download_urls(self):
|
||||
return [self.url()]
|
||||
def exec(self):
|
||||
return True
|
||||
def url(self):
|
||||
|
||||
@@ -30,7 +30,7 @@ if "--cookies-from-browser" not in args or os.environ.get("FAKE_ALWAYS_BOT"):
|
||||
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("LTSTART 1\\t1\\tm-7NEY4p5s0\\tSong", flush=True)
|
||||
print(f"LTFILE {path}", flush=True)
|
||||
'''
|
||||
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
"""Round 75 — a link that is a playlist says so before it downloads.
|
||||
|
||||
trav pasted a song from a YouTube Mix (``watch?v=…&list=RD…``). yt-dlp's
|
||||
default is to take the whole list, so LinTunes started importing song after
|
||||
song from a list of over 900, with nothing on screen saying why. Now:
|
||||
|
||||
- only a plain song link skips the listing (``link_kind``);
|
||||
- anything else is listed first (``PlaylistProbe``). One song goes straight
|
||||
through, and several become a checklist: a song-in-a-list link ticks only
|
||||
its song, a playlist link ticks them all;
|
||||
- the chosen songs are downloaded by their own pages, never the list link;
|
||||
- an import of several songs gets a per-song progress window.
|
||||
|
||||
yt-dlp itself is never run. A fake on PATH speaks the same marker lines.
|
||||
"""
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from lintunes import url_import
|
||||
from lintunes.gui.url_import_dialog import (
|
||||
UrlImportDialog, format_duration, linked_song_id,
|
||||
)
|
||||
from lintunes.gui.url_import_progress import (
|
||||
IMPORTED, NOT_DOWNLOADED, SKIPPED, WAITING, UrlImportProgress,
|
||||
)
|
||||
from lintunes.library_manager import LibraryManager
|
||||
from lintunes.models import Library
|
||||
from lintunes.preferences import Preferences
|
||||
from lintunes.url_import import (
|
||||
PlaylistProbe, UrlImportWorker, error_item, link_kind,
|
||||
)
|
||||
|
||||
MIX = "https://www.youtube.com/watch?app=desktop&v=bbb&list=RDbbb&start_radio=1"
|
||||
|
||||
# Lists $FAKE_ENTRIES songs (aaa, bbb, ccc, …) for --flat-playlist; downloads
|
||||
# each URL it's given otherwise, failing any whose id starts with "bad".
|
||||
# Every run appends its argv to $FAKE_YTDLP_LOG.
|
||||
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("\\x1f".join(args) + "\\n")
|
||||
urls = args[args.index("--") + 1:]
|
||||
if "--flat-playlist" in args:
|
||||
n = int(os.environ.get("FAKE_ENTRIES", "3"))
|
||||
loops = int(os.environ.get("FAKE_LOOPS", "1"))
|
||||
for i in list(range(n)) * loops:
|
||||
vid = chr(ord("a") + i) * 3
|
||||
print(f"LTENTRY {vid}\\t{60 * (i + 1)}.0\\t"
|
||||
f"https://www.youtube.com/watch?v={vid}\\tSong {vid}", flush=True)
|
||||
if n == 0:
|
||||
print("ERROR: [generic] Unsupported URL: " + urls[0], file=sys.stderr,
|
||||
flush=True)
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
dest = args[args.index("-P") + 1]
|
||||
for url in urls:
|
||||
vid = url.rsplit("=", 1)[-1]
|
||||
if vid.startswith("bad"):
|
||||
print(f"ERROR: [youtube] {vid}: Video unavailable", file=sys.stderr,
|
||||
flush=True)
|
||||
continue
|
||||
print(f"LTSTART 1\\t1\\t{vid}\\tSong {vid}", flush=True)
|
||||
print("LTPROG 50.0%", flush=True)
|
||||
path = os.path.join(dest, f"Song {vid} [{vid}].mp3")
|
||||
src = os.environ.get("FAKE_YTDLP_AUDIO")
|
||||
with open(path, "wb") as f:
|
||||
f.write(open(src, "rb").read() if src else b"ID3fake")
|
||||
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("XDG_CONFIG_HOME", str(tmp_path / "config"))
|
||||
return lambda: ([line.split("\x1f") for line in
|
||||
log.read_text().splitlines()] if log.exists() else [])
|
||||
|
||||
|
||||
def _pump(qapp, until, timeout=10.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while not until() and time.monotonic() < deadline:
|
||||
qapp.processEvents()
|
||||
time.sleep(0.01)
|
||||
assert until(), "timed out waiting"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# A. what a link names, from the URL alone
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url, kind", [
|
||||
("https://www.youtube.com/watch?v=abc", "song"),
|
||||
("https://m.youtube.com/watch?v=abc&t=30", "song"),
|
||||
("https://music.youtube.com/watch?v=abc", "song"),
|
||||
("https://youtu.be/abc", "song"),
|
||||
("https://youtu.be/abc?list=PL1", "song_in_list"),
|
||||
("https://www.youtube.com/shorts/abc", "song"),
|
||||
(MIX, "song_in_list"),
|
||||
("https://www.youtube.com/watch?v=abc&list=PLxyz&index=4", "song_in_list"),
|
||||
("https://www.youtube.com/playlist?list=PLxyz", "list"),
|
||||
("https://soundcloud.com/someone/sets/an-album", "unknown"),
|
||||
("https://soundcloud.com/someone/a-song", "unknown"),
|
||||
("https://www.youtube.com/@channel", "unknown"),
|
||||
("not a url", "unknown"),
|
||||
])
|
||||
def test_link_kind(url, kind):
|
||||
assert link_kind(url) == kind
|
||||
|
||||
|
||||
def test_linked_song_id():
|
||||
assert linked_song_id(MIX) == "bbb"
|
||||
assert linked_song_id("https://youtu.be/xyz?list=PL") == "xyz"
|
||||
assert linked_song_id("https://soundcloud.com/a/b") == ""
|
||||
|
||||
|
||||
def test_format_duration():
|
||||
assert format_duration(None) == ""
|
||||
assert format_duration(65) == "1:05"
|
||||
assert format_duration(3725) == "1:02:05"
|
||||
|
||||
|
||||
def test_entry_and_error_lines():
|
||||
assert url_import.parse_line(
|
||||
"LTENTRY abc\t195.0\thttps://y.test/watch?v=abc\tA: Song\tX\n") == (
|
||||
"entry", {"id": "abc", "duration": 195,
|
||||
"url": "https://y.test/watch?v=abc", "title": "A: Song\tX"})
|
||||
assert url_import.parse_line("LTENTRY abc\tNA\thttps://y/v\tT\n")[1][
|
||||
"duration"] is None
|
||||
# no page to download it from: not an entry we can use
|
||||
assert url_import.parse_line("LTENTRY abc\t1\t\tT\n") is None
|
||||
assert error_item("[youtube] -5o7lcpMDqs: Video unavailable") == (
|
||||
"-5o7lcpMDqs", "Video unavailable")
|
||||
assert error_item("Unable to download webpage") is None
|
||||
|
||||
|
||||
def test_several_urls_all_follow_the_double_dash(tmp_path):
|
||||
cmd = url_import.build_command(["https://a/1", "https://a/2"], tmp_path)
|
||||
assert cmd[cmd.index("--"):] == ["--", "https://a/1", "https://a/2"]
|
||||
probe = url_import.build_probe_command(MIX, "firefox")
|
||||
assert "--flat-playlist" in probe and probe[-2:] == ["--", MIX]
|
||||
assert probe[probe.index("--cookies-from-browser") + 1] == "firefox"
|
||||
assert "--extract-audio" not in probe # listing downloads nothing
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# B. the probe and the worker
|
||||
|
||||
|
||||
def test_probe_streams_entries_in_order(fake):
|
||||
probe = PlaylistProbe(MIX)
|
||||
got = []
|
||||
probe.entry.connect(lambda e: got.append(e["id"]))
|
||||
probe.finished.connect(lambda n: got.append(("finished", n)))
|
||||
probe._run()
|
||||
assert got == ["aaa", "bbb", "ccc", ("finished", 3)]
|
||||
assert not probe.busy()
|
||||
|
||||
|
||||
def test_a_probe_that_finds_nothing_fails(fake, monkeypatch):
|
||||
monkeypatch.setenv("FAKE_ENTRIES", "0")
|
||||
probe = PlaylistProbe("https://x.test/nothing")
|
||||
got = []
|
||||
probe.failed.connect(got.append)
|
||||
probe._run()
|
||||
assert got == ["[generic] Unsupported URL: https://x.test/nothing"]
|
||||
|
||||
|
||||
def test_worker_counts_across_urls_and_names_failures(fake, tmp_path):
|
||||
worker = UrlImportWorker(["https://y/watch?v=aaa",
|
||||
"https://y/watch?v=badone",
|
||||
"https://y/watch?v=ccc"])
|
||||
worker.temp_dir = tmp_path / "dl"
|
||||
worker.temp_dir.mkdir()
|
||||
events = []
|
||||
worker.item_started.connect(
|
||||
lambda i, n, vid, t: events.append(("start", i, n, vid)))
|
||||
worker.item_failed.connect(lambda vid, why: events.append(("bad", vid)))
|
||||
worker.downloaded.connect(lambda p: events.append(("file",)))
|
||||
worker.finished.connect(lambda d: events.append(("done", d["downloaded"])))
|
||||
worker._run()
|
||||
assert events == [("start", 1, 3, "aaa"), ("file",), ("bad", "badone"),
|
||||
("start", 2, 3, "ccc"), ("file",), ("done", 2)]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# C. the dialog
|
||||
|
||||
|
||||
def _dialog(qapp, url):
|
||||
qapp.clipboard().setText(url)
|
||||
dialog = UrlImportDialog("Adds to the end of “Chill”")
|
||||
assert dialog.url() == url
|
||||
return dialog
|
||||
|
||||
|
||||
def test_a_plain_song_link_is_never_listed(qapp, fake):
|
||||
dialog = _dialog(qapp, "https://www.youtube.com/watch?v=abc")
|
||||
dialog._on_import()
|
||||
assert dialog.result() == dialog.DialogCode.Accepted
|
||||
assert dialog.chosen_entries() == []
|
||||
assert fake() == [] # yt-dlp was never run
|
||||
|
||||
|
||||
def test_a_mix_ticks_only_the_linked_song(qapp, fake):
|
||||
dialog = _dialog(qapp, MIX)
|
||||
dialog._on_import()
|
||||
assert not dialog._ok.isEnabled() # nothing to import while checking
|
||||
_pump(qapp, lambda: dialog._probe is None)
|
||||
assert dialog.result() != dialog.DialogCode.Accepted
|
||||
assert dialog._list.count() == 3
|
||||
assert dialog._ok.text() == "Import 1 Song"
|
||||
assert [e["id"] for e in dialog.chosen_entries()] == ["bbb"]
|
||||
assert "3 songs" in dialog._status.text()
|
||||
|
||||
dialog._list.item(2).setCheckState(Qt.CheckState.Checked)
|
||||
assert dialog._ok.text() == "Import 2 Songs"
|
||||
dialog._check_all(False)
|
||||
assert not dialog._ok.isEnabled()
|
||||
dialog._check_all(True)
|
||||
assert dialog._ok.text() == "Import 3 Songs"
|
||||
dialog._on_import()
|
||||
assert dialog.result() == dialog.DialogCode.Accepted
|
||||
assert [e["url"] for e in dialog.chosen_entries()] == [
|
||||
"https://www.youtube.com/watch?v=aaa",
|
||||
"https://www.youtube.com/watch?v=bbb",
|
||||
"https://www.youtube.com/watch?v=ccc"]
|
||||
|
||||
|
||||
def test_a_looping_mix_lists_each_song_once(qapp, fake, monkeypatch):
|
||||
monkeypatch.setenv("FAKE_LOOPS", "3")
|
||||
dialog = _dialog(qapp, MIX)
|
||||
dialog._on_import()
|
||||
_pump(qapp, lambda: dialog._probe is None)
|
||||
assert dialog._list.count() == 3
|
||||
assert [e["id"] for e in dialog.chosen_entries()] == ["bbb"]
|
||||
|
||||
|
||||
def test_just_this_song_never_waits_for_the_list(qapp, fake):
|
||||
dialog = _dialog(qapp, MIX)
|
||||
dialog._on_import()
|
||||
assert dialog._just.isVisibleTo(dialog)
|
||||
dialog._just.click() # before a single row has arrived
|
||||
assert dialog.result() == dialog.DialogCode.Accepted
|
||||
assert dialog._probe is None
|
||||
assert dialog.chosen_entries() == []
|
||||
assert dialog.download_urls() == ["https://www.youtube.com/watch?v=bbb"]
|
||||
|
||||
|
||||
def test_download_urls(qapp, fake):
|
||||
song = _dialog(qapp, "https://www.youtube.com/watch?v=abc")
|
||||
song._on_import()
|
||||
assert song.download_urls() == ["https://www.youtube.com/watch?v=abc"]
|
||||
playlist = _dialog(qapp, "https://www.youtube.com/playlist?list=PLx")
|
||||
playlist._on_import()
|
||||
assert not playlist._just.isVisibleTo(playlist) # no song to pick
|
||||
_pump(qapp, lambda: playlist._probe is None)
|
||||
assert len(playlist.download_urls()) == 3
|
||||
|
||||
|
||||
def test_a_playlist_link_ticks_everything(qapp, fake):
|
||||
dialog = _dialog(qapp, "https://www.youtube.com/playlist?list=PLx")
|
||||
dialog._on_import()
|
||||
_pump(qapp, lambda: dialog._probe is None)
|
||||
assert len(dialog.chosen_entries()) == 3
|
||||
|
||||
|
||||
def test_a_one_song_listing_goes_straight_through(qapp, fake, monkeypatch):
|
||||
monkeypatch.setenv("FAKE_ENTRIES", "1")
|
||||
dialog = _dialog(qapp, "https://soundcloud.com/someone/a-song")
|
||||
dialog._on_import()
|
||||
_pump(qapp, lambda: dialog.result() == dialog.DialogCode.Accepted)
|
||||
assert dialog.chosen_entries() == [] # the link itself downloads
|
||||
|
||||
|
||||
def test_a_failed_listing_stays_open_and_says_why(qapp, fake, monkeypatch):
|
||||
monkeypatch.setenv("FAKE_ENTRIES", "0")
|
||||
dialog = _dialog(qapp, "https://x.test/nothing")
|
||||
dialog._on_import()
|
||||
_pump(qapp, lambda: dialog._probe is None)
|
||||
assert dialog.result() != dialog.DialogCode.Accepted
|
||||
assert "Unsupported URL" in dialog._status.text()
|
||||
assert dialog._ok.isEnabled() and dialog._url.isEnabled()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D. the progress window
|
||||
|
||||
|
||||
def test_progress_rows_follow_the_download(qapp):
|
||||
window = UrlImportProgress([{"id": "a", "title": "A"},
|
||||
{"id": "b", "title": "B"},
|
||||
{"id": "c", "title": "C"},
|
||||
{"id": "d", "title": "D"}])
|
||||
assert window.status("a") == WAITING
|
||||
window.started("a")
|
||||
window.progress(43)
|
||||
assert window.status("a") == "Downloading 43%"
|
||||
window.landed(True)
|
||||
assert window.status("a") == IMPORTED
|
||||
window.failed("b", "Video unavailable")
|
||||
assert window.status("b") == "Couldn't download: Video unavailable"
|
||||
window.started("c")
|
||||
window.finish(cancelled=True)
|
||||
assert window.status("c") == SKIPPED and window.status("d") == SKIPPED
|
||||
assert window._summary.text().startswith("Done: 1 of 4 songs imported")
|
||||
assert not window._stop.isEnabled()
|
||||
|
||||
quiet = UrlImportProgress([{"id": "x", "title": "X"}])
|
||||
quiet.finish(cancelled=False)
|
||||
assert quiet.status("x") == NOT_DOWNLOADED
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# E. through the window
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def window(qapp, tmp_path, fake, monkeypatch):
|
||||
from lintunes.gui.main_window import MainWindow
|
||||
library = Library(music_folder=str(tmp_path / "media"))
|
||||
(tmp_path / "media").mkdir()
|
||||
manager = LibraryManager(library, tmp_path / "data")
|
||||
win = MainWindow(manager, Preferences(tmp_path / "data"))
|
||||
monkeypatch.setattr(win, "_enqueue_identify", lambda tracks: None)
|
||||
yield win
|
||||
win.close()
|
||||
|
||||
|
||||
def test_only_the_chosen_songs_download(window, qapp, fake, mp3_file,
|
||||
monkeypatch):
|
||||
from lintunes.gui import main_window as mw
|
||||
monkeypatch.setenv("FAKE_YTDLP_AUDIO", str(mp3_file))
|
||||
chosen = [{"id": "aaa", "title": "Song aaa",
|
||||
"url": "https://www.youtube.com/watch?v=aaa"},
|
||||
{"id": "ccc", "title": "Song ccc",
|
||||
"url": "https://www.youtube.com/watch?v=ccc"}]
|
||||
|
||||
class _Dialog:
|
||||
cookies_used = None
|
||||
def __init__(self, description, parent=None, **_kw):
|
||||
pass
|
||||
def exec(self):
|
||||
return True
|
||||
def url(self):
|
||||
return MIX
|
||||
def add_to_playlist(self):
|
||||
return False
|
||||
def chosen_entries(self):
|
||||
return chosen
|
||||
def download_urls(self):
|
||||
return [c["url"] for c in chosen]
|
||||
monkeypatch.setattr(mw, "UrlImportDialog", _Dialog)
|
||||
|
||||
window._import_from_url()
|
||||
worker = window._url_worker
|
||||
_pump(qapp, lambda: worker.temp_dir is None)
|
||||
|
||||
[argv] = fake()
|
||||
assert argv[argv.index("--") + 1:] == [c["url"] for c in chosen]
|
||||
assert MIX not in argv # never the whole list
|
||||
progress = window._url_progress
|
||||
assert progress is not None and progress.isVisible()
|
||||
assert progress.status("aaa") == IMPORTED
|
||||
assert progress.status("ccc") == IMPORTED
|
||||
assert len(window._manager.library.tracks) == 2
|
||||
|
||||
# Hidden, it comes back from the status bar only while downloading.
|
||||
progress.close()
|
||||
window._sync_label.clicked.emit()
|
||||
assert not progress.isVisible() # done downloading: stays hidden
|
||||
worker._busy = True # as if still going
|
||||
window._sync_label.clicked.emit()
|
||||
assert progress.isVisible()
|
||||
worker._busy = False
|
||||
|
||||
|
||||
def test_one_song_gets_no_progress_window(window, qapp, fake, mp3_file,
|
||||
monkeypatch):
|
||||
from lintunes.gui import main_window as mw
|
||||
monkeypatch.setenv("FAKE_YTDLP_AUDIO", str(mp3_file))
|
||||
|
||||
class _Dialog:
|
||||
cookies_used = None
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
def exec(self):
|
||||
return True
|
||||
def url(self):
|
||||
return "https://www.youtube.com/watch?v=aaa"
|
||||
def add_to_playlist(self):
|
||||
return False
|
||||
def chosen_entries(self):
|
||||
return []
|
||||
def download_urls(self):
|
||||
return [self.url()]
|
||||
monkeypatch.setattr(mw, "UrlImportDialog", _Dialog)
|
||||
window._import_from_url()
|
||||
worker = window._url_worker
|
||||
_pump(qapp, lambda: worker.temp_dir is None)
|
||||
assert window._url_progress is None
|
||||
assert len(window._manager.library.tracks) == 1
|
||||
Reference in New Issue
Block a user