use crate::network::{IoEvent, Network};
use crate::user_config::UserConfig;
use super::util::{Flag, Format, FormatType, JumpDirection, Type};
use anyhow::{anyhow, Result};
use rand::{thread_rng, Rng};
use rspotify::model::{
context::CurrentPlaybackContext,
idtypes::{Id, PlayContextId, PlayableId},
PlayableItem,
};
use rspotify::prelude::*;
pub struct CliApp {
pub net: Network,
pub config: UserConfig,
}
impl CliApp {
pub fn new(net: Network, config: UserConfig) -> Self {
Self { net, config }
}
async fn is_a_saved_track(&mut self, id: &str) -> bool {
if let Ok(track_id) = rspotify::model::idtypes::TrackId::from_id(id) {
self
.net
.handle_network_event(IoEvent::CurrentUserSavedTracksContains(vec![
track_id.into_static()
]))
.await;
self.net.app.lock().await.liked_song_ids_set.contains(id)
} else {
false
}
}
pub fn format_output(&self, mut format: String, values: Vec<Format>) -> String {
for val in values {
format = format.replace(val.get_placeholder(), &val.inner(self.config.clone()));
}
for p in &["%a", "%b", "%t", "%p", "%h", "%u", "%d", "%v", "%f", "%s"] {
format = format.replace(p, "None");
}
format.trim().to_string()
}
pub async fn toggle_playback(&mut self) {
let context = self.net.app.lock().await.current_playback_context.clone();
if let Some(c) = context {
if c.is_playing {
self.net.handle_network_event(IoEvent::PausePlayback).await;
return;
}
}
self
.net
.handle_network_event(IoEvent::StartPlayback(None, None, None))
.await;
}
pub async fn share_track_or_episode(&mut self) -> Result<String> {
let app = self.net.app.lock().await;
if let Some(CurrentPlaybackContext {
item: Some(item), ..
}) = &app.current_playback_context
{
match item {
PlayableItem::Track(track) => {
if let Some(id) = &track.id {
Ok(format!("https://open.spotify.com/track/{}", id.id()))
} else {
Err(anyhow!("track has no ID"))
}
}
PlayableItem::Episode(episode) => Ok(format!(
"https://open.spotify.com/episode/{}",
episode.id.id()
)),
}
} else {
Err(anyhow!(
"failed to generate a shareable url for the current song"
))
}
}
pub async fn share_album_or_show(&mut self) -> Result<String> {
let app = self.net.app.lock().await;
if let Some(CurrentPlaybackContext {
item: Some(item), ..
}) = &app.current_playback_context
{
match item {
PlayableItem::Track(track) => {
if let Some(id) = &track.album.id {
Ok(format!("https://open.spotify.com/album/{}", id.id()))
} else {
Err(anyhow!("album has no ID"))
}
}
PlayableItem::Episode(episode) => Ok(format!(
"https://open.spotify.com/show/{}",
episode.show.id.id()
)),
}
} else {
Err(anyhow!(
"failed to generate a shareable url for the current song"
))
}
}
pub async fn set_device(&mut self, name: String) -> Result<()> {
let mut app = self.net.app.lock().await;
let mut device_index = 0;
if let Some(dp) = &app.devices {
for (i, d) in dp.devices.iter().enumerate() {
if d.name == name {
device_index = i;
if let Some(id) = d.id.clone() {
self
.net
.client_config
.set_device_id(id)
.map_err(|_e| anyhow!("failed to use device with name '{}'", d.name))?;
}
}
}
} else {
return Err(anyhow!("no device available"));
}
app.selected_device_index = Some(device_index);
Ok(())
}
pub async fn update_query_limits(&mut self, max: String) -> Result<()> {
let num = max
.parse::<u32>()
.map_err(|_e| anyhow!("limit must be between 1 and 50"))?;
if num > 50 || num == 0 {
return Err(anyhow!("limit must be between 1 and 50"));
};
self
.net
.handle_network_event(IoEvent::UpdateSearchLimits(num, num))
.await;
Ok(())
}
pub async fn volume(&mut self, vol: String) -> Result<()> {
let num = vol
.parse::<u32>()
.map_err(|_e| anyhow!("volume must be between 0 and 100"))?;
if num > 100 {
return Err(anyhow!("volume must be between 0 and 100"));
};
self
.net
.handle_network_event(IoEvent::ChangeVolume(num as u8))
.await;
Ok(())
}
pub async fn jump(&mut self, d: &JumpDirection) {
match d {
JumpDirection::Next => self.net.handle_network_event(IoEvent::NextTrack).await,
JumpDirection::Previous => self.net.handle_network_event(IoEvent::PreviousTrack).await,
}
}
pub async fn list(&mut self, item: Type, format: &str) -> String {
match item {
Type::Device => {
if let Some(devices) = &self.net.app.lock().await.devices {
devices
.devices
.iter()
.map(|d| {
self.format_output(
format.to_string(),
vec![
Format::Device(d.name.clone()),
Format::Volume(d.volume_percent.unwrap_or(0)),
],
)
})
.collect::<Vec<String>>()
.join("\n")
} else {
"No devices available".to_string()
}
}
Type::Playlist => {
self.net.handle_network_event(IoEvent::GetPlaylists).await;
if let Some(playlists) = &self.net.app.lock().await.playlists {
playlists
.items
.iter()
.map(|p| {
self.format_output(
format.to_string(),
Format::from_type(FormatType::Playlist(Box::new(p.clone()))),
)
})
.collect::<Vec<String>>()
.join("\n")
} else {
"No playlists found".to_string()
}
}
Type::Liked => {
self
.net
.handle_network_event(IoEvent::GetCurrentSavedTracks(None))
.await;
let liked_songs = self
.net
.app
.lock()
.await
.track_table
.tracks
.iter()
.map(|t| {
self.format_output(
format.to_string(),
Format::from_type(FormatType::Track(Box::new(t.clone()))),
)
})
.collect::<Vec<String>>();
if liked_songs.is_empty() {
"No liked songs found".to_string()
} else {
liked_songs.join("\n")
}
}
_ => unreachable!(),
}
}
pub async fn transfer_playback(&mut self, device: &str) -> Result<()> {
let mut id = String::new();
if let Some(devices) = &self.net.app.lock().await.devices {
for d in &devices.devices {
if d.name == device {
if let Some(device_id) = &d.id {
id.push_str(device_id);
break;
}
break;
}
}
};
if id.is_empty() {
Err(anyhow!("no device with name '{}'", device))
} else {
self
.net
.handle_network_event(IoEvent::TransferPlaybackToDevice(id.to_string()))
.await;
Ok(())
}
}
pub async fn seek(&mut self, seconds_str: String) -> Result<()> {
let seconds = match seconds_str.parse::<i32>() {
Ok(s) => s.unsigned_abs(),
Err(_) => return Err(anyhow!("failed to convert seconds to i32")),
};
let (current_pos, duration) = {
self
.net
.handle_network_event(IoEvent::GetCurrentPlayback)
.await;
let app = self.net.app.lock().await;
if let Some(CurrentPlaybackContext {
progress: Some(ms),
item: Some(item),
..
}) = &app.current_playback_context
{
let duration = match item {
PlayableItem::Track(track) => track.duration.num_milliseconds() as u32,
PlayableItem::Episode(episode) => episode.duration.num_milliseconds() as u32,
};
(ms.num_milliseconds() as u32, duration)
} else {
return Err(anyhow!("no context available"));
}
};
let ms = seconds * 1000;
let position_to_seek = if seconds_str.starts_with('+') {
current_pos + ms
} else if seconds_str.starts_with('-') {
current_pos.saturating_sub(ms)
} else {
seconds * 1000
};
if position_to_seek > duration {
self.jump(&JumpDirection::Next).await;
} else {
self
.net
.handle_network_event(IoEvent::Seek(position_to_seek))
.await;
}
Ok(())
}
pub async fn mark(&mut self, flag: Flag) -> Result<()> {
let c = {
let app = self.net.app.lock().await;
app
.current_playback_context
.clone()
.ok_or_else(|| anyhow!("no context available"))?
};
match flag {
Flag::Like(s) => {
let id = match c.item {
Some(i) => match i {
PlayableItem::Track(t) => t.id.ok_or_else(|| anyhow!("item has no id")),
PlayableItem::Episode(_) => Err(anyhow!("saving episodes not yet implemented")),
},
None => Err(anyhow!("no item playing")),
}?;
let id_string = id.id().to_string();
if s && !self.is_a_saved_track(&id_string).await {
self
.net
.handle_network_event(IoEvent::ToggleSaveTrack(PlayableId::Track(
id.into_static(),
)))
.await;
} else if !s && self.is_a_saved_track(&id_string).await {
self
.net
.handle_network_event(IoEvent::ToggleSaveTrack(PlayableId::Track(
id.into_static(),
)))
.await;
}
}
Flag::Shuffle => {
self
.net
.handle_network_event(IoEvent::Shuffle(c.shuffle_state))
.await
}
Flag::Repeat => {
self
.net
.handle_network_event(IoEvent::Repeat(c.repeat_state))
.await;
}
}
Ok(())
}
pub async fn get_status(&mut self, format: String) -> Result<String> {
self
.net
.handle_network_event(IoEvent::GetCurrentPlayback)
.await;
self
.net
.handle_network_event(IoEvent::GetCurrentSavedTracks(None))
.await;
let context = self
.net
.app
.lock()
.await
.current_playback_context
.clone()
.ok_or_else(|| anyhow!("no context available"))?;
let playing_item = context.item.ok_or_else(|| anyhow!("no track playing"))?;
let mut hs = match playing_item {
PlayableItem::Track(track) => {
let id = track
.id
.clone()
.map(|track_id| track_id.id().to_string())
.unwrap_or_default();
let mut hs = Format::from_type(FormatType::Track(Box::new(track.clone())));
if let Some(ms) = &context.progress {
hs.push(Format::Position((
ms.num_milliseconds() as u32,
track.duration.num_milliseconds() as u32,
)))
}
hs.push(Format::Flags((
context.repeat_state,
context.shuffle_state,
self.is_a_saved_track(&id).await,
)));
hs
}
PlayableItem::Episode(episode) => {
let mut hs = Format::from_type(FormatType::Episode(Box::new(episode.clone())));
if let Some(ms) = &context.progress {
hs.push(Format::Position((
ms.num_milliseconds() as u32,
episode.duration.num_milliseconds() as u32,
)))
}
hs.push(Format::Flags((
context.repeat_state,
context.shuffle_state,
false,
)));
hs
}
};
hs.push(Format::Device(context.device.name));
hs.push(Format::Volume(context.device.volume_percent.unwrap_or(0)));
hs.push(Format::Playing(context.is_playing));
Ok(self.format_output(format, hs))
}
pub async fn play_uri(&mut self, uri: String, queue: bool, random: bool) {
let offset = if random {
if uri.contains("spotify:playlist:") {
let id_str = uri.split(':').next_back().unwrap();
if let Ok(playlist_id) = rspotify::model::idtypes::PlaylistId::from_id(id_str) {
match self.net.spotify.playlist(playlist_id, None, None).await {
Ok(p) => {
let num = p.tracks.total;
Some(thread_rng().gen_range(0..num) as usize)
}
Err(e) => {
self
.net
.app
.lock()
.await
.handle_error(anyhow!(e.to_string()));
return;
}
}
} else {
None
}
} else {
None
}
} else {
None
};
if uri.contains("spotify:track:") {
let id_str = uri.split(':').next_back().unwrap();
if let Ok(track_id) = rspotify::model::idtypes::TrackId::from_id(id_str) {
let playable_id = PlayableId::Track(track_id.into_static());
if queue {
self
.net
.handle_network_event(IoEvent::AddItemToQueue(playable_id))
.await;
} else {
self
.net
.handle_network_event(IoEvent::StartPlayback(
None,
Some(vec![playable_id]),
Some(0),
))
.await;
}
}
} else {
let parts: Vec<&str> = uri.split(':').collect();
if parts.len() >= 3 {
let id_str = parts[2];
let context_id = if uri.contains("spotify:playlist:") {
rspotify::model::idtypes::PlaylistId::from_id(id_str)
.ok()
.map(|id| PlayContextId::Playlist(id.into_static()))
} else if uri.contains("spotify:album:") {
rspotify::model::idtypes::AlbumId::from_id(id_str)
.ok()
.map(|id| PlayContextId::Album(id.into_static()))
} else if uri.contains("spotify:artist:") {
rspotify::model::idtypes::ArtistId::from_id(id_str)
.ok()
.map(|id| PlayContextId::Artist(id.into_static()))
} else if uri.contains("spotify:show:") {
rspotify::model::idtypes::ShowId::from_id(id_str)
.ok()
.map(|id| PlayContextId::Show(id.into_static()))
} else {
None
};
if let Some(context_id) = context_id {
self
.net
.handle_network_event(IoEvent::StartPlayback(Some(context_id), None, offset))
.await;
}
}
}
}
pub async fn play(&mut self, name: String, item: Type, queue: bool, random: bool) -> Result<()> {
self
.net
.handle_network_event(IoEvent::GetSearchResults(name.clone(), None))
.await;
let uri = {
let results = &self.net.app.lock().await.search_results;
match item {
Type::Track => {
if let Some(r) = &results.tracks {
if let Some(id) = &r.items[0].id {
format!("spotify:track:{}", id.id())
} else {
return Err(anyhow!("track has no id"));
}
} else {
return Err(anyhow!("no tracks with name '{}'", name));
}
}
Type::Album => {
if let Some(r) = &results.albums {
let album = &r.items[0];
if let Some(id) = &album.id {
format!("spotify:album:{}", id.id())
} else {
return Err(anyhow!("album {} has no id", album.name));
}
} else {
return Err(anyhow!("no albums with name '{}'", name));
}
}
Type::Artist => {
if let Some(r) = &results.artists {
format!("spotify:artist:{}", r.items[0].id.id())
} else {
return Err(anyhow!("no artists with name '{}'", name));
}
}
Type::Show => {
if let Some(r) = &results.shows {
format!("spotify:show:{}", r.items[0].id.id())
} else {
return Err(anyhow!("no shows with name '{}'", name));
}
}
Type::Playlist => {
if let Some(r) = &results.playlists {
let p = &r.items[0];
format!("spotify:playlist:{}", p.id.id())
} else {
return Err(anyhow!("no playlists with name '{}'", name));
}
}
_ => unreachable!(),
}
};
self.play_uri(uri, queue, random).await;
Ok(())
}
pub async fn query(&mut self, search: String, format: String, item: Type) -> String {
self
.net
.handle_network_event(IoEvent::GetSearchResults(search.clone(), None))
.await;
let app = self.net.app.lock().await;
match item {
Type::Playlist => {
if let Some(results) = &app.search_results.playlists {
results
.items
.iter()
.map(|r| {
self.format_output(
format.clone(),
Format::from_type(FormatType::Playlist(Box::new(r.clone()))),
)
})
.collect::<Vec<String>>()
.join("\n")
} else {
format!("no playlists with name '{}'", search)
}
}
Type::Track => {
if let Some(results) = &app.search_results.tracks {
results
.items
.iter()
.map(|r| {
self.format_output(
format.clone(),
Format::from_type(FormatType::Track(Box::new(r.clone()))),
)
})
.collect::<Vec<String>>()
.join("\n")
} else {
format!("no tracks with name '{}'", search)
}
}
Type::Artist => {
if let Some(results) = &app.search_results.artists {
results
.items
.iter()
.map(|r| {
self.format_output(
format.clone(),
Format::from_type(FormatType::Artist(Box::new(r.clone()))),
)
})
.collect::<Vec<String>>()
.join("\n")
} else {
format!("no artists with name '{}'", search)
}
}
Type::Show => {
if let Some(results) = &app.search_results.shows {
results
.items
.iter()
.map(|r| {
self.format_output(
format.clone(),
Format::from_type(FormatType::Show(Box::new(r.clone()))),
)
})
.collect::<Vec<String>>()
.join("\n")
} else {
format!("no shows with name '{}'", search)
}
}
Type::Album => {
if let Some(results) = &app.search_results.albums {
results
.items
.iter()
.map(|r| {
self.format_output(
format.clone(),
Format::from_type(FormatType::Album(Box::new(r.clone()))),
)
})
.collect::<Vec<String>>()
.join("\n")
} else {
format!("no albums with name '{}'", search)
}
}
_ => unreachable!(),
}
}
}