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:
vaibhav
2026-02-11 04:21:56 +05:30
parent a0c3f5b502
commit 9bf9a2296e
20 changed files with 1424 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.env

1091
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View 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
View 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
View File

0
migrations/.keep Normal file
View File

View 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;

View 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
View File

@@ -0,0 +1 @@
edition = "2024"

1
src/db/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod models;

59
src/db/models.rs Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
pub mod system_router;
pub mod user_management_router;

View 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()),
)
}

View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
pub mod system;
pub mod types;

7
src/types/system.rs Normal file
View 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
View 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,
}
}
}