use crate::route::ToAPI; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::FromRow; use crate::get_pg; use super::track::Track; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Album { /// Unique identifier for the album in the database. pub id: uuid::Uuid, /// Human-readable title of the album. pub title: String, /// Optional foreign key referencing the artist ID. If present, this indicates that this album is associated with a specific artist. pub artist: Option, } impl Album { /// Creates a new album in the database with the given `title` and optional `artist`. pub async fn create(title: &str, artist: Option) -> Self { sqlx::query_as("INSERT INTO album (title, artist) VALUES ($1, $2) RETURNING *") .bind(title) .bind(artist) .fetch_one(get_pg!()) .await .unwrap() } /// Retrieves a list of all albums in the database. pub async fn find_all() -> Vec { sqlx::query_as("SELECT * FROM album") .fetch_all(get_pg!()) .await .unwrap() } /// Finds an album in the database by its `title` and optional `artist`. pub async fn find(title: &str, artist: Option) -> Option { sqlx::query_as("SELECT * FROM album WHERE title = $1 AND artist = $2") .bind(title) .bind(artist) .fetch_optional(get_pg!()) .await .unwrap() } /// Retrieves a list of albums from the database where the specified `column` matches a regular expression. pub async fn find_regex_col(col: &str, query: &str) -> Vec { sqlx::query_as(&format!("SELECT * FROM album WHERE {col} ~* $1")) .bind(query) .fetch_all(get_pg!()) .await .unwrap() } /// Retrieves a list of albums from the database associated with the specified `artist`. pub async fn find_of_artist(artist: &uuid::Uuid) -> Vec { sqlx::query_as("SELECT * FROM album WHERE artist = $1") .bind(artist) .fetch_all(get_pg!()) .await .unwrap() } /// Deletes an album from the database. pub async fn remove(&self) { sqlx::query("DELETE FROM album WHERE id = $1") .bind(self.id) .fetch_one(get_pg!()) .await .unwrap(); } /// Retrieves an album from the database by its `id`. pub async fn get(id: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM album WHERE id = $1") .bind(id) .fetch_optional(get_pg!()) .await .unwrap() } /// Retrieves a list of tracks from the database associated with the specified `album`. pub async fn get_tracks_of_album(album: &uuid::Uuid) -> Vec { sqlx::query_as("SELECT * FROM track WHERE album = $1") .bind(album) .fetch_all(get_pg!()) .await .unwrap() } /// Retrieves the cover image of an album, or `None` if it doesn't exist. /// /// This method first retrieves a list of tracks in the album, then looks for a file named `cover.{ext}` where `{ext}` is one of `png`, `jpg`, `jpeg`, `avif`. The first existing cover file found will be returned. pub async fn get_cover(&self) -> Option { let track_path = Self::get_tracks_of_album(&self.id) .await .first()? .path .clone(); let track_path = std::path::Path::new(&track_path); for ext in ["png", "jpg", "jpeg", "avif", "webp"] { let cover_file = track_path.parent()?.join(format!("cover.{ext}")); if cover_file.exists() { return Some(cover_file.to_str().unwrap().to_string()); } } None } } impl ToAPI for Album { async fn api(&self) -> serde_json::Value { json!({ "id": &self.id, "title": &self.title, "artist": self.artist, "cover_url": if self.get_cover().await.is_some() { Some(format!("/album/{}/cover", self.id)) } else { None } }) } }