New player
All checks were successful
Publish Server Image / build-and-push-image (push) Successful in 2m7s

This commit is contained in:
2026-03-18 02:44:59 +00:00
parent d5068aaa33
commit ff3ad15b95
11 changed files with 1970 additions and 0 deletions

229
furumi-web-player/src/db.rs Normal file
View File

@@ -0,0 +1,229 @@
use serde::Serialize;
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions;
pub async fn connect(database_url: &str) -> Result<PgPool, sqlx::Error> {
PgPoolOptions::new()
.max_connections(10)
.connect(database_url)
.await
}
// --- Models ---
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct ArtistListItem {
pub slug: String,
pub name: String,
pub album_count: i64,
pub track_count: i64,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct ArtistDetail {
pub slug: String,
pub name: String,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct AlbumListItem {
pub slug: String,
pub name: String,
pub year: Option<i32>,
pub track_count: i64,
pub has_cover: bool,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct TrackListItem {
pub slug: String,
pub title: String,
pub track_number: Option<i32>,
pub duration_secs: Option<f64>,
pub artist_name: String,
pub album_name: Option<String>,
pub album_slug: Option<String>,
pub genre: Option<String>,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct TrackDetail {
pub slug: String,
pub title: String,
pub track_number: Option<i32>,
pub duration_secs: Option<f64>,
pub genre: Option<String>,
pub storage_path: String,
pub artist_name: String,
pub artist_slug: String,
pub album_name: Option<String>,
pub album_slug: Option<String>,
pub album_year: Option<i32>,
}
#[derive(Debug, sqlx::FromRow)]
pub struct CoverInfo {
pub file_path: String,
pub mime_type: String,
}
#[derive(Debug, sqlx::FromRow)]
pub struct TrackCoverLookup {
pub storage_path: String,
pub album_id: Option<i64>,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct SearchResult {
pub result_type: String, // "artist", "album", "track"
pub slug: String,
pub name: String,
pub detail: Option<String>, // artist name for albums/tracks
}
// --- Queries ---
pub async fn list_artists(pool: &PgPool) -> Result<Vec<ArtistListItem>, sqlx::Error> {
sqlx::query_as::<_, ArtistListItem>(
r#"SELECT ar.slug, ar.name,
COUNT(DISTINCT al.id) AS album_count,
COUNT(DISTINCT t.id) AS track_count
FROM artists ar
LEFT JOIN albums al ON al.artist_id = ar.id
LEFT JOIN tracks t ON t.artist_id = ar.id
GROUP BY ar.id, ar.slug, ar.name
HAVING COUNT(DISTINCT t.id) > 0
ORDER BY ar.name"#
)
.fetch_all(pool)
.await
}
pub async fn get_artist(pool: &PgPool, slug: &str) -> Result<Option<ArtistDetail>, sqlx::Error> {
sqlx::query_as::<_, ArtistDetail>(
"SELECT slug, name FROM artists WHERE slug = $1"
)
.bind(slug)
.fetch_optional(pool)
.await
}
pub async fn list_albums_by_artist(pool: &PgPool, artist_slug: &str) -> Result<Vec<AlbumListItem>, sqlx::Error> {
sqlx::query_as::<_, AlbumListItem>(
r#"SELECT al.slug, al.name, al.year,
COUNT(t.id) AS track_count,
EXISTS(SELECT 1 FROM album_images ai WHERE ai.album_id = al.id AND ai.image_type = 'cover') AS has_cover
FROM albums al
JOIN artists ar ON al.artist_id = ar.id
LEFT JOIN tracks t ON t.album_id = al.id
WHERE ar.slug = $1
GROUP BY al.id, al.slug, al.name, al.year
ORDER BY al.year NULLS LAST, al.name"#
)
.bind(artist_slug)
.fetch_all(pool)
.await
}
pub async fn list_tracks_by_album(pool: &PgPool, album_slug: &str) -> Result<Vec<TrackListItem>, sqlx::Error> {
sqlx::query_as::<_, TrackListItem>(
r#"SELECT t.slug, t.title, t.track_number, t.duration_secs,
ar.name AS artist_name,
al.name AS album_name, al.slug AS album_slug, t.genre
FROM tracks t
JOIN artists ar ON t.artist_id = ar.id
LEFT JOIN albums al ON t.album_id = al.id
WHERE al.slug = $1
ORDER BY t.track_number NULLS LAST, t.title"#
)
.bind(album_slug)
.fetch_all(pool)
.await
}
pub async fn get_track(pool: &PgPool, slug: &str) -> Result<Option<TrackDetail>, sqlx::Error> {
sqlx::query_as::<_, TrackDetail>(
r#"SELECT t.slug, t.title, t.track_number, t.duration_secs, t.genre,
t.storage_path, ar.name AS artist_name, ar.slug AS artist_slug,
al.name AS album_name, al.slug AS album_slug, al.year AS album_year
FROM tracks t
JOIN artists ar ON t.artist_id = ar.id
LEFT JOIN albums al ON t.album_id = al.id
WHERE t.slug = $1"#
)
.bind(slug)
.fetch_optional(pool)
.await
}
pub async fn get_track_cover_lookup(pool: &PgPool, track_slug: &str) -> Result<Option<TrackCoverLookup>, sqlx::Error> {
sqlx::query_as::<_, TrackCoverLookup>(
"SELECT storage_path, album_id FROM tracks WHERE slug = $1"
)
.bind(track_slug)
.fetch_optional(pool)
.await
}
pub async fn get_album_cover_by_id(pool: &PgPool, album_id: i64) -> Result<Option<CoverInfo>, sqlx::Error> {
sqlx::query_as::<_, CoverInfo>(
r#"SELECT file_path, mime_type FROM album_images
WHERE album_id = $1 AND image_type = 'cover' LIMIT 1"#
)
.bind(album_id)
.fetch_optional(pool)
.await
}
pub async fn get_album_cover(pool: &PgPool, album_slug: &str) -> Result<Option<CoverInfo>, sqlx::Error> {
sqlx::query_as::<_, CoverInfo>(
r#"SELECT ai.file_path, ai.mime_type
FROM album_images ai
JOIN albums al ON ai.album_id = al.id
WHERE al.slug = $1 AND ai.image_type = 'cover'
LIMIT 1"#
)
.bind(album_slug)
.fetch_optional(pool)
.await
}
pub async fn search(pool: &PgPool, query: &str, limit: i32) -> Result<Vec<SearchResult>, sqlx::Error> {
let pattern = format!("%{}%", query);
sqlx::query_as::<_, SearchResult>(
r#"SELECT * FROM (
SELECT 'artist' AS result_type, slug, name, NULL AS detail
FROM artists WHERE name ILIKE $1
UNION ALL
SELECT 'album' AS result_type, al.slug, al.name, ar.name AS detail
FROM albums al JOIN artists ar ON al.artist_id = ar.id
WHERE al.name ILIKE $1
UNION ALL
SELECT 'track' AS result_type, t.slug, t.title AS name, ar.name AS detail
FROM tracks t JOIN artists ar ON t.artist_id = ar.id
WHERE t.title ILIKE $1
) sub ORDER BY result_type, name LIMIT $2"#
)
.bind(&pattern)
.bind(limit)
.fetch_all(pool)
.await
}
pub async fn list_all_tracks_by_artist(pool: &PgPool, artist_slug: &str) -> Result<Vec<TrackListItem>, sqlx::Error> {
sqlx::query_as::<_, TrackListItem>(
r#"SELECT t.slug, t.title, t.track_number, t.duration_secs,
ar.name AS artist_name,
al.name AS album_name, al.slug AS album_slug, t.genre
FROM tracks t
JOIN artists ar ON t.artist_id = ar.id
LEFT JOIN albums al ON t.album_id = al.id
WHERE ar.slug = $1
ORDER BY al.year NULLS LAST, al.name, t.track_number NULLS LAST, t.title"#
)
.bind(artist_slug)
.fetch_all(pool)
.await
}