Compare commits
3 Commits
a0c3f5b502
...
123a409e14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123a409e14 | ||
|
|
fcaf70b717 | ||
|
|
9bf9a2296e |
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"
|
||||||
150
README.md
150
README.md
@@ -1,3 +1,149 @@
|
|||||||
# soundsonic
|
# SoundSonic
|
||||||
|
|
||||||
open subsonic api implementation
|
A Rust-based music streaming server implementing the OpenSubsonic API protocol.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
SoundSonic is a lightweight music streaming server built with Rust that implements the [OpenSubsonic](https://opensubsonic.netlify.app/) API specification. It allows you to stream your music collection to any OpenSubsonic-compatible client.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **OpenSubsonic API Support**: Compatible with any client supporting the OpenSubsonic protocol
|
||||||
|
- **User Management**: Role-based access control with customizable user permissions
|
||||||
|
- **SQLite Database**: Lightweight local storage with Diesel ORM
|
||||||
|
- **RESTful API**: Clean, well-structured API endpoints
|
||||||
|
- **Async Runtime**: Built on Tokio for high performance
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
- **Rust** (Edition 2024)
|
||||||
|
- **Axum** - Web framework
|
||||||
|
- **Tokio** - Async runtime
|
||||||
|
- **Diesel** - ORM with SQLite
|
||||||
|
- **Serde** - Serialization/deserialization
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust (latest stable version)
|
||||||
|
- SQLite
|
||||||
|
- Diesel CLI (for database migrations)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd soundsonic
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set up the database URL:
|
||||||
|
```bash
|
||||||
|
export DATABASE_URL=database.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Or create a `.env` file:
|
||||||
|
```
|
||||||
|
DATABASE_URL=database.db
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run database migrations:
|
||||||
|
```bash
|
||||||
|
diesel migration run
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Build and run the server:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on `http://0.0.0.0:3311`
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### System
|
||||||
|
|
||||||
|
- `GET/POST /rest/getOpenSubsonicExtensions` - Get supported OpenSubsonic extensions
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
|
||||||
|
- `GET/POST /rest/createUser.view` - Create a new user with specified permissions
|
||||||
|
|
||||||
|
### Request/Response Format
|
||||||
|
|
||||||
|
All API responses follow the OpenSubsonic response format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"subsonic-response": {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.16.1",
|
||||||
|
"type": "SoundSonic",
|
||||||
|
"serverVersion": "1.16.1",
|
||||||
|
"openSubsonic": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Roles
|
||||||
|
|
||||||
|
Users can have the following permissions:
|
||||||
|
|
||||||
|
- `adminRole` - Administrative privileges
|
||||||
|
- `streamRole` - Stream music
|
||||||
|
- `downloadRole` - Download music
|
||||||
|
- `uploadRole` - Upload music
|
||||||
|
- `playlistRole` - Manage playlists
|
||||||
|
- `coverArtRole` - Manage cover art
|
||||||
|
- `commentRole` - Add comments
|
||||||
|
- `podcastRole` - Access podcasts
|
||||||
|
- `shareRole` - Share content
|
||||||
|
- `jukeboxRole` - Jukebox control
|
||||||
|
- `videoConversionRole` - Video conversion
|
||||||
|
- `settingsRole` - Change settings
|
||||||
|
- `ldapAuthenticated` - LDAP authentication
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The server can be configured via environment variables:
|
||||||
|
|
||||||
|
- `DATABASE_URL` - Path to the SQLite database file (required)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running in development mode:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for production:
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database schema changes:
|
||||||
|
```bash
|
||||||
|
diesel migration generate <migration_name>
|
||||||
|
diesel migration run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported OpenSubsonic Extensions
|
||||||
|
|
||||||
|
- `apiKeyAuthentication` - API key-based authentication
|
||||||
|
- `formPost` - Form-based POST requests
|
||||||
|
- `indexBasedQueue` - Index-based queue management
|
||||||
|
- `songLyrics` - Song lyrics support
|
||||||
|
- `transcodeOffset` - Transcoding offset support
|
||||||
|
- `transcoding` - Audio transcoding capabilities
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Your License Here]
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- [OpenSubsonic](https://opensubsonic.netlify.app/) - The API specification this server implements
|
||||||
|
- [Subsonic](http://www.subsonic.org/) - The original music streaming server
|
||||||
|
|||||||
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,
|
||||||
|
}
|
||||||
28
src/main.rs
Normal file
28
src/main.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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::routes::user_management_router::user_management_routs;
|
||||||
|
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().merge(user_management_routs()))
|
||||||
|
.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::OpenSubSonicResponses;
|
||||||
|
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<OpenSubSonicResponses>) {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(OpenSubSonicResponses::open_subsonic_extentions()),
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/routes/user_management_router.rs
Normal file
56
src/routes/user_management_router.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::{
|
||||||
|
db::models::NewUser,
|
||||||
|
schema::users,
|
||||||
|
state::AppState,
|
||||||
|
types::types::{OpenSubSonicResponses, OpenSubsonicErrorCode},
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
Json, Router,
|
||||||
|
extract::{Form, State, rejection::FormRejection},
|
||||||
|
http::StatusCode,
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, insert_into};
|
||||||
|
|
||||||
|
pub fn user_management_routs() -> Router<AppState> {
|
||||||
|
let userm_routs = Router::new().route("/createUser.view", get(create_user).post(create_user));
|
||||||
|
return userm_routs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_if_user_exist(user_name: &String, db_con: &AppState) -> bool {
|
||||||
|
let conn = &mut db_con.pool.get().unwrap();
|
||||||
|
let result = users::table
|
||||||
|
.select(users::id)
|
||||||
|
.filter(users::username.eq(user_name))
|
||||||
|
.first::<Option<i32>>(conn);
|
||||||
|
match result {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(
|
||||||
|
State(db_con): State<AppState>,
|
||||||
|
form: Result<Form<NewUser>, FormRejection>,
|
||||||
|
) -> (StatusCode, Json<OpenSubSonicResponses>) {
|
||||||
|
let params = if let Ok(Form(parsed)) = form {
|
||||||
|
parsed
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(OpenSubSonicResponses::open_subsonic_response()),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if check_if_user_exist(¶ms.username, &db_con) {
|
||||||
|
let response = OpenSubSonicResponses::open_subsonic_response_error(
|
||||||
|
OpenSubsonicErrorCode::WrongUsernameOrPassword,
|
||||||
|
);
|
||||||
|
return (StatusCode::CONFLICT, Json(response));
|
||||||
|
};
|
||||||
|
let conn = &mut db_con.pool.get().unwrap();
|
||||||
|
let _ = insert_into(users::table).values(params).execute(conn);
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(OpenSubSonicResponses::open_subsonic_response()),
|
||||||
|
)
|
||||||
|
}
|
||||||
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;
|
||||||
12
src/types/system.rs
Normal file
12
src/types/system.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct OpenSubSonicExtension {
|
||||||
|
pub name: String,
|
||||||
|
pub versions: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct OpenSubSonicExtensions {
|
||||||
|
pub openSubsonicExtensions: Vec<OpenSubSonicExtension>,
|
||||||
|
}
|
||||||
155
src/types/types.rs
Normal file
155
src/types/types.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::types::system::{OpenSubSonicExtension, OpenSubSonicExtensions};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum OpenSubsonicErrorCode {
|
||||||
|
Generic = 0,
|
||||||
|
MissingParameter = 10,
|
||||||
|
ClientMustUpgrade = 20,
|
||||||
|
ServerMustUpgrade = 30,
|
||||||
|
WrongUsernameOrPassword = 40,
|
||||||
|
TokenAuthNotSupportedForLdap = 41,
|
||||||
|
AuthMechanismNotSupported = 42,
|
||||||
|
ConflictingAuthMechanisms = 43,
|
||||||
|
InvalidApiKey = 44,
|
||||||
|
NotAuthorized = 50,
|
||||||
|
TrialExpired = 60,
|
||||||
|
NotFound = 70,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenSubsonicErrorCode {
|
||||||
|
pub const fn description(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Generic => "A generic error.",
|
||||||
|
Self::MissingParameter => "Required parameter is missing.",
|
||||||
|
Self::ClientMustUpgrade => {
|
||||||
|
"Incompatible Subsonic REST protocol version. Client must upgrade."
|
||||||
|
}
|
||||||
|
Self::ServerMustUpgrade => {
|
||||||
|
"Incompatible Subsonic REST protocol version. Server must upgrade."
|
||||||
|
}
|
||||||
|
Self::WrongUsernameOrPassword => "Wrong username or password.",
|
||||||
|
Self::TokenAuthNotSupportedForLdap => {
|
||||||
|
"Token authentication not supported for LDAP users."
|
||||||
|
}
|
||||||
|
Self::AuthMechanismNotSupported => "Provided authentication mechanism not supported.",
|
||||||
|
Self::ConflictingAuthMechanisms => {
|
||||||
|
"Multiple conflicting authentication mechanisms provided."
|
||||||
|
}
|
||||||
|
Self::InvalidApiKey => "Invalid API key.",
|
||||||
|
Self::NotAuthorized => "User is not authorized for the given operation.",
|
||||||
|
Self::TrialExpired => {
|
||||||
|
"The trial period for the Subsonic server is over. Please upgrade to Subsonic Premium."
|
||||||
|
}
|
||||||
|
Self::NotFound => "The requested data was not found.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BaseResponse<T> {
|
||||||
|
status: String,
|
||||||
|
version: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
type_name: String,
|
||||||
|
server_version: String,
|
||||||
|
open_subsonic: bool,
|
||||||
|
#[serde(flatten)]
|
||||||
|
data: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
code: u8,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum OpenSubSonicResponses {
|
||||||
|
OpenSubSonicResponseError {
|
||||||
|
#[serde(rename = "subsonic-response")]
|
||||||
|
subsonic_response: BaseResponse<ErrorResponse>,
|
||||||
|
},
|
||||||
|
OpenSubSonicResponse {
|
||||||
|
#[serde(rename = "subsonic-response")]
|
||||||
|
subsonic_response: BaseResponse<()>,
|
||||||
|
},
|
||||||
|
OpenSubSonicExtensions {
|
||||||
|
#[serde(rename = "subsonic-response")]
|
||||||
|
subsonic_response: BaseResponse<OpenSubSonicExtensions>,
|
||||||
|
// openSubSonicExtensions: Vec<OpenSubSonicExtension>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenSubSonicResponses {
|
||||||
|
fn base_response<T>(inner_response: Option<T>) -> BaseResponse<T> {
|
||||||
|
BaseResponse {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
version: "1.16.1".to_string(),
|
||||||
|
type_name: "SoundSonic".to_string(),
|
||||||
|
server_version: "1.16.1".to_string(),
|
||||||
|
open_subsonic: true,
|
||||||
|
data: inner_response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_subsonic_response_error(error_code: OpenSubsonicErrorCode) -> Self {
|
||||||
|
Self::OpenSubSonicResponseError {
|
||||||
|
subsonic_response: BaseResponse {
|
||||||
|
status: "failed".to_string(),
|
||||||
|
version: "1.16.1".to_string(),
|
||||||
|
type_name: "SoundSonic".to_string(),
|
||||||
|
server_version: "1.16.1".to_string(),
|
||||||
|
open_subsonic: true,
|
||||||
|
data: Some(ErrorResponse {
|
||||||
|
code: error_code as u8,
|
||||||
|
message: error_code.description().to_string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_subsonic_response() -> Self {
|
||||||
|
Self::OpenSubSonicResponse {
|
||||||
|
subsonic_response: Self::base_response(None::<()>),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_response: Self::base_response(Some(OpenSubSonicExtensions {
|
||||||
|
openSubsonicExtensions: extension,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user