Added merge
All checks were successful
Publish Metadata Agent Image / build-and-push-image (push) Successful in 1m9s
Publish Web Player Image / build-and-push-image (push) Successful in 1m12s
Publish Server Image / build-and-push-image (push) Successful in 2m18s

This commit is contained in:
2026-03-19 02:36:27 +00:00
parent e1210e6e20
commit a7af27d064
4 changed files with 565 additions and 29 deletions

View File

@@ -588,6 +588,7 @@ pub struct TrackRow {
pub id: i64,
pub title: String,
pub artist_name: String,
pub album_id: Option<i64>,
pub album_name: Option<String>,
pub year: Option<i32>,
pub track_number: Option<i32>,
@@ -618,7 +619,7 @@ pub async fn search_tracks(
limit: i64, offset: i64,
) -> Result<Vec<TrackRow>, sqlx::Error> {
sqlx::query_as::<_, TrackRow>(
r#"SELECT t.id, t.title, ar.name AS artist_name, al.name AS album_name,
r#"SELECT t.id, t.title, ar.name AS artist_name, t.album_id, al.name AS album_name,
al.year, t.track_number, t.duration_secs, t.genre
FROM tracks t
JOIN track_artists ta ON ta.track_id = t.id AND ta.role = 'primary'
@@ -712,6 +713,160 @@ pub async fn count_artists_lib(pool: &PgPool, q: &str) -> Result<i64, sqlx::Erro
Ok(n)
}
// --- Track full details ---
#[derive(Debug, Serialize)]
pub struct TrackFull {
pub id: i64,
pub title: String,
pub artist_id: i64,
pub artist_name: String,
pub album_id: Option<i64>,
pub album_name: Option<String>,
pub track_number: Option<i32>,
pub duration_secs: Option<f64>,
pub genre: Option<String>,
pub file_hash: String,
pub file_size: i64,
pub storage_path: String,
pub featured_artists: Vec<String>,
}
pub async fn get_track_full(pool: &PgPool, id: i64) -> Result<Option<TrackFull>, sqlx::Error> {
#[derive(sqlx::FromRow)]
struct Row {
id: i64, title: String, artist_id: i64, artist_name: String,
album_id: Option<i64>, album_name: Option<String>,
track_number: Option<i32>, duration_secs: Option<f64>,
genre: Option<String>, file_hash: String, file_size: i64, storage_path: String,
}
let row: Option<Row> = sqlx::query_as(
r#"SELECT t.id, t.title,
ta_p.artist_id, ar.name AS artist_name,
t.album_id, al.name AS album_name,
t.track_number, t.duration_secs, t.genre,
t.file_hash, t.file_size, t.storage_path
FROM tracks t
JOIN track_artists ta_p ON ta_p.track_id = t.id AND ta_p.role = 'primary'
JOIN artists ar ON ar.id = ta_p.artist_id
LEFT JOIN albums al ON al.id = t.album_id
WHERE t.id = $1"#,
).bind(id).fetch_optional(pool).await?;
let row = match row { Some(r) => r, None => return Ok(None) };
let feat: Vec<(String,)> = sqlx::query_as(
"SELECT ar.name FROM track_artists ta JOIN artists ar ON ar.id=ta.artist_id WHERE ta.track_id=$1 AND ta.role='featured' ORDER BY ta.id"
).bind(id).fetch_all(pool).await?;
Ok(Some(TrackFull {
id: row.id, title: row.title, artist_id: row.artist_id, artist_name: row.artist_name,
album_id: row.album_id, album_name: row.album_name, track_number: row.track_number,
duration_secs: row.duration_secs, genre: row.genre, file_hash: row.file_hash,
file_size: row.file_size, storage_path: row.storage_path,
featured_artists: feat.into_iter().map(|(n,)| n).collect(),
}))
}
#[derive(Deserialize)]
pub struct TrackUpdateFields {
pub title: String,
pub artist_id: i64,
pub album_id: Option<i64>,
pub track_number: Option<i32>,
pub genre: Option<String>,
#[serde(default)]
pub featured_artists: Vec<String>,
}
pub async fn update_track_metadata(pool: &PgPool, id: i64, f: &TrackUpdateFields) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE tracks SET title=$2, album_id=$3, track_number=$4, genre=$5 WHERE id=$1")
.bind(id).bind(&f.title).bind(f.album_id).bind(f.track_number).bind(&f.genre)
.execute(pool).await?;
sqlx::query("UPDATE track_artists SET artist_id=$2 WHERE track_id=$1 AND role='primary'")
.bind(id).bind(f.artist_id).execute(pool).await?;
// Rebuild featured artists
sqlx::query("DELETE FROM track_artists WHERE track_id=$1 AND role='featured'")
.bind(id).execute(pool).await?;
for name in &f.featured_artists {
let feat_id = upsert_artist(pool, name).await?;
link_track_artist(pool, id, feat_id, "featured").await?;
}
Ok(())
}
// --- Album full details ---
#[derive(Debug, Serialize)]
pub struct AlbumDetails {
pub id: i64,
pub name: String,
pub year: Option<i32>,
pub artist_id: i64,
pub artist_name: String,
pub tracks: Vec<AlbumTrackRow>,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct AlbumTrackRow {
pub id: i64,
pub title: String,
pub track_number: Option<i32>,
pub duration_secs: Option<f64>,
pub artist_name: String,
pub genre: Option<String>,
}
pub async fn get_album_details(pool: &PgPool, id: i64) -> Result<Option<AlbumDetails>, sqlx::Error> {
let row: Option<(i64, String, Option<i32>, i64, String)> = sqlx::query_as(
"SELECT a.id, a.name, a.year, ar.id, ar.name FROM albums a JOIN artists ar ON ar.id=a.artist_id WHERE a.id=$1"
).bind(id).fetch_optional(pool).await?;
let (aid, aname, ayear, artist_id, artist_name) = match row { Some(r) => r, None => return Ok(None) };
let tracks: Vec<AlbumTrackRow> = sqlx::query_as(
r#"SELECT t.id, t.title, t.track_number, t.duration_secs, ar.name AS artist_name, t.genre
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.album_id=$1 ORDER BY t.track_number NULLS LAST, t.title"#
).bind(id).fetch_all(pool).await?;
Ok(Some(AlbumDetails { id: aid, name: aname, year: ayear, artist_id, artist_name, tracks }))
}
pub async fn update_album_full(pool: &PgPool, id: i64, name: &str, year: Option<i32>, artist_id: i64) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE albums SET name=$2, year=$3, artist_id=$4 WHERE id=$1")
.bind(id).bind(name).bind(year).bind(artist_id).execute(pool).await?;
Ok(())
}
pub async fn reorder_tracks(pool: &PgPool, orders: &[(i64, i32)]) -> Result<(), sqlx::Error> {
for &(track_id, track_number) in orders {
sqlx::query("UPDATE tracks SET track_number=$2 WHERE id=$1")
.bind(track_id).bind(track_number).execute(pool).await?;
}
Ok(())
}
pub async fn get_album_cover(pool: &PgPool, album_id: i64) -> Result<Option<(String, String)>, sqlx::Error> {
let row: Option<(String, String)> = sqlx::query_as(
"SELECT file_path, mime_type FROM album_images WHERE album_id=$1 LIMIT 1"
).bind(album_id).fetch_optional(pool).await?;
Ok(row)
}
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(
"SELECT id, name FROM albums WHERE artist_id=$1 AND ($2='' OR name ILIKE '%'||$2||'%') ORDER BY year NULLS LAST, name LIMIT 15"
).bind(aid).bind(q).fetch_all(pool).await?;
Ok(rows)
} else {
let rows: Vec<(i64, String)> = sqlx::query_as(
"SELECT id, name FROM albums WHERE $1='' OR name ILIKE '%'||$1||'%' ORDER BY name LIMIT 15"
).bind(q).fetch_all(pool).await?;
Ok(rows)
}
}
// =================== Artist Merges ===================
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]