chore: bump deps; fix clippy lints & cleanup

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I4c4815ad145650a07f108614034d2e996a6a6964
This commit is contained in:
raf 2026-03-02 17:05:28 +03:00
commit cd1161ee5d
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
41 changed files with 1528 additions and 953 deletions

View file

@ -136,7 +136,7 @@ pub fn create_router_with_tls(
)
// Webhooks (read)
.route("/webhooks", get(routes::webhooks::list_webhooks))
// Auth endpoints (self-service) — login handled separately with stricter rate limit
// Auth endpoints (self-service); login is handled separately with a stricter rate limit
.route("/auth/logout", post(routes::auth::logout))
.route("/auth/me", get(routes::auth::me))
.route("/auth/revoke-all", post(routes::auth::revoke_all_sessions))

View file

@ -721,8 +721,6 @@ impl From<pinakes_core::users::UserLibraryAccess> for UserLibraryResponse {
}
}
// ===== Social (Ratings, Comments, Favorites, Shares) =====
#[derive(Debug, Serialize)]
pub struct RatingResponse {
pub id: String,
@ -816,8 +814,6 @@ impl From<pinakes_core::social::ShareLink> for ShareLinkResponse {
}
}
// ===== Playlists =====
#[derive(Debug, Serialize)]
pub struct PlaylistResponse {
pub id: String,
@ -875,8 +871,6 @@ pub struct ReorderPlaylistRequest {
pub new_position: i32,
}
// ===== Analytics =====
#[derive(Debug, Serialize)]
pub struct UsageEventResponse {
pub id: String,
@ -924,8 +918,6 @@ pub struct WatchProgressResponse {
pub progress_secs: f64,
}
// ===== Subtitles =====
#[derive(Debug, Serialize)]
pub struct SubtitleResponse {
pub id: String,
@ -968,8 +960,6 @@ pub struct UpdateSubtitleOffsetRequest {
pub offset_ms: i64,
}
// ===== Enrichment =====
#[derive(Debug, Serialize)]
pub struct ExternalMetadataResponse {
pub id: String,
@ -1005,8 +995,6 @@ impl From<pinakes_core::enrichment::ExternalMetadata>
}
}
// ===== Transcode =====
#[derive(Debug, Serialize)]
pub struct TranscodeSessionResponse {
pub id: String,
@ -1039,8 +1027,6 @@ pub struct CreateTranscodeRequest {
pub profile: String,
}
// ===== Managed Storage / Upload =====
#[derive(Debug, Serialize)]
pub struct UploadResponse {
pub media_id: String,
@ -1081,8 +1067,6 @@ impl From<pinakes_core::model::ManagedStorageStats>
}
}
// ===== Sync =====
#[derive(Debug, Deserialize)]
pub struct RegisterDeviceRequest {
pub name: String,
@ -1269,8 +1253,6 @@ pub struct AcknowledgeChangesRequest {
pub cursor: i64,
}
// ===== Enhanced Sharing =====
#[derive(Debug, Deserialize)]
pub struct CreateShareRequest {
pub target_type: String,

View file

@ -438,13 +438,123 @@ async fn main() -> Result<()> {
}
},
JobKind::Enrich { media_ids } => {
// Enrichment job placeholder
use pinakes_core::{
enrichment::{
MetadataEnricher,
books::BookEnricher,
lastfm::LastFmEnricher,
musicbrainz::MusicBrainzEnricher,
tmdb::TmdbEnricher,
},
media_type::MediaCategory,
};
let enrich_cfg = &config.enrichment;
let mut enrichers: Vec<Box<dyn MetadataEnricher>> = Vec::new();
if enrich_cfg.enabled {
if enrich_cfg.sources.musicbrainz.enabled {
enrichers.push(Box::new(MusicBrainzEnricher::new()));
}
if let (true, Some(key)) = (
enrich_cfg.sources.tmdb.enabled,
enrich_cfg.sources.tmdb.api_key.clone(),
) {
enrichers.push(Box::new(TmdbEnricher::new(key)));
}
if let (true, Some(key)) = (
enrich_cfg.sources.lastfm.enabled,
enrich_cfg.sources.lastfm.api_key.clone(),
) {
enrichers.push(Box::new(LastFmEnricher::new(key)));
}
// BookEnricher handles documents/epub. No dedicated config
// key is required; the Google Books key is optional.
enrichers.push(Box::new(BookEnricher::new(None)));
}
let total = media_ids.len();
let mut enriched: usize = 0;
let mut errors: usize = 0;
'items: for media_id in media_ids {
if cancel.is_cancelled() {
break 'items;
}
let item = match storage.get_media(media_id).await {
Ok(i) => i,
Err(e) => {
tracing::warn!(
%media_id,
error = %e,
"enrich: failed to fetch media item"
);
errors += 1;
continue;
},
};
// Select enrichers appropriate for this media category.
let category = item.media_type.category();
for enricher in &enrichers {
let source = enricher.source();
use pinakes_core::enrichment::EnrichmentSourceType;
let applicable = match source {
EnrichmentSourceType::MusicBrainz
| EnrichmentSourceType::LastFm => {
category == MediaCategory::Audio
},
EnrichmentSourceType::Tmdb => {
category == MediaCategory::Video
},
EnrichmentSourceType::OpenLibrary
| EnrichmentSourceType::GoogleBooks => {
category == MediaCategory::Document
},
};
if !applicable {
continue;
}
match enricher.enrich(&item).await {
Ok(Some(meta)) => {
if let Err(e) = storage.store_external_metadata(&meta).await
{
tracing::warn!(
%media_id,
%source,
error = %e,
"enrich: failed to store external metadata"
);
errors += 1;
} else {
enriched += 1;
}
},
Ok(None) => {},
Err(e) => {
tracing::warn!(
%media_id,
%source,
error = %e,
"enrich: enricher returned error"
);
errors += 1;
},
}
}
}
JobQueue::complete(
&jobs,
job_id,
serde_json::json!({"media_ids": media_ids.len(), "status": "not_implemented"}),
)
.await;
&jobs,
job_id,
serde_json::json!({
"total": total,
"enriched": enriched,
"errors": errors,
}),
)
.await;
},
JobKind::CleanupAnalytics => {
let before = chrono::Utc::now() - chrono::Duration::days(90);
@ -460,6 +570,27 @@ async fn main() -> Result<()> {
Err(e) => JobQueue::fail(&jobs, job_id, e.to_string()).await,
}
},
JobKind::TrashPurge => {
let retention_days = config.trash.retention_days;
let before = chrono::Utc::now()
- chrono::Duration::days(retention_days as i64);
match storage.purge_old_trash(before).await {
Ok(count) => {
tracing::info!(count, "purged {} items from trash", count);
JobQueue::complete(
&jobs,
job_id,
serde_json::json!({"purged": count, "retention_days": retention_days}),
)
.await;
},
Err(e) => {
tracing::error!(error = %e, "failed to purge trash");
JobQueue::fail(&jobs, job_id, e.to_string()).await;
},
}
},
};
drop(cancel);
})

View file

@ -836,8 +836,6 @@ pub async fn get_media_count(
Ok(Json(MediaCountResponse { count }))
}
// ===== File Management Endpoints =====
pub async fn rename_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,
@ -978,8 +976,6 @@ pub async fn batch_move_media(
}
}
// ===== Trash Endpoints =====
pub async fn soft_delete_media(
State(state): State<AppState>,
Path(id): Path<Uuid>,

View file

@ -25,8 +25,6 @@ use uuid::Uuid;
use crate::{error::ApiError, state::AppState};
// ===== Response DTOs =====
/// Response for backlinks query
#[derive(Debug, Serialize)]
pub struct BacklinksResponse {
@ -200,8 +198,6 @@ pub struct UnresolvedLinksResponse {
pub count: u64,
}
// ===== Handlers =====
/// Get backlinks (incoming links) to a media item.
///
/// GET /api/v1/media/{id}/backlinks

View file

@ -93,7 +93,12 @@ pub async fn create_share(
let recipient = match req.recipient_type.as_str() {
"public_link" => {
let token = generate_share_token();
let password_hash = req.password.as_ref().map(|p| hash_share_password(p));
let password_hash = req
.password
.as_ref()
.map(|p| hash_share_password(p))
.transpose()
.map_err(ApiError)?;
ShareRecipient::PublicLink {
token,
password_hash,
@ -409,35 +414,37 @@ pub async fn access_shared(
.map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?;
// Check expiration
if let Some(expires_at) = share.expires_at {
if Utc::now() > expires_at {
return Err(ApiError::not_found("Share has expired"));
}
if let Some(expires_at) = share.expires_at
&& Utc::now() > expires_at
{
return Err(ApiError::not_found("Share has expired"));
}
// Check password if required
if let ShareRecipient::PublicLink { password_hash, .. } = &share.recipient {
if let Some(hash) = password_hash {
let provided_password = params
.password
.as_ref()
.ok_or_else(|| ApiError::unauthorized("Password required"))?;
if let ShareRecipient::PublicLink {
password_hash: Some(hash),
..
} = &share.recipient
{
let provided_password = params
.password
.as_ref()
.ok_or_else(|| ApiError::unauthorized("Password required"))?;
if !verify_share_password(provided_password, hash) {
// Log failed attempt
let activity = ShareActivity {
id: Uuid::now_v7(),
share_id: share.id,
actor_id: None,
actor_ip: Some(addr.ip().to_string()),
action: ShareActivityAction::PasswordFailed,
details: None,
timestamp: Utc::now(),
};
let _ = state.storage.record_share_activity(&activity).await;
if !verify_share_password(provided_password, hash) {
// Log failed attempt
let activity = ShareActivity {
id: Uuid::now_v7(),
share_id: share.id,
actor_id: None,
actor_ip: Some(addr.ip().to_string()),
action: ShareActivityAction::PasswordFailed,
details: None,
timestamp: Utc::now(),
};
let _ = state.storage.record_share_activity(&activity).await;
return Err(ApiError::unauthorized("Invalid password"));
}
return Err(ApiError::unauthorized("Invalid password"));
}
}
@ -473,8 +480,6 @@ pub async fn access_shared(
Ok(Json(item.into()))
},
_ => {
// For collections/tags, return a placeholder
// Full implementation would return the collection contents
Err(ApiError::bad_request(
"Collection/tag sharing not yet fully implemented",
))

View file

@ -13,8 +13,6 @@ pub struct ShareLinkQuery {
pub password: Option<String>,
}
// ===== Ratings =====
pub async fn rate_media(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -46,8 +44,6 @@ pub async fn get_media_ratings(
))
}
// ===== Comments =====
pub async fn add_comment(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -80,8 +76,6 @@ pub async fn get_media_comments(
))
}
// ===== Favorites =====
pub async fn add_favorite(
State(state): State<AppState>,
Extension(username): Extension<String>,
@ -120,8 +114,6 @@ pub async fn list_favorites(
Ok(Json(items.into_iter().map(MediaResponse::from).collect()))
}
// ===== Share Links =====
pub async fn create_share_link(
State(state): State<AppState>,
Extension(username): Extension<String>,

View file

@ -301,7 +301,7 @@ pub async fn report_changes(
if !config.sync.enabled {
return Err(ApiError::bad_request("Sync is not enabled"));
}
let conflict_resolution = config.sync.default_conflict_resolution.clone();
let conflict_resolution = config.sync.default_conflict_resolution;
drop(config);
let mut accepted = Vec::new();
@ -514,7 +514,7 @@ pub async fn create_upload(
.ok_or_else(|| ApiError::unauthorized("Invalid device token"))?;
let chunk_size = req.chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE);
let chunk_count = (req.expected_size + chunk_size - 1) / chunk_size;
let chunk_count = req.expected_size.div_ceil(chunk_size);
let now = Utc::now();
let session = UploadSession {
@ -784,10 +784,10 @@ pub async fn cancel_upload(
})?;
// Clean up temp file if manager is available
if let Some(ref manager) = state.chunked_upload_manager {
if let Err(e) = manager.cancel(id).await {
tracing::warn!(session_id = %id, error = %e, "failed to clean up temp file");
}
if let Some(ref manager) = state.chunked_upload_manager
&& let Err(e) = manager.cancel(id).await
{
tracing::warn!(session_id = %id, error = %e, "failed to clean up temp file");
}
session.status = UploadStatus::Cancelled;
@ -827,38 +827,37 @@ pub async fn download_file(
let file_size = metadata.len();
// Check for Range header
if let Some(range_header) = headers.get(header::RANGE) {
if let Ok(range_str) = range_header.to_str() {
if let Some(range) = parse_range_header(range_str, file_size) {
// Partial content response
let (start, end) = range;
let length = end - start + 1;
if let Some(range_header) = headers.get(header::RANGE)
&& let Ok(range_str) = range_header.to_str()
&& let Some(range) = parse_range_header(range_str, file_size)
{
// Partial content response
let (start, end) = range;
let length = end - start + 1;
let file = tokio::fs::File::open(&item.path).await.map_err(|e| {
ApiError::internal(format!("Failed to reopen file: {}", e))
})?;
let file = tokio::fs::File::open(&item.path).await.map_err(|e| {
ApiError::internal(format!("Failed to reopen file: {}", e))
})?;
let stream = ReaderStream::new(file);
let body = Body::from_stream(stream);
let stream = ReaderStream::new(file);
let body = Body::from_stream(stream);
return Ok(
return Ok(
(
StatusCode::PARTIAL_CONTENT,
[
(header::CONTENT_TYPE, item.media_type.mime_type()),
(header::CONTENT_LENGTH, length.to_string()),
(
StatusCode::PARTIAL_CONTENT,
[
(header::CONTENT_TYPE, item.media_type.mime_type()),
(header::CONTENT_LENGTH, length.to_string()),
(
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", start, end, file_size),
),
(header::ACCEPT_RANGES, "bytes".to_string()),
],
body,
)
.into_response(),
);
}
}
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", start, end, file_size),
),
(header::ACCEPT_RANGES, "bytes".to_string()),
],
body,
)
.into_response(),
);
}
// Full content response