pinakes-server: relativize media paths against configured root directories
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9f113e6402030c46ad97f636985b5d6c6a6a6964
This commit is contained in:
parent
5077e9f117
commit
9c67c81a79
11 changed files with 212 additions and 42 deletions
|
|
@ -1,9 +1,39 @@
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Strip the longest matching root prefix from `full_path`, returning a
|
||||||
|
/// forward-slash-separated relative path string. Falls back to the full path
|
||||||
|
/// string when no root matches. If `roots` is empty, returns the full path as a
|
||||||
|
/// string so internal callers that have not yet migrated still work.
|
||||||
|
pub fn relativize_path(full_path: &Path, roots: &[PathBuf]) -> String {
|
||||||
|
let mut best: Option<&PathBuf> = None;
|
||||||
|
for root in roots {
|
||||||
|
if full_path.starts_with(root) {
|
||||||
|
let is_longer = best.map_or(true, |b| root.components().count() > b.components().count());
|
||||||
|
if is_longer {
|
||||||
|
best = Some(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(root) = best {
|
||||||
|
if let Ok(rel) = full_path.strip_prefix(root) {
|
||||||
|
// Normalise to forward slashes on all platforms.
|
||||||
|
return rel
|
||||||
|
.components()
|
||||||
|
.map(|c| c.as_os_str().to_string_lossy())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
full_path.to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct MediaResponse {
|
pub struct MediaResponse {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -233,12 +263,18 @@ impl From<pinakes_core::model::ManagedStorageStats>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversion helpers
|
impl MediaResponse {
|
||||||
impl From<pinakes_core::model::MediaItem> for MediaResponse {
|
/// Build a `MediaResponse` from a `MediaItem`, stripping the longest
|
||||||
fn from(item: pinakes_core::model::MediaItem) -> Self {
|
/// matching root prefix from the path before serialization. Pass the
|
||||||
|
/// configured root directories so that clients receive a relative path
|
||||||
|
/// (e.g. `"Music/song.mp3"`) rather than a full server filesystem path.
|
||||||
|
pub fn new(
|
||||||
|
item: pinakes_core::model::MediaItem,
|
||||||
|
roots: &[PathBuf],
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: item.id.0.to_string(),
|
id: item.id.0.to_string(),
|
||||||
path: item.path.to_string_lossy().to_string(),
|
path: relativize_path(&item.path, roots),
|
||||||
file_name: item.file_name,
|
file_name: item.file_name,
|
||||||
media_type: serde_json::to_value(item.media_type)
|
media_type: serde_json::to_value(item.media_type)
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -282,6 +318,60 @@ impl From<pinakes_core::model::MediaItem> for MediaResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Conversion helpers
|
||||||
|
impl From<pinakes_core::model::MediaItem> for MediaResponse {
|
||||||
|
/// Convert using no root stripping. Prefer `MediaResponse::new(item, roots)`
|
||||||
|
/// at route-handler call sites where roots are available.
|
||||||
|
fn from(item: pinakes_core::model::MediaItem) -> Self {
|
||||||
|
Self::new(item, &[])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relativize_path_strips_matching_root() {
|
||||||
|
let roots = vec![PathBuf::from("/home/user/music")];
|
||||||
|
let path = Path::new("/home/user/music/artist/song.mp3");
|
||||||
|
assert_eq!(relativize_path(path, &roots), "artist/song.mp3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relativize_path_picks_longest_root() {
|
||||||
|
let roots = vec![
|
||||||
|
PathBuf::from("/home/user"),
|
||||||
|
PathBuf::from("/home/user/music"),
|
||||||
|
];
|
||||||
|
let path = Path::new("/home/user/music/song.mp3");
|
||||||
|
assert_eq!(relativize_path(path, &roots), "song.mp3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relativize_path_no_match_returns_full() {
|
||||||
|
let roots = vec![PathBuf::from("/home/user/music")];
|
||||||
|
let path = Path::new("/srv/videos/movie.mkv");
|
||||||
|
assert_eq!(relativize_path(path, &roots), "/srv/videos/movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relativize_path_empty_roots_returns_full() {
|
||||||
|
let path = Path::new("/home/user/music/song.mp3");
|
||||||
|
assert_eq!(
|
||||||
|
relativize_path(path, &[]),
|
||||||
|
"/home/user/music/song.mp3"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relativize_path_exact_root_match() {
|
||||||
|
let roots = vec![PathBuf::from("/media/library")];
|
||||||
|
let path = Path::new("/media/library/file.mp3");
|
||||||
|
assert_eq!(relativize_path(path, &roots), "file.mp3");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Watch progress
|
// Watch progress
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct WatchProgressRequest {
|
pub struct WatchProgressRequest {
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,13 @@ pub async fn get_most_viewed(
|
||||||
) -> Result<Json<Vec<MostViewedResponse>>, ApiError> {
|
) -> Result<Json<Vec<MostViewedResponse>>, ApiError> {
|
||||||
let limit = params.limit.unwrap_or(20).min(MAX_LIMIT);
|
let limit = params.limit.unwrap_or(20).min(MAX_LIMIT);
|
||||||
let results = state.storage.get_most_viewed(limit).await?;
|
let results = state.storage.get_most_viewed(limit).await?;
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
results
|
results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(item, count)| {
|
.map(|(item, count)| {
|
||||||
MostViewedResponse {
|
MostViewedResponse {
|
||||||
media: MediaResponse::from(item),
|
media: MediaResponse::new(item, &roots),
|
||||||
view_count: count,
|
view_count: count,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -51,7 +52,13 @@ pub async fn get_recently_viewed(
|
||||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||||
let limit = params.limit.unwrap_or(20).min(MAX_LIMIT);
|
let limit = params.limit.unwrap_or(20).min(MAX_LIMIT);
|
||||||
let items = state.storage.get_recently_viewed(user_id, limit).await?;
|
let items = state.storage.get_recently_viewed(user_id, limit).await?;
|
||||||
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn record_event(
|
pub async fn record_event(
|
||||||
|
|
|
||||||
|
|
@ -194,8 +194,9 @@ pub async fn list_books(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
let response: Vec<MediaResponse> =
|
let response: Vec<MediaResponse> =
|
||||||
items.into_iter().map(MediaResponse::from).collect();
|
items.into_iter().map(|item| MediaResponse::new(item, &roots)).collect();
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,8 +224,9 @@ pub async fn get_series_books(
|
||||||
Path(series_name): Path<String>,
|
Path(series_name): Path<String>,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let items = state.storage.get_series_books(&series_name).await?;
|
let items = state.storage.get_series_books(&series_name).await?;
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
let response: Vec<MediaResponse> =
|
let response: Vec<MediaResponse> =
|
||||||
items.into_iter().map(MediaResponse::from).collect();
|
items.into_iter().map(|item| MediaResponse::new(item, &roots)).collect();
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,8 +260,9 @@ pub async fn get_author_books(
|
||||||
.search_books(None, Some(&author_name), None, None, None, &pagination)
|
.search_books(None, Some(&author_name), None, None, None, &pagination)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
let response: Vec<MediaResponse> =
|
let response: Vec<MediaResponse> =
|
||||||
items.into_iter().map(MediaResponse::from).collect();
|
items.into_iter().map(|item| MediaResponse::new(item, &roots)).collect();
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,8 +320,9 @@ pub async fn get_reading_list(
|
||||||
.get_reading_list(user_id.0, params.status)
|
.get_reading_list(user_id.0, params.status)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
let response: Vec<MediaResponse> =
|
let response: Vec<MediaResponse> =
|
||||||
items.into_iter().map(MediaResponse::from).collect();
|
items.into_iter().map(|item| MediaResponse::new(item, &roots)).collect();
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,5 +126,11 @@ pub async fn get_members(
|
||||||
let items =
|
let items =
|
||||||
pinakes_core::collections::get_members(&state.storage, collection_id)
|
pinakes_core::collections::get_members(&state.storage, collection_id)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub async fn list_duplicates(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Json<Vec<DuplicateGroupResponse>>, ApiError> {
|
) -> Result<Json<Vec<DuplicateGroupResponse>>, ApiError> {
|
||||||
let groups = state.storage.find_duplicates().await?;
|
let groups = state.storage.find_duplicates().await?;
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
|
||||||
let response: Vec<DuplicateGroupResponse> = groups
|
let response: Vec<DuplicateGroupResponse> = groups
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -18,8 +19,10 @@ pub async fn list_duplicates(
|
||||||
.first()
|
.first()
|
||||||
.map(|i| i.content_hash.0.clone())
|
.map(|i| i.content_hash.0.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let media_items: Vec<MediaResponse> =
|
let media_items: Vec<MediaResponse> = items
|
||||||
items.into_iter().map(MediaResponse::from).collect();
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect();
|
||||||
DuplicateGroupResponse {
|
DuplicateGroupResponse {
|
||||||
content_hash,
|
content_hash,
|
||||||
items: media_items,
|
items: media_items,
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,13 @@ pub async fn list_media(
|
||||||
params.sort,
|
params.sort,
|
||||||
);
|
);
|
||||||
let items = state.storage.list_media(&pagination).await?;
|
let items = state.storage.list_media(&pagination).await?;
|
||||||
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_media(
|
pub async fn get_media(
|
||||||
|
|
@ -128,7 +134,8 @@ pub async fn get_media(
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<Json<MediaResponse>, ApiError> {
|
) -> Result<Json<MediaResponse>, ApiError> {
|
||||||
let item = state.storage.get_media(MediaId(id)).await?;
|
let item = state.storage.get_media(MediaId(id)).await?;
|
||||||
Ok(Json(MediaResponse::from(item)))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(MediaResponse::new(item, &roots)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum length for short text fields (title, artist, album, genre).
|
/// Maximum length for short text fields (title, artist, album, genre).
|
||||||
|
|
@ -206,7 +213,8 @@ pub async fn update_media(
|
||||||
&serde_json::json!({"media_id": item.id.to_string()}),
|
&serde_json::json!({"media_id": item.id.to_string()}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Json(MediaResponse::from(item)))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(MediaResponse::new(item, &roots)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_media(
|
pub async fn delete_media(
|
||||||
|
|
@ -574,12 +582,14 @@ pub async fn preview_directory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let roots_for_walk = roots.clone();
|
||||||
let files: Vec<DirectoryPreviewFile> =
|
let files: Vec<DirectoryPreviewFile> =
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
fn walk_dir(
|
fn walk_dir(
|
||||||
dir: &std::path::Path,
|
dir: &std::path::Path,
|
||||||
recursive: bool,
|
recursive: bool,
|
||||||
|
roots: &[std::path::PathBuf],
|
||||||
result: &mut Vec<DirectoryPreviewFile>,
|
result: &mut Vec<DirectoryPreviewFile>,
|
||||||
) {
|
) {
|
||||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
|
|
@ -596,7 +606,7 @@ pub async fn preview_directory(
|
||||||
}
|
}
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
if recursive {
|
if recursive {
|
||||||
walk_dir(&path, recursive, result);
|
walk_dir(&path, recursive, roots, result);
|
||||||
}
|
}
|
||||||
} else if path.is_file()
|
} else if path.is_file()
|
||||||
&& let Some(mt) =
|
&& let Some(mt) =
|
||||||
|
|
@ -612,7 +622,7 @@ pub async fn preview_directory(
|
||||||
.and_then(|v| v.as_str().map(String::from))
|
.and_then(|v| v.as_str().map(String::from))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
result.push(DirectoryPreviewFile {
|
result.push(DirectoryPreviewFile {
|
||||||
path: path.to_string_lossy().to_string(),
|
path: crate::dto::relativize_path(&path, roots),
|
||||||
file_name,
|
file_name,
|
||||||
media_type,
|
media_type,
|
||||||
file_size: size,
|
file_size: size,
|
||||||
|
|
@ -620,7 +630,7 @@ pub async fn preview_directory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
walk_dir(&dir, recursive, &mut result);
|
walk_dir(&dir, recursive, &roots_for_walk, &mut result);
|
||||||
result
|
result
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
@ -948,7 +958,8 @@ pub async fn rename_media(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(MediaResponse::from(item)))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(MediaResponse::new(item, &roots)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn move_media_endpoint(
|
pub async fn move_media_endpoint(
|
||||||
|
|
@ -994,7 +1005,8 @@ pub async fn move_media_endpoint(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(MediaResponse::from(item)))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(MediaResponse::new(item, &roots)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn batch_move_media(
|
pub async fn batch_move_media(
|
||||||
|
|
@ -1144,7 +1156,8 @@ pub async fn restore_media(
|
||||||
&serde_json::json!({"media_id": media_id.to_string(), "restored": true}),
|
&serde_json::json!({"media_id": media_id.to_string(), "restored": true}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Json(MediaResponse::from(item)))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(MediaResponse::new(item, &roots)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_trash(
|
pub async fn list_trash(
|
||||||
|
|
@ -1159,9 +1172,13 @@ pub async fn list_trash(
|
||||||
|
|
||||||
let items = state.storage.list_trash(&pagination).await?;
|
let items = state.storage.list_trash(&pagination).await?;
|
||||||
let count = state.storage.count_trash().await?;
|
let count = state.storage.count_trash().await?;
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
|
||||||
Ok(Json(TrashResponse {
|
Ok(Json(TrashResponse {
|
||||||
items: items.into_iter().map(MediaResponse::from).collect(),
|
items: items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
total_count: count,
|
total_count: count,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,13 +121,16 @@ pub async fn get_timeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to response format
|
// Convert to response format
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
let mut timeline: Vec<TimelineGroup> = groups
|
let mut timeline: Vec<TimelineGroup> = groups
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(date, items)| {
|
.map(|(date, items)| {
|
||||||
let cover_id = items.first().map(|i| i.id.0.to_string());
|
let cover_id = items.first().map(|i| i.id.0.to_string());
|
||||||
let count = items.len();
|
let count = items.len();
|
||||||
let items: Vec<MediaResponse> =
|
let items: Vec<MediaResponse> = items
|
||||||
items.into_iter().map(MediaResponse::from).collect();
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect();
|
||||||
|
|
||||||
TimelineGroup {
|
TimelineGroup {
|
||||||
date,
|
date,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ use crate::{
|
||||||
|
|
||||||
/// Check whether a user has access to a playlist.
|
/// Check whether a user has access to a playlist.
|
||||||
///
|
///
|
||||||
/// * `require_write` – when `true` only the playlist owner is allowed (for
|
/// # Arguments
|
||||||
/// mutations such as update, delete, add/remove/reorder items). When `false`
|
///
|
||||||
|
/// * `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.
|
/// the playlist must either be public or owned by the requesting user.
|
||||||
async fn check_playlist_access(
|
async fn check_playlist_access(
|
||||||
storage: &pinakes_core::storage::DynStorageBackend,
|
storage: &pinakes_core::storage::DynStorageBackend,
|
||||||
|
|
@ -185,7 +187,13 @@ pub async fn list_items(
|
||||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||||
check_playlist_access(&state.storage, id, user_id, false).await?;
|
check_playlist_access(&state.storage, id, user_id, false).await?;
|
||||||
let items = state.storage.get_playlist_items(id).await?;
|
let items = state.storage.get_playlist_items(id).await?;
|
||||||
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reorder_item(
|
pub async fn reorder_item(
|
||||||
|
|
@ -213,5 +221,11 @@ pub async fn shuffle_playlist(
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
let mut items = state.storage.get_playlist_items(id).await?;
|
let mut items = state.storage.get_playlist_items(id).await?;
|
||||||
items.shuffle(&mut rand::rng());
|
items.shuffle(&mut rand::rng());
|
||||||
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,14 @@ pub async fn search(
|
||||||
};
|
};
|
||||||
|
|
||||||
let results = state.storage.search(&request).await?;
|
let results = state.storage.search(&request).await?;
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
|
||||||
Ok(Json(SearchResponse {
|
Ok(Json(SearchResponse {
|
||||||
items: results.items.into_iter().map(MediaResponse::from).collect(),
|
items: results
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
total_count: results.total_count,
|
total_count: results.total_count,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -84,9 +89,14 @@ pub async fn search_post(
|
||||||
};
|
};
|
||||||
|
|
||||||
let results = state.storage.search(&request).await?;
|
let results = state.storage.search(&request).await?;
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
|
||||||
Ok(Json(SearchResponse {
|
Ok(Json(SearchResponse {
|
||||||
items: results.items.into_iter().map(MediaResponse::from).collect(),
|
items: results
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
total_count: results.total_count,
|
total_count: results.total_count,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -506,6 +506,7 @@ pub async fn access_shared(
|
||||||
let _ = state.storage.record_share_activity(&activity).await;
|
let _ = state.storage.record_share_activity(&activity).await;
|
||||||
|
|
||||||
// Return the shared content
|
// Return the shared content
|
||||||
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
match &share.target {
|
match &share.target {
|
||||||
ShareTarget::Media { media_id } => {
|
ShareTarget::Media { media_id } => {
|
||||||
let item = state
|
let item = state
|
||||||
|
|
@ -514,8 +515,8 @@ pub async fn access_shared(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::not_found(format!("Media not found: {e}")))?;
|
.map_err(|e| ApiError::not_found(format!("Media not found: {e}")))?;
|
||||||
|
|
||||||
Ok(Json(SharedContentResponse::Single(MediaResponse::from(
|
Ok(Json(SharedContentResponse::Single(MediaResponse::new(
|
||||||
item,
|
item, &roots,
|
||||||
))))
|
))))
|
||||||
},
|
},
|
||||||
ShareTarget::Collection { collection_id } => {
|
ShareTarget::Collection { collection_id } => {
|
||||||
|
|
@ -527,8 +528,10 @@ pub async fn access_shared(
|
||||||
ApiError::not_found(format!("Collection not found: {e}"))
|
ApiError::not_found(format!("Collection not found: {e}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let items: Vec<MediaResponse> =
|
let items: Vec<MediaResponse> = members
|
||||||
members.into_iter().map(MediaResponse::from).collect();
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Json(SharedContentResponse::Multiple { items }))
|
Ok(Json(SharedContentResponse::Multiple { items }))
|
||||||
},
|
},
|
||||||
|
|
@ -553,8 +556,11 @@ pub async fn access_shared(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::internal(format!("Search failed: {e}")))?;
|
.map_err(|e| ApiError::internal(format!("Search failed: {e}")))?;
|
||||||
|
|
||||||
let items: Vec<MediaResponse> =
|
let items: Vec<MediaResponse> = results
|
||||||
results.items.into_iter().map(MediaResponse::from).collect();
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Json(SharedContentResponse::Multiple { items }))
|
Ok(Json(SharedContentResponse::Multiple { items }))
|
||||||
},
|
},
|
||||||
|
|
@ -585,8 +591,11 @@ pub async fn access_shared(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::internal(format!("Search failed: {e}")))?;
|
.map_err(|e| ApiError::internal(format!("Search failed: {e}")))?;
|
||||||
|
|
||||||
let items: Vec<MediaResponse> =
|
let items: Vec<MediaResponse> = results
|
||||||
results.items.into_iter().map(MediaResponse::from).collect();
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Json(SharedContentResponse::Multiple { items }))
|
Ok(Json(SharedContentResponse::Multiple { items }))
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,13 @@ pub async fn list_favorites(
|
||||||
.storage
|
.storage
|
||||||
.get_user_favorites(user_id, &Pagination::default())
|
.get_user_favorites(user_id, &Pagination::default())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| MediaResponse::new(item, &roots))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_share_link(
|
pub async fn create_share_link(
|
||||||
|
|
@ -205,5 +211,6 @@ pub async fn access_shared_media(
|
||||||
}
|
}
|
||||||
state.storage.increment_share_views(&token).await?;
|
state.storage.increment_share_views(&token).await?;
|
||||||
let item = state.storage.get_media(link.media_id).await?;
|
let item = state.storage.get_media(link.media_id).await?;
|
||||||
Ok(Json(MediaResponse::from(item)))
|
let roots = state.config.read().await.directories.roots.clone();
|
||||||
|
Ok(Json(MediaResponse::new(item, &roots)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue