- 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>
120 lines
4.9 KiB
Python
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)
|