"""Round 66: the share selector and the trimmed library copy a friend reads. * Nothing is shared by default. "Share whole library" is live (new songs are offered too); checked artists/albums store the track ids they covered then, so a new album shows up unchecked; shared playlists are live and their tracks are locked in the Library section with a tooltip naming them. * Offering never sends anything — the copy only lists what may be asked for, and keeps the tags while dropping my listening history and anything local. * The copy is written only when its bytes would change. """ import json from PyQt6.QtCore import Qt from lintunes.cassette import share from lintunes.cassette.state import CassetteState, Friend, Selection, out_dir from lintunes.models import Playlist, PlaylistType, Track from lintunes.models.library import Library def _library(): library = Library() rows = [ (1, "Reel One", "Analog Ghost", "Tape Hiss", "Folk"), (2, "Reel Two", "Analog Ghost", "Tape Hiss", "Jazz"), (3, "B-Side", "Analog Ghost", "Other Album", "Jazz"), (4, "Étoile", "Marguerite Vaux", "Nuit Blanche", "Jazz"), (5, "Café", "Marguerite Vaux", "Nuit Blanche", "Electronic"), ] for tid, name, artist, album, genre in rows: library.tracks[tid] = Track( track_id=tid, name=name, artist=artist, album=album, genre=genre, year=1987, total_time=180000, size=4_000_000, play_count=41, rating=80, loved=True, comments="private note", location=f"/music/{tid}.mp3", date_added="2020-01-01T00:00:00", play_date_utc="2026-01-01T00:00:00") library.playlists["P1"] = Playlist(name="Road Trip", persistent_id="P1", track_ids=[4, 1]) library.playlists["P2"] = Playlist(name="Morning", persistent_id="P2", track_ids=[5]) library.playlists["F"] = Playlist(name="Boxes", persistent_id="F", playlist_type=PlaylistType.FOLDER) library.playlists["S"] = Playlist(name="Music", persistent_id="S", is_system=True, playlist_type=PlaylistType.SYSTEM) return library # ---- what is offered ---- class TestOffered: def test_nothing_by_default(self): assert share.offered_ids(_library(), Selection()) == set() def test_whole_library_is_live(self): library = _library() selection = Selection(all_library=True) library.tracks[9] = Track(track_id=9, name="New") assert 9 in share.offered_ids(library, selection) def test_checked_tracks_plus_shared_playlists(self): selection = Selection(track_ids={2, 99}, playlist_ids={"P1"}) assert share.offered_ids(_library(), selection) == {1, 2, 4} def test_shared_playlist_is_live(self): library = _library() selection = Selection(playlist_ids={"P1"}) library.playlists["P1"].track_ids.append(3) assert 3 in share.offered_ids(library, selection) def test_locks_name_every_playlist(self): library = _library() library.playlists["P2"].track_ids.append(4) locks = share.locks(library, Selection(all_playlists=True)) assert sorted(locks[4]) == ["Morning", "Road Trip"] def test_folders_and_system_playlists_are_never_shared(self): names = [p.name for p in share.shared_playlists( _library(), Selection(all_playlists=True))] assert names == ["Morning", "Road Trip"] # ---- what the copy holds ---- class TestCopy: def test_tags_yes_history_no(self): library = _library() entry = share.build_library(library, Selection(track_ids={1}))["tracks"][0] assert entry["id"] == 1 and entry["name"] == "Reel One" assert entry["artist"] == "Analog Ghost" and entry["year"] == 1987 assert entry["size"] == 4_000_000 and entry["total_time"] == 180000 for private in ("play_count", "rating", "loved", "comments", "location", "date_added", "play_date_utc", "persistent_id"): assert private not in entry def test_playlists_keep_order_and_drop_unoffered(self): library = _library() library.playlists["P1"].track_ids.append(77) # dangling id data = share.build_playlists(library, Selection(playlist_ids={"P1"})) assert data["playlists"] == [{"id": "P1", "name": "Road Trip", "smart": False, "track_ids": [4, 1]}] def test_written_only_when_changed(self, tmp_path): library = _library() selection = Selection(track_ids={1}) assert share.publish_share(tmp_path, library, selection) mtime = (tmp_path / share.LIBRARY_FILE).stat().st_mtime_ns assert not share.publish_share(tmp_path, library, selection) assert (tmp_path / share.LIBRARY_FILE).stat().st_mtime_ns == mtime selection.track_ids.add(2) assert share.publish_share(tmp_path, library, selection) data = json.loads((tmp_path / share.LIBRARY_FILE).read_text()) assert [t["id"] for t in data["tracks"]] == [1, 2] # ---- the service ---- class TestRepublish: def test_each_friend_gets_their_selection(self, qapp, tmp_path): from lintunes.cassette.service import CassetteService library = _library() service = CassetteService(root=tmp_path, config_loader=lambda: {}) service.set_library_provider(lambda: library) service._state.friends["a"] = Friend(token="a", device_id="A", name="Amy", selection=Selection(track_ids={1})) service._state.friends["b"] = Friend(token="b", device_id="B", name="Bo", selection=Selection(playlist_ids={"P2"})) assert sorted(service.republish()) == ["a", "b"] assert service.republish() == [] def ids(token): data = json.loads((out_dir(tmp_path, token) / share.LIBRARY_FILE).read_text()) return [t["id"] for t in data["tracks"]] assert ids("a") == [1] assert ids("b") == [5] service._state.share_same_for_all = True service._state.general = Selection(track_ids={3}) service.republish() assert ids("a") == ids("b") == [3] # ---- the selector ---- def _selector(qapp, selection=None, library=None): from lintunes.gui.share_selector import ShareSelector return ShareSelector(library or _library(), selection or Selection()) def _track_items(selector): return {selector.tracks.topLevelItem(i).data(0, Qt.ItemDataRole.UserRole): selector.tracks.topLevelItem(i) for i in range(selector.tracks.topLevelItemCount())} def _row(column, text): return column.findItems(text, Qt.MatchFlag.MatchExactly)[0] class TestSelector: def test_starts_empty(self, qapp): selector = _selector(qapp) assert all(item.checkState(0) == Qt.CheckState.Unchecked for item in _track_items(selector).values()) def test_checking_an_artist_checks_its_tracks(self, qapp): selection = Selection() selector = _selector(qapp, selection) _row(selector.artists, "Analog Ghost").setCheckState(Qt.CheckState.Checked) assert selection.track_ids == {1, 2, 3} assert _row(selector.albums, "Tape Hiss").checkState() == Qt.CheckState.Checked def test_album_then_partial_artist(self, qapp): selection = Selection() selector = _selector(qapp, selection) _row(selector.albums, "Tape Hiss").setCheckState(Qt.CheckState.Checked) assert selection.track_ids == {1, 2} assert _row(selector.artists, "Analog Ghost").checkState() == \ Qt.CheckState.PartiallyChecked def test_genre_filter_scopes_the_artist_box(self, qapp): selection = Selection() selector = _selector(qapp, selection) selector.genres.setCurrentItem(_row(selector.genres, "Jazz")) _row(selector.artists, "Analog Ghost").setCheckState(Qt.CheckState.Checked) assert selection.track_ids == {2, 3} # not the Folk one def test_playlist_tracks_are_locked_with_a_tooltip(self, qapp): selection = Selection() selector = _selector(qapp, selection) _row(selector.playlists, "Road Trip").setCheckState(Qt.CheckState.Checked) assert selection.playlist_ids == {"P1"} item = _track_items(selector)[4] assert item.checkState(0) == Qt.CheckState.Checked assert not item.flags() & Qt.ItemFlag.ItemIsUserCheckable assert "Road Trip" in item.toolTip(0) assert 4 not in selection.track_ids # locked, not chosen def test_select_none_keeps_locked_tracks(self, qapp): selection = Selection(track_ids={2, 3}, playlist_ids={"P1"}) selector = _selector(qapp, selection) selector.lib_none.click() assert selection.track_ids == set() assert _track_items(selector)[1].checkState(0) == Qt.CheckState.Checked def test_select_all_buttons(self, qapp): selection = Selection() selector = _selector(qapp, selection) selector.lib_all.click() selector.pl_all.click() assert selection.track_ids == {1, 2, 3, 4, 5} assert selection.playlist_ids == {"P1", "P2"} def test_whole_library_greys_the_browser(self, qapp): selection = Selection() selector = _selector(qapp, selection) selector.whole_library.setChecked(True) assert selection.all_library assert not selector._browser_host.isEnabled() assert all(i.checkState(0) == Qt.CheckState.Checked for i in _track_items(selector).values()) # ---- the dialog ---- class TestSameForAll: def test_turning_it_off_restores_each_friend(self, qapp, tmp_path): from lintunes.gui.sync_settings_dialog import SyncSettingsDialog from tests.test_round64 import _NoProbe state = CassetteState() state.friends["a"] = Friend(token="a", device_id="A", name="Amy", selection=Selection(track_ids={1})) dialog = SyncSettingsDialog(state, tmp_path, {}, probe=_NoProbe(), library=_library()) dialog._show_page(1) page = dialog.friend_pages["a"] dialog.general.same_for_all.setChecked(True) assert dialog.general.selector is not None assert page.using_general.isVisibleTo(dialog) assert not page.selector.whole_library.isEnabled() dialog.general.selector.lib_all.click() dialog.general.same_for_all.setChecked(False) assert dialog.state.friends["a"].selection.track_ids == {1} assert dialog.state.general.track_ids == {1, 2, 3, 4, 5} assert not dialog.state.share_same_for_all