Reworked agent UI. Artist management form.
All checks were successful
Publish Metadata Agent Image (dev) / build-and-push-image (push) Successful in 1m8s
Publish Web Player Image (dev) / build-and-push-image (push) Successful in 1m9s
Publish Metadata Agent Image / build-and-push-image (push) Successful in 1m7s
Publish Web Player Image / build-and-push-image (push) Successful in 1m10s
Publish Server Image / build-and-push-image (push) Successful in 2m23s

This commit is contained in:
2026-03-19 13:24:48 +00:00
parent 7c2c7b0ce5
commit b1eaa1b6e9
11 changed files with 741 additions and 38 deletions

View File

@@ -143,6 +143,7 @@ pub async fn update_queue_item(
track_number: body.norm_track_number,
genre: body.norm_genre,
featured_artists: body.featured_artists,
release_type: None,
confidence: Some(1.0), // manual edit = full confidence
notes: Some("Manually edited".to_owned()),
};
@@ -574,6 +575,116 @@ pub async fn search_albums_for_artist(State(state): State<S>, Query(q): Query<Al
}
}
// --- Artist full admin form ---
pub async fn get_artist_full(State(state): State<S>, Path(id): Path<i64>) -> impl IntoResponse {
let artist = match db::get_artist_by_id(&state.pool, id).await {
Ok(Some(a)) => a,
Ok(None) => return error_response(StatusCode::NOT_FOUND, "not found"),
Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
};
let (albums, appearances) = tokio::join!(
db::get_artist_albums(&state.pool, id),
db::get_artist_appearances(&state.pool, id),
);
// For each album, load tracks
let albums = match albums {
Ok(a) => a,
Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
};
let mut albums_with_tracks = Vec::new();
for album in albums {
let tracks = db::get_album_tracks_admin(&state.pool, album.id).await.unwrap_or_default();
albums_with_tracks.push(serde_json::json!({
"id": album.id, "name": album.name, "year": album.year,
"release_type": album.release_type, "hidden": album.hidden,
"track_count": album.track_count, "tracks": tracks,
}));
}
(StatusCode::OK, Json(serde_json::json!({
"artist": artist,
"albums": albums_with_tracks,
"appearances": appearances.unwrap_or_default(),
}))).into_response()
}
#[derive(Deserialize)]
pub struct SetHiddenBody { pub hidden: bool }
pub async fn set_track_hidden(State(state): State<S>, Path(id): Path<i64>, Json(b): Json<SetHiddenBody>) -> impl IntoResponse {
match db::set_track_hidden(&state.pool, id, b.hidden).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
pub async fn set_album_hidden(State(state): State<S>, Path(id): Path<i64>, Json(b): Json<SetHiddenBody>) -> impl IntoResponse {
match db::set_album_hidden(&state.pool, id, b.hidden).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
pub async fn set_artist_hidden(State(state): State<S>, Path(id): Path<i64>, Json(b): Json<SetHiddenBody>) -> impl IntoResponse {
match db::set_artist_hidden(&state.pool, id, b.hidden).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
#[derive(Deserialize)]
pub struct SetReleaseTypeBody { pub release_type: String }
pub async fn set_album_release_type(State(state): State<S>, Path(id): Path<i64>, Json(b): Json<SetReleaseTypeBody>) -> impl IntoResponse {
let valid = ["album","single","ep","compilation","live"];
if !valid.contains(&b.release_type.as_str()) {
return error_response(StatusCode::BAD_REQUEST, "invalid release_type");
}
match db::set_album_release_type(&state.pool, id, &b.release_type).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
#[derive(Deserialize)]
pub struct RenameArtistBody { pub name: String }
pub async fn rename_artist_api(State(state): State<S>, Path(id): Path<i64>, Json(b): Json<RenameArtistBody>) -> impl IntoResponse {
match db::rename_artist_name(&state.pool, id, &b.name).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
#[derive(Deserialize)]
pub struct AddAppearanceBody { pub track_id: i64 }
pub async fn add_appearance(State(state): State<S>, Path(artist_id): Path<i64>, Json(b): Json<AddAppearanceBody>) -> impl IntoResponse {
match db::add_track_appearance(&state.pool, b.track_id, artist_id).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
pub async fn remove_appearance(State(state): State<S>, Path((artist_id, track_id)): Path<(i64, i64)>) -> impl IntoResponse {
match db::remove_track_appearance(&state.pool, track_id, artist_id).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
#[derive(Deserialize)]
pub struct SearchTracksQuery { #[serde(default)] pub q: String }
pub async fn search_tracks_feat(State(state): State<S>, Query(q): Query<SearchTracksQuery>) -> impl IntoResponse {
match db::search_tracks_for_feat(&state.pool, &q.q).await {
Ok(rows) => (StatusCode::OK, Json(serde_json::to_value(
rows.iter().map(|(id, title, artist)| serde_json::json!({"id": id, "title": title, "artist_name": artist})).collect::<Vec<_>>()
).unwrap())).into_response(),
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
// --- Helpers ---
fn error_response(status: StatusCode, message: &str) -> axum::response::Response {