use actix_web::{HttpRequest, HttpResponse, web}; use shanty_db::queries; use crate::state::AppState; use super::helpers::{authenticate, get_query_param, parse_subsonic_id}; use super::response::{self, SubsonicChild}; /// GET /rest/getPlaylists[.view] pub async fn get_playlists(req: HttpRequest, state: web::Data) -> HttpResponse { let (params, user) = match authenticate(&req, &state).await { Ok(v) => v, Err(resp) => return resp, }; let playlists = queries::playlists::list(state.db.conn(), Some(user.id)) .await .unwrap_or_default(); let mut playlist_list: Vec = Vec::new(); for pl in &playlists { let track_count = queries::playlists::get_track_count(state.db.conn(), pl.id) .await .unwrap_or(0); // Calculate total duration let tracks = queries::playlists::get_tracks(state.db.conn(), pl.id) .await .unwrap_or_default(); let duration: i32 = tracks .iter() .filter_map(|t| t.duration.map(|d| d as i32)) .sum(); let mut pl_json = serde_json::json!({ "id": format!("pl-{}", pl.id), "name": pl.name, "owner": user.username, "public": false, "songCount": track_count, "duration": duration, "created": pl.created_at.format("%Y-%m-%dT%H:%M:%S").to_string(), "changed": pl.updated_at.format("%Y-%m-%dT%H:%M:%S").to_string(), }); if let Some(ref desc) = pl.description { pl_json["comment"] = serde_json::json!(desc); } playlist_list.push(pl_json); } response::ok( ¶ms.format, serde_json::json!({ "playlists": { "playlist": playlist_list, } }), ) } /// GET /rest/getPlaylist[.view] pub async fn get_playlist(req: HttpRequest, state: web::Data) -> HttpResponse { let (params, user) = match authenticate(&req, &state).await { Ok(v) => v, Err(resp) => return resp, }; let id_str = match get_query_param(&req, "id") { Some(id) => id, None => { return response::error( ¶ms.format, response::ERROR_MISSING_PARAM, "missing required parameter: id", ); } }; let (_prefix, playlist_id) = match parse_subsonic_id(&id_str) { Some(v) => v, None => { return response::error( ¶ms.format, response::ERROR_NOT_FOUND, "invalid playlist id", ); } }; let playlist = match queries::playlists::get_by_id(state.db.conn(), playlist_id).await { Ok(p) => p, Err(_) => { return response::error( ¶ms.format, response::ERROR_NOT_FOUND, "playlist not found", ); } }; let tracks = queries::playlists::get_tracks(state.db.conn(), playlist_id) .await .unwrap_or_default(); let duration: i32 = tracks .iter() .filter_map(|t| t.duration.map(|d| d as i32)) .sum(); let entry_list: Vec = tracks .iter() .map(|track| serde_json::to_value(SubsonicChild::from_track(track)).unwrap_or_default()) .collect(); let mut pl_json = serde_json::json!({ "id": format!("pl-{}", playlist.id), "name": playlist.name, "owner": user.username, "public": false, "songCount": tracks.len(), "duration": duration, "created": playlist.created_at.format("%Y-%m-%dT%H:%M:%S").to_string(), "changed": playlist.updated_at.format("%Y-%m-%dT%H:%M:%S").to_string(), "entry": entry_list, }); if let Some(ref desc) = playlist.description { pl_json["comment"] = serde_json::json!(desc); } response::ok( ¶ms.format, serde_json::json!({ "playlist": pl_json, }), ) } /// GET /rest/createPlaylist[.view] pub async fn create_playlist(req: HttpRequest, state: web::Data) -> HttpResponse { let (params, user) = match authenticate(&req, &state).await { Ok(v) => v, Err(resp) => return resp, }; let name = match get_query_param(&req, "name") { Some(n) => n, None => { return response::error( ¶ms.format, response::ERROR_MISSING_PARAM, "missing required parameter: name", ); } }; // Collect songId params (can be repeated) let qs = req.query_string(); let query_params: Vec<(String, String)> = serde_urlencoded::from_str(qs).unwrap_or_default(); let track_ids: Vec = query_params .iter() .filter(|(k, _)| k == "songId") .filter_map(|(_, v)| parse_subsonic_id(v).map(|(_, id)| id)) .collect(); match queries::playlists::create(state.db.conn(), &name, None, Some(user.id), &track_ids).await { Ok(playlist) => { let tracks = queries::playlists::get_tracks(state.db.conn(), playlist.id) .await .unwrap_or_default(); let duration: i32 = tracks .iter() .filter_map(|t| t.duration.map(|d| d as i32)) .sum(); let entry_list: Vec = tracks .iter() .map(|track| { serde_json::to_value(SubsonicChild::from_track(track)).unwrap_or_default() }) .collect(); let mut pl_json = serde_json::json!({ "id": format!("pl-{}", playlist.id), "name": playlist.name, "owner": user.username, "public": false, "songCount": tracks.len(), "duration": duration, "created": playlist.created_at.format("%Y-%m-%dT%H:%M:%S").to_string(), "changed": playlist.updated_at.format("%Y-%m-%dT%H:%M:%S").to_string(), "entry": entry_list, }); if let Some(ref desc) = playlist.description { pl_json["comment"] = serde_json::json!(desc); } response::ok( ¶ms.format, serde_json::json!({ "playlist": pl_json, }), ) } Err(e) => response::error( ¶ms.format, response::ERROR_GENERIC, &format!("failed to create playlist: {e}"), ), } } /// GET /rest/deletePlaylist[.view] pub async fn delete_playlist(req: HttpRequest, state: web::Data) -> HttpResponse { let (params, _user) = match authenticate(&req, &state).await { Ok(v) => v, Err(resp) => return resp, }; let id_str = match get_query_param(&req, "id") { Some(id) => id, None => { return response::error( ¶ms.format, response::ERROR_MISSING_PARAM, "missing required parameter: id", ); } }; let (_prefix, playlist_id) = match parse_subsonic_id(&id_str) { Some(v) => v, None => { return response::error( ¶ms.format, response::ERROR_NOT_FOUND, "invalid playlist id", ); } }; match queries::playlists::delete(state.db.conn(), playlist_id).await { Ok(()) => response::ok(¶ms.format, serde_json::json!({})), Err(e) => response::error( ¶ms.format, response::ERROR_GENERIC, &format!("failed to delete playlist: {e}"), ), } }