use anyhow::{Context, Result};
use ratatui::crossterm::event::{KeyCode, KeyEvent};
use rspotify::AuthCodePkceSpotify;
use rspotify::model::{AlbumId, PlayContextId, PlayableId, PlaylistId, TrackId};
use rspotify::prelude::*;
use crate::auth;
use crate::format::join_artists;
const PLAYLIST_LIMIT: u32 = 50;
const SAVED_LIMIT: u32 = 20;
#[derive(Clone, Copy, PartialEq)]
pub enum BrowseTab {
Playlists,
Tracks,
Albums,
}
impl BrowseTab {
pub const ALL: [BrowseTab; 3] = [BrowseTab::Playlists, BrowseTab::Tracks, BrowseTab::Albums];
pub fn label(self) -> &'static str {
match self {
BrowseTab::Playlists => "Playlists",
BrowseTab::Tracks => "Saved Tracks",
BrowseTab::Albums => "Saved Albums",
}
}
pub fn next(self) -> Self {
match self {
BrowseTab::Playlists => BrowseTab::Tracks,
BrowseTab::Tracks => BrowseTab::Albums,
BrowseTab::Albums => BrowseTab::Playlists,
}
}
pub fn prev(self) -> Self {
match self {
BrowseTab::Playlists => BrowseTab::Albums,
BrowseTab::Tracks => BrowseTab::Playlists,
BrowseTab::Albums => BrowseTab::Tracks,
}
}
}
#[derive(Clone)]
pub enum PlayTarget {
Track(String),
Playlist(String),
Album(String),
}
#[derive(Clone)]
pub struct BrowseItem {
pub title: String,
pub subtitle: String,
pub target: PlayTarget,
}
#[derive(Default)]
pub struct BrowseCache {
playlists: Option<Vec<BrowseItem>>,
tracks: Option<Vec<BrowseItem>>,
albums: Option<Vec<BrowseItem>>,
}
impl BrowseCache {
fn slot(&mut self, tab: BrowseTab) -> &mut Option<Vec<BrowseItem>> {
match tab {
BrowseTab::Playlists => &mut self.playlists,
BrowseTab::Tracks => &mut self.tracks,
BrowseTab::Albums => &mut self.albums,
}
}
pub fn get(&self, tab: BrowseTab) -> Option<&Vec<BrowseItem>> {
match tab {
BrowseTab::Playlists => self.playlists.as_ref(),
BrowseTab::Tracks => self.tracks.as_ref(),
BrowseTab::Albums => self.albums.as_ref(),
}
}
pub fn set(&mut self, tab: BrowseTab, items: Vec<BrowseItem>) {
*self.slot(tab) = Some(items);
}
pub fn clear(&mut self, tab: BrowseTab) {
*self.slot(tab) = None;
}
}
pub struct BrowseState {
pub tab: BrowseTab,
pub items: Vec<BrowseItem>,
pub selected: usize,
pub message: Option<String>,
}
pub enum BrowseAction {
None,
Close,
Switch(BrowseTab),
Play,
Reload,
}
pub fn key_action(key: KeyEvent, state: &mut BrowseState) -> BrowseAction {
match key.code {
KeyCode::Esc => BrowseAction::Close,
KeyCode::Left => BrowseAction::Switch(state.tab.prev()),
KeyCode::Right => BrowseAction::Switch(state.tab.next()),
KeyCode::Up => {
state.selected = state.selected.saturating_sub(1);
BrowseAction::None
}
KeyCode::Down => {
if state.selected + 1 < state.items.len() {
state.selected += 1;
}
BrowseAction::None
}
KeyCode::Enter => BrowseAction::Play,
KeyCode::Char('r') => BrowseAction::Reload,
_ => BrowseAction::None,
}
}
pub async fn fetch(spotify: &AuthCodePkceSpotify, tab: BrowseTab) -> Result<Vec<BrowseItem>> {
auth::ensure_fresh_token(spotify).await?;
match tab {
BrowseTab::Playlists => {
let page = spotify
.current_user_playlists_manual(Some(PLAYLIST_LIMIT), None)
.await
.context("failed to fetch the playlist list")?;
Ok(page
.items
.into_iter()
.map(|pl| BrowseItem {
title: pl.name,
subtitle: format!("{} tracks", pl.items.total),
target: PlayTarget::Playlist(pl.id.uri()),
})
.collect())
}
BrowseTab::Tracks => {
let page = spotify
.current_user_saved_tracks_manual(None, Some(SAVED_LIMIT), None)
.await
.context("failed to fetch saved tracks")?;
Ok(page
.items
.into_iter()
.filter_map(|saved| {
let t = saved.track;
let uri = t.id.as_ref()?.uri();
let artists: Vec<String> = t.artists.into_iter().map(|a| a.name).collect();
Some(BrowseItem {
title: t.name,
subtitle: join_artists(&artists),
target: PlayTarget::Track(uri),
})
})
.collect())
}
BrowseTab::Albums => {
let page = spotify
.current_user_saved_albums_manual(None, Some(SAVED_LIMIT), None)
.await
.context("failed to fetch saved albums")?;
Ok(page
.items
.into_iter()
.map(|saved| {
let a = saved.album;
let artists: Vec<String> = a.artists.into_iter().map(|x| x.name).collect();
BrowseItem {
title: a.name,
subtitle: join_artists(&artists),
target: PlayTarget::Album(a.id.uri()),
}
})
.collect())
}
}
}
pub async fn play(spotify: &AuthCodePkceSpotify, target: &PlayTarget) -> Result<()> {
auth::ensure_fresh_token(spotify).await?;
let result = match target {
PlayTarget::Track(uri) => {
let id = TrackId::from_uri(uri).context("failed to parse the track URI")?;
spotify
.start_uris_playback([PlayableId::Track(id)], None, None, None)
.await
}
PlayTarget::Playlist(uri) => {
let id = PlaylistId::from_uri(uri).context("failed to parse the playlist URI")?;
spotify
.start_context_playback(PlayContextId::Playlist(id), None, None, None)
.await
}
PlayTarget::Album(uri) => {
let id = AlbumId::from_uri(uri).context("failed to parse the album URI")?;
spotify
.start_context_playback(PlayContextId::Album(id), None, None, None)
.await
}
};
result.context("failed to start playback (an active device may be required)")?;
Ok(())
}