🏡 index : ~doyle/shalom.git

mod search;

use std::{borrow::Cow, convert::identity, iter, sync::Arc, time::Duration};

use iced::{
    futures::{future, future::Either, stream, stream::FuturesUnordered, FutureExt, StreamExt},
    subscription,
    widget::{container, image::Handle, Column, Text},
    Element, Length, Renderer, Subscription, Theme,
};
use itertools::Itertools;
use serde::Deserialize;
use url::Url;
use yoke::{Yoke, Yokeable};

use crate::{
    config::Config,
    hass_client::MediaPlayerRepeat,
    magic::header_search::header_search,
    oracle::{MediaPlayerSpeaker, MediaPlayerSpeakerState, Oracle, Room},
    pages::room::listen::search::SearchResult,
    subscriptions::{
        download_image, find_fanart_urls, find_musicbrainz_artist, load_image, MaybePendingImage,
    },
    theme::{darken_image, trim_transparent_padding, Image},
    widgets,
};

#[derive(Debug)]
pub struct Listen {
    room: Room,
    oracle: Arc<Oracle>,
    speaker: Option<(&'static str, MediaPlayerSpeaker)>,
    album_art_image: Option<Handle>,
    musicbrainz_artist_id: Option<String>,
    pub background: Option<MaybePendingImage>,
    artist_logo: Option<MaybePendingImage>,
    pub search: SearchState,
    config: Arc<Config>,
}

impl Listen {
    pub fn new(oracle: Arc<Oracle>, room: &Room, config: Arc<Config>) -> Self {
        let speaker = room.speaker(&oracle);

        Self {
            room: room.clone(),
            speaker,
            oracle,
            album_art_image: None,
            musicbrainz_artist_id: None,
            background: None,
            artist_logo: None,
            search: SearchState::Closed,
            config,
        }
    }

    pub fn header_magic(&self, text: Text<'static>, dy_mult: f32) -> Element<'static, Message> {
        let (open, query) = if let Some(v) = self.search.search() {
            (true, v)
        } else {
            (false, "")
        };

        header_search(
            Message::OnSearchTerm,
            Message::OnSearchVisibleToggle,
            open,
            query,
            text,
            dy_mult,
        )
        .into()
    }

    #[allow(clippy::too_many_lines)]
    pub fn update(&mut self, event: Message) -> Option<Event> {
        match event {
            Message::AlbumArtImageLoaded(handle) => {
                self.album_art_image = Some(handle);
                None
            }
            Message::FanArtLoaded(logo, background) => {
                self.background = background.map(MaybePendingImage::Loading);
                self.artist_logo = logo.map(MaybePendingImage::Loading);
                None
            }
            Message::MusicbrainzArtistLoaded(v) => {
                eprintln!("musicbrainz artist {v}");
                self.musicbrainz_artist_id = Some(v);
                None
            }
            Message::UpdateSpeaker => {
                let new = self.room.speaker(&self.oracle);

                if self
                    .speaker
                    .as_ref()
                    .and_then(|(_, v)| v.entity_picture.as_ref())
                    != new
                        .as_ref()
                        .as_ref()
                        .and_then(|(_, v)| v.entity_picture.as_ref())
                {
                    self.album_art_image = None;
                    self.artist_logo = None;
                    self.background = None;
                }

                if self
                    .speaker
                    .as_ref()
                    .and_then(|(_, v)| v.media_artist.as_ref())
                    != new
                        .as_ref()
                        .as_ref()
                        .and_then(|(_, v)| v.media_artist.as_ref())
                {
                    self.musicbrainz_artist_id = None;
                }

                self.speaker = new;

                None
            }
            Message::OnSpeakerVolumeChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.volume = new;
                Some(Event::SetSpeakerVolume(id, new))
            }
            Message::OnSpeakerPositionChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.actual_media_position = Some(new);
                Some(Event::SetSpeakerPosition(id, new))
            }
            Message::OnSpeakerStateChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.state = if new {
                    MediaPlayerSpeakerState::Playing
                } else {
                    MediaPlayerSpeakerState::Paused
                };
                Some(Event::SetSpeakerPlaying(id, new))
            }
            Message::OnSpeakerMuteChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.muted = new;
                Some(Event::SetSpeakerMuted(id, new))
            }
            Message::OnSpeakerRepeatChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.repeat = new;
                Some(Event::SetSpeakerRepeat(id, new))
            }
            Message::OnSpeakerNextTrack => Some(Event::SpeakerNextTrack(self.speaker.as_ref()?.0)),
            Message::OnSpeakerPreviousTrack => {
                Some(Event::SpeakerPreviousTrack(self.speaker.as_ref()?.0))
            }
            Message::OnSpeakerShuffleChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.shuffle = new;
                Some(Event::SetSpeakerShuffle(id, new))
            }
            Message::BackgroundDownloaded(handle) => {
                self.background = Some(MaybePendingImage::Downloaded(handle));
                None
            }
            Message::ArtistLogoDownloaded(handle) => {
                self.artist_logo = Some(MaybePendingImage::Downloaded(handle));
                None
            }
            Message::OnSearchTerm(v) => {
                self.search = self.search.open(v);
                None
            }
            Message::OnSearchVisibleToggle => {
                self.search = if matches!(self.search, SearchState::Closed) {
                    SearchState::Open {
                        search: String::new(),
                        results_search: String::new(),
                        results: Ok(vec![]),
                    }
                } else {
                    SearchState::Closed
                };
                None
            }
            Message::SpotifySearchResult((res, search)) => {
                if self.search.search() != Some(&search) {
                    return None;
                }

                if let SearchState::Open { results, .. } = &mut self.search {
                    if let Ok(results) = results {
                        results.push(res);
                    } else {
                        *results = Ok(vec![res]);
                    }
                }

                None
            }
            Message::SpotifySearchResultError((res, search)) => {
                if self.search.search() != Some(&search) {
                    return None;
                }

                if let SearchState::Open { results, .. } = &mut self.search {
                    *results = Err(res);
                }

                None
            }
            Message::OnPlayTrack(uri) => Some(Event::PlayTrack(self.speaker.as_ref()?.0, uri)),
        }
    }

    pub fn view(&self, style: &Theme) -> Element<'_, Message, Renderer> {
        if self.search.is_open() {
            container(
                search::search(style.clone(), self.search.results())
                    .on_track_press(Message::OnPlayTrack),
            )
            .padding([0, 40, 40, 40])
            .width(Length::Fill)
            .into()
        } else if let Some((_, speaker)) = self.speaker.clone() {
            container(
                widgets::media_player::media_player(speaker, self.album_art_image.clone())
                    .with_artist_logo(
                        self.artist_logo
                            .as_ref()
                            .and_then(MaybePendingImage::handle),
                    )
                    .on_volume_change(Message::OnSpeakerVolumeChange)
                    .on_mute_change(Message::OnSpeakerMuteChange)
                    .on_repeat_change(Message::OnSpeakerRepeatChange)
                    .on_state_change(Message::OnSpeakerStateChange)
                    .on_position_change(Message::OnSpeakerPositionChange)
                    .on_next_track(Message::OnSpeakerNextTrack)
                    .on_previous_track(Message::OnSpeakerPreviousTrack)
                    .on_shuffle_change(Message::OnSpeakerShuffleChange),
            )
            .into()
        } else {
            Column::new().into()
        }
    }

    pub fn subscription(&self) -> Subscription<Message> {
        let album_art_subscription = if let (Some(uri), None) = (
            self.speaker
                .as_ref()
                .and_then(|(_, v)| v.entity_picture.as_ref()),
            &self.album_art_image,
        ) {
            download_image(uri.clone(), identity, Message::AlbumArtImageLoaded)
        } else {
            Subscription::none()
        };

        let musicbrainz_artist_id_subscription = if let (Some(artist), None) = (
            self.speaker
                .as_ref()
                .and_then(|(_, v)| v.media_artist.as_ref()),
            &self.musicbrainz_artist_id,
        ) {
            find_musicbrainz_artist(artist.to_string(), Message::MusicbrainzArtistLoaded)
        } else {
            Subscription::none()
        };

        let fanart_subscription = if let (None, None, Some(musicbrainz_id)) = (
            &self.background,
            &self.artist_logo,
            &self.musicbrainz_artist_id,
        ) {
            find_fanart_urls(musicbrainz_id.clone(), Message::FanArtLoaded)
        } else {
            Subscription::none()
        };

        let background_subscription =
            if let Some(MaybePendingImage::Loading(url)) = &self.background {
                download_image(
                    url.clone(),
                    |image| crate::theme::blur(&darken_image(image, 0.3), 15),
                    Message::BackgroundDownloaded,
                )
            } else {
                Subscription::none()
            };

        let logo_subscription = if let Some(MaybePendingImage::Loading(url)) = &self.artist_logo {
            download_image(
                url.clone(),
                trim_transparent_padding,
                Message::ArtistLogoDownloaded,
            )
        } else {
            Subscription::none()
        };

        let speaker_subscription = if let Some(speaker_id) = self.speaker.as_ref().map(|(k, _)| *k)
        {
            subscription::run_with_id(
                speaker_id,
                self.oracle
                    .subscribe_id(speaker_id)
                    .map(|()| Message::UpdateSpeaker),
            )
        } else {
            Subscription::none()
        };

        let spotify_result = if let SearchState::Open { search, .. } = &self.search {
            search_spotify(search, &self.config.spotify.token)
        } else {
            Subscription::none()
        };

        Subscription::batch([
            album_art_subscription,
            speaker_subscription,
            musicbrainz_artist_id_subscription,
            background_subscription,
            logo_subscription,
            fanart_subscription,
            spotify_result,
        ])
    }
}

#[derive(Debug, Hash, Clone)]
pub enum SearchState {
    Open {
        search: String,
        results_search: String,
        results: Result<Vec<SearchResult>, String>,
    },
    Closed,
}

impl SearchState {
    pub fn is_open(&self) -> bool {
        matches!(self, Self::Open { search, .. } if !search.is_empty())
    }

    pub fn results(&self) -> search::SearchState<'_> {
        match self {
            Self::Open {
                results,
                results_search,
                ..
            } => match results {
                Ok(v) if results_search.is_empty() && v.is_empty() => search::SearchState::NotReady,
                Ok(v) => search::SearchState::Ready(v.as_slice()),
                Err(e) => search::SearchState::Error(e),
            },
            Self::Closed => search::SearchState::NotReady,
        }
    }

    pub fn search(&self) -> Option<&str> {
        match self {
            Self::Open { search, .. } => Some(search),
            Self::Closed => None,
        }
    }

    pub fn open(&self, search: String) -> Self {
        match self {
            Self::Open { results_search, .. } => Self::Open {
                search,
                results_search: results_search.clone(),
                results: Ok(vec![]),
            },
            Self::Closed => Self::Open {
                search,
                results_search: String::new(),
                results: Ok(vec![]),
            },
        }
    }
}

#[derive(Clone)]
pub enum Event {
    SetSpeakerVolume(&'static str, f32),
    SetSpeakerPosition(&'static str, Duration),
    SetSpeakerPlaying(&'static str, bool),
    SetSpeakerMuted(&'static str, bool),
    SetSpeakerShuffle(&'static str, bool),
    SetSpeakerRepeat(&'static str, MediaPlayerRepeat),
    SpeakerNextTrack(&'static str),
    SpeakerPreviousTrack(&'static str),
    PlayTrack(&'static str, String),
}

#[derive(Clone, Debug)]
pub enum Message {
    AlbumArtImageLoaded(Handle),
    BackgroundDownloaded(Handle),
    ArtistLogoDownloaded(Handle),
    MusicbrainzArtistLoaded(String),
    FanArtLoaded(Option<Url>, Option<Url>),
    UpdateSpeaker,
    OnSpeakerVolumeChange(f32),
    OnSpeakerPositionChange(Duration),
    OnSpeakerStateChange(bool),
    OnSpeakerMuteChange(bool),
    OnSpeakerShuffleChange(bool),
    OnSpeakerRepeatChange(MediaPlayerRepeat),
    OnSpeakerNextTrack,
    OnSpeakerPreviousTrack,
    OnSearchTerm(String),
    OnSearchVisibleToggle,
    SpotifySearchResult((SearchResult, String)),
    SpotifySearchResultError((String, String)),
    OnPlayTrack(String),
}

fn search_spotify(search_param: &str, token: &str) -> Subscription<Message> {
    if search_param.is_empty() {
        return Subscription::none();
    }

    let token = token.to_string();

    let search = search_param.to_string();
    subscription::run_with_id(
        format!("search-{search}"),
        stream::once(async move {
            eprintln!("sending search {search}");

            let mut url = Url::parse("https://api.spotify.com/v1/search").unwrap();
            url.query_pairs_mut()
                .append_pair("q", &search)
                .append_pair("type", "album,artist,playlist,track")
                .append_pair("market", "GB")
                .append_pair("limit", "20");

            let res = reqwest::Client::new()
                .get(url)
                .header("Authorization", format!("Bearer {token}"))
                .send()
                .await
                .unwrap()
                .text()
                .await
                .unwrap();

            eprintln!("{search} - {}", std::str::from_utf8(res.as_ref()).unwrap());

            (
                Yoke::attach_to_cart(res, |s| serde_json::from_str(s).unwrap()),
                search,
            )
        })
        .flat_map(
            |(res, search): (Yoke<SpotifySearchResult<'static>, String>, String)| {
                let res = res.get();

                if let Some(error) = &res.error {
                    return Either::Left(stream::iter(iter::once(
                        Message::SpotifySearchResultError((error.message.to_string(), search)),
                    )));
                }

                let results = FuturesUnordered::new();

                for track in &res.tracks.items {
                    let image_url = track.album.images.last().map(|v| v.url.to_string());
                    let track_name = track.name.to_string();
                    let artist_name = track.artists.iter().map(|v| &v.name).join(", ");
                    let uri = track.uri.to_string();

                    results.push(tokio::spawn(
                        async move {
                            let image = load_album_art(image_url).await;
                            SearchResult::track(image, track_name, artist_name, uri)
                        }
                        .boxed(),
                    ));
                }

                for artist in &res.artists.items {
                    let image_url = artist.images.last().map(|v| v.url.to_string());
                    let artist_name = artist.name.to_string();
                    let uri = artist.uri.to_string();

                    results.push(tokio::spawn(
                        async move {
                            let image = load_album_art(image_url).await;
                            SearchResult::artist(image, artist_name, uri)
                        }
                        .boxed(),
                    ));
                }

                for albums in &res.albums.items {
                    let image_url = albums.images.last().map(|v| v.url.to_string());
                    let album_name = albums.name.to_string();
                    let uri = albums.uri.to_string();

                    results.push(tokio::spawn(
                        async move {
                            let image = load_album_art(image_url).await;
                            SearchResult::album(image, album_name, uri)
                        }
                        .boxed(),
                    ));
                }

                for playlist in &res.playlists.items {
                    let image_url = playlist.images.last().map(|v| v.url.to_string());
                    let playlist_name = playlist.name.to_string();
                    let uri = playlist.uri.to_string();

                    results.push(tokio::spawn(
                        async move {
                            let image = load_album_art(image_url).await;
                            SearchResult::playlist(image, playlist_name, uri)
                        }
                        .boxed(),
                    ));
                }

                Either::Right(
                    results
                        .filter_map(|v| future::ready(v.ok()))
                        .zip(stream::repeat(search))
                        .map(Message::SpotifySearchResult),
                )
            },
        ),
    )
}

async fn load_album_art(image_url: Option<String>) -> Handle {
    if let Some(image_url) = image_url {
        load_image(image_url, identity).await
    } else {
        Image::UnknownArtist.into()
    }
}

#[derive(Deserialize, Yokeable)]
pub struct SpotifySearchResult<'a> {
    #[serde(borrow, default)]
    tracks: SpotifySearchResultWrapper<SpotifyTrack<'a>>,
    #[serde(borrow, default)]
    artists: SpotifySearchResultWrapper<SpotifyArtist<'a>>,
    #[serde(borrow, default)]
    albums: SpotifySearchResultWrapper<SpotifyAlbum<'a>>,
    #[serde(borrow, default)]
    playlists: SpotifySearchResultWrapper<SpotifyPlaylist<'a>>,
    #[serde(borrow, default)]
    error: Option<SpotifyError<'a>>,
}

#[derive(Deserialize)]
pub struct SpotifyError<'a> {
    message: &'a str,
}

#[derive(Deserialize)]
pub struct SpotifySearchResultWrapper<T> {
    items: Vec<T>,
}

impl<T> Default for SpotifySearchResultWrapper<T> {
    fn default() -> Self {
        Self { items: Vec::new() }
    }
}

#[allow(dead_code)]
#[derive(Deserialize, Yokeable)]
pub struct SpotifyTrack<'a> {
    #[serde(borrow)]
    album: SpotifyAlbum<'a>,
    #[serde(borrow)]
    artists: Vec<SpotifyArtist<'a>>,
    #[serde(borrow)]
    name: Cow<'a, str>,
    #[serde(borrow)]
    uri: Cow<'a, str>,
}

#[derive(Deserialize, Yokeable)]
#[allow(dead_code)]
pub struct SpotifyAlbum<'a> {
    #[serde(borrow)]
    name: Cow<'a, str>,
    #[serde(borrow, default)]
    images: Vec<SpotifyImage<'a>>,
    #[serde(borrow, default)]
    artists: Vec<SpotifyArtist<'a>>,
    #[serde(borrow)]
    uri: Cow<'a, str>,
}

#[derive(Deserialize, Yokeable)]
pub struct SpotifyPlaylist<'a> {
    #[serde(borrow)]
    name: Cow<'a, str>,
    #[serde(borrow, default)]
    images: Vec<SpotifyImage<'a>>,
    #[serde(borrow)]
    uri: Cow<'a, str>,
}

#[derive(Deserialize)]
pub struct SpotifyArtist<'a> {
    #[serde(borrow)]
    name: Cow<'a, str>,
    #[serde(borrow, default)]
    images: Vec<SpotifyImage<'a>>,
    #[serde(borrow)]
    uri: Cow<'a, str>,
}

#[derive(Deserialize)]
pub struct SpotifyImage<'a> {
    #[serde(borrow)]
    url: Cow<'a, str>,
}