From e5a2dfaadee9de08c2be28c12b9b3fff65bcdb59 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 4 Oct 2024 20:17:09 +0200 Subject: [PATCH 01/10] fix --- src/library/playlist.rs | 6 ++++-- src/route/playlist.rs | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/library/playlist.rs b/src/library/playlist.rs index f50124b..f34cbca 100644 --- a/src/library/playlist.rs +++ b/src/library/playlist.rs @@ -5,7 +5,9 @@ use crate::{get_pg, library::user::User}; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Playlist { - pub id: String, + pub id: uuid::Uuid, + #[sqlx(default)] + pub id_str: Option, pub owner: String, pub title: String, pub visibility: Visibility, @@ -62,7 +64,7 @@ impl Playlist { impl Playlist { pub async fn api(&self) -> serde_json::Value { serde_json::json!({ - "id": self.id, + "id": if let Some(id) = &self.id_str { id.clone() } else { self.id.to_string() }, "owner": self.owner, "visibility": serde_json::to_value(&self.visibility).unwrap(), "title": self.title, diff --git a/src/route/playlist.rs b/src/route/playlist.rs index 17aeb45..ad4d7e3 100644 --- a/src/route/playlist.rs +++ b/src/route/playlist.rs @@ -42,7 +42,8 @@ pub async fn recently_added_playlist() -> FallibleApiResponse { pub async fn playlist_route(id: &str, u: User) -> FallibleApiResponse { if id == "recents" { return Ok(Playlist { - id: "recents".to_string(), + id: uuid::Uuid::nil(), + id_str: Some("recents".to_string()), owner: u.username, title: "Recently Played".to_string(), visibility: Visibility::Public, @@ -54,7 +55,8 @@ pub async fn playlist_route(id: &str, u: User) -> FallibleApiResponse { if id == "recentlyAdded" { return Ok(Playlist { - id: "recentlyAdded".to_string(), + id: uuid::Uuid::nil(), + id_str: Some("recentlyAdded".to_string()), owner: u.username, title: "Recently Added".to_string(), visibility: Visibility::Public, @@ -170,7 +172,7 @@ pub async fn playlist_edit_route( .bind(new_title) .bind(new_vis) .bind(tracks_ref) - .bind(&to_uuid(&playlist_id)?) + .bind(&playlist_id) .fetch_one(get_pg!()) .await .unwrap(); From 268ab4cc763c9d4f746e8ef63fabe489f5d47366 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 4 Oct 2024 20:24:04 +0200 Subject: [PATCH 02/10] fix --- src/route/playlist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/route/playlist.rs b/src/route/playlist.rs index ad4d7e3..65d550c 100644 --- a/src/route/playlist.rs +++ b/src/route/playlist.rs @@ -102,7 +102,7 @@ pub async fn playlist_tracks_route(id: &str, u: User) -> FallibleApiResponse { tracks.push(track); } - let tracks: Vec = sqlx::query_as("SELECT * FROM track WHERE id IN ANY($1)") + let tracks: Vec = sqlx::query_as("SELECT * FROM track WHERE id = ANY($1)") .bind(tracks) .fetch_all(get_pg!()) .await From 334790da70b4712c77a4f1b5217fed209d8b165c Mon Sep 17 00:00:00 2001 From: JMARyA Date: Sun, 6 Oct 2024 01:12:26 +0200 Subject: [PATCH 03/10] docs --- src/library/album.rs | 16 ++++- src/library/artist.rs | 11 +++- src/library/event.rs | 23 ++++++- src/library/metadata.rs | 9 +++ src/library/mod.rs | 79 ++++++++++++++++++++--- src/library/playlist.rs | 68 ++++++++++++++++++-- src/library/search.rs | 37 +++++++---- src/library/track.rs | 134 ++++++++++++++++++++++++++++++++++++++-- src/library/user.rs | 26 +++++++- 9 files changed, 366 insertions(+), 37 deletions(-) diff --git a/src/library/album.rs b/src/library/album.rs index 97fbfd1..67d7629 100644 --- a/src/library/album.rs +++ b/src/library/album.rs @@ -9,12 +9,15 @@ 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) @@ -24,6 +27,7 @@ impl Album { .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!()) @@ -31,6 +35,7 @@ impl Album { .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) @@ -40,6 +45,7 @@ impl Album { .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) @@ -48,6 +54,7 @@ impl Album { .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) @@ -56,6 +63,7 @@ impl Album { .unwrap() } + /// Deletes an album from the database. pub async fn remove(&self) { sqlx::query("DELETE FROM album WHERE id = $1") .bind(self.id) @@ -64,6 +72,7 @@ impl Album { .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) @@ -72,6 +81,7 @@ impl Album { .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) @@ -80,9 +90,9 @@ impl Album { .unwrap() } - /// Returns the cover image of an album, or `None` if it doesn't exist. + /// Retrieves the cover image of an album, or `None` if it doesn't exist. /// - /// This method first retrieves the 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. + /// 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 diff --git a/src/library/artist.rs b/src/library/artist.rs index d8b3d72..3024466 100644 --- a/src/library/artist.rs +++ b/src/library/artist.rs @@ -9,11 +9,13 @@ use super::track::Track; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Artist { + /// Unique identifier for the artist pub id: uuid::Uuid, + /// Name of the artist pub name: String, } - impl Artist { + /// Creates a new artist with the given name. pub async fn create(name: &str) -> Self { sqlx::query_as("INSERT INTO artist (name) VALUES ($1) RETURNING id, name") .bind(name) @@ -22,6 +24,7 @@ impl Artist { .unwrap() } + /// Retrieves an artist by their unique identifier. pub async fn get(id: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM artist WHERE id = $1") .bind(id) @@ -30,6 +33,7 @@ impl Artist { .unwrap() } + /// Retrieves an artist by their name. pub async fn find(name: &str) -> Option { sqlx::query_as("SELECT * FROM artist WHERE name = $1") .bind(name) @@ -38,6 +42,7 @@ impl Artist { .unwrap() } + /// Retrieves all artists in the database. pub async fn find_all() -> Vec { sqlx::query_as("SELECT * FROM artist") .fetch_all(get_pg!()) @@ -45,6 +50,7 @@ impl Artist { .unwrap() } + /// Searches for artists with columns matching a given regular expression. pub async fn find_regex_col(col: &str, query: &str) -> Vec { sqlx::query_as(&format!("SELECT * FROM artist WHERE {col} ~* $1")) .bind(query) @@ -53,6 +59,7 @@ impl Artist { .unwrap() } + /// Deletes an artist. pub async fn remove(&self) { sqlx::query("DELETE FROM artist WHERE id = $1") .bind(self.id) @@ -61,7 +68,7 @@ impl Artist { .unwrap(); } - /// Gets the image of an artist or `None` if it can't be found. + /// Retrieves the image of an artist, or `None` if it cannot be found. /// /// This function gets a track from the artist. It then expects the folder structure to be `Artist/Album/Track.ext` and searches for an image file named `artist` in the artist folder. pub async fn get_image_of(id: &uuid::Uuid) -> Option { diff --git a/src/library/event.rs b/src/library/event.rs index f5e0b77..e7fc37d 100644 --- a/src/library/event.rs +++ b/src/library/event.rs @@ -2,25 +2,43 @@ use serde::{Deserialize, Serialize}; use sqlx::prelude::FromRow; use crate::{get_pg, library::user::User}; - +/// Represents a user event in the database. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Event { + /// Unique identifier for this event. pub id: uuid::Uuid, + + /// Timestamp of when this event occurred. pub time: chrono::DateTime, + + /// Type of event (e.g. play, stop). pub kind: EventKind, + + /// Username of the user who performed this action. pub user: String, + + /// ID of the track associated with this event. pub track: uuid::Uuid, } +/// Enum representing different types of events that can occur. #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] #[sqlx(type_name = "event_kind", rename_all = "lowercase")] pub enum EventKind { + /// A user started playing a track. Play, + + /// A user finished playing a track. Played, + + /// A user stopped playing a track. Stop, } impl Event { + /// Creates a new event in the database with the given kind, user, and track. + /// + /// Returns the newly created event object. pub async fn create(kind: EventKind, user: &User, track: uuid::Uuid) -> Self { sqlx::query_as("INSERT INTO events (kind, \"user\", track) VALUES ($1, $2, $3) RETURNING *") .bind(kind) @@ -31,6 +49,9 @@ impl Event { .unwrap() } + /// Retrieves the latest 300 events performed by a given user. + /// + /// Returns a vector of event objects. pub async fn get_latest_events_of(u: &User) -> Vec { sqlx::query_as("SELECT * FROM events WHERE \"user\" = $1 ORDER BY time DESC LIMIT 300") .bind(&u.username) diff --git a/src/library/metadata.rs b/src/library/metadata.rs index 32746ff..7ec6893 100644 --- a/src/library/metadata.rs +++ b/src/library/metadata.rs @@ -6,31 +6,40 @@ use serde::{Deserialize, Serialize}; pub struct AudioMetadata(pub serde_json::Value); impl AudioMetadata { + /// Get a key from the JSON data if it exists. fn get_key(&self, key: &str) -> Option<&str> { self.0.as_object()?.get(key)?.as_str() } + /// Get the title of the audio metadata if it exists. pub fn title(&self) -> Option<&str> { self.get_key("title") } + /// Get the artist of the audio metadata if it exists. pub fn artist(&self) -> Option<&str> { self.get_key("artist") } + /// Get the album of the audio metadata if it exists. pub fn album(&self) -> Option<&str> { self.get_key("album") } + /// Get the track number of the audio metadata if it exists. pub fn track_number(&self) -> Option { + // Note: This will return an error if the track number is not a valid integer self.get_key("tracknumber").map(|x| x.parse().ok())? } } +/// Get the audio metadata for a given file path, or None if it fails. pub fn get_metadata(file_path: &str) -> Option { + // Simply wrap the JSON data in an `AudioMetadata` instance Some(AudioMetadata(get_metadata_json(file_path)?)) } +/// Get the JSON data for a given file path using the `extract_metadata.py` script, or None if it fails. pub fn get_metadata_json(file_path: &str) -> Option { let output = std::process::Command::new("python3") .arg("./extract_metadata.py") diff --git a/src/library/mod.rs b/src/library/mod.rs index 5d8907e..8969257 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -34,41 +34,59 @@ fn is_music_file(path: &Path) -> bool { pub struct Libary { root_dir: PathBuf, } - impl Libary { + /// Creates a new instance of the Libary struct with the specified `root_dir`. pub const fn new(root_dir: PathBuf) -> Self { Self { root_dir } } + /// Finds or creates an artist by their name and returns its ID. + /// + /// If the artist already exists, this method returns their ID. Otherwise, it creates a new artist record and returns its ID. pub async fn find_or_create_artist(&self, artist: &str) -> uuid::Uuid { if let Some(artist) = Artist::find(artist).await { + // Return the existing artist's ID. artist.id } else { + // Create a new artist record with the given name and return its ID. Artist::create(artist).await.id } } + /// Finds or creates an album by its title and returns its ID. + /// + /// This method takes an optional `artist_id` parameter, which specifies the + /// artist to associate with the new album. If no `artist_id` is provided, + /// the album will be created without an associated artist. pub async fn find_or_create_album( &self, album: &str, artist_id: Option, ) -> uuid::Uuid { if let Some(album) = Album::find(album, artist_id).await { + // Return the existing album's ID. album.id } else { + // Create a new album record with the given title and return its ID. Album::create(album, artist_id).await.id } } + /// Adds a music file at the specified `path` to the library. + /// + /// This method searches for the path in the existing library records. If it's + /// already present, no action is taken. Otherwise, it extracts metadata from + /// the file and creates a new track record with associated artist and album IDs, + /// if available. pub async fn add_path_to_library(&self, path: &str) { - // search for path already present + // Check if the path is already present in the library records. if Track::of_path(path).await.is_some() { return; } log::info!("Adding {path} to library"); - // add track to library + // Extract metadata from the file at the specified path. let metadata = metadata::get_metadata(path); let mut entry = json!({ @@ -76,6 +94,7 @@ impl Libary { }); if let Some(meta) = &metadata { + // If artist information is available, create or retrieve its ID and associate it with this track record. if let Some(artist) = meta.artist() { let artist_id = self.find_or_create_artist(artist).await; entry @@ -86,6 +105,7 @@ impl Libary { log::warn!("{path} has no artist"); } + // If album information is available, create or retrieve its ID and associate it with this track record. if let Some(album) = meta.album() { let album_id = self .find_or_create_album( @@ -106,6 +126,7 @@ impl Libary { log::warn!("{path} has no album and will be treated as single"); } + // Extract the title of this track from the metadata. if let Some(title) = meta.title() { entry .as_object_mut() @@ -114,6 +135,7 @@ impl Libary { } } + // Add any available metadata to the track record. if let Some(metadata) = metadata { entry .as_object_mut() @@ -121,7 +143,7 @@ impl Libary { .insert("meta".into(), metadata.0); } - // if no title in metadata use file name + // If no title is specified in the metadata, use the file name as the title. if entry.as_object().unwrap().get("title").is_none() { entry.as_object_mut().unwrap().insert( "title".into(), @@ -134,9 +156,11 @@ impl Libary { ); } + // Create a new track record with the extracted metadata and add it to the library. Track::create(entry.as_object().unwrap()).await; } + /// Retrieves all artists from the database. pub async fn get_artists(&self) -> Vec { sqlx::query_as("SELECT * FROM artist") .fetch_all(get_pg!()) @@ -144,6 +168,7 @@ impl Libary { .unwrap() } + /// Retrieves all albums for a given artist ID. pub async fn get_albums_by_artist(&self, artist: &uuid::Uuid) -> Vec { sqlx::query_as("SELECT * FROM album WHERE artist = $1") .bind(artist) @@ -152,6 +177,7 @@ impl Libary { .unwrap() } + /// Retrieves all singles (tracks with no album) for a given artist ID. pub async fn get_singles_by_artist(&self, artist: &uuid::Uuid) -> Vec { sqlx::query_as("SELECT * FROM track WHERE album IS NULL AND artist = $1") .bind(artist) @@ -160,26 +186,43 @@ impl Libary { .unwrap() } + /// Retrieves an album record by its ID, returning None if not found. pub async fn get_album_by_id(&self, album: &uuid::Uuid) -> Option { Album::get(album).await } + /// Retrieves a track record by its ID, returning None if not found. pub async fn get_track_by_id(&self, track_id: &uuid::Uuid) -> Option { Track::get(track_id).await } - + /// Reloads metadata for a given track ID. + /// + /// This method retrieves the current metadata associated with the track, + /// updates it if necessary (e.g., artist or album is missing), and then + /// persists the updated metadata to the database. pub async fn reload_metadata(&self, track_id: &uuid::Uuid) -> Result<(), ()> { + // Retrieve the track record from the database. let track = Track::get(track_id).await.ok_or_else(|| ())?; + + // Get the path associated with the track (e.g., file system location). let path = &track.path; + log::info!("Rescanning metadata for {path}"); + // Retrieve the current metadata for the track. let metadata = metadata::get_metadata(path); + // Create an update object to store changes to the metadata. let mut update = json!({}); + // If metadata is available, extract artist and album information. if let Some(meta) = &metadata { + // Get the artist associated with the track (if present). if let Some(artist) = meta.artist() { + // Create or retrieve an artist record in the database. let artist_id = self.find_or_create_artist(artist).await; + + // Add the artist ID to the update object. update .as_object_mut() .unwrap() @@ -188,7 +231,10 @@ impl Libary { log::warn!("{path} has no artist"); } + // Get the album associated with the track (if present). if let Some(album) = meta.album() { + // Create or retrieve an album record in the database, using + // the update object to pass additional metadata information. let album_id = self .find_or_create_album( album, @@ -199,6 +245,8 @@ impl Libary { .map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap()), ) .await; + + // Add the album ID to the update object. update .as_object_mut() .unwrap() @@ -207,6 +255,7 @@ impl Libary { log::warn!("{path} has no album and will be treated as single"); } + // Get the track title (if present). if let Some(title) = meta.title() { update .as_object_mut() @@ -215,6 +264,7 @@ impl Libary { } } + // Add any remaining metadata to the update object. if let Some(metadata) = metadata { update .as_object_mut() @@ -222,7 +272,7 @@ impl Libary { .insert("meta".into(), metadata.0); } - // if no title in metadata use file name + // If no track title is available, use the file name as a fallback. if update.as_object().unwrap().get("title").is_none() { update.as_object_mut().unwrap().insert( "title".into(), @@ -235,27 +285,34 @@ impl Libary { ); } + // Apply the updated metadata to the track record. track.update(&update).await.unwrap(); Ok(()) } + /// Cleans up lost files from the database. + /// + /// This method iterates over all tracks, albums, and artists in the + /// database and removes any records that no longer exist on disk. pub async fn clean_lost_files(&self) { - // tracks + // Clean up lost track records. for track in Track::find_all().await { if !std::path::Path::new(&track.path).exists() { log::info!("Cleaning lost {}", track.path); track.remove().await; } } - // albums + + // Clean up lost album records. for album in Album::find_all().await { if Track::find_of_album(&album.id).await.is_empty() { log::info!("Cleaning album {} with no tracks", album.title); album.remove().await; } } - // artists + + // Clean up lost artist records. for artist in Artist::find_all().await { if Track::find_first_of_artist(&artist.id).await.is_none() && Album::find_of_artist(&artist.id).await.is_empty() @@ -266,18 +323,22 @@ impl Libary { } } + /// Rescan the entire library. pub async fn rescan(&self, cache: &RouteCache) { cache.invalidate("albums", "latest").await; log::info!("Rescanning library"); + for entry in WalkDir::new(self.root_dir.clone()) .follow_links(true) .into_iter() .filter_map(std::result::Result::ok) { let path = entry.path(); + if path.is_file() && is_music_file(path) { let path = path.to_string_lossy().to_string(); + self.add_path_to_library(&path).await; } } diff --git a/src/library/playlist.rs b/src/library/playlist.rs index f34cbca..468c26c 100644 --- a/src/library/playlist.rs +++ b/src/library/playlist.rs @@ -5,12 +5,23 @@ use crate::{get_pg, library::user::User}; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Playlist { + // Unique identifier of the playlist pub id: uuid::Uuid, + + // Optional string representation of the playlist's ID #[sqlx(default)] pub id_str: Option, + + // Owner of the playlist pub owner: String, + + // Human-readable title of the playlist pub title: String, + + // Level of visibility for this playlist (public, private, etc.) pub visibility: Visibility, + + // IDs of tracks that belong to this playlist pub tracks: Vec, } @@ -20,8 +31,15 @@ pub enum Visibility { Private, Public, } - impl Playlist { + /// Creates a new playlist with the given owner, title, visibility, and tracks. + /// + /// # Arguments + /// + /// * `owner`: The user who owns the playlist. + /// * `title`: The human-readable title of the playlist. + /// * `visibility`: The level of visibility for this playlist (public, private, etc.). + /// * `tracks`: A list of track IDs that belong to this playlist. pub async fn create( owner: User, title: &str, @@ -29,33 +47,71 @@ impl Playlist { tracks: &[uuid::Uuid], ) -> Option { sqlx::query_as("INSERT INTO playlist (owner, title, visibility, tracks) VALUES ($1, $2, $3, $4) RETURNING *") - .bind(owner.username) - .bind(title) - .bind(visibility) - .bind(tracks) - .fetch_one(get_pg!()).await.ok() + .bind(owner.username) + .bind(title) + .bind(visibility) + .bind(tracks) + .fetch_one(get_pg!()) + .await.ok() } + /// Finds playlists where a given column matches a regular expression. + /// + /// # Arguments + /// + /// * `col`: The name of the column to search in (e.g., "title", "owner"). + /// * `query`: A string containing the regular expression pattern. + /// + /// # Returns + /// + /// A list of `Playlist` instances that match the query. pub async fn find_regex_col(col: &str, query: &str) -> Vec { sqlx::query_as(&format!("SELECT * FROM playlist WHERE {col} ~* $1")) + // Bind the regular expression pattern as a string .bind(query) + // Execute the query using the PostgreSQL connection pool and fetch all results .fetch_all(get_pg!()) .await + // Return the result as a list of `Playlist` instances (unwrap() is used here, but consider adding error handling) .unwrap() } + /// Retrieves a playlist by its unique ID. + /// + /// # Arguments + /// + /// * `id`: The UUID of the playlist to retrieve. + /// + /// # Returns + /// + /// An optional `Playlist` instance if retrieval was successful; otherwise, `None`. pub async fn get(id: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM playlist WHERE id = $1") + // Bind the UUID of the playlist to retrieve .bind(id) + // Execute the query using the PostgreSQL connection pool and fetch an optional result .fetch_optional(get_pg!()) + // Return the result as an optional `Playlist` instance (unwrap() is used here, but consider adding error handling) .await .unwrap() } + /// Retrieves all playlists owned by a given user. + /// + /// # Arguments + /// + /// * `u`: The user who owns the playlists to retrieve. + /// + /// # Returns + /// + /// A list of `Playlist` instances that belong to the given user. pub async fn of(u: &User) -> Vec { sqlx::query_as("SELECT * FROM playlist WHERE owner = $1") + // Bind the username of the user who owns the playlists .bind(&u.username) + // Execute the query using the PostgreSQL connection pool and fetch all results .fetch_all(get_pg!()) + // Return the result as a list of `Playlist` instances (unwrap() is used here, but consider adding error handling) .await .unwrap() } diff --git a/src/library/search.rs b/src/library/search.rs index 75cb67f..36a0b2b 100644 --- a/src/library/search.rs +++ b/src/library/search.rs @@ -5,33 +5,39 @@ use crate::route::ToAPI; use super::{album::Album, artist::Artist, playlist::Playlist, track::Track}; +// Calculate a score for a given field based on its similarity to the search term and recency factor fn calculate_score(field: &str, search_term: &str, date_added: Option) -> f64 { - // Exact match bonus + // Exact match bonus: assign a high score if the field exactly matches the search term let exact_match_bonus = if field.eq_ignore_ascii_case(search_term) { 10.0 } else { 0.0 }; - // String similarity score + // String similarity score: calculate how similar the field is to the search term using string matching let similarity_score = string_similarity(field, search_term); - // Recency factor + // Recency factor: assign a boost to recent items based on their age in days let recency_score = if let Some(date) = date_added { recency_factor(date) } else { - 0.0 + 0.0 // Assign a low score for no date added }; - // Calculate total score + // Calculate the total score by summing up the exact match bonus, string similarity score, and recency factor exact_match_bonus + similarity_score + recency_score } +// Calculate the similarity between two strings using character matching and length penalty fn string_similarity(field: &str, search_term: &str) -> f64 { + // Initialize a counter for matching characters let mut match_count = 0; + + // Convert both strings to lowercase for case-insensitive comparison let field_lower = field.to_lowercase(); let search_lower = search_term.to_lowercase(); + // Iterate over the characters in the search term and count matches with the field for (i, c) in search_lower.chars().enumerate() { if let Some(field_char) = field_lower.chars().nth(i) { if field_char == c { @@ -40,17 +46,26 @@ fn string_similarity(field: &str, search_term: &str) -> f64 { } } + // Calculate a penalty based on the length difference between the two strings let length_diff_penalty = (field.len() as f64 - search_term.len() as f64).abs() * 0.1; + + // Calculate the base similarity score based on the number of matching characters and string lengths let base_similarity = (match_count as f64 / search_term.len() as f64) * 8.0; + // Return the similarity score, capping it at 0 to prevent negative scores (base_similarity - length_diff_penalty).max(0.0) } +// Calculate a recency factor based on how old an item is in days fn recency_factor(date_added: i64) -> f64 { + // Get the current timestamp let current_time = chrono::Utc::now().timestamp(); + + // Calculate the age of the item in seconds and convert it to days let age_in_seconds = current_time - date_added; let age_in_days = age_in_seconds as f64 / 86400.0; // Convert to days + // Assign a boost based on how old the item is, with recent items getting more points if age_in_days < 30.0 { 5.0 // Boost recent items } else if age_in_days < 365.0 { @@ -67,10 +82,11 @@ pub struct SearchResult { score: f64, } +// Perform a search across artists, albums, tracks, and playlists based on the query term pub async fn search_for(query: String) -> Option> { let mut results: Vec = Vec::new(); - // Add artist results + // Search for artists matching the query term for artist in Artist::find_regex_col("name", &query).await { results.push(SearchResult { kind: "artist".to_string(), @@ -79,7 +95,7 @@ pub async fn search_for(query: String) -> Option> { }); } - // Add album results + // Search for albums matching the query term for album in Album::find_regex_col("title", &query).await { results.push(SearchResult { kind: "album".to_string(), @@ -88,7 +104,7 @@ pub async fn search_for(query: String) -> Option> { }); } - // Add track results + // Search for tracks matching the query term for track in Track::find_regex_col("title", &query).await { results.push(SearchResult { kind: "track".to_string(), @@ -97,7 +113,7 @@ pub async fn search_for(query: String) -> Option> { }); } - // Add playlist results + // Search for playlists matching the query term for playlist in Playlist::find_regex_col("title", &query).await { results.push(SearchResult { kind: "playlist".to_string(), @@ -106,9 +122,8 @@ pub async fn search_for(query: String) -> Option> { }); } - // Sort results by score (descending) + // Sort the search results by their score in descending order results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(Ordering::Equal)); - // Return results Some(results) } diff --git a/src/library/track.rs b/src/library/track.rs index 458c334..867ac85 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -13,16 +13,34 @@ use super::{event::Event, metadata::AudioMetadata, user::User}; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Track { + /// The unique identifier of the track pub id: uuid::Uuid, + + // The file path where the track is stored pub path: String, + + /// The title of the track pub title: String, + + /// The date when the track was added to the library pub date_added: chrono::DateTime, + + /// Optional associated album pub album: Option, + + /// Optional associated artist pub artist: Option, + + /// Optional metadata for the track pub meta: Option, } impl Track { + /// Create a new track. + /// + /// # Arguments + /// + /// * `data` - The data to create the track with. pub async fn create(data: &serde_json::Map) { sqlx::query( "INSERT INTO track (path, title, meta, album, artist) VALUES ($1, $2, $3, $4, $5)", @@ -43,6 +61,15 @@ impl Track { .unwrap(); } + /// Find a track by path. + /// + /// # Arguments + /// + /// * `path` - The path of the track to find. + /// + /// # Returns + /// + /// The found track, or None if no track is found. pub async fn of_path(path: &str) -> Option { sqlx::query_as("SELECT * FROM track WHERE path = $1") .bind(path) @@ -51,6 +78,11 @@ impl Track { .unwrap() } + /// Find all recently added tracks. + /// + /// # Returns + /// + /// A vector of recently added tracks. pub async fn find_recently_added() -> Vec { sqlx::query_as("SELECT * FROM track ORDER BY date_added DESC LIMIT 90") .fetch_all(get_pg!()) @@ -58,6 +90,15 @@ impl Track { .unwrap() } + /// Find a track by ID. + /// + /// # Arguments + /// + /// * `id` - The ID of the track to find. + /// + /// # Returns + /// + /// The found track, or None if no track is found. pub async fn get(id: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM track WHERE id = $1") .bind(id) @@ -66,6 +107,15 @@ impl Track { .unwrap() } + /// Update a track. + /// + /// # Arguments + /// + /// * `update_set` - The update set to apply to the track. + /// + /// # Returns + /// + /// Some(()) if the update was successful, or None if not. pub async fn update(&self, update_set: &serde_json::Value) -> Option<()> { let map = update_set.as_object()?; let artist = map @@ -93,6 +143,11 @@ impl Track { Some(()) } + /// Find all tracks. + /// + /// # Returns + /// + /// A vector of all tracks. pub async fn find_all() -> Vec { sqlx::query_as("SELECT * FROM track") .fetch_all(get_pg!()) @@ -100,6 +155,11 @@ impl Track { .unwrap() } + /// Remove a track. + /// + /// # Arguments + /// + /// * `id` - The ID of the track to remove. pub async fn remove(&self) { sqlx::query("DELETE FROM track WHERE id = $1") .bind(self.id) @@ -108,6 +168,15 @@ impl Track { .unwrap(); } + /// Find tracks by album. + /// + /// # Arguments + /// + /// * `album` - The album to find tracks for. + /// + /// # Returns + /// + /// A vector of tracks with the specified album. pub async fn find_of_album(album: &uuid::Uuid) -> Vec { sqlx::query_as("SELECT * FROM track WHERE album = $1") .bind(album) @@ -116,6 +185,18 @@ impl Track { .unwrap() } + /// Retrieves a list of tracks that are the latest listened to for a given user. + /// + /// This function fetches the latest events associated with the user and then filters them by track ID. + /// It uses these IDs to query the database for all tracks with matching IDs. + /// + /// # Arguments + /// + /// * `u`: The user object whose latest tracks are being retrieved. + /// + /// # Returns + /// + /// A vector of `Track` objects representing the latest tracks for the given user. pub async fn get_latest_of_user(u: &User) -> Vec { let latest_events = Event::get_latest_events_of(u).await; let mut ids = HashSet::new(); @@ -133,17 +214,46 @@ impl Track { .unwrap() } - /// Transcode audio to OPUS with `bitrate` + /// Transcodes audio to OPUS format with the specified bitrate. + /// + /// # Arguments + /// + /// * `bitrate`: The desired bitrate for the transcoded audio. + /// + /// # Returns + /// + /// A `String` representing the path to the transceded audio file, or `None` if the transcoding failed. pub fn get_opus(&self, bitrate: u32) -> Option { self.transcode("libopus", bitrate, "opus") } - /// Transcode audio to AAC with `bitrate` + /// Transcodes audio to AAC format with the specified bitrate. + /// + /// # Arguments + /// + /// * `bitrate`: The desired bitrate for the transcoded audio. + /// + /// # Returns + /// + /// A `String` representing the path to the transceded audio file, or `None` if the transcoding failed. pub fn get_aac(&self, bitrate: u32) -> Option { self.transcode("aac", bitrate, "aac") } - /// Transcode audio + /// Transcodes audio from its original format to the specified codec and extension. + /// + /// This function creates a new directory for the transceded audio if it does not exist, + /// and then uses `ffmpeg` to perform the transcoding operation. + /// + /// # Arguments + /// + /// * `codec`: The desired codec for the transceded audio (e.g. "libopus", "aac"). + /// * `bitrate`: The desired bitrate for the transceded audio. + /// * `ext`: The desired extension for the transceded audio file (e.g. "opus", "aac"). + /// + /// # Returns + /// + /// A `String` representing the path to the transceded audio file, or `None` if the transcoding failed. pub fn transcode(&self, codec: &str, bitrate: u32, ext: &str) -> Option { let transcoded = format!("./data/transcode/{codec}/{bitrate}/{}.{ext}", self.id); @@ -173,6 +283,16 @@ impl Track { Some(transcoded) } + /// Finds tracks in the database that match a regular expression pattern. + /// + /// # Arguments + /// + /// * `col`: The name of the column to filter on. + /// * `query`: The regular expression pattern to match against. + /// + /// # Returns + /// + /// A vector of `Track` objects representing the matching tracks. pub async fn find_regex_col(col: &str, query: &str) -> Vec { sqlx::query_as(&format!("SELECT * FROM track WHERE {col} ~* $1")) .bind(query) @@ -181,7 +301,11 @@ impl Track { .unwrap() } - /// Find tracks with no album or artist + /// Retrieves a list of tracks with no album or artist associated. + /// + /// # Returns + /// + /// A vector of `Track` objects representing the orphaned tracks. pub async fn get_orphans() -> Vec { sqlx::query_as("SELECT * FROM track WHERE artist IS NULL AND album IS NULL") .fetch_all(get_pg!()) @@ -189,6 +313,7 @@ impl Track { .unwrap() } + /// Finds the first track associated with a given artist. pub async fn find_first_of_artist(artist: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM track WHERE artist = $1 LIMIT 1") .bind(artist) @@ -197,6 +322,7 @@ impl Track { .unwrap() } + /// Finds the first track of a given album. pub async fn find_first_of_album(album: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM track WHERE album = $1 LIMIT 1") .bind(album) diff --git a/src/library/user.rs b/src/library/user.rs index 87b43b8..4931c16 100644 --- a/src/library/user.rs +++ b/src/library/user.rs @@ -17,19 +17,25 @@ fn gen_token(token_length: usize) -> String { #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct User { + /// The username chosen by the user pub username: String, + /// The hashed password for the user pub password: String, + /// The role of the user pub user_role: UserRole, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] #[sqlx(type_name = "user_role", rename_all = "lowercase")] pub enum UserRole { + /// A regular user with limited permissions Regular, + /// An admin user with full system privileges Admin, } impl User { + /// Find a user by their username pub async fn find(username: &str) -> Option { sqlx::query_as("SELECT * FROM users WHERE username = $1") .bind(username) @@ -38,7 +44,11 @@ impl User { .unwrap() } + /// Create a new user with the given details + /// + /// Returns an Option containing the created user, or None if a user already exists with the same username pub async fn create(username: &str, password: &str, role: UserRole) -> Option { + // Check if a user already exists with the same username if Self::find(username).await.is_some() { return None; } @@ -60,6 +70,7 @@ impl User { Some(u) } + /// Login a user with the given username and password pub async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> { let u = Self::find(username).await?; @@ -70,7 +81,9 @@ impl User { Some((u.session().await, u.user_role)) } - /// Change the password of a `User` + /// Change the password of a User + /// + /// Returns a Result indicating whether the password change was successful or not pub async fn passwd(self, old: &str, new: &str) -> Result<(), ()> { if self.verify_pw(old) { sqlx::query("UPDATE users SET \"password\" = $1 WHERE username = $2;") @@ -86,6 +99,7 @@ impl User { Err(()) } + /// Find all users in the system pub async fn find_all() -> Vec { sqlx::query_as("SELECT * FROM users") .fetch_all(get_pg!()) @@ -93,6 +107,9 @@ impl User { .unwrap() } + /// Generate a new session token for the user + /// + /// Returns a Session instance containing the generated token and associated user pub async fn session(&self) -> Session { sqlx::query_as( "INSERT INTO user_session (token, \"user\") VALUES ($1, $2) RETURNING id, token, \"user\"", @@ -104,10 +121,14 @@ impl User { .unwrap() } + /// Check if the user is an admin pub const fn is_admin(&self) -> bool { matches!(self.user_role, UserRole::Admin) } + /// Verify that a provided password matches the hashed password for the user + /// + /// Returns a boolean indicating whether the passwords match or not pub fn verify_pw(&self, password: &str) -> bool { bcrypt::verify(password, &self.password).unwrap() } @@ -124,7 +145,10 @@ impl ToAPI for User { #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Session { + /// The unique ID of the session token pub id: uuid::Uuid, + /// The generated session token pub token: String, + /// The username associated with the session token pub user: String, } From 0554040341c63201fe2a2f18a80c6a960c552d82 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Sun, 6 Oct 2024 17:38:27 +0200 Subject: [PATCH 04/10] fix --- src/library/track.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/library/track.rs b/src/library/track.rs index 867ac85..6c45bf5 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -198,17 +198,8 @@ impl Track { /// /// A vector of `Track` objects representing the latest tracks for the given user. pub async fn get_latest_of_user(u: &User) -> Vec { - let latest_events = Event::get_latest_events_of(u).await; - let mut ids = HashSet::new(); - - for event in latest_events { - if !ids.contains(&event.track) { - ids.insert(event.track.clone()); - } - } - - sqlx::query_as("SELECT * FROM track WHERE id = ANY($1)") - .bind(ids.into_iter().collect::>()) + sqlx::query_as("SELECT DISTINCT t.* FROM track t JOIN events e ON t.id = e.track WHERE e.user = $1 ORDER BY e.time DESC") + .bind(&u.username) .fetch_all(get_pg!()) .await .unwrap() From 43abe4300f5a6f5d086a7a789d2627285e15391d Mon Sep 17 00:00:00 2001 From: JMARyA Date: Mon, 7 Oct 2024 08:21:03 +0200 Subject: [PATCH 05/10] fix --- src/library/track.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/library/track.rs b/src/library/track.rs index 6c45bf5..0127741 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::prelude::FromRow; -use std::{collections::HashSet, str::FromStr}; +use std::str::FromStr; use crate::{ get_pg, @@ -9,7 +9,7 @@ use crate::{ route::{to_uuid, ToAPI}, }; -use super::{event::Event, metadata::AudioMetadata, user::User}; +use super::{metadata::AudioMetadata, user::User}; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Track { @@ -198,11 +198,22 @@ impl Track { /// /// A vector of `Track` objects representing the latest tracks for the given user. pub async fn get_latest_of_user(u: &User) -> Vec { - sqlx::query_as("SELECT DISTINCT t.* FROM track t JOIN events e ON t.id = e.track WHERE e.user = $1 ORDER BY e.time DESC") + let res: Vec<(uuid::Uuid, String, String, chrono::DateTime, Option, Option, Option, chrono::DateTime)> = sqlx::query_as("SELECT DISTINCT(t.*), e.time FROM track t JOIN events e ON t.id = e.track WHERE e.user = $1 ORDER BY e.time DESC") .bind(&u.username) .fetch_all(get_pg!()) .await - .unwrap() + .unwrap(); + res.into_iter() + .map(|x| Self { + id: x.0, + path: x.1, + title: x.2, + date_added: x.3, + album: x.4, + artist: x.5, + meta: x.6, + }) + .collect() } /// Transcodes audio to OPUS format with the specified bitrate. From 70d45e42a8e7e80533a5647c0d19cb0bfc08e3a1 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Mon, 7 Oct 2024 15:36:08 +0200 Subject: [PATCH 06/10] idk --- src/library/track.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/library/track.rs b/src/library/track.rs index 0127741..2e580cd 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -198,7 +198,8 @@ impl Track { /// /// A vector of `Track` objects representing the latest tracks for the given user. pub async fn get_latest_of_user(u: &User) -> Vec { - let res: Vec<(uuid::Uuid, String, String, chrono::DateTime, Option, Option, Option, chrono::DateTime)> = sqlx::query_as("SELECT DISTINCT(t.*), e.time FROM track t JOIN events e ON t.id = e.track WHERE e.user = $1 ORDER BY e.time DESC") + // todo : weird + let res: Vec<(uuid::Uuid, String, String, chrono::DateTime, Option, Option, Option, chrono::DateTime)> = sqlx::query_as("SELECT DISTINCT(t.*), e.time FROM track t JOIN events e ON t.id = e.track WHERE e.user = $1 ORDER BY e.time DESC LIMIT 300") .bind(&u.username) .fetch_all(get_pg!()) .await From d309c5d8c14bef07dfc4e00ebf0fdab8062593b1 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Mon, 7 Oct 2024 15:50:46 +0200 Subject: [PATCH 07/10] lyrics --- src/library/track.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 3 ++- src/route/track.rs | 15 +++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/library/track.rs b/src/library/track.rs index 2e580cd..d28b41c 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::prelude::FromRow; -use std::str::FromStr; +use std::{io::Read, str::FromStr}; use crate::{ get_pg, @@ -325,6 +325,47 @@ impl Track { .unwrap() } + pub async fn get_lyrics(&self) -> Option { + let base_path = std::path::Path::new(&self.path).with_extension(""); + let extensions = vec!["lrc", "txt"]; + + for ext in &extensions { + let candidate = base_path.with_extension(ext); + if candidate.exists() { + if let Ok(mut file) = std::fs::File::open(&candidate) { + let mut contents = String::new(); + if file.read_to_string(&mut contents).is_ok() { + // Determine if it's timed lyrics or plain text + let lyrics_type = if ext == &"lrc" { "timed" } else { "plain" }; + + return Some(json!({ + "lyrics": contents, + "source": "file", + "type": lyrics_type + })); + } + } + } + } + + if let Some(lyrics) = self.meta.as_ref()?.get("lyrics").and_then(|l| l.as_str()) { + // Check if lyrics seem to be timed or plain text + let lyrics_type = if lyrics.contains('[') && lyrics.contains(']') { + "timed" // A rough check for LRC-style brackets indicating timing + } else { + "plain" + }; + + return Some(json!({ + "lyrics": lyrics, + "type": lyrics_type + })); + } + + // No lyrics found + None + } + /// Finds the first track of a given album. pub async fn find_first_of_album(album: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM track WHERE album = $1 LIMIT 1") diff --git a/src/main.rs b/src/main.rs index 892700a..b4af49c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,7 +90,8 @@ async fn rocket() -> _ { route::admin::get_orphans_route, route::admin::get_singles_route, route::event::event_report_route, - route::search::search_route + route::search::search_route, + route::track::track_lyrics_route ], ) .manage(lib) diff --git a/src/route/track.rs b/src/route/track.rs index 6676755..9f03007 100644 --- a/src/route/track.rs +++ b/src/route/track.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use super::api_error; +use super::no_uuid_error; use super::to_uuid; use super::FallibleApiResponse; use super::ToAPI; @@ -60,3 +61,17 @@ pub async fn track_audio_aac128_route(track_id: &str, lib: &State) -> Op .await?; NamedFile::open(track.get_aac(128)?).await.ok() } + +#[get("/track//lyrics")] +pub async fn track_lyrics_route(track_id: &str, lib: &State) -> FallibleApiResponse { + let track = lib + .get_track_by_id(&uuid::Uuid::from_str(track_id).map_err(|_| no_uuid_error())?) + .await + .ok_or_else(|| api_error("No such track"))?; + + if let Some(lyrics) = track.get_lyrics().await { + return Ok(lyrics); + } + + Ok(json!({})) +} From 39e2897f0bfa80c98be466c9126020aea0055b68 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Mon, 7 Oct 2024 18:35:01 +0200 Subject: [PATCH 08/10] fix --- src/library/track.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/library/track.rs b/src/library/track.rs index d28b41c..2e51627 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -198,23 +198,21 @@ impl Track { /// /// A vector of `Track` objects representing the latest tracks for the given user. pub async fn get_latest_of_user(u: &User) -> Vec { - // todo : weird - let res: Vec<(uuid::Uuid, String, String, chrono::DateTime, Option, Option, Option, chrono::DateTime)> = sqlx::query_as("SELECT DISTINCT(t.*), e.time FROM track t JOIN events e ON t.id = e.track WHERE e.user = $1 ORDER BY e.time DESC LIMIT 300") + let ids: Vec<(uuid::Uuid,)> = sqlx::query_as( + "SELECT DISTINCT(track) FROM events WHERE \"user\" = $1 ORDER BY time DESC LIMIT 300", + ) .bind(&u.username) - .fetch_all(get_pg!()) - .await - .unwrap(); - res.into_iter() - .map(|x| Self { - id: x.0, - path: x.1, - title: x.2, - date_added: x.3, - album: x.4, - artist: x.5, - meta: x.6, - }) - .collect() + .fetch_all(get_pg!()) + .await + .unwrap(); + + let mut tracks: Vec<_> = Vec::with_capacity(300); + + for track in ids { + tracks.push(Track::get(&track.0).await.unwrap()); + } + + tracks } /// Transcodes audio to OPUS format with the specified bitrate. From 8d352838e8788233a1781c70edd14dca1ed23434 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Mon, 7 Oct 2024 20:15:37 +0200 Subject: [PATCH 09/10] fix --- src/library/track.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/track.rs b/src/library/track.rs index 2e51627..ab7aac6 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -198,8 +198,8 @@ impl Track { /// /// A vector of `Track` objects representing the latest tracks for the given user. pub async fn get_latest_of_user(u: &User) -> Vec { - let ids: Vec<(uuid::Uuid,)> = sqlx::query_as( - "SELECT DISTINCT(track) FROM events WHERE \"user\" = $1 ORDER BY time DESC LIMIT 300", + let ids: Vec<(uuid::Uuid, chrono::DateTime)> = sqlx::query_as( + "SELECT DISTINCT(track), time FROM events WHERE \"user\" = $1 ORDER BY time DESC LIMIT 300", ) .bind(&u.username) .fetch_all(get_pg!()) From 60869448e64500257fbfa1a881a0daa2e955dd8b Mon Sep 17 00:00:00 2001 From: JMARyA Date: Thu, 17 Oct 2024 10:47:02 +0200 Subject: [PATCH 10/10] fix --- src/library/track.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/library/track.rs b/src/library/track.rs index ab7aac6..c2211f2 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -128,15 +128,14 @@ impl Track { let meta = map.get("meta"); sqlx::query( - "UPDATE track SET artist = $1, album = $2, title = $3, meta = $4 WHERE id = $5; -", + "UPDATE track SET artist = $1, album = $2, title = $3, meta = $4 WHERE id = $5", ) .bind(artist) .bind(album) .bind(title) .bind(meta) .bind(self.id) - .fetch_one(get_pg!()) + .execute(get_pg!()) .await .unwrap();