fixed up the featured artist thing

This commit is contained in:
Connor Johnstone
2026-03-24 11:38:07 -04:00
parent 36345b12ee
commit 7c30f288cd
11 changed files with 258 additions and 107 deletions

View File

@@ -11,10 +11,7 @@ pub async fn run_refresh() -> Result<String, String> {
let cookies_path = shanty_config::data_dir().join("cookies.txt");
if !profile_dir.exists() {
return Err(format!(
"no Firefox profile at {}",
profile_dir.display()
));
return Err(format!("no Firefox profile at {}", profile_dir.display()));
}
let script = find_script()?;

View File

@@ -69,19 +69,32 @@ async fn get_album(
let mbid = path.into_inner();
// Try fetching as a release first
let mb_tracks = match state.mb_client.get_release_tracks(&mbid).await {
Ok(tracks) => tracks,
let (mb_tracks, _release_mbid) = match state.mb_client.get_release_tracks(&mbid).await {
Ok(tracks) => (tracks, mbid.clone()),
Err(_) => {
// Probably a release-group MBID. Browse releases for this group.
let release_mbid = resolve_release_from_group(&state, &mbid).await?;
state
let resolved = resolve_release_from_group(&state, &mbid).await?;
let tracks = state
.mb_client
.get_release_tracks(&release_mbid)
.get_release_tracks(&resolved)
.await
.map_err(|e| ApiError::Internal(format!("MusicBrainz error: {e}")))?
.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
.mb_client
.get_recording(&first_track.recording_mbid)
.await
.ok()
.map(|r| r.artist)
} else {
None
};
// Get all wanted items to check local status
let all_wanted = queries::wanted::list(state.db.conn(), None, None).await?;
@@ -112,6 +125,7 @@ async fn get_album(
Ok(HttpResponse::Ok().json(serde_json::json!({
"mbid": mbid,
"artist": album_artist,
"tracks": tracks,
})))
}

View File

@@ -376,14 +376,18 @@ pub async fn enrich_artist(
.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");
// Fetch release groups and filter by allowed secondary types
// Fetch release groups and split into primary vs featured
let all_release_groups = state
.search
.get_release_groups(&mbid)
.await
.map_err(|e| ApiError::Internal(e.to_string()))?;
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()
.filter(|rg| {
if rg.secondary_types.is_empty() {
@@ -395,6 +399,31 @@ pub async fn enrich_artist(
})
.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
let all_wanted = queries::wanted::list(state.db.conn(), None, None).await?;
let artist_wanted: Vec<_> = all_wanted
@@ -609,6 +638,7 @@ pub async fn enrich_artist(
Ok(serde_json::json!({
"artist": artist,
"albums": albums,
"featured_albums": featured_albums,
"artist_status": artist_status,
"total_available_tracks": total_available_tracks,
"total_watched_tracks": total_artist_watched,

View File

@@ -58,7 +58,9 @@ async fn get_status(
let work_queue = queries::work_queue::counts_all(conn).await.ok();
// 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
.iter()
.map(|j| {
@@ -158,13 +160,8 @@ async fn trigger_organize(
}
for track in &tracks {
let payload = serde_json::json!({"track_id": track.id});
queries::work_queue::enqueue(
conn,
WorkTaskType::Organize,
&payload.to_string(),
None,
)
.await?;
queries::work_queue::enqueue(conn, WorkTaskType::Organize, &payload.to_string(), None)
.await?;
count += 1;
}
offset += 500;

View File

@@ -15,12 +15,27 @@ use crate::state::AppState;
/// Spawn the unified scheduler background loop.
pub fn spawn(state: web::Data<AppState>) {
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"] {
if let Err(e) =
queries::scheduler_state::get_or_create(state.db.conn(), job_name).await
{
tracing::error!(job = job_name, error = %e, "failed to init scheduler state");
match 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");
}
}
}
@@ -55,8 +70,7 @@ where
// If config says disabled, ensure DB state reflects it
if !config_enabled {
if job.enabled {
let _ =
queries::scheduler_state::set_enabled(state.db.conn(), job_name, false).await;
let _ = queries::scheduler_state::set_enabled(state.db.conn(), job_name, false).await;
let _ =
queries::scheduler_state::update_next_run(state.db.conn(), job_name, None).await;
}
@@ -103,8 +117,7 @@ where
// Update last run and schedule next
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 _ =
queries::scheduler_state::update_next_run(state.db.conn(), job_name, Some(next)).await;
let _ = 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) {

View File

@@ -64,7 +64,8 @@ impl WorkerManager {
tokio::spawn(async move {
loop {
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 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
let scan_config = shanty_index::ScanConfig {
root: cfg.library_path.clone(),
@@ -360,8 +365,7 @@ async fn process_tag(
let track_id = payload
.get("track_id")
.and_then(|v| v.as_i64())
.ok_or("missing track_id in payload")?
as i32;
.ok_or("missing track_id in payload")? as i32;
let conn = state.db.conn();
let cfg = state.config.read().await.clone();
@@ -394,8 +398,7 @@ async fn process_organize(
let track_id = payload
.get("track_id")
.and_then(|v| v.as_i64())
.ok_or("missing track_id in payload")?
as i32;
.ok_or("missing track_id in payload")? as i32;
let conn = state.db.conn();
let cfg = state.config.read().await.clone();