selene-core 0.8.1

selene-core is the backend for Selene, a local-first music player
Documentation
use thiserror::Error;

use crate::{
    REQWEST_CLIENT,
    library::track::ResolvedTrack,
    scrobbling::lastfm::{
        LASTFM_API_KEY, LASTFM_SCROBBLER_ORIGIN, LastFmClient, LastFmClientError, LastFmResult,
        request_sig,
    },
};

#[derive(Debug, Error)]
pub enum LastFmMethodError {
    #[error("Could not scrobble the given track because the track has no title")]
    MissingTrackTitle,

    #[error("Could not scrobble the given track because the track has no artists")]
    MissingArtist,
}

impl LastFmClient {
    pub async fn now_playing(&self, track: &ResolvedTrack) -> Result<(), LastFmClientError> {
        let mut query: Vec<(&'static str, String)> = vec![
            ("method", "track.updatenowplaying".to_owned()),
            ("api_key", LASTFM_API_KEY.to_owned()),
            ("sk", self.key.clone()),
        ];

        add_form_by_track(&mut query, track)?;

        let sig = request_sig(&query);

        query.push(("api_sig", sig));
        query.push(("format", "json".to_owned()));

        REQWEST_CLIENT
            .post(LASTFM_SCROBBLER_ORIGIN)
            .form(&query)
            .send()
            .await
            .unwrap()
            .json::<LastFmResult<serde::de::IgnoredAny>>()
            .await
            .unwrap()
            .into_result()?;

        Ok(())
    }

    pub async fn scrobble(
        &self,
        track: &ResolvedTrack,
        started_at: u64,
        chosen_by_user: bool,
    ) -> Result<(), LastFmClientError> {
        let mut query: Vec<(&'static str, String)> = vec![
            ("method", "track.scrobble".to_owned()),
            ("api_key", LASTFM_API_KEY.to_owned()),
            ("sk", self.key.clone()),
            ("timestamp", started_at.to_string()),
            ("chosenByUser", u8::from(chosen_by_user).to_string()),
        ];

        add_form_by_track(&mut query, track)?;

        let sig = request_sig(&query);

        query.push(("api_sig", sig));
        query.push(("format", "json".to_owned()));

        REQWEST_CLIENT
            .post(LASTFM_SCROBBLER_ORIGIN)
            .form(&query)
            .send()
            .await
            .unwrap()
            .json::<LastFmResult<serde::de::IgnoredAny>>()
            .await
            .unwrap()
            .into_result()?;

        Ok(())
    }
}

fn add_form_by_track(
    query: &mut Vec<(&'static str, String)>,
    track: &ResolvedTrack,
) -> Result<(), LastFmMethodError> {
    let track_title = track
        .metadata
        .title
        .as_ref()
        .ok_or(LastFmMethodError::MissingTrackTitle)?
        .to_owned();

    let main_track_artist = track
        .artists()
        .first()
        .ok_or(LastFmMethodError::MissingArtist)?
        .name()
        .to_owned();

    let duration = track.container().stream().duration();

    query.extend([
        ("artist", main_track_artist.clone()),
        ("track", track_title),
        ("duration", duration.to_string()),
    ]);

    if let Some((album, artists, ..)) = track.album_info() {
        query.push(("album", album.name().to_owned()));

        if let Some(main_album_artist) = artists.first().map(|a| a.name())
            && main_album_artist != main_track_artist
        {
            query.push(("albumArtist", main_album_artist.to_owned()));
        }
    }

    Ok(())
}