Minimal subsonic functionality
This commit is contained in:
250
src/routes/subsonic/playlists.rs
Normal file
250
src/routes/subsonic/playlists.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
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<AppState>) -> 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<serde_json::Value> = 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<AppState>) -> 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<serde_json::Value> = 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<AppState>) -> 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<i32> = 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<serde_json::Value> = 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<AppState>) -> 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}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user