Lots of fixes for artist detail page and track management
This commit is contained in:
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user