diff --git a/Cargo.lock b/Cargo.lock index 23c93d8..14117ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,6 +875,7 @@ dependencies = [ "itoa", "pq-sys", "r2d2", + "time", ] [[package]] @@ -1709,6 +1710,7 @@ dependencies = [ "openssl", "pbkdf2", "serde", + "time", "wasm-bindgen", ] @@ -1867,6 +1869,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "object" version = "0.32.2" @@ -2748,12 +2756,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2768,10 +2777,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] diff --git a/Cargo.toml b/Cargo.toml index 8d1b2a6..aecc4be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,11 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [ "BsSkipEndFill" ] } dotenv = { version = "0.15.0", optional = true } -diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } +diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional = true } lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } +time = "0.3.34" diesel_migrations = { version = "2.1.0", optional = true } actix-identity = { version = "0.7.0", optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } diff --git a/migrations/2024-02-06-145714_create_artists_table/down.sql b/migrations/2024-02-06-145714_create_artists_table/down.sql new file mode 100644 index 0000000..943c085 --- /dev/null +++ b/migrations/2024-02-06-145714_create_artists_table/down.sql @@ -0,0 +1 @@ +DROP TABLE artists; diff --git a/migrations/2024-02-06-145714_create_artists_table/up.sql b/migrations/2024-02-06-145714_create_artists_table/up.sql new file mode 100644 index 0000000..73802e6 --- /dev/null +++ b/migrations/2024-02-06-145714_create_artists_table/up.sql @@ -0,0 +1,4 @@ +CREATE TABLE artists ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + name VARCHAR NOT NULL +); diff --git a/migrations/2024-02-06-150214_create_albums_table/down.sql b/migrations/2024-02-06-150214_create_albums_table/down.sql new file mode 100644 index 0000000..31baf23 --- /dev/null +++ b/migrations/2024-02-06-150214_create_albums_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE album_artists; +DROP TABLE albums; diff --git a/migrations/2024-02-06-150214_create_albums_table/up.sql b/migrations/2024-02-06-150214_create_albums_table/up.sql new file mode 100644 index 0000000..154bab2 --- /dev/null +++ b/migrations/2024-02-06-150214_create_albums_table/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE albums ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + title VARCHAR NOT NULL, + release_date DATE +); + +-- A table to store artists for each album +-- Needed because an album can have multiple artists, but in Postgres we can't store an array of foreign keys +CREATE TABLE album_artists ( + album_id INTEGER REFERENCES albums(id) ON DELETE CASCADE NOT NULL, + artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE NULL, + PRIMARY KEY (album_id, artist_id) +); diff --git a/migrations/2024-02-06-150334_create_songs_table/down.sql b/migrations/2024-02-06-150334_create_songs_table/down.sql new file mode 100644 index 0000000..b5ef474 --- /dev/null +++ b/migrations/2024-02-06-150334_create_songs_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE song_artists; +DROP TABLE songs; diff --git a/migrations/2024-02-06-150334_create_songs_table/up.sql b/migrations/2024-02-06-150334_create_songs_table/up.sql new file mode 100644 index 0000000..91249a1 --- /dev/null +++ b/migrations/2024-02-06-150334_create_songs_table/up.sql @@ -0,0 +1,16 @@ +CREATE TABLE songs ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + title VARCHAR NOT NULL, + album_id INTEGER REFERENCES albums(id), + track INTEGER, + duration INTEGER NOT NULL, + release_date DATE, + storage_path VARCHAR NOT NULL, + image_path VARCHAR +); + +CREATE TABLE song_artists ( + song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE NOT NULL, + artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE NOT NULL, + PRIMARY KEY (song_id, artist_id) +); diff --git a/src/models.rs b/src/models.rs index e6ee017..0c94672 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,8 +1,16 @@ use std::time::SystemTime; +use std::error::Error; +use time::Date; use serde::{Deserialize, Serialize}; -#[cfg(feature = "ssr")] -use diesel::prelude::*; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use diesel::prelude::*; + use crate::database::PgPooledConn; + } +} // These "models" are used to represent the data in the database // Diesel uses these models to generate the SQL queries that are used to interact with the database. @@ -34,3 +42,248 @@ pub struct User { #[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))] pub created_at: Option, } + +/// Model for an artist +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Artist { + /// A unique id for the artist + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The artist's name + pub name: String, +} + +impl Artist { + /// Add an album to this artist in the database + /// + /// # Arguments + /// + /// * `new_album_id` - The id of the album to add to this artist + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_album(self: &Self, new_album_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; + + diesel::insert_into(album_artists) + .values((album_id.eq(new_album_id), artist_id.eq(my_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get albums by artist from the database + /// + /// The `id` field of this artist must be present (Some) to get albums + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of albums, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_albums(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::albums::dsl::*; + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to get albums")?; + + let my_albums = albums + .inner_join(album_artists) + .filter(artist_id.eq(my_id)) + .select(albums::all_columns()) + .load(conn)?; + + Ok(my_albums) + } + + /// Add a song to this artist in the database + /// + /// The `id` field of this artist must be present (Some) to add a song + /// + /// # Arguments + /// + /// * `new_song_id` - The id of the song to add to this artist + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_song(self: &Self, new_song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; + + diesel::insert_into(song_artists) + .values((song_id.eq(new_song_id), artist_id.eq(my_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get songs by this artist from the database + /// + /// The `id` field of this artist must be present (Some) to get songs + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of songs, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::songs::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to get songs")?; + + let my_songs = songs + .inner_join(song_artists) + .filter(artist_id.eq(my_id)) + .select(songs::all_columns()) + .load(conn)?; + + Ok(my_songs) + } +} + +/// Model for an album +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Album { + /// A unique id for the album + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The album's title + pub title: String, + /// The album's release date + pub release_date: Option, +} + +impl Album { + /// Add an artist to this album in the database + /// + /// The `id` field of this album must be present (Some) to add an artist + /// + /// # Arguments + /// + /// * `new_artist_id` - The id of the artist to add to this album + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_artist(self: &Self, new_artist_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Album id must be present (Some) to add an artist")?; + + diesel::insert_into(album_artists) + .values((album_id.eq(my_id), artist_id.eq(new_artist_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get songs by this artist from the database + /// + /// The `id` field of this album must be present (Some) to get songs + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of songs, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::songs::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Album id must be present (Some) to get songs")?; + + let my_songs = songs + .inner_join(song_artists) + .filter(album_id.eq(my_id)) + .select(songs::all_columns()) + .load(conn)?; + + Ok(my_songs) + } +} + +/// Model for a song +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Song { + /// A unique id for the song + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The song's title + pub title: String, + /// The album the song is from + pub album_id: Option, + /// The track number of the song on the album + pub track: Option, + /// The duration of the song in seconds + pub duration: i32, + /// The song's release date + pub release_date: Option, + /// The path to the song's audio file + pub storage_path: String, + /// The path to the song's image file + pub image_path: Option, +} + +impl Song { + /// Add an artist to this song in the database + /// + /// The `id` field of this song must be present (Some) to add an artist + /// + /// # Arguments + /// + /// * `new_artist_id` - The id of the artist to add to this song + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_artists(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::artists::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Song id must be present (Some) to get artists")?; + + let my_artists = artists + .inner_join(song_artists) + .filter(song_id.eq(my_id)) + .select(artists::all_columns()) + .load(conn)?; + + Ok(my_artists) + } +} diff --git a/src/schema.rs b/src/schema.rs index 2e9b462..b98d736 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,47 @@ // @generated automatically by Diesel CLI. +diesel::table! { + album_artists (album_id, artist_id) { + album_id -> Int4, + artist_id -> Int4, + } +} + +diesel::table! { + albums (id) { + id -> Int4, + title -> Varchar, + release_date -> Nullable, + } +} + +diesel::table! { + artists (id) { + id -> Int4, + name -> Varchar, + } +} + +diesel::table! { + song_artists (song_id, artist_id) { + song_id -> Int4, + artist_id -> Int4, + } +} + +diesel::table! { + songs (id) { + id -> Int4, + title -> Varchar, + album_id -> Nullable, + track -> Nullable, + duration -> Int4, + release_date -> Nullable, + storage_path -> Varchar, + image_path -> Nullable, + } +} + diesel::table! { users (id) { id -> Int4, @@ -9,3 +51,18 @@ diesel::table! { created_at -> Timestamp, } } + +diesel::joinable!(album_artists -> albums (album_id)); +diesel::joinable!(album_artists -> artists (artist_id)); +diesel::joinable!(song_artists -> artists (artist_id)); +diesel::joinable!(song_artists -> songs (song_id)); +diesel::joinable!(songs -> albums (album_id)); + +diesel::allow_tables_to_appear_in_same_query!( + album_artists, + albums, + artists, + song_artists, + songs, + users, +);