Updated the dashboard page
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use gloo_timers::callback::Interval;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
@@ -8,20 +9,139 @@ use crate::types::Status;
|
|||||||
pub fn dashboard() -> Html {
|
pub fn dashboard() -> Html {
|
||||||
let status = use_state(|| None::<Status>);
|
let status = use_state(|| None::<Status>);
|
||||||
let error = use_state(|| None::<String>);
|
let error = use_state(|| None::<String>);
|
||||||
|
let message = use_state(|| None::<String>);
|
||||||
|
|
||||||
{
|
// Fetch status function
|
||||||
|
let fetch_status = {
|
||||||
let status = status.clone();
|
let status = status.clone();
|
||||||
let error = error.clone();
|
let error = error.clone();
|
||||||
use_effect_with((), move |_| {
|
Callback::from(move |_: ()| {
|
||||||
|
let status = status.clone();
|
||||||
|
let error = error.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
match api::get_status().await {
|
match api::get_status().await {
|
||||||
Ok(s) => status.set(Some(s)),
|
Ok(s) => {
|
||||||
|
error.set(None);
|
||||||
|
status.set(Some(s));
|
||||||
|
}
|
||||||
Err(e) => error.set(Some(e.0)),
|
Err(e) => error.set(Some(e.0)),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
{
|
||||||
|
let fetch = fetch_status.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
fetch.emit(());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-refresh every 5 seconds
|
||||||
|
{
|
||||||
|
let fetch = fetch_status.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
let interval = Interval::new(1_000, move || {
|
||||||
|
fetch.emit(());
|
||||||
|
});
|
||||||
|
// Keep the interval alive for the component's lifetime
|
||||||
|
move || drop(interval)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action handlers
|
||||||
|
let on_sync = {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch_status.clone();
|
||||||
|
Callback::from(move |_: MouseEvent| {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match api::sync_downloads().await {
|
||||||
|
Ok(s) => {
|
||||||
|
message.set(Some(format!("Synced: {} enqueued, {} skipped", s.enqueued, s.skipped)));
|
||||||
|
fetch.emit(());
|
||||||
|
}
|
||||||
|
Err(e) => error.set(Some(e.0)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_process = {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch_status.clone();
|
||||||
|
Callback::from(move |_: MouseEvent| {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match api::process_downloads().await {
|
||||||
|
Ok(t) => {
|
||||||
|
message.set(Some(format!("Downloads started (task: {})", &t.task_id[..8])));
|
||||||
|
fetch.emit(());
|
||||||
|
}
|
||||||
|
Err(e) => error.set(Some(e.0)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_index = {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch_status.clone();
|
||||||
|
Callback::from(move |_: MouseEvent| {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match api::trigger_index().await {
|
||||||
|
Ok(t) => { message.set(Some(format!("Indexing started ({})", &t.task_id[..8]))); fetch.emit(()); }
|
||||||
|
Err(e) => error.set(Some(e.0)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_tag = {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch_status.clone();
|
||||||
|
Callback::from(move |_: MouseEvent| {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match api::trigger_tag().await {
|
||||||
|
Ok(t) => { message.set(Some(format!("Tagging started ({})", &t.task_id[..8]))); fetch.emit(()); }
|
||||||
|
Err(e) => error.set(Some(e.0)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_organize = {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch_status.clone();
|
||||||
|
Callback::from(move |_: MouseEvent| {
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match api::trigger_organize().await {
|
||||||
|
Ok(t) => { message.set(Some(format!("Organizing started ({})", &t.task_id[..8]))); fetch.emit(()); }
|
||||||
|
Err(e) => error.set(Some(e.0)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(ref err) = *error {
|
if let Some(ref err) = *error {
|
||||||
return html! { <div class="error">{ format!("Error: {err}") }</div> };
|
return html! { <div class="error">{ format!("Error: {err}") }</div> };
|
||||||
}
|
}
|
||||||
@@ -36,6 +156,13 @@ pub fn dashboard() -> Html {
|
|||||||
<h2>{ "Dashboard" }</h2>
|
<h2>{ "Dashboard" }</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
if let Some(ref msg) = *message {
|
||||||
|
<div class="card" style="border-color: var(--success);">
|
||||||
|
<p>{ msg }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats grid
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="value">{ s.library.total_items }</div>
|
<div class="value">{ s.library.total_items }</div>
|
||||||
@@ -55,34 +182,117 @@ pub fn dashboard() -> Html {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// Actions
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>{ "Download Queue" }</h3>
|
<h3>{ "Actions" }</h3>
|
||||||
<p>{ format!("{} pending, {} downloading", s.queue.pending, s.queue.downloading) }</p>
|
<div class="actions mt-1">
|
||||||
|
<button class="btn btn-primary" onclick={on_sync}>{ "Sync Watchlist" }</button>
|
||||||
|
<button class="btn btn-success" onclick={on_process}>{ "Process Downloads" }</button>
|
||||||
|
<button class="btn btn-secondary" onclick={on_index}>{ "Re-index" }</button>
|
||||||
|
<button class="btn btn-secondary" onclick={on_tag}>{ "Auto-tag" }</button>
|
||||||
|
<button class="btn btn-secondary" onclick={on_organize}>{ "Organize" }</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// Background Tasks
|
||||||
if !s.tasks.is_empty() {
|
if !s.tasks.is_empty() {
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>{ "Background Tasks" }</h3>
|
<h3>{ "Background Tasks" }</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr><th>{ "Type" }</th><th>{ "Status" }</th><th>{ "Progress" }</th><th>{ "Result" }</th></tr>
|
||||||
<th>{ "Type" }</th>
|
|
||||||
<th>{ "Status" }</th>
|
|
||||||
<th>{ "Result" }</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ for s.tasks.iter().map(|t| html! {
|
{ for s.tasks.iter().map(|t| {
|
||||||
<tr>
|
let progress_html = if let Some(ref p) = t.progress {
|
||||||
<td>{ &t.task_type }</td>
|
if p.total > 0 {
|
||||||
<td><StatusBadge status={t.status.clone()} /></td>
|
let pct = (p.current as f64 / p.total as f64 * 100.0) as u32;
|
||||||
<td class="text-sm text-muted">{ t.result.as_deref().unwrap_or("") }</td>
|
html! {
|
||||||
</tr>
|
<div>
|
||||||
|
<div class="text-sm">{ &p.message }{ format!(" ({}/{})", p.current, p.total) }</div>
|
||||||
|
<div style="background: var(--bg-card); border-radius: 4px; height: 8px; margin-top: 4px;">
|
||||||
|
<div style={format!("background: var(--accent); border-radius: 4px; height: 100%; width: {pct}%;")}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! { <span class="text-sm text-muted">{ &p.message }</span> }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<tr>
|
||||||
|
<td>{ &t.task_type }</td>
|
||||||
|
<td><StatusBadge status={t.status.clone()} /></td>
|
||||||
|
<td>{ progress_html }</td>
|
||||||
|
<td class="text-sm text-muted">{ t.result.as_deref().unwrap_or("") }</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download Queue Items
|
||||||
|
if !s.queue.items.is_empty() {
|
||||||
|
<div class="card">
|
||||||
|
<h3>{ format!("Download Queue ({} pending, {} downloading, {} failed)",
|
||||||
|
s.queue.pending, s.queue.downloading, s.queue.failed) }</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>{ "Query" }</th><th>{ "Status" }</th><th>{ "Error" }</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ for s.queue.items.iter().map(|item| html! {
|
||||||
|
<tr>
|
||||||
|
<td>{ &item.query }</td>
|
||||||
|
<td><StatusBadge status={item.status.clone()} /></td>
|
||||||
|
<td class="text-sm text-muted">{ item.error_message.as_deref().unwrap_or("") }</td>
|
||||||
|
</tr>
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
} else if s.queue.pending > 0 || s.queue.downloading > 0 {
|
||||||
|
<div class="card">
|
||||||
|
<h3>{ "Download Queue" }</h3>
|
||||||
|
<p>{ format!("{} pending, {} downloading", s.queue.pending, s.queue.downloading) }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tagging Queue
|
||||||
|
if let Some(ref tagging) = s.tagging {
|
||||||
|
if tagging.needs_tagging > 0 {
|
||||||
|
<div class="card">
|
||||||
|
<h3>{ format!("Tagging Queue ({} tracks need metadata)", tagging.needs_tagging) }</h3>
|
||||||
|
if !tagging.items.is_empty() {
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>{ "Title" }</th><th>{ "Artist" }</th><th>{ "Album" }</th><th>{ "MBID" }</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ for tagging.items.iter().map(|t| html! {
|
||||||
|
<tr>
|
||||||
|
<td>{ t.title.as_deref().unwrap_or("Unknown") }</td>
|
||||||
|
<td>{ t.artist.as_deref().unwrap_or("") }</td>
|
||||||
|
<td class="text-muted">{ t.album.as_deref().unwrap_or("—") }</td>
|
||||||
|
<td class="text-sm text-muted">
|
||||||
|
if t.album.is_some() {
|
||||||
|
{ "needs album link" }
|
||||||
|
} else {
|
||||||
|
{ "needs metadata" }
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use crate::types::AppConfig;
|
|||||||
pub fn settings_page() -> Html {
|
pub fn settings_page() -> Html {
|
||||||
let config = use_state(|| None::<AppConfig>);
|
let config = use_state(|| None::<AppConfig>);
|
||||||
let error = use_state(|| None::<String>);
|
let error = use_state(|| None::<String>);
|
||||||
let message = use_state(|| None::<String>);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
@@ -22,53 +21,20 @@ pub fn settings_page() -> Html {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let trigger = |action: &'static str| {
|
|
||||||
let message = message.clone();
|
|
||||||
let error = error.clone();
|
|
||||||
Callback::from(move |_: MouseEvent| {
|
|
||||||
let message = message.clone();
|
|
||||||
let error = error.clone();
|
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
|
||||||
let result = match action {
|
|
||||||
"index" => api::trigger_index().await,
|
|
||||||
"tag" => api::trigger_tag().await,
|
|
||||||
"organize" => api::trigger_organize().await,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
match result {
|
|
||||||
Ok(t) => message.set(Some(format!("{action} started (task: {})", t.task_id))),
|
|
||||||
Err(e) => error.set(Some(e.0)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>{ "Settings" }</h2>
|
<h2>{ "Settings" }</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
if let Some(ref msg) = *message {
|
|
||||||
<div class="card" style="border-color: var(--success);">{ msg }</div>
|
|
||||||
}
|
|
||||||
if let Some(ref err) = *error {
|
if let Some(ref err) = *error {
|
||||||
<div class="card error">{ err }</div>
|
<div class="card error">{ err }</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3>{ "Actions" }</h3>
|
|
||||||
<div class="actions mt-1">
|
|
||||||
<button class="btn btn-primary" onclick={trigger("index")}>{ "Re-index Library" }</button>
|
|
||||||
<button class="btn btn-primary" onclick={trigger("tag")}>{ "Auto-tag Tracks" }</button>
|
|
||||||
<button class="btn btn-primary" onclick={trigger("organize")}>{ "Organize Files" }</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ match &*config {
|
{ match &*config {
|
||||||
None => html! { <p class="loading">{ "Loading configuration..." }</p> },
|
None => html! { <p class="loading">{ "Loading configuration..." }</p> },
|
||||||
Some(c) => html! {
|
Some(c) => html! {
|
||||||
<div class="card mt-2">
|
<div class="card">
|
||||||
<h3>{ "Configuration" }</h3>
|
<h3>{ "Configuration" }</h3>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -102,11 +102,19 @@ pub struct DownloadItem {
|
|||||||
|
|
||||||
// --- Tasks ---
|
// --- Tasks ---
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct TaskProgress {
|
||||||
|
pub current: u64,
|
||||||
|
pub total: u64,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub struct TaskInfo {
|
pub struct TaskInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub task_type: String,
|
pub task_type: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
pub progress: Option<TaskProgress>,
|
||||||
pub result: Option<String>,
|
pub result: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +125,19 @@ pub struct TaskRef {
|
|||||||
|
|
||||||
// --- Status ---
|
// --- Status ---
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct TaggingStatus {
|
||||||
|
pub needs_tagging: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub items: Vec<Track>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub library: LibrarySummary,
|
pub library: LibrarySummary,
|
||||||
pub queue: QueueStatus,
|
pub queue: QueueStatus,
|
||||||
|
#[serde(default)]
|
||||||
|
pub tagging: Option<TaggingStatus>,
|
||||||
pub tasks: Vec<TaskInfo>,
|
pub tasks: Vec<TaskInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +154,10 @@ pub struct LibrarySummary {
|
|||||||
pub struct QueueStatus {
|
pub struct QueueStatus {
|
||||||
pub pending: usize,
|
pub pending: usize,
|
||||||
pub downloading: usize,
|
pub downloading: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub failed: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub items: Vec<DownloadItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- API responses ---
|
// --- API responses ---
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ async fn trigger_process(
|
|||||||
let tid = task_id.clone();
|
let tid = task_id.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
// Count total pending for progress reporting
|
||||||
|
let total = shanty_db::queries::downloads::list(
|
||||||
|
state.db.conn(),
|
||||||
|
Some(shanty_db::entities::download_queue::DownloadStatus::Pending),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|v| v.len() as u64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
state.tasks.update_progress(&tid, 0, total, "Starting downloads...");
|
||||||
|
|
||||||
let cookies = state.config.download.cookies_path.clone();
|
let cookies = state.config.download.cookies_path.clone();
|
||||||
let format: shanty_dl::AudioFormat = state.config.download.format.parse().unwrap_or(shanty_dl::AudioFormat::Opus);
|
let format: shanty_dl::AudioFormat = state.config.download.format.parse().unwrap_or(shanty_dl::AudioFormat::Opus);
|
||||||
let source: shanty_dl::SearchSource = state.config.download.search_source.parse().unwrap_or(shanty_dl::SearchSource::YouTubeMusic);
|
let source: shanty_dl::SearchSource = state.config.download.search_source.parse().unwrap_or(shanty_dl::SearchSource::YouTubeMusic);
|
||||||
|
|||||||
@@ -21,19 +21,31 @@ async fn get_status(
|
|||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let summary = shanty_watch::library_summary(state.db.conn()).await?;
|
let summary = shanty_watch::library_summary(state.db.conn()).await?;
|
||||||
let pending = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Pending))
|
let pending_items = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Pending)).await?;
|
||||||
.await?
|
let downloading_items = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Downloading)).await?;
|
||||||
.len();
|
let failed_items = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Failed)).await?;
|
||||||
let downloading = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Downloading))
|
|
||||||
.await?
|
|
||||||
.len();
|
|
||||||
let tasks = state.tasks.list();
|
let tasks = state.tasks.list();
|
||||||
|
|
||||||
|
// Combine active/recent download items for the dashboard
|
||||||
|
let mut queue_items = Vec::new();
|
||||||
|
queue_items.extend(downloading_items.iter().cloned());
|
||||||
|
queue_items.extend(pending_items.iter().cloned());
|
||||||
|
queue_items.extend(failed_items.iter().take(5).cloned());
|
||||||
|
|
||||||
|
// Tracks needing metadata (tagging queue)
|
||||||
|
let needs_tagging = queries::tracks::get_needing_metadata(state.db.conn()).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
"library": summary,
|
"library": summary,
|
||||||
"queue": {
|
"queue": {
|
||||||
"pending": pending,
|
"pending": pending_items.len(),
|
||||||
"downloading": downloading,
|
"downloading": downloading_items.len(),
|
||||||
|
"failed": failed_items.len(),
|
||||||
|
"items": queue_items,
|
||||||
|
},
|
||||||
|
"tagging": {
|
||||||
|
"needs_tagging": needs_tagging.len(),
|
||||||
|
"items": needs_tagging.iter().take(20).collect::<Vec<_>>(),
|
||||||
},
|
},
|
||||||
"tasks": tasks,
|
"tasks": tasks,
|
||||||
})))
|
})))
|
||||||
@@ -47,6 +59,7 @@ async fn trigger_index(
|
|||||||
let tid = task_id.clone();
|
let tid = task_id.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
state.tasks.update_progress(&tid, 0, 0, "Scanning library...");
|
||||||
let scan_config = shanty_index::ScanConfig {
|
let scan_config = shanty_index::ScanConfig {
|
||||||
root: state.config.library_path.clone(),
|
root: state.config.library_path.clone(),
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
@@ -69,6 +82,7 @@ async fn trigger_tag(
|
|||||||
let tid = task_id.clone();
|
let tid = task_id.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
state.tasks.update_progress(&tid, 0, 0, "Preparing tagger...");
|
||||||
let mb = match shanty_tag::MusicBrainzClient::new() {
|
let mb = match shanty_tag::MusicBrainzClient::new() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -81,6 +95,7 @@ async fn trigger_tag(
|
|||||||
write_tags: state.config.tagging.write_tags,
|
write_tags: state.config.tagging.write_tags,
|
||||||
confidence: state.config.tagging.confidence,
|
confidence: state.config.tagging.confidence,
|
||||||
};
|
};
|
||||||
|
state.tasks.update_progress(&tid, 0, 0, "Tagging tracks...");
|
||||||
match shanty_tag::run_tagging(state.db.conn(), &mb, &tag_config, None).await {
|
match shanty_tag::run_tagging(state.db.conn(), &mb, &tag_config, None).await {
|
||||||
Ok(stats) => state.tasks.complete(&tid, format!("{stats}")),
|
Ok(stats) => state.tasks.complete(&tid, format!("{stats}")),
|
||||||
Err(e) => state.tasks.fail(&tid, e.to_string()),
|
Err(e) => state.tasks.fail(&tid, e.to_string()),
|
||||||
@@ -98,6 +113,7 @@ async fn trigger_organize(
|
|||||||
let tid = task_id.clone();
|
let tid = task_id.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
state.tasks.update_progress(&tid, 0, 0, "Organizing files...");
|
||||||
let org_config = shanty_org::OrgConfig {
|
let org_config = shanty_org::OrgConfig {
|
||||||
target_dir: state.config.library_path.clone(),
|
target_dir: state.config.library_path.clone(),
|
||||||
format: state.config.organization_format.clone(),
|
format: state.config.organization_format.clone(),
|
||||||
@@ -105,7 +121,18 @@ async fn trigger_organize(
|
|||||||
copy: false,
|
copy: false,
|
||||||
};
|
};
|
||||||
match shanty_org::organize_from_db(state.db.conn(), &org_config).await {
|
match shanty_org::organize_from_db(state.db.conn(), &org_config).await {
|
||||||
Ok(stats) => state.tasks.complete(&tid, format!("{stats}")),
|
Ok(stats) => {
|
||||||
|
// Promote all Downloaded wanted items to Owned
|
||||||
|
let promoted = queries::wanted::promote_downloaded_to_owned(state.db.conn())
|
||||||
|
.await
|
||||||
|
.unwrap_or(0);
|
||||||
|
let msg = if promoted > 0 {
|
||||||
|
format!("{stats} — {promoted} items marked as owned")
|
||||||
|
} else {
|
||||||
|
format!("{stats}")
|
||||||
|
};
|
||||||
|
state.tasks.complete(&tid, msg);
|
||||||
|
}
|
||||||
Err(e) => state.tasks.fail(&tid, e.to_string()),
|
Err(e) => state.tasks.fail(&tid, e.to_string()),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
22
src/tasks.rs
22
src/tasks.rs
@@ -4,11 +4,19 @@ use std::sync::Mutex;
|
|||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct TaskProgress {
|
||||||
|
pub current: u64,
|
||||||
|
pub total: u64,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct TaskInfo {
|
pub struct TaskInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub task_type: String,
|
pub task_type: String,
|
||||||
pub status: TaskStatus,
|
pub status: TaskStatus,
|
||||||
|
pub progress: Option<TaskProgress>,
|
||||||
pub started_at: NaiveDateTime,
|
pub started_at: NaiveDateTime,
|
||||||
pub completed_at: Option<NaiveDateTime>,
|
pub completed_at: Option<NaiveDateTime>,
|
||||||
pub result: Option<String>,
|
pub result: Option<String>,
|
||||||
@@ -39,6 +47,7 @@ impl TaskManager {
|
|||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
task_type: task_type.to_string(),
|
task_type: task_type.to_string(),
|
||||||
status: TaskStatus::Running,
|
status: TaskStatus::Running,
|
||||||
|
progress: None,
|
||||||
started_at: Utc::now().naive_utc(),
|
started_at: Utc::now().naive_utc(),
|
||||||
completed_at: None,
|
completed_at: None,
|
||||||
result: None,
|
result: None,
|
||||||
@@ -47,10 +56,22 @@ impl TaskManager {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update progress on a running task.
|
||||||
|
pub fn update_progress(&self, id: &str, current: u64, total: u64, message: &str) {
|
||||||
|
if let Some(task) = self.tasks.lock().unwrap().get_mut(id) {
|
||||||
|
task.progress = Some(TaskProgress {
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
message: message.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark a task as completed with a result string.
|
/// Mark a task as completed with a result string.
|
||||||
pub fn complete(&self, id: &str, result: String) {
|
pub fn complete(&self, id: &str, result: String) {
|
||||||
if let Some(task) = self.tasks.lock().unwrap().get_mut(id) {
|
if let Some(task) = self.tasks.lock().unwrap().get_mut(id) {
|
||||||
task.status = TaskStatus::Completed;
|
task.status = TaskStatus::Completed;
|
||||||
|
task.progress = None;
|
||||||
task.completed_at = Some(Utc::now().naive_utc());
|
task.completed_at = Some(Utc::now().naive_utc());
|
||||||
task.result = Some(result);
|
task.result = Some(result);
|
||||||
}
|
}
|
||||||
@@ -60,6 +81,7 @@ impl TaskManager {
|
|||||||
pub fn fail(&self, id: &str, error: String) {
|
pub fn fail(&self, id: &str, error: String) {
|
||||||
if let Some(task) = self.tasks.lock().unwrap().get_mut(id) {
|
if let Some(task) = self.tasks.lock().unwrap().get_mut(id) {
|
||||||
task.status = TaskStatus::Failed;
|
task.status = TaskStatus::Failed;
|
||||||
|
task.progress = None;
|
||||||
task.completed_at = Some(Utc::now().naive_utc());
|
task.completed_at = Some(Utc::now().naive_utc());
|
||||||
task.result = Some(error);
|
task.result = Some(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user