Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use PlaylistStore in GraphQL resolvers

+201 -204
+75 -111
crates/graphql/src/schema/saved_playlist.rs
··· 1 1 use async_graphql::*; 2 - use rockbox_library::entity::track::Track as LibraryTrack; 3 - use rockbox_playlists::{Playlist, PlaylistFolder}; 2 + use rockbox_library::repo; 3 + use rockbox_playlists::PlaylistStore; 4 + use sqlx::{Pool, Sqlite}; 4 5 5 6 use crate::{ 6 7 rockbox_url, ··· 20 21 ctx: &Context<'_>, 21 22 folder_id: Option<String>, 22 23 ) -> Result<Vec<SavedPlaylist>, Error> { 23 - let client = ctx.data::<reqwest::Client>()?; 24 - let mut url = format!("{}/saved-playlists", rockbox_url()); 25 - if let Some(fid) = folder_id.as_deref() { 26 - if !fid.is_empty() { 27 - url = format!("{}?folder_id={}", url, fid); 28 - } 29 - } 30 - let playlists = client 31 - .get(&url) 32 - .send() 33 - .await? 34 - .json::<Vec<Playlist>>() 35 - .await?; 24 + let store = ctx.data::<PlaylistStore>()?; 25 + let playlists = match folder_id.as_deref() { 26 + Some(fid) if !fid.is_empty() => store.list_by_folder(fid).await?, 27 + _ => store.list().await?, 28 + }; 36 29 Ok(playlists.into_iter().map(SavedPlaylist::from).collect()) 37 30 } 38 31 ··· 41 34 ctx: &Context<'_>, 42 35 id: String, 43 36 ) -> Result<Option<SavedPlaylist>, Error> { 44 - let client = ctx.data::<reqwest::Client>()?; 45 - let url = format!("{}/saved-playlists/{}", rockbox_url(), id); 46 - let resp = client.get(&url).send().await?; 47 - if resp.status().as_u16() == 404 { 48 - return Ok(None); 37 + let store = ctx.data::<PlaylistStore>()?; 38 + Ok(store.get(&id).await?.map(SavedPlaylist::from)) 39 + } 40 + 41 + async fn saved_playlist_tracks( 42 + &self, 43 + ctx: &Context<'_>, 44 + playlist_id: String, 45 + ) -> Result<Vec<Track>, Error> { 46 + let store = ctx.data::<PlaylistStore>()?; 47 + let pool = ctx.data::<Pool<Sqlite>>()?; 48 + let track_ids = store.get_track_ids(&playlist_id).await?; 49 + let mut tracks = Vec::with_capacity(track_ids.len()); 50 + for id in &track_ids { 51 + if let Some(t) = repo::track::find(pool.clone(), id).await? { 52 + tracks.push(Track::from(t)); 53 + } 49 54 } 50 - Ok(Some(SavedPlaylist::from(resp.json::<Playlist>().await?))) 55 + Ok(tracks) 51 56 } 52 57 53 58 async fn saved_playlist_track_ids( ··· 55 60 ctx: &Context<'_>, 56 61 playlist_id: String, 57 62 ) -> Result<Vec<String>, Error> { 58 - let client = ctx.data::<reqwest::Client>()?; 59 - let url = format!( 60 - "{}/saved-playlists/{}/track-ids", 61 - rockbox_url(), 62 - playlist_id 63 - ); 64 - Ok(client.get(&url).send().await?.json::<Vec<String>>().await?) 63 + let store = ctx.data::<PlaylistStore>()?; 64 + Ok(store.get_track_ids(&playlist_id).await?) 65 65 } 66 66 67 - async fn saved_playlist_tracks( 67 + async fn playlist_folders( 68 68 &self, 69 69 ctx: &Context<'_>, 70 - playlist_id: String, 71 - ) -> Result<Vec<Track>, Error> { 72 - let client = ctx.data::<reqwest::Client>()?; 73 - let url = format!("{}/saved-playlists/{}/tracks", rockbox_url(), playlist_id); 74 - let tracks = client 75 - .get(&url) 76 - .send() 77 - .await? 78 - .json::<Vec<LibraryTrack>>() 79 - .await?; 80 - Ok(tracks.into_iter().map(Track::from).collect()) 81 - } 82 - 83 - async fn playlist_folders(&self, ctx: &Context<'_>) -> Result<Vec<SavedPlaylistFolder>, Error> { 84 - let client = ctx.data::<reqwest::Client>()?; 85 - let url = format!("{}/saved-playlists/folders", rockbox_url()); 86 - let folders = client 87 - .get(&url) 88 - .send() 89 - .await? 90 - .json::<Vec<PlaylistFolder>>() 91 - .await?; 70 + ) -> Result<Vec<SavedPlaylistFolder>, Error> { 71 + let store = ctx.data::<PlaylistStore>()?; 72 + let folders = store.list_folders().await?; 92 73 Ok(folders.into_iter().map(SavedPlaylistFolder::from).collect()) 93 74 } 94 75 } ··· 103 84 ctx: &Context<'_>, 104 85 name: String, 105 86 ) -> Result<SavedPlaylistFolder, Error> { 106 - let client = ctx.data::<reqwest::Client>()?; 107 - let url = format!("{}/saved-playlists/folders", rockbox_url()); 108 - let folder = client 109 - .post(&url) 110 - .json(&serde_json::json!({ "name": name })) 111 - .send() 112 - .await? 113 - .json::<PlaylistFolder>() 114 - .await?; 87 + let store = ctx.data::<PlaylistStore>()?; 88 + let folder = store.create_folder(&name).await?; 115 89 Ok(SavedPlaylistFolder::from(folder)) 116 90 } 117 91 118 - async fn delete_playlist_folder(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 119 - let client = ctx.data::<reqwest::Client>()?; 120 - let url = format!("{}/saved-playlists/folders/{}", rockbox_url(), id); 121 - client.delete(&url).send().await?; 92 + async fn delete_playlist_folder( 93 + &self, 94 + ctx: &Context<'_>, 95 + id: String, 96 + ) -> Result<bool, Error> { 97 + let store = ctx.data::<PlaylistStore>()?; 98 + store.delete_folder(&id).await?; 122 99 Ok(true) 123 100 } 124 101 ··· 131 108 folder_id: Option<String>, 132 109 track_ids: Option<Vec<String>>, 133 110 ) -> Result<SavedPlaylist, Error> { 134 - let client = ctx.data::<reqwest::Client>()?; 135 - let url = format!("{}/saved-playlists", rockbox_url()); 136 - let playlist = client 137 - .post(&url) 138 - .json(&serde_json::json!({ 139 - "name": name, 140 - "description": description, 141 - "image": image, 142 - "folder_id": folder_id, 143 - "track_ids": track_ids, 144 - })) 145 - .send() 146 - .await? 147 - .json::<Playlist>() 111 + let store = ctx.data::<PlaylistStore>()?; 112 + let playlist = store 113 + .create( 114 + &name, 115 + description.as_deref(), 116 + image.as_deref(), 117 + folder_id.as_deref(), 118 + ) 148 119 .await?; 149 - Ok(SavedPlaylist::from(playlist)) 120 + if let Some(ids) = track_ids.filter(|v| !v.is_empty()) { 121 + store.add_tracks(&playlist.id, &ids).await?; 122 + } 123 + // refetch to get the correct track_count 124 + let updated = store.get(&playlist.id).await?.unwrap_or(playlist); 125 + Ok(SavedPlaylist::from(updated)) 150 126 } 151 127 152 128 async fn update_saved_playlist( ··· 158 134 image: Option<String>, 159 135 folder_id: Option<String>, 160 136 ) -> Result<bool, Error> { 161 - let client = ctx.data::<reqwest::Client>()?; 162 - let url = format!("{}/saved-playlists/{}", rockbox_url(), id); 163 - client 164 - .put(&url) 165 - .json(&serde_json::json!({ 166 - "name": name, 167 - "description": description, 168 - "image": image, 169 - "folder_id": folder_id, 170 - })) 171 - .send() 137 + let store = ctx.data::<PlaylistStore>()?; 138 + store 139 + .update( 140 + &id, 141 + &name, 142 + description.as_deref(), 143 + image.as_deref(), 144 + folder_id.as_deref(), 145 + ) 172 146 .await?; 173 147 Ok(true) 174 148 } 175 149 176 - async fn delete_saved_playlist(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 177 - let client = ctx.data::<reqwest::Client>()?; 178 - let url = format!("{}/saved-playlists/{}", rockbox_url(), id); 179 - client.delete(&url).send().await?; 180 - Ok(true) 150 + async fn delete_saved_playlist( 151 + &self, 152 + ctx: &Context<'_>, 153 + id: String, 154 + ) -> Result<bool, Error> { 155 + let store = ctx.data::<PlaylistStore>()?; 156 + store.delete(&id).await.map_err(|e| async_graphql::Error::new(e.to_string())) 181 157 } 182 158 183 159 async fn add_tracks_to_saved_playlist( ··· 186 162 playlist_id: String, 187 163 track_ids: Vec<String>, 188 164 ) -> Result<bool, Error> { 189 - let client = ctx.data::<reqwest::Client>()?; 190 - let url = format!("{}/saved-playlists/{}/tracks", rockbox_url(), playlist_id); 191 - client 192 - .post(&url) 193 - .json(&serde_json::json!({ "track_ids": track_ids })) 194 - .send() 195 - .await?; 165 + let store = ctx.data::<PlaylistStore>()?; 166 + store.add_tracks(&playlist_id, &track_ids).await?; 196 167 Ok(true) 197 168 } 198 169 ··· 202 173 playlist_id: String, 203 174 track_id: String, 204 175 ) -> Result<bool, Error> { 205 - let client = ctx.data::<reqwest::Client>()?; 206 - let url = format!( 207 - "{}/saved-playlists/{}/tracks/{}", 208 - rockbox_url(), 209 - playlist_id, 210 - track_id 211 - ); 212 - client.delete(&url).send().await?; 213 - Ok(true) 176 + let store = ctx.data::<PlaylistStore>()?; 177 + store.remove_track(&playlist_id, &track_id).await.map_err(|e| async_graphql::Error::new(e.to_string())) 214 178 } 215 179 216 180 async fn play_saved_playlist(
+123 -93
crates/graphql/src/schema/smart_playlist.rs
··· 1 1 use async_graphql::*; 2 - use rockbox_library::entity::track::Track as LibraryTrack; 3 - use rockbox_playlists::SmartPlaylist as RsSmartPlaylist; 2 + use rockbox_library::repo; 3 + use rockbox_playlists::{rules::Candidate, PlaylistStore}; 4 + use sqlx::{Pool, Sqlite}; 5 + use std::collections::HashMap; 4 6 5 7 use crate::{ 6 8 rockbox_url, ··· 10 12 }, 11 13 }; 12 14 15 + /// Resolve the tracks for a smart playlist directly against the SQLite DB, 16 + /// replicating the logic from the server handler without going through HTTP. 17 + async fn resolve_smart_playlist_tracks( 18 + store: &PlaylistStore, 19 + pool: &Pool<Sqlite>, 20 + id: &str, 21 + ) -> Result<Vec<Track>, Error> { 22 + let criteria = match store.get_smart_playlist(id).await? { 23 + Some(p) => p.rules, 24 + None => return Ok(vec![]), 25 + }; 26 + 27 + let all_tracks = repo::track::all(pool.clone()).await?; 28 + 29 + let stats_map: HashMap<String, rockbox_playlists::TrackStats> = store 30 + .get_all_track_stats() 31 + .await? 32 + .into_iter() 33 + .map(|s| (s.track_id.clone(), s)) 34 + .collect(); 35 + 36 + let liked_ids: std::collections::HashSet<String> = 37 + repo::favourites::all_tracks(pool.clone()) 38 + .await? 39 + .into_iter() 40 + .map(|t| t.id) 41 + .collect(); 42 + 43 + let candidates: Vec<Candidate> = all_tracks 44 + .iter() 45 + .map(|t| { 46 + let stats = stats_map.get(&t.id); 47 + Candidate { 48 + id: t.id.clone(), 49 + title: t.title.clone(), 50 + artist: t.artist.clone(), 51 + album: t.album.clone(), 52 + year: t.year.map(|y| y as i64), 53 + genre: t.genre.clone(), 54 + duration_ms: t.length as i64 * 1000, 55 + bitrate: t.bitrate as i64, 56 + date_added_ts: t.created_at.timestamp(), 57 + play_count: stats.map(|s| s.play_count).unwrap_or(0), 58 + skip_count: stats.map(|s| s.skip_count).unwrap_or(0), 59 + last_played: stats.and_then(|s| s.last_played), 60 + last_skipped: stats.and_then(|s| s.last_skipped), 61 + is_liked: liked_ids.contains(&t.id), 62 + } 63 + }) 64 + .collect(); 65 + 66 + let resolved = rockbox_playlists::rules::resolve(&criteria, candidates); 67 + 68 + let track_map: HashMap<&str, &rockbox_library::entity::track::Track> = 69 + all_tracks.iter().map(|t| (t.id.as_str(), t)).collect(); 70 + 71 + Ok(resolved 72 + .iter() 73 + .filter_map(|c| track_map.get(c.id.as_str()).map(|t| Track::from((*t).clone()))) 74 + .collect()) 75 + } 76 + 13 77 #[derive(Default)] 14 78 pub struct SmartPlaylistQuery; 15 79 16 80 #[Object] 17 81 impl SmartPlaylistQuery { 18 82 async fn smart_playlists(&self, ctx: &Context<'_>) -> Result<Vec<SmartPlaylist>, Error> { 19 - let client = ctx.data::<reqwest::Client>()?; 20 - let url = format!("{}/smart-playlists", rockbox_url()); 21 - let playlists = client 22 - .get(&url) 23 - .send() 24 - .await? 25 - .json::<Vec<RsSmartPlaylist>>() 26 - .await?; 83 + let store = ctx.data::<PlaylistStore>()?; 84 + let playlists = store.list_smart_playlists().await?; 27 85 Ok(playlists.into_iter().map(SmartPlaylist::from).collect()) 28 86 } 29 87 ··· 32 90 ctx: &Context<'_>, 33 91 id: String, 34 92 ) -> Result<Option<SmartPlaylist>, Error> { 35 - let client = ctx.data::<reqwest::Client>()?; 36 - let url = format!("{}/smart-playlists/{}", rockbox_url(), id); 37 - let resp = client.get(&url).send().await?; 38 - if resp.status().as_u16() == 404 { 39 - return Ok(None); 40 - } 41 - Ok(Some(SmartPlaylist::from( 42 - resp.json::<RsSmartPlaylist>().await?, 43 - ))) 93 + let store = ctx.data::<PlaylistStore>()?; 94 + Ok(store.get_smart_playlist(&id).await?.map(SmartPlaylist::from)) 44 95 } 45 96 46 - async fn smart_playlist_track_ids( 97 + async fn smart_playlist_tracks( 47 98 &self, 48 99 ctx: &Context<'_>, 49 100 id: String, 50 - ) -> Result<Vec<String>, Error> { 51 - let client = ctx.data::<reqwest::Client>()?; 52 - let url = format!("{}/smart-playlists/{}/tracks", rockbox_url(), id); 53 - let tracks = client 54 - .get(&url) 55 - .send() 56 - .await? 57 - .json::<Vec<serde_json::Value>>() 58 - .await?; 59 - Ok(tracks 60 - .into_iter() 61 - .filter_map(|t| t.get("id").and_then(|v| v.as_str()).map(|s| s.to_string())) 62 - .collect()) 101 + ) -> Result<Vec<Track>, Error> { 102 + let store = ctx.data::<PlaylistStore>()?; 103 + let pool = ctx.data::<Pool<Sqlite>>()?; 104 + resolve_smart_playlist_tracks(store, pool, &id).await 63 105 } 64 106 65 - async fn smart_playlist_tracks( 107 + async fn smart_playlist_track_ids( 66 108 &self, 67 109 ctx: &Context<'_>, 68 110 id: String, 69 - ) -> Result<Vec<Track>, Error> { 70 - let client = ctx.data::<reqwest::Client>()?; 71 - let url = format!("{}/smart-playlists/{}/tracks", rockbox_url(), id); 72 - let tracks = client 73 - .get(&url) 74 - .send() 75 - .await? 76 - .json::<Vec<LibraryTrack>>() 77 - .await?; 78 - Ok(tracks.into_iter().map(Track::from).collect()) 111 + ) -> Result<Vec<String>, Error> { 112 + let store = ctx.data::<PlaylistStore>()?; 113 + let pool = ctx.data::<Pool<Sqlite>>()?; 114 + let tracks = resolve_smart_playlist_tracks(store, pool, &id).await?; 115 + Ok(tracks.into_iter().filter_map(|t| t.id).collect()) 79 116 } 80 117 81 118 async fn track_stats( ··· 83 120 ctx: &Context<'_>, 84 121 track_id: String, 85 122 ) -> Result<Option<TrackStats>, Error> { 86 - let client = ctx.data::<reqwest::Client>()?; 87 - let url = format!("{}/track-stats/{}", rockbox_url(), track_id); 88 - let resp = client.get(&url).send().await?; 89 - if resp.status().as_u16() == 404 { 90 - return Ok(None); 91 - } 92 - Ok(Some(TrackStats::from( 93 - resp.json::<rockbox_playlists::TrackStats>().await?, 94 - ))) 123 + let store = ctx.data::<PlaylistStore>()?; 124 + Ok(store 125 + .get_track_stats(&track_id) 126 + .await? 127 + .map(TrackStats::from)) 95 128 } 96 129 } 97 130 ··· 109 142 folder_id: Option<String>, 110 143 rules: String, 111 144 ) -> Result<SmartPlaylist, Error> { 112 - let client = ctx.data::<reqwest::Client>()?; 113 - let rules_val: serde_json::Value = serde_json::from_str(&rules)?; 114 - let url = format!("{}/smart-playlists", rockbox_url()); 115 - let playlist = client 116 - .post(&url) 117 - .json(&serde_json::json!({ 118 - "name": name, 119 - "description": description, 120 - "image": image, 121 - "folder_id": folder_id, 122 - "rules": rules_val, 123 - })) 124 - .send() 125 - .await? 126 - .json::<RsSmartPlaylist>() 145 + let store = ctx.data::<PlaylistStore>()?; 146 + let criteria: rockbox_playlists::rules::RuleCriteria = serde_json::from_str(&rules)?; 147 + let playlist = store 148 + .create_smart_playlist( 149 + &name, 150 + description.as_deref(), 151 + image.as_deref(), 152 + folder_id.as_deref(), 153 + &criteria, 154 + ) 127 155 .await?; 128 156 Ok(SmartPlaylist::from(playlist)) 129 157 } ··· 138 166 folder_id: Option<String>, 139 167 rules: String, 140 168 ) -> Result<bool, Error> { 141 - let client = ctx.data::<reqwest::Client>()?; 142 - let rules_val: serde_json::Value = serde_json::from_str(&rules)?; 143 - let url = format!("{}/smart-playlists/{}", rockbox_url(), id); 144 - client 145 - .put(&url) 146 - .json(&serde_json::json!({ 147 - "name": name, 148 - "description": description, 149 - "image": image, 150 - "folder_id": folder_id, 151 - "rules": rules_val, 152 - })) 153 - .send() 169 + let store = ctx.data::<PlaylistStore>()?; 170 + let criteria: rockbox_playlists::rules::RuleCriteria = serde_json::from_str(&rules)?; 171 + store 172 + .update_smart_playlist( 173 + &id, 174 + &name, 175 + description.as_deref(), 176 + image.as_deref(), 177 + folder_id.as_deref(), 178 + &criteria, 179 + ) 154 180 .await?; 155 181 Ok(true) 156 182 } 157 183 158 - async fn delete_smart_playlist(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 159 - let client = ctx.data::<reqwest::Client>()?; 160 - let url = format!("{}/smart-playlists/{}", rockbox_url(), id); 161 - client.delete(&url).send().await?; 162 - Ok(true) 184 + async fn delete_smart_playlist( 185 + &self, 186 + ctx: &Context<'_>, 187 + id: String, 188 + ) -> Result<bool, Error> { 189 + let store = ctx.data::<PlaylistStore>()?; 190 + store.delete_smart_playlist(&id).await.map_err(|e| async_graphql::Error::new(e.to_string())) 163 191 } 164 192 165 - async fn play_smart_playlist(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 193 + async fn play_smart_playlist( 194 + &self, 195 + ctx: &Context<'_>, 196 + id: String, 197 + ) -> Result<bool, Error> { 166 198 let client = ctx.data::<reqwest::Client>()?; 167 199 let url = format!("{}/smart-playlists/{}/play", rockbox_url(), id); 168 200 client.post(&url).send().await?; ··· 174 206 ctx: &Context<'_>, 175 207 track_id: String, 176 208 ) -> Result<bool, Error> { 177 - let client = ctx.data::<reqwest::Client>()?; 178 - let url = format!("{}/track-stats/{}/played", rockbox_url(), track_id); 179 - client.post(&url).send().await?; 209 + let store = ctx.data::<PlaylistStore>()?; 210 + store.record_play(&track_id).await?; 180 211 Ok(true) 181 212 } 182 213 ··· 185 216 ctx: &Context<'_>, 186 217 track_id: String, 187 218 ) -> Result<bool, Error> { 188 - let client = ctx.data::<reqwest::Client>()?; 189 - let url = format!("{}/track-stats/{}/skipped", rockbox_url(), track_id); 190 - client.post(&url).send().await?; 219 + let store = ctx.data::<PlaylistStore>()?; 220 + store.record_skip(&track_id).await?; 191 221 Ok(true) 192 222 } 193 223 }
+3
crates/graphql/src/server.rs
··· 16 16 use async_graphql::{http::GraphiQLSource, Schema}; 17 17 use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; 18 18 use rockbox_library::{create_connection_pool, repo}; 19 + use rockbox_playlists::PlaylistStore; 19 20 use rockbox_sys::events::RockboxCommand; 20 21 use rockbox_webui::{dist, index, index_spa}; 21 22 use sqlx::{Pool, Sqlite}; ··· 90 91 pub async fn start(cmd_tx: Arc<Mutex<Sender<RockboxCommand>>>) -> Result<(), Error> { 91 92 let client = reqwest::Client::new(); 92 93 let pool = create_connection_pool().await?; 94 + let playlist_store = PlaylistStore::new(pool.clone()); 93 95 94 96 let schema = Schema::build( 95 97 Query::default(), ··· 99 101 .data(cmd_tx) 100 102 .data(client) 101 103 .data(pool.clone()) 104 + .data(playlist_store) 102 105 .finish(); 103 106 104 107 let graphql_port = std::env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or("6062".to_string());