feat: discord presence, cached audio playback, multiinstance safety

Add
- Discord rich presence ("Listening to", cover, progress, Listen Along
  button). enable via [discord] client_id in config.toml : retries quietly
  if Discord isn't running. new dep: discord-rich-presence.
- :save! / :w! to overwrite a playlist. Plain :save now refuses to
  clobber an existing name.

Change
- Download audio with yt-dlp into the cache dir and play the local file
  (mpv --ytdl=no) instead of streaming. Keep the last 40 files and
  prefetch the next queued track.
- show the the actual yt-dlp/mpv error on playback failure instead of the
  generic "update yt-dlp" hint; allow wider status messages (70 - 120 char).
- Split artists from YouTube's linked artist runs and yt-dlp's 'artists'
  list (new Track::artists) rather than guessing from the display string.
  Fallback splitter keeps names like "Tyler, The Creator" whole and
  matches separators case-insensitively. (shit)
- Restyle selection (accent bold text + bar) and tab bar (underlined
  active tab instead of pills)
- Share one ureq agent across yt Music requests

Fix
- Multiple drift instances: only the first (flock on session.lock) owns
  the session, and playlist saves merge with what other instances wrote.
- Collection picker no longer steals keys while typing.
- "Play next" now shifts `played` indices so shuffle history stays right.
- Leaving the queue for a playlist/result list marks the queue as done.
- Replace unwrap() in lyrics sorting with total_cmp (NaN-safe).
This commit is contained in:
socatlolmeow 2026-10-29 18:19:00 +01:00
commit eb6b16e70b
16 changed files with 966 additions and 171 deletions

View file

@ -30,6 +30,7 @@ pub enum Command {
Shuffle,
Repeat(Option<RepeatMode>),
Save(String),
SaveForce(String),
Load(String),
Append(String),
DeletePlaylist(String),
@ -172,6 +173,7 @@ pub fn parse(line: &str) -> Result<Option<Command>, String> {
other => return Err(format!(":repeat expects off|all|one, got '{other}'")),
},
"save" | "w" => Save(name_arg(arg, "save")?),
"save!" | "w!" => SaveForce(name_arg(arg, "save!")?),
"load" | "e" => Load(name_arg(arg, "load")?),
"append" => Append(name_arg(arg, "append")?),
"delpl" | "deletepl" | "delete" => DeletePlaylist(name_arg(arg, "delpl")?),
@ -278,5 +280,9 @@ mod tests {
Ok(Some(Command::Save("my mix".into())))
);
assert!(parse("save").is_err());
assert_eq!(
parse("save! my mix"),
Ok(Some(Command::SaveForce("my mix".into())))
);
}
}