Compare commits

..

30 Commits

Author SHA1 Message Date
phoenix d273c801bf Update rust (#249)
* Updated rust version

* Updated rust in workflow

* Updating rust version in docker file

* Updating postgresql

* Updated crates

* cargo update

* Updated readme

* Version bump

* Warning fix
2026-04-05 18:13:49 -04:00
KD bcd960d9a1 tsk-245: Modify result when getting songs but no songs are present (#247)
* tsk-245: Modify result when getting songs but no songs are present

* tsk-245: Fixed endpoint indicating success when no content is found

* tsk-245: Version bump
2025-11-07 11:48:07 -05:00
KD dbbbf02acf tsk-244: Remove code causing migrations to be ran twice (#246)
* tsk-244: Remove code causing migrations to be ran twice

* tsk-244: Cleanup

* tsk-244: Version bump
2025-11-06 12:56:47 -05:00
KD 01b34c9848 tsk-230: Using UserClaims from icarus_models (#243)
* tsk-230: Using UserClaims from icarus_models

* tsk-230: Cleanup

* tsk-230: Version bump
2025-11-05 09:59:13 -05:00
KD 1b9bd4359c tsk-207: Move code elsewhere that is found in src/main.rs (#242)
* tsk-207: Move code elsewhere that is found in src/main.rs

* tsk-207: Some refactoring

* tsk-207: Moving file around

* tsk-207: Code formatting

* tsk-207: Version bump

* tsk-207: Test fix
2025-11-04 11:19:12 -05:00
KD d05502dd8e tsk-227: Change statement when evaluating multipart form when queueing song (#241)
* tsk-227: Change statement when evaluating multipart form when queueing song

* tsk-227: Version bump
2025-11-04 10:47:51 -05:00
KD 147a50c867 tsk-196: Moving SongQueue struct (#240)
* tsk-196: Moving SongQueue struct

* tsk-196: Code formatting

* tsk-196: Version bump
2025-11-03 13:56:18 -05:00
KD a80e21b918 tsk-204: Changing name of test payload function (#239)
* tsk-204: Changing name of test payload function

* tsk-204: Added more test payload functions

* tsk-204: Version bump
2025-11-03 13:41:56 -05:00
KD 731aae5240 tsk-206: Moving request functions to their own module in testing (#238)
* tsk-206: Moving request functions to their own module in testing

* tsk-206: Version bump

* tsk-206: Forgot some imports. Some cleanup

* tsk-206: Forgot some imports

* tsk-206: How did I miss these?

* tsk-206: Code formatting
2025-11-02 21:30:40 -05:00
KD 79e909bfc0 tsk-86: Postgresql version bump 18.0 (#237)
* tsk-86: Postgresql version bump 18.0

* tsk-186: Updated exposed port on docker

* tsk-186: cargo update

* tsk-186: Version bump
2025-11-02 17:03:14 -05:00
KD 7940527418 tsk-208: Moving config and endpoint code (#236)
* tsk-208: Moving config and endpoint code

* tsk-208: Code formatting

* tsk-208: Version bump

* tsk-208: Accidentally changed port
2025-11-01 14:57:40 -04:00
KD f859eaf0a2 tsk-205: Put functions within a util module (#235)
* tsk-205: Put functions within a util module

* tsk-205: Code formatting

* tsk-205: Version bump

* tsk-205: Code cleanup
2025-11-01 14:32:53 -04:00
KD 22080a2f29 tsk-201: Making content disposition more unique (#234)
* tsk-201: Making content disposition more unique

* tsk-201: Cleanup

* tsk-201: Version bump
2025-10-31 12:34:43 -04:00
KD c630cb84b5 tsk-200: Utilizing mime for Content type for fetching queued CoverArt data (#233)
* tsk-200: Utilizing mime for Content type for fetching queued CoverArt data

* tsk-200: Cleanup

* tsk-200: Version bump
2025-10-31 12:20:11 -04:00
KD ca38465f2d tsk-202: Make CoverArt filename random during creation (#232)
* tsk-202: Make downloaded CoverArt filename random

* tsk-202: Cleanup

* tsk-202: Version bump

* tsk-202: Version bump down
2025-10-30 16:48:26 -04:00
KD fd4e661b3c tsk-224: Save file_type to CoverArt (#231)
* tsk-224: Save file_type to CoverArt

* tsk-224: Updated migrations

* tsk-224: Code formatting

* tsk-224: Fix test

* tsk-224: Test fix

* tsk-224: Version bump

* tsk-224: Warning fix
2025-10-30 16:25:59 -04:00
KD 6995ed483c icarus_models version bump (#229)
* icarus_models version bump

* Fix test

* Build fixes

* Version bump

* Using standard version
2025-10-30 15:35:02 -04:00
KD 2cc5e99590 tsk-192: jsonwebtoken version bump (#228)
* tsk-192: jsonwebtoken version bump

* tsk-192: Added feature to jsonwebtoken

* tsk-192: Code cleanup

* tsk-192: cargo update

* tsk-192: Version bump
2025-10-29 16:40:17 -04:00
KD 09d7fa32c8 tsk-179: Modify status code of song queue endpoint (#226)
* tsk-179: Updating docs for endpoint status codes

* tsk-179: Updated status code

* tsk-179: Improved error handling

* tsk-179: Code formatting

* tsk-179: Version bump
2025-10-29 15:20:04 -04:00
KD 27ef175828 tsk-213: Identify audio file type in initial step when queueing (#225)
* tsk-213: Adding song check when queueing songs

* tsk-213: Added check for updating queued song

* tsk-213: Removed todo

* tsk-213: Code cleanup

* tsk-213: Warning fix

* tsk-213: Version bump

* tsk-213: Minor status code changes
2025-10-29 14:58:34 -04:00
KD 1871ebe40b tsk-203: Addressing hard coding when downloading coverart (#223)
* tsk-203: Addressing hard coding when downloading coverart

* tsk-203: Code formatting

* tsk-203: Version bump

* tsk-203: Make downloaded coverart file random

* tsk-203: Code formatting
2025-10-27 14:16:04 -04:00
KD 1a5a9fcaaf tsk-194: Make distinction in MetadataQueue endpoints (#222)
* tsk-194: Moving metadata module

* tsk-194: Build fix

* tsk-194: Name changes

* tsk-194: Code formatting

* tsk-194: Version bump

* tsk-194: Test fixes
2025-10-27 13:25:30 -04:00
KD 8b36bc8c49 tsk-218: Separate Song and SongQueue in the endpoints (#221)
* tsk-218: Added empty file

* tsk-218: Created song module in queue module

* tsk-218: Code formatting

* tsk-218: Version bump

* tsk-218: Cleanup

* tsk-218: Test fixes
2025-10-26 17:48:17 -04:00
KD 003e5b77c1 tsk-197: Rename download_flac() function (#220)
* tsk-197: Renamed function

* tsk-197: Version bump

* tsk-197: icarus_models version bump
2025-10-25 21:34:33 -04:00
KD 02d9ef013e tsk-199: Separate CoverArt queue endpoints (#219)
* tsk-199: Created queue module for queue endpoints

* tsk-199: Code formatting

* tsk-199: Test fixes

* tsk-199: Version bump

* tsk-199: Updated readme
2025-10-25 21:17:40 -04:00
KD 351b4acb1c tsk-211: Fix SQL error when checking queue (#217)
* tsk-211: Fixing error on getting queue item

* tsk-211: Some cleanup

* tsk-211: Cleanup

* tsk-211: Updated readme

* tsk-211: Version bump
2025-10-22 21:42:19 -04:00
KD 381dd49b56 tsk-209: Create repo module (#216)
* tsk-209: Refactored repo code

* tsk-209: Refactoring more code

* tsk-209: Code formatting

* tsk-209: Refactored metadata repo code

* tsk-209: Refactored the other code

* tsk-209: Code formatting

* tsk-209: Version bump

* tsk-209: Fix test
2025-10-22 20:37:15 -04:00
KD a9ba1c5193 tsk-212: Post fixes (#215)
* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: Got functionality working

* tsk-212: Migration changes

Added file_type to CoverArtQueue

* tsk-212: Add support to save file_type from CoverArtQueue

* tsk-212: Warning fix

* tsk-212: Version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta related changes

* tsk-212: Replaced code with constants

* tsk-212: icarus_meta version bump
2025-10-21 22:42:32 -04:00
KD fb360c8269 tsk-212: Identify type of CoverArt (#214)
* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: Got functionality working

* tsk-212: Migration changes

Added file_type to CoverArtQueue

* tsk-212: Add support to save file_type from CoverArtQueue

* tsk-212: Warning fix

* tsk-212: Version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta related changes

* tsk-212: Replaced code with constants
2025-10-21 22:24:52 -04:00
KD e461e78661 icarus_models version bump (#210)
* icarus_models version bump

* icarus_models related changes

* Code formatting

* Test changeS:

* Test changes

* Migration changes

* Migration changes

* Warning fix

* Cleanup

* Updated readme

* Removed line from migrations

* Code cleanup

* Version bump

* cargo update

* icarus_models version bump
2025-10-20 11:19:06 -04:00
32 changed files with 4087 additions and 3556 deletions
+2 -2
View File
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v5
with: with:
fetch-depth: 0 # Important for git describe --tags fetch-depth: 0 # Important for git describe --tags
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.90.0 toolchain: 1.94
components: cargo components: cargo
- name: Extract Version from Cargo.toml - name: Extract Version from Cargo.toml
+3 -3
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
services: services:
postgres: postgres:
image: postgres:17.5 image: postgres:18.3-alpine
env: env:
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER }} POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
@@ -27,10 +27,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Install Rust 1.90.0 - name: Install Rust 1.94
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.90.0 toolchain: 1.94
components: clippy, rustfmt components: clippy, rustfmt
override: true override: true
Generated
+718 -500
View File
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -1,35 +1,35 @@
[package] [package]
name = "icarus" name = "icarus"
version = "0.3.0" version = "0.4.0"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.94"
[dependencies] [dependencies]
axum = { version = "0.8.6", features = ["multipart"] } axum = { version = "0.8.8", features = ["multipart"] }
axum-extra = { version = "0.10.3", features = ["cookie"] } axum-extra = { version = "0.12.5", features = ["cookie"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.145" } serde_json = { version = "1.0.149" }
tower = { version = "0.5.2", features = ["full"] } tower = { version = "0.5.3", features = ["full"] }
tokio = { version = "1.47.1", features = ["full"] } tokio = { version = "1.51", features = ["full"] }
tokio-util = { version = "0.7.16", features = ["io"] } tokio-util = { version = "0.7.18", features = ["io"] }
tower-http = { version = "0.6.6", features = ["cors", "timeout"] } tower-http = { version = "0.6.8", features = ["cors", "timeout"] }
tracing-subscriber = "0.3.20" tracing-subscriber = "0.3.23"
futures = { version = "0.3.31" } futures = { version = "0.3.32" }
mime_guess = { version = "2.0.5" } mime_guess = { version = "2.0.5" }
uuid = { version = "1.18.1", features = ["v4", "serde"] } uuid = { version = "1.23", features = ["v4", "serde"] }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] }
time = { version = "0.3.44", features = ["formatting", "macros", "parsing", "serde"] } time = { version = "0.3.47", features = ["formatting", "macros", "parsing", "serde"] }
thiserror = "2.0.17" thiserror = "2.0.18"
base64 = "0.22.1" base64 = "0.22.1"
jsonwebtoken = { version = "9.3.1" } jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
josekit = { version = "0.10.3" } josekit = { version = "0.10.3" }
utoipa = { version = "5.4.0", features = ["axum_extras"] } utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.4.0" } icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.5.0" }
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.7.0" } icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.10.0" }
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.5.0" } icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.6.0" }
[dev-dependencies] [dev-dependencies]
common-multipart-rfc7578 = { version = "0.7.0" } common-multipart-rfc7578 = { version = "0.7.0" }
url = { version = "2.5.7" } url = { version = "2.5.8" }
tempfile = { version = "3.23.0" } tempfile = { version = "3.27.0" }
+2 -2
View File
@@ -1,5 +1,5 @@
# Stage 1: Build the application # Stage 1: Build the application
FROM rust:1.90 as builder FROM rust:1.94 as builder
# Set the working directory inside the container # Set the working directory inside the container
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -63,7 +63,7 @@ COPY --from=builder /usr/src/app/.env .
COPY --from=builder /usr/src/app/migrations ./migrations COPY --from=builder /usr/src/app/migrations ./migrations
# Expose the port your Axum app listens on (e.g., 3000 or 8000) # Expose the port your Axum app listens on (e.g., 3000 or 8000)
EXPOSE 3000 EXPOSE 8000
# Set the command to run your application # Set the command to run your application
# Ensure this matches the binary name copied above # Ensure this matches the binary name copied above
+6 -2
View File
@@ -3,8 +3,12 @@ Web API for the Icarus project.
### Requires ### Requires
`icarus_auth` +v0.6.0 `icarus_auth` v0.7.x
`songparser` +v0.4.1 `songparser` v0.5.x
### Compatible with
`icarus-dm` v0.9.x
## Getting Started ## Getting Started
Quickest way to get started is with docker. Make sure `icarus_auth` and `songparser` repositories Quickest way to get started is with docker. Make sure `icarus_auth` and `songparser` repositories
+2 -3
View File
@@ -9,7 +9,6 @@ services:
container_name: icarus # Optional: Give the container a specific name container_name: icarus # Optional: Give the container a specific name
ports: ports:
# Map host port 8000 to container port 3000 (adjust as needed) # Map host port 8000 to container port 3000 (adjust as needed)
# Format: "HOST_PORT:CONTAINER_PORT"
- "8000:8000" - "8000:8000"
env_file: env_file:
- .env - .env
@@ -66,7 +65,7 @@ services:
# PostgreSQL Database Service # PostgreSQL Database Service
# --- icarus web api db --- # --- icarus web api db ---
main_db: main_db:
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller) image: postgres:18.3-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: icarus_db # Optional: Give the container a specific name container_name: icarus_db # Optional: Give the container a specific name
environment: environment:
# These MUST match the user, password, and database name in the DATABASE_URL above # These MUST match the user, password, and database name in the DATABASE_URL above
@@ -92,7 +91,7 @@ services:
# --- icarus web auth api db --- # --- icarus web auth api db ---
auth_db: auth_db:
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller) image: postgres:18.3-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: icarus_auth_db # Optional: Give the container a specific name container_name: icarus_auth_db # Optional: Give the container a specific name
environment: environment:
# These MUST match the user, password, and database name in the DATABASE_URL above # These MUST match the user, password, and database name in the DATABASE_URL above
+4 -3
View File
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS "metadataQueue" (
CREATE TABLE IF NOT EXISTS "coverartQueue" ( CREATE TABLE IF NOT EXISTS "coverartQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
data BYTEA NULL, data BYTEA NULL,
file_type TEXT NOT NULL,
song_queue_id UUID NULL song_queue_id UUID NULL
); );
@@ -55,8 +56,8 @@ CREATE TABLE IF NOT EXISTS "song" (
CREATE TABLE IF NOT EXISTS "coverart" ( CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL, title TEXT NOT NULL,
-- TODO: Separate path later directory TEXT NOT NULL,
path TEXT NOT NULL, filename TEXT NOT NULL,
file_type TEXT NOT NULL,
song_id UUID NOT NULL song_id UUID NOT NULL
-- TODO: Add type later
); );
-116
View File
@@ -1,116 +0,0 @@
use axum::{
Json,
http::{Request, StatusCode},
middleware::Next,
response::IntoResponse,
};
use axum_extra::extract::cookie::CookieJar;
use jsonwebtoken::{DecodingKey, Validation, decode};
use serde::{Deserialize, Serialize};
use thiserror::Error;
fn deserialize_i64_from_f64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: serde::Deserializer<'de>,
{
let val = f64::deserialize(deserializer)?;
// Handle NaN and infinity cases
if val.is_nan() || val.is_infinite() {
return Err(serde::de::Error::custom("invalid float value"));
}
// Round to nearest integer and convert
let rounded = val.round();
// Check if the rounded value can fit in i64
if rounded < (i64::MIN as f64) || rounded > (i64::MAX as f64) {
return Err(serde::de::Error::custom("float out of i64 range"));
}
Ok(rounded as i64)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserClaims {
pub iss: String,
pub aud: String, // Audience
pub sub: String, // Subject (user ID)
#[serde(deserialize_with = "deserialize_i64_from_f64")]
pub exp: i64, // Expiration time (UTC timestamp)
#[serde(deserialize_with = "deserialize_i64_from_f64")]
pub iat: i64, // Issued at (UTC timestamp)
// pub azp: String,
// pub gty: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>, // Optional roles
}
#[derive(Error, Debug)]
pub enum JwtError {
#[error("Token creation failed")]
TokenCreation,
#[error("Token verification failed")]
TokenVerification,
#[error("Invalid token")]
InvalidToken,
#[error("Token expired")]
ExpiredToken,
#[error("Invalid key")]
InvalidKey,
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub status: &'static str,
pub message: String,
}
pub async fn auth<B>(
cookie_jar: CookieJar,
req: Request<axum::body::Body>,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
println!("Cookie: {cookie_jar:?}");
let token = cookie_jar
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
req.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|auth_header| auth_header.to_str().ok())
.and_then(|auth_value| {
println!("Auth value: {auth_value:?}");
auth_value.strip_prefix("Bearer ").map(String::from)
})
});
let token = token.ok_or_else(|| {
let json_error = ErrorResponse {
status: "fail",
message: "You are not logged in, please provide token".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?;
let secret_key = icarus_envy::environment::get_secret_main_key().await.value;
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_audience(&["icarus"]); // Must match exactly what's in the token
let claims = decode::<UserClaims>(
&token,
// TODO: Replace with code to get secret from env
&DecodingKey::from_secret(secret_key.as_ref()),
&validation,
)
.map_err(|err| {
eprintln!("Error: {err:?}");
let json_error = ErrorResponse {
status: "fail",
message: "Invalid token - Error decoding claims".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?
.claims;
println!("Claims: {claims:?}");
Ok(next.run(req).await)
}
+59
View File
@@ -0,0 +1,59 @@
use axum::{
Json,
http::{Request, StatusCode},
middleware::Next,
response::IntoResponse,
};
use axum_extra::extract::cookie::CookieJar;
use jsonwebtoken::{DecodingKey, Validation, decode};
#[derive(Debug, serde::Serialize)]
pub struct ErrorResponse {
pub status: &'static str,
pub message: String,
}
pub async fn auth<B>(
cookie_jar: CookieJar,
req: Request<axum::body::Body>,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
let token = cookie_jar
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
req.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|auth_header| auth_header.to_str().ok())
.and_then(|auth_value| auth_value.strip_prefix("Bearer ").map(String::from))
});
let token = token.ok_or_else(|| {
let json_error = ErrorResponse {
status: "fail",
message: "You are not logged in, please provide token".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?;
let secret_key = icarus_envy::environment::get_secret_main_key().await.value;
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_audience(&["icarus"]); // Must match exactly what's in the token
let _claims = decode::<icarus_models::token::UserClaims>(
&token,
&DecodingKey::from_secret(secret_key.as_ref()),
&validation,
)
.map_err(|err| {
eprintln!("Error: {err:?}");
let json_error = ErrorResponse {
status: "fail",
message: "Invalid token - Error decoding claims".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?
.claims;
Ok(next.run(req).await)
}
+69 -692
View File
@@ -1,48 +1,4 @@
// TODO: Separate queue and coverart endpoints
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct CoverArtQueue {
pub id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
pub mod request { pub mod request {
pub mod queue {
#[derive(utoipa::ToSchema)]
pub struct Request {
/// Filename
pub file: String,
#[schema(rename = "type")]
/// File type. Should be a file and not a value
pub file_type: String,
/// Raw data of the cover art file
pub value: Vec<u8>,
}
}
pub mod link {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
pub mod fetch_coverart_with_data {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
pub mod create_coverart { pub mod create_coverart {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
@@ -51,13 +7,6 @@ pub mod request {
} }
} }
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub coverart_queue_id: uuid::Uuid,
}
}
pub mod get_coverart { pub mod get_coverart {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params { pub struct Params {
@@ -67,42 +16,6 @@ pub mod request {
} }
pub mod response { pub mod response {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
pub mod link {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Id {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Id>,
}
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<super::super::CoverArtQueue>,
}
}
pub mod fetch_coverart_with_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Vec<u8>>,
}
}
pub mod create_coverart { pub mod create_coverart {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
@@ -111,14 +24,6 @@ pub mod response {
} }
} }
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod get_coverart { pub mod get_coverart {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
@@ -128,548 +33,11 @@ pub mod response {
} }
} }
pub mod db {
use sqlx::Row;
pub async fn insert(pool: &sqlx::PgPool, data: &Vec<u8>) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverartQueue" (data) VALUES($1) RETURNING id;
"#,
)
.bind(data)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e:?}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update(
pool: &sqlx::PgPool,
coverart_id: &uuid::Uuid,
song_queue_id: &uuid::Uuid,
) -> Result<i32, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "coverartQueue" SET song_queue_id = $1 WHERE id = $2;
"#,
)
.bind(song_queue_id)
.bind(coverart_id)
.execute(pool)
.await;
match result {
Ok(_) => Ok(0),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<super::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, song_queue_id FROM "coverartQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(super::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<super::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, song_queue_id FROM "coverartQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(super::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_data_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "coverartQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_data_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "coverartQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn wipe_data(
pool: &sqlx::PgPool,
coverart_queue_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "coverartQueue" SET data = NULL WHERE id = $1 RETURNING id;
"#,
)
.bind(coverart_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating query: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
}
pub mod cov_db {
use sqlx::Row;
pub async fn create(
pool: &sqlx::PgPool,
coverart: &icarus_models::coverart::CoverArt,
song_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverart" (title, path, song_id) VALUES($1, $2, $3) RETURNING id;
"#,
)
.bind(&coverart.title)
.bind(&coverart.path)
.bind(song_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, path, song_id FROM "coverart" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(icarus_models::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
path: row
.try_get("path")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_with_song_id(
pool: &sqlx::PgPool,
song_id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, path, song_id FROM "coverart" WHERE song_id = $1;
"#,
)
.bind(song_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(icarus_models::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
path: row
.try_get("path")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn delete_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
DELETE FROM "coverart"
WHERE id = $1
RETURNING id, title, path, song_id
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error deleting data: {e:?}");
});
match result {
Ok(row) => Ok(icarus_models::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
path: row
.try_get("path")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
}),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
}
pub mod endpoint { pub mod endpoint {
use axum::response::IntoResponse; use axum::response::IntoResponse;
/// Endpoint to queue cover art use crate::repo;
#[utoipa::path( use crate::repo::queue as repo_queue;
post,
path = super::super::endpoints::QUEUECOVERART,
request_body(
content = super::request::queue::Request,
),
responses(
(status = 200, description = "Successful", body = super::response::Response),
(status = 400, description = "Error queueing cover art", body = super::response::Response)
)
)]
pub async fn queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::Response>,
) {
let mut response = super::response::Response::default();
match multipart.next_field().await {
Ok(Some(field)) => {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = field.content_type().unwrap().to_string();
let data = field.bytes().await.unwrap();
let raw_data = data.to_vec();
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
file_name,
name,
content_type,
data.len()
);
match super::db::insert(&pool, &raw_data).await {
Ok(id) => {
response.message = String::from("Successful");
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Ok(None) => (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)),
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to link queued cover art
#[utoipa::path(
post,
path = super::super::endpoints::QUEUECOVERARTLINK,
request_body(
content = super::request::link::Request,
description = "Linking queued cover art to queued song",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued cover art linked", body = super::response::link::Response),
(status = 400, description = "Linkage failed", body = super::response::link::Response)
)
)]
pub async fn link(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::link::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::link::Response>,
) {
let mut response = super::response::link::Response::default();
let id = payload.coverart_id;
let song_id = payload.song_queue_id;
match super::db::update(&pool, &id, &song_id).await {
Ok(_o) => {
response.data.push(super::response::link::Id {
song_queue_id: song_id,
coverart_id: id,
});
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to fetch cover art details
#[utoipa::path(
get,
path = super::super::endpoints::QUEUECOVERART,
params(
("id" = uuid::Uuid, Path, description = "Queued cover art Id"),
("song_queue_id" = uuid::Uuid, Path, description = "Queued song Id")
),
responses(
(status = 200, description = "Queued song linked", body = super::response::fetch_coverart_no_data::Response),
(status = 400, description = "Linkage failed", body = super::response::fetch_coverart_no_data::Response)
)
)]
pub async fn fetch_coverart_no_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<
super::request::fetch_coverart_no_data::Params,
>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::fetch_coverart_no_data::Response>,
) {
let mut response = super::response::fetch_coverart_no_data::Response::default();
match params.id {
Some(id) => match super::db::get_coverart_queue_with_id(&pool, &id).await {
Ok(cover_art_queue) => {
response.message = String::from("Successful");
response.data.push(cover_art_queue);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
_ => match params.song_queue_id {
Some(song_queue_id) => {
match super::db::get_coverart_queue_with_song_queue_id(&pool, &song_queue_id)
.await
{
Ok(cover_art_queue) => {
response.message = String::from("Successful");
response.data.push(cover_art_queue);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
None => {
response.message = String::from("No valid id provided");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
}
}
/// Endpoint to fetch the queued cover art data
#[utoipa::path(
get,
path = super::super::endpoints::QUEUECOVERARTDATA,
params(("id" = uuid::Uuid, Path, description = "Queued cover art Id")),
responses(
(status = 200, description = "Queued cover art data", body = Vec<u8>),
(status = 400, description = "Error fetching queued cover art data", body = Vec<u8>)
)
)]
pub async fn fetch_coverart_with_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
match super::db::get_coverart_queue_data_with_id(&pool, &id).await {
Ok(data) => {
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
let headers = response.headers_mut();
// TODO: Address this hard coding for the coverart content type
headers.insert(axum::http::header::CONTENT_TYPE, "image".parse().unwrap());
// TODO: Make the conent disposition more dynamic
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{id}.jpg\"")
.parse()
.unwrap(),
);
(axum::http::StatusCode::OK, response)
}
Err(_err) => (
axum::http::StatusCode::BAD_REQUEST,
axum::response::Response::default(),
),
}
}
/// Endpoint to create cover art /// Endpoint to create cover art
#[utoipa::path( #[utoipa::path(
@@ -695,26 +63,48 @@ pub mod endpoint {
let mut response = super::response::create_coverart::Response::default(); let mut response = super::response::create_coverart::Response::default();
let id = payload.coverart_queue_id; let id = payload.coverart_queue_id;
match super::db::get_coverart_queue_data_with_id(&pool, &id).await { match repo_queue::coverart::get_coverart_queue_data_with_id(&pool, &id).await {
Ok(data) => { Ok(data) => {
let song_id = payload.song_id; let song_id = payload.song_id;
match crate::callers::song::song_db::get_song(&pool, &song_id).await { match crate::repo::song::get_song(&pool, &song_id).await {
Ok(song) => { Ok(song) => {
let directory = icarus_envy::environment::get_root_directory().await.value; let directory = icarus_envy::environment::get_root_directory().await.value;
let dir = std::path::Path::new(&directory); let file_type =
icarus_meta::detection::coverart::file_type_from_data(&data).unwrap();
let coverart_type = if file_type.file_type
== icarus_meta::detection::coverart::constants::JPEG_TYPE
{
icarus_models::types::CoverArtType::JpegExtension
} else if file_type.file_type
== icarus_meta::detection::coverart::constants::JPG_TYPE
{
icarus_models::types::CoverArtType::JpgExtension
} else if file_type.file_type
== icarus_meta::detection::coverart::constants::PNG_TYPE
{
icarus_models::types::CoverArtType::PngExtension
} else {
response.message = String::from("Invalid CoverArt type");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
};
let filename =
icarus_models::coverart::generate_filename(coverart_type, true)
.unwrap();
// TODO: Make this random and the file extension should not be hard coded
let filename = format!("{}-coverart.jpeg", &song.filename[..8]);
let save_path = dir.join(&filename);
let path = String::from(save_path.to_str().unwrap());
let mut coverart = let mut coverart =
icarus_models::coverart::init::init_coverart_only_path(path); icarus_models::coverart::init::init_coverart_dir_and_filename(
&directory, &filename,
);
coverart.title = song.album.clone(); coverart.title = song.album.clone();
coverart.file_type = file_type.file_type;
coverart.data = data; coverart.data = data;
match coverart.save_to_filesystem() { match coverart.save_to_filesystem() {
Ok(_) => { Ok(_) => {
match super::cov_db::create(&pool, &coverart, &song.id).await { match repo::coverart::create(&pool, &coverart, &song.id).await {
Ok(id) => { Ok(id) => {
coverart.song_id = song_id; coverart.song_id = song_id;
coverart.id = id; coverart.id = id;
@@ -753,50 +143,6 @@ pub mod endpoint {
} }
} }
/// Endpoint to wipe data from the cover art queue
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUECOVERARTDATAWIPE,
request_body(
content = super::request::wipe_data_from_coverart_queue::Request,
description = "Data required to wipe the data from the cover art queue",
content_type = "application/json"
),
responses(
(status = 200, description = "Data wiped from cover art queue", body = super::response::wipe_data_from_coverart_queue::Response),
(status = 400, description = "Error wiping the data", body = super::response::wipe_data_from_coverart_queue::Response),
(status = 404, description = "Cover art not found", body = super::response::wipe_data_from_coverart_queue::Response)
)
)]
pub async fn wipe_data_from_coverart_queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::wipe_data_from_coverart_queue::Response>,
) {
let mut response = super::response::wipe_data_from_coverart_queue::Response::default();
let coverart_queue_id = payload.coverart_queue_id;
match super::db::get_coverart_queue_with_id(&pool, &coverart_queue_id).await {
Ok(coverart_queue) => match super::db::wipe_data(&pool, &coverart_queue.id).await {
Ok(id) => {
response.message = String::from("Success");
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
}
/// Endpoint to get cover art with criteria /// Endpoint to get cover art with criteria
#[utoipa::path( #[utoipa::path(
get, get,
@@ -819,7 +165,7 @@ pub mod endpoint {
let mut response = super::response::get_coverart::Response::default(); let mut response = super::response::get_coverart::Response::default();
match params.id { match params.id {
Some(id) => match super::cov_db::get_coverart(&pool, &id).await { Some(id) => match repo::coverart::get_coverart(&pool, &id).await {
Ok(coverart) => { Ok(coverart) => {
response.data.push(coverart); response.data.push(coverart);
response.message = String::from(super::super::response::SUCCESSFUL); response.message = String::from(super::super::response::SUCCESSFUL);
@@ -853,20 +199,51 @@ pub mod endpoint {
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>, axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) { ) -> (axum::http::StatusCode, axum::response::Response) {
match super::cov_db::get_coverart(&pool, &id).await { match repo::coverart::get_coverart(&pool, &id).await {
Ok(coverart) => match icarus_models::coverart::io::to_data(&coverart) { Ok(coverart) => match icarus_models::coverart::io::to_data(&coverart) {
Ok(data) => { Ok(data) => {
let (file_type, img_type) =
match icarus_meta::detection::coverart::file_type_from_data(&data) {
Ok(file_type) => {
if file_type.file_type
== icarus_meta::detection::coverart::constants::JPEG_TYPE
{
(file_type, icarus_models::types::CoverArtType::JpegExtension)
} else if file_type.file_type
== icarus_meta::detection::coverart::constants::JPG_TYPE
{
(file_type, icarus_models::types::CoverArtType::JpgExtension)
} else if file_type.file_type
== icarus_meta::detection::coverart::constants::PNG_TYPE
{
(file_type, icarus_models::types::CoverArtType::PngExtension)
} else {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::response::Response::default(),
);
}
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::response::Response::default(),
);
}
};
let bytes = axum::body::Bytes::from(data); let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response(); let mut response = bytes.into_response();
let headers = response.headers_mut(); let headers = response.headers_mut();
// TODO: Address hard coding let filename =
icarus_models::coverart::generate_filename(img_type, true).unwrap();
headers.insert( headers.insert(
axum::http::header::CONTENT_TYPE, axum::http::header::CONTENT_TYPE,
"audio/jpg".parse().unwrap(), file_type.mime.parse().unwrap(),
); );
headers.insert( headers.insert(
axum::http::header::CONTENT_DISPOSITION, axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{id}.jpg\"") format!("attachment; filename=\"{filename}\"")
.parse() .parse()
.unwrap(), .unwrap(),
); );
-313
View File
@@ -1,313 +0,0 @@
// TODO: Explicitly make this module target queueing a song's metadata
pub mod request {
pub mod queue_metadata {
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub album: String,
pub album_artist: String,
pub artist: String,
pub disc: i32,
pub disc_count: i32,
pub duration: i64,
pub genre: String,
pub title: String,
pub track: i32,
pub track_count: i32,
pub year: i32,
}
impl Request {
pub async fn to_json_value(&self) -> serde_json::Value {
serde_json::json!(
{
"song_queue_id": &self.song_queue_id,
"album": &self.album,
"album_artist": &self.album_artist,
"genre": &self.genre,
"year": &self.year,
"track_count": &self.track_count,
"disc_count": &self.disc_count,
"title": &self.title,
"artist": &self.artist,
"disc": &self.disc,
"track": &self.track,
"duration": &self.duration,
})
}
}
}
pub mod fetch_metadata {
#[derive(
Debug,
Default,
serde::Deserialize,
serde::Serialize,
sqlx::FromRow,
sqlx::Decode,
utoipa::ToSchema,
)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
}
pub mod response {
pub mod queue_metadata {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod fetch_metadata {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<crate::callers::metadata::metadata_queue::MetadataQueue>,
}
}
}
pub mod metadata_queue {
use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct MetadataQueue {
pub id: uuid::Uuid,
pub metadata: serde_json::Value,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
pub song_queue_id: uuid::Uuid,
}
pub async fn insert(
pool: &sqlx::PgPool,
metadata: &serde_json::Value,
song_queue_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "metadataQueue" (metadata, song_queue_id) VALUES($1, $2) RETURNING id;
"#,
)
.bind(metadata)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<MetadataQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT * FROM "metadataQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => Ok(MetadataQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
metadata: row
.try_get("metadata")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
created_at: row
.try_get("created_at")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<MetadataQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, metadata, created_at, song_queue_id FROM "metadataQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let data: serde_json::Value = row
.try_get("metadata")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(MetadataQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
metadata: data,
created_at: row
.try_get("created_at")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
})
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
}
/// Module for metadata related endpoints
pub mod endpoint {
use axum::{Json, http::StatusCode};
/// Endpoint to create queued metadata
#[utoipa::path(
post,
path = super::super::endpoints::QUEUEMETADATA,
request_body(
content = super::request::queue_metadata::Request,
description = "Data required to create queued metadata",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued metadata created", body = super::response::queue_metadata::Response),
(status = 400, description = "Error creating queued metadata", body = super::response::queue_metadata::Response)
)
)]
pub async fn queue_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
Json(payload): Json<super::request::queue_metadata::Request>,
) -> (StatusCode, Json<super::response::queue_metadata::Response>) {
let mut results: Vec<uuid::Uuid> = Vec::new();
let mut response = super::response::queue_metadata::Response::default();
let meta = payload.to_json_value().await;
match super::metadata_queue::insert(&pool, &meta, &payload.song_queue_id).await {
Ok(metadata_queue_id) => {
results.push(metadata_queue_id);
response.data = results;
response.message = if response.data.is_empty() {
String::from("Error")
} else {
String::from(super::super::response::SUCCESSFUL)
};
(StatusCode::OK, Json(response))
}
Err(err) => {
response.message = err.to_string();
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
/// Endpoint to get queued metadata
#[utoipa::path(
get,
path = super::super::endpoints::QUEUEMETADATA,
params(
("id" = uuid::Uuid, Path, description = "Id of queued metadata"),
("song_queue_id" = uuid::Uuid, Path, description = "Id of queued song")
),
responses(
(status = 200, description = "Queued metadata retrieved", body = super::response::fetch_metadata::Response),
(status = 400, description = "Error retrieving queued metadata", body = super::response::fetch_metadata::Response)
)
)]
pub async fn fetch_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>,
) -> (StatusCode, Json<super::response::fetch_metadata::Response>) {
let mut response = super::response::fetch_metadata::Response::default();
match params.id {
Some(id) => {
println!("Something works {id}");
match super::metadata_queue::get_with_id(&pool, &id).await {
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(StatusCode::OK, Json(response))
}
Err(err) => {
response.message = err.to_string();
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
_ => match params.song_queue_id {
Some(song_queue_id) => {
println!("Song queue Id is probably not nil");
match super::metadata_queue::get_with_song_queue_id(&pool, &song_queue_id).await
{
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(StatusCode::OK, Json(response))
}
Err(err) => {
response.message = err.to_string();
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
None => {
println!("What is going on?");
(StatusCode::BAD_REQUEST, Json(response))
}
},
}
}
}
+7 -12
View File
@@ -1,19 +1,9 @@
pub mod coverart; pub mod coverart;
pub mod metadata; pub mod queue;
pub mod song; pub mod song;
pub mod endpoints { pub mod endpoints {
pub const QUEUESONG: &str = "/api/v2/song/queue"; pub const ROOT: &str = "/";
pub const QUEUESONGLINKUSERID: &str = "/api/v2/song/queue/link";
pub const QUEUESONGDATA: &str = "/api/v2/song/queue/{id}";
pub const QUEUESONGUPDATE: &str = "/api/v2/song/queue/{id}";
pub const NEXTQUEUESONG: &str = "/api/v2/song/queue/next";
pub const QUEUEMETADATA: &str = "/api/v2/song/metadata/queue";
pub const QUEUECOVERART: &str = "/api/v2/coverart/queue";
pub const QUEUECOVERARTDATA: &str = "/api/v2/coverart/queue/data/{id}";
pub const QUEUECOVERARTLINK: &str = "/api/v2/coverart/queue/link";
pub const QUEUESONGDATAWIPE: &str = "/api/v2/song/queue/data/wipe";
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
pub const CREATESONG: &str = "/api/v2/song"; pub const CREATESONG: &str = "/api/v2/song";
pub const GETSONGS: &str = "/api/v2/song"; pub const GETSONGS: &str = "/api/v2/song";
@@ -29,3 +19,8 @@ pub mod endpoints {
pub mod response { pub mod response {
pub const SUCCESSFUL: &str = "SUCCESSFUL"; pub const SUCCESSFUL: &str = "SUCCESSFUL";
} }
/// Basic handler that responds with a static string
pub async fn root() -> &'static str {
"Hello, World!"
}
+412
View File
@@ -0,0 +1,412 @@
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct CoverArtQueue {
pub id: uuid::Uuid,
pub file_type: String,
pub song_queue_id: uuid::Uuid,
}
pub mod request {
pub mod queue {
#[derive(utoipa::ToSchema)]
pub struct Request {
/// Filename
pub file: String,
#[schema(rename = "type")]
/// File type. Should be a file and not a value
pub file_type: String,
/// Raw data of the cover art file
pub value: Vec<u8>,
}
}
pub mod link {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
pub mod fetch_coverart_with_data {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub coverart_queue_id: uuid::Uuid,
}
}
}
pub mod response {
pub mod queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod link {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Id {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Id>,
}
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<super::super::CoverArtQueue>,
}
}
pub mod fetch_coverart_with_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Vec<u8>>,
}
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
}
mod helper {
pub fn is_coverart_file_type_valid(file_type: &String) -> bool {
let valid_file_types = vec![
String::from(icarus_meta::detection::coverart::constants::JPEG_TYPE),
String::from(icarus_meta::detection::coverart::constants::JPG_TYPE),
String::from(icarus_meta::detection::coverart::constants::PNG_TYPE),
];
for valid_file_type in valid_file_types {
if valid_file_type == *file_type {
return true;
}
}
false
}
}
pub mod endpoint {
use axum::response::IntoResponse;
use crate::repo::queue as repo;
/// Endpoint to queue cover art
#[utoipa::path(
post,
path = super::super::endpoints::QUEUECOVERART,
request_body(
content = super::request::queue::Request,
),
responses(
(status = 200, description = "Successful", body = super::response::queue::Response),
(status = 400, description = "Error queueing cover art", body = super::response::queue::Response)
)
)]
pub async fn queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::queue::Response>,
) {
let mut response = super::response::queue::Response::default();
match multipart.next_field().await {
Ok(Some(field)) => {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = field.content_type().unwrap().to_string();
let data = field.bytes().await.unwrap();
let raw_data = data.to_vec();
let file_type =
match icarus_meta::detection::coverart::file_type_from_data(&raw_data) {
Ok(file_type) => file_type,
Err(err) => {
eprintln!("Error: {err:?}");
response.message = err.to_string();
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
};
if !super::helper::is_coverart_file_type_valid(&file_type.file_type) {
response.message = format!("CoverArt file type not supported: {file_type:?}");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
} else {
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {}, file-type = {:?})",
file_name,
name,
content_type,
data.len(),
file_type
);
match repo::coverart::insert(&pool, &raw_data, &file_type.file_type).await {
Ok(id) => {
response.message = String::from("Successful");
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
}
Ok(None) => (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)),
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to link queued cover art
#[utoipa::path(
post,
path = super::super::endpoints::QUEUECOVERARTLINK,
request_body(
content = super::request::link::Request,
description = "Linking queued cover art to queued song",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued cover art linked", body = super::response::link::Response),
(status = 400, description = "Linkage failed", body = super::response::link::Response)
)
)]
pub async fn link(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::link::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::link::Response>,
) {
let mut response = super::response::link::Response::default();
let id = payload.coverart_id;
let song_id = payload.song_queue_id;
match repo::coverart::update(&pool, &id, &song_id).await {
Ok(_o) => {
response.data.push(super::response::link::Id {
song_queue_id: song_id,
coverart_id: id,
});
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to fetch cover art details
#[utoipa::path(
get,
path = super::super::endpoints::QUEUECOVERART,
params(
("id" = uuid::Uuid, Path, description = "Queued cover art Id"),
("song_queue_id" = uuid::Uuid, Path, description = "Queued song Id")
),
responses(
(status = 200, description = "Queued song linked", body = super::response::fetch_coverart_no_data::Response),
(status = 400, description = "Linkage failed", body = super::response::fetch_coverart_no_data::Response)
)
)]
pub async fn fetch_coverart_no_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<
super::request::fetch_coverart_no_data::Params,
>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::fetch_coverart_no_data::Response>,
) {
let mut response = super::response::fetch_coverart_no_data::Response::default();
match params.id {
Some(id) => match repo::coverart::get_coverart_queue_with_id(&pool, &id).await {
Ok(cover_art_queue) => {
response.message = String::from("Successful");
response.data.push(cover_art_queue);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
_ => match params.song_queue_id {
Some(song_queue_id) => match repo::coverart::get_coverart_queue_with_song_queue_id(
&pool,
&song_queue_id,
)
.await
{
Ok(cover_art_queue) => {
response.message = String::from("Successful");
response.data.push(cover_art_queue);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
None => {
response.message = String::from("No valid id provided");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
}
}
/// Endpoint to fetch the queued cover art data
#[utoipa::path(
get,
path = super::super::endpoints::QUEUECOVERARTDATA,
params(("id" = uuid::Uuid, Path, description = "Queued cover art Id")),
responses(
(status = 200, description = "Queued cover art data", body = Vec<u8>),
(status = 400, description = "Error fetching queued cover art data", body = Vec<u8>)
)
)]
pub async fn fetch_coverart_with_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
match repo::coverart::get_coverart_queue_data_with_id(&pool, &id).await {
Ok(data) => {
let file_type =
icarus_meta::detection::coverart::file_type_from_data(&data).unwrap();
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
let headers = response.headers_mut();
headers.insert(
axum::http::header::CONTENT_TYPE,
file_type.mime.parse().unwrap(),
);
let coverart_type = if file_type.file_type
== icarus_meta::detection::coverart::constants::JPEG_TYPE
{
icarus_models::types::CoverArtType::JpegExtension
} else if file_type.file_type
== icarus_meta::detection::coverart::constants::JPG_TYPE
{
icarus_models::types::CoverArtType::JpgExtension
} else if file_type.file_type
== icarus_meta::detection::coverart::constants::PNG_TYPE
{
icarus_models::types::CoverArtType::PngExtension
} else {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::response::Response::default(),
);
};
let filename =
icarus_models::coverart::generate_filename(coverart_type, true).unwrap();
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{filename}\"")
.parse()
.unwrap(),
);
(axum::http::StatusCode::OK, response)
}
Err(_err) => (
axum::http::StatusCode::BAD_REQUEST,
axum::response::Response::default(),
),
}
}
/// Endpoint to wipe data from the cover art queue
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUECOVERARTDATAWIPE,
request_body(
content = super::request::wipe_data_from_coverart_queue::Request,
description = "Data required to wipe the data from the cover art queue",
content_type = "application/json"
),
responses(
(status = 200, description = "Data wiped from cover art queue", body = super::response::wipe_data_from_coverart_queue::Response),
(status = 400, description = "Error wiping the data", body = super::response::wipe_data_from_coverart_queue::Response),
(status = 404, description = "Cover art not found", body = super::response::wipe_data_from_coverart_queue::Response)
)
)]
pub async fn wipe_data_from_coverart_queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::wipe_data_from_coverart_queue::Response>,
) {
let mut response = super::response::wipe_data_from_coverart_queue::Response::default();
let coverart_queue_id = payload.coverart_queue_id;
match repo::coverart::get_coverart_queue_with_id(&pool, &coverart_queue_id).await {
Ok(coverart_queue) => {
match repo::coverart::wipe_data(&pool, &coverart_queue.id).await {
Ok(id) => {
response.message = String::from("Success");
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
pub mod request {
pub mod queue_metadata {
#[derive(
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema,
)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub album: String,
pub album_artist: String,
pub artist: String,
pub disc: i32,
pub disc_count: i32,
pub duration: i64,
pub genre: String,
pub title: String,
pub track: i32,
pub track_count: i32,
pub year: i32,
}
impl Request {
pub async fn to_json_value(&self) -> serde_json::Value {
serde_json::json!(
{
"song_queue_id": &self.song_queue_id,
"album": &self.album,
"album_artist": &self.album_artist,
"genre": &self.genre,
"year": &self.year,
"track_count": &self.track_count,
"disc_count": &self.disc_count,
"title": &self.title,
"artist": &self.artist,
"disc": &self.disc,
"track": &self.track,
"duration": &self.duration,
})
}
}
}
pub mod fetch_metadata {
#[derive(
Debug,
Default,
serde::Deserialize,
serde::Serialize,
sqlx::FromRow,
sqlx::Decode,
utoipa::ToSchema,
)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
}
pub mod response {
pub mod queue_metadata {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod fetch_metadata {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<crate::repo::queue::metadata::MetadataQueue>,
}
}
}
/// Module for metadata related endpoints
pub mod endpoint {
use crate::repo::queue as repo_queue;
/// Endpoint to create queued metadata
#[utoipa::path(
post,
path = super::super::endpoints::QUEUEMETADATA,
request_body(
content = super::request::queue_metadata::Request,
description = "Data required to create queued metadata",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued metadata created", body = super::response::queue_metadata::Response),
(status = 400, description = "Error creating queued metadata", body = super::response::queue_metadata::Response)
)
)]
pub async fn queue_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::queue_metadata::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::queue_metadata::Response>,
) {
let mut results: Vec<uuid::Uuid> = Vec::new();
let mut response = super::response::queue_metadata::Response::default();
let meta = payload.to_json_value().await;
match repo_queue::metadata::insert(&pool, &meta, &payload.song_queue_id).await {
Ok(metadata_queue_id) => {
results.push(metadata_queue_id);
response.data = results;
response.message = if response.data.is_empty() {
String::from("Error")
} else {
String::from(super::super::super::response::SUCCESSFUL)
};
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to get queued metadata
#[utoipa::path(
get,
path = super::super::endpoints::QUEUEMETADATA,
params(
("id" = uuid::Uuid, Path, description = "Id of queued metadata"),
("song_queue_id" = uuid::Uuid, Path, description = "Id of queued song")
),
responses(
(status = 200, description = "Queued metadata retrieved", body = super::response::fetch_metadata::Response),
(status = 400, description = "Error retrieving queued metadata", body = super::response::fetch_metadata::Response)
)
)]
pub async fn fetch_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::fetch_metadata::Response>,
) {
let mut response = super::response::fetch_metadata::Response::default();
match params.id {
Some(id) => {
println!("Something works {id}");
match repo_queue::metadata::get_with_id(&pool, &id).await {
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
_ => match params.song_queue_id {
Some(song_queue_id) => {
println!("Song queue Id is probably not nil");
match repo_queue::metadata::get_with_song_queue_id(&pool, &song_queue_id).await
{
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
None => {
println!("What is going on?");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
}
}
}
+17
View File
@@ -0,0 +1,17 @@
pub mod coverart;
pub mod metadata;
pub mod song;
pub mod endpoints {
pub const QUEUESONG: &str = "/api/v2/song/queue";
pub const QUEUESONGLINKUSERID: &str = "/api/v2/song/queue/link";
pub const QUEUESONGDATA: &str = "/api/v2/song/queue/{id}";
pub const QUEUESONGUPDATE: &str = "/api/v2/song/queue/{id}";
pub const NEXTQUEUESONG: &str = "/api/v2/song/queue/next";
pub const QUEUEMETADATA: &str = "/api/v2/song/metadata/queue";
pub const QUEUECOVERART: &str = "/api/v2/coverart/queue";
pub const QUEUECOVERARTDATA: &str = "/api/v2/coverart/queue/data/{id}";
pub const QUEUECOVERARTLINK: &str = "/api/v2/coverart/queue/link";
pub const QUEUESONGDATAWIPE: &str = "/api/v2/song/queue/data/wipe";
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
}
+517
View File
@@ -0,0 +1,517 @@
pub mod request {
pub mod song_queue {
#[derive(utoipa::ToSchema)]
pub struct SongQueueRequest {
/// Filename
pub file: String,
#[schema(rename = "type")]
/// File type. Should be a file and not a value
pub file_type: String,
/// Raw data of the flac file
pub value: Vec<u8>,
}
}
pub mod update_status {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub id: uuid::Uuid,
pub status: String,
}
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
}
}
pub mod link_user_id {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub user_id: uuid::Uuid,
}
}
}
pub mod response {
pub mod song_queue {
/// Song queue response
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
/// Id of the queued song
pub data: Vec<uuid::Uuid>,
}
}
pub mod fetch_queue_song {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<crate::repo::queue::song::dbtype::SongQueue>,
}
}
pub mod update_status {
#[derive(serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct ChangedStatus {
pub old_status: String,
pub new_status: String,
}
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<ChangedStatus>,
}
}
pub mod update_song_queue {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod link_user_id {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
}
pub async fn is_song_valid(data: &[u8]) -> Result<bool, std::io::Error> {
match icarus_meta::detection::song::file_type_from_data(data) {
Ok(file_type) => {
if file_type.file_type == icarus_meta::detection::song::constants::FLAC_TYPE {
Ok(true)
} else {
Ok(false)
}
}
Err(err) => Err(err),
}
}
pub mod endpoint {
use axum::response::IntoResponse;
use crate::repo::queue as repo;
/// Endpoint to queue a song. Starts the process and places the song in a queue
#[utoipa::path(
post,
path = super::super::endpoints::QUEUESONG,
request_body(
content = super::request::song_queue::SongQueueRequest,
description = "Multipart form data for uploading song",
content_type = "multipart/form-data"
),
responses(
(status = 201, description = "Song queued", body = super::response::song_queue::Response),
(status = 400, description = "Invalid request passed", body = super::response::song_queue::Response),
(status = 500, description = "Error queueing song", body = super::response::song_queue::Response)
)
)]
pub async fn queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::song_queue::Response>,
) {
let mut results: Vec<uuid::Uuid> = Vec::new();
let mut response = super::response::song_queue::Response::default();
match multipart.next_field().await {
Ok(multipart_field) => {
if let Some(field) = multipart_field {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = field.content_type().unwrap().to_string();
let data = field.bytes().await.unwrap();
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
file_name,
name,
content_type,
data.len()
);
let raw_data: Vec<u8> = data.to_vec();
match super::is_song_valid(&raw_data).await {
Ok(valid) => {
if valid {
match repo::song::insert(
&pool,
&raw_data,
&file_name,
&crate::repo::queue::song::status::PENDING.to_string(),
)
.await
{
Ok(queued_song) => {
results.push(queued_song);
}
Err(err) => {
response.message = err.to_string();
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
}
} else {
response.message = String::from("Invalid song type");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
}
Err(err) => {
response.message = err.to_string();
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
}
response.data = results;
response.message = if response.data.is_empty() {
String::from("Error")
} else {
String::from(super::super::super::response::SUCCESSFUL)
};
(axum::http::StatusCode::CREATED, axum::Json(response))
} else {
response.message = String::from("No field found in multipart");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to link a user id to a queued song
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONGLINKUSERID,
request_body(
content = super::request::link_user_id::Request,
description = "User Id and queued song id",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued song linked", body = super::response::link_user_id::Response),
(status = 400, description = "Linkage failed", body = super::response::link_user_id::Response)
)
)]
pub async fn link_user_id(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::link_user_id::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::link_user_id::Response>,
) {
let mut response = super::response::link_user_id::Response::default();
match repo::song::get_song_queue(&pool, &payload.song_queue_id).await {
Ok(song_queue) => {
match repo::song::link_user_id(&pool, &song_queue.id, &payload.user_id).await {
Ok(user_id) => {
response.message = String::from(crate::callers::response::SUCCESSFUL);
response.data.push(user_id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to fetch the next queued song as long as it is available
#[utoipa::path(
get,
path = super::super::endpoints::NEXTQUEUESONG,
responses(
(status = 200, description = "Queued song is present and available", body = super::response::fetch_queue_song::Response),
(status = 400, description = "Linkage failed", body = super::response::fetch_queue_song::Response)
)
)]
pub async fn fetch_queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::fetch_queue_song::Response>,
) {
let mut response = super::response::fetch_queue_song::Response::default();
match repo::song::get_most_recent_and_update(&pool).await {
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to download the queued song
#[utoipa::path(
get,
path = super::super::endpoints::QUEUESONGDATA,
params(("id" = uuid::Uuid, Path, description = "Queued song Id")),
responses(
(status = 200, description = "Queued song linked", body = Vec<u8>),
(status = 400, description = "Linkage failed", body = Vec<u8>)
)
)]
pub async fn download_queued_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
println!("Id: {id}");
match repo::song::get_data(&pool, &id).await {
Ok(data) => {
let by = axum::body::Bytes::from(data);
let mut response = by.into_response();
let headers = response.headers_mut();
headers.insert(
axum::http::header::CONTENT_TYPE,
"audio/flac".parse().unwrap(),
);
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{id}.flac\"")
.parse()
.unwrap(),
);
(axum::http::StatusCode::OK, response)
}
Err(_err) => (
axum::http::StatusCode::BAD_REQUEST,
axum::response::Response::default(),
),
}
}
/// Endpoint to update the status of a queued song
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONG,
request_body(
content = super::request::update_status::Request,
description = "Update the status of a queued song",
content_type = "application/json"
),
responses(
(status = 200, description = "Status has been updated", body = super::response::update_status::Response),
(status = 400, description = "Error updating status of queued song", body = super::response::update_status::Response)
)
)]
pub async fn update_song_queue_status(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::update_status::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::update_status::Response>,
) {
let mut response = super::response::update_status::Response::default();
if repo::song::status::is_valid(&payload.status).await {
let id = payload.id;
if !id.is_nil() {
match repo::song::get_status_of_song_queue(&pool, &id).await {
Ok(old) => {
match repo::song::update_song_queue_status(&pool, &payload.status, &id)
.await
{
Ok(new) => {
response.message = String::from("Successful");
response
.data
.push(super::response::update_status::ChangedStatus {
old_status: old,
new_status: new,
});
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("Id is nil");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
} else {
response.message = String::from("Status not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
/// Endpoint to update the queued song data
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONGUPDATE,
request_body(
content = super::request::song_queue::SongQueueRequest,
description = "Multipart form data for uploading song",
content_type = "multipart/form-data"
),
params(("id" = uuid::Uuid, Path, description = "Queued song Id")),
responses(
(status = 200, description = "Queued song updated", body = super::response::update_song_queue::Response),
(status = 400, description = "Error updating queued song", body = super::response::update_song_queue::Response),
(status = 404, description = "Queued song not found", body = super::response::update_song_queue::Response),
(status = 500, description = "Error updating queued song", body = super::response::update_song_queue::Response)
)
)]
pub async fn update_song_queue(
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::update_song_queue::Response>,
) {
let mut response = super::response::update_song_queue::Response::default();
if let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = match field.content_type() {
Some(ct) => ct.to_string(),
None => String::new(),
};
let data = field.bytes().await.unwrap();
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
file_name,
name,
content_type,
data.len()
);
let raw_data: Vec<u8> = data.to_vec();
match super::is_song_valid(&raw_data).await {
Ok(valid) => {
if valid {
match repo::song::update(&pool, &raw_data, &id).await {
Ok(_) => {
response.message =
String::from(super::super::super::response::SUCCESSFUL);
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("Invalid song type");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
Err(err) => {
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("No data provided");
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
/// Endpoint to wipe the data from a queued song
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONGDATAWIPE,
request_body(
content = super::request::wipe_data_from_song_queue::Request,
description = "Pass the queued song Id to wipe the data",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued song data wiped", body = super::response::wipe_data_from_song_queue::Response),
(status = 404, description = "Queued song cannot be found", body = super::response::wipe_data_from_song_queue::Response)
)
)]
pub async fn wipe_data_from_song_queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::wipe_data_from_song_queue::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::wipe_data_from_song_queue::Response>,
) {
let mut response = super::response::wipe_data_from_song_queue::Response::default();
let id = payload.song_queue_id;
match repo::song::get_song_queue(&pool, &id).await {
Ok(song_queue) => match repo::song::wipe_data(&pool, &song_queue.id).await {
Ok(wiped_id) => {
response.message = String::from("Success");
response.data.push(wiped_id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
}
}
+61 -1163
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
/// Full hosting address that the application will be broadcasting
pub fn get_full() -> String {
format!("{ADDRESS}:{PORT}")
}
pub const ADDRESS: &str = "0.0.0.0";
pub const PORT: &str = "8000";
+257
View File
@@ -0,0 +1,257 @@
use std::time::Duration;
use axum::routing::{delete, get, patch, post};
use tower_http::timeout::TimeoutLayer;
use utoipa::OpenApi;
use crate::callers::coverart as coverart_caller;
use crate::callers::queue::coverart as coverart_queue_callers;
use crate::callers::queue::metadata as metadata_queue_caller;
use crate::callers::queue::song as song_queue_callers;
use crate::callers::song as song_caller;
use coverart_caller::endpoint as coverart_endpoints;
use coverart_caller::response as coverart_responses;
use coverart_queue_callers::endpoint as coverart_queue_endpoints;
use coverart_queue_callers::response as coverart_queue_responses;
use metadata_queue_caller::endpoint as metadata_queue_endpoints;
use metadata_queue_caller::response as metadata_queue_responses;
use song_caller::endpoint as song_endpoints;
use song_caller::response as song_responses;
use song_queue_callers::endpoint as song_queue_endpoints;
use song_queue_callers::response as song_queue_responses;
mod cors {
pub async fn configure_cors() -> tower_http::cors::CorsLayer {
let cors = tower_http::cors::CorsLayer::new()
.allow_methods([
axum::http::Method::GET,
axum::http::Method::POST,
axum::http::Method::PUT,
axum::http::Method::DELETE,
]) // Specify allowed methods:cite[2]
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
]) // Specify allowed headers:cite[2]
.allow_credentials(true) // If you need to send cookies or authentication headers:cite[2]
.max_age(std::time::Duration::from_secs(3600)); // Cache the preflight response for 1 hour:cite[2]
// Dynamically set the allowed origin based on the environment
match std::env::var(icarus_envy::keys::APP_ENV).as_deref() {
Ok("production") => {
// In production, allow only your specific, trusted origins
let allowed_origins_env = icarus_envy::environment::get_allowed_origins().await;
match icarus_envy::utility::delimitize(&allowed_origins_env) {
Ok(alwd) => {
let allowed_origins: Vec<axum::http::HeaderValue> = alwd
.into_iter()
.map(|a| a.parse::<axum::http::HeaderValue>().unwrap())
.collect();
cors.allow_origin(allowed_origins)
}
Err(err) => {
eprintln!("Error getting allowed origins: Error: {err:?}");
std::process::exit(-1);
}
}
}
_ => {
// Development (default): Allow localhost origins
cors.allow_origin(vec![
"http://localhost:8000".parse().unwrap(),
"http://127.0.0.1:8000".parse().unwrap(),
"http://localhost:4200".parse().unwrap(),
"http://127.0.0.1:4200".parse().unwrap(),
])
}
}
}
}
#[derive(utoipa::OpenApi)]
#[openapi(
paths(song_queue_endpoints::queue_song, song_queue_endpoints::link_user_id, song_queue_endpoints::fetch_queue_song, song_queue_endpoints::download_queued_song,
song_queue_endpoints::update_song_queue_status, song_queue_endpoints::update_song_queue, song_endpoints::create_metadata, song_queue_endpoints::wipe_data_from_song_queue, song_endpoints::get_songs, song_endpoints::get_all_songs, song_endpoints::stream_song, song_endpoints::download_song,
song_endpoints::delete_song, coverart_queue_endpoints::queue, coverart_queue_endpoints::link, coverart_queue_endpoints::fetch_coverart_no_data,
coverart_queue_endpoints::fetch_coverart_with_data, coverart_endpoints::create_coverart, coverart_queue_endpoints::wipe_data_from_coverart_queue,
coverart_endpoints::get_coverart, coverart_endpoints::download_coverart,
metadata_queue_endpoints::queue_metadata, metadata_queue_endpoints::fetch_metadata),
components(schemas(song_queue_callers::response::song_queue::Response, song_queue_callers::response::link_user_id::Response, song_queue_callers::response::fetch_queue_song::Response,
song_queue_responses::update_status::Response, song_queue_callers::response::update_song_queue::Response, song_responses::create_metadata::Response,
song_queue_callers::response::wipe_data_from_song_queue::Response, song_responses::get_songs::Response, song_responses::delete_song::Response,
coverart_queue_responses::queue::Response, coverart_queue_responses::link::Response, coverart_queue_responses::fetch_coverart_no_data::Response,
coverart_queue_responses::fetch_coverart_with_data::Response, coverart_responses::create_coverart::Response,
coverart_queue_responses::wipe_data_from_coverart_queue::Response, coverart_responses::get_coverart::Response,
metadata_queue_responses::queue_metadata::Response, metadata_queue_responses::fetch_metadata::Response)),
tags(
(name = "Icarus API", description = "Web API to manage music")
)
)]
struct ApiDoc;
pub async fn routes() -> axum::Router {
axum::Router::new()
.route(crate::callers::endpoints::ROOT, get(crate::callers::root))
.route(
crate::callers::queue::endpoints::QUEUESONG,
post(crate::callers::queue::song::endpoint::queue_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONG,
patch(crate::callers::queue::song::endpoint::update_song_queue_status).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGLINKUSERID,
patch(crate::callers::queue::song::endpoint::link_user_id).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGDATA,
get(crate::callers::queue::song::endpoint::download_queued_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::NEXTQUEUESONG,
get(crate::callers::queue::song::endpoint::fetch_queue_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGUPDATE,
patch(crate::callers::queue::song::endpoint::update_song_queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGDATAWIPE,
patch(crate::callers::queue::song::endpoint::wipe_data_from_song_queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUEMETADATA,
post(crate::callers::queue::metadata::endpoint::queue_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUEMETADATA,
get(crate::callers::queue::metadata::endpoint::fetch_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERART,
post(crate::callers::queue::coverart::endpoint::queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERARTDATA,
get(crate::callers::queue::coverart::endpoint::fetch_coverart_with_data).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERART,
get(crate::callers::queue::coverart::endpoint::fetch_coverart_no_data).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERARTLINK,
patch(crate::callers::queue::coverart::endpoint::link).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERARTDATAWIPE,
patch(crate::callers::queue::coverart::endpoint::wipe_data_from_coverart_queue)
.route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::callers::endpoints::CREATESONG,
post(crate::callers::song::endpoint::create_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::CREATECOVERART,
post(crate::callers::coverart::endpoint::create_coverart).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::GETSONGS,
get(crate::callers::song::endpoint::get_songs).route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::callers::endpoints::GETCOVERART,
get(crate::callers::coverart::endpoint::get_coverart).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::DOWNLOADCOVERART,
get(crate::callers::coverart::endpoint::download_coverart).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::STREAMSONG,
get(crate::callers::song::endpoint::stream_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::DOWNLOADSONG,
get(crate::callers::song::endpoint::download_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::DELETESONG,
delete(crate::callers::song::endpoint::delete_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::GETALLSONGS,
get(crate::callers::song::endpoint::get_all_songs),
)
.layer(cors::configure_cors().await)
}
pub async fn app() -> axum::Router {
let pool = crate::db::create_pool()
.await
.expect("Failed to create pool");
crate::db::migrations(&pool).await;
let cors = cors::configure_cors().await;
routes()
.await
.merge(
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
)
.layer(axum::Extension(pool))
.layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024))
.layer(TimeoutLayer::with_status_code(
axum::http::StatusCode::OK,
Duration::from_secs(300),
))
.layer(cors)
}
+2
View File
@@ -0,0 +1,2 @@
pub mod host;
pub mod init;
+24
View File
@@ -0,0 +1,24 @@
use sqlx::postgres::PgPoolOptions;
pub mod connection_settings {
pub const MAXCONN: u32 = 10;
}
pub async fn create_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let database_url = icarus_envy::environment::get_db_url().await.value;
println!("Database url: {database_url}");
PgPoolOptions::new()
.max_connections(connection_settings::MAXCONN)
.connect(&database_url)
.await
}
pub async fn migrations(pool: &sqlx::PgPool) {
// Run migrations using the sqlx::migrate! macro
// Assumes your migrations are in a ./migrations folder relative to Cargo.toml
sqlx::migrate!("./migrations")
.run(pool)
.await
.expect("Failed to run migrations");
}
+548 -722
View File
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
use sqlx::Row;
pub async fn create(
pool: &sqlx::PgPool,
coverart: &icarus_models::coverart::CoverArt,
song_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverart" (title, directory, filename, file_type, song_id) VALUES($1, $2, $3, $4, $5) RETURNING id;
"#,
)
.bind(&coverart.title)
.bind(&coverart.directory)
.bind(&coverart.filename)
.bind(&coverart.file_type)
.bind(song_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, directory, filename, file_type, song_id FROM "coverart" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(icarus_models::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
..Default::default()
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_with_song_id(
pool: &sqlx::PgPool,
song_id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, directory, filename, file_type, song_id FROM "coverart" WHERE song_id = $1;
"#,
)
.bind(song_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(icarus_models::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn delete_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
DELETE FROM "coverart"
WHERE id = $1
RETURNING id, title, directory, filename, file_type, song_id
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error deleting data: {e:?}");
});
match result {
Ok(row) => Ok(icarus_models::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
}),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod coverart;
pub mod queue;
pub mod song;
+197
View File
@@ -0,0 +1,197 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
data: &Vec<u8>,
file_type: &str,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverartQueue" (data, file_type) VALUES($1, $2) RETURNING id;
"#,
)
.bind(data)
.bind(file_type)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e:?}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update(
pool: &sqlx::PgPool,
coverart_id: &uuid::Uuid,
song_queue_id: &uuid::Uuid,
) -> Result<i32, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "coverartQueue" SET song_queue_id = $1 WHERE id = $2;
"#,
)
.bind(song_queue_id)
.bind(coverart_id)
.execute(pool)
.await;
match result {
Ok(_) => Ok(0),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<crate::callers::queue::coverart::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, file_type, song_queue_id FROM "coverartQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(crate::callers::queue::coverart::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<crate::callers::queue::coverart::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, file_type, song_queue_id FROM "coverartQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(crate::callers::queue::coverart::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_data_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "coverartQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_data_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "coverartQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn wipe_data(
pool: &sqlx::PgPool,
coverart_queue_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "coverartQueue" SET data = NULL WHERE id = $1 RETURNING id;
"#,
)
.bind(coverart_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating query: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
+126
View File
@@ -0,0 +1,126 @@
use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct MetadataQueue {
pub id: uuid::Uuid,
pub metadata: serde_json::Value,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
pub song_queue_id: uuid::Uuid,
}
pub async fn insert(
pool: &sqlx::PgPool,
metadata: &serde_json::Value,
song_queue_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "metadataQueue" (metadata, song_queue_id) VALUES($1, $2) RETURNING id;
"#,
)
.bind(metadata)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<MetadataQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT * FROM "metadataQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => Ok(MetadataQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
metadata: row
.try_get("metadata")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
created_at: row
.try_get("created_at")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<MetadataQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, metadata, created_at, song_queue_id FROM "metadataQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let data: serde_json::Value = row
.try_get("metadata")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(MetadataQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
metadata: data,
created_at: row
.try_get("created_at")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
})
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod coverart;
pub mod metadata;
pub mod song;
+303
View File
@@ -0,0 +1,303 @@
use sqlx::Row;
pub mod status {
pub const PENDING: &str = "pending";
pub const READY: &str = "ready";
pub const PROCESSING: &str = "processing";
pub const DONE: &str = "done";
pub async fn is_valid(status: &str) -> bool {
status == PENDING || status == PROCESSING || status == DONE || status == READY
}
}
pub mod dbtype {
/// Maps to the songQueue table. Does not contain the data field
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct SongQueue {
pub id: uuid::Uuid,
pub filename: String,
pub status: String,
pub user_id: uuid::Uuid,
}
}
pub async fn insert(
pool: &sqlx::PgPool,
data: &Vec<u8>,
filename: &String,
status: &String,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "songQueue" (data, filename, status) VALUES($1, $2, $3) RETURNING id;
"#,
)
.bind(data)
.bind(filename)
.bind(status)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update(
pool: &sqlx::PgPool,
data: &Vec<u8>,
id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET data = $1 WHERE id = $2 RETURNING data;
"#,
)
.bind(data)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_most_recent_and_update(
pool: &sqlx::PgPool,
) -> Result<dbtype::SongQueue, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue"
SET status = $1
WHERE id = (
SELECT id FROM "songQueue"
WHERE status = $2
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, filename, status, user_id;
"#,
)
.bind(status::PROCESSING)
.bind(status::READY)
.fetch_one(pool)
.await;
match result {
Ok(row) => {
let user_id_result = row.try_get("user_id");
let song_queue = dbtype::SongQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
status: row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
user_id: match user_id_result {
Ok(id) => id,
Err(_) => uuid::Uuid::nil(),
},
};
Ok(song_queue)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_status_of_song_queue(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<String, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, status FROM "songQueue" WHERE id = $1
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error selecting: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update_song_queue_status(
pool: &sqlx::PgPool,
status: &String,
id: &uuid::Uuid,
) -> Result<String, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET status = $1 WHERE id = $2 RETURNING status;
"#,
)
.bind(status)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating record {e}");
});
match result {
Ok(row) => Ok(row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn link_user_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
user_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET user_id = $1 WHERE id = $2 RETURNING user_id;
"#,
)
.bind(user_id)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating record {e}");
});
match result {
Ok(row) => Ok(row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_song_queue(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<dbtype::SongQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, filename, status, user_id FROM "songQueue" WHERE id = $1
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e}");
});
match result {
Ok(row) => {
let user_id_result = row.try_get("user_id");
let song_queue = dbtype::SongQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
status: row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
user_id: match user_id_result {
Ok(id) => id,
Err(_) => uuid::Uuid::nil(),
},
};
Ok(song_queue)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn wipe_data(pool: &sqlx::PgPool, id: &uuid::Uuid) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET data = NULL WHERE id = $1 RETURNING id;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating record: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_data(pool: &sqlx::PgPool, id: &uuid::Uuid) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "songQueue"
WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let data = row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(data)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
+347
View File
@@ -0,0 +1,347 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
song: &icarus_models::song::Song,
) -> Result<(time::OffsetDateTime, uuid::Uuid), sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "song" (title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, filename, directory, user_id)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING date_created, id;
"#
)
.bind(&song.title)
.bind(&song.artist)
.bind(&song.album_artist)
.bind(&song.album)
.bind(&song.genre)
.bind(song.year)
.bind(song.track)
.bind(song.disc)
.bind(song.track_count)
.bind(song.disc_count)
.bind(song.duration)
.bind(&song.audio_type)
.bind(&song.filename)
.bind(&song.directory)
.bind(song.user_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting query: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
let date_created_time: time::OffsetDateTime = row
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
let date_created = date_created_time;
Ok((date_created, id))
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_song(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::song::Song, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT * FROM "song" WHERE id = $1
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e}");
});
match result {
Ok(row) => {
let date_created_time: time::OffsetDateTime = row
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(icarus_models::song::Song {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
artist: row
.try_get("artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album_artist: row
.try_get("album_artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album: row
.try_get("album")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
genre: row
.try_get("genre")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
year: row
.try_get("year")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track: row
.try_get("track")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc: row
.try_get("disc")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track_count: row
.try_get("track_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc_count: row
.try_get("disc_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
duration: row
.try_get("duration")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
audio_type: row
.try_get("audio_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_all_songs(
pool: &sqlx::PgPool,
) -> Result<Vec<icarus_models::song::Song>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT * FROM "song";
"#,
)
.fetch_all(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(rows) => {
let mut songs: Vec<icarus_models::song::Song> = Vec::new();
for row in rows {
let date_created_time: time::OffsetDateTime = row
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
let song = icarus_models::song::Song {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
artist: row
.try_get("artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album_artist: row
.try_get("album_artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album: row
.try_get("album")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
genre: row
.try_get("genre")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
year: row
.try_get("year")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track: row
.try_get("track")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc: row
.try_get("disc")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track_count: row
.try_get("track_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc_count: row
.try_get("disc_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
duration: row
.try_get("duration")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
audio_type: row
.try_get("audio_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
};
songs.push(song);
}
Ok(songs)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn delete_song(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::song::Song, sqlx::Error> {
let result = sqlx::query(
// icarus_models::song::Song,
r#"
DELETE FROM "song"
WHERE id = $1
RETURNING id, title, artist, album, album_artist, genre, year, disc, track, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error deleting data: {e:?}")
});
match result {
Ok(row) => {
let date_created_time: time::OffsetDateTime = row
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(icarus_models::song::Song {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
artist: row
.try_get("artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album_artist: row
.try_get("album_artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album: row
.try_get("album")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
genre: row
.try_get("genre")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
year: row
.try_get("year")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track: row
.try_get("track")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc: row
.try_get("disc")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track_count: row
.try_get("track_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc_count: row
.try_get("disc_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
duration: row
.try_get("duration")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
audio_type: row
.try_get("audio_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
+4 -1
View File
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS "metadataQueue" (
CREATE TABLE IF NOT EXISTS "coverartQueue" ( CREATE TABLE IF NOT EXISTS "coverartQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
data BYTEA NULL, data BYTEA NULL,
file_type TEXT NOT NULL,
song_queue_id UUID NULL song_queue_id UUID NULL
); );
@@ -53,6 +54,8 @@ CREATE TABLE IF NOT EXISTS "song" (
CREATE TABLE IF NOT EXISTS "coverart" ( CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL, title TEXT NOT NULL,
path TEXT NOT NULL, directory TEXT NOT NULL,
filename TEXT NOT NULL,
file_type TEXT NOT NULL,
song_id UUID NOT NULL song_id UUID NOT NULL
); );
@@ -1,6 +1,6 @@
-- Add migration script here -- Add migration script here
INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('44cf7940-34ff-489f-9124-d0ec90a55af9', 'Hypocrite Like The Rest', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 1, 1, 9, 1, 139, 'flac', '2020-01-01 13:00:00-05', 'track01.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670'); INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('44cf7940-34ff-489f-9124-d0ec90a55af9', 'Hypocrite Like The Rest', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 1, 1, 9, 1, 139, 'flac', '2020-01-01 13:00:00-05', 'track01.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
INSERT INTO "coverart" VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I/Coverart-1.jpg', '44cf7940-34ff-489f-9124-d0ec90a55af9'); INSERT INTO "coverart" (id, title, directory, filename, file_type, song_id) VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I', 'Coverart-1.jpg', 'jpeg', '44cf7940-34ff-489f-9124-d0ec90a55af9');
INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('94cf7940-34ff-489f-9124-d0ec90a55af4', 'It''s Too Late', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 2, 1, 9, 1, 116, 'flac', '2020-01-01 13:01:00-05', 'track02.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670'); INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('94cf7940-34ff-489f-9124-d0ec90a55af4', 'It''s Too Late', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 2, 1, 9, 1, 116, 'flac', '2020-01-01 13:01:00-05', 'track02.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
INSERT INTO "coverart" VALUES('d96122cd-5ae9-4013-9934-60768d3006e9', 'I', 'tests/I/Coverart-2.jpg', '94cf7940-34ff-489f-9124-d0ec90a55af4'); INSERT INTO "coverart" (id, title, directory, filename, file_type, song_id) VALUES('d96122cd-5ae9-4013-9934-60768d3006e9', 'I', 'tests/I', 'Coverart-2.jpg', 'jpeg', '94cf7940-34ff-489f-9124-d0ec90a55af4');