refactor: sqlx -> sea-orm; let's get a bit fancier.
This commit is contained in:
parent
d575c317c3
commit
7198eda4ee
44 changed files with 2264 additions and 580 deletions
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM users WHERE name = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "password",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_admin",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d08992cf2c132fedbed21b94d545e154fa2a7a2a2bf79fd033341d1bb5a6c0f2"
|
||||
}
|
||||
1320
Cargo.lock
generated
1320
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
42
Cargo.toml
42
Cargo.toml
|
|
@ -1,38 +1,20 @@
|
|||
[package]
|
||||
name = "rave"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = ["crates-io"]
|
||||
[workspace]
|
||||
members = ["app", "entities", "migration"]
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0.0"
|
||||
color-eyre = "0.6.2"
|
||||
md5 = "0.7.0"
|
||||
poem = { version = "1.3.58", features = [
|
||||
"compression",
|
||||
"cookie",
|
||||
"session",
|
||||
"static-files",
|
||||
"xml",
|
||||
[workspace.dependencies]
|
||||
entities = { path = "entities" }
|
||||
migration = { path = "migration" }
|
||||
sea-orm = { version = "0.12", features = [
|
||||
"sqlx-postgres",
|
||||
"runtime-tokio-rustls",
|
||||
"with-time",
|
||||
"postgres-array",
|
||||
] }
|
||||
quick-xml = { version = "0.30.0", features = ["serialize"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
sqlx = { version = "0.7.2", features = ["time", "postgres", "runtime-tokio"] }
|
||||
time = { version = "0.3.29", features = [
|
||||
"serde-human-readable",
|
||||
"macros",
|
||||
"parsing",
|
||||
] }
|
||||
tokio = { version = "1.32.0", features = ["full"] }
|
||||
tracing = { version = "0.1.37", features = ["async-await"] }
|
||||
tracing-subscriber = { version = "0.3.17", features = [
|
||||
"env-filter",
|
||||
"tracing",
|
||||
"parking_lot",
|
||||
"time",
|
||||
] }
|
||||
url = { version = "2.4.1", features = ["serde"] }
|
||||
url-escape = "0.1.1"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
|
|
|
|||
37
app/Cargo.toml
Normal file
37
app/Cargo.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "rave"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = ["forge-lys-ee"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0.0"
|
||||
color-eyre = "0.6.2"
|
||||
md5 = "0.7.0"
|
||||
poem = { version = "1.3.58", features = [
|
||||
"compression",
|
||||
"cookie",
|
||||
"session",
|
||||
"static-files",
|
||||
"xml",
|
||||
] }
|
||||
poem-ext = "0.9.4"
|
||||
quick-xml = { version = "0.30.0", features = ["serialize"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = "1.0.107"
|
||||
time = { workspace = true }
|
||||
tokio = { version = "1.32.0", features = ["full"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.17", features = [
|
||||
"env-filter",
|
||||
"tracing",
|
||||
"parking_lot",
|
||||
"time",
|
||||
] }
|
||||
url = { version = "2.4.1", features = ["serde"] }
|
||||
url-escape = "0.1.1"
|
||||
sea-orm = { workspace = true }
|
||||
entities = { workspace = true }
|
||||
migration = { workspace = true }
|
||||
|
|
@ -5,13 +5,15 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use poem::{
|
||||
listener::TcpListener,
|
||||
middleware,
|
||||
web::{CompressionAlgo, CompressionLevel},
|
||||
Endpoint, EndpointExt, Route,
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use poem_ext::db::DbTransactionMiddleware;
|
||||
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
|
|
@ -20,7 +22,6 @@ mod random_types;
|
|||
mod rest;
|
||||
mod subsonic;
|
||||
mod ui;
|
||||
mod user;
|
||||
mod utils;
|
||||
|
||||
const LISTEN: &str = "0.0.0.0:1234";
|
||||
|
|
@ -32,11 +33,10 @@ async fn main() -> Result<()> {
|
|||
|
||||
let route = create_route();
|
||||
|
||||
let pool = create_pool().await;
|
||||
let dbc = create_pool().await?;
|
||||
Migrator::up(&dbc, None).await?;
|
||||
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
|
||||
let route = route.with(utils::middleware::DbConnectionMiddleware::new(pool));
|
||||
let route = route.with(DbTransactionMiddleware::new(dbc));
|
||||
|
||||
let server = create_server();
|
||||
|
||||
|
|
@ -51,12 +51,16 @@ async fn main() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
async fn create_pool() -> PgPool {
|
||||
|
||||
async fn create_pool() -> Result<DatabaseConnection> {
|
||||
let url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
|
||||
|
||||
PgPool::connect(&url)
|
||||
.await
|
||||
.expect("Failed to connect to database")
|
||||
let mut opt = ConnectOptions::new(url);
|
||||
opt.max_connections(100)
|
||||
.min_connections(5)
|
||||
.sqlx_logging(true);
|
||||
|
||||
Ok(Database::connect(opt).await?)
|
||||
}
|
||||
|
||||
fn create_route() -> Box<dyn Endpoint<Output = poem::Response>> {
|
||||
30
app/src/rest/get_album.rs
Normal file
30
app/src/rest/get_album.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use crate::{
|
||||
authentication::Authentication,
|
||||
subsonic::SubsonicResponse,
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
use poem::web::{Data, Query};
|
||||
use poem_ext::db::DbTxn;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn get_album(
|
||||
Data(txn): Data<&DbTxn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetAlbumParams>,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
}
|
||||
|
||||
todo!("get_album not implemented");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct GetAlbumParams {
|
||||
pub id: i32,
|
||||
}
|
||||
263
app/src/rest/get_album_list.rs
Normal file
263
app/src/rest/get_album_list.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
#![allow(clippy::unused_async)] // todo: remove
|
||||
|
||||
use entities::{album, prelude::Album};
|
||||
use poem::web::{Data, Query};
|
||||
use poem_ext::db::DbTxn;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
random_types::SortType,
|
||||
subsonic::{Error, SubsonicResponse},
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
macro_rules! error_or {
|
||||
($thing:ident) => {
|
||||
match $thing {
|
||||
Ok(a) => Ok(a),
|
||||
Err(e) => Err(Error::Generic(Some(e.to_string()))),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn get_album_list(
|
||||
Data(txn): Data<&DbTxn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetAlbumListParams>,
|
||||
) -> SubsonicResponse {
|
||||
let txn = txn.clone();
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
}
|
||||
|
||||
let params = match params.verify() {
|
||||
Ok(p) => p,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let album_list = match params.r#type {
|
||||
SortType::Random => get_album_list_random(txn, params).await,
|
||||
SortType::Newest => get_album_list_newest(txn, params).await,
|
||||
SortType::Highest => get_album_list_highest(txn, params).await,
|
||||
SortType::Frequent => get_album_list_frequent(txn, params).await,
|
||||
SortType::Recent => get_album_list_recent(txn, params).await,
|
||||
SortType::AlphabeticalByName => get_album_list_alphabetical_by_name(txn, params).await,
|
||||
SortType::AlphabeticalByArtist => get_album_list_alphabetical_by_artist(txn, params).await,
|
||||
SortType::Starred => get_album_list_starred(txn, params).await,
|
||||
SortType::ByYear => get_album_list_by_year(txn, params).await,
|
||||
SortType::ByGenre => get_album_list_by_genre(txn, params).await,
|
||||
};
|
||||
|
||||
match album_list {
|
||||
Ok(a) => SubsonicResponse::new_album_list(a),
|
||||
Err(e) => SubsonicResponse::new_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_random(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
Err(Error::Generic(Some(
|
||||
"Sorting by random not implemented".to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_newest(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
let albums = Album::find()
|
||||
.order_by_desc(album::Column::Created)
|
||||
.limit(params.size)
|
||||
.offset(params.offset)
|
||||
.all(&*conn)
|
||||
.await;
|
||||
|
||||
error_or!(albums)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_highest(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
Err(Error::Generic(Some(
|
||||
"Sorting by highest rating not implemented".to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_frequent(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
let albums = Album::find()
|
||||
.order_by_desc(album::Column::PlayCount)
|
||||
.limit(params.size)
|
||||
.offset(params.offset)
|
||||
.all(&*conn)
|
||||
.await;
|
||||
|
||||
error_or!(albums)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_recent(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
Err(Error::Generic(Some(
|
||||
"Sorting by recently played not implemented".to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_alphabetical_by_name(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
let albums = Album::find()
|
||||
.order_by_desc(album::Column::Name)
|
||||
.limit(params.size)
|
||||
.offset(params.offset)
|
||||
.all(&*conn)
|
||||
.await;
|
||||
|
||||
error_or!(albums)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_alphabetical_by_artist(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
let albums = Album::find()
|
||||
.order_by_desc(album::Column::Artist)
|
||||
.limit(params.size)
|
||||
.offset(params.offset)
|
||||
.all(&*conn)
|
||||
.await;
|
||||
|
||||
error_or!(albums)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_starred(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
let albums = Album::find()
|
||||
.filter(album::Column::Starred.is_not_null())
|
||||
.limit(params.size)
|
||||
.offset(params.offset)
|
||||
.all(&*conn)
|
||||
.await;
|
||||
|
||||
error_or!(albums)
|
||||
}
|
||||
|
||||
async fn get_album_list_by_year(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
let from_year = params.from_year;
|
||||
let to_year = params.to_year;
|
||||
|
||||
let (Some(from_year), Some(to_year)) = (from_year, to_year) else {
|
||||
return Err(Error::RequiredParameterMissing(Some(
|
||||
"Missing required parameter: fromYear or toYear".to_string(),
|
||||
)));
|
||||
};
|
||||
|
||||
let albums = Album::find()
|
||||
.filter(album::Column::Year.is_not_null())
|
||||
.filter(album::Column::Year.between(from_year, to_year))
|
||||
.limit(params.size)
|
||||
.offset(params.offset)
|
||||
.all(&*conn)
|
||||
.await;
|
||||
|
||||
error_or!(albums)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
async fn get_album_list_by_genre(
|
||||
conn: DbTxn,
|
||||
params: GetAlbumListParams,
|
||||
) -> Result<Vec<album::Model>, Error> {
|
||||
let genre = params.genre;
|
||||
|
||||
let Some(genre) = genre else {
|
||||
return Err(Error::RequiredParameterMissing(Some(
|
||||
"Missing required parameter: genre".to_string(),
|
||||
)));
|
||||
};
|
||||
|
||||
let albums = Album::find()
|
||||
.filter(album::Column::Genre.is_not_null())
|
||||
.filter(album::Column::Genre.eq(genre))
|
||||
.limit(params.size)
|
||||
.offset(params.offset)
|
||||
.all(&*conn)
|
||||
.await;
|
||||
|
||||
error_or!(albums)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct GetAlbumListParams {
|
||||
#[serde(rename = "type")]
|
||||
pub r#type: SortType,
|
||||
#[serde(default = "default_size")]
|
||||
pub size: u64,
|
||||
#[serde(default)]
|
||||
pub offset: u64,
|
||||
#[serde(default)]
|
||||
pub from_year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub to_year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub genre: Option<String>,
|
||||
#[serde(default)]
|
||||
pub music_folder_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl GetAlbumListParams {
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn verify(self) -> Result<Self, SubsonicResponse> {
|
||||
if self.r#type == SortType::ByYear {
|
||||
if self.from_year.is_none() || self.to_year.is_none() {
|
||||
return Err(SubsonicResponse::new_error(
|
||||
Error::RequiredParameterMissing(Some(
|
||||
"Missing required parameter: fromYear or toYear".to_string(),
|
||||
)),
|
||||
));
|
||||
}
|
||||
} else if self.r#type == SortType::ByGenre && self.genre.is_none() {
|
||||
return Err(SubsonicResponse::new_error(
|
||||
Error::RequiredParameterMissing(Some(
|
||||
"Missing required parameter: genre".to_string(),
|
||||
)),
|
||||
));
|
||||
} else if self.size > 500 || self.size < 1 {
|
||||
return Err(SubsonicResponse::new_error(Error::Generic(Some(
|
||||
"size must be between 1 and 500".to_string(),
|
||||
))));
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_size() -> u64 {
|
||||
10
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
use poem::web::Data;
|
||||
use poem_ext::db::DbTxn;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
subsonic::SubsonicResponse,
|
||||
utils::{self, middleware::DbConn},
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn get_license(Data(conn): Data<&DbConn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(conn.clone(), auth).await;
|
||||
pub async fn get_license(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
use poem::web::Data;
|
||||
use poem_ext::db::DbTxn;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
subsonic::{MusicFolder, SubsonicResponse},
|
||||
utils::{self, middleware::DbConn},
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn get_music_folders(
|
||||
Data(conn): Data<&DbConn>,
|
||||
auth: Authentication,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(conn.clone(), auth).await;
|
||||
pub async fn get_music_folders(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
|
|
@ -8,8 +8,6 @@ mod get_music_folders;
|
|||
mod ping;
|
||||
// rest/getAlbumList
|
||||
mod get_album_list;
|
||||
// rest/getAlbumList2
|
||||
mod get_album_list2;
|
||||
// rest/getAlbum
|
||||
mod get_album;
|
||||
// rest/stream
|
||||
|
|
@ -21,7 +19,7 @@ pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> {
|
|||
.at("/getLicense", get_license::get_license)
|
||||
.at("/getMusicFolders", get_music_folders::get_music_folders)
|
||||
.at("/getAlbumList", get_album_list::get_album_list)
|
||||
.at("/getAlbumList2", get_album_list2::get_album_list2)
|
||||
.at("/getAlbumList2", get_album_list::get_album_list)
|
||||
.at("/getAlbum", get_album::get_album)
|
||||
.at("/stream", stream::stream)
|
||||
.boxed()
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
use poem::web::Data;
|
||||
use poem_ext::db::DbTxn;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
subsonic::SubsonicResponse,
|
||||
utils::{self, middleware::DbConn},
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn ping(Data(conn): Data<&DbConn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(conn.clone(), auth).await;
|
||||
pub async fn ping(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => SubsonicResponse::new_empty(),
|
||||
|
|
@ -3,22 +3,23 @@ use poem::{
|
|||
web::{Data, Query},
|
||||
IntoResponse, Response,
|
||||
};
|
||||
use poem_ext::db::DbTxn;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
utils::{self, middleware::DbConn},
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
const SONG: &[u8] = include_bytes!("../../../data.mp3");
|
||||
const SONG: &[u8] = include_bytes!("../../../../data.mp3");
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn stream(
|
||||
Data(conn): Data<&DbConn>,
|
||||
Data(txn): Data<&DbTxn>,
|
||||
auth: Authentication,
|
||||
Query(_params): Query<StreamParams>,
|
||||
) -> Response {
|
||||
let u = utils::verify_user(conn.clone(), auth).await;
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
use std::fmt::Display;
|
||||
|
||||
use entities::album;
|
||||
use poem::{http::StatusCode, IntoResponse, Response};
|
||||
use serde::{ser::SerializeStruct, Serialize, Serializer};
|
||||
use serde::{ser::SerializeStruct, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::authentication::VersionTriple;
|
||||
|
|
@ -42,16 +43,16 @@ impl SubsonicResponse {
|
|||
Self::new(SubResponseType::MusicFolders { music_folders })
|
||||
}
|
||||
|
||||
pub fn new_album_list(albums: Vec<Child>) -> Self {
|
||||
pub fn new_album_list(albums: Vec<album::Model>) -> Self {
|
||||
Self::new(SubResponseType::AlbumList { albums })
|
||||
}
|
||||
|
||||
pub fn new_album_list2(albums: Vec<AlbumId3>) -> Self {
|
||||
pub fn new_album_list2(albums: Vec<album::Model>) -> Self {
|
||||
Self::new(SubResponseType::AlbumList2 { albums })
|
||||
}
|
||||
|
||||
pub fn new_album(album: AlbumId3) -> Self {
|
||||
Self::new(SubResponseType::Album(album))
|
||||
pub fn new_album(album: album::Model, songs: Vec<Child>) -> Self {
|
||||
Self::new(SubResponseType::Album { album, songs })
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
|
|
@ -85,23 +86,29 @@ pub enum SubResponseType {
|
|||
#[serde(rename = "albumList")]
|
||||
AlbumList {
|
||||
#[serde(rename = "album")]
|
||||
albums: Vec<Child>,
|
||||
albums: Vec<album::Model>,
|
||||
},
|
||||
#[serde(rename = "albumList2")]
|
||||
AlbumList2 {
|
||||
#[serde(rename = "album")]
|
||||
albums: Vec<AlbumId3>,
|
||||
albums: Vec<album::Model>,
|
||||
},
|
||||
#[serde(rename = "album")]
|
||||
Album(AlbumId3),
|
||||
Album {
|
||||
#[serde(flatten)]
|
||||
album: album::Model,
|
||||
#[serde(flatten)]
|
||||
songs: Vec<Child>,
|
||||
},
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct AlbumId3 {
|
||||
#[serde(rename = "@id", serialize_with = "album_id")]
|
||||
pub id: i32,
|
||||
#[serde(rename = "@parent")]
|
||||
#[serde(rename = "@id", serialize_with = "crate::utils::album_id")]
|
||||
pub id: i64,
|
||||
#[serde(rename = "@name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "@artist", skip_serializing_if = "Option::is_none")]
|
||||
pub artist: Option<String>,
|
||||
|
|
@ -123,15 +130,8 @@ pub struct AlbumId3 {
|
|||
pub year: Option<i32>,
|
||||
#[serde(rename = "@genre", skip_serializing_if = "Option::is_none")]
|
||||
pub genre: Option<String>,
|
||||
#[serde(rename = "song", skip_serializing_if = "Vec::is_empty")]
|
||||
pub songs: Vec<Child>,
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn album_id<S: Serializer>(id: &i32, s: S) -> Result<S::Ok, S::Error> {
|
||||
let str = format!("al-{id}");
|
||||
|
||||
s.serialize_str(&str)
|
||||
#[serde(rename = "@musicFolder", skip_serializing_if = "Option::is_none")]
|
||||
pub folder_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
|
|
@ -219,7 +219,7 @@ pub enum MediaType {
|
|||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MusicFolder {
|
||||
#[serde(rename = "@id")]
|
||||
pub id: i32,
|
||||
pub id: i64,
|
||||
#[serde(rename = "@name")]
|
||||
pub name: String,
|
||||
}
|
||||
51
app/src/utils.rs
Normal file
51
app/src/utils.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use entities::{prelude::User, user};
|
||||
use poem_ext::db::DbTxn;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use serde::Serializer;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
subsonic::{Error, SubsonicResponse},
|
||||
};
|
||||
|
||||
pub async fn verify_user(
|
||||
conn: DbTxn,
|
||||
auth: Authentication,
|
||||
) -> Result<user::Model, SubsonicResponse> {
|
||||
let user = User::find()
|
||||
.filter(user::Column::Name.eq(&auth.username))
|
||||
.one(&*conn)
|
||||
.await;
|
||||
|
||||
match user {
|
||||
Ok(Some(u)) => {
|
||||
let ours = md5::compute(format!("{}{}", u.password, auth.salt));
|
||||
let ours = format!("{ours:x}");
|
||||
|
||||
if ours == auth.token {
|
||||
Ok(u)
|
||||
} else {
|
||||
Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword(
|
||||
None,
|
||||
)))
|
||||
}
|
||||
}
|
||||
Ok(None) => Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword(
|
||||
None,
|
||||
))),
|
||||
Err(e) => {
|
||||
error!("Error getting user: {e}");
|
||||
Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword(
|
||||
None,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
pub fn album_id<S: Serializer>(id: &i64, s: S) -> Result<S::Ok, S::Error> {
|
||||
let str = format!("al-{id}");
|
||||
|
||||
s.serialize_str(&str)
|
||||
}
|
||||
5
build.rs
5
build.rs
|
|
@ -1,5 +0,0 @@
|
|||
// generated by `sqlx migrate build-script`
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
||||
12
entities/Cargo.toml
Normal file
12
entities/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "entities"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sea-orm = { workspace = true }
|
||||
time = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
57
entities/src/album.rs
Normal file
57
entities/src/album.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "album")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub artist: Option<String>,
|
||||
pub artist_id: Option<i64>,
|
||||
pub cover_art: Option<String>,
|
||||
pub song_count: i32,
|
||||
pub duration: i64,
|
||||
pub play_count: i64,
|
||||
pub created: DateTimeWithTimeZone,
|
||||
pub starred: Option<DateTimeWithTimeZone>,
|
||||
pub year: Option<i32>,
|
||||
pub genre: Option<String>,
|
||||
pub music_folder_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::artist::Entity",
|
||||
from = "Column::ArtistId",
|
||||
to = "super::artist::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "SetNull"
|
||||
)]
|
||||
Artist,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::music_folder::Entity",
|
||||
from = "Column::MusicFolderId",
|
||||
to = "super::music_folder::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
MusicFolder,
|
||||
}
|
||||
|
||||
impl Related<super::artist::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Artist.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::music_folder::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::MusicFolder.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
31
entities/src/artist.rs
Normal file
31
entities/src/artist.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "artist")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
pub cover_art: Option<String>,
|
||||
pub artist_image_url: Option<String>,
|
||||
pub album_count: i32,
|
||||
pub starred: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::album::Entity")]
|
||||
Album,
|
||||
}
|
||||
|
||||
impl Related<super::album::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Album.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
8
entities/src/lib.rs
Normal file
8
entities/src/lib.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod album;
|
||||
pub mod artist;
|
||||
pub mod music_folder;
|
||||
pub mod user;
|
||||
27
entities/src/music_folder.rs
Normal file
27
entities/src/music_folder.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "music_folder")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::album::Entity")]
|
||||
Album,
|
||||
}
|
||||
|
||||
impl Related<super::album::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Album.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
6
entities/src/prelude.rs
Normal file
6
entities/src/prelude.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
pub use super::album::Entity as Album;
|
||||
pub use super::artist::Entity as Artist;
|
||||
pub use super::music_folder::Entity as MusicFolder;
|
||||
pub use super::user::Entity as User;
|
||||
20
entities/src/user.rs
Normal file
20
entities/src/user.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
16
migration/Cargo.toml
Normal file
16
migration/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "migration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "migration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
version = "0.12"
|
||||
features = ["runtime-tokio-rustls", "sqlx-postgres"]
|
||||
41
migration/README.md
Normal file
41
migration/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Running Migrator CLI
|
||||
|
||||
- Generate a new migration file
|
||||
```sh
|
||||
cargo run -- generate MIGRATION_NAME
|
||||
```
|
||||
- Apply all pending migrations
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
```sh
|
||||
cargo run -- up
|
||||
```
|
||||
- Apply first 10 pending migrations
|
||||
```sh
|
||||
cargo run -- up -n 10
|
||||
```
|
||||
- Rollback last applied migrations
|
||||
```sh
|
||||
cargo run -- down
|
||||
```
|
||||
- Rollback last 10 applied migrations
|
||||
```sh
|
||||
cargo run -- down -n 10
|
||||
```
|
||||
- Drop all tables from the database, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- fresh
|
||||
```
|
||||
- Rollback all applied migrations, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- refresh
|
||||
```
|
||||
- Rollback all applied migrations
|
||||
```sh
|
||||
cargo run -- reset
|
||||
```
|
||||
- Check the status of all migrations
|
||||
```sh
|
||||
cargo run -- status
|
||||
```
|
||||
20
migration/src/lib.rs
Normal file
20
migration/src/lib.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20220101_000001_create_user;
|
||||
mod m20231009_181004_create_music_folder;
|
||||
mod m20231009_181104_create_artist;
|
||||
mod m20231009_181346_create_album;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20220101_000001_create_user::Migration),
|
||||
Box::new(m20231009_181004_create_music_folder::Migration),
|
||||
Box::new(m20231009_181104_create_artist::Migration),
|
||||
Box::new(m20231009_181346_create_album::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
68
migration/src/m20220101_000001_create_user.rs
Normal file
68
migration/src/m20220101_000001_create_user.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(User::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(User::Id)
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
.auto_increment()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(User::Name).string().not_null().unique_key())
|
||||
.col(ColumnDef::new(User::Password).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(User::IsAdmin)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.table(User::Table)
|
||||
.col(User::Name)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = Query::insert()
|
||||
.into_table(User::Table)
|
||||
.columns([User::Name, User::Password, User::IsAdmin])
|
||||
.values_panic(["admin".into(), "admin".into(), true.into()])
|
||||
.to_owned();
|
||||
|
||||
manager.exec_stmt(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(User::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum User {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Password,
|
||||
IsAdmin,
|
||||
}
|
||||
45
migration/src/m20231009_181004_create_music_folder.rs
Normal file
45
migration/src/m20231009_181004_create_music_folder.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(MusicFolder::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(MusicFolder::Id)
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
.auto_increment()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(MusicFolder::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(MusicFolder::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum MusicFolder {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
}
|
||||
63
migration/src/m20231009_181104_create_artist.rs
Normal file
63
migration/src/m20231009_181104_create_artist.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Artist::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Artist::Id)
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
.auto_increment()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Artist::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Artist::CoverArt).string().null())
|
||||
.col(ColumnDef::new(Artist::ArtistImageUrl).string().null())
|
||||
.col(
|
||||
ColumnDef::new(Artist::AlbumCount)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Artist::Starred)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Artist::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Artist {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
CoverArt,
|
||||
ArtistImageUrl,
|
||||
AlbumCount,
|
||||
Starred,
|
||||
}
|
||||
104
migration/src/m20231009_181346_create_album.rs
Normal file
104
migration/src/m20231009_181346_create_album.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::{
|
||||
m20231009_181004_create_music_folder::MusicFolder, m20231009_181104_create_artist::Artist,
|
||||
};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Album::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Album::Id)
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Album::Name).string().not_null())
|
||||
.col(ColumnDef::new(Album::Artist).string().null())
|
||||
.col(ColumnDef::new(Album::ArtistId).big_integer().null())
|
||||
.col(ColumnDef::new(Album::CoverArt).string().null())
|
||||
.col(ColumnDef::new(Album::SongCount).integer().not_null())
|
||||
.col(ColumnDef::new(Album::Duration).big_integer().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Album::PlayCount)
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Album::Created)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Album::Starred)
|
||||
.timestamp_with_time_zone()
|
||||
.null(),
|
||||
)
|
||||
.col(ColumnDef::new(Album::Year).integer().null())
|
||||
.col(ColumnDef::new(Album::Genre).string().null())
|
||||
.col(ColumnDef::new(Album::MusicFolderId).big_integer().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.from_tbl(Album::Table)
|
||||
.from_col(Album::MusicFolderId)
|
||||
.to_tbl(MusicFolder::Table)
|
||||
.to_col(MusicFolder::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.from_tbl(Album::Table)
|
||||
.from_col(Album::ArtistId)
|
||||
.to_tbl(Artist::Table)
|
||||
.to_col(Artist::Id)
|
||||
.on_delete(ForeignKeyAction::SetNull)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Album::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Album {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Artist,
|
||||
ArtistId,
|
||||
CoverArt,
|
||||
SongCount,
|
||||
Duration,
|
||||
PlayCount,
|
||||
Created,
|
||||
Starred,
|
||||
Year,
|
||||
Genre,
|
||||
MusicFolderId,
|
||||
}
|
||||
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-- Add migration script here
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
INSERT INTO users (id, name, password, is_admin)
|
||||
VALUES (0, 'admin', 'admin', TRUE);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
-- Add migration script here
|
||||
CREATE TABLE IF NOT EXISTS album (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
artist TEXT,
|
||||
artist_id INTEGER,
|
||||
cover_art TEXT,
|
||||
song_count INTEGER,
|
||||
duration INTEGER,
|
||||
play_count INTEGER,
|
||||
created timestamptz DEFAULT current_timestamp,
|
||||
starred timestamptz DEFAULT current_timestamp,
|
||||
year INTEGER,
|
||||
genre TEXT
|
||||
);
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
-- Add migration script here
|
||||
CREATE TABLE IF NOT EXISTS tracks (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
parent INTEGER NOT NULL REFERENCES album(id),
|
||||
is_dir BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
title TEXT NOT NULL,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
track INTEGER,
|
||||
year INTEGER,
|
||||
genre TEXT,
|
||||
cover_art TEXT,
|
||||
duration INTEGER,
|
||||
path TEXT,
|
||||
play_count INTEGER,
|
||||
created timestamptz DEFAULT current_timestamp,
|
||||
starred timestamptz,
|
||||
album_id TEXT,
|
||||
artist_id TEXT,
|
||||
disc_number INTEGER
|
||||
)
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
use crate::{
|
||||
authentication::Authentication,
|
||||
subsonic::{AlbumId3, Child, Error, MediaType, SubsonicResponse},
|
||||
utils::{self, middleware::DbConn},
|
||||
};
|
||||
|
||||
use poem::web::{Data, Query};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn get_album(
|
||||
Data(conn): Data<&DbConn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetAlbumParams>,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(conn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
let album = match params.id {
|
||||
11 => AlbumId3 {
|
||||
id: 11,
|
||||
name: "Example".to_string(),
|
||||
artist: Some("Example".to_string()),
|
||||
song_count: 5,
|
||||
duration: 100,
|
||||
songs: vec![
|
||||
Child {
|
||||
id: "tr-111".to_string(),
|
||||
title: "Example - 1".to_string(),
|
||||
album: Some("Example".to_string()),
|
||||
duration: Some(20),
|
||||
content_type: Some("audio/mpeg".to_string()),
|
||||
r#type: Some(MediaType::Music),
|
||||
track: Some({
|
||||
count += 1;
|
||||
count
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Child {
|
||||
id: "tr-112".to_string(),
|
||||
title: "Example - 2".to_string(),
|
||||
album: Some("Example".to_string()),
|
||||
duration: Some(20),
|
||||
content_type: Some("audio/mpeg".to_string()),
|
||||
r#type: Some(MediaType::Music),
|
||||
track: Some({
|
||||
count += 1;
|
||||
count
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Child {
|
||||
id: "tr-113".to_string(),
|
||||
title: "Example - 3".to_string(),
|
||||
album: Some("Example".to_string()),
|
||||
duration: Some(20),
|
||||
content_type: Some("audio/mpeg".to_string()),
|
||||
r#type: Some(MediaType::Music),
|
||||
track: Some({
|
||||
count += 1;
|
||||
count
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Child {
|
||||
id: "tr-114".to_string(),
|
||||
title: "Example - 4".to_string(),
|
||||
album: Some("Example".to_string()),
|
||||
duration: Some(20),
|
||||
content_type: Some("audio/mpeg".to_string()),
|
||||
r#type: Some(MediaType::Music),
|
||||
track: Some({
|
||||
count += 1;
|
||||
count
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Child {
|
||||
id: "tr-115".to_string(),
|
||||
title: "Example - 5".to_string(),
|
||||
album: Some("Example".to_string()),
|
||||
duration: Some(20),
|
||||
content_type: Some("audio/mpeg".to_string()),
|
||||
r#type: Some(MediaType::Music),
|
||||
track: Some({
|
||||
count += 1;
|
||||
count
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
12 => AlbumId3 {
|
||||
id: 12,
|
||||
name: "Example 2".to_string(),
|
||||
artist: Some("Example 2".to_string()),
|
||||
song_count: 7,
|
||||
duration: 200,
|
||||
..Default::default()
|
||||
},
|
||||
_ => {
|
||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(Some(
|
||||
"Album does not exist".to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
SubsonicResponse::new_album(album)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct GetAlbumParams {
|
||||
pub id: i32,
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
use poem::web::{Data, Query};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
random_types::SortType,
|
||||
subsonic::{Child, Error, SubsonicResponse},
|
||||
utils::{self, middleware::DbConn},
|
||||
};
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn get_album_list(
|
||||
Data(conn): Data<&DbConn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetAlbumListParams>,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(conn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
}
|
||||
|
||||
let _params = match params.verify() {
|
||||
Ok(p) => p,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let album_list = vec![
|
||||
Child {
|
||||
id: "al-11".to_string(),
|
||||
parent: Some(1),
|
||||
title: "Example".to_string(),
|
||||
artist: Some("Example".to_string()),
|
||||
is_dir: true,
|
||||
..Default::default()
|
||||
},
|
||||
Child {
|
||||
id: "al-12".to_string(),
|
||||
parent: Some(1),
|
||||
title: "Example 2".to_string(),
|
||||
artist: Some("Example 2".to_string()),
|
||||
is_dir: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
SubsonicResponse::new_album_list(album_list)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct GetAlbumListParams {
|
||||
#[serde(rename = "type")]
|
||||
pub r#type: SortType,
|
||||
#[serde(default = "default_size")]
|
||||
pub size: i32,
|
||||
#[serde(default)]
|
||||
pub offset: i32,
|
||||
#[serde(default)]
|
||||
pub from_year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub to_year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub genre: Option<String>,
|
||||
#[serde(default)]
|
||||
pub music_folder_id: Option<i32>,
|
||||
}
|
||||
|
||||
impl GetAlbumListParams {
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn verify(self) -> Result<Self, SubsonicResponse> {
|
||||
if self.r#type == SortType::ByYear {
|
||||
if self.from_year.is_none() || self.to_year.is_none() {
|
||||
return Err(SubsonicResponse::new_error(
|
||||
Error::RequiredParameterMissing(Some(
|
||||
"Missing required parameter: fromYear or toYear".to_string(),
|
||||
)),
|
||||
));
|
||||
}
|
||||
} else if self.r#type == SortType::ByGenre && self.genre.is_none() {
|
||||
return Err(SubsonicResponse::new_error(
|
||||
Error::RequiredParameterMissing(Some(
|
||||
"Missing required parameter: genre".to_string(),
|
||||
)),
|
||||
));
|
||||
} else if self.size > 500 || self.size < 1 {
|
||||
return Err(SubsonicResponse::new_error(Error::Generic(Some(
|
||||
"size must be between 1 and 500".to_string(),
|
||||
))));
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_size() -> i32 {
|
||||
10
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
use poem::web::{Data, Query};
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
rest::get_album_list::GetAlbumListParams,
|
||||
subsonic::{AlbumId3, SubsonicResponse},
|
||||
utils::{self, middleware::DbConn},
|
||||
};
|
||||
|
||||
#[poem::handler]
|
||||
pub async fn get_album_list2(
|
||||
Data(conn): Data<&DbConn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetAlbumListParams>,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(conn.clone(), auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
}
|
||||
|
||||
let params = match params.verify() {
|
||||
Ok(p) => p,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if params.offset > 0 {
|
||||
return SubsonicResponse::new_album_list2(Vec::new());
|
||||
}
|
||||
|
||||
let album_list = vec![
|
||||
AlbumId3 {
|
||||
id: 11,
|
||||
name: "Example".to_string(),
|
||||
artist: Some("Example".to_string()),
|
||||
song_count: 5,
|
||||
duration: 100,
|
||||
..Default::default()
|
||||
},
|
||||
AlbumId3 {
|
||||
id: 12,
|
||||
name: "Example 2".to_string(),
|
||||
artist: Some("Example 2".to_string()),
|
||||
song_count: 7,
|
||||
duration: 200,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
SubsonicResponse::new_album_list2(album_list)
|
||||
}
|
||||
30
src/user.rs
30
src/user.rs
|
|
@ -1,30 +0,0 @@
|
|||
use color_eyre::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::utils::middleware::DbConn;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
/// I hate this. It's stored in plaintext. Why?
|
||||
password: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn verify(&self, hashed_password: &str, salt: &str) -> bool {
|
||||
let ours = md5::compute(format!("{}{}", self.password, salt));
|
||||
let ours = format!("{ours:x}");
|
||||
|
||||
ours == hashed_password
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user(conn: DbConn, name: &str) -> Result<Option<User>> {
|
||||
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE name = $1", name)
|
||||
.fetch_optional(&*conn)
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
36
src/utils.rs
36
src/utils.rs
|
|
@ -1,36 +0,0 @@
|
|||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
authentication::Authentication,
|
||||
subsonic::{Error, SubsonicResponse},
|
||||
user::{get_user, User},
|
||||
};
|
||||
|
||||
use self::middleware::DbConn;
|
||||
|
||||
pub async fn verify_user(conn: DbConn, auth: Authentication) -> Result<User, SubsonicResponse> {
|
||||
let user = get_user(conn, &auth.username).await;
|
||||
|
||||
match user {
|
||||
Ok(Some(u)) => {
|
||||
if u.verify(&auth.token, &auth.salt) {
|
||||
Ok(u)
|
||||
} else {
|
||||
Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword(
|
||||
None,
|
||||
)))
|
||||
}
|
||||
}
|
||||
Ok(None) => Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword(
|
||||
None,
|
||||
))),
|
||||
Err(e) => {
|
||||
error!("Error getting user: {e}");
|
||||
Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword(
|
||||
None,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod middleware;
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use poem::{Endpoint, Middleware};
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub type DbConn = Arc<PgPool>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbConnectionMiddleware {
|
||||
db: PgPool,
|
||||
}
|
||||
|
||||
impl DbConnectionMiddleware {
|
||||
pub const fn new(db: PgPool) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Endpoint> Middleware<E> for DbConnectionMiddleware {
|
||||
type Output = DbConnectionMwEndpoint<E>;
|
||||
|
||||
fn transform(&self, ep: E) -> Self::Output {
|
||||
DbConnectionMwEndpoint {
|
||||
inner: ep,
|
||||
db: self.db.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbConnectionMwEndpoint<E> {
|
||||
inner: E,
|
||||
db: PgPool,
|
||||
}
|
||||
|
||||
#[poem::async_trait]
|
||||
impl<E: Endpoint> Endpoint for DbConnectionMwEndpoint<E> {
|
||||
type Output = E::Output;
|
||||
|
||||
async fn call(&self, mut req: poem::Request) -> Result<Self::Output, poem::Error> {
|
||||
let conn = Arc::new(self.db.clone());
|
||||
|
||||
req.extensions_mut().insert(conn);
|
||||
|
||||
self.inner.call(req).await
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue