Lots of fixes for artist detail page and track management

This commit is contained in:
Connor Johnstone
2026-03-18 13:44:49 -04:00
parent 2314346925
commit a268ec4e56
12 changed files with 889 additions and 80 deletions

View File

@@ -1,7 +1,9 @@
use actix_web::{web, HttpResponse};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use shanty_db::entities::wanted_item::WantedStatus;
use shanty_db::queries;
use shanty_tag::provider::MetadataProvider;
use crate::error::ApiError;
use crate::state::AppState;
@@ -22,6 +24,16 @@ pub struct AddAlbumRequest {
mbid: Option<String>,
}
#[derive(Serialize)]
struct AlbumTrackInfo {
recording_mbid: String,
title: String,
track_number: Option<i32>,
disc_number: Option<i32>,
duration_ms: Option<u64>,
status: Option<String>,
}
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/albums")
@@ -29,7 +41,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.route(web::post().to(add_album)),
)
.service(
web::resource("/albums/{id}")
web::resource("/albums/{mbid}")
.route(web::get().to(get_album)),
);
}
@@ -42,19 +54,107 @@ async fn list_albums(
Ok(HttpResponse::Ok().json(albums))
}
/// Get album by MBID. Accepts either a release MBID or a release-group MBID.
/// Tries as a release first; if that fails (404), treats it as a release-group
/// and browses for its first release.
async fn get_album(
state: web::Data<AppState>,
path: web::Path<i32>,
path: web::Path<String>,
) -> Result<HttpResponse, ApiError> {
let id = path.into_inner();
let album = queries::albums::get_by_id(state.db.conn(), id).await?;
let tracks = queries::tracks::get_by_album(state.db.conn(), id).await?;
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,
Err(_) => {
// Probably a release-group MBID. Browse releases for this group.
let release_mbid = resolve_release_from_group(&state, &mbid).await?;
state.mb_client
.get_release_tracks(&release_mbid)
.await
.map_err(|e| ApiError::Internal(format!("MusicBrainz error: {e}")))?
}
};
// Get all wanted items to check local status
let all_wanted = queries::wanted::list(state.db.conn(), None).await?;
let tracks: Vec<AlbumTrackInfo> = mb_tracks
.into_iter()
.map(|t| {
let status = all_wanted
.iter()
.find(|w| w.musicbrainz_id.as_deref() == Some(&t.recording_mbid))
.map(|w| match w.status {
WantedStatus::Owned => "owned",
WantedStatus::Downloaded => "downloaded",
WantedStatus::Wanted => "wanted",
WantedStatus::Available => "available",
})
.map(String::from);
AlbumTrackInfo {
recording_mbid: t.recording_mbid,
title: t.title,
track_number: t.track_number,
disc_number: t.disc_number,
duration_ms: t.duration_ms,
status,
}
})
.collect();
Ok(HttpResponse::Ok().json(serde_json::json!({
"album": album,
"mbid": mbid,
"tracks": tracks,
})))
}
/// Given a release-group MBID, find the first release MBID via the MB API.
async fn resolve_release_from_group(
state: &web::Data<AppState>,
release_group_mbid: &str,
) -> Result<String, ApiError> {
// Use the MB client's get_json (it's private, so we go through search)
// The approach: search for releases by this release group
// MB API: /ws/2/release?release-group={mbid}&fmt=json&limit=1
// Since we can't call get_json directly, use the artist_releases approach
// to find a release that matches this group.
//
// Actually, the simplest: the MetadataProvider trait has get_artist_releases
// which returns releases, but we need releases for a release GROUP.
// Let's add a direct HTTP call here via reqwest.
let url = format!(
"https://musicbrainz.org/ws/2/release?release-group={release_group_mbid}&fmt=json&limit=1"
);
// Respect rate limiting by going through a small delay
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let client = reqwest::Client::builder()
.user_agent("Shanty/0.1.0 (shanty-music-app)")
.build()
.map_err(|e| ApiError::Internal(e.to_string()))?;
let resp: serde_json::Value = client
.get(&url)
.send()
.await
.map_err(|e| ApiError::Internal(e.to_string()))?
.json()
.await
.map_err(|e| ApiError::Internal(e.to_string()))?;
resp.get("releases")
.and_then(|r| r.as_array())
.and_then(|arr| arr.first())
.and_then(|r| r.get("id"))
.and_then(|id| id.as_str())
.map(String::from)
.ok_or_else(|| ApiError::NotFound(format!("no releases found for release group {release_group_mbid}")))
}
async fn add_album(
state: web::Data<AppState>,
body: web::Json<AddAlbumRequest>,