"""Round 20: Ctrl+I save-path performance. - ``tagging.write_tags`` now does ONE mutagen parse + ONE save per call: grouping/compilation/bpm ride the same save (via registered Easy keys) instead of a second full parse+save of the audio file. - ``LibraryView`` coalesces browser rebuilds through a 0 ms single-shot timer, so an N-track edit rebuilds the genre/artist/album cascade once. - ``perf.timed`` logs wall-clock durations on the ``lintunes.perf`` logger. """ import logging import mutagen import pytest from lintunes import tagging from lintunes.library_manager import LibraryManager from lintunes.models import Track from lintunes.models.library import Library from lintunes.perf import timed def _manager(tmp_path, tracks=()): library = Library() for track in tracks: library.tracks[track.track_id] = track return LibraryManager(library, tmp_path) EXTRAS = {"grouping": "Test Group", "compilation": True, "bpm": 128} # ---- single parse + save in write_tags ---- class TestSingleSave: @pytest.mark.parametrize("fixture", ["mp3_file", "m4a_file", "flac_file"]) def test_one_parse_per_write(self, fixture, request, monkeypatch): path = request.getfixturevalue(fixture) opens = [] real_file = mutagen.File def counting_file(*args, **kwargs): opens.append(args) return real_file(*args, **kwargs) monkeypatch.setattr(tagging.mutagen, "File", counting_file) tagging.write_tags(path, {"name": "Title", **EXTRAS}) assert len(opens) == 1 def test_mp3_extras_use_itunes_frames(self, mp3_file): tagging.write_tags(mp3_file, EXTRAS) raw = mutagen.File(mp3_file) assert [str(t) for t in raw.tags["GRP1"].text] == ["Test Group"] assert str(raw.tags["TCMP"].text[0]) == "1" assert str(raw.tags["TBPM"].text[0]) == "128" def test_m4a_extras_use_itunes_atoms(self, m4a_file): tagging.write_tags(m4a_file, EXTRAS) raw = mutagen.File(m4a_file) assert raw.tags["\xa9grp"] == ["Test Group"] assert bool(raw.tags["cpil"]) assert list(raw.tags["tmpo"]) == [128] def test_flac_extras_use_vorbis_comments(self, flac_file): tagging.write_tags(flac_file, EXTRAS) raw = mutagen.File(flac_file) assert raw["grouping"] == ["Test Group"] assert raw["compilation"] == ["1"] assert raw["bpm"] == ["128"] @pytest.mark.parametrize("fixture", ["mp3_file", "m4a_file", "flac_file"]) def test_clearing_extras_removes_them(self, fixture, request): path = request.getfixturevalue(fixture) tagging.write_tags(path, EXTRAS) tagging.write_tags(path, {"grouping": "", "compilation": False, "bpm": 0}) easy = mutagen.File(path, easy=True) for key in ("grouping", "compilation", "bpm"): assert not easy.tags.get(key), key # ---- coalesced browser rebuild ---- class TestCoalescedBrowserRebuild: def _view_with_counter(self, qapp, tmp_path, monkeypatch, tracks): from lintunes.gui.library_view import LibraryView manager = _manager(tmp_path, tracks) view = LibraryView(manager) rebuilds = [] # _rebuild_browser looks methods up on self, so patching one of them # counts rebuilds without disturbing the timer wiring. monkeypatch.setattr(view, "_populate_genres", lambda: rebuilds.append(1)) return manager, view, rebuilds def test_multi_edit_rebuilds_once(self, qapp, tmp_path, monkeypatch): tracks = [Track(track_id=i, name=f"T{i}", artist="Old") for i in (1, 2, 3)] manager, view, rebuilds = self._view_with_counter( qapp, tmp_path, monkeypatch, tracks) manager.edit_tracks_fields([1, 2, 3], {"artist": "New"}) assert rebuilds == [] # deferred to the event loop qapp.processEvents() assert len(rebuilds) == 1 assert all(t.artist == "New" for t in tracks) def test_single_edit_still_rebuilds(self, qapp, tmp_path, monkeypatch): track = Track(track_id=1, name="T", genre="Pop") manager, view, rebuilds = self._view_with_counter( qapp, tmp_path, monkeypatch, [track]) manager.edit_track_fields(1, {"genre": "Rock"}) qapp.processEvents() assert len(rebuilds) == 1 def test_non_browser_edit_skips_rebuild(self, qapp, tmp_path, monkeypatch): track = Track(track_id=1, name="T") manager, view, rebuilds = self._view_with_counter( qapp, tmp_path, monkeypatch, [track]) manager.edit_track_fields(1, {"name": "Renamed"}) qapp.processEvents() assert rebuilds == [] # ---- perf logging ---- def test_timed_logs_duration(caplog): with caplog.at_level(logging.INFO, logger="lintunes.perf"): with timed("unit test %s", "label"): pass messages = [r.getMessage() for r in caplog.records] assert any("unit test label" in m and "ms" in m for m in messages)