The table already computed and emitted the drop row for external URL drops, but PlaylistView._on_files_dropped threw it away, so imported files always appended. Thread it through: playlist_view.files_dropped gains a row slot (None = append), MainWindow.import_files and file_importer.import_paths take an optional position and hand it to add_tracks_to_playlist, which already supported positional insert. Files already in the library dedup to the existing track and insert at the drop position without duplicating the library entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
174 lines
6.8 KiB
Python
174 lines
6.8 KiB
Python
"""Round 18: the 2026-07-02 "New backlog" batch.
|
|
|
|
1. Visualizer: gray-mode color slider pref + mode persists across launches.
|
|
2. External file drops insert at the drop position in a playlist.
|
|
3. Renaming artist/album/album_artist moves the file to keep the music
|
|
folder organized (Artist/Album, Unknown Artist fallback).
|
|
"""
|
|
|
|
from PyQt6.QtCore import QObject, pyqtSignal
|
|
from PyQt6.QtGui import QColor
|
|
|
|
from lintunes import theme
|
|
from lintunes.preferences import Preferences
|
|
from lintunes.gui.visualizer import (
|
|
VisualizerWidget, MODE_ON, MODE_DIM, MODE_OFF)
|
|
|
|
|
|
class _FakePlayer(QObject):
|
|
audio_buffer = pyqtSignal(object)
|
|
playing_changed = pyqtSignal(bool)
|
|
track_changed = pyqtSignal(object)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._playing = False
|
|
|
|
def is_playing(self):
|
|
return self._playing
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 1. Visualizer: gray color pref + persistent mode
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestVisualizerModePersistence:
|
|
def test_no_prefs_defaults_on(self, qapp):
|
|
vis = VisualizerWidget(_FakePlayer())
|
|
assert vis._mode == MODE_ON
|
|
|
|
def test_saved_mode_restored(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("visualizer_mode", MODE_DIM)
|
|
vis = VisualizerWidget(_FakePlayer(), prefs)
|
|
assert vis._mode == MODE_DIM
|
|
|
|
def test_saved_off_restored_and_stays_idle(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("visualizer_mode", MODE_OFF)
|
|
vis = VisualizerWidget(_FakePlayer(), prefs)
|
|
assert vis._mode == MODE_OFF
|
|
vis._on_playing_changed(True) # playback starts
|
|
assert not vis._timer.isActive()
|
|
|
|
def test_bogus_saved_mode_falls_back_to_on(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("visualizer_mode", "sparkle") # hand-edited prefs file
|
|
vis = VisualizerWidget(_FakePlayer(), prefs)
|
|
assert vis._mode == MODE_ON
|
|
|
|
def test_click_persists_mode_to_disk(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
vis = VisualizerWidget(_FakePlayer(), prefs)
|
|
|
|
vis.mousePressEvent(None) # on -> dim
|
|
|
|
assert prefs.get("visualizer_mode") == MODE_DIM
|
|
# A fresh Preferences (= next launch) sees it too.
|
|
assert Preferences(tmp_path).get("visualizer_mode") == MODE_DIM
|
|
|
|
|
|
class TestVisualizerGrayColor:
|
|
def test_default_gray_resolves_for_visualizer_key(self, qapp):
|
|
value = theme.default_gray("color_visualizer_gray")
|
|
assert isinstance(value, int)
|
|
assert 0 <= value <= 255
|
|
|
|
def test_slider_pref_drives_dim_color(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("color_visualizer_gray", 100)
|
|
vis = VisualizerWidget(_FakePlayer(), prefs)
|
|
vis._mode = MODE_DIM
|
|
assert vis._bar_color() == QColor(100, 100, 100)
|
|
|
|
def test_unset_pref_keeps_derived_color(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
with_prefs = VisualizerWidget(_FakePlayer(), prefs)
|
|
without = VisualizerWidget(_FakePlayer())
|
|
with_prefs._mode = MODE_DIM
|
|
without._mode = MODE_DIM
|
|
assert with_prefs._bar_color() == without._bar_color()
|
|
|
|
def test_on_mode_ignores_gray_pref(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("color_visualizer_gray", 100)
|
|
vis = VisualizerWidget(_FakePlayer(), prefs)
|
|
assert vis._mode == MODE_ON
|
|
assert vis._bar_color() == QColor(vis.palette().highlight().color())
|
|
|
|
def test_visualizer_row_in_preferences_dialog(self, qapp):
|
|
from lintunes.gui.preferences_dialog import _COLOR_ROWS
|
|
assert "color_visualizer_gray" in [key for key, _label in _COLOR_ROWS]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 2. External file drops insert at the drop position
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _seeded_manager(tmp_path, track_ids=(1, 2, 3)):
|
|
from lintunes.models import Library, Track
|
|
from lintunes.library_manager import LibraryManager
|
|
tracks = {tid: Track(track_id=tid, name=f"T{tid}") for tid in track_ids}
|
|
return LibraryManager(Library(tracks=tracks), tmp_path / "data")
|
|
|
|
|
|
class TestFileDropPosition:
|
|
def test_import_paths_inserts_at_position(self, qapp, tmp_path, mp3_file):
|
|
from lintunes.importers import file_importer
|
|
manager = _seeded_manager(tmp_path)
|
|
playlist = manager.create_playlist("Drops")
|
|
manager.add_tracks_to_playlist(playlist.persistent_id, [1, 2, 3])
|
|
|
|
imported = file_importer.import_paths(
|
|
[mp3_file], tmp_path / "music", manager,
|
|
playlist.persistent_id, position=1)
|
|
|
|
assert len(imported) == 1
|
|
new_id = imported[0].track_id
|
|
assert playlist.track_ids == [1, new_id, 2, 3]
|
|
|
|
def test_import_paths_default_appends(self, qapp, tmp_path, mp3_file):
|
|
from lintunes.importers import file_importer
|
|
manager = _seeded_manager(tmp_path)
|
|
playlist = manager.create_playlist("Drops")
|
|
manager.add_tracks_to_playlist(playlist.persistent_id, [1, 2])
|
|
|
|
imported = file_importer.import_paths(
|
|
[mp3_file], tmp_path / "music", manager, playlist.persistent_id)
|
|
|
|
assert playlist.track_ids == [1, 2, imported[0].track_id]
|
|
|
|
def test_duplicate_file_inserts_without_new_track(self, qapp, tmp_path,
|
|
mp3_file):
|
|
from lintunes.importers import file_importer
|
|
manager = _seeded_manager(tmp_path)
|
|
existing = file_importer.import_file(
|
|
mp3_file, tmp_path / "music", manager)
|
|
playlist = manager.create_playlist("Drops")
|
|
manager.add_tracks_to_playlist(playlist.persistent_id, [1, 2])
|
|
count_before = len(manager.library.tracks)
|
|
|
|
imported = file_importer.import_paths(
|
|
[mp3_file], tmp_path / "music", manager,
|
|
playlist.persistent_id, position=0)
|
|
|
|
assert imported[0].track_id == existing.track_id
|
|
assert playlist.track_ids == [existing.track_id, 1, 2]
|
|
assert len(manager.library.tracks) == count_before
|
|
|
|
def test_playlist_view_relays_drop_row(self, qapp, tmp_path):
|
|
from lintunes.gui.playlist_view import PlaylistView
|
|
manager = _seeded_manager(tmp_path)
|
|
playlist = manager.create_playlist("Drops")
|
|
view = PlaylistView(manager)
|
|
view.show_playlist(playlist)
|
|
received = []
|
|
view.files_dropped.connect(
|
|
lambda paths, pid, row: received.append((paths, pid, row)))
|
|
|
|
view._on_files_dropped(["/x.mp3"], 2)
|
|
view._on_files_dropped(["/y.mp3"], None)
|
|
|
|
assert received == [(["/x.mp3"], playlist.persistent_id, 2),
|
|
(["/y.mp3"], playlist.persistent_id, None)]
|