meta: move public crates to packages/
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I928162008cb1ba02e1aa0e7aa971e8326a6a6964
This commit is contained in:
parent
70b0113d8a
commit
00bab69598
308 changed files with 53890 additions and 53889 deletions
|
|
@ -1,386 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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.
|
||||
#[must_use]
|
||||
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.is_none_or(|b| root.components().count() > b.components().count());
|
||||
if is_longer {
|
||||
best = Some(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(root) = best
|
||||
&& 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, utoipa::ToSchema)]
|
||||
pub struct MediaResponse {
|
||||
pub id: String,
|
||||
pub path: String,
|
||||
pub file_name: String,
|
||||
pub media_type: String,
|
||||
pub content_hash: String,
|
||||
pub file_size: u64,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
pub duration_secs: Option<f64>,
|
||||
pub description: Option<String>,
|
||||
pub has_thumbnail: bool,
|
||||
#[schema(value_type = Object)]
|
||||
pub custom_fields: FxHashMap<String, CustomFieldResponse>,
|
||||
|
||||
// Photo-specific metadata
|
||||
pub date_taken: Option<DateTime<Utc>>,
|
||||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
pub camera_make: Option<String>,
|
||||
pub camera_model: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
// Markdown links
|
||||
pub links_extracted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct CustomFieldResponse {
|
||||
pub field_type: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ImportRequest {
|
||||
#[schema(value_type = String)]
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct ImportResponse {
|
||||
pub media_id: String,
|
||||
pub was_duplicate: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateMediaRequest {
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
// File Management
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct RenameMediaRequest {
|
||||
pub new_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct MoveMediaRequest {
|
||||
#[schema(value_type = String)]
|
||||
pub destination: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct BatchMoveRequest {
|
||||
pub media_ids: Vec<Uuid>,
|
||||
#[schema(value_type = String)]
|
||||
pub destination: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct TrashResponse {
|
||||
pub items: Vec<MediaResponse>,
|
||||
pub total_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct TrashInfoResponse {
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct EmptyTrashResponse {
|
||||
pub deleted_count: u64,
|
||||
}
|
||||
|
||||
// Enhanced Import
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ImportWithOptionsRequest {
|
||||
#[schema(value_type = String)]
|
||||
pub path: PathBuf,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
pub new_tags: Option<Vec<String>>,
|
||||
pub collection_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct BatchImportRequest {
|
||||
#[schema(value_type = Vec<String>)]
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
pub new_tags: Option<Vec<String>>,
|
||||
pub collection_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct BatchImportResponse {
|
||||
pub results: Vec<BatchImportItemResult>,
|
||||
pub total: usize,
|
||||
pub imported: usize,
|
||||
pub duplicates: usize,
|
||||
pub errors: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct BatchImportItemResult {
|
||||
pub path: String,
|
||||
pub media_id: Option<String>,
|
||||
pub was_duplicate: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct DirectoryImportRequest {
|
||||
#[schema(value_type = String)]
|
||||
pub path: PathBuf,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
pub new_tags: Option<Vec<String>>,
|
||||
pub collection_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct DirectoryPreviewResponse {
|
||||
pub files: Vec<DirectoryPreviewFile>,
|
||||
pub total_count: usize,
|
||||
pub total_size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct DirectoryPreviewFile {
|
||||
pub path: String,
|
||||
pub file_name: String,
|
||||
pub media_type: String,
|
||||
pub file_size: u64,
|
||||
}
|
||||
|
||||
// Custom Fields
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct SetCustomFieldRequest {
|
||||
pub name: String,
|
||||
pub field_type: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
// Media update extended
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateMediaFullRequest {
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
// Search with sort
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct MediaCountResponse {
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
// Duplicates
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct DuplicateGroupResponse {
|
||||
pub content_hash: String,
|
||||
pub items: Vec<MediaResponse>,
|
||||
}
|
||||
|
||||
// Open
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct OpenRequest {
|
||||
pub media_id: Uuid,
|
||||
}
|
||||
|
||||
// Upload
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct UploadResponse {
|
||||
pub media_id: String,
|
||||
pub content_hash: String,
|
||||
pub was_duplicate: bool,
|
||||
pub file_size: u64,
|
||||
}
|
||||
|
||||
impl From<pinakes_core::model::UploadResult> for UploadResponse {
|
||||
fn from(result: pinakes_core::model::UploadResult) -> Self {
|
||||
Self {
|
||||
media_id: result.media_id.0.to_string(),
|
||||
content_hash: result.content_hash.0,
|
||||
was_duplicate: result.was_duplicate,
|
||||
file_size: result.file_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct ManagedStorageStatsResponse {
|
||||
pub total_blobs: u64,
|
||||
pub total_size_bytes: u64,
|
||||
pub orphaned_blobs: u64,
|
||||
pub deduplication_ratio: f64,
|
||||
}
|
||||
|
||||
impl From<pinakes_core::model::ManagedStorageStats>
|
||||
for ManagedStorageStatsResponse
|
||||
{
|
||||
fn from(stats: pinakes_core::model::ManagedStorageStats) -> Self {
|
||||
Self {
|
||||
total_blobs: stats.total_blobs,
|
||||
total_size_bytes: stats.total_size_bytes,
|
||||
orphaned_blobs: stats.orphaned_blobs,
|
||||
deduplication_ratio: stats.deduplication_ratio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaResponse {
|
||||
/// Build a `MediaResponse` from a `MediaItem`, stripping the longest
|
||||
/// 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.
|
||||
#[must_use]
|
||||
pub fn new(item: pinakes_core::model::MediaItem, roots: &[PathBuf]) -> Self {
|
||||
Self {
|
||||
id: item.id.0.to_string(),
|
||||
path: relativize_path(&item.path, roots),
|
||||
file_name: item.file_name,
|
||||
media_type: serde_json::to_value(item.media_type)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default(),
|
||||
content_hash: item.content_hash.0,
|
||||
file_size: item.file_size,
|
||||
title: item.title,
|
||||
artist: item.artist,
|
||||
album: item.album,
|
||||
genre: item.genre,
|
||||
year: item.year,
|
||||
duration_secs: item.duration_secs,
|
||||
description: item.description,
|
||||
has_thumbnail: item.thumbnail_path.is_some(),
|
||||
custom_fields: item
|
||||
.custom_fields
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(k, CustomFieldResponse {
|
||||
field_type: v.field_type.to_string(),
|
||||
value: v.value,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
|
||||
// Photo-specific metadata
|
||||
date_taken: item.date_taken,
|
||||
latitude: item.latitude,
|
||||
longitude: item.longitude,
|
||||
camera_make: item.camera_make,
|
||||
camera_model: item.camera_model,
|
||||
rating: item.rating,
|
||||
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
|
||||
// Markdown links
|
||||
links_extracted_at: item.links_extracted_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct WatchProgressRequest {
|
||||
pub progress_secs: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct WatchProgressResponse {
|
||||
pub progress_secs: f64,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue