feat: Add initial project structure and dependencies
This commit introduces the foundational elements of the project: - `.gitignore` file to exclude build artifacts and environment variables. - `Cargo.lock` and `Cargo.toml` defining project dependencies and metadata. - `diesel.toml` for Diesel CLI configuration. - Initial migration files (`down.sql`, `up.sql`) for database schema setup. - `rustfmt.toml` for code formatting. - Basic module structure for database, routes, schema, state, and types. - `src/main.rs` with basic Axum server setup and dotenv loading. - `src/routes/system_router.rs` for basic API endpoint. - `src/state.rs` for managing application state and database connection pool. - Type definitions for Subsonic API responses and extensions.
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
.env
|
||||||
1091
Cargo.lock
generated
Normal file
1091
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "soundsonic"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
diesel = { version = "2.3.6", features = ["sqlite", "r2d2", "returning_clauses_for_sqlite_3_35"] }
|
||||||
|
# build libsqlite3 as part of the build process
|
||||||
|
# uncomment this line if you run into setup issues
|
||||||
|
# libsqlite3-sys = { version = "0.36", features = ["bundled"] }
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
axum = "0.8.8"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
|
tracing-subscriber = "0.3.22"
|
||||||
9
diesel.toml
Normal file
9
diesel.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# For documentation on how to configure this file,
|
||||||
|
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
|
[print_schema]
|
||||||
|
file = "src/schema.rs"
|
||||||
|
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||||
|
|
||||||
|
[migrations_directory]
|
||||||
|
dir = "migrations"
|
||||||
0
migrations/.diesel_lock
Normal file
0
migrations/.diesel_lock
Normal file
0
migrations/.keep
Normal file
0
migrations/.keep
Normal file
4
migrations/2026-02-09-205759-0000_init/down.sql
Normal file
4
migrations/2026-02-09-205759-0000_init/down.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
-- down.sql (Diesel)
|
||||||
|
DROP TABLE IF EXISTS user_music_folders;
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
28
migrations/2026-02-09-205759-0000_init/up.sql
Normal file
28
migrations/2026-02-09-205759-0000_init/up.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
ldapAuthenticated BOOLEAN NOT NULL DEFAULT 0 CHECK (ldapAuthenticated IN (0,1)),
|
||||||
|
adminRole BOOLEAN NOT NULL DEFAULT 0 CHECK (adminRole IN (0,1)),
|
||||||
|
settingsRole BOOLEAN NOT NULL DEFAULT 1 CHECK (settingsRole IN (0,1)),
|
||||||
|
streamRole BOOLEAN NOT NULL DEFAULT 1 CHECK (streamRole IN (0,1)),
|
||||||
|
jukeboxRole BOOLEAN NOT NULL DEFAULT 0 CHECK (jukeboxRole IN (0,1)),
|
||||||
|
downloadRole BOOLEAN NOT NULL DEFAULT 0 CHECK (downloadRole IN (0,1)),
|
||||||
|
uploadRole BOOLEAN NOT NULL DEFAULT 0 CHECK (uploadRole IN (0,1)),
|
||||||
|
playlistRole BOOLEAN NOT NULL DEFAULT 0 CHECK (playlistRole IN (0,1)),
|
||||||
|
coverArtRole BOOLEAN NOT NULL DEFAULT 0 CHECK (coverArtRole IN (0,1)),
|
||||||
|
commentRole BOOLEAN NOT NULL DEFAULT 0 CHECK (commentRole IN (0,1)),
|
||||||
|
podcastRole BOOLEAN NOT NULL DEFAULT 0 CHECK (podcastRole IN (0,1)),
|
||||||
|
shareRole BOOLEAN NOT NULL DEFAULT 0 CHECK (shareRole IN (0,1)),
|
||||||
|
videoConversionRole BOOLEAN NOT NULL DEFAULT 0 CHECK (videoConversionRole IN (0,1))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_music_folders (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
music_folder_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, music_folder_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
edition = "2024"
|
||||||
1
src/db/mod.rs
Normal file
1
src/db/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod models;
|
||||||
59
src/db/models.rs
Normal file
59
src/db/models.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::schema::{user_music_folders, users};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
||||||
|
#[diesel(table_name = users)]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub email: String,
|
||||||
|
|
||||||
|
pub ldapAuthenticated: bool,
|
||||||
|
pub adminRole: bool,
|
||||||
|
pub settingsRole: bool,
|
||||||
|
pub streamRole: bool,
|
||||||
|
pub jukeboxRole: bool,
|
||||||
|
pub downloadRole: bool,
|
||||||
|
pub uploadRole: bool,
|
||||||
|
pub playlistRole: bool,
|
||||||
|
pub coverArtRole: bool,
|
||||||
|
pub commentRole: bool,
|
||||||
|
pub podcastRole: bool,
|
||||||
|
pub shareRole: bool,
|
||||||
|
pub videoConversionRole: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Insertable)]
|
||||||
|
#[diesel(table_name = users)]
|
||||||
|
#[diesel(treat_none_as_default_value = true)]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct NewUser {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub email: String,
|
||||||
|
|
||||||
|
pub ldapAuthenticated: Option<bool>,
|
||||||
|
pub adminRole: Option<bool>,
|
||||||
|
pub settingsRole: Option<bool>,
|
||||||
|
pub streamRole: Option<bool>,
|
||||||
|
pub jukeboxRole: Option<bool>,
|
||||||
|
pub downloadRole: Option<bool>,
|
||||||
|
pub uploadRole: Option<bool>,
|
||||||
|
pub playlistRole: Option<bool>,
|
||||||
|
pub coverArtRole: Option<bool>,
|
||||||
|
pub commentRole: Option<bool>,
|
||||||
|
pub podcastRole: Option<bool>,
|
||||||
|
pub shareRole: Option<bool>,
|
||||||
|
pub videoConversionRole: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Queryable, Selectable, Insertable)]
|
||||||
|
#[diesel(table_name = user_music_folders)]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct UserMusicFolder {
|
||||||
|
pub user_id: i32,
|
||||||
|
pub music_folder_id: i32,
|
||||||
|
}
|
||||||
27
src/main.rs
Normal file
27
src/main.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
mod db;
|
||||||
|
mod routes;
|
||||||
|
mod schema;
|
||||||
|
mod state;
|
||||||
|
mod types;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use dotenvy::dotenv;
|
||||||
|
|
||||||
|
use crate::routes::system_router::system_routers;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
dotenv().ok();
|
||||||
|
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
|
let state = AppState::new(&database_url)
|
||||||
|
.unwrap_or_else(|_| panic!("Error creating pool for {}", database_url));
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let app = Router::new()
|
||||||
|
.nest("/rest", system_routers())
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3311").await.unwrap();
|
||||||
|
let _ = axum::serve(listener, app).await;
|
||||||
|
}
|
||||||
2
src/routes/mod.rs
Normal file
2
src/routes/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod system_router;
|
||||||
|
pub mod user_management_router;
|
||||||
17
src/routes/system_router.rs
Normal file
17
src/routes/system_router.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::state::AppState;
|
||||||
|
use crate::types::types::SubSonicResponses;
|
||||||
|
use axum::{Json, Router, http::StatusCode, routing::get};
|
||||||
|
|
||||||
|
pub fn system_routers() -> Router<AppState> {
|
||||||
|
Router::new().route(
|
||||||
|
"/getOpenSubsonicExtensions",
|
||||||
|
get(get_opensubsonic_extentions).post(get_opensubsonic_extentions),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_opensubsonic_extentions() -> (StatusCode, Json<SubSonicResponses>) {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(SubSonicResponses::open_subsonic_extentions()),
|
||||||
|
)
|
||||||
|
}
|
||||||
28
src/routes/user_management_router.rs
Normal file
28
src/routes/user_management_router.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use crate::{db::models::NewUser, schema::users, state::AppState};
|
||||||
|
use axum::{Json, Router, extract::{Form, Query, State}, http::StatusCode, response::IntoResponse, routing::{Route, get}};
|
||||||
|
use diesel::{RunQueryDsl, insert_into};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn user_management_routs() -> Router<AppState> {
|
||||||
|
let userm_routs = Router::new()
|
||||||
|
.route("/createUser.view", get())create_user))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(
|
||||||
|
query: Option<Query<NewUser>>,
|
||||||
|
form: Option<Form<NewUser>>,
|
||||||
|
State(db_con): State<AppState>
|
||||||
|
) -> {
|
||||||
|
let params = match (query, form) {
|
||||||
|
(Some(q), _) => q.0,
|
||||||
|
(_, Some(f)) => f.0,
|
||||||
|
_ => {
|
||||||
|
return ( StatusCode::BAD_REQUEST, Json(json!({ "error": "missing parameters" })))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
insert_into(users::table).values(params).execute(db_con);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
)),
|
||||||
|
)}
|
||||||
34
src/schema.rs
Normal file
34
src/schema.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
user_music_folders (user_id, music_folder_id) {
|
||||||
|
user_id -> Integer,
|
||||||
|
music_folder_id -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
users (id) {
|
||||||
|
id -> Nullable<Integer>,
|
||||||
|
username -> Text,
|
||||||
|
password -> Text,
|
||||||
|
email -> Text,
|
||||||
|
ldapAuthenticated -> Bool,
|
||||||
|
adminRole -> Bool,
|
||||||
|
settingsRole -> Bool,
|
||||||
|
streamRole -> Bool,
|
||||||
|
jukeboxRole -> Bool,
|
||||||
|
downloadRole -> Bool,
|
||||||
|
uploadRole -> Bool,
|
||||||
|
playlistRole -> Bool,
|
||||||
|
coverArtRole -> Bool,
|
||||||
|
commentRole -> Bool,
|
||||||
|
podcastRole -> Bool,
|
||||||
|
shareRole -> Bool,
|
||||||
|
videoConversionRole -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::joinable!(user_music_folders -> users (user_id));
|
||||||
|
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(user_music_folders, users,);
|
||||||
17
src/state.rs
Normal file
17
src/state.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use diesel::{
|
||||||
|
SqliteConnection,
|
||||||
|
r2d2::{ConnectionManager, Pool, PoolError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub pool: Pool<ConnectionManager<SqliteConnection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(database_url: &str) -> Result<Self, PoolError> {
|
||||||
|
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
|
||||||
|
let pool = Pool::builder().build(manager)?;
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/types/mod.rs
Normal file
2
src/types/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod system;
|
||||||
|
pub mod types;
|
||||||
7
src/types/system.rs
Normal file
7
src/types/system.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct OpenSubSonicExtension {
|
||||||
|
pub name: String,
|
||||||
|
pub versions: Vec<i8>,
|
||||||
|
}
|
||||||
79
src/types/types.rs
Normal file
79
src/types/types.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::types::system::OpenSubSonicExtension;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BaseResponse {
|
||||||
|
status: String,
|
||||||
|
version: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
type_name: String,
|
||||||
|
server_version: String,
|
||||||
|
open_subsonic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum SubSonicResponses {
|
||||||
|
SubSonicResponse {
|
||||||
|
#[serde(rename = "subsonic-response")]
|
||||||
|
subsonic_reponse: BaseResponse,
|
||||||
|
},
|
||||||
|
OpenSubSonicExtensions {
|
||||||
|
#[serde(rename = "subsonic-response")]
|
||||||
|
subsonic_reponse: BaseResponse,
|
||||||
|
openSubSonicExtensions: Vec<OpenSubSonicExtension>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubSonicResponses {
|
||||||
|
fn base_response() -> BaseResponse {
|
||||||
|
BaseResponse {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
version: "1.16.1".to_string(),
|
||||||
|
type_name: "SoundSonic".to_string(),
|
||||||
|
server_version: "1.".to_string(),
|
||||||
|
open_subsonic: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subsonic_response() -> Self {
|
||||||
|
Self::SubSonicResponse {
|
||||||
|
subsonic_reponse: Self::base_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_subsonic_extentions() -> Self {
|
||||||
|
let mut extension: Vec<OpenSubSonicExtension> = Vec::new();
|
||||||
|
extension.push(OpenSubSonicExtension {
|
||||||
|
name: "apiKeyAuthentication".to_string(),
|
||||||
|
versions: vec![1],
|
||||||
|
});
|
||||||
|
extension.push(OpenSubSonicExtension {
|
||||||
|
name: "formPost".to_string(),
|
||||||
|
versions: vec![1],
|
||||||
|
});
|
||||||
|
extension.push(OpenSubSonicExtension {
|
||||||
|
name: "indexBasedQueue".to_string(),
|
||||||
|
versions: vec![1],
|
||||||
|
});
|
||||||
|
extension.push(OpenSubSonicExtension {
|
||||||
|
name: "songLyrics".to_string(),
|
||||||
|
versions: vec![1],
|
||||||
|
});
|
||||||
|
extension.push(OpenSubSonicExtension {
|
||||||
|
name: "transcodeOffset".to_string(),
|
||||||
|
versions: vec![1],
|
||||||
|
});
|
||||||
|
extension.push(OpenSubSonicExtension {
|
||||||
|
name: "transcoding".to_string(),
|
||||||
|
versions: vec![1],
|
||||||
|
});
|
||||||
|
|
||||||
|
Self::OpenSubSonicExtensions {
|
||||||
|
subsonic_reponse: Self::base_response(),
|
||||||
|
openSubSonicExtensions: extension,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user