Compare commits
1 Commits
a268ec4e56
...
8b16859526
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b16859526 |
@@ -1,8 +1,10 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use gloo_timers::callback::Interval;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::components::watch_indicator::WatchIndicator;
|
|
||||||
use crate::pages::Route;
|
use crate::pages::Route;
|
||||||
use crate::types::FullArtistDetail;
|
use crate::types::FullArtistDetail;
|
||||||
|
|
||||||
@@ -18,11 +20,16 @@ pub fn artist_page(props: &Props) -> Html {
|
|||||||
let message = use_state(|| None::<String>);
|
let message = use_state(|| None::<String>);
|
||||||
let id = props.id.clone();
|
let id = props.id.clone();
|
||||||
|
|
||||||
|
// Flag to prevent the background enrichment from overwriting a user-triggered refresh
|
||||||
|
let user_acted: Rc<Cell<bool>> = use_memo((), |_| Cell::new(false));
|
||||||
|
|
||||||
// Full fetch (with track counts) — used for refresh after actions
|
// Full fetch (with track counts) — used for refresh after actions
|
||||||
let fetch = {
|
let fetch = {
|
||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
let error = error.clone();
|
let error = error.clone();
|
||||||
|
let user_acted = user_acted.clone();
|
||||||
Callback::from(move |id: String| {
|
Callback::from(move |id: String| {
|
||||||
|
user_acted.set(true);
|
||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
let error = error.clone();
|
let error = error.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
@@ -39,7 +46,9 @@ pub fn artist_page(props: &Props) -> Html {
|
|||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
let error = error.clone();
|
let error = error.clone();
|
||||||
let id = id.clone();
|
let id = id.clone();
|
||||||
|
let user_acted = user_acted.clone();
|
||||||
use_effect_with(id.clone(), move |_| {
|
use_effect_with(id.clone(), move |_| {
|
||||||
|
user_acted.set(false);
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
// Phase 1: quick load (instant for browsing artists)
|
// Phase 1: quick load (instant for browsing artists)
|
||||||
match api::get_artist_full_quick(&id).await {
|
match api::get_artist_full_quick(&id).await {
|
||||||
@@ -48,9 +57,14 @@ pub fn artist_page(props: &Props) -> Html {
|
|||||||
detail.set(Some(d));
|
detail.set(Some(d));
|
||||||
|
|
||||||
// Phase 2: if not enriched, fetch full data in background
|
// Phase 2: if not enriched, fetch full data in background
|
||||||
if needs_enrich {
|
if needs_enrich && !user_acted.get() {
|
||||||
match api::get_artist_full(&id).await {
|
match api::get_artist_full(&id).await {
|
||||||
Ok(full) => detail.set(Some(full)),
|
Ok(full) => {
|
||||||
|
// Only apply if user hasn't triggered a refresh
|
||||||
|
if !user_acted.get() {
|
||||||
|
detail.set(Some(full));
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(_) => {} // quick data is still showing, don't overwrite with error
|
Err(_) => {} // quick data is still showing, don't overwrite with error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,6 +75,31 @@ pub fn artist_page(props: &Props) -> Html {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-refresh when background tasks complete (downloads, organize, etc.)
|
||||||
|
{
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
let id = id.clone();
|
||||||
|
let had_tasks: Rc<Cell<bool>> = use_memo((), |_| Cell::new(false));
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
let interval = Interval::new(3_000, move || {
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
let id = id.clone();
|
||||||
|
let had_tasks = had_tasks.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
if let Ok(status) = api::get_status().await {
|
||||||
|
let has_running = status.tasks.iter().any(|t| t.status == "Running");
|
||||||
|
if had_tasks.get() && !has_running {
|
||||||
|
// Tasks just finished — refresh artist data
|
||||||
|
fetch.emit(id);
|
||||||
|
}
|
||||||
|
had_tasks.set(has_running);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
move || drop(interval)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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> };
|
||||||
}
|
}
|
||||||
@@ -69,10 +108,53 @@ pub fn artist_page(props: &Props) -> Html {
|
|||||||
return html! { <p class="loading">{ "Loading discography from MusicBrainz..." }</p> };
|
return html! { <p class="loading">{ "Loading discography from MusicBrainz..." }</p> };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let watch_all_btn = {
|
||||||
|
let artist_status = d.artist_status.clone();
|
||||||
|
let show = artist_status != "owned";
|
||||||
|
if show {
|
||||||
|
let artist_name = d.artist.name.clone();
|
||||||
|
let artist_mbid = d.artist.musicbrainz_id.clone();
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
let artist_id = id.clone();
|
||||||
|
html! {
|
||||||
|
<button class="btn btn-sm btn-success"
|
||||||
|
onclick={Callback::from(move |_: MouseEvent| {
|
||||||
|
let artist_name = artist_name.clone();
|
||||||
|
let artist_mbid = artist_mbid.clone();
|
||||||
|
let message = message.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let fetch = fetch.clone();
|
||||||
|
let artist_id = artist_id.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match api::add_artist(&artist_name, artist_mbid.as_deref()).await {
|
||||||
|
Ok(s) => {
|
||||||
|
message.set(Some(format!(
|
||||||
|
"Watching {}: added {} tracks ({} already owned)",
|
||||||
|
artist_name, s.tracks_added, s.tracks_already_owned
|
||||||
|
)));
|
||||||
|
fetch.emit(artist_id);
|
||||||
|
}
|
||||||
|
Err(e) => error.set(Some(e.0)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})}>
|
||||||
|
{ "Watch All" }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>{ &d.artist.name }</h2>
|
<div class="flex items-center justify-between">
|
||||||
|
<h2>{ &d.artist.name }</h2>
|
||||||
|
{ watch_all_btn }
|
||||||
|
</div>
|
||||||
if d.enriched {
|
if d.enriched {
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
<span style="color: var(--accent);">
|
<span style="color: var(--accent);">
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ pub fn dashboard() -> Html {
|
|||||||
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 class="tasks-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>{ "Type" }</th><th>{ "Status" }</th><th>{ "Progress" }</th><th>{ "Result" }</th></tr>
|
<tr><th>{ "Type" }</th><th>{ "Status" }</th><th>{ "Progress" }</th><th>{ "Result" }</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ pub fn library_page() -> Html {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{ "Name" }</th>
|
<th>{ "Name" }</th>
|
||||||
<th>{ "Status" }</th>
|
<th>{ "Owned" }</th>
|
||||||
|
<th>{ "Watched" }</th>
|
||||||
|
<th>{ "Tracks" }</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -60,14 +62,30 @@ pub fn library_page() -> Html {
|
|||||||
<td>
|
<td>
|
||||||
if a.total_items > 0 {
|
if a.total_items > 0 {
|
||||||
<span class="text-sm" style={
|
<span class="text-sm" style={
|
||||||
if a.total_owned == a.total_items { "color: var(--success);" }
|
if a.total_owned >= a.total_watched && a.total_watched > 0 { "color: var(--success);" }
|
||||||
else if a.total_owned > 0 { "color: var(--warning);" }
|
else if a.total_owned > 0 { "color: var(--warning);" }
|
||||||
else { "color: var(--accent);" }
|
else { "color: var(--text-muted);" }
|
||||||
}>
|
}>
|
||||||
{ format!("{}/{} owned", a.total_owned, a.total_items) }
|
{ format!("{}/{}", a.total_owned, a.total_watched) }
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
if a.total_items > 0 {
|
||||||
|
<span class="text-sm" style={
|
||||||
|
if a.total_watched >= a.total_items { "color: var(--accent);" }
|
||||||
|
else if a.total_watched > 0 { "color: var(--accent);" }
|
||||||
|
else { "color: var(--text-muted);" }
|
||||||
|
}>
|
||||||
|
{ format!("{}/{}", a.total_watched, a.total_items) }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-muted text-sm">
|
||||||
|
if a.total_items > 0 {
|
||||||
|
{ a.total_items }
|
||||||
|
}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
use web_sys::HtmlSelectElement;
|
use web_sys::HtmlSelectElement;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
use crate::pages::Route;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
|
||||||
enum SearchResults {
|
enum SearchResults {
|
||||||
@@ -34,15 +36,15 @@ pub fn search_page() -> Html {
|
|||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
error.set(None);
|
error.set(None);
|
||||||
match st.as_str() {
|
match st.as_str() {
|
||||||
"artist" => match api::search_artist(&q, 10).await {
|
"artist" => match api::search_artist(&q, 50).await {
|
||||||
Ok(r) => results.set(SearchResults::Artists(r)),
|
Ok(r) => results.set(SearchResults::Artists(r)),
|
||||||
Err(e) => error.set(Some(e.0)),
|
Err(e) => error.set(Some(e.0)),
|
||||||
},
|
},
|
||||||
"album" => match api::search_album(&q, None, 10).await {
|
"album" => match api::search_album(&q, None, 50).await {
|
||||||
Ok(r) => results.set(SearchResults::Albums(r)),
|
Ok(r) => results.set(SearchResults::Albums(r)),
|
||||||
Err(e) => error.set(Some(e.0)),
|
Err(e) => error.set(Some(e.0)),
|
||||||
},
|
},
|
||||||
"track" => match api::search_track(&q, None, 10).await {
|
"track" => match api::search_track(&q, None, 50).await {
|
||||||
Ok(r) => results.set(SearchResults::Tracks(r)),
|
Ok(r) => results.set(SearchResults::Tracks(r)),
|
||||||
Err(e) => error.set(Some(e.0)),
|
Err(e) => error.set(Some(e.0)),
|
||||||
},
|
},
|
||||||
@@ -52,24 +54,6 @@ pub fn search_page() -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let on_add_artist = {
|
|
||||||
let message = message.clone();
|
|
||||||
let error = error.clone();
|
|
||||||
move |name: String, mbid: String| {
|
|
||||||
let message = message.clone();
|
|
||||||
let error = error.clone();
|
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
|
||||||
match api::add_artist(&name, Some(&mbid)).await {
|
|
||||||
Ok(s) => message.set(Some(format!(
|
|
||||||
"Added {} tracks ({} already owned)",
|
|
||||||
s.tracks_added, s.tracks_already_owned
|
|
||||||
))),
|
|
||||||
Err(e) => error.set(Some(e.0)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_add_album = {
|
let on_add_album = {
|
||||||
let message = message.clone();
|
let message = message.clone();
|
||||||
let error = error.clone();
|
let error = error.clone();
|
||||||
@@ -132,17 +116,16 @@ pub fn search_page() -> Html {
|
|||||||
SearchResults::Artists(items) => html! {
|
SearchResults::Artists(items) => html! {
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>{ "Name" }</th><th>{ "Country" }</th><th>{ "Type" }</th><th>{ "Score" }</th><th></th></tr>
|
<tr><th>{ "Name" }</th><th>{ "Country" }</th><th>{ "Type" }</th><th>{ "Score" }</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ for items.iter().map(|a| {
|
{ for items.iter().map(|a| {
|
||||||
let name = a.name.clone();
|
|
||||||
let mbid = a.id.clone();
|
|
||||||
let on_add = on_add_artist.clone();
|
|
||||||
html! {
|
html! {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{ &a.name }
|
<Link<Route> to={Route::Artist { id: a.id.clone() }}>
|
||||||
|
{ &a.name }
|
||||||
|
</Link<Route>>
|
||||||
if let Some(ref d) = a.disambiguation {
|
if let Some(ref d) = a.disambiguation {
|
||||||
<span class="text-muted text-sm">{ format!(" ({d})") }</span>
|
<span class="text-muted text-sm">{ format!(" ({d})") }</span>
|
||||||
}
|
}
|
||||||
@@ -150,12 +133,6 @@ pub fn search_page() -> Html {
|
|||||||
<td>{ a.country.as_deref().unwrap_or("") }</td>
|
<td>{ a.country.as_deref().unwrap_or("") }</td>
|
||||||
<td>{ a.artist_type.as_deref().unwrap_or("") }</td>
|
<td>{ a.artist_type.as_deref().unwrap_or("") }</td>
|
||||||
<td>{ a.score }</td>
|
<td>{ a.score }</td>
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-success"
|
|
||||||
onclick={Callback::from(move |_| on_add(name.clone(), mbid.clone()))}>
|
|
||||||
{ "Add" }
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@@ -175,7 +152,11 @@ pub fn search_page() -> Html {
|
|||||||
let on_add = on_add_album.clone();
|
let on_add = on_add_album.clone();
|
||||||
html! {
|
html! {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{ &a.title }</td>
|
<td>
|
||||||
|
<Link<Route> to={Route::Album { mbid: a.id.clone() }}>
|
||||||
|
{ &a.title }
|
||||||
|
</Link<Route>>
|
||||||
|
</td>
|
||||||
<td>{ &a.artist }</td>
|
<td>{ &a.artist }</td>
|
||||||
<td>{ a.year.as_deref().unwrap_or("") }</td>
|
<td>{ a.year.as_deref().unwrap_or("") }</td>
|
||||||
<td>{ a.score }</td>
|
<td>{ a.score }</td>
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ input:focus, select:focus { outline: none; border-color: var(--accent); }
|
|||||||
.badge-failed { background: var(--danger); color: white; }
|
.badge-failed { background: var(--danger); color: white; }
|
||||||
.badge-completed { background: var(--success); color: white; }
|
.badge-completed { background: var(--success); color: white; }
|
||||||
|
|
||||||
|
/* Task table fixed column widths */
|
||||||
|
table.tasks-table { table-layout: fixed; }
|
||||||
|
table.tasks-table th:nth-child(1) { width: 100px; }
|
||||||
|
table.tasks-table th:nth-child(2) { width: 100px; }
|
||||||
|
table.tasks-table th:nth-child(3) { width: 40%; }
|
||||||
|
table.tasks-table td { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
/* Page header */
|
/* Page header */
|
||||||
.page-header { margin-bottom: 1.5rem; }
|
.page-header { margin-bottom: 1.5rem; }
|
||||||
.page-header h2 { font-size: 1.5rem; }
|
.page-header h2 { font-size: 1.5rem; }
|
||||||
|
|||||||
@@ -162,14 +162,38 @@ async fn add_album(
|
|||||||
if body.artist.is_none() && body.album.is_none() && body.mbid.is_none() {
|
if body.artist.is_none() && body.album.is_none() && body.mbid.is_none() {
|
||||||
return Err(ApiError::BadRequest("provide artist+album or mbid".into()));
|
return Err(ApiError::BadRequest("provide artist+album or mbid".into()));
|
||||||
}
|
}
|
||||||
let summary = shanty_watch::add_album(
|
|
||||||
|
// Try adding with the given MBID first. If it fails (e.g., the MBID is a release-group,
|
||||||
|
// not a release), resolve it to an actual release MBID and retry.
|
||||||
|
let mut mbid = body.mbid.clone();
|
||||||
|
let result = shanty_watch::add_album(
|
||||||
state.db.conn(),
|
state.db.conn(),
|
||||||
body.artist.as_deref(),
|
body.artist.as_deref(),
|
||||||
body.album.as_deref(),
|
body.album.as_deref(),
|
||||||
body.mbid.as_deref(),
|
mbid.as_deref(),
|
||||||
&state.mb_client,
|
&state.mb_client,
|
||||||
)
|
)
|
||||||
.await?;
|
.await;
|
||||||
|
|
||||||
|
let summary = match result {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) if mbid.is_some() => {
|
||||||
|
// MBID might be a release-group — resolve to the first release
|
||||||
|
let rg_mbid = mbid.as_deref().unwrap();
|
||||||
|
let release_mbid = resolve_release_from_group(&state, rg_mbid).await?;
|
||||||
|
mbid = Some(release_mbid);
|
||||||
|
shanty_watch::add_album(
|
||||||
|
state.db.conn(),
|
||||||
|
body.artist.as_deref(),
|
||||||
|
body.album.as_deref(),
|
||||||
|
mbid.as_deref(),
|
||||||
|
&state.mb_client,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
"tracks_added": summary.tracks_added,
|
"tracks_added": summary.tracks_added,
|
||||||
"tracks_already_owned": summary.tracks_already_owned,
|
"tracks_already_owned": summary.tracks_already_owned,
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ async fn get_cached_album_tracks(
|
|||||||
rg_id: &str,
|
rg_id: &str,
|
||||||
first_release_id: Option<&str>,
|
first_release_id: Option<&str>,
|
||||||
ttl_seconds: i64,
|
ttl_seconds: i64,
|
||||||
|
extend_ttl: bool,
|
||||||
) -> Result<CachedAlbumTracks, ApiError> {
|
) -> Result<CachedAlbumTracks, ApiError> {
|
||||||
let cache_key = format!("artist_rg_tracks:{rg_id}");
|
let cache_key = format!("artist_rg_tracks:{rg_id}");
|
||||||
|
|
||||||
@@ -156,6 +157,10 @@ async fn get_cached_album_tracks(
|
|||||||
.map_err(|e| ApiError::Internal(e.to_string()))?
|
.map_err(|e| ApiError::Internal(e.to_string()))?
|
||||||
{
|
{
|
||||||
if let Ok(cached) = serde_json::from_str::<CachedAlbumTracks>(&json) {
|
if let Ok(cached) = serde_json::from_str::<CachedAlbumTracks>(&json) {
|
||||||
|
// Extend TTL if artist is now watched (upgrades 7-day browse cache to permanent)
|
||||||
|
if extend_ttl {
|
||||||
|
let _ = queries::cache::set(state.db.conn(), &cache_key, "musicbrainz", &json, ttl_seconds).await;
|
||||||
|
}
|
||||||
return Ok(cached);
|
return Ok(cached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,13 +358,15 @@ async fn get_artist_full(
|
|||||||
|
|
||||||
// If artist has any watched items, cache permanently (10 years);
|
// If artist has any watched items, cache permanently (10 years);
|
||||||
// otherwise cache for 7 days (just browsing)
|
// otherwise cache for 7 days (just browsing)
|
||||||
let cache_ttl = if artist_wanted.is_empty() { 7 * 86400 } else { 10 * 365 * 86400 };
|
let is_watched = !artist_wanted.is_empty();
|
||||||
|
let cache_ttl = if is_watched { 10 * 365 * 86400 } else { 7 * 86400 };
|
||||||
|
|
||||||
let cached = match get_cached_album_tracks(
|
let cached = match get_cached_album_tracks(
|
||||||
&state,
|
&state,
|
||||||
&rg.id,
|
&rg.id,
|
||||||
rg.first_release_id.as_deref(),
|
rg.first_release_id.as_deref(),
|
||||||
cache_ttl,
|
cache_ttl,
|
||||||
|
is_watched,
|
||||||
).await {
|
).await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -87,17 +87,6 @@ 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);
|
||||||
@@ -108,8 +97,19 @@ async fn trigger_process(
|
|||||||
format,
|
format,
|
||||||
cookies_path: cookies,
|
cookies_path: cookies,
|
||||||
};
|
};
|
||||||
match shanty_dl::run_queue(state.db.conn(), &backend, &backend_config, false).await {
|
|
||||||
Ok(stats) => state.tasks.complete(&tid, format!("{stats}")),
|
let task_state = state.clone();
|
||||||
|
let progress_tid = tid.clone();
|
||||||
|
let on_progress: shanty_dl::ProgressFn = Box::new(move |current, total, msg| {
|
||||||
|
task_state.tasks.update_progress(&progress_tid, current, total, msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
match shanty_dl::run_queue_with_progress(state.db.conn(), &backend, &backend_config, false, Some(on_progress)).await {
|
||||||
|
Ok(stats) => {
|
||||||
|
// Invalidate cached artist totals so library/detail pages show fresh data
|
||||||
|
let _ = shanty_db::queries::cache::purge_prefix(state.db.conn(), "artist_totals:").await;
|
||||||
|
state.tasks.complete(&tid, format!("{stats}"));
|
||||||
|
}
|
||||||
Err(e) => state.tasks.fail(&tid, e.to_string()),
|
Err(e) => state.tasks.fail(&tid, e.to_string()),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub struct AlbumTrackSearchParams {
|
|||||||
limit: u32,
|
limit: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_limit() -> u32 { 10 }
|
fn default_limit() -> u32 { 25 }
|
||||||
|
|
||||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(web::resource("/search/artist").route(web::get().to(search_artist)))
|
cfg.service(web::resource("/search/artist").route(web::get().to(search_artist)))
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ async fn trigger_organize(
|
|||||||
let promoted = queries::wanted::promote_downloaded_to_owned(state.db.conn())
|
let promoted = queries::wanted::promote_downloaded_to_owned(state.db.conn())
|
||||||
.await
|
.await
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
// Invalidate cached artist totals so library/detail pages show fresh data
|
||||||
|
let _ = queries::cache::purge_prefix(state.db.conn(), "artist_totals:").await;
|
||||||
let msg = if promoted > 0 {
|
let msg = if promoted > 0 {
|
||||||
format!("{stats} — {promoted} items marked as owned")
|
format!("{stats} — {promoted} items marked as owned")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user