various: bump dependencies; wire up dead code

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I12432bc956453cc4b0a2db82dce1b4976a6a6964
This commit is contained in:
raf 2026-02-05 14:36:01 +03:00
commit 875bdf5ebc
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
6 changed files with 764 additions and 427 deletions

828
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,11 @@ license = "MIT"
readme = true
[workspace.dependencies]
# Internal Dependencies
pinakes-core = {path = "./crates/pinakes-core"}
pinakes-server = {path = "./crates/pinakes-server"}
pinakes-plugin-api = {path = "./crates/plugin/api"}
# Async runtime
tokio = { version = "1.49.0", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["rt"] }
@ -25,7 +30,7 @@ serde_json = "1.0.149"
toml = "0.9.11"
# CLI argument parsing
clap = { version = "4.5.56", features = ["derive", "env"] }
clap = { version = "4.5.57", features = ["derive", "env"] }
# Date/time
chrono = { version = "0.4.43", features = ["serde"] }
@ -80,8 +85,8 @@ winnow = "0.7.14"
axum = { version = "0.8.8", features = ["macros", "multipart"] }
tower = "0.5.3"
tower-http = { version = "0.6.8", features = ["cors", "trace", "set-header"] }
governor = "0.8.1"
tower_governor = "0.6.0"
governor = "0.10.4"
tower_governor = "0.8.0"
# HTTP client
reqwest = { version = "0.13.1", features = ["json", "query", "blocking"] }
@ -94,10 +99,10 @@ crossterm = "0.29.0"
dioxus = { version = "0.7.3", features = ["desktop", "router"] }
# Async trait (dyn-compatible async methods)
async-trait = "0.1"
async-trait = "0.1.89"
# Async utilities
futures = "0.3"
futures = "0.3.31"
# Image processing (thumbnails)
image = { version = "0.25.9", default-features = false, features = [
@ -110,15 +115,15 @@ image = { version = "0.25.9", default-features = false, features = [
] }
# Markdown rendering
pulldown-cmark = "0.12.2"
pulldown-cmark = "0.13.0"
# Password hashing
argon2 = { version = "0.5.3", features = ["std"] }
# Misc
mime_guess = "2.0.5"
regex = "1.11"
regex = "1.12.3"
# WASM runtime for plugins
wasmtime = { version = "30.0.2", features = ["component-model"] }
wit-bindgen = "0.39.0"
wasmtime = { version = "41.0.3", features = ["component-model"] }
wit-bindgen = "0.52.0"

View file

@ -65,9 +65,7 @@ pub fn create_router_with_tls(
// Login route with strict rate limiting
let login_route = Router::new()
.route("/auth/login", post(routes::auth::login))
.layer(GovernorLayer {
config: login_governor,
});
.layer(GovernorLayer::new(login_governor));
// Public routes (no auth required)
let public_routes = Router::new()
@ -82,16 +80,12 @@ pub fn create_router_with_tls(
let search_routes = Router::new()
.route("/search", get(routes::search::search))
.route("/search", post(routes::search::search_post))
.layer(GovernorLayer {
config: search_governor,
});
.layer(GovernorLayer::new(search_governor));
// Streaming routes with enhanced rate limiting (5 concurrent)
let streaming_routes = Router::new()
.route("/media/{id}/stream", get(routes::media::stream_media))
.layer(GovernorLayer {
config: stream_governor,
});
.layer(GovernorLayer::new(stream_governor));
// Read-only routes: any authenticated user (Viewer+)
let viewer_routes = Router::new()
@ -561,9 +555,7 @@ pub fn create_router_with_tls(
let router = Router::new()
.nest("/api/v1", full_api)
.layer(DefaultBodyLimit::max(10 * 1024 * 1024))
.layer(GovernorLayer {
config: global_governor,
})
.layer(GovernorLayer::new(global_governor))
.layer(TraceLayer::new_for_http())
.layer(cors)
.layer(security_headers);

View file

@ -6,7 +6,9 @@ use futures::future::join_all;
use crate::client::*;
use crate::components::{
audit, collections, database, detail, duplicates, import, library, search, settings, tags,
audit, collections, database, detail, duplicates, import, library,
media_player::PlayQueue,
search, settings, statistics, tags, tasks,
};
// Login component available via crate::components::login when auth gating is needed
use crate::styles;
@ -23,6 +25,8 @@ enum View {
Audit,
Import,
Duplicates,
Statistics,
Tasks,
Settings,
Database,
}
@ -38,6 +42,8 @@ impl View {
Self::Audit => "Audit Log",
Self::Import => "Import",
Self::Duplicates => "Duplicates",
Self::Statistics => "Statistics",
Self::Tasks => "Tasks",
Self::Settings => "Settings",
Self::Database => "Database",
}
@ -68,6 +74,8 @@ pub fn App() -> Element {
let mut audit_list = use_signal(Vec::<AuditEntryResponse>::new);
let mut config_data = use_signal(|| Option::<ConfigResponse>::None);
let mut db_stats = use_signal(|| Option::<crate::client::DatabaseStatsResponse>::None);
let mut library_stats = use_signal(|| Option::<crate::client::LibraryStatisticsResponse>::None);
let mut scheduled_tasks = use_signal(Vec::<crate::client::ScheduledTaskResponse>::new);
let mut duplicate_groups = use_signal(Vec::<crate::client::DuplicateGroupResponse>::new);
let mut preview_files = use_signal(Vec::<DirectoryPreviewFile>::new);
let mut preview_total_size = use_signal(|| 0u64);
@ -111,6 +119,7 @@ pub fn App() -> Element {
let mut login_error = use_signal(|| Option::<String>::None);
let mut login_loading = use_signal(|| false);
let mut auto_play_media = use_signal(|| false);
let mut play_queue = use_signal(PlayQueue::default);
// Theme state (Phase 3.3)
let mut current_theme = use_signal(|| "dark".to_string());
@ -538,6 +547,40 @@ pub fn App() -> Element {
span { class: "nav-icon", "\u{2261}" }
span { class: "nav-item-text", "Duplicates" }
}
button {
class: if *current_view.read() == View::Statistics { "nav-item active" } else { "nav-item" },
onclick: {
let client = client.read().clone();
move |_| {
current_view.set(View::Statistics);
let client = client.clone();
spawn(async move {
if let Ok(stats) = client.library_statistics().await {
library_stats.set(Some(stats));
}
});
}
},
span { class: "nav-icon", "\u{1f4ca}" }
span { class: "nav-item-text", "Statistics" }
}
button {
class: if *current_view.read() == View::Tasks { "nav-item active" } else { "nav-item" },
onclick: {
let client = client.read().clone();
move |_| {
current_view.set(View::Tasks);
let client = client.clone();
spawn(async move {
if let Ok(tasks_data) = client.list_scheduled_tasks().await {
scheduled_tasks.set(tasks_data);
}
});
}
},
span { class: "nav-icon", "\u{23f0}" }
span { class: "nav-item-text", "Tasks" }
}
button {
class: if *current_view.read() == View::Settings { "nav-item active" } else { "nav-item" },
onclick: {
@ -1175,6 +1218,98 @@ pub fn App() -> Element {
});
}
},
play_queue: if play_queue.read().is_empty() { None } else { Some(play_queue.read().clone()) },
on_queue_select: {
move |idx: usize| {
let mut q = play_queue.write();
q.current_index = idx;
// Update selected_media to the item at this index
if let Some(item) = q.items.get(idx) {
let media_id = item.media_id.clone();
let client = client.read().clone();
spawn(async move {
if let Ok(media) = client.get_media(&media_id).await {
selected_media.set(Some(media));
auto_play_media.set(true);
}
});
}
}
},
on_queue_remove: {
move |idx: usize| {
play_queue.write().remove(idx);
}
},
on_queue_clear: {
move |_| {
play_queue.write().clear();
}
},
on_queue_toggle_repeat: {
move |_| {
play_queue.write().toggle_repeat();
}
},
on_queue_toggle_shuffle: {
move |_| {
play_queue.write().toggle_shuffle();
}
},
on_queue_next: {
move |_| {
let mut q = play_queue.write();
if let Some(item) = q.next() {
let media_id = item.media_id.clone();
drop(q);
let client = client.read().clone();
spawn(async move {
if let Ok(media) = client.get_media(&media_id).await {
selected_media.set(Some(media));
auto_play_media.set(true);
}
});
}
}
},
on_queue_previous: {
move |_| {
let mut q = play_queue.write();
if let Some(item) = q.previous() {
let media_id = item.media_id.clone();
drop(q);
let client = client.read().clone();
spawn(async move {
if let Ok(media) = client.get_media(&media_id).await {
selected_media.set(Some(media));
auto_play_media.set(true);
}
});
}
}
},
on_track_ended: {
move |_| {
let mut q = play_queue.write();
if let Some(item) = q.next() {
let media_id = item.media_id.clone();
drop(q);
let client = client.read().clone();
spawn(async move {
if let Ok(media) = client.get_media(&media_id).await {
selected_media.set(Some(media));
auto_play_media.set(true);
}
});
}
}
},
on_add_to_queue: {
move |item: crate::components::media_player::QueueItem| {
play_queue.write().add(item);
show_toast("Added to queue".into(), false);
}
},
}
},
None => rsx! {
@ -1979,6 +2114,86 @@ pub fn App() -> Element {
}
}
}
View::Statistics => {
let refresh_stats = {
let client = client.read().clone();
move || {
let client = client.clone();
spawn(async move {
match client.library_statistics().await {
Ok(stats) => library_stats.set(Some(stats)),
Err(e) => show_toast(format!("Failed to load statistics: {e}"), true),
}
});
}
};
rsx! {
statistics::Statistics {
stats: library_stats.read().clone(),
on_refresh: {
let refresh_stats = refresh_stats.clone();
move |_| refresh_stats()
},
}
}
}
View::Tasks => {
let refresh_tasks = {
let client = client.read().clone();
move || {
let client = client.clone();
spawn(async move {
match client.list_scheduled_tasks().await {
Ok(tasks_data) => scheduled_tasks.set(tasks_data),
Err(e) => show_toast(format!("Failed to load tasks: {e}"), true),
}
});
}
};
rsx! {
tasks::Tasks {
tasks: scheduled_tasks.read().clone(),
on_refresh: {
let refresh_tasks = refresh_tasks.clone();
move |_| refresh_tasks()
},
on_toggle: {
let client = client.read().clone();
let refresh_tasks = refresh_tasks.clone();
move |task_id: String| {
let client = client.clone();
let refresh_tasks = refresh_tasks.clone();
spawn(async move {
match client.toggle_scheduled_task(&task_id).await {
Ok(_) => {
show_toast("Task toggled".into(), false);
refresh_tasks();
}
Err(e) => show_toast(format!("Toggle failed: {e}"), true),
}
});
}
},
on_run_now: {
let client = client.read().clone();
let refresh_tasks = refresh_tasks.clone();
move |task_id: String| {
let client = client.clone();
let refresh_tasks = refresh_tasks.clone();
spawn(async move {
match client.run_scheduled_task_now(&task_id).await {
Ok(_) => {
show_toast("Task started".into(), false);
refresh_tasks();
}
Err(e) => show_toast(format!("Run failed: {e}"), true),
}
});
}
},
}
}
}
View::Settings => {
let cfg_ref = config_data.read();
match cfg_ref.as_ref() {

View file

@ -482,34 +482,6 @@ impl ApiClient {
.await?)
}
pub async fn batch_import(
&self,
paths: &[String],
tag_ids: &[String],
new_tags: &[String],
collection_id: Option<&str>,
) -> Result<BatchImportResponse> {
let mut body = serde_json::json!({"paths": paths});
if !tag_ids.is_empty() {
body["tag_ids"] = serde_json::json!(tag_ids);
}
if !new_tags.is_empty() {
body["new_tags"] = serde_json::json!(new_tags);
}
if let Some(cid) = collection_id {
body["collection_id"] = serde_json::json!(cid);
}
Ok(self
.client
.post(self.url("/media/import/batch"))
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn import_directory(
&self,
path: &str,
@ -967,17 +939,6 @@ impl ApiClient {
// ── UI Config ──
pub async fn get_ui_config(&self) -> Result<UiConfigResponse> {
Ok(self
.client
.get(self.url("/config/ui"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn update_ui_config(&self, updates: serde_json::Value) -> Result<UiConfigResponse> {
Ok(self
.client

View file

@ -2,7 +2,7 @@ use dioxus::prelude::*;
use super::image_viewer::ImageViewer;
use super::markdown_viewer::MarkdownViewer;
use super::media_player::MediaPlayer;
use super::media_player::{MediaPlayer, PlayQueue, QueueItem, QueuePanel};
use super::pdf_viewer::PdfViewer;
use super::utils::{format_duration, format_size, media_category, type_badge_class};
use crate::client::{ApiClient, MediaResponse, MediaUpdateEvent, TagResponse};
@ -14,6 +14,7 @@ pub fn Detail(
all_tags: Vec<TagResponse>,
server_url: String,
#[props(default = false)] autoplay: bool,
#[props(default)] play_queue: Option<PlayQueue>,
on_back: EventHandler<()>,
on_open: EventHandler<String>,
on_update: EventHandler<MediaUpdateEvent>,
@ -22,6 +23,15 @@ pub fn Detail(
on_set_custom_field: EventHandler<(String, String, String, String)>,
on_delete_custom_field: EventHandler<(String, String)>,
on_delete: EventHandler<String>,
#[props(default)] on_queue_select: Option<EventHandler<usize>>,
#[props(default)] on_queue_remove: Option<EventHandler<usize>>,
#[props(default)] on_queue_clear: Option<EventHandler<()>>,
#[props(default)] on_queue_toggle_repeat: Option<EventHandler<()>>,
#[props(default)] on_queue_toggle_shuffle: Option<EventHandler<()>>,
#[props(default)] on_queue_next: Option<EventHandler<()>>,
#[props(default)] on_queue_previous: Option<EventHandler<()>>,
#[props(default)] on_track_ended: Option<EventHandler<()>>,
#[props(default)] on_add_to_queue: Option<EventHandler<QueueItem>>,
) -> Element {
let mut editing = use_signal(|| false);
let mut show_image_viewer = use_signal(|| false);
@ -225,6 +235,9 @@ pub fn Detail(
let thumb_for_player = thumbnail_url.clone();
let file_name_for_viewer = media.file_name.clone();
// Clone queue handlers for use in the component
let has_queue = play_queue.is_some();
rsx! {
// Media preview
div { class: "detail-preview",
@ -235,6 +248,7 @@ pub fn Detail(
title: media.title.clone(),
thumbnail_url: if has_thumbnail { Some(thumb_for_player.clone()) } else { None },
autoplay,
on_track_ended: on_track_ended,
}
} else if category == "video" {
MediaPlayer {
@ -242,6 +256,7 @@ pub fn Detail(
media_type: "video".to_string(),
title: media.title.clone(),
autoplay,
on_track_ended: on_track_ended,
}
} else if category == "image" {
if has_thumbnail {
@ -290,6 +305,71 @@ pub fn Detail(
}
}
// Play queue panel (only for audio/video with a queue)
if has_queue && (category == "audio" || category == "video") {
if let Some(ref queue) = play_queue {
QueuePanel {
queue: queue.clone(),
on_select: {
let handler = on_queue_select;
move |idx| {
if let Some(ref h) = handler {
h.call(idx);
}
}
},
on_remove: {
let handler = on_queue_remove;
move |idx| {
if let Some(ref h) = handler {
h.call(idx);
}
}
},
on_clear: {
let handler = on_queue_clear;
move |_| {
if let Some(ref h) = handler {
h.call(());
}
}
},
on_toggle_repeat: {
let handler = on_queue_toggle_repeat;
move |_| {
if let Some(ref h) = handler {
h.call(());
}
}
},
on_toggle_shuffle: {
let handler = on_queue_toggle_shuffle;
move |_| {
if let Some(ref h) = handler {
h.call(());
}
}
},
on_next: {
let handler = on_queue_next;
move |_| {
if let Some(ref h) = handler {
h.call(());
}
}
},
on_previous: {
let handler = on_queue_previous;
move |_| {
if let Some(ref h) = handler {
h.call(());
}
}
},
}
}
}
// Action bar
div { class: "detail-actions",
button {
@ -305,6 +385,46 @@ pub fn Detail(
},
"Open"
}
// Add to Queue button for audio/video content
if (category == "audio" || category == "video") && on_add_to_queue.is_some() {
{
// Check if this item is currently playing
let is_current = play_queue.as_ref()
.and_then(|q| q.current())
.map(|item| item.media_id == id)
.unwrap_or(false);
let media_id_for_queue = id.clone();
let title_for_queue = media.title.clone().unwrap_or_else(|| media.file_name.clone());
let artist_for_queue = media.artist.clone();
let duration_for_queue = media.duration_secs;
let media_type_for_queue = category.to_string();
let stream_url_for_queue = stream_url.clone();
let thumbnail_for_queue = if has_thumbnail { Some(thumbnail_url.clone()) } else { None };
let on_add = on_add_to_queue;
rsx! {
button {
class: if is_current { "btn btn-secondary disabled" } else { "btn btn-secondary" },
disabled: is_current,
title: if is_current { "Currently playing" } else { "Add to play queue" },
onclick: move |_| {
if let Some(ref handler) = on_add {
let item = QueueItem {
media_id: media_id_for_queue.clone(),
title: title_for_queue.clone(),
artist: artist_for_queue.clone(),
duration_secs: duration_for_queue,
media_type: media_type_for_queue.clone(),
stream_url: stream_url_for_queue.clone(),
thumbnail_url: thumbnail_for_queue.clone(),
};
handler.call(item);
}
},
if is_current { "\u{266b} Playing" } else { "\u{2795} Queue" }
}
}
}
}
if is_editing {
button { class: "btn btn-primary", onclick: on_save_click, "Save" }
button { class: "btn btn-ghost", onclick: on_cancel_click, "Cancel" }