Round 17: task batch + cruft sweep (ratings, art download, MPRIS/exit/BT fixes)
Features:
- Rating hover dots: hovering a rating cell shows 5 clickable slots
(RatingDelegate); click slot k sets k stars, clicking the current count
clears. Undoable, library-only (ratings never rewrite music files).
- Search bar moved into the Library header strip, right of the Library
button, so the tracklist top aligns with the playlist tree.
- Right-click "Download Album Art…": iTunes Search API (no key), off-thread
fetch, preview/confirm dialog with Next Result, embeds via write_artwork
+ size refresh, invalidates the MPRIS art cache.
Fixes:
- MPRIS media keys: PropertiesChanged sent invalidated_properties as "av"
instead of "as", so gsd-media-keys dropped it and never MRU-bumped
lintunes (why the play/pause key kept waking stale players). Now an
explicit QDBusArgument string array; loopback-verified sa{sv}as.
- Exit segfault: ordered Player.shutdown() (stop, clear source, detach
buffer/audio outputs) from closeEvent/aboutToQuit; scripted run exits 0.
- BT zero-volume after pause/resume: volume re-applied on resume, device
swap, and BufferedMedia (needs verify on the affected machine).
Cruft sweep:
- Tag writes filtered to EDITABLE_FIELDS; failures logged + surfaced in
the status bar (was a swallowed print).
- O(n²) import fixed (location index + cached max track id); O(1)
refresh/reveal via TrackTableModel row index.
- lastfm: scrobble-queue thread lock, login prefs write marshalled to the
GUI thread, JSON/raise_for_status order fixed.
- Shared read_json/write_json; TableSettingsMixin dedupes view settings.
TASKS.md rewritten as a resumable board; tests in tests/test_round17.py
(251 total pass).
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import logging
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt6.QtCore import QObject, QTimer, pyqtSignal
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from lintunes import smart, tagging
|
||||
from lintunes.models import Library, Playlist, PlaylistType
|
||||
from lintunes.storage import json_storage
|
||||
@@ -32,6 +35,7 @@ class LibraryManager(QObject):
|
||||
track_fields_edited = pyqtSignal(int, list) # track_id, changed field names
|
||||
library_reloaded = pyqtSignal() # library re-read from disk (sync)
|
||||
conflict_resolved = pyqtSignal(list) # list[ConflictSummary] just merged
|
||||
tag_write_failed = pyqtSignal(str, str) # track name, error text
|
||||
|
||||
def __init__(self, library: Library, data_dir: Path, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -65,10 +69,12 @@ class LibraryManager(QObject):
|
||||
self._own_sigs: dict[str, tuple | None] = {}
|
||||
self.snapshot_sync_state()
|
||||
|
||||
self._max_track_id = max(self.library.tracks.keys(), default=0)
|
||||
|
||||
# ---- id generation ----
|
||||
|
||||
def new_track_id(self) -> int:
|
||||
return max(self.library.tracks.keys(), default=0) + 1
|
||||
return self._max_track_id + 1
|
||||
|
||||
def new_persistent_id(self) -> str:
|
||||
while True:
|
||||
@@ -312,6 +318,7 @@ class LibraryManager(QObject):
|
||||
if not track.persistent_id:
|
||||
track.persistent_id = secrets.token_hex(8).upper()
|
||||
self.library.tracks[track.track_id] = track
|
||||
self._max_track_id = max(self._max_track_id, track.track_id)
|
||||
self._dirty_tracks = True
|
||||
self._dirty_metadata = True
|
||||
self._schedule_save()
|
||||
@@ -403,13 +410,18 @@ class LibraryManager(QObject):
|
||||
for tid, new, _old in changes]))
|
||||
|
||||
def _write_track_tags(self, track, field_map: dict) -> bool:
|
||||
if not track.location:
|
||||
# Library-only fields (rating, size, start/stop times) have no tag
|
||||
# representation; don't rewrite the audio file for them.
|
||||
writable = {k: v for k, v in field_map.items()
|
||||
if k in tagging.EDITABLE_FIELDS}
|
||||
if not writable or not track.location:
|
||||
return True
|
||||
try:
|
||||
tagging.write_tags(track.location, field_map)
|
||||
tagging.write_tags(track.location, writable)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Could not write tags to {track.location}: {e}")
|
||||
log.warning("Could not write tags to %s: %s", track.location, e)
|
||||
self.tag_write_failed.emit(track.name or track.location, str(e))
|
||||
return False
|
||||
|
||||
def _apply_track_fields(self, track_id: int, field_map: dict):
|
||||
@@ -653,6 +665,8 @@ class LibraryManager(QObject):
|
||||
self.library.import_date = disk.import_date
|
||||
self.library.library_settings = disk.library_settings
|
||||
|
||||
self._max_track_id = max(self.library.tracks.keys(), default=0)
|
||||
|
||||
self._recomputing = True
|
||||
try:
|
||||
self.recompute_all_smart()
|
||||
|
||||
Reference in New Issue
Block a user