plavno 0.4.0

A lightweight terminal music player for Linux, macOS and Windows with service waves and library shuffle
mod bridge;
mod local;
mod multi;
mod rate_limit;
mod soundcloud;
mod vk;
pub(crate) mod web;
mod yandex;
mod yandex_cache;
mod youtube;

pub use bridge::Bridge;
pub use local::{Local, LocalDirectory};
pub use multi::Multi;
pub use rate_limit::RateLimited;
pub use soundcloud::SoundCloud;
pub use vk::Vk;
pub use yandex::Yandex;
pub use yandex::http as yandex_http;
pub use yandex::network_error_at;
pub use youtube::YouTube;

use anyhow::Result;
use std::path::PathBuf;

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Track {
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub source: String,
    pub id: String,
    pub title: String,
    pub artist: String,
    pub duration: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cover_url: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub genre: Option<String>,
    #[serde(skip)]
    pub batch: String,
}

impl Track {
    pub fn cache_key(&self) -> String {
        if self.source.is_empty() {
            self.id.clone()
        } else {
            format!("{}:{}", self.source, self.id)
        }
    }
}

#[derive(Clone)]
pub struct Service {
    pub key: &'static str,
    pub name: &'static str,
    pub status: String,
    pub available: bool,
    pub favorites: bool,
    pub favorites_shuffle: bool,
    pub wave_styles: bool,
    pub allow_repeats: bool,
}

#[derive(Clone)]
pub enum Input {
    Http {
        url: String,
        headers: Vec<(String, String)>,
        aes128_key: Option<[u8; 16]>,
        hint: &'static str,
        quality: String,
    },
    Stream {
        url: String,
        key: Option<[u8; 16]>,
        iv: [u8; 16],
        skip: u64,
        hint: &'static str,
        quality: String,
    },
    Pipe {
        program: PathBuf,
        args: Vec<String>,
        request: serde_json::Value,
        quality: String,
    },
    File(PathBuf),
}

#[derive(Clone, Copy)]
pub enum Feedback {
    Started,
    Finished,
    Skipped,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PlaybackMode {
    #[default]
    MyVibe,
    FavoritesShuffle,
    FavoritesNewest,
}

impl PlaybackMode {
    pub fn is_favorites(self) -> bool {
        matches!(self, Self::FavoritesShuffle | Self::FavoritesNewest)
    }

    pub fn label(self) -> &'static str {
        match self {
            Self::MyVibe => "My Vibe",
            Self::FavoritesShuffle => "Favorites Shuffle",
            Self::FavoritesNewest => "Favorites · Newest first",
        }
    }

    pub fn key(self) -> &'static str {
        match self {
            Self::MyVibe => "w",
            Self::FavoritesShuffle => "f",
            Self::FavoritesNewest => "Shift+F",
        }
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WaveStyle {
    Calm,
    #[default]
    Balanced,
    Energetic,
    Work,
}

impl WaveStyle {
    pub const ALL: [Self; 4] = [Self::Calm, Self::Balanced, Self::Energetic, Self::Work];

    pub fn label(self) -> &'static str {
        match self {
            Self::Calm => "Calm",
            Self::Balanced => "Balanced",
            Self::Energetic => "Energetic",
            Self::Work => "Work",
        }
    }

    pub fn description(self, source: &str) -> &'static str {
        match self {
            Self::Calm => "Relaxed tracks",
            Self::Balanced => "Personal recommendations without mood filters",
            Self::Energetic => "Active tracks",
            Self::Work if source == "yandex" => "Calm ambient without vocals",
            Self::Work if source == "ytmusic" => "Focus playlists from YouTube Music",
            Self::Work => "Calm instrumental tracks",
        }
    }
}

pub trait Backend: Send {
    fn suspend(&mut self) {}
    fn services(&self) -> Vec<Service> {
        vec![Service {
            key: self.key(),
            name: self.name(),
            status: "Ready".into(),
            available: true,
            favorites: self.supports_favorites(),
            favorites_shuffle: self.supports_favorites_shuffle(),
            wave_styles: self.supports_wave_styles(),
            allow_repeats: self.allows_repeats(),
        }]
    }
    fn start_source(&mut self, source: &str, mode: PlaybackMode) -> Result<Vec<Track>> {
        anyhow::ensure!(source == self.key(), "Unknown source");
        self.start_mode(mode)
    }
    fn key(&self) -> &'static str {
        self.name()
    }
    fn name(&self) -> &'static str;
    fn supports_wave_styles(&self) -> bool {
        false
    }
    fn allows_repeats(&self) -> bool {
        false
    }
    fn set_wave_style(&mut self, style: WaveStyle) -> Result<()> {
        anyhow::ensure!(
            style == WaveStyle::Balanced,
            "This source does not support wave styles"
        );
        Ok(())
    }
    fn start_with_style(
        &mut self,
        source: &str,
        mode: PlaybackMode,
        style: WaveStyle,
    ) -> Result<Vec<Track>> {
        if self.supports_wave_styles() {
            self.set_wave_style(style)?;
        }
        self.start_source(source, mode)
    }
    fn start(&mut self) -> Result<Vec<Track>>;
    fn start_mode(&mut self, mode: PlaybackMode) -> Result<Vec<Track>> {
        anyhow::ensure!(
            mode == PlaybackMode::MyVibe,
            "This source does not support favorites playback"
        );
        self.start()
    }
    fn more(&mut self, history: &[String]) -> Result<Vec<Track>>;
    fn resolve(&mut self, track: &Track) -> Result<Input>;
    fn feedback(&mut self, track: &Track, kind: Feedback, seconds: f64) -> Result<()>;
    fn supports_favorites(&self) -> bool {
        false
    }
    fn supports_favorites_shuffle(&self) -> bool {
        false
    }
    fn favorite_state(&mut self, _track: &Track) -> Result<Option<bool>> {
        Ok(None)
    }
    fn toggle_favorite(&mut self, _track: &Track) -> Result<bool> {
        anyhow::bail!("This source does not support favorites")
    }
    fn set_dislike(&mut self, _track: &Track, _disliked: bool) -> Result<bool> {
        Ok(false)
    }
}