Implement saved tracks, albums, and artists

This commit is contained in:
KoffeinFlummi
2019-04-16 19:52:22 +02:00
parent dd69a8c6f2
commit 210c7d9f4e
14 changed files with 688 additions and 391 deletions

View File

@@ -1,7 +1,8 @@
use std::fmt;
use std::sync::Arc;
use rspotify::spotify::model::album::{FullAlbum, SimplifiedAlbum};
use chrono::{DateTime, Utc};
use rspotify::spotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum};
use queue::Queue;
use spotify::Spotify;
@@ -13,14 +14,16 @@ pub struct Album {
pub id: String,
pub title: String,
pub artists: Vec<String>,
pub artist_ids: Vec<String>,
pub year: String,
pub cover_url: Option<String>,
pub url: String,
pub tracks: Option<Vec<Track>>,
pub added_at: Option<DateTime<Utc>>
}
impl Album {
fn load_tracks(&mut self, spotify: Arc<Spotify>) {
pub fn load_tracks(&mut self, spotify: Arc<Spotify>) {
if self.tracks.is_some() {
return;
}
@@ -43,10 +46,12 @@ impl From<&SimplifiedAlbum> for Album {
id: sa.id.clone(),
title: sa.name.clone(),
artists: sa.artists.iter().map(|sa| sa.name.clone()).collect(),
artist_ids: sa.artists.iter().map(|sa| sa.id.clone()).collect(),
year: sa.release_date.split('-').next().unwrap().into(),
cover_url: sa.images.get(0).map(|i| i.url.clone()),
url: sa.uri.clone(),
tracks: None,
added_at: None,
}
}
}
@@ -65,14 +70,24 @@ impl From<&FullAlbum> for Album {
id: fa.id.clone(),
title: fa.name.clone(),
artists: fa.artists.iter().map(|sa| sa.name.clone()).collect(),
artist_ids: fa.artists.iter().map(|sa| sa.id.clone()).collect(),
year: fa.release_date.split('-').next().unwrap().into(),
cover_url: fa.images.get(0).map(|i| i.url.clone()),
url: fa.uri.clone(),
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)

View File

@@ -1,7 +1,7 @@
use std::fmt;
use std::sync::Arc;
use rspotify::spotify::model::artist::FullArtist;
use rspotify::spotify::model::artist::{FullArtist, SimplifiedArtist};
use album::Album;
use queue::Queue;
@@ -15,11 +15,15 @@ pub struct Artist {
pub name: String,
pub url: String,
pub albums: Option<Vec<Album>>,
pub tracks: Option<Vec<Track>>,
}
impl Artist {
fn load_albums(&mut self, spotify: Arc<Spotify>) {
if self.albums.is_some() {
if let Some(albums) = self.albums.as_mut() {
for album in albums {
album.load_tracks(spotify.clone());
}
return;
}
@@ -41,7 +45,9 @@ impl Artist {
}
fn tracks(&self) -> Option<Vec<&Track>> {
if let Some(albums) = self.albums.as_ref() {
if let Some(tracks) = self.tracks.as_ref() {
Some(tracks.iter().collect())
} else if let Some(albums) = self.albums.as_ref() {
Some(
albums
.iter()
@@ -55,6 +61,18 @@ impl Artist {
}
}
impl From<&SimplifiedArtist> for Artist {
fn from(sa: &SimplifiedArtist) -> Self {
Self {
id: sa.id.clone(),
name: sa.name.clone(),
url: sa.uri.clone(),
albums: None,
tracks: None,
}
}
}
impl From<&FullArtist> for Artist {
fn from(fa: &FullArtist) -> Self {
Self {
@@ -62,6 +80,7 @@ impl From<&FullArtist> for Artist {
name: fa.name.clone(),
url: fa.uri.clone(),
albums: None,
tracks: None,
}
}
}
@@ -100,7 +119,12 @@ impl ListItem for Artist {
}
fn display_right(&self) -> String {
"".into()
// TODO: indicate following status
if let Some(tracks) = self.tracks.as_ref() {
format!("{} saved tracks", tracks.len())
} else {
"".into()
}
}
fn play(&mut self, queue: Arc<Queue>) {

View File

@@ -5,7 +5,7 @@ use cursive::event::{Event, Key};
use cursive::views::ViewRef;
use cursive::Cursive;
use playlists::Playlists;
use library::Library;
use queue::{Queue, RepeatSetting};
use spotify::Spotify;
use traits::ViewExt;
@@ -47,7 +47,7 @@ impl CommandManager {
&mut self,
spotify: Arc<Spotify>,
queue: Arc<Queue>,
playlists: Arc<Playlists>,
library: Arc<Library>,
) {
self.register_aliases("quit", vec!["q", "x"]);
self.register_aliases("playpause", vec!["pause", "toggleplay", "toggleplayback"]);
@@ -113,14 +113,13 @@ impl CommandManager {
}
{
let playlists = playlists.clone();
let library = library.clone();
self.register_command(
"playlists",
Some(Box::new(move |_s, args| {
if let Some(arg) = args.get(0) {
if arg == "update" {
playlists.fetch_playlists();
playlists.save_cache();
library.update_playlists();
}
}
Ok(None)
@@ -303,7 +302,7 @@ impl CommandManager {
kb.insert("F1".into(), "focus queue".into());
kb.insert("F2".into(), "focus search".into());
kb.insert("F3".into(), "focus playlists".into());
kb.insert("F3".into(), "focus library".into());
kb.insert("Up".into(), "move up".into());
kb.insert("Down".into(), "move down".into());

445
src/library.rs Normal file
View File

@@ -0,0 +1,445 @@
use std::collections::HashMap;
use std::iter::Iterator;
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::thread;
use rspotify::spotify::model::artist::SimplifiedArtist;
use rspotify::spotify::model::playlist::{FullPlaylist, SimplifiedPlaylist};
use serde::de::DeserializeOwned;
use serde::Serialize;
use album::Album;
use artist::Artist;
use config;
use events::EventManager;
use playlist::Playlist;
use spotify::Spotify;
use track::Track;
const CACHE_TRACKS: &str = "tracks.db";
const CACHE_ALBUMS: &str = "albums.db";
const CACHE_ARTISTS: &str = "artists.db";
const CACHE_PLAYLISTS: &str = "playlists.db";
#[derive(Clone)]
pub struct Library {
pub tracks: Arc<RwLock<Vec<Track>>>,
pub albums: Arc<RwLock<Vec<Album>>>,
pub artists: Arc<RwLock<Vec<Artist>>>,
pub playlists: Arc<RwLock<Vec<Playlist>>>,
ev: EventManager,
spotify: Arc<Spotify>,
}
impl Library {
pub fn new(ev: &EventManager, spotify: Arc<Spotify>) -> Self {
let library = Self {
tracks: Arc::new(RwLock::new(Vec::new())),
albums: Arc::new(RwLock::new(Vec::new())),
artists: Arc::new(RwLock::new(Vec::new())),
playlists: Arc::new(RwLock::new(Vec::new())),
ev: ev.clone(),
spotify,
};
{
// download playlists via web api in a background thread
let library = library.clone();
thread::spawn(move || {
// load cache (if existing)
library.load_caches();
library.fetch_artists();
library.fetch_tracks();
library.fetch_albums();
library.fetch_playlists();
library.populate_artists();
// re-cache for next startup
library.save_caches();
});
}
library
}
pub fn items(&self) -> RwLockReadGuard<Vec<Playlist>> {
self.playlists
.read()
.expect("could not readlock listview content")
}
fn load_cache<T: DeserializeOwned>(&self, cache_path: PathBuf, store: Arc<RwLock<Vec<T>>>) {
if let Ok(contents) = std::fs::read_to_string(&cache_path) {
debug!("loading cache from {}", cache_path.display());
let parsed: Result<Vec<T>, _> = serde_json::from_str(&contents);
match parsed {
Ok(cache) => {
debug!("cache from {} loaded ({} lists)", cache_path.display(), cache.len());
let mut store = store.write().expect("can't writelock store");
store.clear();
store.extend(cache);
// force refresh of UI (if visible)
self.ev.trigger();
}
Err(e) => {
error!("can't parse cache: {}", e);
}
}
}
}
fn load_caches(&self) {
self.load_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone());
self.load_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone());
self.load_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone());
self.load_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone());
}
fn save_cache<T: Serialize>(&self, cache_path: PathBuf, store: Arc<RwLock<Vec<T>>>) {
match serde_json::to_string(&store.deref()) {
Ok(contents) => std::fs::write(cache_path, contents).unwrap(),
Err(e) => error!("could not write cache: {:?}", e),
}
}
fn save_caches(&self) {
self.save_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone());
self.save_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone());
self.save_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone());
self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone());
}
pub fn process_simplified_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist {
Self::_process_playlist(
list.id.clone(),
list.name.clone(),
list.snapshot_id.clone(),
spotify,
)
}
pub fn process_full_playlist(list: &FullPlaylist, spotify: &Spotify) -> Playlist {
Self::_process_playlist(
list.id.clone(),
list.name.clone(),
list.snapshot_id.clone(),
spotify,
)
}
fn _process_playlist(
id: String,
name: String,
snapshot_id: String,
spotify: &Spotify,
) -> Playlist {
let mut collected_tracks = Vec::new();
let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0);
while let Some(ref tracks) = tracks_result.clone() {
for listtrack in &tracks.items {
collected_tracks.push((&listtrack.track).into());
}
debug!("got {} tracks", tracks.items.len());
// load next batch if necessary
tracks_result = match tracks.next {
Some(_) => {
debug!("requesting tracks again..");
spotify.user_playlist_tracks(
&id,
100,
tracks.offset + tracks.items.len() as u32,
)
}
None => None,
}
}
Playlist {
id: id.clone(),
name: name.clone(),
snapshot_id: snapshot_id.clone(),
tracks: collected_tracks,
}
}
fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool {
for local in self.playlists.read().expect("can't readlock playlists").iter() {
if local.id == remote.id {
return local.snapshot_id != remote.snapshot_id;
}
}
true
}
fn append_or_update(&self, updated: &Playlist) -> usize {
let mut store = self.playlists.write().expect("can't writelock playlists");
for (index, mut local) in store.iter_mut().enumerate() {
if local.id == updated.id {
*local = updated.clone();
return index;
}
}
store.push(updated.clone());
store.len() - 1
}
pub fn delete_playlist(&self, id: &str) {
let mut store = self.playlists.write().expect("can't writelock playlists");
if let Some(position) = store.iter().position(|ref i| i.id == id) {
if self.spotify.delete_playlist(id) {
store.remove(position);
self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone());
}
}
}
pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) {
debug!("saving {} tracks to {}", tracks.len(), id);
self.spotify.overwrite_playlist(id, &tracks);
self.update_playlists();
}
pub fn save_playlist(&self, name: &str, tracks: &[Track]) {
debug!("saving {} tracks to new list {}", tracks.len(), name);
match self.spotify.create_playlist(name, None, None) {
Some(id) => self.overwrite_playlist(&id, &tracks),
None => error!("could not create new playlist.."),
}
}
pub fn update_playlists(&self) {
self.fetch_playlists();
self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone());
}
fn fetch_playlists(&self) {
debug!("loading playlists");
let mut stale_lists = self.playlists.read().unwrap().clone();
let mut lists_result = self.spotify.current_user_playlist(50, 0);
while let Some(ref lists) = lists_result.clone() {
for remote in &lists.items {
// remove from stale playlists so we won't prune it later on
if let Some(index) = stale_lists.iter().position(|x| x.id == remote.id) {
stale_lists.remove(index);
}
if self.needs_download(remote) {
info!("updating playlist {}", remote.name);
let playlist = Self::process_simplified_playlist(remote, &self.spotify);
self.append_or_update(&playlist);
// trigger redraw
self.ev.trigger();
}
}
// load next batch if necessary
lists_result = match lists.next {
Some(_) => {
debug!("requesting playlists again..");
self.spotify
.current_user_playlist(50, lists.offset + lists.items.len() as u32)
}
None => None,
}
}
// remove stale playlists
for stale in stale_lists {
let index = self
.playlists
.read()
.unwrap()
.iter()
.position(|x| x.id == stale.id);
if let Some(index) = index {
debug!("removing stale list: {:?}", stale.name);
self.playlists.write().unwrap().remove(index);
}
}
// trigger redraw
self.ev.trigger();
}
fn fetch_artists(&self) {
let mut artists: Vec<Artist> = Vec::new();
let mut last: Option<String> = None;
let mut i: u32 = 0;
loop {
let page = self.spotify.current_user_followed_artists(last);
debug!("artists page: {}", i);
i += 1;
if page.is_none() {
error!("Failed to fetch artists.");
return;
}
let page = page.unwrap();
artists.extend(page.items.iter().map(|fa| fa.into()));
if page.next.is_some() {
last = Some(artists.last().unwrap().id.clone());
} else {
break;
}
}
for artist in artists.iter_mut() {
// Only play saved tracks
artist.albums = Some(Vec::new());
artist.tracks = Some(Vec::new());
}
*(self.artists.write().unwrap()) = artists;
}
fn insert_artist(&self, artist: &SimplifiedArtist) {
let mut artists = self.artists.write().unwrap();
if artists.iter().any(|a| a.id == artist.id) {
return;
}
artists.push(artist.into());
}
fn fetch_albums(&self) {
let mut albums: Vec<Album> = Vec::new();
let mut i: u32 = 0;
loop {
let page = self.spotify.current_user_saved_albums(albums.len() as u32);
debug!("albums page: {}", i);
i += 1;
if page.is_none() {
error!("Failed to fetch albums.");
return;
}
let page = page.unwrap();
albums.extend(page.items.iter().map(|a| a.into()));
if page.next.is_none() {
break;
}
}
*(self.albums.write().unwrap()) = albums;
}
fn fetch_tracks(&self) {
let mut tracks: Vec<Track> = Vec::new();
let mut i: u32 = 0;
loop {
let page = self.spotify.current_user_saved_tracks(tracks.len() as u32);
debug!("tracks page: {}", i);
i += 1;
if page.is_none() {
error!("Failed to fetch tracks.");
return;
}
let page = page.unwrap();
if page.offset == 0 {
// If first page matches the first items in store and total is
// identical, assume list is unchanged.
let store = self.tracks.read().unwrap();
if page.total as usize == store.len() &&
!page.items
.iter()
.enumerate()
.any(|(i, t)| &t.track.id != &store[i].id)
{
return;
}
}
for track in page.items.iter() {
for artist in track.track.artists.iter() {
self.insert_artist(artist);
}
tracks.push(track.into());
}
if page.next.is_none() {
break;
}
}
*(self.tracks.write().unwrap()) = tracks;
}
fn populate_artists(&self) {
let mut artists = self.artists.write().unwrap();
let mut lookup: HashMap<String, Option<usize>> = HashMap::new();
artists.sort_unstable_by(|a, b| a.name.partial_cmp(&b.name).unwrap());
{
let albums = self.albums.read().unwrap();
for album in albums.iter() {
for artist_id in &album.artist_ids {
let index = if let Some(i) = lookup.get(artist_id).cloned() {
i
} else {
let i = artists.iter().position(|a| &a.id == artist_id);
lookup.insert(artist_id.clone(), i);
i
};
if let Some(i) = index {
let mut artist = artists.get_mut(i).unwrap();
if artist.albums.is_none() {
artist.albums = Some(Vec::new());
}
if let Some(albums) = artist.albums.as_mut() {
albums.push(album.clone());
}
}
}
}
}
{
let tracks = self.tracks.read().unwrap();
for track in tracks.iter() {
for artist_id in &track.artist_ids {
let index = if let Some(i) = lookup.get(artist_id).cloned() {
i
} else {
let i = artists.iter().position(|a| &a.id == artist_id);
lookup.insert(artist_id.clone(), i);
i
};
if let Some(i) = index {
let mut artist = artists.get_mut(i).unwrap();
if artist.tracks.is_none() {
artist.tracks = Some(Vec::new());
}
if let Some(tracks) = artist.tracks.as_mut() {
tracks.push(track.clone());
}
}
}
}
}
}
}

View File

@@ -31,7 +31,6 @@ extern crate rand;
use std::fs;
use std::process;
use std::sync::Arc;
use std::thread;
use clap::{App, Arg};
use cursive::traits::Identifiable;
@@ -45,7 +44,8 @@ mod authentication;
mod commands;
mod config;
mod events;
mod playlists;
mod library;
mod playlist;
mod queue;
mod spotify;
mod theme;
@@ -58,7 +58,7 @@ mod mpris;
use commands::CommandManager;
use events::{Event, EventManager};
use playlists::Playlists;
use library::Library;
use spotify::PlayerEvent;
fn setup_logging(filename: &str) -> Result<(), fern::InitError> {
@@ -156,25 +156,10 @@ fn main() {
#[cfg(feature = "mpris")]
let mpris_manager = Arc::new(mpris::MprisManager::new(spotify.clone(), queue.clone()));
let playlists = Arc::new(Playlists::new(&event_manager, &spotify));
{
// download playlists via web api in a background thread
let playlists = playlists.clone();
thread::spawn(move || {
// load cache (if existing)
playlists.load_cache();
// fetch or update cached playlists
playlists.fetch_playlists();
// re-cache for next startup
playlists.save_cache();
});
}
let library = Arc::new(Library::new(&event_manager, spotify.clone()));
let mut cmd_manager = CommandManager::new();
cmd_manager.register_all(spotify.clone(), queue.clone(), playlists.clone());
cmd_manager.register_all(spotify.clone(), queue.clone(), library.clone());
let cmd_manager = Arc::new(cmd_manager);
CommandManager::register_keybindings(
@@ -185,9 +170,9 @@ fn main() {
let search = ui::search::SearchView::new(event_manager.clone(), spotify.clone(), queue.clone());
let playlistsview = ui::playlists::PlaylistView::new(&playlists, queue.clone());
let libraryview = ui::library::LibraryView::new(queue.clone(), library.clone());
let queueview = ui::queue::QueueView::new(queue.clone(), playlists.clone());
let queueview = ui::queue::QueueView::new(queue.clone(), library.clone());
let status = ui::statusbar::StatusBar::new(
queue.clone(),
@@ -197,7 +182,7 @@ fn main() {
let mut layout = ui::layout::Layout::new(status, &event_manager, theme)
.view("search", search.with_id("search"), "Search")
.view("playlists", playlistsview.with_id("playlists"), "Playlists")
.view("library", libraryview.with_id("library"), "Library")
.view("queue", queueview, "Queue");
// initial view is queue

47
src/playlist.rs Normal file
View File

@@ -0,0 +1,47 @@
use std::iter::Iterator;
use std::sync::Arc;
use queue::Queue;
use track::Track;
use traits::ListItem;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Playlist {
pub id: String,
pub name: String,
pub snapshot_id: String,
pub tracks: Vec<Track>,
}
impl ListItem for Playlist {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
let playing: Vec<String> = queue
.queue
.read()
.unwrap()
.iter()
.map(|t| t.id.clone())
.collect();
let ids: Vec<String> = self.tracks.iter().map(|t| t.id.clone()).collect();
!ids.is_empty() && playing == ids
}
fn display_left(&self) -> String {
self.name.clone()
}
fn display_right(&self) -> String {
format!("{} tracks", self.tracks.len())
}
fn play(&mut self, queue: Arc<Queue>) {
let index = queue.append_next(self.tracks.iter().collect());
queue.play(index, true);
}
fn queue(&mut self, queue: Arc<Queue>) {
for track in self.tracks.iter() {
queue.append(track);
}
}
}

View File

@@ -1,262 +0,0 @@
use std::iter::Iterator;
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::{Arc, RwLock, RwLockReadGuard};
use rspotify::spotify::model::playlist::{FullPlaylist, SimplifiedPlaylist};
use config;
use events::EventManager;
use queue::Queue;
use spotify::Spotify;
use track::Track;
use traits::ListItem;
const CACHE_FILE: &str = "playlists.db";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Playlist {
pub id: String,
pub name: String,
pub snapshot_id: String,
pub tracks: Vec<Track>,
}
#[derive(Clone)]
pub struct Playlists {
pub store: Arc<RwLock<Vec<Playlist>>>,
ev: EventManager,
spotify: Arc<Spotify>,
cache_path: PathBuf,
}
impl ListItem for Playlist {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
let playing: Vec<String> = queue
.queue
.read()
.unwrap()
.iter()
.map(|t| t.id.clone())
.collect();
let ids: Vec<String> = self.tracks.iter().map(|t| t.id.clone()).collect();
!ids.is_empty() && playing == ids
}
fn display_left(&self) -> String {
self.name.clone()
}
fn display_right(&self) -> String {
format!("{} tracks", self.tracks.len())
}
fn play(&mut self, queue: Arc<Queue>) {
let index = queue.append_next(self.tracks.iter().collect());
queue.play(index, true);
}
fn queue(&mut self, queue: Arc<Queue>) {
for track in self.tracks.iter() {
queue.append(track);
}
}
}
impl Playlists {
pub fn new(ev: &EventManager, spotify: &Arc<Spotify>) -> Playlists {
Playlists {
store: Arc::new(RwLock::new(Vec::new())),
ev: ev.clone(),
spotify: spotify.clone(),
cache_path: config::cache_path(CACHE_FILE),
}
}
pub fn items(&self) -> RwLockReadGuard<Vec<Playlist>> {
self.store
.read()
.expect("could not readlock listview content")
}
pub fn load_cache(&self) {
if let Ok(contents) = std::fs::read_to_string(&self.cache_path) {
debug!(
"loading playlist cache from {}",
self.cache_path.to_str().unwrap()
);
let parsed: Result<Vec<Playlist>, _> = serde_json::from_str(&contents);
match parsed {
Ok(cache) => {
debug!("playlist cache loaded ({} lists)", cache.len());
let mut store = self.store.write().expect("can't writelock playlist store");
store.clear();
store.extend(cache);
// force refresh of UI (if visible)
self.ev.trigger();
}
Err(e) => {
error!("can't parse playlist cache: {}", e);
}
}
}
}
pub fn save_cache(&self) {
match serde_json::to_string(&self.store.deref()) {
Ok(contents) => std::fs::write(&self.cache_path, contents).unwrap(),
Err(e) => error!("could not write playlist cache: {:?}", e),
}
}
pub fn process_simplified_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist {
Playlists::_process_playlist(
list.id.clone(),
list.name.clone(),
list.snapshot_id.clone(),
spotify,
)
}
pub fn process_full_playlist(list: &FullPlaylist, spotify: &Spotify) -> Playlist {
Playlists::_process_playlist(
list.id.clone(),
list.name.clone(),
list.snapshot_id.clone(),
spotify,
)
}
pub fn _process_playlist(
id: String,
name: String,
snapshot_id: String,
spotify: &Spotify,
) -> Playlist {
let mut collected_tracks = Vec::new();
let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0);
while let Some(ref tracks) = tracks_result.clone() {
for listtrack in &tracks.items {
collected_tracks.push((&listtrack.track).into());
}
debug!("got {} tracks", tracks.items.len());
// load next batch if necessary
tracks_result = match tracks.next {
Some(_) => {
debug!("requesting tracks again..");
spotify.user_playlist_tracks(
&id,
100,
tracks.offset + tracks.items.len() as u32,
)
}
None => None,
}
}
Playlist {
id: id.clone(),
name: name.clone(),
snapshot_id: snapshot_id.clone(),
tracks: collected_tracks,
}
}
fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool {
for local in self.store.read().expect("can't readlock playlists").iter() {
if local.id == remote.id {
return local.snapshot_id != remote.snapshot_id;
}
}
true
}
fn append_or_update(&self, updated: &Playlist) -> usize {
let mut store = self.store.write().expect("can't writelock playlists");
for (index, mut local) in store.iter_mut().enumerate() {
if local.id == updated.id {
*local = updated.clone();
return index;
}
}
store.push(updated.clone());
store.len() - 1
}
pub fn delete_playlist(&self, id: &str) {
let mut store = self.store.write().expect("can't writelock playlists");
if let Some(position) = store.iter().position(|ref i| i.id == id) {
if self.spotify.delete_playlist(id) {
store.remove(position);
self.save_cache();
}
}
}
pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) {
debug!("saving {} tracks to {}", tracks.len(), id);
self.spotify.overwrite_playlist(id, &tracks);
self.fetch_playlists();
self.save_cache();
}
pub fn save_playlist(&self, name: &str, tracks: &[Track]) {
debug!("saving {} tracks to new list {}", tracks.len(), name);
match self.spotify.create_playlist(name, None, None) {
Some(id) => self.overwrite_playlist(&id, &tracks),
None => error!("could not create new playlist.."),
}
}
pub fn fetch_playlists(&self) {
debug!("loading playlists");
let mut stale_lists = self.store.read().unwrap().clone();
let mut lists_result = self.spotify.current_user_playlist(50, 0);
while let Some(ref lists) = lists_result.clone() {
for remote in &lists.items {
// remove from stale playlists so we won't prune it later on
if let Some(index) = stale_lists.iter().position(|x| x.id == remote.id) {
stale_lists.remove(index);
}
if self.needs_download(remote) {
info!("updating playlist {}", remote.name);
let playlist = Self::process_simplified_playlist(remote, &self.spotify);
self.append_or_update(&playlist);
// trigger redraw
self.ev.trigger();
}
}
// load next batch if necessary
lists_result = match lists.next {
Some(_) => {
debug!("requesting playlists again..");
self.spotify
.current_user_playlist(50, lists.offset + lists.items.len() as u32)
}
None => None,
}
}
// remove stale playlists
for stale in stale_lists {
let index = self
.store
.read()
.unwrap()
.iter()
.position(|x| x.id == stale.id);
if let Some(index) = index {
debug!("removing stale list: {:?}", stale.name);
self.store.write().unwrap().remove(index);
}
}
// trigger redraw
self.ev.trigger();
}
}

View File

@@ -13,14 +13,14 @@ use librespot::playback::player::Player;
use rspotify::spotify::client::ApiError;
use rspotify::spotify::client::Spotify as SpotifyAPI;
use rspotify::spotify::model::album::{FullAlbum, SimplifiedAlbum};
use rspotify::spotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum};
use rspotify::spotify::model::artist::FullArtist;
use rspotify::spotify::model::page::Page;
use rspotify::spotify::model::page::{CursorBasedPage, Page};
use rspotify::spotify::model::playlist::{FullPlaylist, PlaylistTrack, SimplifiedPlaylist};
use rspotify::spotify::model::search::{
SearchAlbums, SearchArtists, SearchPlaylists, SearchTracks,
};
use rspotify::spotify::model::track::FullTrack;
use rspotify::spotify::model::track::{FullTrack, SavedTrack};
use failure::Error;
@@ -527,6 +527,19 @@ impl Spotify {
})
}
pub fn current_user_followed_artists(&self, last: Option<String>) -> Option<CursorBasedPage<FullArtist>> {
self.api_with_retry(|api| api.current_user_followed_artists(50, last.clone()))
.map(|cp| cp.artists)
}
pub fn current_user_saved_albums(&self, offset: u32) -> Option<Page<SavedAlbum>> {
self.api_with_retry(|api| api.current_user_saved_albums(50, offset))
}
pub fn current_user_saved_tracks(&self, offset: u32) -> Option<Page<SavedTrack>> {
self.api_with_retry(|api| api.current_user_saved_tracks(50, offset))
}
pub fn load(&self, track: &Track) {
info!("loading track: {:?}", track);
self.channel

View File

@@ -1,8 +1,9 @@
use std::fmt;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use rspotify::spotify::model::album::FullAlbum;
use rspotify::spotify::model::track::{FullTrack, SimplifiedTrack};
use rspotify::spotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
use queue::Queue;
use traits::ListItem;
@@ -15,10 +16,12 @@ pub struct Track {
pub disc_number: i32,
pub duration: u32,
pub artists: Vec<String>,
pub artist_ids: Vec<String>,
pub album: String,
pub album_artists: Vec<String>,
pub cover_url: String,
pub url: String,
pub added_at: Option<DateTime<Utc>>
}
impl Track {
@@ -28,6 +31,11 @@ impl Track {
.iter()
.map(|ref artist| artist.name.clone())
.collect::<Vec<String>>();
let artist_ids = track
.artists
.iter()
.map(|ref artist| artist.id.clone())
.collect::<Vec<String>>();
let album_artists = album
.artists
.iter()
@@ -46,10 +54,12 @@ impl Track {
disc_number: track.disc_number,
duration: track.duration_ms,
artists,
artist_ids,
album: album.name.clone(),
album_artists,
cover_url,
url: track.uri.clone(),
added_at: None,
}
}
@@ -67,6 +77,11 @@ impl From<&FullTrack> for Track {
.iter()
.map(|ref artist| artist.name.clone())
.collect::<Vec<String>>();
let artist_ids = track
.artists
.iter()
.map(|ref artist| artist.id.clone())
.collect::<Vec<String>>();
let album_artists = track
.album
.artists
@@ -86,14 +101,24 @@ impl From<&FullTrack> for Track {
disc_number: track.disc_number,
duration: track.duration_ms,
artists,
artist_ids,
album: track.album.name.clone(),
album_artists,
cover_url,
url: track.uri.clone(),
added_at: None,
}
}
}
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)

79
src/ui/library.rs Normal file
View File

@@ -0,0 +1,79 @@
use std::sync::Arc;
use cursive::view::ViewWrapper;
use cursive::views::Dialog;
use cursive::Cursive;
use commands::CommandResult;
use library::Library;
use queue::Queue;
use traits::ViewExt;
use ui::listview::ListView;
use ui::modal::Modal;
use ui::tabview::TabView;
pub struct LibraryView {
list: TabView,
library: Arc<Library>,
}
impl LibraryView {
pub fn new(queue: Arc<Queue>, library: Arc<Library>) -> Self {
let tabs = TabView::new()
.tab("tracks", "Tracks", ListView::new(library.tracks.clone(), queue.clone()))
.tab("albums", "Albums", ListView::new(library.albums.clone(), queue.clone()))
.tab("artists", "Artists", ListView::new(library.artists.clone(), queue.clone()))
.tab("playlists", "Playlists", ListView::new(library.playlists.clone(), queue.clone()));
Self {
list: tabs,
library,
}
}
pub fn delete_dialog(&mut self) -> Option<Modal<Dialog>> {
return None;
// TODO
//let store = self.library.items();
//let current = store.get(self.list.get_selected_index());
//if let Some(playlist) = current {
// let library = self.library.clone();
// let id = playlist.id.clone();
// let dialog = Dialog::text("Are you sure you want to delete this playlist?")
// .padding((1, 1, 1, 0))
// .title("Delete playlist")
// .dismiss_button("No")
// .button("Yes", move |s: &mut Cursive| {
// library.delete_playlist(&id);
// s.pop_layer();
// });
// Some(Modal::new(dialog))
//} else {
// None
//}
}
}
impl ViewWrapper for LibraryView {
wrap_impl!(self.list: TabView);
}
impl ViewExt for LibraryView {
fn on_command(
&mut self,
s: &mut Cursive,
cmd: &str,
args: &[String],
) -> Result<CommandResult, String> {
if cmd == "delete" {
if let Some(dialog) = self.delete_dialog() {
s.add_layer(dialog);
}
return Ok(CommandResult::Consumed(None));
}
self.list.on_command(s, cmd, args)
}
}

View File

@@ -1,7 +1,7 @@
pub mod layout;
pub mod library;
pub mod listview;
pub mod modal;
pub mod playlists;
pub mod queue;
pub mod search;
pub mod statusbar;

View File

@@ -1,74 +0,0 @@
use std::sync::Arc;
use cursive::traits::Identifiable;
use cursive::view::ViewWrapper;
use cursive::views::{Dialog, IdView};
use cursive::Cursive;
use commands::CommandResult;
use playlists::{Playlist, Playlists};
use queue::Queue;
use traits::ViewExt;
use ui::listview::ListView;
use ui::modal::Modal;
pub struct PlaylistView {
list: IdView<ListView<Playlist>>,
playlists: Playlists,
}
pub const LIST_ID: &str = "playlist_list";
impl PlaylistView {
pub fn new(playlists: &Playlists, queue: Arc<Queue>) -> PlaylistView {
let list = ListView::new(playlists.store.clone(), queue).with_id(LIST_ID);
PlaylistView {
list,
playlists: playlists.clone(),
}
}
pub fn delete_dialog(&mut self) -> Option<Modal<Dialog>> {
let list = self.list.get_mut();
let store = self.playlists.items();
let current = store.get(list.get_selected_index());
if let Some(playlist) = current {
let playlists = self.playlists.clone();
let id = playlist.id.clone();
let dialog = Dialog::text("Are you sure you want to delete this playlist?")
.padding((1, 1, 1, 0))
.title("Delete playlist")
.dismiss_button("No")
.button("Yes", move |s: &mut Cursive| {
playlists.delete_playlist(&id);
s.pop_layer();
});
Some(Modal::new(dialog))
} else {
None
}
}
}
impl ViewWrapper for PlaylistView {
wrap_impl!(self.list: IdView<ListView<Playlist>>);
}
impl ViewExt for PlaylistView {
fn on_command(
&mut self,
s: &mut Cursive,
cmd: &str,
args: &[String],
) -> Result<CommandResult, String> {
if cmd == "delete" {
if let Some(dialog) = self.delete_dialog() {
s.add_layer(dialog);
}
return Ok(CommandResult::Consumed(None));
}
self.list.on_command(s, cmd, args)
}
}

View File

@@ -8,7 +8,7 @@ use std::cmp::min;
use std::sync::Arc;
use commands::CommandResult;
use playlists::Playlists;
use library::Library;
use queue::Queue;
use track::Track;
use traits::ViewExt;
@@ -17,17 +17,17 @@ use ui::modal::Modal;
pub struct QueueView {
list: ListView<Track>,
playlists: Arc<Playlists>,
library: Arc<Library>,
queue: Arc<Queue>,
}
impl QueueView {
pub fn new(queue: Arc<Queue>, playlists: Arc<Playlists>) -> QueueView {
pub fn new(queue: Arc<Queue>, library: Arc<Library>) -> QueueView {
let list = ListView::new(queue.queue.clone(), queue.clone());
QueueView {
list,
playlists,
library,
queue,
}
}
@@ -35,20 +35,20 @@ impl QueueView {
fn save_dialog_cb(
s: &mut Cursive,
queue: Arc<Queue>,
playlists: Arc<Playlists>,
library: Arc<Library>,
id: Option<String>,
) {
let tracks = queue.queue.read().unwrap().clone();
match id {
Some(id) => {
playlists.overwrite_playlist(&id, &tracks);
library.overwrite_playlist(&id, &tracks);
s.pop_layer();
}
None => {
s.pop_layer();
let edit = EditView::new()
.on_submit(move |s: &mut Cursive, name| {
playlists.save_playlist(name, &tracks);
library.save_playlist(name, &tracks);
s.pop_layer();
})
.with_id("name")
@@ -63,16 +63,16 @@ impl QueueView {
}
}
fn save_dialog(queue: Arc<Queue>, playlists: Arc<Playlists>) -> Modal<Dialog> {
fn save_dialog(queue: Arc<Queue>, library: Arc<Library>) -> Modal<Dialog> {
let mut list_select: SelectView<Option<String>> = SelectView::new().autojump();
list_select.add_item("[Create new]", None);
for list in playlists.items().iter() {
for list in library.items().iter() {
list_select.add_item(list.name.clone(), Some(list.id.clone()));
}
list_select.set_on_submit(move |s, selected| {
Self::save_dialog_cb(s, queue.clone(), playlists.clone(), selected.clone())
Self::save_dialog_cb(s, queue.clone(), library.clone(), selected.clone())
});
let dialog = Dialog::new()
@@ -92,9 +92,9 @@ impl ViewWrapper for QueueView {
Event::Char('s') => {
debug!("save list");
let queue = self.queue.clone();
let playlists = self.playlists.clone();
let library = self.library.clone();
let cb = move |s: &mut Cursive| {
let dialog = Self::save_dialog(queue.clone(), playlists.clone());
let dialog = Self::save_dialog(queue.clone(), library.clone());
s.add_layer(dialog)
};
EventResult::Consumed(Some(Callback::from_fn(cb)))

View File

@@ -13,7 +13,8 @@ use album::Album;
use artist::Artist;
use commands::CommandResult;
use events::EventManager;
use playlists::{Playlist, Playlists};
use library::Library;
use playlist::Playlist;
use queue::Queue;
use spotify::{Spotify, URIType};
use track::Track;
@@ -218,7 +219,7 @@ impl SearchView {
_append: bool,
) -> u32 {
if let Some(results) = spotify.playlist(&query) {
let pls = vec![Playlists::process_full_playlist(&results, &&spotify)];
let pls = vec![Library::process_full_playlist(&results, &&spotify)];
let mut r = playlists.write().unwrap();
*r = pls;
return 1;
@@ -238,7 +239,7 @@ impl SearchView {
.playlists
.items
.iter()
.map(|sp| Playlists::process_simplified_playlist(sp, &&spotify))
.map(|sp| Library::process_simplified_playlist(sp, &&spotify))
.collect();
let mut r = playlists.write().unwrap();