229 lines
6.9 KiB
Rust
229 lines
6.9 KiB
Rust
use actix_session::Session;
|
|
use actix_web::{HttpResponse, web};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use shanty_data::MetadataFetcher;
|
|
use shanty_db::entities::wanted_item::WantedStatus;
|
|
use shanty_db::queries;
|
|
|
|
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}/watch").route(web::delete().to(unwatch_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, _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 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
|
|
.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?;
|
|
|
|
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,
|
|
"artist": album_artist,
|
|
"tracks": tracks,
|
|
})))
|
|
}
|
|
|
|
/// Given a release-group MBID, find the first release MBID via the shared MB client.
|
|
async fn resolve_release_from_group(
|
|
state: &web::Data<AppState>,
|
|
release_group_mbid: &str,
|
|
) -> Result<String, ApiError> {
|
|
state
|
|
.mb_client
|
|
.resolve_release_from_group(release_group_mbid)
|
|
.await
|
|
.map_err(|e| {
|
|
ApiError::NotFound(format!(
|
|
"no releases found for release group {release_group_mbid}: {e}"
|
|
))
|
|
})
|
|
}
|
|
|
|
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,
|
|
})))
|
|
}
|
|
|
|
async fn unwatch_album(
|
|
state: web::Data<AppState>,
|
|
session: Session,
|
|
path: web::Path<String>,
|
|
) -> Result<HttpResponse, ApiError> {
|
|
auth::require_auth(&session)?;
|
|
let mbid = path.into_inner();
|
|
let conn = state.db.conn();
|
|
|
|
// Get the album's tracks from MB to find their recording MBIDs
|
|
let tracks = match state.mb_client.get_release_tracks(&mbid).await {
|
|
Ok(t) => t,
|
|
Err(_) => {
|
|
// Try as release-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}")))?
|
|
}
|
|
};
|
|
|
|
let mut removed = 0u64;
|
|
for track in &tracks {
|
|
removed += queries::wanted::remove_by_mbid(conn, &track.recording_mbid).await?;
|
|
}
|
|
|
|
Ok(HttpResponse::Ok().json(serde_json::json!({"removed": removed})))
|
|
}
|