fixed up the featured artist thing
This commit is contained in:
@@ -176,8 +176,12 @@ pub async fn add_album(
|
|||||||
post_json(&format!("{BASE}/albums"), &body).await
|
post_json(&format!("{BASE}/albums"), &body).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn watch_track(title: &str, mbid: &str) -> Result<WatchTrackResponse, ApiError> {
|
pub async fn watch_track(
|
||||||
let body = serde_json::json!({"title": title, "mbid": mbid}).to_string();
|
artist: Option<&str>,
|
||||||
|
title: &str,
|
||||||
|
mbid: &str,
|
||||||
|
) -> Result<WatchTrackResponse, ApiError> {
|
||||||
|
let body = serde_json::json!({"artist": artist, "title": title, "mbid": mbid}).to_string();
|
||||||
post_json(&format!("{BASE}/tracks/watch"), &body).await
|
post_json(&format!("{BASE}/tracks/watch"), &body).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,13 +122,15 @@ pub fn album_page(props: &Props) -> Html {
|
|||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
let title = t.title.clone();
|
let title = t.title.clone();
|
||||||
let mbid = t.recording_mbid.clone();
|
let mbid = t.recording_mbid.clone();
|
||||||
|
let artist = d.artist.clone();
|
||||||
Callback::from(move |_: MouseEvent| {
|
Callback::from(move |_: MouseEvent| {
|
||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
let title = title.clone();
|
let title = title.clone();
|
||||||
let mbid = mbid.clone();
|
let mbid = mbid.clone();
|
||||||
|
let artist = artist.clone();
|
||||||
let idx = idx;
|
let idx = idx;
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
if let Ok(resp) = api::watch_track(&title, &mbid).await {
|
if let Ok(resp) = api::watch_track(artist.as_deref(), &title, &mbid).await {
|
||||||
if let Some(ref d) = *detail {
|
if let Some(ref d) = *detail {
|
||||||
let mut updated = d.clone();
|
let mut updated = d.clone();
|
||||||
if let Some(track) = updated.tracks.get_mut(idx) {
|
if let Some(track) = updated.tracks.get_mut(idx) {
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ pub fn artist_page(props: &Props) -> Html {
|
|||||||
<p class="text-muted">{ "No releases found on MusicBrainz." }</p>
|
<p class="text-muted">{ "No releases found on MusicBrainz." }</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group albums by type
|
// Group albums by type (primary credit only)
|
||||||
{ for ["Album", "EP", "Single"].iter().map(|release_type| {
|
{ for ["Album", "EP", "Single"].iter().map(|release_type| {
|
||||||
let type_albums: Vec<_> = d.albums.iter()
|
let type_albums: Vec<_> = d.albums.iter()
|
||||||
.filter(|a| a.release_type.as_deref().unwrap_or("Album") == *release_type)
|
.filter(|a| a.release_type.as_deref().unwrap_or("Album") == *release_type)
|
||||||
@@ -397,6 +397,56 @@ pub fn artist_page(props: &Props) -> Html {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
// Featured releases (collapsible, pre-collapsed)
|
||||||
|
{ for ["Album", "EP", "Single"].iter().map(|release_type| {
|
||||||
|
let featured: Vec<_> = d.featured_albums.iter()
|
||||||
|
.filter(|a| a.release_type.as_deref().unwrap_or("Album") == *release_type)
|
||||||
|
.collect();
|
||||||
|
if featured.is_empty() {
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
html! {
|
||||||
|
<details class="mb-2">
|
||||||
|
<summary class="text-muted" style="cursor: pointer;">
|
||||||
|
{ format!("Featured {}s ({})", release_type, featured.len()) }
|
||||||
|
</summary>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 60px;"></th>
|
||||||
|
<th>{ "Title" }</th>
|
||||||
|
<th>{ "Date" }</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ for featured.iter().map(|album| {
|
||||||
|
let cover_url = format!("https://coverartarchive.org/release/{}/front-250", album.mbid);
|
||||||
|
html! {
|
||||||
|
<tr style="opacity: 0.6;">
|
||||||
|
<td>
|
||||||
|
<img class="album-art" src={cover_url}
|
||||||
|
loading="lazy"
|
||||||
|
onerror={Callback::from(|e: web_sys::Event| {
|
||||||
|
if let Some(el) = e.target_dyn_into::<web_sys::HtmlElement>() {
|
||||||
|
el.set_attribute("style", "display:none").ok();
|
||||||
|
}
|
||||||
|
})} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Link<Route> to={Route::Album { mbid: album.mbid.clone() }}>
|
||||||
|
{ &album.title }
|
||||||
|
</Link<Route>>
|
||||||
|
</td>
|
||||||
|
<td class="text-muted">{ album.date.as_deref().unwrap_or("") }</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,12 +240,8 @@ pub fn dashboard() -> Html {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|t| t.status == "Pending" || t.status == "Running");
|
.any(|t| t.status == "Pending" || t.status == "Running");
|
||||||
|
|
||||||
// Pre-compute scheduled task rows
|
// Skip callbacks for scheduler
|
||||||
let scheduled_rows = {
|
let on_skip_pipeline = {
|
||||||
let mut rows = Vec::new();
|
|
||||||
if let Some(ref sched) = s.scheduled {
|
|
||||||
if let Some(ref next) = sched.next_pipeline {
|
|
||||||
let on_skip = {
|
|
||||||
let message = message.clone();
|
let message = message.clone();
|
||||||
let error = error.clone();
|
let error = error.clone();
|
||||||
let fetch = fetch_status.clone();
|
let fetch = fetch_status.clone();
|
||||||
@@ -264,17 +260,7 @@ pub fn dashboard() -> Html {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
rows.push(html! {
|
let on_skip_monitor = {
|
||||||
<tr>
|
|
||||||
<td>{ "Auto Pipeline" }</td>
|
|
||||||
<td><span class="badge badge-pending">{ "Scheduled" }</span></td>
|
|
||||||
<td class="text-sm text-muted">{ format!("Next run: {}", format_next_run(next)) }</td>
|
|
||||||
<td><button class="btn btn-sm btn-danger" onclick={on_skip}>{ "Skip" }</button></td>
|
|
||||||
</tr>
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(ref next) = sched.next_monitor {
|
|
||||||
let on_skip = {
|
|
||||||
let message = message.clone();
|
let message = message.clone();
|
||||||
let error = error.clone();
|
let error = error.clone();
|
||||||
let fetch = fetch_status.clone();
|
let fetch = fetch_status.clone();
|
||||||
@@ -293,19 +279,68 @@ pub fn dashboard() -> Html {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
rows.push(html! {
|
|
||||||
|
let scheduled_jobs_html = {
|
||||||
|
let next_pipeline = s.scheduled.as_ref().and_then(|sc| sc.next_pipeline.as_ref());
|
||||||
|
let next_monitor = s.scheduled.as_ref().and_then(|sc| sc.next_monitor.as_ref());
|
||||||
|
let pipeline_next_str = next_pipeline.map(|n| format_next_run(n)).unwrap_or_default();
|
||||||
|
let monitor_next_str = next_monitor.map(|n| format_next_run(n)).unwrap_or_default();
|
||||||
|
let pipeline_last = s.scheduler.as_ref()
|
||||||
|
.and_then(|sc| sc.get("pipeline"))
|
||||||
|
.and_then(|j| j.get("last_result"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let monitor_last = s.scheduler.as_ref()
|
||||||
|
.and_then(|sc| sc.get("monitor"))
|
||||||
|
.and_then(|j| j.get("last_result"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="card">
|
||||||
|
<h3>{ "Scheduled Jobs" }</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>{ "Job" }</th><th>{ "Status" }</th><th>{ "Next Run" }</th><th>{ "Last Result" }</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{ "Auto Pipeline" }</td>
|
||||||
|
<td>{ if next_pipeline.is_some() {
|
||||||
|
html! { <span class="badge badge-pending">{ "Scheduled" }</span> }
|
||||||
|
} else {
|
||||||
|
html! { <span class="text-muted text-sm">{ "Idle" }</span> }
|
||||||
|
}}</td>
|
||||||
|
<td class="text-sm text-muted">{ pipeline_next_str }</td>
|
||||||
|
<td class="text-sm text-muted">{ pipeline_last }</td>
|
||||||
|
<td>{ if next_pipeline.is_some() {
|
||||||
|
html! { <button class="btn btn-sm btn-danger" onclick={on_skip_pipeline}>{ "Skip" }</button> }
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{ "Monitor Check" }</td>
|
<td>{ "Monitor Check" }</td>
|
||||||
<td><span class="badge badge-pending">{ "Scheduled" }</span></td>
|
<td>{ if next_monitor.is_some() {
|
||||||
<td class="text-sm text-muted">{ format!("Next run: {}", format_next_run(next)) }</td>
|
html! { <span class="badge badge-pending">{ "Scheduled" }</span> }
|
||||||
<td><button class="btn btn-sm btn-danger" onclick={on_skip}>{ "Skip" }</button></td>
|
} else {
|
||||||
|
html! { <span class="text-muted text-sm">{ "Idle" }</span> }
|
||||||
|
}}</td>
|
||||||
|
<td class="text-sm text-muted">{ monitor_next_str }</td>
|
||||||
|
<td class="text-sm text-muted">{ monitor_last }</td>
|
||||||
|
<td>{ if next_monitor.is_some() {
|
||||||
|
html! { <button class="btn btn-sm btn-danger" onclick={on_skip_monitor}>{ "Skip" }</button> }
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
});
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
rows
|
|
||||||
};
|
};
|
||||||
let has_scheduled = !scheduled_rows.is_empty();
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
@@ -394,8 +429,11 @@ pub fn dashboard() -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background Tasks (always show if there are tasks or scheduled items)
|
// Scheduled Jobs (always visible)
|
||||||
if !s.tasks.is_empty() || has_scheduled {
|
{ scheduled_jobs_html }
|
||||||
|
|
||||||
|
// Background Tasks (one-off tasks like MB import)
|
||||||
|
if !s.tasks.is_empty() {
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>{ "Background Tasks" }</h3>
|
<h3>{ "Background Tasks" }</h3>
|
||||||
<table class="tasks-table">
|
<table class="tasks-table">
|
||||||
@@ -403,7 +441,6 @@ pub fn dashboard() -> Html {
|
|||||||
<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>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ for scheduled_rows.into_iter() }
|
|
||||||
{ for s.tasks.iter().map(|t| {
|
{ for s.tasks.iter().map(|t| {
|
||||||
let progress_html = if let Some(ref p) = t.progress {
|
let progress_html = if let Some(ref p) = t.progress {
|
||||||
if p.total > 0 {
|
if p.total > 0 {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ pub struct FullAlbumInfo {
|
|||||||
pub struct FullArtistDetail {
|
pub struct FullArtistDetail {
|
||||||
pub artist: Artist,
|
pub artist: Artist,
|
||||||
pub albums: Vec<FullAlbumInfo>,
|
pub albums: Vec<FullAlbumInfo>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub featured_albums: Vec<FullAlbumInfo>,
|
||||||
pub artist_status: String,
|
pub artist_status: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub total_available_tracks: u32,
|
pub total_available_tracks: u32,
|
||||||
@@ -120,6 +122,8 @@ pub struct Track {
|
|||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub struct MbAlbumDetail {
|
pub struct MbAlbumDetail {
|
||||||
pub mbid: String,
|
pub mbid: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub artist: Option<String>,
|
||||||
pub tracks: Vec<MbAlbumTrack>,
|
pub tracks: Vec<MbAlbumTrack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ pub async fn run_refresh() -> Result<String, String> {
|
|||||||
let cookies_path = shanty_config::data_dir().join("cookies.txt");
|
let cookies_path = shanty_config::data_dir().join("cookies.txt");
|
||||||
|
|
||||||
if !profile_dir.exists() {
|
if !profile_dir.exists() {
|
||||||
return Err(format!(
|
return Err(format!("no Firefox profile at {}", profile_dir.display()));
|
||||||
"no Firefox profile at {}",
|
|
||||||
profile_dir.display()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let script = find_script()?;
|
let script = find_script()?;
|
||||||
|
|||||||
@@ -69,17 +69,30 @@ async fn get_album(
|
|||||||
let mbid = path.into_inner();
|
let mbid = path.into_inner();
|
||||||
|
|
||||||
// Try fetching as a release first
|
// Try fetching as a release first
|
||||||
let mb_tracks = match state.mb_client.get_release_tracks(&mbid).await {
|
let (mb_tracks, _release_mbid) = match state.mb_client.get_release_tracks(&mbid).await {
|
||||||
Ok(tracks) => tracks,
|
Ok(tracks) => (tracks, mbid.clone()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Probably a release-group MBID. Browse releases for this group.
|
// Probably a release-group MBID. Browse releases for this group.
|
||||||
let release_mbid = resolve_release_from_group(&state, &mbid).await?;
|
let resolved = resolve_release_from_group(&state, &mbid).await?;
|
||||||
|
let tracks = state
|
||||||
|
.mb_client
|
||||||
|
.get_release_tracks(&resolved)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(format!("MusicBrainz error: {e}")))?;
|
||||||
|
(tracks, resolved)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the album artist from the release's recording credits
|
||||||
|
let album_artist = if let Some(first_track) = mb_tracks.first() {
|
||||||
state
|
state
|
||||||
.mb_client
|
.mb_client
|
||||||
.get_release_tracks(&release_mbid)
|
.get_recording(&first_track.recording_mbid)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::Internal(format!("MusicBrainz error: {e}")))?
|
.ok()
|
||||||
}
|
.map(|r| r.artist)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get all wanted items to check local status
|
// Get all wanted items to check local status
|
||||||
@@ -112,6 +125,7 @@ async fn get_album(
|
|||||||
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
"mbid": mbid,
|
"mbid": mbid,
|
||||||
|
"artist": album_artist,
|
||||||
"tracks": tracks,
|
"tracks": tracks,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,14 +376,18 @@ pub async fn enrich_artist(
|
|||||||
.await;
|
.await;
|
||||||
tracing::debug!(mbid = %mbid, has_photo = artist_photo.is_some(), has_bio = artist_bio.is_some(), has_banner = artist_banner.is_some(), "artist enrichment data");
|
tracing::debug!(mbid = %mbid, has_photo = artist_photo.is_some(), has_bio = artist_bio.is_some(), has_banner = artist_banner.is_some(), "artist enrichment data");
|
||||||
|
|
||||||
// Fetch release groups and filter by allowed secondary types
|
// Fetch release groups and split into primary vs featured
|
||||||
let all_release_groups = state
|
let all_release_groups = state
|
||||||
.search
|
.search
|
||||||
.get_release_groups(&mbid)
|
.get_release_groups(&mbid)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||||
let allowed = state.config.read().await.allowed_secondary_types.clone();
|
let allowed = state.config.read().await.allowed_secondary_types.clone();
|
||||||
let release_groups: Vec<_> = all_release_groups
|
|
||||||
|
let (primary_rgs, featured_rgs): (Vec<_>, Vec<_>) =
|
||||||
|
all_release_groups.into_iter().partition(|rg| !rg.featured);
|
||||||
|
|
||||||
|
let release_groups: Vec<_> = primary_rgs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|rg| {
|
.filter(|rg| {
|
||||||
if rg.secondary_types.is_empty() {
|
if rg.secondary_types.is_empty() {
|
||||||
@@ -395,6 +399,31 @@ pub async fn enrich_artist(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Featured release groups — just pass through with type filtering
|
||||||
|
let featured_albums: Vec<FullAlbumInfo> = featured_rgs
|
||||||
|
.iter()
|
||||||
|
.filter(|rg| {
|
||||||
|
if rg.secondary_types.is_empty() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
rg.secondary_types.iter().all(|st| allowed.contains(st))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|rg| FullAlbumInfo {
|
||||||
|
mbid: rg.first_release_id.clone().unwrap_or_else(|| rg.id.clone()),
|
||||||
|
title: rg.title.clone(),
|
||||||
|
release_type: rg.primary_type.clone(),
|
||||||
|
date: rg.first_release_date.clone(),
|
||||||
|
track_count: 0,
|
||||||
|
local_album_id: None,
|
||||||
|
watched_tracks: 0,
|
||||||
|
owned_tracks: 0,
|
||||||
|
downloaded_tracks: 0,
|
||||||
|
total_local_tracks: 0,
|
||||||
|
status: "featured".to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Get all wanted items for this artist
|
// Get all wanted items for this artist
|
||||||
let all_wanted = queries::wanted::list(state.db.conn(), None, None).await?;
|
let all_wanted = queries::wanted::list(state.db.conn(), None, None).await?;
|
||||||
let artist_wanted: Vec<_> = all_wanted
|
let artist_wanted: Vec<_> = all_wanted
|
||||||
@@ -609,6 +638,7 @@ pub async fn enrich_artist(
|
|||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"artist": artist,
|
"artist": artist,
|
||||||
"albums": albums,
|
"albums": albums,
|
||||||
|
"featured_albums": featured_albums,
|
||||||
"artist_status": artist_status,
|
"artist_status": artist_status,
|
||||||
"total_available_tracks": total_available_tracks,
|
"total_available_tracks": total_available_tracks,
|
||||||
"total_watched_tracks": total_artist_watched,
|
"total_watched_tracks": total_artist_watched,
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ async fn get_status(
|
|||||||
let work_queue = queries::work_queue::counts_all(conn).await.ok();
|
let work_queue = queries::work_queue::counts_all(conn).await.ok();
|
||||||
|
|
||||||
// Scheduler state from DB
|
// Scheduler state from DB
|
||||||
let scheduler_jobs = queries::scheduler_state::list_all(conn).await.unwrap_or_default();
|
let scheduler_jobs = queries::scheduler_state::list_all(conn)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
let scheduler_json: serde_json::Value = scheduler_jobs
|
let scheduler_json: serde_json::Value = scheduler_jobs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|j| {
|
.map(|j| {
|
||||||
@@ -158,12 +160,7 @@ async fn trigger_organize(
|
|||||||
}
|
}
|
||||||
for track in &tracks {
|
for track in &tracks {
|
||||||
let payload = serde_json::json!({"track_id": track.id});
|
let payload = serde_json::json!({"track_id": track.id});
|
||||||
queries::work_queue::enqueue(
|
queries::work_queue::enqueue(conn, WorkTaskType::Organize, &payload.to_string(), None)
|
||||||
conn,
|
|
||||||
WorkTaskType::Organize,
|
|
||||||
&payload.to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,29 @@ use crate::state::AppState;
|
|||||||
/// Spawn the unified scheduler background loop.
|
/// Spawn the unified scheduler background loop.
|
||||||
pub fn spawn(state: web::Data<AppState>) {
|
pub fn spawn(state: web::Data<AppState>) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// Initialize scheduler state rows in DB
|
// Initialize scheduler state rows in DB with next_run_at pre-populated
|
||||||
for job_name in ["pipeline", "monitor", "cookie_refresh"] {
|
for job_name in ["pipeline", "monitor", "cookie_refresh"] {
|
||||||
if let Err(e) =
|
match queries::scheduler_state::get_or_create(state.db.conn(), job_name).await {
|
||||||
queries::scheduler_state::get_or_create(state.db.conn(), job_name).await
|
Ok(job) => {
|
||||||
{
|
if job.next_run_at.is_none() {
|
||||||
|
let (enabled, interval_secs) = read_job_config(&state, job_name).await;
|
||||||
|
if enabled {
|
||||||
|
let next =
|
||||||
|
Utc::now().naive_utc() + chrono::Duration::seconds(interval_secs);
|
||||||
|
let _ = queries::scheduler_state::update_next_run(
|
||||||
|
state.db.conn(),
|
||||||
|
job_name,
|
||||||
|
Some(next),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
tracing::error!(job = job_name, error = %e, "failed to init scheduler state");
|
tracing::error!(job = job_name, error = %e, "failed to init scheduler state");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Check each job
|
// Check each job
|
||||||
@@ -55,8 +70,7 @@ where
|
|||||||
// If config says disabled, ensure DB state reflects it
|
// If config says disabled, ensure DB state reflects it
|
||||||
if !config_enabled {
|
if !config_enabled {
|
||||||
if job.enabled {
|
if job.enabled {
|
||||||
let _ =
|
let _ = queries::scheduler_state::set_enabled(state.db.conn(), job_name, false).await;
|
||||||
queries::scheduler_state::set_enabled(state.db.conn(), job_name, false).await;
|
|
||||||
let _ =
|
let _ =
|
||||||
queries::scheduler_state::update_next_run(state.db.conn(), job_name, None).await;
|
queries::scheduler_state::update_next_run(state.db.conn(), job_name, None).await;
|
||||||
}
|
}
|
||||||
@@ -103,8 +117,7 @@ where
|
|||||||
// Update last run and schedule next
|
// Update last run and schedule next
|
||||||
let _ = queries::scheduler_state::update_last_run(state.db.conn(), job_name, &result_str).await;
|
let _ = queries::scheduler_state::update_last_run(state.db.conn(), job_name, &result_str).await;
|
||||||
let next = Utc::now().naive_utc() + chrono::Duration::seconds(interval_secs);
|
let next = Utc::now().naive_utc() + chrono::Duration::seconds(interval_secs);
|
||||||
let _ =
|
let _ = queries::scheduler_state::update_next_run(state.db.conn(), job_name, Some(next)).await;
|
||||||
queries::scheduler_state::update_next_run(state.db.conn(), job_name, Some(next)).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_job_config(state: &web::Data<AppState>, job_name: &str) -> (bool, i64) {
|
async fn read_job_config(state: &web::Data<AppState>, job_name: &str) -> (bool, i64) {
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ impl WorkerManager {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(6 * 3600)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(6 * 3600)).await;
|
||||||
let _ = queries::work_queue::cleanup_completed(cleanup_state.db.conn(), 7).await;
|
let _ =
|
||||||
|
queries::work_queue::cleanup_completed(cleanup_state.db.conn(), 7).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -316,7 +317,11 @@ async fn process_index(
|
|||||||
let cfg = state.config.read().await.clone();
|
let cfg = state.config.read().await.clone();
|
||||||
let mut downstream = Vec::new();
|
let mut downstream = Vec::new();
|
||||||
|
|
||||||
if payload.get("scan_all").and_then(|v| v.as_bool()).unwrap_or(false) {
|
if payload
|
||||||
|
.get("scan_all")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
// Full library scan
|
// Full library scan
|
||||||
let scan_config = shanty_index::ScanConfig {
|
let scan_config = shanty_index::ScanConfig {
|
||||||
root: cfg.library_path.clone(),
|
root: cfg.library_path.clone(),
|
||||||
@@ -360,8 +365,7 @@ async fn process_tag(
|
|||||||
let track_id = payload
|
let track_id = payload
|
||||||
.get("track_id")
|
.get("track_id")
|
||||||
.and_then(|v| v.as_i64())
|
.and_then(|v| v.as_i64())
|
||||||
.ok_or("missing track_id in payload")?
|
.ok_or("missing track_id in payload")? as i32;
|
||||||
as i32;
|
|
||||||
|
|
||||||
let conn = state.db.conn();
|
let conn = state.db.conn();
|
||||||
let cfg = state.config.read().await.clone();
|
let cfg = state.config.read().await.clone();
|
||||||
@@ -394,8 +398,7 @@ async fn process_organize(
|
|||||||
let track_id = payload
|
let track_id = payload
|
||||||
.get("track_id")
|
.get("track_id")
|
||||||
.and_then(|v| v.as_i64())
|
.and_then(|v| v.as_i64())
|
||||||
.ok_or("missing track_id in payload")?
|
.ok_or("missing track_id in payload")? as i32;
|
||||||
as i32;
|
|
||||||
|
|
||||||
let conn = state.db.conn();
|
let conn = state.db.conn();
|
||||||
let cfg = state.config.read().await.clone();
|
let cfg = state.config.read().await.clone();
|
||||||
|
|||||||
Reference in New Issue
Block a user