submarine 0.1.0

A library for connecting with a subsonic server
Documentation
use std::fmt::Display;
use std::str::FromStr;

use crate::data::{Child, ResponseType};
use crate::{Client, Parameter, SubsonicError};

#[derive(Debug, PartialEq, Eq)]
pub enum Order {
    Random,
    Newest,
    Highest,
    Frequent,
    Recent,
    AlphabeticalByName,
    AlphabeticalByArtist,
    Starred,
}

impl FromStr for Order {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "random" => Ok(Self::Random),
            "newest" => Ok(Self::Newest),
            "highest" => Ok(Self::Highest),
            "frequent" => Ok(Self::Frequent),
            "recent" => Ok(Self::Recent),
            "alphabeticalByName" => Ok(Self::AlphabeticalByName),
            "alphabeticalByArtist" => Ok(Self::AlphabeticalByArtist),
            "starred" => Ok(Self::Starred),
            _ => Err(()),
        }
    }
}

impl Display for Order {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Random => write!(f, "random"),
            Self::Newest => write!(f, "newest"),
            Self::Highest => write!(f, "highest"),
            Self::Frequent => write!(f, "frequent"),
            Self::Recent => write!(f, "recent"),
            Self::AlphabeticalByName => write!(f, "alphabeticalByName"),
            Self::AlphabeticalByArtist => write!(f, "alphabeticalByArtist"),
            Self::Starred => write!(f, "starred"),
        }
    }
}

impl Client {
    pub(crate) fn create_paras(
        size: Option<usize>,
        offset: Option<usize>,
        music_folder_id: Option<impl Into<String>>,
    ) -> Parameter {
        let mut paras = Parameter::new();
        if let Some(size) = size {
            paras.push("size", size.to_string()); //500 is maximum
        }
        if let Some(offset) = offset {
            paras.push("offset", offset.to_string());
        }
        if let Some(folder_id) = music_folder_id {
            paras.push("musicFolderId", folder_id);
        }
        paras
    }

    /// reference: http://www.subsonic.org/pages/api.jsp#getAlbumList
    pub async fn get_album_list(
        &self,
        order: Order,
        size: Option<usize>,
        offset: Option<usize>,
        music_folder_id: Option<impl Into<String>>,
    ) -> Result<Vec<Child>, SubsonicError> {
        let mut paras = Self::create_paras(size, offset, music_folder_id);
        paras.push("type", order.to_string());

        let body = self.request("getAlbumList", Some(paras), None).await?;
        if let ResponseType::AlbumList { album_list } = body.data {
            Ok(album_list.album)
        } else {
            Err(SubsonicError::Submarine(String::from(
                "expected type AlbumList but found wrong type",
            )))
        }
    }

    /// reference: http://www.subsonic.org/pages/api.jsp#getAlbumList
    pub async fn get_album_list_by_year(
        &self,
        from_year: Option<usize>,
        to_year: Option<usize>,
        size: Option<usize>,
        offset: Option<usize>,
        music_folder_id: Option<impl Into<String>>,
    ) -> Result<Vec<Child>, SubsonicError> {
        let mut paras = Self::create_paras(size, offset, music_folder_id);
        paras.push("type", "byYear");
        if let Some(from) = from_year {
            paras.push("fromYear", from.to_string());
        }
        if let Some(to) = to_year {
            paras.push("toYear", to.to_string());
        }

        let body = self.request("getAlbumList", Some(paras), None).await?;
        if let ResponseType::AlbumList { album_list } = body.data {
            Ok(album_list.album)
        } else {
            Err(SubsonicError::Submarine(String::from(
                "expected type AlbumList but found wrong type",
            )))
        }
    }

    /// reference: http://www.subsonic.org/pages/api.jsp#getAlbumList
    pub async fn get_album_list_by_genre(
        &self,
        genre: impl Into<String>,
        size: Option<usize>,
        offset: Option<usize>,
        music_folder_id: Option<impl Into<String>>,
    ) -> Result<Vec<Child>, SubsonicError> {
        let mut paras = Self::create_paras(size, offset, music_folder_id);
        paras.push("type", "byGenre");
        paras.push("genre", genre);

        let body = self.request("getAlbumList", Some(paras), None).await?;
        if let ResponseType::AlbumList { album_list } = body.data {
            Ok(album_list.album)
        } else {
            Err(SubsonicError::Submarine(String::from(
                "expected type AlbumList but found wrong type",
            )))
        }
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use crate::data::{OuterResponse, ResponseType};

    use super::Order;

    #[test]
    fn conversion_order() {
        let oracle = vec![
            Order::Random,
            Order::Newest,
            Order::Highest,
            Order::Frequent,
            Order::Recent,
            Order::AlphabeticalByName,
            Order::AlphabeticalByArtist,
            Order::Starred,
        ];
        for test in oracle {
            println!("testing: {test:?}");
            println!("  to_string: {}", test.to_string());
            println!("  to_order: {:?}", Order::from_str(&test.to_string()));
            assert_eq!(
                test,
                Order::from_str(&test.to_string()).unwrap(),
                "{test:?}"
            );
        }
    }

    #[test]
    fn conversion_get_album_list() {
        let response_body = r##"
            {
              "subsonic-response": {
                "status": "ok",
                "version": "1.16.1",
                "type": "navidrome",
                "serverVersion": "0.49.3 (8b93962f)",
                "albumList": {
                  "album": [
                    {
                      "id": "c686e803911d72cf98bd077c72326759",
                      "parent": "f97bc375b1576bdfd1116641304c4cf0",
                      "isDir": true,
                      "title": "Voodooism the Sequel",
                      "name": "Voodooism the Sequel",
                      "album": "Voodooism the Sequel",
                      "artist": "cYsmix",
                      "year": 2014,
                      "coverArt": "al-c686e803911d72cf98bd077c72326759_6476fade",
                      "starred": "2023-06-24T10:30:05Z",
                      "duration": 4846,
                      "playCount": 53,
                      "played": "2023-08-29T11:11:58Z",
                      "created": "2023-05-31T07:51:00.680123782Z",
                      "artistId": "f97bc375b1576bdfd1116641304c4cf0",
                      "songCount": 9,
                      "isVideo": false
                    },
                    {
                      "id": "9a53fb46776475612d41954e0e7a7a18",
                      "parent": "8dd319f210acbb691fd4f809e487e17f",
                      "isDir": true,
                      "title": "Planet Punk",
                      "name": "Planet Punk",
                      "album": "Planet Punk",
                      "artist": "Die Ärzte",
                      "year": 1995,
                      "genre": "Punk",
                      "coverArt": "al-9a53fb46776475612d41954e0e7a7a18_62687631",
                      "starred": "2022-05-10T09:40:50Z",
                      "duration": 3469,
                      "playCount": 29,
                      "played": "2023-03-21T10:03:18Z",
                      "created": "2021-08-12T14:34:03.204608575Z",
                      "artistId": "8dd319f210acbb691fd4f809e487e17f",
                      "songCount": 17,
                      "isVideo": false
                    }
                  ]
                }
              }
            }"##;
        let response = serde_json::from_str::<OuterResponse>(response_body)
            .unwrap()
            .inner;
        if let ResponseType::AlbumList { album_list } = response.data {
            assert_eq!(album_list.album.len(), 2);
            assert_eq!(album_list.album.first().unwrap().year, Some(2014));
        } else {
            panic!("wrong type: {response:?}");
        }
    }
}