Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ied03277d450e39299470667ef479c3526a6a6964
1204 lines
35 KiB
Rust
1204 lines
35 KiB
Rust
pub mod migrations;
|
|
pub mod postgres;
|
|
pub mod sqlite;
|
|
|
|
use std::{path::PathBuf, sync::Arc};
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use rustc_hash::FxHashMap;
|
|
use uuid::Uuid;
|
|
|
|
use crate::{
|
|
analytics::UsageEvent,
|
|
enrichment::ExternalMetadata,
|
|
error::Result,
|
|
model::{
|
|
AuditEntry,
|
|
Collection,
|
|
CollectionKind,
|
|
ContentHash,
|
|
CustomField,
|
|
ManagedBlob,
|
|
ManagedStorageStats,
|
|
MediaId,
|
|
MediaItem,
|
|
Pagination,
|
|
Tag,
|
|
},
|
|
playlists::Playlist,
|
|
search::{SearchRequest, SearchResults},
|
|
social::{Comment, Rating, ShareLink},
|
|
subtitles::Subtitle,
|
|
transcode::{TranscodeSession, TranscodeStatus},
|
|
users::UserId,
|
|
};
|
|
|
|
/// Statistics about the database.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct DatabaseStats {
|
|
pub media_count: u64,
|
|
pub tag_count: u64,
|
|
pub collection_count: u64,
|
|
pub audit_count: u64,
|
|
pub database_size_bytes: u64,
|
|
pub backend_name: String,
|
|
}
|
|
|
|
/// Session data for database-backed session storage.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SessionData {
|
|
pub session_token: String,
|
|
pub user_id: Option<String>,
|
|
pub username: String,
|
|
pub role: String,
|
|
pub created_at: DateTime<Utc>,
|
|
pub expires_at: DateTime<Utc>,
|
|
pub last_accessed: DateTime<Utc>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
pub trait StorageBackend: Send + Sync + 'static {
|
|
// Migrations
|
|
|
|
/// Apply all pending database migrations.
|
|
/// Called on server startup to ensure the schema is up to date.
|
|
async fn run_migrations(&self) -> Result<()>;
|
|
|
|
// Root directories
|
|
|
|
/// Register a root directory for media scanning.
|
|
async fn add_root_dir(&self, path: PathBuf) -> Result<()>;
|
|
|
|
/// List all registered root directories.
|
|
async fn list_root_dirs(&self) -> Result<Vec<PathBuf>>;
|
|
|
|
/// Remove a root directory registration.
|
|
/// Does not delete any media items found under this directory.
|
|
async fn remove_root_dir(&self, path: &std::path::Path) -> Result<()>;
|
|
|
|
// Media CRUD
|
|
|
|
/// Insert a new media item into the database.
|
|
/// Returns `Database` error if an item with the same ID already exists.
|
|
async fn insert_media(&self, item: &MediaItem) -> Result<()>;
|
|
|
|
/// Retrieve a media item by its ID.
|
|
/// Returns `NotFound` if no item exists with the given ID.
|
|
async fn get_media(&self, id: MediaId) -> Result<MediaItem>;
|
|
|
|
/// Return the total number of media items in the database.
|
|
async fn count_media(&self) -> Result<u64>;
|
|
|
|
/// Look up a media item by its content hash.
|
|
/// Returns `None` if no item matches the hash.
|
|
async fn get_media_by_hash(
|
|
&self,
|
|
hash: &ContentHash,
|
|
) -> Result<Option<MediaItem>>;
|
|
|
|
/// Get a media item by its file path (used for incremental scanning).
|
|
/// Returns `None` if no item exists at the given path.
|
|
async fn get_media_by_path(
|
|
&self,
|
|
path: &std::path::Path,
|
|
) -> Result<Option<MediaItem>>;
|
|
|
|
/// List media items with pagination (offset and limit).
|
|
async fn list_media(&self, pagination: &Pagination)
|
|
-> Result<Vec<MediaItem>>;
|
|
|
|
/// Update an existing media item's metadata.
|
|
/// Returns `NotFound` if the item does not exist.
|
|
async fn update_media(&self, item: &MediaItem) -> Result<()>;
|
|
|
|
/// Permanently delete a media item by ID.
|
|
/// Returns `NotFound` if the item does not exist.
|
|
async fn delete_media(&self, id: MediaId) -> Result<()>;
|
|
|
|
/// Delete all media items from the database. Returns the number deleted.
|
|
async fn delete_all_media(&self) -> Result<u64>;
|
|
|
|
// Tags
|
|
|
|
/// Create a new tag with an optional parent for hierarchical tagging.
|
|
async fn create_tag(
|
|
&self,
|
|
name: &str,
|
|
parent_id: Option<Uuid>,
|
|
) -> Result<Tag>;
|
|
|
|
/// Retrieve a tag by its ID. Returns `NotFound` if it does not exist.
|
|
async fn get_tag(&self, id: Uuid) -> Result<Tag>;
|
|
|
|
/// List all tags in the database.
|
|
async fn list_tags(&self) -> Result<Vec<Tag>>;
|
|
|
|
/// Delete a tag by ID. Also removes all media-tag associations.
|
|
async fn delete_tag(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Associate a tag with a media item. No-op if already tagged.
|
|
async fn tag_media(&self, media_id: MediaId, tag_id: Uuid) -> Result<()>;
|
|
|
|
/// Remove a tag association from a media item.
|
|
async fn untag_media(&self, media_id: MediaId, tag_id: Uuid) -> Result<()>;
|
|
|
|
/// Get all tags associated with a media item.
|
|
async fn get_media_tags(&self, media_id: MediaId) -> Result<Vec<Tag>>;
|
|
|
|
/// Get all descendant tags of a parent tag (recursive).
|
|
async fn get_tag_descendants(&self, tag_id: Uuid) -> Result<Vec<Tag>>;
|
|
|
|
// Collections
|
|
|
|
/// Create a new collection. Smart collections use `filter_query` for
|
|
/// automatic membership; manual collections use explicit add/remove.
|
|
async fn create_collection(
|
|
&self,
|
|
name: &str,
|
|
kind: CollectionKind,
|
|
description: Option<&str>,
|
|
filter_query: Option<&str>,
|
|
) -> Result<Collection>;
|
|
|
|
/// Retrieve a collection by ID. Returns `NotFound` if it does not exist.
|
|
async fn get_collection(&self, id: Uuid) -> Result<Collection>;
|
|
|
|
/// List all collections.
|
|
async fn list_collections(&self) -> Result<Vec<Collection>>;
|
|
|
|
/// Delete a collection and all its membership entries.
|
|
async fn delete_collection(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Add a media item to a manual collection at the given position.
|
|
async fn add_to_collection(
|
|
&self,
|
|
collection_id: Uuid,
|
|
media_id: MediaId,
|
|
position: i32,
|
|
) -> Result<()>;
|
|
|
|
/// Remove a media item from a collection.
|
|
async fn remove_from_collection(
|
|
&self,
|
|
collection_id: Uuid,
|
|
media_id: MediaId,
|
|
) -> Result<()>;
|
|
|
|
/// Get all media items in a collection, ordered by position.
|
|
async fn get_collection_members(
|
|
&self,
|
|
collection_id: Uuid,
|
|
) -> Result<Vec<MediaItem>>;
|
|
|
|
// Search
|
|
|
|
/// Execute a full-text search with filters, sorting, and pagination.
|
|
/// Uses FTS5 on `SQLite` and tsvector/trigram on `PostgreSQL`.
|
|
async fn search(&self, request: &SearchRequest) -> Result<SearchResults>;
|
|
|
|
// Audit
|
|
|
|
/// Record an audit log entry for an action on a media item.
|
|
async fn record_audit(&self, entry: &AuditEntry) -> Result<()>;
|
|
|
|
/// List audit entries, optionally filtered to a specific media item.
|
|
async fn list_audit_entries(
|
|
&self,
|
|
media_id: Option<MediaId>,
|
|
pagination: &Pagination,
|
|
) -> Result<Vec<AuditEntry>>;
|
|
|
|
// Custom fields
|
|
|
|
/// Set a custom field on a media item (upserts if the field name exists).
|
|
async fn set_custom_field(
|
|
&self,
|
|
media_id: MediaId,
|
|
name: &str,
|
|
field: &CustomField,
|
|
) -> Result<()>;
|
|
|
|
/// Get all custom fields for a media item, keyed by field name.
|
|
async fn get_custom_fields(
|
|
&self,
|
|
media_id: MediaId,
|
|
) -> Result<FxHashMap<String, CustomField>>;
|
|
|
|
/// Delete a custom field from a media item by name.
|
|
async fn delete_custom_field(
|
|
&self,
|
|
media_id: MediaId,
|
|
name: &str,
|
|
) -> Result<()>;
|
|
|
|
// Batch operations (transactional where supported)
|
|
|
|
/// Delete multiple media items in a single transaction.
|
|
/// Returns the number of items actually deleted.
|
|
async fn batch_delete_media(&self, ids: &[MediaId]) -> Result<u64>;
|
|
|
|
/// Apply multiple tags to multiple media items.
|
|
/// Returns the number of new associations created.
|
|
async fn batch_tag_media(
|
|
&self,
|
|
media_ids: &[MediaId],
|
|
tag_ids: &[Uuid],
|
|
) -> Result<u64>;
|
|
|
|
// Integrity
|
|
|
|
/// List all media item IDs, paths, and content hashes.
|
|
/// Used by integrity checks to verify files still exist on disk.
|
|
async fn list_media_paths(
|
|
&self,
|
|
) -> Result<Vec<(MediaId, std::path::PathBuf, ContentHash)>>;
|
|
|
|
/// Update metadata fields on multiple media items at once.
|
|
/// Only non-`None` fields are applied. Returns the number updated.
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn batch_update_media(
|
|
&self,
|
|
ids: &[MediaId],
|
|
title: Option<&str>,
|
|
artist: Option<&str>,
|
|
album: Option<&str>,
|
|
genre: Option<&str>,
|
|
year: Option<i32>,
|
|
description: Option<&str>,
|
|
) -> Result<u64>;
|
|
|
|
// Saved searches
|
|
|
|
/// Persist a search query so it can be re-executed later.
|
|
async fn save_search(
|
|
&self,
|
|
id: uuid::Uuid,
|
|
name: &str,
|
|
query: &str,
|
|
sort_order: Option<&str>,
|
|
) -> Result<()>;
|
|
|
|
/// List all saved searches.
|
|
async fn list_saved_searches(&self)
|
|
-> Result<Vec<crate::model::SavedSearch>>;
|
|
|
|
/// Get a single saved search by ID.
|
|
async fn get_saved_search(
|
|
&self,
|
|
id: uuid::Uuid,
|
|
) -> Result<crate::model::SavedSearch>;
|
|
|
|
/// Delete a saved search by ID.
|
|
async fn delete_saved_search(&self, id: uuid::Uuid) -> Result<()>;
|
|
|
|
// Duplicates
|
|
|
|
/// Find groups of media items with identical content hashes.
|
|
async fn find_duplicates(&self) -> Result<Vec<Vec<MediaItem>>>;
|
|
|
|
/// Find groups of visually similar media using perceptual hashing.
|
|
/// `threshold` is the maximum Hamming distance to consider a match.
|
|
async fn find_perceptual_duplicates(
|
|
&self,
|
|
threshold: u32,
|
|
) -> Result<Vec<Vec<MediaItem>>>;
|
|
|
|
// Database management
|
|
|
|
/// Collect aggregate database statistics (counts, size, backend name).
|
|
async fn database_stats(&self) -> Result<DatabaseStats>;
|
|
|
|
/// Reclaim unused disk space (VACUUM for `SQLite`, VACUUM FULL for Postgres).
|
|
async fn vacuum(&self) -> Result<()>;
|
|
|
|
/// Delete all data from all tables. Destructive; used in tests.
|
|
async fn clear_all_data(&self) -> Result<()>;
|
|
|
|
// Thumbnail helpers
|
|
/// List all media IDs, optionally filtering to those missing thumbnails.
|
|
async fn list_media_ids_for_thumbnails(
|
|
&self,
|
|
only_missing: bool,
|
|
) -> Result<Vec<crate::model::MediaId>>;
|
|
|
|
// Library statistics
|
|
|
|
/// Compute comprehensive library statistics (sizes, counts by type,
|
|
/// top tags/collections, duplicates).
|
|
async fn library_statistics(&self) -> Result<LibraryStatistics>;
|
|
|
|
// User Management
|
|
|
|
/// List all registered users.
|
|
async fn list_users(&self) -> Result<Vec<crate::users::User>>;
|
|
|
|
/// Get a user by ID. Returns `NotFound` if no such user exists.
|
|
async fn get_user(
|
|
&self,
|
|
id: crate::users::UserId,
|
|
) -> Result<crate::users::User>;
|
|
|
|
/// Look up a user by username. Returns `NotFound` if not found.
|
|
async fn get_user_by_username(
|
|
&self,
|
|
username: &str,
|
|
) -> Result<crate::users::User>;
|
|
|
|
/// Create a new user with the given credentials and role.
|
|
async fn create_user(
|
|
&self,
|
|
username: &str,
|
|
password_hash: &str,
|
|
role: crate::config::UserRole,
|
|
profile: Option<crate::users::UserProfile>,
|
|
) -> Result<crate::users::User>;
|
|
|
|
/// Update a user's password, role, or profile. Only non-`None` fields
|
|
/// are applied.
|
|
async fn update_user(
|
|
&self,
|
|
id: crate::users::UserId,
|
|
password_hash: Option<&str>,
|
|
role: Option<crate::config::UserRole>,
|
|
profile: Option<crate::users::UserProfile>,
|
|
) -> Result<crate::users::User>;
|
|
|
|
/// Delete a user and all associated sessions.
|
|
async fn delete_user(&self, id: crate::users::UserId) -> Result<()>;
|
|
|
|
/// Get the library access grants for a user.
|
|
async fn get_user_libraries(
|
|
&self,
|
|
user_id: crate::users::UserId,
|
|
) -> Result<Vec<crate::users::UserLibraryAccess>>;
|
|
|
|
/// Grant a user access to a library root path with the given permission.
|
|
async fn grant_library_access(
|
|
&self,
|
|
user_id: crate::users::UserId,
|
|
root_path: &str,
|
|
permission: crate::users::LibraryPermission,
|
|
) -> Result<()>;
|
|
|
|
/// Revoke a user's access to a library root path.
|
|
async fn revoke_library_access(
|
|
&self,
|
|
user_id: crate::users::UserId,
|
|
root_path: &str,
|
|
) -> Result<()>;
|
|
|
|
/// Check if a user has access to a specific media item based on library
|
|
/// permissions. Returns the permission level if access is granted, or an
|
|
/// error if denied. Admin users (role=admin) bypass library checks and have
|
|
/// full access.
|
|
async fn check_library_access(
|
|
&self,
|
|
user_id: crate::users::UserId,
|
|
media_id: crate::model::MediaId,
|
|
) -> Result<crate::users::LibraryPermission> {
|
|
// Default implementation: get the media item's path and check against
|
|
// user's library access
|
|
let media = self.get_media(media_id).await?;
|
|
// Get user's library permissions
|
|
let libraries = self.get_user_libraries(user_id).await?;
|
|
|
|
// If user has no library restrictions, they have no access (unless they're
|
|
// admin) This default impl requires at least one matching library
|
|
// permission. Use Path::starts_with for component-wise matching to prevent
|
|
// prefix collisions (e.g. /data/user1 vs /data/user1-other).
|
|
for lib in &libraries {
|
|
if media.path.starts_with(std::path::Path::new(&lib.root_path)) {
|
|
return Ok(lib.permission);
|
|
}
|
|
}
|
|
|
|
Err(crate::error::PinakesError::Authorization(format!(
|
|
"user {user_id} has no access to media {media_id}"
|
|
)))
|
|
}
|
|
|
|
/// Check if a user has at least read access to a media item
|
|
async fn has_media_read_access(
|
|
&self,
|
|
user_id: crate::users::UserId,
|
|
media_id: crate::model::MediaId,
|
|
) -> Result<bool> {
|
|
match self.check_library_access(user_id, media_id).await {
|
|
Ok(perm) => Ok(perm.can_read()),
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|
|
|
|
/// Check if a user has write access to a media item
|
|
async fn has_media_write_access(
|
|
&self,
|
|
user_id: crate::users::UserId,
|
|
media_id: crate::model::MediaId,
|
|
) -> Result<bool> {
|
|
match self.check_library_access(user_id, media_id).await {
|
|
Ok(perm) => Ok(perm.can_write()),
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|
|
|
|
/// Rate a media item (1-5 stars) with an optional text review.
|
|
/// Upserts: replaces any existing rating by the same user.
|
|
async fn rate_media(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
stars: u8,
|
|
review: Option<&str>,
|
|
) -> Result<Rating>;
|
|
|
|
/// Get all ratings for a media item.
|
|
async fn get_media_ratings(&self, media_id: MediaId) -> Result<Vec<Rating>>;
|
|
|
|
/// Get a specific user's rating for a media item, if any.
|
|
async fn get_user_rating(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
) -> Result<Option<Rating>>;
|
|
|
|
/// Delete a rating by ID.
|
|
async fn delete_rating(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Add a comment on a media item, optionally as a reply to another comment.
|
|
async fn add_comment(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
text: &str,
|
|
parent_id: Option<Uuid>,
|
|
) -> Result<Comment>;
|
|
|
|
/// Get all comments for a media item.
|
|
async fn get_media_comments(&self, media_id: MediaId)
|
|
-> Result<Vec<Comment>>;
|
|
|
|
/// Delete a comment by ID.
|
|
async fn delete_comment(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Mark a media item as a favorite for a user.
|
|
async fn add_favorite(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
) -> Result<()>;
|
|
|
|
/// Remove a media item from a user's favorites.
|
|
async fn remove_favorite(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
) -> Result<()>;
|
|
|
|
/// Get a user's favorited media items with pagination.
|
|
async fn get_user_favorites(
|
|
&self,
|
|
user_id: UserId,
|
|
pagination: &Pagination,
|
|
) -> Result<Vec<MediaItem>>;
|
|
|
|
/// Check whether a media item is in a user's favorites.
|
|
async fn is_favorite(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
) -> Result<bool>;
|
|
|
|
/// Create a public share link for a media item with optional password
|
|
/// and expiry.
|
|
async fn create_share_link(
|
|
&self,
|
|
media_id: MediaId,
|
|
created_by: UserId,
|
|
token: &str,
|
|
password_hash: Option<&str>,
|
|
expires_at: Option<DateTime<Utc>>,
|
|
) -> Result<ShareLink>;
|
|
|
|
/// Look up a share link by its token. Returns `NotFound` if invalid.
|
|
async fn get_share_link(&self, token: &str) -> Result<ShareLink>;
|
|
|
|
/// Increment the view counter for a share link.
|
|
async fn increment_share_views(&self, token: &str) -> Result<()>;
|
|
|
|
/// Delete a share link by ID.
|
|
async fn delete_share_link(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Create a new playlist. Smart playlists auto-populate via `filter_query`.
|
|
async fn create_playlist(
|
|
&self,
|
|
owner_id: UserId,
|
|
name: &str,
|
|
description: Option<&str>,
|
|
is_public: bool,
|
|
is_smart: bool,
|
|
filter_query: Option<&str>,
|
|
) -> Result<Playlist>;
|
|
|
|
/// Get a playlist by ID. Returns `NotFound` if it does not exist.
|
|
async fn get_playlist(&self, id: Uuid) -> Result<Playlist>;
|
|
|
|
/// List playlists, optionally filtered to a specific owner.
|
|
async fn list_playlists(
|
|
&self,
|
|
owner_id: Option<UserId>,
|
|
) -> Result<Vec<Playlist>>;
|
|
|
|
/// Update a playlist's name, description, or visibility.
|
|
/// Only non-`None` fields are applied.
|
|
async fn update_playlist(
|
|
&self,
|
|
id: Uuid,
|
|
name: Option<&str>,
|
|
description: Option<&str>,
|
|
is_public: Option<bool>,
|
|
) -> Result<Playlist>;
|
|
|
|
/// Delete a playlist and all its item associations.
|
|
async fn delete_playlist(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Add a media item to a playlist at the given position.
|
|
async fn add_to_playlist(
|
|
&self,
|
|
playlist_id: Uuid,
|
|
media_id: MediaId,
|
|
position: i32,
|
|
) -> Result<()>;
|
|
|
|
/// Remove a media item from a playlist.
|
|
async fn remove_from_playlist(
|
|
&self,
|
|
playlist_id: Uuid,
|
|
media_id: MediaId,
|
|
) -> Result<()>;
|
|
|
|
/// Get all media items in a playlist, ordered by position.
|
|
async fn get_playlist_items(
|
|
&self,
|
|
playlist_id: Uuid,
|
|
) -> Result<Vec<MediaItem>>;
|
|
|
|
/// Move a media item to a new position within a playlist.
|
|
async fn reorder_playlist(
|
|
&self,
|
|
playlist_id: Uuid,
|
|
media_id: MediaId,
|
|
new_position: i32,
|
|
) -> Result<()>;
|
|
|
|
/// Record a usage/analytics event (play, view, download, etc.).
|
|
async fn record_usage_event(&self, event: &UsageEvent) -> Result<()>;
|
|
|
|
/// Query usage events, optionally filtered by media item and/or user.
|
|
async fn get_usage_events(
|
|
&self,
|
|
media_id: Option<MediaId>,
|
|
user_id: Option<UserId>,
|
|
limit: u64,
|
|
) -> Result<Vec<UsageEvent>>;
|
|
|
|
/// Get the most-viewed media items with their view counts.
|
|
async fn get_most_viewed(&self, limit: u64) -> Result<Vec<(MediaItem, u64)>>;
|
|
|
|
/// Get media items recently viewed by a user, most recent first.
|
|
async fn get_recently_viewed(
|
|
&self,
|
|
user_id: UserId,
|
|
limit: u64,
|
|
) -> Result<Vec<MediaItem>>;
|
|
|
|
/// Update playback/watch progress for a user on a media item (in seconds).
|
|
async fn update_watch_progress(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
progress_secs: f64,
|
|
) -> Result<()>;
|
|
|
|
/// Get the stored watch progress (in seconds) for a user/media pair.
|
|
async fn get_watch_progress(
|
|
&self,
|
|
user_id: UserId,
|
|
media_id: MediaId,
|
|
) -> Result<Option<f64>>;
|
|
|
|
/// Delete usage events older than the given timestamp.
|
|
/// Returns the number of events deleted.
|
|
async fn cleanup_old_events(&self, before: DateTime<Utc>) -> Result<u64>;
|
|
|
|
/// Add a subtitle track for a media item.
|
|
async fn add_subtitle(&self, subtitle: &Subtitle) -> Result<()>;
|
|
|
|
/// Get all subtitle tracks for a media item.
|
|
async fn get_media_subtitles(
|
|
&self,
|
|
media_id: MediaId,
|
|
) -> Result<Vec<Subtitle>>;
|
|
|
|
/// Delete a subtitle track by ID.
|
|
async fn delete_subtitle(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Adjust the timing offset (in milliseconds) for a subtitle track.
|
|
async fn update_subtitle_offset(
|
|
&self,
|
|
id: Uuid,
|
|
offset_ms: i64,
|
|
) -> Result<()>;
|
|
|
|
/// Store metadata fetched from an external source (e.g., `MusicBrainz`,
|
|
/// `TMDb`).
|
|
async fn store_external_metadata(
|
|
&self,
|
|
meta: &ExternalMetadata,
|
|
) -> Result<()>;
|
|
|
|
/// Get all external metadata records for a media item.
|
|
async fn get_external_metadata(
|
|
&self,
|
|
media_id: MediaId,
|
|
) -> Result<Vec<ExternalMetadata>>;
|
|
|
|
/// Delete an external metadata record by ID.
|
|
async fn delete_external_metadata(&self, id: Uuid) -> Result<()>;
|
|
|
|
/// Create a new transcoding session for a media item.
|
|
async fn create_transcode_session(
|
|
&self,
|
|
session: &TranscodeSession,
|
|
) -> Result<()>;
|
|
|
|
/// Get a transcoding session by ID.
|
|
async fn get_transcode_session(&self, id: Uuid) -> Result<TranscodeSession>;
|
|
|
|
/// List transcoding sessions, optionally filtered to a media item.
|
|
async fn list_transcode_sessions(
|
|
&self,
|
|
media_id: Option<MediaId>,
|
|
) -> Result<Vec<TranscodeSession>>;
|
|
|
|
/// Update the status and progress of a transcoding session.
|
|
async fn update_transcode_status(
|
|
&self,
|
|
id: Uuid,
|
|
status: TranscodeStatus,
|
|
progress: f32,
|
|
) -> Result<()>;
|
|
|
|
/// Delete transcode sessions that expired before the given timestamp.
|
|
/// Returns the number of sessions cleaned up.
|
|
async fn cleanup_expired_transcodes(
|
|
&self,
|
|
before: DateTime<Utc>,
|
|
) -> Result<u64>;
|
|
|
|
/// Create a new session in the database
|
|
async fn create_session(&self, session: &SessionData) -> Result<()>;
|
|
|
|
/// Get a session by its token, returns None if not found or expired
|
|
async fn get_session(
|
|
&self,
|
|
session_token: &str,
|
|
) -> Result<Option<SessionData>>;
|
|
|
|
/// Update the `last_accessed` timestamp for a session
|
|
async fn touch_session(&self, session_token: &str) -> Result<()>;
|
|
|
|
/// Extend a session's expiry time.
|
|
/// Only extends sessions that have not already expired.
|
|
/// Returns the new expiry time, or None if the session was not found or
|
|
/// already expired.
|
|
async fn extend_session(
|
|
&self,
|
|
session_token: &str,
|
|
new_expires_at: DateTime<Utc>,
|
|
) -> Result<Option<DateTime<Utc>>>;
|
|
|
|
/// Delete a specific session
|
|
async fn delete_session(&self, session_token: &str) -> Result<()>;
|
|
|
|
/// Delete all sessions for a specific user
|
|
async fn delete_user_sessions(&self, username: &str) -> Result<u64>;
|
|
|
|
/// Delete all expired sessions (where `expires_at` < now)
|
|
async fn delete_expired_sessions(&self) -> Result<u64>;
|
|
|
|
/// List all active sessions (optionally filtered by username)
|
|
async fn list_active_sessions(
|
|
&self,
|
|
username: Option<&str>,
|
|
) -> Result<Vec<SessionData>>;
|
|
|
|
// Book Management Methods
|
|
|
|
/// Upsert book metadata for a media item
|
|
async fn upsert_book_metadata(
|
|
&self,
|
|
metadata: &crate::model::BookMetadata,
|
|
) -> Result<()>;
|
|
|
|
/// Get book metadata for a media item
|
|
async fn get_book_metadata(
|
|
&self,
|
|
media_id: MediaId,
|
|
) -> Result<Option<crate::model::BookMetadata>>;
|
|
|
|
/// Add an author to a book
|
|
async fn add_book_author(
|
|
&self,
|
|
media_id: MediaId,
|
|
author: &crate::model::AuthorInfo,
|
|
) -> Result<()>;
|
|
|
|
/// Get all authors for a book
|
|
async fn get_book_authors(
|
|
&self,
|
|
media_id: MediaId,
|
|
) -> Result<Vec<crate::model::AuthorInfo>>;
|
|
|
|
/// List all distinct authors with book counts
|
|
async fn list_all_authors(
|
|
&self,
|
|
pagination: &Pagination,
|
|
) -> Result<Vec<(String, u64)>>;
|
|
|
|
/// List all series with book counts
|
|
async fn list_series(&self) -> Result<Vec<(String, u64)>>;
|
|
|
|
/// Get all books in a series, ordered by `series_index`
|
|
async fn get_series_books(&self, series_name: &str)
|
|
-> Result<Vec<MediaItem>>;
|
|
|
|
/// Update reading progress for a user and book
|
|
async fn update_reading_progress(
|
|
&self,
|
|
user_id: uuid::Uuid,
|
|
media_id: MediaId,
|
|
current_page: i32,
|
|
) -> Result<()>;
|
|
|
|
/// Get reading progress for a user and book
|
|
async fn get_reading_progress(
|
|
&self,
|
|
user_id: uuid::Uuid,
|
|
media_id: MediaId,
|
|
) -> Result<Option<crate::model::ReadingProgress>>;
|
|
|
|
/// Get reading list for a user filtered by status
|
|
async fn get_reading_list(
|
|
&self,
|
|
user_id: uuid::Uuid,
|
|
status: Option<crate::model::ReadingStatus>,
|
|
) -> Result<Vec<MediaItem>>;
|
|
|
|
/// Search books with book-specific criteria
|
|
async fn search_books(
|
|
&self,
|
|
isbn: Option<&str>,
|
|
author: Option<&str>,
|
|
series: Option<&str>,
|
|
publisher: Option<&str>,
|
|
language: Option<&str>,
|
|
pagination: &Pagination,
|
|
) -> Result<Vec<MediaItem>>;
|
|
|
|
/// Insert a media item that uses managed storage
|
|
async fn insert_managed_media(&self, item: &MediaItem) -> Result<()>;
|
|
|
|
/// Get or create a managed blob record (for deduplication tracking)
|
|
async fn get_or_create_blob(
|
|
&self,
|
|
hash: &ContentHash,
|
|
size: u64,
|
|
mime_type: &str,
|
|
) -> Result<ManagedBlob>;
|
|
|
|
/// Get a managed blob by its content hash
|
|
async fn get_blob(&self, hash: &ContentHash) -> Result<Option<ManagedBlob>>;
|
|
|
|
/// Increment the reference count for a blob
|
|
async fn increment_blob_ref(&self, hash: &ContentHash) -> Result<()>;
|
|
|
|
/// Decrement the reference count for a blob. Returns true if blob should be
|
|
/// deleted.
|
|
async fn decrement_blob_ref(&self, hash: &ContentHash) -> Result<bool>;
|
|
|
|
/// Update the `last_verified` timestamp for a blob
|
|
async fn update_blob_verified(&self, hash: &ContentHash) -> Result<()>;
|
|
|
|
/// List orphaned blobs (`reference_count` = 0)
|
|
async fn list_orphaned_blobs(&self) -> Result<Vec<ManagedBlob>>;
|
|
|
|
/// Delete a blob record
|
|
async fn delete_blob(&self, hash: &ContentHash) -> Result<()>;
|
|
|
|
/// Get managed storage statistics
|
|
async fn managed_storage_stats(&self) -> Result<ManagedStorageStats>;
|
|
|
|
/// Register a new sync device
|
|
async fn register_device(
|
|
&self,
|
|
device: &crate::sync::SyncDevice,
|
|
token_hash: &str,
|
|
) -> Result<crate::sync::SyncDevice>;
|
|
|
|
/// Get a sync device by ID
|
|
async fn get_device(
|
|
&self,
|
|
id: crate::sync::DeviceId,
|
|
) -> Result<crate::sync::SyncDevice>;
|
|
|
|
/// Get a sync device by its token hash
|
|
async fn get_device_by_token(
|
|
&self,
|
|
token_hash: &str,
|
|
) -> Result<Option<crate::sync::SyncDevice>>;
|
|
|
|
/// List all devices for a user
|
|
async fn list_user_devices(
|
|
&self,
|
|
user_id: UserId,
|
|
) -> Result<Vec<crate::sync::SyncDevice>>;
|
|
|
|
/// Update a sync device
|
|
async fn update_device(&self, device: &crate::sync::SyncDevice)
|
|
-> Result<()>;
|
|
|
|
/// Delete a sync device
|
|
async fn delete_device(&self, id: crate::sync::DeviceId) -> Result<()>;
|
|
|
|
/// Update the `last_seen_at` timestamp for a device
|
|
async fn touch_device(&self, id: crate::sync::DeviceId) -> Result<()>;
|
|
|
|
/// Record a change in the sync log
|
|
async fn record_sync_change(
|
|
&self,
|
|
change: &crate::sync::SyncLogEntry,
|
|
) -> Result<()>;
|
|
|
|
/// Get changes since a cursor position
|
|
async fn get_changes_since(
|
|
&self,
|
|
cursor: i64,
|
|
limit: u64,
|
|
) -> Result<Vec<crate::sync::SyncLogEntry>>;
|
|
|
|
/// Get the current sync cursor (highest sequence number)
|
|
async fn get_current_sync_cursor(&self) -> Result<i64>;
|
|
|
|
/// Clean up old sync log entries
|
|
async fn cleanup_old_sync_log(&self, before: DateTime<Utc>) -> Result<u64>;
|
|
|
|
/// Get sync state for a device and path
|
|
async fn get_device_sync_state(
|
|
&self,
|
|
device_id: crate::sync::DeviceId,
|
|
path: &str,
|
|
) -> Result<Option<crate::sync::DeviceSyncState>>;
|
|
|
|
/// Insert or update device sync state
|
|
async fn upsert_device_sync_state(
|
|
&self,
|
|
state: &crate::sync::DeviceSyncState,
|
|
) -> Result<()>;
|
|
|
|
/// List all pending sync items for a device
|
|
async fn list_pending_sync(
|
|
&self,
|
|
device_id: crate::sync::DeviceId,
|
|
) -> Result<Vec<crate::sync::DeviceSyncState>>;
|
|
|
|
/// Create a new upload session
|
|
async fn create_upload_session(
|
|
&self,
|
|
session: &crate::sync::UploadSession,
|
|
) -> Result<()>;
|
|
|
|
/// Get an upload session by ID
|
|
async fn get_upload_session(
|
|
&self,
|
|
id: Uuid,
|
|
) -> Result<crate::sync::UploadSession>;
|
|
|
|
/// Update an upload session
|
|
async fn update_upload_session(
|
|
&self,
|
|
session: &crate::sync::UploadSession,
|
|
) -> Result<()>;
|
|
|
|
/// Record a received chunk
|
|
async fn record_chunk(
|
|
&self,
|
|
upload_id: Uuid,
|
|
chunk: &crate::sync::ChunkInfo,
|
|
) -> Result<()>;
|
|
|
|
/// Get all chunks for an upload
|
|
async fn get_upload_chunks(
|
|
&self,
|
|
upload_id: Uuid,
|
|
) -> Result<Vec<crate::sync::ChunkInfo>>;
|
|
|
|
/// Clean up expired upload sessions
|
|
async fn cleanup_expired_uploads(&self) -> Result<u64>;
|
|
|
|
/// Record a sync conflict
|
|
async fn record_conflict(
|
|
&self,
|
|
conflict: &crate::sync::SyncConflict,
|
|
) -> Result<()>;
|
|
|
|
/// Get unresolved conflicts for a device
|
|
async fn get_unresolved_conflicts(
|
|
&self,
|
|
device_id: crate::sync::DeviceId,
|
|
) -> Result<Vec<crate::sync::SyncConflict>>;
|
|
|
|
/// Resolve a conflict
|
|
async fn resolve_conflict(
|
|
&self,
|
|
id: Uuid,
|
|
resolution: crate::config::ConflictResolution,
|
|
) -> Result<()>;
|
|
|
|
/// Create a new share
|
|
async fn create_share(
|
|
&self,
|
|
share: &crate::sharing::Share,
|
|
) -> Result<crate::sharing::Share>;
|
|
|
|
/// Get a share by ID
|
|
async fn get_share(
|
|
&self,
|
|
id: crate::sharing::ShareId,
|
|
) -> Result<crate::sharing::Share>;
|
|
|
|
/// Get a share by its public token
|
|
async fn get_share_by_token(
|
|
&self,
|
|
token: &str,
|
|
) -> Result<crate::sharing::Share>;
|
|
|
|
/// List shares created by a user
|
|
async fn list_shares_by_owner(
|
|
&self,
|
|
owner_id: UserId,
|
|
pagination: &Pagination,
|
|
) -> Result<Vec<crate::sharing::Share>>;
|
|
|
|
/// List shares received by a user
|
|
async fn list_shares_for_user(
|
|
&self,
|
|
user_id: UserId,
|
|
pagination: &Pagination,
|
|
) -> Result<Vec<crate::sharing::Share>>;
|
|
|
|
/// List all shares for a specific target
|
|
async fn list_shares_for_target(
|
|
&self,
|
|
target: &crate::sharing::ShareTarget,
|
|
) -> Result<Vec<crate::sharing::Share>>;
|
|
|
|
/// Update a share
|
|
async fn update_share(
|
|
&self,
|
|
share: &crate::sharing::Share,
|
|
) -> Result<crate::sharing::Share>;
|
|
|
|
/// Delete a share
|
|
async fn delete_share(&self, id: crate::sharing::ShareId) -> Result<()>;
|
|
|
|
/// Record that a share was accessed
|
|
async fn record_share_access(
|
|
&self,
|
|
id: crate::sharing::ShareId,
|
|
) -> Result<()>;
|
|
|
|
/// Check share access for a user and target
|
|
async fn check_share_access(
|
|
&self,
|
|
user_id: Option<UserId>,
|
|
target: &crate::sharing::ShareTarget,
|
|
) -> Result<Option<crate::sharing::SharePermissions>>;
|
|
|
|
/// Get effective permissions for a media item (considering inheritance)
|
|
async fn get_effective_share_permissions(
|
|
&self,
|
|
user_id: Option<UserId>,
|
|
media_id: MediaId,
|
|
) -> Result<Option<crate::sharing::SharePermissions>>;
|
|
|
|
/// Batch delete shares
|
|
async fn batch_delete_shares(
|
|
&self,
|
|
ids: &[crate::sharing::ShareId],
|
|
) -> Result<u64>;
|
|
|
|
/// Clean up expired shares
|
|
async fn cleanup_expired_shares(&self) -> Result<u64>;
|
|
|
|
/// Record share activity
|
|
async fn record_share_activity(
|
|
&self,
|
|
activity: &crate::sharing::ShareActivity,
|
|
) -> Result<()>;
|
|
|
|
/// Get activity for a share
|
|
async fn get_share_activity(
|
|
&self,
|
|
share_id: crate::sharing::ShareId,
|
|
pagination: &Pagination,
|
|
) -> Result<Vec<crate::sharing::ShareActivity>>;
|
|
|
|
/// Create a share notification
|
|
async fn create_share_notification(
|
|
&self,
|
|
notification: &crate::sharing::ShareNotification,
|
|
) -> Result<()>;
|
|
|
|
/// Get unread notifications for a user
|
|
async fn get_unread_notifications(
|
|
&self,
|
|
user_id: UserId,
|
|
) -> Result<Vec<crate::sharing::ShareNotification>>;
|
|
|
|
/// Mark a notification as read. Scoped to `user_id` to prevent cross-user
|
|
/// modification. Silently no-ops if the notification belongs to a different
|
|
/// user, which avoids leaking notification existence via error responses.
|
|
async fn mark_notification_read(
|
|
&self,
|
|
id: Uuid,
|
|
user_id: UserId,
|
|
) -> Result<()>;
|
|
|
|
/// Mark all notifications as read for a user
|
|
async fn mark_all_notifications_read(&self, user_id: UserId) -> Result<()>;
|
|
|
|
/// Rename a media item (changes `file_name` and updates path accordingly).
|
|
/// For external storage, this actually renames the file on disk.
|
|
/// For managed storage, this only updates the metadata.
|
|
/// Returns the old path for sync log recording.
|
|
async fn rename_media(&self, id: MediaId, new_name: &str) -> Result<String>;
|
|
|
|
/// Move a media item to a new directory.
|
|
/// For external storage, this actually moves the file on disk.
|
|
/// For managed storage, this only updates the path in metadata.
|
|
/// Returns the old path for sync log recording.
|
|
async fn move_media(
|
|
&self,
|
|
id: MediaId,
|
|
new_directory: &std::path::Path,
|
|
) -> Result<String>;
|
|
|
|
/// Batch move multiple media items to a new directory.
|
|
async fn batch_move_media(
|
|
&self,
|
|
ids: &[MediaId],
|
|
new_directory: &std::path::Path,
|
|
) -> Result<Vec<(MediaId, String)>> {
|
|
let mut results = Vec::new();
|
|
for id in ids {
|
|
let old_path = self.move_media(*id, new_directory).await?;
|
|
results.push((*id, old_path));
|
|
}
|
|
Ok(results)
|
|
}
|
|
|
|
/// Soft delete a media item (set `deleted_at` timestamp).
|
|
async fn soft_delete_media(&self, id: MediaId) -> Result<()>;
|
|
|
|
/// Restore a soft-deleted media item.
|
|
async fn restore_media(&self, id: MediaId) -> Result<()>;
|
|
|
|
/// List all soft-deleted media items.
|
|
async fn list_trash(&self, pagination: &Pagination)
|
|
-> Result<Vec<MediaItem>>;
|
|
|
|
/// Permanently delete all items in trash.
|
|
async fn empty_trash(&self) -> Result<u64>;
|
|
|
|
/// Permanently delete items in trash older than the specified date.
|
|
async fn purge_old_trash(&self, before: DateTime<Utc>) -> Result<u64>;
|
|
|
|
/// Count items in trash.
|
|
async fn count_trash(&self) -> Result<u64>;
|
|
|
|
/// Save extracted markdown links for a media item.
|
|
/// This replaces any existing links for the source media.
|
|
async fn save_markdown_links(
|
|
&self,
|
|
media_id: MediaId,
|
|
links: &[crate::model::MarkdownLink],
|
|
) -> Result<()>;
|
|
|
|
/// Get outgoing links from a media item.
|
|
async fn get_outgoing_links(
|
|
&self,
|
|
media_id: MediaId,
|
|
) -> Result<Vec<crate::model::MarkdownLink>>;
|
|
|
|
/// Get backlinks (incoming links) to a media item.
|
|
async fn get_backlinks(
|
|
&self,
|
|
media_id: MediaId,
|
|
) -> Result<Vec<crate::model::BacklinkInfo>>;
|
|
|
|
/// Clear all links for a media item.
|
|
async fn clear_links_for_media(&self, media_id: MediaId) -> Result<()>;
|
|
|
|
/// Get graph data for visualization.
|
|
///
|
|
/// If `center_id` is provided, returns nodes within `depth` hops of that
|
|
/// node. If `center_id` is None, returns the entire graph (limited by
|
|
/// internal max).
|
|
async fn get_graph_data(
|
|
&self,
|
|
center_id: Option<MediaId>,
|
|
depth: u32,
|
|
) -> Result<crate::model::GraphData>;
|
|
|
|
/// Resolve unresolved links by matching `target_path` against media item
|
|
/// paths. Returns the number of links that were resolved.
|
|
async fn resolve_links(&self) -> Result<u64>;
|
|
|
|
/// Update the `links_extracted_at` timestamp for a media item.
|
|
async fn mark_links_extracted(&self, media_id: MediaId) -> Result<()>;
|
|
|
|
/// Get count of unresolved links (links where `target_media_id` is NULL).
|
|
async fn count_unresolved_links(&self) -> Result<u64>;
|
|
|
|
/// Create a backup of the database to the specified path.
|
|
///
|
|
/// Only supported for `SQLite` (uses VACUUM INTO). `PostgreSQL`
|
|
/// deployments should use `pg_dump` directly; this method returns
|
|
/// `PinakesError::InvalidOperation` for unsupported backends.
|
|
async fn backup(&self, _dest: &std::path::Path) -> Result<()> {
|
|
Err(crate::error::PinakesError::InvalidOperation(
|
|
"backup not supported for this storage backend; use pg_dump for \
|
|
PostgreSQL"
|
|
.to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Comprehensive library statistics.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct LibraryStatistics {
|
|
pub total_media: u64,
|
|
pub total_size_bytes: u64,
|
|
pub avg_file_size_bytes: u64,
|
|
pub media_by_type: Vec<(String, u64)>,
|
|
pub storage_by_type: Vec<(String, u64)>,
|
|
pub newest_item: Option<String>,
|
|
pub oldest_item: Option<String>,
|
|
pub top_tags: Vec<(String, u64)>,
|
|
pub top_collections: Vec<(String, u64)>,
|
|
pub total_tags: u64,
|
|
pub total_collections: u64,
|
|
pub total_duplicates: u64,
|
|
}
|
|
|
|
pub type DynStorageBackend = Arc<dyn StorageBackend>;
|