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
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:
@@ -25,6 +25,7 @@ pub async fn migrate(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
|
||||
pub struct Artist {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -33,6 +34,8 @@ pub struct Album {
|
||||
pub artist_id: i64,
|
||||
pub name: String,
|
||||
pub year: Option<i32>,
|
||||
pub release_type: String,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -64,6 +67,7 @@ pub struct PendingTrack {
|
||||
pub norm_track_number: Option<i32>,
|
||||
pub norm_genre: Option<String>,
|
||||
pub norm_featured_artists: Option<String>, // JSON array
|
||||
pub norm_release_type: Option<String>,
|
||||
pub confidence: Option<f64>,
|
||||
pub llm_notes: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
@@ -172,6 +176,7 @@ pub async fn update_pending_normalized(
|
||||
norm_year = $6, norm_track_number = $7, norm_genre = $8,
|
||||
norm_featured_artists = $9,
|
||||
confidence = $10, llm_notes = $11, error_message = $12,
|
||||
norm_release_type = $13,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1"#,
|
||||
)
|
||||
@@ -187,6 +192,7 @@ pub async fn update_pending_normalized(
|
||||
.bind(norm.confidence)
|
||||
.bind(&norm.notes)
|
||||
.bind(error_message)
|
||||
.bind(&norm.release_type)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -254,18 +260,19 @@ pub async fn upsert_artist(pool: &PgPool, name: &str) -> Result<i64, sqlx::Error
|
||||
Ok(row.0)
|
||||
}
|
||||
|
||||
pub async fn upsert_album(pool: &PgPool, artist_id: i64, name: &str, year: Option<i32>) -> Result<i64, sqlx::Error> {
|
||||
pub async fn upsert_album(pool: &PgPool, artist_id: i64, name: &str, year: Option<i32>, release_type: &str) -> Result<i64, sqlx::Error> {
|
||||
let slug = generate_slug();
|
||||
let row: (i64,) = sqlx::query_as(
|
||||
r#"INSERT INTO albums (artist_id, name, year, slug)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (artist_id, name) DO UPDATE SET year = COALESCE(EXCLUDED.year, albums.year)
|
||||
r#"INSERT INTO albums (artist_id, name, year, slug, release_type)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (artist_id, name) DO UPDATE SET year = COALESCE(EXCLUDED.year, albums.year), release_type = EXCLUDED.release_type
|
||||
RETURNING id"#
|
||||
)
|
||||
.bind(artist_id)
|
||||
.bind(name)
|
||||
.bind(year)
|
||||
.bind(&slug)
|
||||
.bind(release_type)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(row.0)
|
||||
@@ -343,7 +350,7 @@ pub async fn approve_and_finalize(
|
||||
let artist_id = upsert_artist(pool, artist_name).await?;
|
||||
|
||||
let album_id = match pt.norm_album.as_deref() {
|
||||
Some(album_name) => Some(upsert_album(pool, artist_id, album_name, pt.norm_year).await?),
|
||||
Some(album_name) => Some(upsert_album(pool, artist_id, album_name, pt.norm_year, pt.norm_release_type.as_deref().unwrap_or("album")).await?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -480,6 +487,7 @@ pub struct NormalizedFields {
|
||||
pub genre: Option<String>,
|
||||
#[serde(default)]
|
||||
pub featured_artists: Vec<String>,
|
||||
pub release_type: Option<String>,
|
||||
pub confidence: Option<f64>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
@@ -526,13 +534,13 @@ pub async fn delete_pending(pool: &PgPool, id: Uuid) -> Result<bool, sqlx::Error
|
||||
}
|
||||
|
||||
pub async fn list_artists_all(pool: &PgPool) -> Result<Vec<Artist>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Artist>("SELECT id, name FROM artists ORDER BY name")
|
||||
sqlx::query_as::<_, Artist>("SELECT id, name, hidden FROM artists ORDER BY name")
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_albums_by_artist(pool: &PgPool, artist_id: i64) -> Result<Vec<Album>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Album>("SELECT id, artist_id, name, year FROM albums WHERE artist_id = $1 ORDER BY year, name")
|
||||
sqlx::query_as::<_, Album>("SELECT id, artist_id, name, year, release_type, hidden FROM albums WHERE artist_id = $1 ORDER BY year, name")
|
||||
.bind(artist_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
@@ -603,6 +611,8 @@ pub struct AlbumRow {
|
||||
pub artist_name: String,
|
||||
pub year: Option<i32>,
|
||||
pub track_count: i64,
|
||||
pub release_type: String,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, sqlx::FromRow)]
|
||||
@@ -610,7 +620,12 @@ pub struct ArtistRow {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub album_count: i64,
|
||||
pub single_count: i64,
|
||||
pub ep_count: i64,
|
||||
pub compilation_count: i64,
|
||||
pub live_count: i64,
|
||||
pub track_count: i64,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
pub async fn search_tracks(
|
||||
@@ -657,13 +672,13 @@ pub async fn search_albums(
|
||||
) -> Result<Vec<AlbumRow>, sqlx::Error> {
|
||||
sqlx::query_as::<_, AlbumRow>(
|
||||
r#"SELECT a.id, a.name, ar.name AS artist_name, a.year,
|
||||
COUNT(t.id) AS track_count
|
||||
COUNT(t.id) AS track_count, a.release_type, a.hidden
|
||||
FROM albums a
|
||||
JOIN artists ar ON ar.id = a.artist_id
|
||||
LEFT JOIN tracks t ON t.album_id = a.id
|
||||
WHERE ($1 = '' OR a.name ILIKE '%' || $1 || '%')
|
||||
AND ($2 = '' OR ar.name ILIKE '%' || $2 || '%')
|
||||
GROUP BY a.id, a.name, ar.name, a.year
|
||||
GROUP BY a.id, a.name, ar.name, a.year, a.release_type, a.hidden
|
||||
ORDER BY ar.name, a.year NULLS LAST, a.name
|
||||
LIMIT $3 OFFSET $4"#,
|
||||
)
|
||||
@@ -690,13 +705,18 @@ pub async fn search_artists_lib(
|
||||
) -> Result<Vec<ArtistRow>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ArtistRow>(
|
||||
r#"SELECT ar.id, ar.name,
|
||||
COUNT(DISTINCT al.id) AS album_count,
|
||||
COUNT(DISTINCT ta.track_id) AS track_count
|
||||
COUNT(DISTINCT CASE WHEN al.release_type = 'album' THEN al.id END) AS album_count,
|
||||
COUNT(DISTINCT CASE WHEN al.release_type = 'single' THEN al.id END) AS single_count,
|
||||
COUNT(DISTINCT CASE WHEN al.release_type = 'ep' THEN al.id END) AS ep_count,
|
||||
COUNT(DISTINCT CASE WHEN al.release_type = 'compilation' THEN al.id END) AS compilation_count,
|
||||
COUNT(DISTINCT CASE WHEN al.release_type = 'live' THEN al.id END) AS live_count,
|
||||
COUNT(DISTINCT ta.track_id) AS track_count,
|
||||
ar.hidden
|
||||
FROM artists ar
|
||||
LEFT JOIN albums al ON al.artist_id = ar.id
|
||||
LEFT JOIN track_artists ta ON ta.artist_id = ar.id AND ta.role = 'primary'
|
||||
WHERE ($1 = '' OR ar.name ILIKE '%' || $1 || '%')
|
||||
GROUP BY ar.id, ar.name
|
||||
GROUP BY ar.id, ar.name, ar.hidden
|
||||
ORDER BY ar.name
|
||||
LIMIT $2 OFFSET $3"#,
|
||||
)
|
||||
@@ -853,6 +873,11 @@ pub async fn get_album_cover(pool: &PgPool, album_id: i64) -> Result<Option<(Str
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn get_artist_by_id(pool: &PgPool, id: i64) -> Result<Option<Artist>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Artist>("SELECT id, name, hidden FROM artists WHERE id=$1")
|
||||
.bind(id).fetch_optional(pool).await
|
||||
}
|
||||
|
||||
pub async fn search_albums_for_artist(pool: &PgPool, q: &str, artist_id: Option<i64>) -> Result<Vec<(i64, String)>, sqlx::Error> {
|
||||
if let Some(aid) = artist_id {
|
||||
let rows: Vec<(i64, String)> = sqlx::query_as(
|
||||
@@ -867,6 +892,120 @@ pub async fn search_albums_for_artist(pool: &PgPool, q: &str, artist_id: Option<
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_track_hidden(pool: &PgPool, id: i64, hidden: bool) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE tracks SET hidden=$2 WHERE id=$1").bind(id).bind(hidden).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_album_hidden(pool: &PgPool, id: i64, hidden: bool) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE albums SET hidden=$2 WHERE id=$1").bind(id).bind(hidden).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_artist_hidden(pool: &PgPool, id: i64, hidden: bool) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE artists SET hidden=$2 WHERE id=$1").bind(id).bind(hidden).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_album_release_type(pool: &PgPool, id: i64, release_type: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE albums SET release_type=$2 WHERE id=$1").bind(id).bind(release_type).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn rename_artist_name(pool: &PgPool, id: i64, name: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE artists SET name=$2 WHERE id=$1").bind(id).bind(name).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Full artist data for admin form
|
||||
|
||||
#[derive(Debug, Serialize, sqlx::FromRow)]
|
||||
pub struct ArtistAlbumRow {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub year: Option<i32>,
|
||||
pub release_type: String,
|
||||
pub hidden: bool,
|
||||
pub track_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, sqlx::FromRow)]
|
||||
pub struct ArtistAlbumTrack {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub track_number: Option<i32>,
|
||||
pub duration_secs: Option<f64>,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, sqlx::FromRow)]
|
||||
pub struct AppearanceRow {
|
||||
pub track_id: i64,
|
||||
pub track_title: String,
|
||||
pub primary_artist_id: i64,
|
||||
pub primary_artist_name: String,
|
||||
pub album_id: Option<i64>,
|
||||
pub album_name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_artist_albums(pool: &PgPool, artist_id: i64) -> Result<Vec<ArtistAlbumRow>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ArtistAlbumRow>(
|
||||
r#"SELECT a.id, a.name, a.year, a.release_type, a.hidden,
|
||||
COUNT(t.id) AS track_count
|
||||
FROM albums a
|
||||
LEFT JOIN tracks t ON t.album_id = a.id
|
||||
WHERE a.artist_id = $1
|
||||
GROUP BY a.id, a.name, a.year, a.release_type, a.hidden
|
||||
ORDER BY a.year NULLS LAST, a.name"#
|
||||
).bind(artist_id).fetch_all(pool).await
|
||||
}
|
||||
|
||||
pub async fn get_album_tracks_admin(pool: &PgPool, album_id: i64) -> Result<Vec<ArtistAlbumTrack>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ArtistAlbumTrack>(
|
||||
"SELECT id, title, track_number, duration_secs, hidden FROM tracks WHERE album_id=$1 ORDER BY track_number NULLS LAST, title"
|
||||
).bind(album_id).fetch_all(pool).await
|
||||
}
|
||||
|
||||
pub async fn get_artist_appearances(pool: &PgPool, artist_id: i64) -> Result<Vec<AppearanceRow>, sqlx::Error> {
|
||||
sqlx::query_as::<_, AppearanceRow>(
|
||||
r#"SELECT ta.track_id, t.title AS track_title,
|
||||
ta_p.artist_id AS primary_artist_id, ar_p.name AS primary_artist_name,
|
||||
t.album_id, al.name AS album_name
|
||||
FROM track_artists ta
|
||||
JOIN tracks t ON t.id = ta.track_id
|
||||
JOIN track_artists ta_p ON ta_p.track_id = t.id AND ta_p.role = 'primary'
|
||||
JOIN artists ar_p ON ar_p.id = ta_p.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE ta.artist_id = $1 AND ta.role = 'featured'
|
||||
ORDER BY ar_p.name, al.name NULLS LAST, t.title"#
|
||||
).bind(artist_id).fetch_all(pool).await
|
||||
}
|
||||
|
||||
pub async fn add_track_appearance(pool: &PgPool, track_id: i64, artist_id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"INSERT INTO track_artists (track_id, artist_id, role) VALUES ($1, $2, 'featured') ON CONFLICT DO NOTHING"
|
||||
).bind(track_id).bind(artist_id).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_track_appearance(pool: &PgPool, track_id: i64, artist_id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"DELETE FROM track_artists WHERE track_id=$1 AND artist_id=$2 AND role='featured'"
|
||||
).bind(track_id).bind(artist_id).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search_tracks_for_feat(pool: &PgPool, q: &str) -> Result<Vec<(i64, String, String)>, sqlx::Error> {
|
||||
// Returns (track_id, track_title, primary_artist_name)
|
||||
sqlx::query_as::<_, (i64, String, String)>(
|
||||
r#"SELECT t.id, t.title, ar.name FROM tracks t
|
||||
JOIN track_artists ta ON ta.track_id=t.id AND ta.role='primary'
|
||||
JOIN artists ar ON ar.id=ta.artist_id
|
||||
WHERE t.title ILIKE '%'||$1||'%' OR ar.name ILIKE '%'||$1||'%'
|
||||
ORDER BY ar.name, t.title LIMIT 15"#
|
||||
).bind(q).fetch_all(pool).await
|
||||
}
|
||||
|
||||
// =================== Artist Merges ===================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -951,7 +1090,7 @@ pub async fn get_pending_merges_for_processing(pool: &PgPool) -> Result<Vec<Uuid
|
||||
pub async fn get_artists_full_data(pool: &PgPool, ids: &[i64]) -> Result<Vec<ArtistFullData>, sqlx::Error> {
|
||||
let mut result = Vec::new();
|
||||
for &id in ids {
|
||||
let artist: Artist = sqlx::query_as("SELECT id, name FROM artists WHERE id = $1")
|
||||
let artist: Artist = sqlx::query_as("SELECT id, name, hidden FROM artists WHERE id = $1")
|
||||
.bind(id).fetch_one(pool).await?;
|
||||
let albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE artist_id = $1 ORDER BY year NULLS LAST, name")
|
||||
.bind(id).fetch_all(pool).await?;
|
||||
|
||||
Reference in New Issue
Block a user