treewide: fix as many Clippy warnings as I humanly can

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I3c99acd032679bb7a04505db1a712b906a6a6964
This commit is contained in:
raf 2026-05-20 21:52:31 +03:00
commit 602cfb68b7
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
65 changed files with 1191 additions and 540 deletions

View file

@ -27,8 +27,6 @@ pub fn create_router(
create_router_with_tls(state, rate_limits, None)
}
/// Build a governor rate limiter from per-second and burst-size values.
/// Panics if the config is invalid (callers must validate before use).
fn build_governor(
per_second: u64,
burst_size: u32,
@ -38,13 +36,18 @@ fn build_governor(
governor::middleware::NoOpMiddleware,
>,
> {
Arc::new(
GovernorConfigBuilder::default()
.per_second(per_second)
.burst_size(burst_size)
.finish()
.expect("rate limit config was validated at startup"),
)
// finish() returns None only when per_second=0; clamp to ensure it always
// returns Some
let per_second = per_second.max(1);
let burst_size = burst_size.max(1);
let Some(config) = GovernorConfigBuilder::default()
.per_second(per_second)
.burst_size(burst_size)
.finish()
else {
return build_governor(1, 1);
};
Arc::new(config)
}
/// Create the router with TLS configuration for security headers
@ -521,8 +524,16 @@ pub fn create_router_with_tls(
// CORS configuration: use config-driven origins if specified,
// otherwise fall back to default localhost origins
let cors = {
let origins: Vec<HeaderValue> =
if let Ok(config_read) = state.config.try_read() {
let default_origins = || {
vec![
HeaderValue::from_static("http://localhost:3000"),
HeaderValue::from_static("http://127.0.0.1:3000"),
HeaderValue::from_static("tauri://localhost"),
]
};
let origins: Vec<HeaderValue> = state.config.try_read().map_or_else(
|_| default_origins(),
|config_read| {
if config_read.server.cors_enabled
&& !config_read.server.cors_origins.is_empty()
{
@ -533,19 +544,10 @@ pub fn create_router_with_tls(
.filter_map(|o| HeaderValue::from_str(o).ok())
.collect()
} else {
vec![
HeaderValue::from_static("http://localhost:3000"),
HeaderValue::from_static("http://127.0.0.1:3000"),
HeaderValue::from_static("tauri://localhost"),
]
default_origins()
}
} else {
vec![
HeaderValue::from_static("http://localhost:3000"),
HeaderValue::from_static("http://127.0.0.1:3000"),
HeaderValue::from_static("tauri://localhost"),
]
};
},
);
CorsLayer::new()
.allow_origin(origins)

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use axum::{
extract::{Request, State},
http::StatusCode,
@ -90,8 +92,10 @@ pub async fn require_auth(
if session.expires_at < now {
let username = session.username;
// Delete expired session in a bounded background task
if let Ok(permit) = state.session_semaphore.clone().try_acquire_owned() {
let storage = state.storage.clone();
if let Ok(permit) =
Arc::clone(&state.session_semaphore).try_acquire_owned()
{
let storage = Arc::clone(&state.storage);
let token_owned = token.clone();
tokio::spawn(async move {
if let Err(e) = storage.delete_session(&token_owned).await {
@ -105,8 +109,9 @@ pub async fn require_auth(
}
// Update last_accessed timestamp in a bounded background task
if let Ok(permit) = state.session_semaphore.clone().try_acquire_owned() {
let storage = state.storage.clone();
if let Ok(permit) = Arc::clone(&state.session_semaphore).try_acquire_owned()
{
let storage = Arc::clone(&state.storage);
let token_owned = token.clone();
tokio::spawn(async move {
if let Err(e) = storage.touch_session(&token_owned).await {
@ -209,7 +214,9 @@ pub async fn require_admin(request: Request, next: Next) -> Response {
/// Resolve the authenticated username (from request extensions) to a `UserId`.
///
/// Returns an error if the user cannot be found.
/// # Errors
///
/// Returns an error if the user cannot be found in the database.
pub async fn resolve_user_id(
storage: &pinakes_core::storage::DynStorageBackend,
username: &str,

View file

@ -17,6 +17,7 @@ pub struct CreateShareRequest {
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
#[allow(clippy::struct_field_names)]
pub struct SharePermissionsRequest {
pub can_view: Option<bool>,
pub can_download: Option<bool>,
@ -47,6 +48,7 @@ pub struct ShareResponse {
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
#[allow(clippy::struct_excessive_bools, clippy::struct_field_names)]
pub struct SharePermissionsResponse {
pub can_view: bool,
pub can_download: bool,
@ -197,6 +199,6 @@ pub struct AccessSharedRequest {
#[derive(Debug, Serialize, utoipa::ToSchema)]
#[serde(untagged)]
pub enum SharedContentResponse {
Single(super::MediaResponse),
Single(Box<super::MediaResponse>),
Multiple { items: Vec<super::MediaResponse> },
}

View file

@ -15,7 +15,11 @@ impl IntoResponse for ApiError {
fn into_response(self) -> Response {
use pinakes_core::error::PinakesError;
let (status, message) = match &self.0 {
PinakesError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
PinakesError::NotFound(msg)
| PinakesError::TagNotFound(msg)
| PinakesError::CollectionNotFound(msg) => {
(StatusCode::NOT_FOUND, msg.clone())
},
PinakesError::FileNotFound(path) => {
// Only expose the file name, not the full path
let name = path.file_name().map_or_else(
@ -25,10 +29,6 @@ impl IntoResponse for ApiError {
tracing::debug!(path = %path.display(), "file not found");
(StatusCode::NOT_FOUND, format!("file not found: {name}"))
},
PinakesError::TagNotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
PinakesError::CollectionNotFound(msg) => {
(StatusCode::NOT_FOUND, msg.clone())
},
PinakesError::DuplicateHash(msg) => (StatusCode::CONFLICT, msg.clone()),
PinakesError::UnsupportedMediaType(path) => {
let name = path.file_name().map_or_else(

View file

@ -4,6 +4,7 @@ use anyhow::Result;
use axum::{Router, response::Redirect, routing::any};
use clap::Parser;
use pinakes_core::{config::Config, storage::StorageBackend};
use pinakes_enrichment::EnrichmentSourceType;
use pinakes_server::{app, state::AppState};
use tokio::sync::RwLock;
use tracing::info;
@ -189,7 +190,7 @@ async fn main() -> Result<()> {
// Start filesystem watcher if configured
if config.scanning.watch {
let watch_storage = storage.clone();
let watch_storage = Arc::clone(&storage);
let watch_dirs = config.directories.roots.clone();
let watch_ignore = config.scanning.ignore_patterns.clone();
tokio::spawn(async move {
@ -245,9 +246,9 @@ async fn main() -> Result<()> {
max_concurrent_ops: p.max_concurrent_ops,
plugin_timeout_secs: p.plugin_timeout_secs,
timeouts: pinakes_types::config::PluginTimeoutConfig {
capability_query_secs: p.timeouts.capability_query_secs,
processing_secs: p.timeouts.processing_secs,
event_handler_secs: p.timeouts.event_handler_secs,
capability_query: p.timeouts.capability_query,
processing: p.timeouts.processing,
event_handler: p.timeouts.event_handler,
},
max_consecutive_failures: p.max_consecutive_failures,
trusted_keys: p.trusted_keys.clone(),
@ -297,7 +298,7 @@ async fn main() -> Result<()> {
};
// Initialize job queue with executor
let job_storage = storage.clone();
let job_storage = Arc::clone(&storage);
let job_config = config.clone();
let job_transcode = transcode_service.clone();
let job_webhooks = webhook_dispatcher.clone();
@ -306,7 +307,7 @@ async fn main() -> Result<()> {
config.jobs.worker_count,
config.jobs.job_timeout_secs,
move |job_id, kind, cancel, jobs| {
let storage = job_storage.clone();
let storage = Arc::clone(&job_storage);
let config = job_config.clone();
let transcode_svc = job_transcode.clone();
let webhooks = job_webhooks.clone();
@ -400,10 +401,16 @@ async fn main() -> Result<()> {
if cancel.is_cancelled() {
break;
}
#[expect(
clippy::cast_precision_loss,
reason = "progress ratio; precision loss negligible for \
display"
)]
let progress = i as f32 / total as f32;
JobQueue::update_progress(
&jobs,
job_id,
i as f32 / total as f32,
progress,
format!("{i}/{total}"),
)
.await;
@ -575,7 +582,12 @@ async fn main() -> Result<()> {
enrich_cfg.sources.tmdb.enabled,
enrich_cfg.sources.tmdb.api_key.clone(),
) {
enrichers.push(Box::new(TmdbEnricher::new(key)));
match TmdbEnricher::new(key) {
Ok(e) => enrichers.push(Box::new(e)),
Err(err) => {
tracing::warn!("Failed to build TMDB enricher: {err}");
},
}
}
if let (true, Some(key)) = (
enrich_cfg.sources.lastfm.enabled,
@ -613,7 +625,6 @@ async fn main() -> Result<()> {
let category = item.media_type.category();
for enricher in &enrichers {
let source = enricher.source();
use pinakes_enrichment::EnrichmentSourceType;
let applicable = match source {
EnrichmentSourceType::MusicBrainz
| EnrichmentSourceType::LastFm => {
@ -674,7 +685,7 @@ async fn main() -> Result<()> {
JobKind::CleanupAnalytics => {
let retention_days = config.analytics.retention_days;
let before = chrono::Utc::now()
- chrono::Duration::days(retention_days as i64);
- chrono::Duration::days(retention_days.cast_signed());
match storage.cleanup_old_events(before).await {
Ok(count) => {
JobQueue::complete(
@ -690,7 +701,7 @@ async fn main() -> Result<()> {
JobKind::TrashPurge => {
let retention_days = config.trash.retention_days;
let before = chrono::Utc::now()
- chrono::Duration::days(retention_days as i64);
- chrono::Duration::days(retention_days.cast_signed());
match storage.purge_old_trash(before).await {
Ok(count) => {
@ -723,9 +734,9 @@ async fn main() -> Result<()> {
let shutdown_token = tokio_util::sync::CancellationToken::new();
let config_arc = Arc::new(RwLock::new(config));
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
job_queue.clone(),
Arc::clone(&job_queue),
shutdown_token.clone(),
config_arc.clone(),
Arc::clone(&config_arc),
Some(config_path.clone()),
);
let scheduler = Arc::new(scheduler);
@ -735,7 +746,7 @@ async fn main() -> Result<()> {
// Spawn scheduler background loop
{
let scheduler = scheduler.clone();
let scheduler = Arc::clone(&scheduler);
tokio::spawn(async move {
scheduler.run().await;
});
@ -796,8 +807,8 @@ async fn main() -> Result<()> {
};
let state = AppState {
storage: storage.clone(),
config: config_arc.clone(),
storage: Arc::clone(&storage),
config: Arc::clone(&config_arc),
config_path: Some(config_path),
scan_progress: pinakes_core::scan::ScanProgress::new(),
job_queue,
@ -816,7 +827,7 @@ async fn main() -> Result<()> {
// Periodic session cleanup (every 15 minutes)
{
let storage_clone = storage.clone();
let storage_clone = Arc::clone(&storage);
let cancel = shutdown_token.clone();
tokio::spawn(async move {
let mut interval =
@ -844,7 +855,7 @@ async fn main() -> Result<()> {
// Periodic chunked upload cleanup (every hour)
if let Some(ref manager) = state.chunked_upload_manager {
let manager_clone = manager.clone();
let manager_clone = Arc::clone(manager);
let cancel = shutdown_token.clone();
tokio::spawn(async move {
let mut interval =

View file

@ -39,6 +39,9 @@ const MAX_LIMIT: u64 = 100;
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_most_viewed(
State(state): State<AppState>,
Query(params): Query<PaginationParams>,
@ -74,6 +77,9 @@ pub async fn get_most_viewed(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_recently_viewed(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -103,6 +109,9 @@ pub async fn get_recently_viewed(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn record_event(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -141,6 +150,9 @@ pub async fn record_event(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_watch_progress(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -174,6 +186,9 @@ pub async fn get_watch_progress(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_watch_progress(
State(state): State<AppState>,
Extension(username): Extension<String>,

View file

@ -24,6 +24,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_audit(
State(state): State<AppState>,
Query(params): Query<PaginationParams>,

View file

@ -1,8 +1,10 @@
use argon2::password_hash::PasswordVerifier;
use axum::{
Json,
extract::State,
http::{HeaderMap, StatusCode},
};
use rand::seq::IndexedRandom as _;
use crate::{
dto::{LoginRequest, LoginResponse, UserInfoResponse},
@ -17,6 +19,16 @@ const DUMMY_HASH: &str =
"$argon2id$v=19$m=19456,t=2,\
p=1$VGltaW5nU2FmZUR1bW15$c2ltdWxhdGVkX2hhc2hfZm9yX3RpbWluZ19zYWZldHk";
/// Authenticate a user with username and password, creating a session.
///
/// # Errors
///
/// Returns an error if the credentials are invalid or the session cannot be
/// created.
///
/// # Panics
///
/// Panics if the CHARSET is empty (it is not).
#[utoipa::path(
post,
path = "/api/v1/auth/login",
@ -53,12 +65,8 @@ pub async fn login(
// Always perform password verification to prevent timing attacks.
// If the user doesn't exist, we verify against a dummy hash to ensure
// consistent response times regardless of whether the username exists.
use argon2::password_hash::PasswordVerifier;
let (hash_to_verify, user_found) = match user {
Some(u) => (&u.password_hash as &str, true),
None => (DUMMY_HASH, false),
};
let (hash_to_verify, user_found) =
user.map_or((DUMMY_HASH, false), |u| (&u.password_hash as &str, true));
let parsed_hash = argon2::password_hash::PasswordHash::new(hash_to_verify)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
@ -97,13 +105,14 @@ pub async fn login(
// Generate session token using unbiased uniform distribution
#[expect(clippy::expect_used)]
let token: String = {
use rand::seq::IndexedRandom;
const CHARSET: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rand::rng();
(0..48)
.map(|_| *CHARSET.choose(&mut rng).expect("non-empty charset") as char)
.collect()
std::iter::repeat_with(|| {
*CHARSET.choose(&mut rng).expect("non-empty charset") as char
})
.take(48)
.collect()
};
let role = user.role;
@ -118,7 +127,9 @@ pub async fn login(
role: role.to_string(),
created_at: now,
expires_at: now
+ chrono::Duration::hours(config.accounts.session_expiry_hours as i64),
+ chrono::Duration::hours(
config.accounts.session_expiry_hours.cast_signed(),
),
last_accessed: now,
};
@ -195,6 +206,12 @@ pub async fn logout(
StatusCode::OK
}
/// Return current user info from the bearer token session.
///
/// # Errors
///
/// Returns an error if the token is missing, invalid, or the session lookup
/// fails.
#[utoipa::path(
get,
path = "/api/v1/auth/me",
@ -243,6 +260,11 @@ fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
/// Refresh the current session, extending its expiry by the configured
/// duration.
///
/// # Errors
///
/// Returns an error if the token is missing, the session does not exist, or the
/// database update fails.
#[utoipa::path(
post,
path = "/api/v1/auth/refresh",
@ -261,7 +283,7 @@ pub async fn refresh(
let token = extract_bearer_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
let config = state.config.read().await;
let expiry_hours = config.accounts.session_expiry_hours as i64;
let expiry_hours = config.accounts.session_expiry_hours.cast_signed();
drop(config);
let new_expires_at =
@ -297,9 +319,8 @@ pub async fn revoke_all_sessions(
State(state): State<AppState>,
headers: HeaderMap,
) -> StatusCode {
let token = match extract_bearer_token(&headers) {
Some(t) => t,
None => return StatusCode::UNAUTHORIZED,
let Some(token) = extract_bearer_token(&headers) else {
return StatusCode::UNAUTHORIZED;
};
// Get current session to find username
@ -340,7 +361,11 @@ pub async fn revoke_all_sessions(
}
}
/// List all active sessions (admin only)
/// List all active sessions (admin only).
///
/// # Errors
///
/// Returns an error if the database query fails.
#[derive(serde::Serialize, utoipa::ToSchema)]
pub struct SessionListResponse {
pub sessions: Vec<SessionInfo>,
@ -367,6 +392,9 @@ pub struct SessionInfo {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_active_sessions(
State(state): State<AppState>,
) -> Result<Json<SessionListResponse>, StatusCode> {

View file

@ -23,6 +23,9 @@ use crate::{error::ApiError, state::AppState};
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_backup(
State(state): State<AppState>,
) -> Result<Response, ApiError> {

View file

@ -168,19 +168,23 @@ pub struct AuthorSummary {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_book_metadata(
State(state): State<AppState>,
Path(media_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
let media_id = MediaId(media_id);
let metadata =
state
.storage
.get_book_metadata(media_id)
.await?
.ok_or(ApiError(PinakesError::NotFound(
let metadata = state
.storage
.get_book_metadata(media_id)
.await?
.ok_or_else(|| {
ApiError(PinakesError::NotFound(
"Book metadata not found".to_string(),
)))?;
))
})?;
Ok(Json(BookMetadataResponse::from(metadata)))
}
@ -206,6 +210,9 @@ pub async fn get_book_metadata(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_books(
State(state): State<AppState>,
Query(query): Query<SearchBooksQuery>,
@ -247,6 +254,9 @@ pub async fn list_books(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_series(
State(state): State<AppState>,
) -> Result<impl IntoResponse, ApiError> {
@ -276,6 +286,9 @@ pub async fn list_series(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_series_books(
State(state): State<AppState>,
Path(series_name): Path<String>,
@ -304,6 +317,9 @@ pub async fn get_series_books(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_authors(
State(state): State<AppState>,
Query(pagination): Query<Pagination>,
@ -338,6 +354,9 @@ pub async fn list_authors(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_author_books(
State(state): State<AppState>,
Path(author_name): Path<String>,
@ -369,6 +388,9 @@ pub async fn get_author_books(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_reading_progress(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -381,9 +403,11 @@ pub async fn get_reading_progress(
.storage
.get_reading_progress(user_id.0, media_id)
.await?
.ok_or(ApiError(PinakesError::NotFound(
"Reading progress not found".to_string(),
)))?;
.ok_or_else(|| {
ApiError(PinakesError::NotFound(
"Reading progress not found".to_string(),
))
})?;
Ok(Json(ReadingProgressResponse::from(progress)))
}
@ -402,6 +426,9 @@ pub async fn get_reading_progress(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_reading_progress(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -438,6 +465,9 @@ pub async fn update_reading_progress(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_reading_list(
State(state): State<AppState>,
Extension(username): Extension<String>,

View file

@ -30,6 +30,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_collection(
State(state): State<AppState>,
Json(req): Json<CreateCollectionRequest>,
@ -85,6 +88,9 @@ pub async fn create_collection(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_collections(
State(state): State<AppState>,
) -> Result<Json<Vec<CollectionResponse>>, ApiError> {
@ -107,6 +113,9 @@ pub async fn list_collections(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_collection(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -129,6 +138,9 @@ pub async fn get_collection(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_collection(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -158,6 +170,9 @@ pub async fn delete_collection(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn add_member(
State(state): State<AppState>,
Path(collection_id): Path<Uuid>,
@ -190,6 +205,9 @@ pub async fn add_member(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn remove_member(
State(state): State<AppState>,
Path((collection_id, media_id)): Path<(Uuid, Uuid)>,
@ -216,6 +234,9 @@ pub async fn remove_member(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_members(
State(state): State<AppState>,
Path(collection_id): Path<Uuid>,

View file

@ -26,6 +26,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_config(
State(state): State<AppState>,
) -> Result<Json<ConfigResponse>, ApiError> {
@ -36,18 +39,15 @@ pub async fn get_config(
.config_path
.as_ref()
.map(|p| p.to_string_lossy().to_string());
let config_writable = match &state.config_path {
Some(path) => {
if path.exists() {
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
} else {
path.parent().is_some_and(|parent| {
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
})
}
},
None => false,
};
let config_writable = state.config_path.as_ref().is_some_and(|path| {
if path.exists() {
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
} else {
path.parent().is_some_and(|parent| {
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
})
}
});
Ok(Json(ConfigResponse {
backend: config.storage.backend.to_string(),
@ -86,6 +86,9 @@ pub async fn get_config(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_ui_config(
State(state): State<AppState>,
) -> Result<Json<UiConfigResponse>, ApiError> {
@ -106,6 +109,9 @@ pub async fn get_ui_config(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_ui_config(
State(state): State<AppState>,
Json(req): Json<UpdateUiConfigRequest>,
@ -153,6 +159,9 @@ pub async fn update_ui_config(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_scanning_config(
State(state): State<AppState>,
Json(req): Json<UpdateScanningRequest>,
@ -179,18 +188,15 @@ pub async fn update_scanning_config(
.config_path
.as_ref()
.map(|p| p.to_string_lossy().to_string());
let config_writable = match &state.config_path {
Some(path) => {
if path.exists() {
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
} else {
path.parent().is_some_and(|parent| {
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
})
}
},
None => false,
};
let config_writable = state.config_path.as_ref().is_some_and(|path| {
if path.exists() {
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
} else {
path.parent().is_some_and(|parent| {
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
})
}
});
Ok(Json(ConfigResponse {
backend: config.storage.backend.to_string(),
@ -232,6 +238,9 @@ pub async fn update_scanning_config(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn add_root(
State(state): State<AppState>,
Json(req): Json<RootDirRequest>,
@ -272,6 +281,9 @@ pub async fn add_root(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn remove_root(
State(state): State<AppState>,
Json(req): Json<RootDirRequest>,

View file

@ -14,6 +14,9 @@ use crate::{dto::DatabaseStatsResponse, error::ApiError, state::AppState};
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn database_stats(
State(state): State<AppState>,
) -> Result<Json<DatabaseStatsResponse>, ApiError> {
@ -40,6 +43,9 @@ pub async fn database_stats(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn vacuum_database(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
@ -59,6 +65,9 @@ pub async fn vacuum_database(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn clear_database(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {

View file

@ -17,6 +17,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_duplicates(
State(state): State<AppState>,
) -> Result<Json<Vec<DuplicateGroupResponse>>, ApiError> {

View file

@ -25,6 +25,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trigger_enrichment(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -52,6 +55,9 @@ pub async fn trigger_enrichment(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_external_metadata(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -79,6 +85,9 @@ pub async fn get_external_metadata(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_enrich(
State(state): State<AppState>,
Json(req): Json<BatchDeleteRequest>, // Reuse: has media_ids field

View file

@ -24,6 +24,9 @@ pub struct ExportRequest {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trigger_export(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
@ -51,6 +54,9 @@ pub async fn trigger_export(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trigger_export_with_options(
State(state): State<AppState>,
Json(req): Json<ExportRequest>,

View file

@ -66,7 +66,8 @@ pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
Ok(count) => {
DatabaseHealth {
status: "ok".to_string(),
latency_ms: db_start.elapsed().as_millis() as u64,
latency_ms: u64::try_from(db_start.elapsed().as_millis())
.unwrap_or(u64::MAX),
media_count: Some(count),
}
},
@ -74,7 +75,8 @@ pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
response.status = "degraded".to_string();
DatabaseHealth {
status: format!("error: {e}"),
latency_ms: db_start.elapsed().as_millis() as u64,
latency_ms: u64::try_from(db_start.elapsed().as_millis())
.unwrap_or(u64::MAX),
media_count: None,
}
},
@ -147,7 +149,8 @@ pub async fn readiness(State(state): State<AppState>) -> impl IntoResponse {
let db_start = Instant::now();
match state.storage.count_media().await {
Ok(_) => {
let latency = db_start.elapsed().as_millis() as u64;
let latency =
u64::try_from(db_start.elapsed().as_millis()).unwrap_or(u64::MAX);
(
StatusCode::OK,
Json(serde_json::json!({
@ -203,7 +206,8 @@ pub async fn health_detailed(
Ok(count) => ("ok".to_string(), Some(count)),
Err(e) => (format!("error: {e}"), None),
};
let db_latency = db_start.elapsed().as_millis() as u64;
let db_latency =
u64::try_from(db_start.elapsed().as_millis()).unwrap_or(u64::MAX);
// Check filesystem
let roots = state.storage.list_root_dirs().await.unwrap_or_default();

View file

@ -21,6 +21,9 @@ pub struct OrphanResolveRequest {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trigger_orphan_detection(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
@ -42,6 +45,9 @@ pub async fn trigger_orphan_detection(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trigger_verify_integrity(
State(state): State<AppState>,
Json(req): Json<VerifyIntegrityRequest>,
@ -73,6 +79,9 @@ pub struct VerifyIntegrityRequest {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trigger_cleanup_thumbnails(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
@ -102,6 +111,9 @@ pub struct GenerateThumbnailsRequest {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn generate_all_thumbnails(
State(state): State<AppState>,
body: Option<Json<GenerateThumbnailsRequest>>,
@ -140,6 +152,9 @@ pub async fn generate_all_thumbnails(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn resolve_orphans(
State(state): State<AppState>,
Json(req): Json<OrphanResolveRequest>,

View file

@ -34,6 +34,9 @@ pub async fn list_jobs(State(state): State<AppState>) -> Json<Vec<Job>> {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_job(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
@ -57,6 +60,9 @@ pub async fn get_job(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn cancel_job(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,

View file

@ -2,7 +2,10 @@ use axum::{
Json,
extract::{Path, Query, State},
};
use pinakes_core::{model::MediaId, storage::DynStorageBackend};
use pinakes_core::{
model::{CustomField, CustomFieldType, MediaId},
storage::DynStorageBackend,
};
use rustc_hash::FxHashMap;
use uuid::Uuid;
@ -113,6 +116,9 @@ async fn apply_import_post_processing(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn import_media(
State(state): State<AppState>,
Json(req): Json<ImportRequest>,
@ -156,6 +162,9 @@ pub async fn import_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_media(
State(state): State<AppState>,
Query(params): Query<PaginationParams>,
@ -184,6 +193,9 @@ pub async fn list_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -199,7 +211,7 @@ const MAX_SHORT_TEXT: usize = 500;
const MAX_LONG_TEXT: usize = 10_000;
fn validate_optional_text(
field: &Option<String>,
field: Option<&str>,
name: &str,
max: usize,
) -> Result<(), ApiError> {
@ -231,16 +243,23 @@ fn validate_optional_text(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(req): Json<UpdateMediaRequest>,
) -> Result<Json<MediaResponse>, ApiError> {
validate_optional_text(&req.title, "title", MAX_SHORT_TEXT)?;
validate_optional_text(&req.artist, "artist", MAX_SHORT_TEXT)?;
validate_optional_text(&req.album, "album", MAX_SHORT_TEXT)?;
validate_optional_text(&req.genre, "genre", MAX_SHORT_TEXT)?;
validate_optional_text(&req.description, "description", MAX_LONG_TEXT)?;
validate_optional_text(req.title.as_deref(), "title", MAX_SHORT_TEXT)?;
validate_optional_text(req.artist.as_deref(), "artist", MAX_SHORT_TEXT)?;
validate_optional_text(req.album.as_deref(), "album", MAX_SHORT_TEXT)?;
validate_optional_text(req.genre.as_deref(), "genre", MAX_SHORT_TEXT)?;
validate_optional_text(
req.description.as_deref(),
"description",
MAX_LONG_TEXT,
)?;
let mut item = state.storage.get_media(MediaId(id)).await?;
@ -302,6 +321,9 @@ pub async fn update_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -353,6 +375,9 @@ pub async fn delete_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn open_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -384,6 +409,9 @@ pub async fn open_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn stream_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -509,6 +537,9 @@ fn parse_range(header: &str, total_size: u64) -> Option<(u64, u64)> {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn import_with_options(
State(state): State<AppState>,
Json(req): Json<ImportWithOptionsRequest>,
@ -557,6 +588,9 @@ pub async fn import_with_options(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_import(
State(state): State<AppState>,
Json(req): Json<BatchImportRequest>,
@ -645,6 +679,9 @@ pub async fn batch_import(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn import_directory_endpoint(
State(state): State<AppState>,
Json(req): Json<DirectoryImportRequest>,
@ -713,6 +750,50 @@ pub async fn import_directory_endpoint(
}))
}
fn walk_dir_preview(
dir: &std::path::Path,
recursive: bool,
roots: &[std::path::PathBuf],
result: &mut Vec<DirectoryPreviewFile>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
// Skip hidden files/dirs
if path
.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with('.'))
{
continue;
}
if path.is_dir() {
if recursive {
walk_dir_preview(&path, recursive, roots, result);
}
} else if path.is_file()
&& let Some(mt) = pinakes_core::media_type::MediaType::from_path(&path)
{
let size = entry.metadata().ok().map_or(0, |m| m.len());
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let media_type = serde_json::to_value(mt)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_default();
result.push(DirectoryPreviewFile {
path: crate::dto::relativize_path(&path, roots),
file_name,
media_type,
file_size: size,
});
}
}
}
#[utoipa::path(
post,
path = "/api/v1/media/import/preview",
@ -726,6 +807,9 @@ pub async fn import_directory_endpoint(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn preview_directory(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
@ -765,51 +849,7 @@ pub async fn preview_directory(
let files: Vec<DirectoryPreviewFile> =
tokio::task::spawn_blocking(move || {
let mut result = Vec::new();
fn walk_dir(
dir: &std::path::Path,
recursive: bool,
roots: &[std::path::PathBuf],
result: &mut Vec<DirectoryPreviewFile>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
// Skip hidden files/dirs
if path
.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with('.'))
{
continue;
}
if path.is_dir() {
if recursive {
walk_dir(&path, recursive, roots, result);
}
} else if path.is_file()
&& let Some(mt) =
pinakes_core::media_type::MediaType::from_path(&path)
{
let size = entry.metadata().ok().map_or(0, |m| m.len());
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let media_type = serde_json::to_value(mt)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_default();
result.push(DirectoryPreviewFile {
path: crate::dto::relativize_path(&path, roots),
file_name,
media_type,
file_size: size,
});
}
}
}
walk_dir(&dir, recursive, &roots_for_walk, &mut result);
walk_dir_preview(&dir, recursive, &roots_for_walk, &mut result);
result
})
.await
@ -843,6 +883,9 @@ pub async fn preview_directory(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn set_custom_field(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -862,7 +905,6 @@ pub async fn set_custom_field(
)),
));
}
use pinakes_core::model::{CustomField, CustomFieldType};
let field_type = match req.field_type.as_str() {
"number" => CustomFieldType::Number,
"date" => CustomFieldType::Date,
@ -897,6 +939,9 @@ pub async fn set_custom_field(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_custom_field(
State(state): State<AppState>,
Path((id, name)): Path<(Uuid, String)>,
@ -922,6 +967,9 @@ pub async fn delete_custom_field(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_tag(
State(state): State<AppState>,
Json(req): Json<BatchTagRequest>,
@ -943,7 +991,7 @@ pub async fn batch_tag(
{
Ok(count) => {
Ok(Json(BatchOperationResponse {
processed: count as usize,
processed: usize::try_from(count).unwrap_or(usize::MAX),
errors: Vec::new(),
}))
},
@ -968,6 +1016,9 @@ pub async fn batch_tag(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_all_media(
State(state): State<AppState>,
) -> Result<Json<BatchOperationResponse>, ApiError> {
@ -986,7 +1037,7 @@ pub async fn delete_all_media(
match state.storage.delete_all_media().await {
Ok(count) => {
Ok(Json(BatchOperationResponse {
processed: count as usize,
processed: usize::try_from(count).unwrap_or(usize::MAX),
errors: Vec::new(),
}))
},
@ -1013,6 +1064,9 @@ pub async fn delete_all_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_delete(
State(state): State<AppState>,
Json(req): Json<BatchDeleteRequest>,
@ -1044,7 +1098,7 @@ pub async fn batch_delete(
match state.storage.batch_delete_media(&media_ids).await {
Ok(count) => {
Ok(Json(BatchOperationResponse {
processed: count as usize,
processed: usize::try_from(count).unwrap_or(usize::MAX),
errors: Vec::new(),
}))
},
@ -1071,6 +1125,9 @@ pub async fn batch_delete(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_add_to_collection(
State(state): State<AppState>,
Json(req): Json<BatchCollectionRequest>,
@ -1090,7 +1147,7 @@ pub async fn batch_add_to_collection(
&state.storage,
req.collection_id,
MediaId(*media_id),
i as i32,
i32::try_from(i).unwrap_or(i32::MAX),
)
.await
{
@ -1115,6 +1172,9 @@ pub async fn batch_add_to_collection(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_update(
State(state): State<AppState>,
Json(req): Json<BatchUpdateRequest>,
@ -1144,7 +1204,7 @@ pub async fn batch_update(
{
Ok(count) => {
Ok(Json(BatchOperationResponse {
processed: count as usize,
processed: usize::try_from(count).unwrap_or(usize::MAX),
errors: Vec::new(),
}))
},
@ -1170,6 +1230,9 @@ pub async fn batch_update(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_thumbnail(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -1214,6 +1277,9 @@ pub async fn get_thumbnail(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_media_count(
State(state): State<AppState>,
) -> Result<Json<MediaCountResponse>, ApiError> {
@ -1237,6 +1303,9 @@ pub async fn get_media_count(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn rename_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -1305,6 +1374,9 @@ pub async fn rename_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn move_media_endpoint(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -1368,6 +1440,9 @@ pub async fn move_media_endpoint(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_move_media(
State(state): State<AppState>,
Json(req): Json<BatchMoveRequest>,
@ -1451,6 +1526,9 @@ pub async fn batch_move_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn soft_delete_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -1511,6 +1589,9 @@ pub async fn soft_delete_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn restore_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -1573,6 +1654,9 @@ pub async fn restore_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_trash(
State(state): State<AppState>,
Query(params): Query<PaginationParams>,
@ -1603,6 +1687,9 @@ pub async fn list_trash(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trash_info(
State(state): State<AppState>,
) -> Result<Json<TrashInfoResponse>, ApiError> {
@ -1622,6 +1709,9 @@ pub async fn trash_info(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn empty_trash(
State(state): State<AppState>,
) -> Result<Json<EmptyTrashResponse>, ApiError> {
@ -1656,6 +1746,14 @@ pub async fn empty_trash(
),
security(("bearer_auth" = []))
)]
// axum handlers cannot be generic over hasher types without breaking routing
#[expect(
clippy::implicit_hasher,
reason = "axum handler; generic over hasher breaks routing"
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn permanent_delete_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,

View file

@ -12,13 +12,16 @@ use axum::{
extract::{Path, Query, State},
routing::{get, post},
};
use pinakes_core::model::{
BacklinkInfo,
GraphData,
GraphEdge,
GraphNode,
MarkdownLink,
MediaId,
use pinakes_core::{
media_type::{BuiltinMediaType, MediaType},
model::{
BacklinkInfo,
GraphData,
GraphEdge,
GraphNode,
MarkdownLink,
MediaId,
},
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@ -214,6 +217,9 @@ pub struct UnresolvedLinksResponse {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_backlinks(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -247,6 +253,9 @@ pub async fn get_backlinks(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_outgoing_links(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -282,6 +291,9 @@ pub async fn get_outgoing_links(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_graph(
State(state): State<AppState>,
Query(params): Query<GraphQuery>,
@ -310,6 +322,9 @@ pub async fn get_graph(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn reindex_links(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -320,7 +335,6 @@ pub async fn reindex_links(
let media = state.storage.get_media(media_id).await?;
// Only process markdown files
use pinakes_core::media_type::{BuiltinMediaType, MediaType};
match &media.media_type {
MediaType::Builtin(BuiltinMediaType::Markdown) => {},
_ => {
@ -369,6 +383,9 @@ pub async fn reindex_links(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn resolve_links(
State(state): State<AppState>,
) -> Result<Json<ResolveLinksResponse>, ApiError> {
@ -391,6 +408,9 @@ pub async fn resolve_links(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_unresolved_count(
State(state): State<AppState>,
) -> Result<Json<UnresolvedLinksResponse>, ApiError> {

View file

@ -81,6 +81,10 @@ pub struct MapMarker {
security(("bearer_auth" = []))
)]
/// Get timeline of photos grouped by date
///
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_timeline(
State(state): State<AppState>,
Query(query): Query<TimelineQuery>,
@ -183,6 +187,10 @@ pub async fn get_timeline(
security(("bearer_auth" = []))
)]
/// Get photos in a bounding box for map view
///
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_map_photos(
State(state): State<AppState>,
Query(query): Query<MapQuery>,

View file

@ -3,6 +3,7 @@ use axum::{
extract::{Extension, Path, State},
};
use pinakes_core::{model::MediaId, playlists::Playlist, users::UserId};
use rand::seq::SliceRandom as _;
use uuid::Uuid;
use crate::{
@ -64,6 +65,9 @@ async fn check_playlist_access(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -102,6 +106,9 @@ pub async fn create_playlist(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_playlists(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -130,6 +137,9 @@ pub async fn list_playlists(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -156,6 +166,9 @@ pub async fn get_playlist(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -198,6 +211,9 @@ pub async fn update_playlist(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -223,6 +239,9 @@ pub async fn delete_playlist(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn add_item(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -235,7 +254,7 @@ pub async fn add_item(
p
} else {
let items = state.storage.get_playlist_items(id).await?;
items.len() as i32
i32::try_from(items.len()).unwrap_or(i32::MAX)
};
state
.storage
@ -260,6 +279,9 @@ pub async fn add_item(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn remove_item(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -287,6 +309,9 @@ pub async fn remove_item(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_items(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -318,6 +343,9 @@ pub async fn list_items(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn reorder_item(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -346,6 +374,9 @@ pub async fn reorder_item(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn shuffle_playlist(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -353,7 +384,6 @@ pub async fn shuffle_playlist(
) -> 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());
let roots = state.config.read().await.directories.roots.clone();

View file

@ -42,6 +42,9 @@ fn require_plugin_manager(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_plugins(
State(state): State<AppState>,
) -> Result<Json<Vec<PluginResponse>>, ApiError> {
@ -69,6 +72,9 @@ pub async fn list_plugins(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_plugin(
State(state): State<AppState>,
Path(id): Path<String>,
@ -99,6 +105,9 @@ pub async fn get_plugin(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn install_plugin(
State(state): State<AppState>,
Json(req): Json<InstallPluginRequest>,
@ -140,6 +149,9 @@ pub async fn install_plugin(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn uninstall_plugin(
State(state): State<AppState>,
Path(id): Path<String>,
@ -170,6 +182,9 @@ pub async fn uninstall_plugin(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn toggle_plugin(
State(state): State<AppState>,
Path(id): Path<String>,
@ -219,6 +234,9 @@ pub async fn toggle_plugin(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_plugin_ui_pages(
State(state): State<AppState>,
) -> Result<Json<Vec<PluginUiPageEntry>>, ApiError> {
@ -249,6 +267,9 @@ pub async fn list_plugin_ui_pages(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_plugin_ui_widgets(
State(state): State<AppState>,
) -> Result<Json<Vec<PluginUiWidgetEntry>>, ApiError> {
@ -275,6 +296,9 @@ pub async fn list_plugin_ui_widgets(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn emit_plugin_event(
State(state): State<AppState>,
Json(req): Json<PluginEventRequest>,
@ -297,6 +321,9 @@ pub async fn emit_plugin_event(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_plugin_ui_theme_extensions(
State(state): State<AppState>,
) -> Result<Json<FxHashMap<String, String>>, ApiError> {
@ -318,6 +345,9 @@ pub async fn list_plugin_ui_theme_extensions(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn reload_plugin(
State(state): State<AppState>,
Path(id): Path<String>,

View file

@ -44,6 +44,9 @@ const VALID_SORT_ORDERS: &[&str] = &[
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_saved_search(
State(state): State<AppState>,
Json(req): Json<CreateSavedSearchRequest>,
@ -100,6 +103,9 @@ pub async fn create_saved_search(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_saved_searches(
State(state): State<AppState>,
) -> Result<Json<Vec<SavedSearchResponse>>, ApiError> {
@ -137,6 +143,9 @@ pub async fn list_saved_searches(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_saved_search(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,

View file

@ -20,6 +20,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn trigger_scan(
State(state): State<AppState>,
Json(req): Json<ScanRequest>,

View file

@ -16,6 +16,9 @@ use crate::{dto::ScheduledTaskResponse, error::ApiError, state::AppState};
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_scheduled_tasks(
State(state): State<AppState>,
) -> Result<Json<Vec<ScheduledTaskResponse>>, ApiError> {
@ -50,23 +53,26 @@ pub async fn list_scheduled_tasks(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn toggle_scheduled_task(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
match state.scheduler.toggle_task(&id).await {
Some(enabled) => {
state.scheduler.toggle_task(&id).await.map_or_else(
|| {
Err(ApiError(pinakes_core::error::PinakesError::NotFound(
format!("scheduled task not found: {id}"),
)))
},
|enabled| {
Ok(Json(serde_json::json!({
"id": id,
"enabled": enabled,
})))
},
None => {
Err(ApiError(pinakes_core::error::PinakesError::NotFound(
format!("scheduled task not found: {id}"),
)))
},
}
)
}
#[utoipa::path(
@ -82,21 +88,24 @@ pub async fn toggle_scheduled_task(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn run_scheduled_task_now(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
match state.scheduler.run_now(&id).await {
Some(job_id) => {
state.scheduler.run_now(&id).await.map_or_else(
|| {
Err(ApiError(pinakes_core::error::PinakesError::NotFound(
format!("scheduled task not found: {id}"),
)))
},
|job_id| {
Ok(Json(serde_json::json!({
"id": id,
"job_id": job_id,
})))
},
None => {
Err(ApiError(pinakes_core::error::PinakesError::NotFound(
format!("scheduled task not found: {id}"),
)))
},
}
)
}

View file

@ -40,6 +40,9 @@ fn resolve_sort(sort: Option<&str>) -> SortOrder {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn search(
State(state): State<AppState>,
Query(params): Query<SearchParams>,
@ -87,6 +90,9 @@ pub async fn search(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn search_post(
State(state): State<AppState>,
Json(body): Json<SearchRequestBody>,

View file

@ -61,6 +61,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_share(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -149,27 +152,28 @@ pub async fn create_share(
};
// Parse permissions
let permissions = if let Some(perms) = req.permissions {
SharePermissions {
view: ShareViewPermissions {
can_view: perms.can_view.unwrap_or(true),
can_download: perms.can_download.unwrap_or(false),
can_reshare: perms.can_reshare.unwrap_or(false),
},
mutate: ShareMutatePermissions {
can_edit: perms.can_edit.unwrap_or(false),
can_delete: perms.can_delete.unwrap_or(false),
can_add: perms.can_add.unwrap_or(false),
},
}
} else {
SharePermissions::view_only()
};
let permissions =
req
.permissions
.map_or_else(SharePermissions::view_only, |perms| {
SharePermissions {
view: ShareViewPermissions {
can_view: perms.can_view.unwrap_or(true),
can_download: perms.can_download.unwrap_or(false),
can_reshare: perms.can_reshare.unwrap_or(false),
},
mutate: ShareMutatePermissions {
can_edit: perms.can_edit.unwrap_or(false),
can_delete: perms.can_delete.unwrap_or(false),
can_add: perms.can_add.unwrap_or(false),
},
}
});
// Calculate expiration
let expires_at = req
.expires_in_hours
.map(|hours| Utc::now() + chrono::Duration::hours(hours as i64));
let expires_at = req.expires_in_hours.map(|hours: u64| {
Utc::now() + chrono::Duration::hours(hours.cast_signed())
});
let share = Share {
id: ShareId(Uuid::now_v7()),
@ -228,6 +232,9 @@ pub async fn create_share(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_outgoing(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -261,6 +268,9 @@ pub async fn list_outgoing(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_incoming(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -293,6 +303,9 @@ pub async fn list_incoming(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_share(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -337,6 +350,9 @@ pub async fn get_share(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_share(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -430,6 +446,9 @@ pub async fn update_share(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_share(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -487,6 +506,9 @@ pub async fn delete_share(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn batch_delete(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -540,6 +562,9 @@ pub async fn batch_delete(
(status = 404, description = "Not found"),
)
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn access_shared(
State(state): State<AppState>,
Path(token): Path<String>,
@ -618,8 +643,8 @@ pub async fn access_shared(
.await
.map_err(|e| ApiError::not_found(format!("Media not found: {e}")))?;
Ok(Json(SharedContentResponse::Single(MediaResponse::new(
item, &roots,
Ok(Json(SharedContentResponse::Single(Box::new(
MediaResponse::new(item, &roots),
))))
},
ShareTarget::Collection { collection_id } => {
@ -724,6 +749,9 @@ pub async fn access_shared(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_activity(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -767,6 +795,9 @@ pub async fn get_activity(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_notifications(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -796,6 +827,9 @@ pub async fn get_notifications(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn mark_notification_read(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -823,6 +857,9 @@ pub async fn mark_notification_read(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn mark_all_read(
State(state): State<AppState>,
Extension(username): Extension<String>,

View file

@ -22,6 +22,8 @@ use crate::{
state::AppState,
};
const MAX_SHARE_EXPIRY_HOURS: u64 = 8760; // 1 year
#[derive(Deserialize)]
pub struct ShareLinkQuery {
pub password: Option<String>,
@ -41,6 +43,9 @@ pub struct ShareLinkQuery {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn rate_media(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -85,6 +90,9 @@ pub async fn rate_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_media_ratings(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -109,6 +117,9 @@ pub async fn get_media_ratings(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn add_comment(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -143,6 +154,9 @@ pub async fn add_comment(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_media_comments(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -165,6 +179,9 @@ pub async fn get_media_comments(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn add_favorite(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -190,6 +207,9 @@ pub async fn add_favorite(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn remove_favorite(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -214,6 +234,9 @@ pub async fn remove_favorite(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_favorites(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -245,6 +268,9 @@ pub async fn list_favorites(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_share_link(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -265,19 +291,18 @@ pub async fn create_share_link(
},
None => None,
};
const MAX_EXPIRY_HOURS: u64 = 8760; // 1 year
if let Some(h) = req.expires_in_hours
&& h > MAX_EXPIRY_HOURS
&& h > MAX_SHARE_EXPIRY_HOURS
{
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(format!(
"expires_in_hours cannot exceed {MAX_EXPIRY_HOURS}"
"expires_in_hours cannot exceed {MAX_SHARE_EXPIRY_HOURS}"
)),
));
}
let expires_at = req
.expires_in_hours
.map(|h| chrono::Utc::now() + chrono::Duration::hours(h as i64));
let expires_at = req.expires_in_hours.map(|h: u64| {
chrono::Utc::now() + chrono::Duration::hours(h.cast_signed())
});
let link = state
.storage
.create_share_link(
@ -305,6 +330,9 @@ pub async fn create_share_link(
(status = 404, description = "Not found"),
)
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn access_shared_media(
State(state): State<AppState>,
Path(token): Path<String>,
@ -330,15 +358,10 @@ pub async fn access_shared_media(
}
// Verify password if set
if let Some(ref hash) = link.password_hash {
let password = match query.password.as_deref() {
Some(p) => p,
None => {
return Err(ApiError(
pinakes_core::error::PinakesError::Authentication(
"password required for this share link".into(),
),
));
},
let Some(password) = query.password.as_deref() else {
return Err(ApiError(pinakes_core::error::PinakesError::Authentication(
"password required for this share link".into(),
)));
};
let valid = pinakes_core::users::auth::verify_password(password, hash)
.unwrap_or(false);

View file

@ -13,6 +13,9 @@ use crate::{dto::LibraryStatisticsResponse, error::ApiError, state::AppState};
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn library_statistics(
State(state): State<AppState>,
) -> Result<Json<LibraryStatisticsResponse>, ApiError> {

View file

@ -1,3 +1,5 @@
use std::fmt::Write as _;
use axum::{
extract::{Path, State},
http::StatusCode,
@ -61,6 +63,9 @@ fn escape_xml(s: &str) -> String {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn hls_master_playlist(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -78,10 +83,11 @@ pub async fn hls_master_playlist(
let bandwidth = estimate_bandwidth(profile);
let encoded_name =
utf8_percent_encode(&profile.name, NON_ALPHANUMERIC).to_string();
playlist.push_str(&format!(
let _ = write!(
playlist,
"#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},RESOLUTION={w}x{h}\n/api/v1/\
media/{id}/stream/hls/{encoded_name}/playlist.m3u8\n\n",
));
);
}
build_response("application/vnd.apple.mpegurl", playlist)
@ -103,6 +109,9 @@ pub async fn hls_master_playlist(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn hls_variant_playlist(
State(state): State<AppState>,
Path((id, profile)): Path<(Uuid, String)>,
@ -118,6 +127,12 @@ pub async fn hls_variant_playlist(
));
}
let segment_duration = 10.0;
#[expect(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "duration/segment_duration is always non-negative and bounded by \
media length"
)]
let num_segments = (duration / segment_duration).ceil() as usize;
let mut playlist = String::from(
@ -126,14 +141,20 @@ pub async fn hls_variant_playlist(
);
for i in 0..num_segments.max(1) {
let seg_dur = if i == num_segments.saturating_sub(1) && duration > 0.0 {
(i as f64).mul_add(-segment_duration, duration)
#[expect(
clippy::cast_precision_loss,
reason = "segment index is small, precision loss is negligible"
)]
let i_f64 = i as f64;
i_f64.mul_add(-segment_duration, duration)
} else {
segment_duration
};
playlist.push_str(&format!("#EXTINF:{seg_dur:.3},\n"));
playlist.push_str(&format!(
"/api/v1/media/{id}/stream/hls/{profile}/segment{i}.ts\n"
));
let _ = writeln!(playlist, "#EXTINF:{seg_dur:.3},");
let _ = writeln!(
playlist,
"/api/v1/media/{id}/stream/hls/{profile}/segment{i}.ts"
);
}
playlist.push_str("#EXT-X-ENDLIST\n");
@ -157,6 +178,9 @@ pub async fn hls_variant_playlist(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn hls_segment(
State(state): State<AppState>,
Path((id, profile, segment)): Path<(Uuid, String, String)>,
@ -206,7 +230,7 @@ pub async fn hls_segment(
Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"no transcode session found; start a transcode first via POST \
/media/{id}/transcode"
/media/:id/transcode"
.into(),
),
))
@ -225,6 +249,9 @@ pub async fn hls_segment(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn dash_manifest(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -239,7 +266,19 @@ pub async fn dash_manifest(
),
));
}
#[expect(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "duration is always non-negative and bounded; hours/minutes fit \
in u32"
)]
let hours = (duration / 3600.0) as u32;
#[expect(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "duration is always non-negative and bounded; hours/minutes fit \
in u32"
)]
let minutes = ((duration % 3600.0) / 60.0) as u32;
let seconds = duration % 60.0;
@ -253,12 +292,13 @@ pub async fn dash_manifest(
let xml_name = escape_xml(&profile.name);
let url_name =
utf8_percent_encode(&profile.name, NON_ALPHANUMERIC).to_string();
representations.push_str(&format!(
r#" <Representation id="{xml_name}" bandwidth="{bandwidth}" width="{w}" height="{h}">
let _ = write!(
representations,
r#" <Representation id="{xml_name}" bandwidth="{bandwidth}" width="{w}" height="{h}">
<SegmentTemplate media="/api/v1/media/{id}/stream/dash/{url_name}/segment$Number$.m4s" initialization="/api/v1/media/{id}/stream/dash/{url_name}/init.mp4" duration="10000" timescale="1000" startNumber="0"/>
</Representation>
"#,
));
);
}
let mpd = format!(
@ -291,6 +331,9 @@ pub async fn dash_manifest(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn dash_segment(
State(state): State<AppState>,
Path((id, profile, segment)): Path<(Uuid, String, String)>,
@ -338,7 +381,7 @@ pub async fn dash_segment(
Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"no transcode session found; start a transcode first via POST \
/media/{id}/transcode"
/media/:id/transcode"
.into(),
),
))

View file

@ -1,3 +1,5 @@
use std::path::Component;
use axum::{
Json,
extract::{Path, State},
@ -38,6 +40,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_subtitles(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -74,6 +79,9 @@ pub async fn list_subtitles(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn add_subtitle(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -139,7 +147,6 @@ pub async fn add_subtitle(
let path = std::path::PathBuf::from(&path_str);
use std::path::Component;
if !path.is_absolute()
|| path.components().any(|c| c == Component::ParentDir)
{
@ -204,6 +211,9 @@ pub async fn add_subtitle(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_subtitle(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -227,6 +237,9 @@ pub async fn delete_subtitle(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_subtitle_content(
State(state): State<AppState>,
Path((media_id, subtitle_id)): Path<(Uuid, Uuid)>,
@ -300,6 +313,9 @@ pub async fn get_subtitle_content(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_offset(
State(state): State<AppState>,
Path(id): Path<Uuid>,

View file

@ -68,6 +68,9 @@ const DEFAULT_CHANGES_LIMIT: u64 = 100;
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn register_device(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -132,6 +135,9 @@ pub async fn register_device(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_devices(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -161,6 +167,9 @@ pub async fn list_devices(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_device(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -197,6 +206,9 @@ pub async fn get_device(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_device(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -246,6 +258,9 @@ pub async fn update_device(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_device(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -287,6 +302,9 @@ pub async fn delete_device(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn regenerate_token(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -342,6 +360,9 @@ pub async fn regenerate_token(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_changes(
State(state): State<AppState>,
Query(params): Query<GetChangesParams>,
@ -391,6 +412,9 @@ pub async fn get_changes(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn report_changes(
State(state): State<AppState>,
Extension(_username): Extension<String>,
@ -505,6 +529,9 @@ pub async fn report_changes(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn acknowledge_changes(
State(state): State<AppState>,
Extension(_username): Extension<String>,
@ -545,6 +572,9 @@ pub async fn acknowledge_changes(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_conflicts(
State(state): State<AppState>,
Extension(_username): Extension<String>,
@ -587,6 +617,9 @@ pub async fn list_conflicts(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn resolve_conflict(
State(state): State<AppState>,
Extension(_username): Extension<String>,
@ -625,6 +658,9 @@ pub async fn resolve_conflict(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_upload(
State(state): State<AppState>,
Extension(_username): Extension<String>,
@ -665,7 +701,8 @@ pub async fn create_upload(
chunk_count,
status: UploadStatus::Pending,
created_at: now,
expires_at: now + chrono::Duration::hours(upload_timeout_hours as i64),
expires_at: now
+ chrono::Duration::hours(upload_timeout_hours.cast_signed()),
last_activity: now,
};
@ -706,6 +743,9 @@ pub async fn create_upload(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn upload_chunk(
State(state): State<AppState>,
Path((session_id, chunk_index)): Path<(Uuid, u64)>,
@ -767,6 +807,9 @@ pub async fn upload_chunk(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_upload_status(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -793,6 +836,9 @@ pub async fn get_upload_status(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn complete_upload(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -809,7 +855,8 @@ pub async fn complete_upload(
.await
.map_err(|e| ApiError::internal(format!("Failed to get chunks: {e}")))?;
if chunks.len() != session.chunk_count as usize {
if chunks.len() != usize::try_from(session.chunk_count).unwrap_or(usize::MAX)
{
return Err(ApiError::bad_request(format!(
"Missing chunks: expected {}, got {}",
session.chunk_count,
@ -961,6 +1008,9 @@ pub async fn complete_upload(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn cancel_upload(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -1004,6 +1054,9 @@ pub async fn cancel_upload(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn download_file(
State(state): State<AppState>,
Path(path): Path<String>,

View file

@ -25,6 +25,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_tag(
State(state): State<AppState>,
Json(req): Json<CreateTagRequest>,
@ -53,6 +56,9 @@ pub async fn create_tag(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_tags(
State(state): State<AppState>,
) -> Result<Json<Vec<TagResponse>>, ApiError> {
@ -73,6 +79,9 @@ pub async fn list_tags(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_tag(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -95,6 +104,9 @@ pub async fn get_tag(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_tag(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -118,6 +130,9 @@ pub async fn delete_tag(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn tag_media(
State(state): State<AppState>,
Path(media_id): Path<Uuid>,
@ -154,6 +169,9 @@ pub async fn tag_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn untag_media(
State(state): State<AppState>,
Path((media_id, tag_id)): Path<(Uuid, Uuid)>,
@ -185,6 +203,9 @@ pub async fn untag_media(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_media_tags(
State(state): State<AppState>,
Path(media_id): Path<Uuid>,

View file

@ -25,6 +25,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn start_transcode(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -55,6 +58,9 @@ pub async fn start_transcode(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_session(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -73,6 +79,9 @@ pub async fn get_session(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_sessions(
State(state): State<AppState>,
Query(params): Query<PaginationParams>,
@ -99,6 +108,9 @@ pub async fn list_sessions(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn cancel_session(
State(state): State<AppState>,
Path(id): Path<Uuid>,

View file

@ -44,6 +44,9 @@ fn sanitize_content_disposition(filename: &str) -> String {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn upload_file(
State(state): State<AppState>,
mut multipart: Multipart,
@ -110,6 +113,9 @@ pub async fn upload_file(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn download_file(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -192,6 +198,9 @@ pub async fn download_file(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn move_to_managed(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -226,6 +235,9 @@ pub async fn move_to_managed(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn managed_stats(
State(state): State<AppState>,
) -> ApiResult<Json<ManagedStorageStatsResponse>> {

View file

@ -27,6 +27,9 @@ use crate::{
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_users(
State(state): State<AppState>,
) -> Result<Json<Vec<UserResponse>>, ApiError> {
@ -53,6 +56,9 @@ pub async fn list_users(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn create_user(
State(state): State<AppState>,
Json(req): Json<CreateUserRequest>,
@ -116,6 +122,9 @@ pub async fn create_user(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_user(
State(state): State<AppState>,
Path(id): Path<String>,
@ -151,6 +160,9 @@ pub async fn get_user(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn update_user(
State(state): State<AppState>,
Path(id): Path<String>,
@ -199,6 +211,9 @@ pub async fn update_user(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn delete_user(
State(state): State<AppState>,
Path(id): Path<String>,
@ -227,6 +242,9 @@ pub async fn delete_user(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn get_user_libraries(
State(state): State<AppState>,
Path(id): Path<String>,
@ -277,6 +295,9 @@ fn validate_root_path(path: &str) -> Result<(), ApiError> {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn grant_library_access(
State(state): State<AppState>,
Path(id): Path<String>,
@ -316,6 +337,9 @@ pub async fn grant_library_access(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn revoke_library_access(
State(state): State<AppState>,
Path(id): Path<String>,

View file

@ -20,6 +20,9 @@ pub struct WebhookInfo {
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn list_webhooks(
State(state): State<AppState>,
) -> Result<Json<Vec<WebhookInfo>>, ApiError> {
@ -48,6 +51,9 @@ pub async fn list_webhooks(
),
security(("bearer_auth" = []))
)]
/// # Errors
///
/// Returns an error if the database operation fails or the request is invalid.
pub async fn test_webhook(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
@ -55,17 +61,20 @@ pub async fn test_webhook(
let count = config.webhooks.len();
drop(config);
if let Some(ref dispatcher) = state.webhook_dispatcher {
dispatcher.dispatch(pinakes_core::webhooks::WebhookEvent::Test);
Ok(Json(serde_json::json!({
"webhooks_configured": count,
"test_sent": true
})))
} else {
Ok(Json(serde_json::json!({
"webhooks_configured": 0,
"test_sent": false,
"message": "no webhooks configured"
})))
}
state.webhook_dispatcher.as_ref().map_or_else(
|| {
Ok(Json(serde_json::json!({
"webhooks_configured": 0,
"test_sent": false,
"message": "no webhooks configured"
})))
},
|dispatcher| {
dispatcher.dispatch(pinakes_core::webhooks::WebhookEvent::Test);
Ok(Json(serde_json::json!({
"webhooks_configured": count,
"test_sent": true
})))
},
)
}