Refactor: move playable models to separate module

This commit is contained in:
Henrik Friedrichsen
2021-11-08 20:44:07 +01:00
parent 2d1923f4e7
commit 74b4e65b64
27 changed files with 86 additions and 83 deletions

305
src/model/album.rs Normal file
View File

@@ -0,0 +1,305 @@
use rand::{seq::IteratorRandom, thread_rng};
use rspotify::model::Id;
use std::fmt;
use std::sync::{Arc, RwLock};
use chrono::{DateTime, Utc};
use log::debug;
use rspotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum};
use crate::library::Library;
use crate::model::artist::Artist;
use crate::model::playable::Playable;
use crate::model::track::Track;
use crate::queue::Queue;
use crate::spotify::Spotify;
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
use crate::ui::{album::AlbumView, listview::ListView};
#[derive(Clone, Deserialize, Serialize)]
pub struct Album {
pub id: Option<String>,
pub title: String,
pub artists: Vec<String>,
pub artist_ids: Vec<String>,
pub year: String,
pub cover_url: Option<String>,
pub url: Option<String>,
pub tracks: Option<Vec<Track>>,
pub added_at: Option<DateTime<Utc>>,
}
impl Album {
pub fn load_all_tracks(&mut self, spotify: Spotify) {
if self.tracks.is_some() {
return;
}
if let Some(ref album_id) = self.id {
let mut collected_tracks = Vec::new();
if let Some(full_album) = spotify.api.full_album(album_id) {
let mut tracks_result = Some(full_album.tracks.clone());
while let Some(ref tracks) = tracks_result {
for t in &tracks.items {
collected_tracks.push(Track::from_simplified_track(t, &full_album));
}
debug!("got {} tracks", tracks.items.len());
// load next batch if necessary
tracks_result = match tracks.next {
Some(_) => {
debug!("requesting tracks again..");
spotify.api.album_tracks(
album_id,
50,
tracks.offset + tracks.items.len() as u32,
)
}
None => None,
}
}
}
self.tracks = Some(collected_tracks)
}
}
}
impl From<&SimplifiedAlbum> for Album {
fn from(sa: &SimplifiedAlbum) -> Self {
Self {
id: sa.id.as_ref().map(|id| id.id().to_string()),
title: sa.name.clone(),
artists: sa.artists.iter().map(|sa| sa.name.clone()).collect(),
artist_ids: sa
.artists
.iter()
.filter_map(|a| a.id.as_ref().map(|id| id.id().to_string()))
.collect(),
year: sa
.release_date
.clone()
.unwrap_or_default()
.split('-')
.next()
.unwrap()
.into(),
cover_url: sa.images.get(0).map(|i| i.url.clone()),
url: sa.id.as_ref().map(|id| id.url()),
tracks: None,
added_at: None,
}
}
}
impl From<&FullAlbum> for Album {
fn from(fa: &FullAlbum) -> Self {
let tracks = Some(
fa.tracks
.items
.iter()
.map(|st| Track::from_simplified_track(st, fa))
.collect(),
);
Self {
id: Some(fa.id.id().to_string()),
title: fa.name.clone(),
artists: fa.artists.iter().map(|sa| sa.name.clone()).collect(),
artist_ids: fa
.artists
.iter()
.filter_map(|a| a.id.as_ref().map(|id| id.id().to_string()))
.collect(),
year: fa.release_date.split('-').next().unwrap().into(),
cover_url: fa.images.get(0).map(|i| i.url.clone()),
url: Some(fa.id.uri()),
tracks,
added_at: None,
}
}
}
impl From<&SavedAlbum> for Album {
fn from(sa: &SavedAlbum) -> Self {
let mut album: Self = (&sa.album).into();
album.added_at = Some(sa.added_at);
album
}
}
impl fmt::Display for Album {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} - {}", self.artists.join(", "), self.title)
}
}
impl fmt::Debug for Album {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"({} - {} ({:?}))",
self.artists.join(", "),
self.title,
self.id
)
}
}
impl ListItem for Album {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
if let Some(tracks) = self.tracks.as_ref() {
let playing: Vec<String> = queue
.queue
.read()
.unwrap()
.iter()
.filter_map(|t| t.id())
.collect();
let ids: Vec<String> = tracks.iter().filter_map(|t| t.id.clone()).collect();
!ids.is_empty() && playing == ids
} else {
false
}
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
fn display_left(&self) -> String {
format!("{}", self)
}
fn display_right(&self, library: Arc<Library>) -> String {
let saved = if library.is_saved_album(self) {
if library.cfg.values().use_nerdfont.unwrap_or(false) {
"\u{f62b} "
} else {
""
}
} else {
""
};
format!("{}{}", saved, self.year)
}
fn play(&mut self, queue: Arc<Queue>) {
self.load_all_tracks(queue.get_spotify());
if let Some(tracks) = self.tracks.as_ref() {
let tracks: Vec<Playable> = tracks
.iter()
.map(|track| Playable::Track(track.clone()))
.collect();
let index = queue.append_next(&tracks);
queue.play(index, true, true);
}
}
fn play_next(&mut self, queue: Arc<Queue>) {
self.load_all_tracks(queue.get_spotify());
if let Some(tracks) = self.tracks.as_ref() {
for t in tracks.iter().rev() {
queue.insert_after_current(Playable::Track(t.clone()));
}
}
}
fn queue(&mut self, queue: Arc<Queue>) {
self.load_all_tracks(queue.get_spotify());
if let Some(tracks) = self.tracks.as_ref() {
for t in tracks {
queue.append(Playable::Track(t.clone()));
}
}
}
fn save(&mut self, library: Arc<Library>) {
library.save_album(self);
}
fn unsave(&mut self, library: Arc<Library>) {
library.unsave_album(self);
}
fn toggle_saved(&mut self, library: Arc<Library>) {
if library.is_saved_album(self) {
library.unsave_album(self);
} else {
library.save_album(self);
}
}
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
Some(AlbumView::new(queue, library, self).into_boxed_view_ext())
}
fn open_recommendations(
&mut self,
queue: Arc<Queue>,
library: Arc<Library>,
) -> Option<Box<dyn ViewExt>> {
self.load_all_tracks(queue.get_spotify());
const MAX_SEEDS: usize = 5;
let track_ids: Vec<&str> = self
.tracks
.as_ref()?
.iter()
.map(|t| t.id.as_deref())
.flatten()
// spotify allows at max 5 seed items, so choose 4 random tracks...
.choose_multiple(&mut thread_rng(), MAX_SEEDS - 1);
let artist_id: Option<String> = self
.artist_ids
.iter()
.cloned()
// ...and one artist
.choose(&mut thread_rng());
if track_ids.is_empty() && artist_id.is_some() {
return None;
}
let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = spotify
.api
.recommendations(
artist_id.as_ref().map(|aid| vec![aid.as_str()]),
None,
Some(track_ids),
)
.map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect());
recommendations.map(|tracks| {
ListView::new(
Arc::new(RwLock::new(tracks)),
queue.clone(),
library.clone(),
)
.set_title(format!("Similar to Album \"{}\"", self.title))
.into_boxed_view_ext()
})
}
fn share_url(&self) -> Option<String> {
self.id
.clone()
.map(|id| format!("https://open.spotify.com/album/{}", id))
}
fn artists(&self) -> Option<Vec<Artist>> {
Some(
self.artist_ids
.iter()
.zip(self.artists.iter())
.map(|(id, name)| Artist::new(id.clone(), name.clone()))
.collect(),
)
}
}

208
src/model/artist.rs Normal file
View File

@@ -0,0 +1,208 @@
use std::fmt;
use std::sync::{Arc, RwLock};
use rspotify::model::artist::{FullArtist, SimplifiedArtist};
use rspotify::model::Id;
use crate::library::Library;
use crate::model::playable::Playable;
use crate::model::track::Track;
use crate::queue::Queue;
use crate::spotify::Spotify;
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
use crate::ui::{artist::ArtistView, listview::ListView};
#[derive(Clone, Deserialize, Serialize)]
pub struct Artist {
pub id: Option<String>,
pub name: String,
pub url: Option<String>,
pub tracks: Option<Vec<Track>>,
pub is_followed: bool,
}
impl Artist {
pub fn new(id: String, name: String) -> Self {
Self {
id: Some(id),
name,
url: None,
tracks: None,
is_followed: false,
}
}
fn load_top_tracks(&mut self, spotify: Spotify) {
if let Some(artist_id) = &self.id {
if self.tracks.is_none() {
self.tracks = spotify.api.artist_top_tracks(artist_id);
}
}
}
}
impl From<&SimplifiedArtist> for Artist {
fn from(sa: &SimplifiedArtist) -> Self {
Self {
id: sa.id.as_ref().map(|id| id.id().to_string()),
name: sa.name.clone(),
url: sa.id.as_ref().map(|id| id.url()),
tracks: None,
is_followed: false,
}
}
}
impl From<&FullArtist> for Artist {
fn from(fa: &FullArtist) -> Self {
Self {
id: Some(fa.id.id().to_string()),
name: fa.name.clone(),
url: Some(fa.id.url()),
tracks: None,
is_followed: false,
}
}
}
impl fmt::Display for Artist {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl fmt::Debug for Artist {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({:?})", self.name, self.id)
}
}
impl ListItem for Artist {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
if let Some(tracks) = &self.tracks {
let playing: Vec<String> = queue
.queue
.read()
.unwrap()
.iter()
.filter_map(|t| t.id())
.collect();
let ids: Vec<String> = tracks.iter().filter_map(|t| t.id.clone()).collect();
!ids.is_empty() && playing == ids
} else {
false
}
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
fn display_left(&self) -> String {
format!("{}", self)
}
fn display_right(&self, library: Arc<Library>) -> String {
let followed = if library.is_followed_artist(self) {
if library.cfg.values().use_nerdfont.unwrap_or(false) {
"\u{f62b} "
} else {
""
}
} else {
""
};
let tracks = if let Some(tracks) = self.tracks.as_ref() {
format!("{:>3} saved tracks", tracks.len())
} else {
"".into()
};
format!("{}{}", followed, tracks)
}
fn play(&mut self, queue: Arc<Queue>) {
self.load_top_tracks(queue.get_spotify());
if let Some(tracks) = self.tracks.as_ref() {
let tracks: Vec<Playable> = tracks
.iter()
.map(|track| Playable::Track(track.clone()))
.collect();
let index = queue.append_next(&tracks);
queue.play(index, true, true);
}
}
fn play_next(&mut self, queue: Arc<Queue>) {
self.load_top_tracks(queue.get_spotify());
if let Some(tracks) = self.tracks.as_ref() {
for t in tracks.iter().rev() {
queue.insert_after_current(Playable::Track(t.clone()));
}
}
}
fn queue(&mut self, queue: Arc<Queue>) {
self.load_top_tracks(queue.get_spotify());
if let Some(tracks) = &self.tracks {
for t in tracks {
queue.append(Playable::Track(t.clone()));
}
}
}
fn save(&mut self, library: Arc<Library>) {
library.follow_artist(self);
}
fn unsave(&mut self, library: Arc<Library>) {
library.unfollow_artist(self);
}
fn toggle_saved(&mut self, library: Arc<Library>) {
if library.is_followed_artist(self) {
library.unfollow_artist(self);
} else {
library.follow_artist(self);
}
}
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
Some(ArtistView::new(queue, library, self).into_boxed_view_ext())
}
fn open_recommendations(
&mut self,
queue: Arc<Queue>,
library: Arc<Library>,
) -> Option<Box<dyn ViewExt>> {
let id = self.id.as_ref()?.to_string();
let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = spotify
.api
.recommendations(Some(vec![&id]), None, None)
.map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect());
recommendations.map(|tracks| {
ListView::new(
Arc::new(RwLock::new(tracks)),
queue.clone(),
library.clone(),
)
.set_title(format!("Similar to Artist \"{}\"", self.name,))
.into_boxed_view_ext()
})
}
fn share_url(&self) -> Option<String> {
self.id
.clone()
.map(|id| format!("https://open.spotify.com/artist/{}", id))
}
}

116
src/model/episode.rs Normal file
View File

@@ -0,0 +1,116 @@
use crate::library::Library;
use crate::model::playable::Playable;
use crate::queue::Queue;
use crate::traits::{ListItem, ViewExt};
use chrono::{DateTime, Utc};
use rspotify::model::show::{FullEpisode, SimplifiedEpisode};
use rspotify::model::Id;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Episode {
pub id: String,
pub uri: String,
pub duration: u32,
pub name: String,
pub description: String,
pub release_date: String,
pub cover_url: Option<String>,
pub added_at: Option<DateTime<Utc>>,
pub list_index: usize,
}
impl Episode {
pub fn duration_str(&self) -> String {
let minutes = self.duration / 60_000;
let seconds = (self.duration / 1000) % 60;
format!("{:02}:{:02}", minutes, seconds)
}
}
impl From<&SimplifiedEpisode> for Episode {
fn from(episode: &SimplifiedEpisode) -> Self {
Self {
id: episode.id.id().to_string(),
uri: episode.id.uri(),
duration: episode.duration.as_millis() as u32,
name: episode.name.clone(),
description: episode.description.clone(),
release_date: episode.release_date.clone(),
cover_url: episode.images.get(0).map(|img| img.url.clone()),
added_at: None,
list_index: 0,
}
}
}
impl From<&FullEpisode> for Episode {
fn from(episode: &FullEpisode) -> Self {
Self {
id: episode.id.id().to_string(),
uri: episode.id.uri(),
duration: episode.duration.as_millis() as u32,
name: episode.name.clone(),
description: episode.description.clone(),
release_date: episode.release_date.clone(),
cover_url: episode.images.get(0).map(|img| img.url.clone()),
added_at: None,
list_index: 0,
}
}
}
impl fmt::Display for Episode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl ListItem for Episode {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
let current = queue.get_current();
current
.map(|t| t.id() == Some(self.id.clone()))
.unwrap_or(false)
}
fn display_left(&self) -> String {
self.name.clone()
}
fn display_right(&self, _library: Arc<Library>) -> String {
format!("{} [{}]", self.duration_str(), self.release_date)
}
fn play(&mut self, queue: Arc<Queue>) {
let index = queue.append_next(&vec![Playable::Episode(self.clone())]);
queue.play(index, true, false);
}
fn play_next(&mut self, queue: Arc<Queue>) {
queue.insert_after_current(Playable::Episode(self.clone()));
}
fn queue(&mut self, queue: Arc<Queue>) {
queue.append(Playable::Episode(self.clone()));
}
fn toggle_saved(&mut self, _library: Arc<Library>) {}
fn save(&mut self, _library: Arc<Library>) {}
fn unsave(&mut self, _library: Arc<Library>) {}
fn open(&self, _queue: Arc<Queue>, _library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
None
}
fn share_url(&self) -> Option<String> {
Some(format!("https://open.spotify.com/episode/{}", self.id))
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
}

7
src/model/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod album;
pub mod artist;
pub mod episode;
pub mod playable;
pub mod playlist;
pub mod show;
pub mod track;

175
src/model/playable.rs Normal file
View File

@@ -0,0 +1,175 @@
use chrono::{DateTime, Utc};
use rspotify::model::PlayableItem;
use crate::library::Library;
use crate::model::album::Album;
use crate::model::artist::Artist;
use crate::model::episode::Episode;
use crate::model::track::Track;
use crate::queue::Queue;
use crate::traits::{ListItem, ViewExt};
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum Playable {
Track(Track),
Episode(Episode),
}
impl Playable {
pub fn id(&self) -> Option<String> {
match self {
Playable::Track(track) => track.id.clone(),
Playable::Episode(episode) => Some(episode.id.clone()),
}
}
pub fn uri(&self) -> String {
match self {
Playable::Track(track) => track.uri.clone(),
Playable::Episode(episode) => episode.uri.clone(),
}
}
pub fn cover_url(&self) -> Option<String> {
match self {
Playable::Track(track) => track.cover_url.clone(),
Playable::Episode(episode) => episode.cover_url.clone(),
}
}
pub fn duration(&self) -> u32 {
match self {
Playable::Track(track) => track.duration,
Playable::Episode(episode) => episode.duration,
}
}
pub fn list_index(&self) -> usize {
match self {
Playable::Track(track) => track.list_index,
Playable::Episode(episode) => episode.list_index,
}
}
pub fn set_list_index(&mut self, index: usize) {
match self {
Playable::Track(track) => track.list_index = index,
Playable::Episode(episode) => episode.list_index = index,
}
}
pub fn added_at(&self) -> Option<DateTime<Utc>> {
match self {
Playable::Track(track) => track.added_at,
Playable::Episode(episode) => episode.added_at,
}
}
pub fn set_added_at(&mut self, added_at: Option<DateTime<Utc>>) {
match self {
Playable::Track(track) => track.added_at = added_at,
Playable::Episode(episode) => episode.added_at = added_at,
}
}
pub fn duration_str(&self) -> String {
let duration = self.duration();
let minutes = duration / 60_000;
let seconds = (duration / 1000) % 60;
format!("{:02}:{:02}", minutes, seconds)
}
pub fn as_listitem(&self) -> Box<dyn ListItem> {
match self {
Playable::Track(track) => track.as_listitem(),
Playable::Episode(episode) => episode.as_listitem(),
}
}
}
impl From<&PlayableItem> for Playable {
fn from(item: &PlayableItem) -> Self {
match item {
PlayableItem::Episode(episode) => Playable::Episode(episode.into()),
PlayableItem::Track(track) => Playable::Track(track.into()),
}
}
}
impl fmt::Display for Playable {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Playable::Track(track) => track.fmt(f),
Playable::Episode(episode) => episode.fmt(f),
}
}
}
impl ListItem for Playable {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
self.as_listitem().is_playing(queue)
}
fn display_left(&self) -> String {
self.as_listitem().display_left()
}
fn display_center(&self, library: Arc<Library>) -> String {
self.as_listitem().display_center(library)
}
fn display_right(&self, library: Arc<Library>) -> String {
self.as_listitem().display_right(library)
}
fn play(&mut self, queue: Arc<Queue>) {
self.as_listitem().play(queue)
}
fn play_next(&mut self, queue: Arc<Queue>) {
self.as_listitem().play_next(queue)
}
fn queue(&mut self, queue: Arc<Queue>) {
self.as_listitem().queue(queue)
}
fn toggle_saved(&mut self, library: Arc<Library>) {
self.as_listitem().toggle_saved(library)
}
fn save(&mut self, library: Arc<Library>) {
self.as_listitem().save(library)
}
fn unsave(&mut self, library: Arc<Library>) {
self.as_listitem().unsave(library)
}
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
self.as_listitem().open(queue, library)
}
fn share_url(&self) -> Option<String> {
self.as_listitem().share_url()
}
fn album(&self, queue: Arc<Queue>) -> Option<Album> {
self.as_listitem().album(queue)
}
fn artists(&self) -> Option<Vec<Artist>> {
self.as_listitem().artists()
}
fn track(&self) -> Option<Track> {
self.as_listitem().track()
}
fn as_listitem(&self) -> Box<dyn ListItem> {
self.as_listitem()
}
}

327
src/model/playlist.rs Normal file
View File

@@ -0,0 +1,327 @@
use std::collections::HashSet;
use std::sync::{Arc, RwLock};
use std::{cmp::Ordering, iter::Iterator};
use rand::{seq::IteratorRandom, thread_rng};
use log::debug;
use rspotify::model::playlist::{FullPlaylist, SimplifiedPlaylist};
use rspotify::model::Id;
use crate::model::playable::Playable;
use crate::model::track::Track;
use crate::queue::Queue;
use crate::spotify::Spotify;
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
use crate::ui::{listview::ListView, playlist::PlaylistView};
use crate::{command::SortDirection, command::SortKey, library::Library};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Playlist {
pub id: String,
pub name: String,
pub owner_id: String,
pub snapshot_id: String,
pub num_tracks: usize,
pub tracks: Option<Vec<Playable>>,
pub collaborative: bool,
}
impl Playlist {
pub fn load_tracks(&mut self, spotify: Spotify) {
if self.tracks.is_some() {
return;
}
self.tracks = Some(self.get_all_tracks(spotify));
}
fn get_all_tracks(&self, spotify: Spotify) -> Vec<Playable> {
let tracks_result = spotify.api.user_playlist_tracks(&self.id);
while !tracks_result.at_end() {
tracks_result.next();
}
let tracks = tracks_result.items.read().unwrap();
tracks.clone()
}
pub fn has_track(&self, track_id: &str) -> bool {
self.tracks.as_ref().map_or(false, |tracks| {
tracks
.iter()
.any(|track| track.id() == Some(track_id.to_string()))
})
}
pub fn delete_track(&mut self, index: usize, spotify: Spotify, library: Arc<Library>) -> bool {
let track = self.tracks.as_ref().unwrap()[index].clone();
debug!("deleting track: {} {:?}", index, track);
match spotify
.api
.delete_tracks(&self.id, &self.snapshot_id, &[track])
{
false => false,
true => {
if let Some(tracks) = &mut self.tracks {
tracks.remove(index);
library.playlist_update(self);
}
true
}
}
}
pub fn append_tracks(
&mut self,
new_tracks: &[Playable],
spotify: Spotify,
library: Arc<Library>,
) {
let mut has_modified = false;
if spotify.api.append_tracks(&self.id, new_tracks, None) {
if let Some(tracks) = &mut self.tracks {
tracks.append(&mut new_tracks.to_vec());
has_modified = true;
}
}
if has_modified {
library.playlist_update(self);
}
}
pub fn sort(&mut self, key: &SortKey, direction: &SortDirection) {
fn compare_artists(a: Vec<String>, b: Vec<String>) -> Ordering {
let sanitize_artists_name = |x: Vec<String>| -> Vec<String> {
x.iter()
.map(|x| {
x.to_lowercase()
.split(' ')
.skip_while(|x| x == &"the")
.collect()
})
.collect()
};
let a = sanitize_artists_name(a);
let b = sanitize_artists_name(b);
a.cmp(&b)
}
if let Some(c) = self.tracks.as_mut() {
c.sort_by(|a, b| match (a.track(), b.track()) {
(Some(a), Some(b)) => match (key, direction) {
(SortKey::Title, SortDirection::Ascending) => {
a.title.to_lowercase().cmp(&b.title.to_lowercase())
}
(SortKey::Title, SortDirection::Descending) => {
b.title.to_lowercase().cmp(&a.title.to_lowercase())
}
(SortKey::Duration, SortDirection::Ascending) => a.duration.cmp(&b.duration),
(SortKey::Duration, SortDirection::Descending) => b.duration.cmp(&a.duration),
(SortKey::Album, SortDirection::Ascending) => a
.album
.map(|x| x.to_lowercase())
.cmp(&b.album.map(|x| x.to_lowercase())),
(SortKey::Album, SortDirection::Descending) => b
.album
.map(|x| x.to_lowercase())
.cmp(&a.album.map(|x| x.to_lowercase())),
(SortKey::Added, SortDirection::Ascending) => a.added_at.cmp(&b.added_at),
(SortKey::Added, SortDirection::Descending) => b.added_at.cmp(&a.added_at),
(SortKey::Artist, SortDirection::Ascending) => {
compare_artists(a.artists, b.artists)
}
(SortKey::Artist, SortDirection::Descending) => {
compare_artists(b.artists, a.artists)
}
},
_ => std::cmp::Ordering::Equal,
})
}
}
}
impl From<&SimplifiedPlaylist> for Playlist {
fn from(list: &SimplifiedPlaylist) -> Self {
Playlist {
id: list.id.id().to_string(),
name: list.name.clone(),
owner_id: list.owner.id.id().to_string(),
snapshot_id: list.snapshot_id.clone(),
num_tracks: list.tracks.total as usize,
tracks: None,
collaborative: list.collaborative,
}
}
}
impl From<&FullPlaylist> for Playlist {
fn from(list: &FullPlaylist) -> Self {
Playlist {
id: list.id.id().to_string(),
name: list.name.clone(),
owner_id: list.owner.id.id().to_string(),
snapshot_id: list.snapshot_id.clone(),
num_tracks: list.tracks.total as usize,
tracks: None,
collaborative: list.collaborative,
}
}
}
impl ListItem for Playlist {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
if let Some(tracks) = self.tracks.as_ref() {
let playing: Vec<String> = queue
.queue
.read()
.unwrap()
.iter()
.filter_map(|t| t.id())
.collect();
let ids: Vec<String> = tracks.iter().filter_map(|t| t.id()).collect();
!ids.is_empty() && playing == ids
} else {
false
}
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
fn display_left(&self) -> String {
self.name.clone()
}
fn display_right(&self, library: Arc<Library>) -> String {
let saved = if library.is_saved_playlist(self) {
if library.cfg.values().use_nerdfont.unwrap_or(false) {
"\u{f62b} "
} else {
""
}
} else {
""
};
let num_tracks = self
.tracks
.as_ref()
.map(|t| t.len())
.unwrap_or(self.num_tracks);
format!("{}{:>4} tracks", saved, num_tracks)
}
fn play(&mut self, queue: Arc<Queue>) {
self.load_tracks(queue.get_spotify());
if let Some(tracks) = &self.tracks {
let index = queue.append_next(tracks);
queue.play(index, true, true);
}
}
fn play_next(&mut self, queue: Arc<Queue>) {
self.load_tracks(queue.get_spotify());
if let Some(tracks) = self.tracks.as_ref() {
for track in tracks.iter().rev() {
queue.insert_after_current(track.clone());
}
}
}
fn queue(&mut self, queue: Arc<Queue>) {
self.load_tracks(queue.get_spotify());
if let Some(tracks) = self.tracks.as_ref() {
for track in tracks.iter() {
queue.append(track.clone());
}
}
}
fn save(&mut self, library: Arc<Library>) {
library.follow_playlist(self);
}
fn unsave(&mut self, library: Arc<Library>) {
library.delete_playlist(&self.id);
}
fn toggle_saved(&mut self, library: Arc<Library>) {
// Don't allow users to unsave their own playlists with one keypress
if !library.is_followed_playlist(self) {
return;
}
if library.is_saved_playlist(self) {
library.delete_playlist(&self.id);
} else {
library.follow_playlist(self);
}
}
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
Some(PlaylistView::new(queue, library, self).into_boxed_view_ext())
}
fn open_recommendations(
&mut self,
queue: Arc<Queue>,
library: Arc<Library>,
) -> Option<Box<dyn ViewExt>> {
self.load_tracks(queue.get_spotify());
const MAX_SEEDS: usize = 5;
let track_ids: Vec<String> = self
.tracks
.as_ref()?
.iter()
.map(|t| t.id())
.flatten()
// only select unique tracks
.collect::<HashSet<_>>()
.into_iter()
// spotify allows at max 5 seed items, so choose them at random
.choose_multiple(&mut thread_rng(), MAX_SEEDS);
if track_ids.is_empty() {
return None;
}
let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = spotify
.api
.recommendations(
None,
None,
Some(track_ids.iter().map(|t| t.as_ref()).collect()),
)
.map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect());
recommendations.map(|tracks| {
ListView::new(
Arc::new(RwLock::new(tracks)),
queue.clone(),
library.clone(),
)
.set_title(format!("Similar to Tracks in \"{}\"", self.name,))
.into_boxed_view_ext()
})
}
fn share_url(&self) -> Option<String> {
Some(format!(
"https://open.spotify.com/user/{}/playlist/{}",
self.owner_id, self.id
))
}
}

156
src/model/show.rs Normal file
View File

@@ -0,0 +1,156 @@
use crate::library::Library;
use crate::model::episode::Episode;
use crate::model::playable::Playable;
use crate::queue::Queue;
use crate::spotify::Spotify;
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
use crate::ui::show::ShowView;
use rspotify::model::show::{FullShow, SimplifiedShow};
use rspotify::model::Id;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Deserialize, Serialize)]
pub struct Show {
pub id: String,
pub uri: String,
pub name: String,
pub publisher: String,
pub description: String,
pub cover_url: Option<String>,
pub episodes: Option<Vec<Episode>>,
}
impl Show {
pub fn load_all_episodes(&mut self, spotify: Spotify) {
if self.episodes.is_some() {
return;
}
let episodes_result = spotify.api.show_episodes(&self.id);
while !episodes_result.at_end() {
episodes_result.next();
}
let episodes = episodes_result.items.read().unwrap().clone();
self.episodes = Some(episodes);
}
}
impl From<&SimplifiedShow> for Show {
fn from(show: &SimplifiedShow) -> Self {
Self {
id: show.id.id().to_string(),
uri: show.id.uri(),
name: show.name.clone(),
publisher: show.publisher.clone(),
description: show.description.clone(),
cover_url: show.images.get(0).map(|i| i.url.clone()),
episodes: None,
}
}
}
impl From<&FullShow> for Show {
fn from(show: &FullShow) -> Self {
Self {
id: show.id.id().to_string(),
uri: show.id.uri(),
name: show.name.clone(),
publisher: show.publisher.clone(),
description: show.description.clone(),
cover_url: show.images.get(0).map(|i| i.url.clone()),
episodes: None,
}
}
}
impl fmt::Display for Show {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} - {}", self.publisher, self.name)
}
}
impl ListItem for Show {
fn is_playing(&self, _queue: Arc<Queue>) -> bool {
false
}
fn display_left(&self) -> String {
format!("{}", self)
}
fn display_right(&self, library: Arc<Library>) -> String {
let saved = if library.is_saved_show(self) {
if library.cfg.values().use_nerdfont.unwrap_or(false) {
"\u{f62b} "
} else {
""
}
} else {
""
};
saved.to_owned()
}
fn play(&mut self, queue: Arc<Queue>) {
self.load_all_episodes(queue.get_spotify());
let playables = self
.episodes
.as_ref()
.unwrap_or(&Vec::new())
.iter()
.map(|ep| Playable::Episode(ep.clone()))
.collect();
let index = queue.append_next(&playables);
queue.play(index, true, true);
}
fn play_next(&mut self, queue: Arc<Queue>) {
self.load_all_episodes(queue.get_spotify());
if let Some(episodes) = self.episodes.as_ref() {
for ep in episodes.iter().rev() {
queue.insert_after_current(Playable::Episode(ep.clone()));
}
}
}
fn queue(&mut self, queue: Arc<Queue>) {
self.load_all_episodes(queue.get_spotify());
for ep in self.episodes.as_ref().unwrap_or(&Vec::new()) {
queue.append(Playable::Episode(ep.clone()));
}
}
fn toggle_saved(&mut self, library: Arc<Library>) {
if library.is_saved_show(self) {
self.unsave(library);
} else {
self.save(library);
}
}
fn save(&mut self, library: Arc<Library>) {
library.save_show(self);
}
fn unsave(&mut self, library: Arc<Library>) {
library.unsave_show(self);
}
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
Some(ShowView::new(queue, library, self).into_boxed_view_ext())
}
fn share_url(&self) -> Option<String> {
Some(format!("https://open.spotify.com/show/{}", self.id))
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
}

306
src/model/track.rs Normal file
View File

@@ -0,0 +1,306 @@
use std::fmt;
use std::sync::{Arc, RwLock};
use chrono::{DateTime, Utc};
use rspotify::model::album::FullAlbum;
use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
use rspotify::model::Id;
use crate::library::Library;
use crate::model::album::Album;
use crate::model::artist::Artist;
use crate::model::playable::Playable;
use crate::queue::Queue;
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
use crate::ui::listview::ListView;
#[derive(Clone, Deserialize, Serialize)]
pub struct Track {
pub id: Option<String>,
pub uri: String,
pub title: String,
pub track_number: u32,
pub disc_number: i32,
pub duration: u32,
pub artists: Vec<String>,
pub artist_ids: Vec<String>,
pub album: Option<String>,
pub album_id: Option<String>,
pub album_artists: Vec<String>,
pub cover_url: Option<String>,
pub url: String,
pub added_at: Option<DateTime<Utc>>,
pub list_index: usize,
}
impl Track {
pub fn from_simplified_track(track: &SimplifiedTrack, album: &FullAlbum) -> Track {
let artists = track
.artists
.iter()
.map(|artist| artist.name.clone())
.collect::<Vec<String>>();
let artist_ids = track
.artists
.iter()
.filter_map(|a| a.id.as_ref().map(|id| id.id().to_string()))
.collect::<Vec<String>>();
let album_artists = album
.artists
.iter()
.map(|artist| artist.name.clone())
.collect::<Vec<String>>();
Self {
id: track.id.as_ref().map(|id| id.id().to_string()),
uri: track.id.as_ref().map(|id| id.uri()).unwrap_or_default(),
title: track.name.clone(),
track_number: track.track_number,
disc_number: track.disc_number,
duration: track.duration.as_millis() as u32,
artists,
artist_ids,
album: Some(album.name.clone()),
album_id: Some(album.id.id().to_string()),
album_artists,
cover_url: album.images.get(0).map(|img| img.url.clone()),
url: track.id.as_ref().map(|id| id.url()).unwrap_or_default(),
added_at: None,
list_index: 0,
}
}
pub fn duration_str(&self) -> String {
let minutes = self.duration / 60_000;
let seconds = (self.duration / 1000) % 60;
format!("{:02}:{:02}", minutes, seconds)
}
}
impl From<&SimplifiedTrack> for Track {
fn from(track: &SimplifiedTrack) -> Self {
let artists = track
.artists
.iter()
.map(|artist| artist.name.clone())
.collect::<Vec<String>>();
let artist_ids = track
.artists
.iter()
.filter_map(|a| a.id.as_ref().map(|a| a.id().to_string()))
.collect::<Vec<String>>();
Self {
id: track.id.as_ref().map(|id| id.id().to_string()),
uri: track.id.as_ref().map(|id| id.uri()).unwrap_or_default(),
title: track.name.clone(),
track_number: track.track_number,
disc_number: track.disc_number,
duration: track.duration.as_millis() as u32,
artists,
artist_ids,
album: None,
album_id: None,
album_artists: Vec::new(),
cover_url: None,
url: track.id.as_ref().map(|id| id.url()).unwrap_or_default(),
added_at: None,
list_index: 0,
}
}
}
impl From<&FullTrack> for Track {
fn from(track: &FullTrack) -> Self {
let artists = track
.artists
.iter()
.map(|artist| artist.name.clone())
.collect::<Vec<String>>();
let artist_ids = track
.artists
.iter()
.filter_map(|a| a.id.as_ref().map(|a| a.id().to_string()))
.collect::<Vec<String>>();
let album_artists = track
.album
.artists
.iter()
.map(|artist| artist.name.clone())
.collect::<Vec<String>>();
Self {
id: Some(track.id.id().to_string()),
uri: track.id.uri(),
title: track.name.clone(),
track_number: track.track_number,
disc_number: track.disc_number,
duration: track.duration.as_millis() as u32,
artists,
artist_ids,
album: Some(track.album.name.clone()),
album_id: track.album.id.as_ref().map(|a| a.id().to_string()),
album_artists,
cover_url: track.album.images.get(0).map(|img| img.url.clone()),
url: track.id.url(),
added_at: None,
list_index: 0,
}
}
}
impl From<&SavedTrack> for Track {
fn from(st: &SavedTrack) -> Self {
let mut track: Self = (&st.track).into();
track.added_at = Some(st.added_at);
track
}
}
impl fmt::Display for Track {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} - {}", self.artists.join(", "), self.title)
}
}
impl fmt::Debug for Track {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"({} - {} ({:?}))",
self.artists.join(", "),
self.title,
self.id
)
}
}
impl ListItem for Track {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
let current = queue.get_current();
current.map(|t| t.id() == self.id).unwrap_or(false)
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
fn display_left(&self) -> String {
format!("{}", self)
}
fn display_center(&self, library: Arc<Library>) -> String {
if library.cfg.values().album_column.unwrap_or(true) {
self.album.clone().unwrap_or_default()
} else {
"".to_string()
}
}
fn display_right(&self, library: Arc<Library>) -> String {
let saved = if library.is_saved_track(&Playable::Track(self.clone())) {
if library.cfg.values().use_nerdfont.unwrap_or(false) {
"\u{f62b} "
} else {
""
}
} else {
""
};
format!("{}{}", saved, self.duration_str())
}
fn play(&mut self, queue: Arc<Queue>) {
let index = queue.append_next(&vec![Playable::Track(self.clone())]);
queue.play(index, true, false);
}
fn play_next(&mut self, queue: Arc<Queue>) {
queue.insert_after_current(Playable::Track(self.clone()));
}
fn queue(&mut self, queue: Arc<Queue>) {
queue.append(Playable::Track(self.clone()));
}
fn save(&mut self, library: Arc<Library>) {
library.save_tracks(vec![self], true);
}
fn unsave(&mut self, library: Arc<Library>) {
library.unsave_tracks(vec![self], true);
}
fn toggle_saved(&mut self, library: Arc<Library>) {
if library.is_saved_track(&Playable::Track(self.clone())) {
library.unsave_tracks(vec![self], true);
} else {
library.save_tracks(vec![self], true);
}
}
fn open(&self, _queue: Arc<Queue>, _library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
None
}
fn open_recommendations(
&mut self,
queue: Arc<Queue>,
library: Arc<Library>,
) -> Option<Box<dyn ViewExt>> {
let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = if let Some(id) = &self.id {
spotify
.api
.recommendations(None, None, Some(vec![id]))
.map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect())
} else {
None
};
recommendations.map(|tracks| {
ListView::new(
Arc::new(RwLock::new(tracks)),
queue.clone(),
library.clone(),
)
.set_title(format!(
"Similar to \"{} - {}\"",
self.artists.join(", "),
self.title
))
.into_boxed_view_ext()
})
}
fn share_url(&self) -> Option<String> {
self.id
.clone()
.map(|id| format!("https://open.spotify.com/track/{}", id))
}
fn album(&self, queue: Arc<Queue>) -> Option<Album> {
let spotify = queue.get_spotify();
match self.album_id {
Some(ref album_id) => spotify.api.album(album_id).map(|ref fa| fa.into()),
None => None,
}
}
fn artists(&self) -> Option<Vec<Artist>> {
Some(
self.artist_ids
.iter()
.zip(self.artists.iter())
.map(|(id, name)| Artist::new(id.clone(), name.clone()))
.collect(),
)
}
fn track(&self) -> Option<Track> {
Some(self.clone())
}
}