From a82e1a87ba3b97404558db86765a016226c0b178 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 11 Jan 2024 03:20:28 +0000 Subject: [PATCH] Implement searching for music via Spotify --- config-example.toml | 3 +++ shalom/src/config.rs | 11 +++++++++-- shalom/src/hass_client.rs | 28 ++++++++++++++++++++++++++++ shalom/src/main.rs | 21 +++++++++++++++++---- shalom/src/oracle.rs | 18 +++++++++++++++--- shalom/src/pages/room.rs | 5 +++-- shalom/src/pages/room/listen.rs | 383 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------- shalom/src/pages/room/listen/search.rs | 46 +++++++++++++++++++++++++++++++++++----------- shalom/src/subscriptions.rs | 62 ++++++++++++++++++++++++++++++++++++-------------------------- 9 files changed, 454 insertions(+), 123 deletions(-) diff --git a/config-example.toml b/config-example.toml index 135142d..5fae77d 100644 --- a/config-example.toml +++ b/config-example.toml @@ -1,3 +1,6 @@ [home-assistant] uri = "http://192.168.1.100/" token = "abcdef" + +[spotify] +token = "abcdef" diff --git a/shalom/src/config.rs b/shalom/src/config.rs index 3fae16e..be2dadc 100644 --- a/shalom/src/config.rs +++ b/shalom/src/config.rs @@ -8,13 +8,20 @@ pub const LAST_FM_API_KEY: &str = "732433605ea7893c761d340a05752695"; pub const LAST_FM_SHARED_SECRET: &str = "420fdb301e6b4a62a888bf51def71670"; pub const FANART_PROJECT_KEY: &str = "df5eb171c6e0e49122ad59830cdf789f"; -#[derive(Deserialize)] +#[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct Config { pub home_assistant: HomeAssistantConfig, + pub spotify: SpotifyConfig, } -#[derive(Deserialize)] +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct SpotifyConfig { + pub token: String, +} + +#[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct HomeAssistantConfig { pub uri: String, diff --git a/shalom/src/hass_client.rs b/shalom/src/hass_client.rs index c12a01f..7df8b0b 100644 --- a/shalom/src/hass_client.rs +++ b/shalom/src/hass_client.rs @@ -295,6 +295,7 @@ pub enum CallServiceRequestMediaPlayer { MediaSeek(CallServiceRequestMediaPlayerMediaSeek), ShuffleSet(CallServiceRequestMediaPlayerShuffleSet), RepeatSet(CallServiceRequestMediaPlayerRepeatSet), + PlayMedia(CallServiceRequestMediaPlayerPlayMedia), MediaPlay, MediaPause, MediaNextTrack, @@ -328,6 +329,33 @@ pub struct CallServiceRequestMediaPlayerRepeatSet { pub repeat: MediaPlayerRepeat, } +#[derive(Serialize)] +pub struct CallServiceRequestMediaPlayerPlayMedia { + pub media_content_id: String, + pub media_content_type: CallServiceRequestMediaPlayerPlayMediaType, + pub enqueue: CallServiceRequestMediaPlayerPlayMediaEnqueue, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CallServiceRequestMediaPlayerPlayMediaType { + Music, + Tvshow, + Video, + Episode, + Channel, + Playlist, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CallServiceRequestMediaPlayerPlayMediaEnqueue { + Add, + Next, + Play, + Replace, +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MediaPlayerRepeat { diff --git a/shalom/src/main.rs b/shalom/src/main.rs index 46b7a4e..98bb1d4 100644 --- a/shalom/src/main.rs +++ b/shalom/src/main.rs @@ -26,6 +26,7 @@ pub struct Shalom { oracle: Option>, home_room: Option<&'static str>, theme: Theme, + config: Option>, } impl Shalom { @@ -40,6 +41,7 @@ impl Shalom { ActivePage::Room(pages::room::Room::new( room, self.oracle.as_ref().unwrap().clone(), + self.config.as_ref().unwrap().clone(), )) } @@ -160,6 +162,14 @@ impl Shalom { Message::UpdateLightResult, ) } + pages::room::listen::Event::PlayTrack(id, uri) => { + let oracle = self.oracle.as_ref().unwrap().clone(); + + Command::perform( + async move { oracle.speaker(id).play_track(uri).await }, + Message::PlayTrackResult, + ) + } } } } @@ -177,6 +187,7 @@ impl Application for Shalom { oracle: None, home_room: Some("living_room"), theme: Theme::default(), + config: None, }; // this is only best-effort to try and prevent blocking when loading @@ -186,8 +197,8 @@ impl Application for Shalom { let command = Command::perform( async { let config = load_config().await; - let client = hass_client::create(config.home_assistant).await; - Oracle::new(client.clone()).await + let client = hass_client::create(config.home_assistant.clone()).await; + (Oracle::new(client.clone()).await, config) }, Message::Loaded, ); @@ -203,8 +214,9 @@ impl Application for Shalom { fn update(&mut self, message: Self::Message) -> Command { #[allow(clippy::single_match)] match (message, &mut self.page, &mut self.context_menu) { - (Message::Loaded(oracle), _, _) => { + (Message::Loaded((oracle, config)), _, _) => { self.oracle = Some(oracle); + self.config = Some(Arc::new(config)); self.page = self.build_home_route(); Command::none() } @@ -288,7 +300,7 @@ async fn load_config() -> Config { #[derive(Debug, Clone)] pub enum Message { - Loaded(Arc), + Loaded((Arc, Config)), CloseContextMenu, OpenOmniPage, OpenHomePage, @@ -296,6 +308,7 @@ pub enum Message { RoomEvent(pages::room::Message), LightControlMenu(context_menus::light_control::Message), UpdateLightResult(()), + PlayTrackResult(()), } #[derive(Debug)] diff --git a/shalom/src/oracle.rs b/shalom/src/oracle.rs index 3ee40ab..32c079f 100644 --- a/shalom/src/oracle.rs +++ b/shalom/src/oracle.rs @@ -30,9 +30,10 @@ use crate::{ }, CallServiceRequestData, CallServiceRequestLight, CallServiceRequestLightTurnOn, CallServiceRequestMediaPlayer, CallServiceRequestMediaPlayerMediaSeek, - CallServiceRequestMediaPlayerRepeatSet, CallServiceRequestMediaPlayerShuffleSet, - CallServiceRequestMediaPlayerVolumeMute, CallServiceRequestMediaPlayerVolumeSet, Event, - HassRequestKind, MediaPlayerRepeat, + CallServiceRequestMediaPlayerPlayMedia, CallServiceRequestMediaPlayerPlayMediaEnqueue, + CallServiceRequestMediaPlayerPlayMediaType, CallServiceRequestMediaPlayerRepeatSet, + CallServiceRequestMediaPlayerShuffleSet, CallServiceRequestMediaPlayerVolumeMute, + CallServiceRequestMediaPlayerVolumeSet, Event, HassRequestKind, MediaPlayerRepeat, }, widgets::colour_picker::clamp_to_u8, }; @@ -460,6 +461,17 @@ impl EloquentSpeaker<'_> { self.call(CallServiceRequestMediaPlayer::MediaPreviousTrack) .await; } + + pub async fn play_track(&self, uri: String) { + self.call(CallServiceRequestMediaPlayer::PlayMedia( + CallServiceRequestMediaPlayerPlayMedia { + media_content_id: uri, + media_content_type: CallServiceRequestMediaPlayerPlayMediaType::Music, + enqueue: CallServiceRequestMediaPlayerPlayMediaEnqueue::Play, + }, + )) + .await; + } } fn build_room( diff --git a/shalom/src/pages/room.rs b/shalom/src/pages/room.rs index bb8fafc..77f3fec 100644 --- a/shalom/src/pages/room.rs +++ b/shalom/src/pages/room.rs @@ -12,6 +12,7 @@ use iced::{ }; use crate::{ + config::Config, oracle::Oracle, subscriptions::MaybePendingImage, widgets::{ @@ -30,12 +31,12 @@ pub struct Room { } impl Room { - pub fn new(id: &'static str, oracle: Arc) -> Self { + pub fn new(id: &'static str, oracle: Arc, config: Arc) -> Self { let room = oracle.room(id).clone(); Self { id, - listen: listen::Listen::new(oracle.clone(), &room), + listen: listen::Listen::new(oracle.clone(), &room, config), lights: lights::Lights::new(oracle, &room), room, current_page: Page::Listen, diff --git a/shalom/src/pages/room/listen.rs b/shalom/src/pages/room/listen.rs index 84a7124..471fdc4 100644 --- a/shalom/src/pages/room/listen.rs +++ b/shalom/src/pages/room/listen.rs @@ -1,22 +1,28 @@ mod search; -use std::{convert::identity, sync::Arc, time::Duration}; +use std::{borrow::Cow, convert::identity, sync::Arc, time::Duration}; use iced::{ - futures::StreamExt, + futures::{future, stream, stream::FuturesUnordered, FutureExt, StreamExt}, subscription, widget::{container, image::Handle, lazy, 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, MaybePendingImage}, - theme::{darken_image, trim_transparent_padding, Image}, + subscriptions::{ + download_image, find_fanart_urls, find_musicbrainz_artist, load_image, MaybePendingImage, + }, + theme::{darken_image, trim_transparent_padding}, widgets, }; @@ -29,12 +35,12 @@ pub struct Listen { musicbrainz_artist_id: Option, pub background: Option, artist_logo: Option, - search_query: String, - search_open: bool, + search: SearchState, + config: Arc, } impl Listen { - pub fn new(oracle: Arc, room: &Room) -> Self { + pub fn new(oracle: Arc, room: &Room, config: Arc) -> Self { let speaker = room.speaker(&oracle); Self { @@ -45,27 +51,31 @@ impl Listen { musicbrainz_artist_id: None, background: None, artist_logo: None, - search_query: String::new(), - search_open: false, + search: SearchState::Closed, + config, } } pub fn header_magic(&self, text: Text<'static>) -> Element<'static, Message> { - lazy( - (self.search_open, self.search_query.clone()), - move |(open, query)| { - header_search( - Message::OnSearchTerm, - Message::OnSearchVisibleChange, - *open, - query, - text.clone(), - ) - }, - ) + lazy(self.search.clone(), move |search| { + let (open, query) = if let Some(v) = search.search() { + (true, v) + } else { + (false, "") + }; + + header_search( + Message::OnSearchTerm, + Message::OnSearchVisibleChange, + open, + query, + text.clone(), + ) + }) .into() } + #[allow(clippy::too_many_lines)] pub fn update(&mut self, event: Message) -> Option { match event { Message::AlbumArtImageLoaded(handle) => { @@ -162,68 +172,62 @@ impl Listen { None } Message::OnSearchTerm(v) => { - self.search_query = v; + self.search = self.search.open(v); None } Message::OnSearchVisibleChange(v) => { - self.search_open = v; - self.search_query = String::new(); + self.search = if v { + SearchState::Open { + search: String::new(), + results: vec![], + needs_result: false, + waiting_for_result: false, + } + } else { + SearchState::Closed + }; + None + } + Message::SpotifySearchResult(res) => { + if let SearchState::Open { + results, + needs_result, + .. + } = &mut self.search + { + if *needs_result { + results.clear(); + *needs_result = false; + } + + results.push(res); + } + + None + } + Message::SpotifySearchResultDone => { + if let SearchState::Open { + waiting_for_result, .. + } = &mut self.search + { + *waiting_for_result = false; + } + 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_open && !self.search_query.is_empty() { - let results = vec![ - SearchResult::album(Image::AlbumArtTest.into(), "Some Album".to_string()), - SearchResult::track( - Image::AlbumArtTest.into(), - "Some Track".to_string(), - "Some Artist".to_string(), - ), - SearchResult::playlist(Image::AlbumArtTest.into(), "Some Playlist".to_string()), - SearchResult::album(Image::AlbumArtTest.into(), "Some Album".to_string()), - SearchResult::track( - Image::AlbumArtTest.into(), - "Some Track".to_string(), - "Some Artist".to_string(), - ), - SearchResult::playlist(Image::AlbumArtTest.into(), "Some Playlist".to_string()), - SearchResult::album(Image::AlbumArtTest.into(), "Some Album".to_string()), - SearchResult::track( - Image::AlbumArtTest.into(), - "Some Track".to_string(), - "Some Artist".to_string(), - ), - SearchResult::playlist(Image::AlbumArtTest.into(), "Some Playlist".to_string()), - SearchResult::album(Image::AlbumArtTest.into(), "Some Album".to_string()), - SearchResult::track( - Image::AlbumArtTest.into(), - "Some Track".to_string(), - "Some Artist".to_string(), - ), - SearchResult::playlist(Image::AlbumArtTest.into(), "Some Playlist".to_string()), - SearchResult::album(Image::AlbumArtTest.into(), "Some Album".to_string()), - SearchResult::track( - Image::AlbumArtTest.into(), - "Some Track".to_string(), - "Some Artist".to_string(), - ), - SearchResult::playlist(Image::AlbumArtTest.into(), "Some Playlist".to_string()), - SearchResult::album(Image::AlbumArtTest.into(), "Some Album".to_string()), - SearchResult::track( - Image::AlbumArtTest.into(), - "Some Track".to_string(), - "Some Artist".to_string(), - ), - SearchResult::playlist(Image::AlbumArtTest.into(), "Some Playlist".to_string()), - ]; - - container(search::search(style.clone(), results)) - .padding([0, 40, 40, 40]) - .width(Length::Fill) - .into() + 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()) @@ -313,6 +317,17 @@ impl Listen { Subscription::none() }; + let spotify_result = if let SearchState::Open { + search, + waiting_for_result: true, + .. + } = &self.search + { + search_spotify(search.to_string(), &self.config.spotify.token) + } else { + Subscription::none() + }; + Subscription::batch([ album_art_subscription, speaker_subscription, @@ -320,11 +335,60 @@ impl Listen { background_subscription, logo_subscription, fanart_subscription, + spotify_result, ]) } } -#[derive(Copy, Clone)] +#[derive(Debug, Hash, Clone)] +pub enum SearchState { + Open { + search: String, + results: Vec, + needs_result: bool, + waiting_for_result: bool, + }, + Closed, +} + +impl SearchState { + pub fn is_open(&self) -> bool { + matches!(self, Self::Open { search, .. } if !search.is_empty()) + } + + pub fn results(&self) -> &[SearchResult] { + match self { + Self::Open { results, .. } => results.as_slice(), + Self::Closed => &[], + } + } + + pub fn search(&self) -> Option<&str> { + match self { + SearchState::Open { search, .. } => Some(search), + SearchState::Closed => None, + } + } + + pub fn open(&self, search: String) -> SearchState { + match self { + Self::Open { results, .. } => Self::Open { + search, + results: results.clone(), + needs_result: true, + waiting_for_result: true, + }, + Self::Closed => Self::Open { + search, + results: vec![], + needs_result: true, + waiting_for_result: true, + }, + } + } +} + +#[derive(Clone)] pub enum Event { SetSpeakerVolume(&'static str, f32), SetSpeakerPosition(&'static str, Duration), @@ -334,6 +398,7 @@ pub enum Event { SetSpeakerRepeat(&'static str, MediaPlayerRepeat), SpeakerNextTrack(&'static str), SpeakerPreviousTrack(&'static str), + PlayTrack(&'static str, String), } #[derive(Clone, Debug)] @@ -354,4 +419,172 @@ pub enum Message { OnSpeakerPreviousTrack, OnSearchTerm(String), OnSearchVisibleChange(bool), + SpotifySearchResult(SearchResult), + SpotifySearchResultDone, + OnPlayTrack(String), +} + +fn search_spotify(search: String, token: &str) -> Subscription { + let token = token.to_string(); + + subscription::run_with_id( + format!("search-{search}"), + stream::once(async move { + 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!("{}", std::str::from_utf8(res.as_ref()).unwrap()); + + Yoke::attach_to_cart(res, |s| serde_json::from_str(s).unwrap()) + }) + .flat_map(|res: Yoke, String>| { + let res = res.get(); + let results = FuturesUnordered::new(); + + for track in &res.tracks.items { + let image_url = track.album.images[0].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( + async move { + let image = load_image(image_url, identity).await; + SearchResult::track(image, track_name, artist_name, uri) + } + .boxed(), + ); + } + + for artist in &res.artists.items { + let image_url = artist.images[0].url.to_string(); + let artist_name = artist.name.to_string(); + let uri = artist.uri.to_string(); + + results.push( + async move { + let image = load_image(image_url, identity).await; + SearchResult::artist(image, artist_name, uri) + } + .boxed(), + ); + } + + for albums in &res.albums.items { + let image_url = albums.images[0].url.to_string(); + let album_name = albums.name.to_string(); + let uri = albums.uri.to_string(); + + results.push( + async move { + let image = load_image(image_url, identity).await; + SearchResult::album(image, album_name, uri) + } + .boxed(), + ); + } + + for playlist in &res.playlists.items { + let image_url = playlist.images[0].url.to_string(); + let playlist_name = playlist.name.to_string(); + let uri = playlist.uri.to_string(); + + results.push( + async move { + let image = load_image(image_url, identity).await; + SearchResult::playlist(image, playlist_name, uri) + } + .boxed(), + ); + } + + results.map(Message::SpotifySearchResult) + }) + .chain(stream::once(future::ready( + Message::SpotifySearchResultDone, + ))), + ) +} + +#[derive(Deserialize, Yokeable)] +pub struct SpotifySearchResult<'a> { + #[serde(borrow)] + tracks: SpotifySearchResultWrapper>, + #[serde(borrow)] + artists: SpotifySearchResultWrapper>, + #[serde(borrow)] + albums: SpotifySearchResultWrapper>, + #[serde(borrow)] + playlists: SpotifySearchResultWrapper>, +} + +#[derive(Deserialize)] +pub struct SpotifySearchResultWrapper { + items: Vec, +} + +#[allow(dead_code)] +#[derive(Deserialize, Yokeable)] +pub struct SpotifyTrack<'a> { + #[serde(borrow)] + album: SpotifyAlbum<'a>, + #[serde(borrow)] + artists: Vec>, + #[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>, + #[serde(borrow, default)] + artists: Vec>, + #[serde(borrow)] + uri: Cow<'a, str>, +} + +#[derive(Deserialize, Yokeable)] +pub struct SpotifyPlaylist<'a> { + #[serde(borrow)] + name: Cow<'a, str>, + #[serde(borrow, default)] + images: Vec>, + #[serde(borrow)] + uri: Cow<'a, str>, +} + +#[derive(Deserialize)] +pub struct SpotifyArtist<'a> { + #[serde(borrow)] + name: Cow<'a, str>, + #[serde(borrow, default)] + images: Vec>, + #[serde(borrow)] + uri: Cow<'a, str>, +} + +#[derive(Deserialize)] +pub struct SpotifyImage<'a> { + #[serde(borrow)] + url: Cow<'a, str>, } diff --git a/shalom/src/pages/room/listen/search.rs b/shalom/src/pages/room/listen/search.rs index 486970b..5a41efd 100644 --- a/shalom/src/pages/room/listen/search.rs +++ b/shalom/src/pages/room/listen/search.rs @@ -11,7 +11,7 @@ use iced::{ use crate::widgets::mouse_area::mouse_area; -pub fn search(theme: Theme, results: Vec) -> Search { +pub fn search(theme: Theme, results: &[SearchResult]) -> Search<'_, M> { Search { on_track_press: None, theme, @@ -19,13 +19,20 @@ pub fn search(theme: Theme, results: Vec) -> S } } -pub struct Search { +pub struct Search<'a, M> { on_track_press: Option M>, theme: Theme, - results: Vec, + results: &'a [SearchResult], } -impl Component for Search { +impl Search<'_, M> { + pub fn on_track_press(mut self, f: fn(String) -> M) -> Self { + self.on_track_press = Some(f); + self + } +} + +impl Component for Search<'_, M> { type State = (); type Event = Event; @@ -44,7 +51,7 @@ impl Component for Search { } let track = mouse_area(search_item_container(result_card(result, &self.theme))) - .on_press(Event::OnTrackPress("hello world".to_string())); + .on_press(Event::OnTrackPress(result.uri.to_string())); col = col.push(track); } @@ -53,13 +60,13 @@ impl Component for Search { } } -impl From> for Element<'static, M, Renderer> { - fn from(value: Search) -> Self { +impl<'a, M: 'static + Clone> From> for Element<'a, M, Renderer> { + fn from(value: Search<'a, M>) -> Self { component(value) } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Event { OnTrackPress(String), } @@ -118,42 +125,58 @@ impl container::StyleSheet for SearchContainer { } #[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, Hash)] pub struct SearchResult { image: Handle, title: String, + uri: String, metadata: ResultMetadata, } impl SearchResult { - pub fn track(image: Handle, title: String, artist: String) -> Self { + pub fn track(image: Handle, title: String, artist: String, uri: String) -> Self { Self { image, title, + uri, metadata: ResultMetadata::Track(artist), } } - pub fn playlist(image: Handle, title: String) -> Self { + pub fn playlist(image: Handle, title: String, uri: String) -> Self { Self { image, title, + uri, metadata: ResultMetadata::Playlist, } } - pub fn album(image: Handle, title: String) -> Self { + pub fn artist(image: Handle, title: String, uri: String) -> Self { + Self { + image, + title, + uri, + metadata: ResultMetadata::Artist, + } + } + + pub fn album(image: Handle, title: String, uri: String) -> Self { Self { image, title, + uri, metadata: ResultMetadata::Album, } } } +#[derive(Debug, Clone, Hash)] pub enum ResultMetadata { Track(String), Playlist, Album, + Artist, } impl Display for ResultMetadata { @@ -162,6 +185,7 @@ impl Display for ResultMetadata { ResultMetadata::Track(v) => write!(f, "Track • {v}"), ResultMetadata::Playlist => write!(f, "Playlist"), ResultMetadata::Album => write!(f, "Album"), + ResultMetadata::Artist => write!(f, "Artist"), } } } diff --git a/shalom/src/subscriptions.rs b/shalom/src/subscriptions.rs index 222710f..60c442a 100644 --- a/shalom/src/subscriptions.rs +++ b/shalom/src/subscriptions.rs @@ -4,6 +4,7 @@ use iced::{futures::stream, subscription, widget::image, Subscription}; use lru::LruCache; use once_cell::sync::Lazy; use parking_lot::Mutex; +use reqwest::IntoUrl; use url::Url; use crate::config::FANART_PROJECT_KEY; @@ -28,41 +29,50 @@ pub fn download_image( post_process: fn(::image::RgbaImage) -> ::image::RgbaImage, resp: impl FnOnce(image::Handle) -> M + Send + 'static, ) -> Subscription { - static CACHE: Lazy>> = - Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(10).unwrap()))); - subscription::run_with_id( url.to_string(), stream::once(async move { eprintln!("{url} dl"); - if let Some(handle) = CACHE.lock().get(&url) { - return (resp)(handle.clone()); - } + (resp)(load_image(url.clone(), post_process).await) + }), + ) +} - let bytes = reqwest::get(url.clone()) - .await - .unwrap() - .bytes() - .await - .unwrap(); +pub async fn load_image( + url: T, + post_process: fn(::image::RgbaImage) -> ::image::RgbaImage, +) -> image::Handle { + static CACHE: Lazy>> = + Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(50).unwrap()))); - let handle = tokio::task::spawn_blocking(move || { - eprintln!("parsing image"); - let img = ::image::load_from_memory(&bytes).unwrap(); - eprintln!("post processing"); - let data = post_process(img.into_rgba8()); - let (h, w) = data.dimensions(); - image::Handle::from_pixels(h, w, data.into_raw()) - }) - .await - .unwrap(); + let url = url.into_url().unwrap(); - CACHE.lock().push(url.clone(), handle.clone()); + if let Some(handle) = CACHE.lock().get(&url) { + return handle.clone(); + } - (resp)(handle) - }), - ) + let bytes = reqwest::get(url.clone()) + .await + .unwrap() + .bytes() + .await + .unwrap(); + + let handle = tokio::task::spawn_blocking(move || { + eprintln!("parsing image"); + let img = ::image::load_from_memory(&bytes).unwrap(); + eprintln!("post processing"); + let data = post_process(img.into_rgba8()); + let (h, w) = data.dimensions(); + image::Handle::from_pixels(h, w, data.into_raw()) + }) + .await + .unwrap(); + + CACHE.lock().push(url.clone(), handle.clone()); + + handle } pub fn find_musicbrainz_artist( -- libgit2 1.7.2