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:
commit
2a389a90d7
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE artists;
|
4
migrations/2024-02-06-145714_create_artists_table/up.sql
Normal file
4
migrations/2024-02-06-145714_create_artists_table/up.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE artists (
|
||||||
|
id SERIAL PRIMARY KEY UNIQUE NOT NULL,
|
||||||
|
name VARCHAR NOT NULL
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE album_artists;
|
||||||
|
DROP TABLE albums;
|
13
migrations/2024-02-06-150214_create_albums_table/up.sql
Normal file
13
migrations/2024-02-06-150214_create_albums_table/up.sql
Normal 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)
|
||||||
|
);
|
2
migrations/2024-02-06-150334_create_songs_table/down.sql
Normal file
2
migrations/2024-02-06-150334_create_songs_table/down.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE song_artists;
|
||||||
|
DROP TABLE songs;
|
16
migrations/2024-02-06-150334_create_songs_table/up.sql
Normal file
16
migrations/2024-02-06-150334_create_songs_table/up.sql
Normal 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)
|
||||||
|
);
|
257
src/models.rs
257
src/models.rs
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user