216 lines
6.8 KiB
Rust
216 lines
6.8 KiB
Rust
use actix_session::Session;
|
|
use actix_web::{HttpResponse, web};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use shanty_db::entities::wanted_item::WantedStatus;
|
|
use shanty_db::queries;
|
|
use shanty_tag::provider::MetadataProvider;
|
|
|
|
use crate::auth;
|
|
use crate::error::ApiError;
|
|
use crate::state::AppState;
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct PaginationParams {
|
|
#[serde(default = "default_limit")]
|
|
limit: u64,
|
|
#[serde(default)]
|
|
offset: u64,
|
|
}
|
|
fn default_limit() -> u64 {
|
|
50
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct AddAlbumRequest {
|
|
artist: Option<String>,
|
|
album: Option<String>,
|
|
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")
|
|
.route(web::get().to(list_albums))
|
|
.route(web::post().to(add_album)),
|
|
)
|
|
.service(web::resource("/albums/{mbid}").route(web::get().to(get_album)));
|
|
}
|
|
|
|
async fn list_albums(
|
|
state: web::Data<AppState>,
|
|
session: Session,
|
|
query: web::Query<PaginationParams>,
|
|
) -> Result<HttpResponse, ApiError> {
|
|
auth::require_auth(&session)?;
|
|
let albums = queries::albums::list(state.db.conn(), query.limit, query.offset).await?;
|
|
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>,
|
|
session: Session,
|
|
path: web::Path<String>,
|
|
) -> Result<HttpResponse, ApiError> {
|
|
auth::require_auth(&session)?;
|
|
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, 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!({
|
|
"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>,
|
|
session: Session,
|
|
body: web::Json<AddAlbumRequest>,
|
|
) -> Result<HttpResponse, ApiError> {
|
|
let (user_id, _, _) = auth::require_auth(&session)?;
|
|
if body.artist.is_none() && body.album.is_none() && body.mbid.is_none() {
|
|
return Err(ApiError::BadRequest("provide artist+album or mbid".into()));
|
|
}
|
|
|
|
// 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(),
|
|
body.artist.as_deref(),
|
|
body.album.as_deref(),
|
|
mbid.as_deref(),
|
|
&state.mb_client,
|
|
Some(user_id),
|
|
)
|
|
.await;
|
|
|
|
let summary = match result {
|
|
Ok(s) => s,
|
|
Err(_) if mbid.is_some() => {
|
|
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,
|
|
Some(user_id),
|
|
)
|
|
.await?
|
|
}
|
|
Err(e) => return Err(e.into()),
|
|
};
|
|
|
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
|
"tracks_added": summary.tracks_added,
|
|
"tracks_already_owned": summary.tracks_already_owned,
|
|
"errors": summary.errors,
|
|
})))
|
|
}
|