Files
lintunes/lintunes/gui/followed_view.py
T
travandClaude Opus 5.5 87d43f8e0c v0.31.0: Cassette followed playlists — a friend's playlist in your sidebar
- A followed playlist (the UFO in friend mode) appears in my sidebar with
  a purple UFO: mixed in with my playlists alphabetically, or inside a
  folder named after the friend (per-friend, in Sync Settings). It's never
  a Playlist of mine, so it stays out of the library and its merges.
- It plays from the cache. Player's queue accepts friend-track keys,
  resolved through Cassette: they scrobble to Last.fm but don't count in
  my play journal. Songs still on their way are grey and skipped.
- The cassette column there is hidden by default (right-click the
  header); pressing one takes the song into my library — straight from
  the cache if it's there, otherwise by asking for it.
- Sync Settings per friend: keep followed songs separate (default) or add
  them to my library. Unfollowing (right-click) frees their cache.
- The bottom bar says when the friend's folder last synced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:40:59 -07:00

120 lines
4.9 KiB
Python

"""A friend's playlist I follow, in my own sidebar, playing from the cache.
Their songs, not mine: the track ids are theirs, so nothing here drags into
my playlists, edits, or copies. What it does do is **play** — a song whose
file has arrived in ``cache/`` plays like any other, queued by an opaque key
(``cassette:<token>:<their id>``) that Player resolves through Cassette; a
song still on its way is greyed and skipped.
The cassette column is hidden by default (right-click the header to show
it); pressing one asks for that song for my library, the same as in friend
mode, and says so at once — there's no Save step here.
"""
from __future__ import annotations
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
from lintunes.gui.track_table import TrackTableView
from lintunes.models.playlist import PlaylistSettings
NOT_HERE_GREY = QColor("#a0a0a0")
FOLLOWED_COLUMNS = ["name", "total_time", "artist", "album", "genre", "year"]
def track_key(token: str, track_id: int) -> str:
return f"cassette:{token}:{track_id}"
def parse_key(key: str) -> tuple[str, int] | None:
parts = key.split(":") if isinstance(key, str) else []
if len(parts) == 3 and parts[0] == "cassette" and parts[2].isdigit():
return parts[1], int(parts[2])
return None
class FollowedPlaylistView(QWidget):
play_requested = pyqtSignal(list, int, str) # keys, start, context
def __init__(self, cassette, parent=None):
super().__init__(parent)
self._cassette = cassette # CassetteUi: cache paths, requests
self.key = "" # "cassette:<token>:<their pid>"
self.token = ""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.title = QLabel()
font = self.title.font()
font.setBold(True)
self.title.setFont(font)
self.title.setContentsMargins(8, 2, 8, 2)
layout.addWidget(self.title)
self.table = TrackTableView(playlist_mode=True)
self.table.set_read_only(True)
self.table.set_content_editable(False)
self._settings = PlaylistSettings(visible_columns=list(FOLLOWED_COLUMNS),
sort_column="#")
self.table.apply_settings(self._settings)
self.table.columns_changed.connect(self._on_columns)
self.table.model_.foreground_for = self._foreground
self.table.cassette_hooks = (self._cassette_state, self._cassette_toggle)
self.table.doubleClicked.connect(self._on_double_click)
layout.addWidget(self.table)
self._cached: set[int] = set()
self._friend_name = ""
def context_id(self) -> str:
return f"followed:{self.key}"
def show_followed(self, key: str):
"""Load (or reload) the followed playlist ``key``."""
self.key = key
parts = key.split(":")
self.token, pid = parts[1], parts[2]
playlist, tracks, friend_name = self._cassette.followed_contents(self.token, pid)
self._friend_name = friend_name
self._cached = self._cassette.cached_ids(self.token)
name = playlist.name if playlist is not None else "(no longer shared)"
self.title.setText(f"{name} · {friend_name}'s playlist")
self.table.set_tracks(tracks)
def refresh(self):
if self.key:
self.show_followed(self.key)
def status_suffix(self) -> str:
"""For the bottom bar: when the friend's folder last synced."""
return self._cassette.last_synced_text(self.token)
def _on_columns(self, columns):
self._settings.visible_columns = columns
self.table.apply_settings(self._settings)
def _foreground(self, track):
return None if track.track_id in self._cached else NOT_HERE_GREY
def _cassette_state(self, track_id: int):
return self._cassette.cassette_state(self.token, track_id)
def _cassette_toggle(self, track_id: int):
self._cassette.toggle_cassette(self.token, track_id)
def _on_double_click(self, index):
tracks = [self.table.model_.track_at(self.table.model_.source_row(row))
for row in range(self.table.model_.rowCount())]
clicked = tracks[index.row()]
playable = [t for t in tracks if t.track_id in self._cached]
if clicked.track_id not in self._cached:
self._cassette.say(f"“{clicked.name}” hasn't arrived from "
f"{self._friend_name} yet.")
return
keys = [track_key(self.token, t.track_id) for t in playable]
self.play_requested.emit(keys, playable.index(clicked), self.context_id())
def set_now_playing(self, key, playing: bool):
parsed = parse_key(key) if key else None
track_id = parsed[1] if parsed and parsed[0] == self.token else None
self.table.model_.set_now_playing(track_id, playing)