pinakes/crates/pinakes-server/src/routes/playlists.rs
NotAShelf 278bcaa4b0
pinakes-ui: streamline sidebar design
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0176fa480e5ba40eea5a39685a4f97896a6a6964
2026-02-04 21:35:34 +03:00

209 lines
7.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use axum::Json;
use axum::extract::{Extension, Path, State};
use uuid::Uuid;
use crate::auth::resolve_user_id;
use crate::dto::*;
use crate::error::ApiError;
use crate::state::AppState;
use pinakes_core::model::MediaId;
use pinakes_core::playlists::Playlist;
use pinakes_core::users::UserId;
/// Check whether a user has access to a playlist.
///
/// * `require_write` when `true` only the playlist owner is allowed (for
/// mutations such as update, delete, add/remove/reorder items). When `false`
/// the playlist must either be public or owned by the requesting user.
async fn check_playlist_access(
storage: &pinakes_core::storage::DynStorageBackend,
playlist_id: Uuid,
user_id: UserId,
require_write: bool,
) -> Result<Playlist, ApiError> {
let playlist = storage.get_playlist(playlist_id).await.map_err(ApiError)?;
if require_write {
// Write operations require ownership
if playlist.owner_id != user_id {
return Err(ApiError(pinakes_core::error::PinakesError::Authorization(
"only the playlist owner can modify this playlist".into(),
)));
}
} else {
// Read operations: allow if public or owner
if !playlist.is_public && playlist.owner_id != user_id {
return Err(ApiError(pinakes_core::error::PinakesError::Authorization(
"playlist is private".into(),
)));
}
}
Ok(playlist)
}
pub async fn create_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
Json(req): Json<CreatePlaylistRequest>,
) -> Result<Json<PlaylistResponse>, ApiError> {
if req.name.is_empty() || req.name.chars().count() > 255 {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"playlist name must be 1-255 characters".into(),
),
));
}
let owner_id = resolve_user_id(&state.storage, &username).await?;
let playlist = state
.storage
.create_playlist(
owner_id,
&req.name,
req.description.as_deref(),
req.is_public.unwrap_or(false),
req.is_smart.unwrap_or(false),
req.filter_query.as_deref(),
)
.await?;
Ok(Json(PlaylistResponse::from(playlist)))
}
pub async fn list_playlists(
State(state): State<AppState>,
Extension(username): Extension<String>,
) -> Result<Json<Vec<PlaylistResponse>>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
// Fetch all playlists and filter to only public ones plus the user's own
let playlists = state.storage.list_playlists(None).await?;
let visible: Vec<PlaylistResponse> = playlists
.into_iter()
.filter(|p| p.is_public || p.owner_id == user_id)
.map(PlaylistResponse::from)
.collect();
Ok(Json(visible))
}
pub async fn get_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path(id): Path<Uuid>,
) -> Result<Json<PlaylistResponse>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
let playlist = check_playlist_access(&state.storage, id, user_id, false).await?;
Ok(Json(PlaylistResponse::from(playlist)))
}
pub async fn update_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path(id): Path<Uuid>,
Json(req): Json<UpdatePlaylistRequest>,
) -> Result<Json<PlaylistResponse>, ApiError> {
if let Some(ref name) = req.name
&& (name.is_empty() || name.chars().count() > 255)
{
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"playlist name must be 1-255 characters".into(),
),
));
}
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, true).await?;
let playlist = state
.storage
.update_playlist(
id,
req.name.as_deref(),
req.description.as_deref(),
req.is_public,
)
.await?;
Ok(Json(PlaylistResponse::from(playlist)))
}
pub async fn delete_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, true).await?;
state.storage.delete_playlist(id).await?;
Ok(Json(serde_json::json!({"deleted": true})))
}
pub async fn add_item(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path(id): Path<Uuid>,
Json(req): Json<PlaylistItemRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, true).await?;
let position = match req.position {
Some(p) => p,
None => {
let items = state.storage.get_playlist_items(id).await?;
items.len() as i32
}
};
state
.storage
.add_to_playlist(id, MediaId(req.media_id), position)
.await?;
Ok(Json(serde_json::json!({"added": true})))
}
pub async fn remove_item(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path((id, media_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<serde_json::Value>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, true).await?;
state
.storage
.remove_from_playlist(id, MediaId(media_id))
.await?;
Ok(Json(serde_json::json!({"removed": true})))
}
pub async fn list_items(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<MediaResponse>>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, false).await?;
let items = state.storage.get_playlist_items(id).await?;
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
}
pub async fn reorder_item(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path(id): Path<Uuid>,
Json(req): Json<ReorderPlaylistRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, true).await?;
state
.storage
.reorder_playlist(id, MediaId(req.media_id), req.new_position)
.await?;
Ok(Json(serde_json::json!({"reordered": true})))
}
pub async fn shuffle_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<MediaResponse>>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, false).await?;
use rand::seq::SliceRandom;
let mut items = state.storage.get_playlist_items(id).await?;
items.shuffle(&mut rand::rng());
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
}