Merge branch '15-create-database-tables-for-songs-artists-and-albums' into 'main'

Create database tables for songs, artists, and albums

Closes #15

See merge request libretunes/libretunes!8
This commit is contained in:
Ethan Girouard 2024-02-16 16:18:34 -05:00
commit 2a389a90d7
10 changed files with 366 additions and 7 deletions

18
Cargo.lock generated
View File

@ -875,6 +875,7 @@ dependencies = [
"itoa", "itoa",
"pq-sys", "pq-sys",
"r2d2", "r2d2",
"time",
] ]
[[package]] [[package]]
@ -1709,6 +1710,7 @@ dependencies = [
"openssl", "openssl",
"pbkdf2", "pbkdf2",
"serde", "serde",
"time",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1867,6 +1869,12 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@ -2748,12 +2756,13 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.31" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"num-conv",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
@ -2768,10 +2777,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
dependencies = [ dependencies = [
"num-conv",
"time-core", "time-core",
] ]

View File

@ -25,10 +25,11 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [
"BsSkipEndFill" "BsSkipEndFill"
] } ] }
dotenv = { version = "0.15.0", optional = true } 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 } lazy_static = { version = "1.4.0", optional = true }
serde = { versions = "1.0.195", features = ["derive"] } serde = { versions = "1.0.195", features = ["derive"] }
openssl = { version = "0.10.63", optional = true } openssl = { version = "0.10.63", optional = true }
time = "0.3.34"
diesel_migrations = { version = "2.1.0", optional = true } diesel_migrations = { version = "2.1.0", optional = true }
actix-identity = { version = "0.7.0", optional = true } actix-identity = { version = "0.7.0", optional = true }
actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true }

View File

@ -0,0 +1 @@
DROP TABLE artists;

View File

@ -0,0 +1,4 @@
CREATE TABLE artists (
id SERIAL PRIMARY KEY UNIQUE NOT NULL,
name VARCHAR NOT NULL
);

View File

@ -0,0 +1,2 @@
DROP TABLE album_artists;
DROP TABLE albums;

View File

@ -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)
);

View File

@ -0,0 +1,2 @@
DROP TABLE song_artists;
DROP TABLE songs;

View File

@ -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)
);

View File

@ -1,8 +1,16 @@
use std::time::SystemTime; use std::time::SystemTime;
use std::error::Error;
use time::Date;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")] use cfg_if::cfg_if;
use diesel::prelude::*;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
use crate::database::PgPooledConn;
}
}
// These "models" are used to represent the data in the database // 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. // 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))] #[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))]
pub created_at: Option<SystemTime>, pub created_at: Option<SystemTime>,
} }
/// 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<i32>,
/// 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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Album>, Box<dyn Error>>` - 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<Vec<Album>, Box<dyn Error>> {
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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Song>, Box<dyn Error>>` - 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<Vec<Song>, Box<dyn Error>> {
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<i32>,
/// The album's title
pub title: String,
/// The album's release date
pub release_date: Option<Date>,
}
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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Song>, Box<dyn Error>>` - 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<Vec<Song>, Box<dyn Error>> {
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<i32>,
/// The song's title
pub title: String,
/// The album the song is from
pub album_id: Option<i32>,
/// The track number of the song on the album
pub track: Option<i32>,
/// The duration of the song in seconds
pub duration: i32,
/// The song's release date
pub release_date: Option<Date>,
/// The path to the song's audio file
pub storage_path: String,
/// The path to the song's image file
pub image_path: Option<String>,
}
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<Vec<Artist>, Box<dyn Error>>` - A result indicating success with an empty value, or an error
///
#[cfg(feature = "ssr")]
pub fn get_artists(self: &Self, conn: &mut PgPooledConn) -> Result<Vec<Artist>, Box<dyn Error>> {
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)
}
}

View File

@ -1,5 +1,47 @@
// @generated automatically by Diesel CLI. // @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<Date>,
}
}
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<Int4>,
track -> Nullable<Int4>,
duration -> Int4,
release_date -> Nullable<Date>,
storage_path -> Varchar,
image_path -> Nullable<Varchar>,
}
}
diesel::table! { diesel::table! {
users (id) { users (id) {
id -> Int4, id -> Int4,
@ -9,3 +51,18 @@ diesel::table! {
created_at -> Timestamp, 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,
);