Compare commits

...

13 Commits

Author SHA1 Message Date
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
KD 2023fc81f8 Version fix (#193)
* Version fix

* Workflow fix

* Test fix

* Test fix

* Removing code that was meant to be removed

Caused an extra file to be created when the request is called

* Updated env sample to address workflow

* Removed import

* env fix

* Version change

* Version bump

Going with the version I done earlier. I won't have the versioning match the api version in the endpoints

* Fixing versions for app dependencies
2025-10-14 22:02:26 -04:00
KD 48a40a4821 tsk-182: Update rust version (#191)
* Updated rust version

* Warning fix

* Updating crates and dependencies

* Version bump

* tsk-182: cargo update

* Updated jsonwebtoken
2025-10-12 18:23:56 -04:00
KD 8bf99be22c icarus_models version bump (#190)
* icarus_models version bump

* icarus_models changes

* Code refactoring

* Changes

* Version bump

* Fixed todo
2025-10-11 19:50:56 -04:00
KD 8c12c8c761 icarus_envy version bump (#189)
* icarus_envy version bump

* icarus_envy related changes

* Code formatting

* Code cleanup

* Version bump
2025-10-10 19:04:01 -04:00
KD caaec248db CORS support (#183)
* Changes to same .env file

* Updated sample env files

* icarus_envy version bump

* Adding CORS

* Added other urls

* Code formatting

* Changed allowed urls

* Modified docker compose. Port changes

* Changed port

* Cleanup

* Version bump
2025-10-10 16:33:15 -04:00
KD 309d76785a Language change (#185)
* Adding skeleton file

* Adding content

* Still having issues with the language

* Some changes

* Ignoring .NET related files
2025-10-01 09:55:04 -04:00
KD c83a37c20a Change language of project (#184)
* Adding skeleton file

* Adding content
2025-10-01 09:48:02 -04:00
KD 032801b903 Merge pull request #153 from kdeng00/v0.2
v0.2 release
2025-08-30 13:34:06 -04:00
kdeng00 b844f06e82 Added license 2025-08-30 13:30:34 -04:00
kdeng00 517a01d60c Version bump 2025-08-30 13:28:37 -04:00
KD 1ae2f88beb tsk-171: Updated Readme (#181) 2025-08-29 22:14:03 -04:00
KD 805df87f5b tsk-170: API documentation (#178)
* tsk-170: Added utopia crates for API documentation

* tsk-170: Adding code to document API

* tsk-170: icarus_models version bump

Added schema capabilities

* tsk-170: crate changes

* tsk-170: Added documentation for song queue endpoint

* tsk-170: Swagger openapi docs are now up

Well, at least one

* tsk-170: Some documentation

* tsk-170: Code formatting

* tsk-170: Saving changes

* tsk-170: Added more documentation

* tsk-170: Added the rest of the documentation

* tsk-170: Formatting

* tsk-170: icarus_models version bump
2025-08-24 19:58:38 -04:00
20 changed files with 1183 additions and 597 deletions
+5
View File
@@ -1,3 +1,8 @@
APP_ENV=development
BACKEND_PORT=8000
FRONTEND_URL=http://localhost:4200
RUST_LOG=debug
ALLOWED_ORIGINS=https://soaricarus.com,https://www.soaricarus.com
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/icarus/mydata
POSTGRES_MAIN_USER=icarus
+5
View File
@@ -1,3 +1,8 @@
APP_ENV=production
BACKEND_PORT=8000
FRONTEND_URL=http://localhost:4200
RUST_LOG=debug
ALLOWED_ORIGINS=https://soaricarus.com,https://www.soaricarus.com,http://localhost:8000,http://127.0.0.1:8000
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/icarus/mydata
POSTGRES_MAIN_USER=icarus
+9
View File
@@ -0,0 +1,9 @@
# Override specific file types
*.cs linguist-language=Rust
*.sln linguist-documentation
*.csproj linguist-documentation
# Or vendor all non-Rust files
*.cs linguist-vendored
*.js linguist-vendored
*.py linguist-vendored
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.88.0
toolchain: 1.90.0
components: cargo
- name: Extract Version from Cargo.toml
+6 -6
View File
@@ -2,9 +2,9 @@ name: Rust CI
on:
push:
branches: [ "v0.2" ]
branches: [ "main" ]
pull_request:
branches: [ "v0.2" ]
branches: [ "main" ]
jobs:
build:
@@ -25,12 +25,12 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust 1.88.0
- name: Install Rust 1.90.0
uses: actions-rs/toolchain@v1
with:
toolchain: 1.88.0
toolchain: 1.90.0
components: clippy, rustfmt
override: true
@@ -69,7 +69,7 @@ jobs:
ROOT_DIRECTORY: "/tmp"
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
run: |
cat .env.sample | head -1 > .env
cat .env.sample | head -6 > .env
cargo test --verbose --
- name: Run clippy
+4
View File
@@ -7,3 +7,7 @@
.DS_STORE
Storage/
*.sln
*.cs
*.csproj
Generated
+479 -325
View File
File diff suppressed because it is too large Load Diff
+21 -19
View File
@@ -1,33 +1,35 @@
[package]
name = "icarus"
version = "0.1.102"
version = "0.3.1"
edition = "2024"
rust-version = "1.88"
rust-version = "1.90"
[dependencies]
axum = { version = "0.8.4", features = ["multipart"] }
axum-extra = { version = "0.10.1", features = ["cookie"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.140" }
axum = { version = "0.8.6", features = ["multipart"] }
axum-extra = { version = "0.10.3", features = ["cookie"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.145" }
tower = { version = "0.5.2", features = ["full"] }
tokio = { version = "1.45.1", features = ["full"] }
tokio-util = { version = "0.7.15", features = ["io"] }
tokio = { version = "1.47.1", features = ["full"] }
tokio-util = { version = "0.7.16", features = ["io"] }
tower-http = { version = "0.6.6", features = ["cors", "timeout"] }
tracing-subscriber = "0.3.19"
tracing-subscriber = "0.3.20"
futures = { version = "0.3.31" }
mime_guess = { version = "2.0.5" }
uuid = { version = "1.17.0", features = ["v4", "serde"] }
uuid = { version = "1.18.1", features = ["v4", "serde"] }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] }
time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] }
thiserror = "1.0"
base64 = "0.21"
time = { version = "0.3.44", features = ["formatting", "macros", "parsing", "serde"] }
thiserror = "2.0.17"
base64 = "0.22.1"
jsonwebtoken = { version = "9.3.1" }
josekit = { version = "0.10.1" }
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0" }
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.5.5" }
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.2" }
josekit = { version = "0.10.3" }
utoipa = { version = "5.4.0", features = ["axum_extras"] }
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_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.8.0" }
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.5.0" }
[dev-dependencies]
common-multipart-rfc7578 = { version = "0.7.0" }
url = { version = "2.5.4" }
tempfile = { version = "3.20.0" }
url = { version = "2.5.7" }
tempfile = { version = "3.23.0" }
+1 -1
View File
@@ -1,5 +1,5 @@
# Stage 1: Build the application
FROM rust:1.88 as builder
FROM rust:1.90 as builder
# Set the working directory inside the container
WORKDIR /usr/src/app
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Kun Deng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+22 -6
View File
@@ -1,17 +1,33 @@
# Icarus
Web API for the Icarus project.
# Getting Started
## Docker
Make sure `icarus_auth` is located in the root of the parent directory if using docker.
### Requires
`icarus_auth` v0.6.1
`songparser` v0.4.2
Create a `.env` file for both projects - `icarus_auth` and `icarus` - in the root of each project.
### Compatible with
`icarus-dm` v0.8.2
## Getting Started
Quickest way to get started is with docker. Make sure `icarus_auth` and `songparser` repositories
are located in the root of the parent directory. Check the respective repositories to ensure they
are setup correctly before configuring `Icarus`.
Copy the `.env.docker.sample` file to `.env`. Ensure that the `ROOT_DIRECTORY` variable is populated
and exists on the docker image's filesystem. The credentials for the database doesn't need to be
changed for development, but if deploying it, it should be modified.
Build containers
```
docker compose build --ssh default api auth_api
docker compose build --ssh default
```
Bring it up
```
docker compose up -d --force-recreate api auth_api
docker compose up -d --force-recreate
```
To view the OpenAPI spec, run the project and access `/swagger-ui`. If running through docker,
the url would be something like `http://localhost:8000/swagger-ui`.
+2 -2
View File
@@ -10,7 +10,7 @@ services:
ports:
# Map host port 8000 to container port 3000 (adjust as needed)
# Format: "HOST_PORT:CONTAINER_PORT"
- "8000:3000"
- "8000:8000"
env_file:
- .env
depends_on:
@@ -29,7 +29,7 @@ services:
container_name: auth_api
restart: unless-stopped
ports:
- "8001:3000" # Map host port 8000 to container port 8000 (adjust container port based on your app's EXPOSE in Dockerfile)
- "8001:8001" # Map host port 8000 to container port 8000 (adjust container port based on your app's EXPOSE in Dockerfile)
# environment:
# Environment variables your API needs, e.g., database connection
# Add other necessary environment variables
+2 -2
View File
@@ -55,8 +55,8 @@ CREATE TABLE IF NOT EXISTS "song" (
CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
-- TODO: Separate path later
path TEXT NOT NULL,
directory TEXT NOT NULL,
filename TEXT NOT NULL,
song_id UUID NOT NULL
-- TODO: Add type later
);
+1 -6
View File
@@ -1,6 +1,3 @@
// use std::collections::BTreeMap;
// use std::sync::Arc;
use axum::{
Json,
http::{Request, StatusCode},
@@ -10,9 +7,7 @@ use axum::{
use axum_extra::extract::cookie::CookieJar;
use jsonwebtoken::{DecodingKey, Validation, decode};
use serde::{Deserialize, Serialize};
// use serde_json::{json, Value};
use thiserror::Error;
// use time::OffsetDateTime;
fn deserialize_i64_from_f64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
@@ -95,7 +90,7 @@ pub async fn auth<B>(
(StatusCode::UNAUTHORIZED, Json(json_error))
})?;
let secret_key = icarus_envy::environment::get_secret_main_key().await;
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
+180 -48
View File
@@ -1,14 +1,26 @@
// TODO: Separate queue and coverart endpoints
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[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 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)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
@@ -16,7 +28,7 @@ pub mod request {
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
@@ -24,7 +36,7 @@ pub mod request {
}
pub mod fetch_coverart_with_data {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
@@ -32,7 +44,7 @@ pub mod request {
}
pub mod create_coverart {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub song_id: uuid::Uuid,
pub coverart_queue_id: uuid::Uuid,
@@ -40,14 +52,14 @@ pub mod request {
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub coverart_queue_id: uuid::Uuid,
}
}
pub mod get_coverart {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
}
@@ -55,20 +67,20 @@ pub mod request {
}
pub mod response {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[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)]
#[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)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Id>,
@@ -76,7 +88,7 @@ pub mod response {
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<super::super::CoverArtQueue>,
@@ -84,7 +96,7 @@ pub mod response {
}
pub mod fetch_coverart_with_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Vec<u8>>,
@@ -92,7 +104,7 @@ pub mod response {
}
pub mod create_coverart {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<icarus_models::coverart::CoverArt>,
@@ -100,7 +112,7 @@ pub mod response {
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
@@ -108,7 +120,7 @@ pub mod response {
}
pub mod get_coverart {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<icarus_models::coverart::CoverArt>,
@@ -313,11 +325,12 @@ pub mod cov_db {
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverart" (title, path, song_id) VALUES($1, $2, $3) RETURNING id;
INSERT INTO "coverart" (title, directory, filename, song_id) VALUES($1, $2, $3, $4) RETURNING id;
"#,
)
.bind(&coverart.title)
.bind(&coverart.path)
.bind(&coverart.directory)
.bind(&coverart.filename)
.bind(song_id)
.fetch_one(pool)
.await
@@ -343,7 +356,7 @@ pub mod cov_db {
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, path, song_id FROM "coverart" WHERE id = $1;
SELECT id, title, directory, filename, song_id FROM "coverart" WHERE id = $1;
"#,
)
.bind(id)
@@ -363,15 +376,19 @@ pub mod cov_db {
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
path: row
.try_get("path")
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
..Default::default()
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
@@ -383,7 +400,7 @@ pub mod cov_db {
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, path, song_id FROM "coverart" WHERE song_id = $1;
SELECT id, title, directory, filename, song_id FROM "coverart" WHERE song_id = $1;
"#,
)
.bind(song_id)
@@ -403,8 +420,12 @@ pub mod cov_db {
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
path: row
.try_get("path")
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
@@ -425,7 +446,7 @@ pub mod cov_db {
r#"
DELETE FROM "coverart"
WHERE id = $1
RETURNING id, title, path, song_id
RETURNING id, title, directory, filename, song_id
"#,
)
.bind(id)
@@ -445,8 +466,12 @@ pub mod cov_db {
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
path: row
.try_get("path")
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_id: row
@@ -461,10 +486,20 @@ pub mod cov_db {
}
pub mod endpoint {
use std::io::Write;
use axum::response::IntoResponse;
/// 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::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,
@@ -510,6 +545,20 @@ pub mod endpoint {
}
}
/// 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>,
@@ -537,6 +586,19 @@ pub mod endpoint {
}
}
/// 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<
@@ -584,6 +646,16 @@ pub mod endpoint {
}
}
/// 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>,
@@ -612,6 +684,20 @@ pub mod endpoint {
}
}
/// Endpoint to create cover art
#[utoipa::path(
post,
path = super::super::endpoints::CREATECOVERART,
request_body(
content = super::request::create_coverart::Request,
description = "Data required to create cover art",
content_type = "application/json"
),
responses(
(status = 200, description = "Cover art created", body = super::response::create_coverart::Response),
(status = 400, description = "Failure in creating cover art", body = super::response::create_coverart::Response)
)
)]
pub async fn create_coverart(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::create_coverart::Request>,
@@ -627,35 +713,42 @@ pub mod endpoint {
let song_id = payload.song_id;
match crate::callers::song::song_db::get_song(&pool, &song_id).await {
Ok(song) => {
let directory = icarus_envy::environment::get_root_directory().await;
let dir = std::path::Path::new(&directory);
let directory = icarus_envy::environment::get_root_directory().await.value;
// 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 =
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.data = data;
let mut file = std::fs::File::create(&save_path).unwrap();
file.write_all(&coverart.data).unwrap();
match coverart.save_to_filesystem() {
Ok(_) => {
match super::cov_db::create(&pool, &coverart, &song.id).await {
Ok(id) => {
coverart.song_id = song_id;
coverart.id = id;
println!("Cover Art created");
match super::cov_db::create(&pool, &coverart, &song.id).await {
Ok(id) => {
// TODO: Populate song_id
coverart.id = id;
println!("Cover Art created");
response.message = String::from("Successful");
response.data.push(coverart);
response.message = String::from("Successful");
response.data.push(coverart);
(axum::http::StatusCode::OK, axum::Json(response))
(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))
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
@@ -672,6 +765,21 @@ 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>,
@@ -701,6 +809,18 @@ pub mod endpoint {
}
}
/// Endpoint to get cover art with criteria
#[utoipa::path(
get,
path = super::super::endpoints::GETCOVERART,
params(
("id" = uuid::Uuid, Path, description = "Cover art Id")
),
responses(
(status = 200, description = "Cover art retrieved", body = super::response::get_coverart::Response),
(status = 400, description = "Error retrieving cover art", body = super::response::get_coverart::Response)
)
)]
pub async fn get_coverart(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::get_coverart::Params>,
@@ -729,12 +849,24 @@ pub mod endpoint {
}
}
/// Endpoint to download cover art
#[utoipa::path(
get,
path = super::super::endpoints::DOWNLOADCOVERART,
params(
("id" = uuid::Uuid, Path, description = "Cover art Id")
),
responses(
(status = 200, description = "Cover art downloading", body = Vec<u8>),
(status = 404, description = "Cover art not found", body = Vec<u8>)
)
)]
pub async fn download_coverart(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
match super::cov_db::get_coverart(&pool, &id).await {
Ok(coverart) => match coverart.to_data() {
Ok(coverart) => match icarus_models::coverart::io::to_data(&coverart) {
Ok(data) => {
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
+40 -8
View File
@@ -1,10 +1,9 @@
// 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)]
#[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub album: String,
@@ -43,7 +42,13 @@ pub mod request {
pub mod fetch_metadata {
#[derive(
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, sqlx::Decode,
Debug,
Default,
serde::Deserialize,
serde::Serialize,
sqlx::FromRow,
sqlx::Decode,
utoipa::ToSchema,
)]
pub struct Params {
pub id: Option<uuid::Uuid>,
@@ -53,11 +58,10 @@ pub mod request {
}
pub mod response {
pub mod queue_metadata {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
@@ -67,7 +71,7 @@ pub mod response {
pub mod fetch_metadata {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<crate::callers::metadata::metadata_queue::MetadataQueue>,
@@ -83,7 +87,7 @@ pub mod metadata_queue {
pub id: uuid::Uuid,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)]
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct MetadataQueue {
pub id: uuid::Uuid,
pub metadata: serde_json::Value,
@@ -204,9 +208,24 @@ pub mod metadata_queue {
}
}
/// 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>,
@@ -221,7 +240,7 @@ pub mod endpoint {
response.message = if response.data.is_empty() {
String::from("Error")
} else {
String::from("Success")
String::from(super::super::response::SUCCESSFUL)
};
(StatusCode::OK, Json(response))
@@ -233,6 +252,19 @@ pub mod endpoint {
}
}
/// 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>,
+257 -109
View File
@@ -7,8 +7,21 @@ pub mod request {
pub message: String,
}
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)]
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub id: uuid::Uuid,
pub status: String,
@@ -16,7 +29,7 @@ pub mod request {
}
pub mod create_metadata {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub title: String,
pub artist: String,
@@ -68,25 +81,21 @@ pub mod request {
duration: self.duration,
audio_type: self.audio_type.clone(),
user_id: self.user_id,
// TODO: Change the type of this in icarus_models lib
date_created: String::new(),
filename: String::new(),
data: Vec::new(),
directory: String::new(),
..Default::default()
}
}
}
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[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)]
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub user_id: uuid::Uuid,
@@ -94,7 +103,7 @@ pub mod request {
}
pub mod get_songs {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
}
@@ -104,16 +113,18 @@ pub mod request {
pub mod response {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
/// Song queue response
#[derive(Default, Deserialize, 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)]
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<crate::callers::song::song_queue::SongQueue>,
@@ -121,13 +132,13 @@ pub mod response {
}
pub mod update_status {
#[derive(serde::Deserialize, serde::Serialize)]
#[derive(serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct ChangedStatus {
pub old_status: String,
pub new_status: String,
}
#[derive(Default, serde::Deserialize, serde::Serialize)]
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<ChangedStatus>,
@@ -135,7 +146,7 @@ pub mod response {
}
pub mod update_song_queue {
#[derive(Default, serde::Deserialize, serde::Serialize)]
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
@@ -143,7 +154,7 @@ pub mod response {
}
pub mod create_metadata {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<icarus_models::song::Song>,
@@ -151,7 +162,7 @@ pub mod response {
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
@@ -159,7 +170,7 @@ pub mod response {
}
pub mod link_user_id {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
@@ -167,7 +178,7 @@ pub mod response {
}
pub mod get_songs {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<icarus_models::song::Song>,
@@ -175,13 +186,13 @@ pub mod response {
}
pub mod delete_song {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct SongAndCoverArt {
pub song: icarus_models::song::Song,
pub coverart: icarus_models::coverart::CoverArt,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<SongAndCoverArt>,
@@ -209,7 +220,7 @@ pub mod song_db {
pub async fn insert(
pool: &sqlx::PgPool,
song: &icarus_models::song::Song,
) -> Result<(String, uuid::Uuid), sqlx::Error> {
) -> 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)
@@ -247,7 +258,7 @@ pub mod song_db {
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
let date_created = date_created_time.to_string();
let date_created = date_created_time;
Ok((date_created, id))
}
@@ -339,7 +350,7 @@ pub mod song_db {
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: date_created_time.to_string(),
date_created: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
@@ -436,7 +447,7 @@ pub mod song_db {
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: date_created_time.to_string(),
date_created: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
@@ -540,7 +551,7 @@ pub mod song_db {
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: date_created_time.to_string(),
date_created: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
@@ -556,12 +567,8 @@ pub mod song_db {
mod song_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)]
// TODO: Move this somewhere else at some point
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct SongQueue {
pub id: uuid::Uuid,
pub filename: String,
@@ -854,12 +861,25 @@ mod song_queue {
}
}
/// Module for song related endpoints
pub mod endpoint {
use axum::{Json, http::StatusCode, response::IntoResponse};
use std::io::Write;
use crate::callers::song::song_queue;
/// 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 = 200, description = "Song queued", body = super::response::Response)
)
)]
pub async fn queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
@@ -881,11 +901,6 @@ pub mod endpoint {
data.len()
);
// TODO: Remove this
// Save the file to disk
let mut file = std::fs::File::create(&file_name).unwrap();
file.write_all(&data).unwrap();
let raw_data: Vec<u8> = data.to_vec();
let queue_repo = song_queue::insert(
&pool,
@@ -908,6 +923,20 @@ pub mod endpoint {
(StatusCode::OK, 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>,
@@ -939,6 +968,15 @@ pub mod endpoint {
}
}
/// 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>,
) -> (
@@ -961,6 +999,16 @@ pub mod endpoint {
}
// TODO: Rename
/// 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_flac(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
@@ -989,6 +1037,20 @@ pub mod endpoint {
}
}
/// 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>,
@@ -1041,6 +1103,22 @@ pub mod endpoint {
}
}
/// 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)
)
)]
pub async fn update_song_queue(
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
@@ -1087,6 +1165,21 @@ pub mod endpoint {
}
}
/// Endpoint to create song
#[utoipa::path(
post,
path = super::super::endpoints::QUEUEMETADATA,
request_body(
content = super::request::create_metadata::Request,
description = "Data needed to create the song and save it to the filesystem",
content_type = "application/json"
),
responses(
(status = 200, description = "Song created", body = super::response::create_metadata::Response),
(status = 400, description = "Error", body = super::response::create_metadata::Response),
(status = 505, description = "Error creating song", body = super::response::create_metadata::Response)
)
)]
pub async fn create_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::create_metadata::Request>,
@@ -1098,9 +1191,11 @@ pub mod endpoint {
if payload.is_valid() {
let mut song = payload.to_song();
song.filename =
song.generate_filename(icarus_models::types::MusicTypes::FlacExtension, true);
song.directory = icarus_envy::environment::get_root_directory().await;
song.filename = icarus_models::song::generate_filename(
icarus_models::types::MusicTypes::FlacExtension,
true,
);
song.directory = icarus_envy::environment::get_root_directory().await.value;
match song_queue::get_data(&pool, &payload.song_queue_id).await {
Ok(data) => {
@@ -1118,40 +1213,23 @@ pub mod endpoint {
}
}
let save_path = dir.join(&song.filename);
match song.save_to_filesystem() {
Ok(_) => match super::song_db::insert(&pool, &song).await {
Ok((date_created, id)) => {
song.id = id;
song.date_created = Some(date_created);
response.message = String::from("Successful");
response.data.push(song);
match std::fs::File::create(&save_path) {
Ok(mut file) => {
file.write_all(&song.data).unwrap();
match song.song_path() {
Ok(_) => match super::song_db::insert(&pool, &song).await {
Ok((date_created, id)) => {
song.id = id;
song.date_created = date_created;
response.message = String::from("Successful");
response.data.push(song);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message =
format!("{:?} song {:?}", err.to_string(), song);
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
(axum::http::StatusCode::OK, axum::Json(response))
}
}
Err(err) => {
response.message = format!("{:?} song {:?}", err.to_string(), song);
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
Err(err) => {
let song_path = song.song_path();
response.message = format!(
"{err:?} Song directory: {} Filename: {} Save Path: {:?} Song Path: {:?}",
song.directory, song.filename, save_path, song_path
);
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
@@ -1170,6 +1248,20 @@ pub mod endpoint {
}
}
/// 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>,
@@ -1200,6 +1292,18 @@ pub mod endpoint {
}
}
// Endpoint to get songs
#[utoipa::path(
get,
path = super::super::endpoints::GETSONGS,
params(
("id" = uuid::Uuid, Path, description = "Id of song")
),
responses(
(status = 200, description = "Songs found", body = super::response::get_songs::Response),
(status = 400, description = "Error getting songs", body = super::response::get_songs::Response)
)
)]
pub async fn get_songs(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::get_songs::Params>,
@@ -1228,6 +1332,15 @@ pub mod endpoint {
}
}
/// Endpoint to get all songs
#[utoipa::path(
get,
path = super::super::endpoints::GETALLSONGS,
responses(
(status = 200, description = "Getting all songs", body = super::response::get_songs::Response),
(status = 404, description = "Song not found", body = super::response::get_songs::Response)
)
)]
pub async fn get_all_songs(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
) -> (
@@ -1249,6 +1362,16 @@ pub mod endpoint {
}
}
/// Ednpoint to stream song
#[utoipa::path(
get,
path = super::super::endpoints::STREAMSONG,
params(("id" = uuid::Uuid, Path, description = "Song Id")),
responses(
(status = 200, description = "Stream song", body = Vec<u8>),
(status = 500, description = "Error streaming song", body = (u64, String))
)
)]
pub async fn stream_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
@@ -1296,12 +1419,23 @@ pub mod endpoint {
}
}
/// Endpoint to download song
#[utoipa::path(
get,
path = super::super::endpoints::DOWNLOADSONG,
params(("id" = uuid::Uuid, Path, description = "Song Id")),
responses(
(status = 200, description = "Download song", body = (u64, Vec<u8>)),
(status = 404, description = "Song not found", body = (u64, Vec<u8>)),
(status = 400, description = "Error downloading song", body = (u64, Vec<u8>))
)
)]
pub async fn download_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
match super::song_db::get_song(&pool, &id).await {
Ok(song) => match song.to_data() {
Ok(song) => match icarus_models::song::io::to_data(&song) {
Ok(data) => {
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
@@ -1331,6 +1465,17 @@ pub mod endpoint {
}
}
/// Endpoint to delete the song
#[utoipa::path(
delete,
path = super::super::endpoints::DELETESONG,
params(("id" = uuid::Uuid, Path, description = "Song Id")),
responses(
(status = 200, description = "Song deleted", body = super::response::delete_song::Response),
(status = 404, description = "Song not found", body = super::response::delete_song::Response),
(status = 500, description = "Error deleting song", body = super::response::delete_song::Response)
)
)]
pub async fn delete_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
@@ -1346,49 +1491,52 @@ pub mod endpoint {
.await
{
Ok(coverart) => {
let coverart_path = std::path::Path::new(&coverart.path);
if coverart_path.exists() {
match song.song_path() {
Ok(song_path) => {
match super::song_db::delete_song(&pool, &song.id).await {
Ok(deleted_song) => {
match super::super::coverart::cov_db::delete_coverart(
&pool,
&coverart.id,
)
.await
{
Ok(deleted_coverart) => {
match std::fs::remove_file(song_path) {
Ok(_) => match std::fs::remove_file(
&coverart.path,
) {
Ok(_) => {
response.message = String::from(super::super::response::SUCCESSFUL);
response.data.push(super::response::delete_song::SongAndCoverArt{ song: deleted_song, coverart: deleted_coverart });
(
axum::http::StatusCode::OK,
axum::Json(response),
)
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
}
}
}
let coverart_path_str = match coverart.get_path() {
Ok(path) => path,
Err(err) => {
response.message = err.to_string();
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
};
let coverart_path = std::path::Path::new(&coverart_path_str);
if coverart_path.exists() {
match super::song_db::delete_song(&pool, &song.id).await {
Ok(deleted_song) => {
match super::super::coverart::cov_db::delete_coverart(
&pool,
&coverart.id,
)
.await
{
Ok(deleted_coverart) => {
match song.remove_from_filesystem() {
Ok(_) => match coverart.remove_from_filesystem() {
Ok(_) => {
response.message = String::from(
super::super::response::SUCCESSFUL,
);
response.data.push(super::response::delete_song::SongAndCoverArt{ song: deleted_song, coverart: deleted_coverart });
(
axum::http::StatusCode::OK,
axum::Json(response),
)
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(
+123 -61
View File
@@ -10,7 +10,7 @@ pub mod db {
}
pub async fn create_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let database_url = icarus_envy::environment::get_db_url().await;
let database_url = icarus_envy::environment::get_db_url().await.value;
println!("Database url: {database_url}");
PgPoolOptions::new()
@@ -45,14 +45,90 @@ async fn main() {
}
pub mod init {
use axum::routing::{delete, get, patch, post};
use std::time::Duration;
use tower_http::timeout::TimeoutLayer;
use axum::http::{
HeaderValue, Method,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
};
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::metadata as metadata_caller;
use crate::callers::song as song_caller;
use coverart_caller::endpoint as coverart_endpoints;
use coverart_caller::response as coverart_responses;
use metadata_caller::endpoint as metadata_endpoints;
use metadata_caller::response as metadata_responses;
use song_caller::endpoint as song_endpoints;
use song_caller::response as song_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_endpoints::queue_song, song_endpoints::link_user_id, song_endpoints::fetch_queue_song, song_endpoints::download_flac,
song_endpoints::update_song_queue_status, song_endpoints::update_song_queue, song_endpoints::create_metadata, song_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_endpoints::queue, coverart_endpoints::link, coverart_endpoints::fetch_coverart_no_data,
coverart_endpoints::fetch_coverart_with_data, coverart_endpoints::create_coverart, coverart_endpoints::wipe_data_from_coverart_queue,
coverart_endpoints::get_coverart, coverart_endpoints::download_coverart,
metadata_endpoints::queue_metadata, metadata_endpoints::fetch_metadata),
components(schemas(song_responses::Response, song_responses::link_user_id::Response, song_responses::fetch_queue_song::Response,
song_responses::update_status::Response, song_responses::update_song_queue::Response, song_responses::create_metadata::Response,
song_responses::wipe_data_from_song_queue::Response, song_responses::get_songs::Response, song_responses::delete_song::Response,
coverart_responses::Response, coverart_responses::link::Response, coverart_responses::fetch_coverart_no_data::Response,
coverart_responses::fetch_coverart_with_data::Response, coverart_responses::create_coverart::Response,
coverart_responses::wipe_data_from_coverart_queue::Response, coverart_responses::get_coverart::Response,
metadata_responses::queue_metadata::Response, metadata_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()
@@ -194,6 +270,7 @@ pub mod init {
crate::callers::endpoints::GETALLSONGS,
get(crate::callers::song::endpoint::get_all_songs),
)
.layer(cors::configure_cors().await)
}
pub async fn app() -> axum::Router {
@@ -203,14 +280,14 @@ pub mod init {
// TODO: Look into handling this. Seems redundant to run migrations multiple times
crate::db::migrations(&pool).await;
let cors = tower_http::cors::CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_credentials(true)
.allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
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::new(Duration::from_secs(300)))
@@ -229,7 +306,7 @@ fn get_address() -> String {
// TODO: Move elsewhere
fn get_port() -> String {
String::from("3000")
String::from("8000")
}
// TODO: Move elsewhere
@@ -258,7 +335,7 @@ mod tests {
pub const LIMIT: usize = 6;
pub async fn get_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let tm_db_url = icarus_envy::environment::get_db_url().await;
let tm_db_url = icarus_envy::environment::get_db_url().await.value;
let tm_options = sqlx::postgres::PgConnectOptions::from_str(&tm_db_url).unwrap();
sqlx::PgPool::connect_with(tm_options).await
}
@@ -271,7 +348,7 @@ mod tests {
}
pub async fn connect_to_db(db_name: &str) -> Result<sqlx::PgPool, sqlx::Error> {
let db_url = icarus_envy::environment::get_db_url().await;
let db_url = icarus_envy::environment::get_db_url().await.value;
let options = sqlx::postgres::PgConnectOptions::from_str(&db_url)?.database(db_name);
sqlx::PgPool::connect_with(options).await
}
@@ -298,7 +375,7 @@ mod tests {
}
pub async fn get_database_name() -> Result<String, Box<dyn std::error::Error>> {
let database_url = icarus_envy::environment::get_db_url().await;
let database_url = icarus_envy::environment::get_db_url().await.value;
let parsed_url = url::Url::parse(&database_url)?;
if parsed_url.scheme() == "postgres" || parsed_url.scheme() == "postgresql" {
@@ -348,7 +425,7 @@ mod tests {
pub const TEST_USER_ID: uuid::Uuid = uuid::uuid!("cc938368-615a-4694-b2ca-6e122fa31c52");
pub async fn test_token() -> Result<String, josekit::JoseError> {
let key: String = icarus_envy::environment::get_secret_main_key().await;
let key: String = icarus_envy::environment::get_secret_main_key().await.value;
let (message, issuer, audience) = token_fields();
let token_resource = icarus_models::token::TokenResource {
@@ -1693,8 +1770,6 @@ mod tests {
song
);
eprintln!("Song: {:?}", song);
match sequence_flow::queue_coverart_flow(&app, &song_queue_id).await {
Ok(response) => {
let resp = get_resp_data::<
@@ -2094,14 +2169,13 @@ mod tests {
}
}
pub async fn other_song_id() -> Result<(uuid::Uuid, String, String, String), uuid::Error>
{
pub async fn other_song_id()
-> Result<(uuid::Uuid, (String, String), (String, String)), uuid::Error> {
match uuid::Uuid::parse_str("94cf7940-34ff-489f-9124-d0ec90a55af4") {
Ok(id) => Ok((
id,
String::from("tests/I/"),
String::from("track02.flac"),
String::from("tests/I/Coverart-2.jpg"),
(String::from("tests/I/"), String::from("track02.flac")),
(String::from("tests/I/"), String::from("Coverart-2.jpg")),
)),
Err(err) => Err(err),
}
@@ -2294,7 +2368,8 @@ mod tests {
async fn get_test_data(
song_directory: &String,
song_filename: &String,
coverart_path: &String,
coverart_directory: &String,
coverart_filename: &String,
) -> Result<(Vec<u8>, Vec<u8>), std::io::Error> {
let song = icarus_models::song::Song {
directory: song_directory.clone(),
@@ -2303,12 +2378,13 @@ mod tests {
};
let coverart = icarus_models::coverart::CoverArt {
path: coverart_path.clone(),
directory: coverart_directory.clone(),
filename: coverart_filename.clone(),
..Default::default()
};
match song.to_data() {
Ok(song_data) => match coverart.to_data() {
match icarus_models::song::io::to_data(&song) {
Ok(song_data) => match icarus_models::coverart::io::to_data(&coverart) {
Ok(coverart_data) => Ok((song_data, coverart_data)),
Err(err) => Err(err),
},
@@ -2320,45 +2396,26 @@ mod tests {
song_directory: &String,
song_filename: &String,
song_data: Vec<u8>,
coverart_path: &String,
coverart_directory: &String,
coverart_filename: &String,
coverart_data: Vec<u8>,
) -> Result<(), std::io::Error> {
let song = icarus_models::song::Song {
directory: song_directory.clone(),
filename: song_filename.clone(),
data: song_data,
..Default::default()
};
let coverart = icarus_models::coverart::CoverArt {
path: coverart_path.clone(),
directory: coverart_directory.clone(),
filename: coverart_filename.clone(),
data: coverart_data,
..Default::default()
};
use std::io::Write;
match song.song_path() {
Ok(song_path) => {
let song_p = std::path::Path::new(&song_path);
match std::fs::File::create(song_p) {
Ok(mut song_file) => match song_file.write_all(&song_data) {
Ok(_) => {}
Err(err) => {
return Err(err);
}
},
Err(err) => {
return Err(err);
}
}
}
Err(err) => {
return Err(err);
}
}
let coverart_p = std::path::Path::new(&coverart.path);
match std::fs::File::create(coverart_p) {
Ok(mut coverart_file) => match coverart_file.write_all(&coverart_data) {
match song.save_to_filesystem() {
Ok(_) => match coverart.save_to_filesystem() {
Ok(_) => Ok(()),
Err(err) => Err(err),
},
@@ -2385,12 +2442,16 @@ mod tests {
let app = super::init::app(pool).await;
let (id, song_directory, song_filename, coverart_path) =
let (id, (song_directory, song_filename), (cover_directory, cover_filename)) =
test_data::other_song_id().await.unwrap();
let (song_data, coverart_data) =
get_test_data(&song_directory, &song_filename, &coverart_path)
.await
.unwrap();
let (song_data, coverart_data) = get_test_data(
&song_directory,
&song_filename,
&cover_directory,
&cover_filename,
)
.await
.unwrap();
let uri =
super::format_url_with_value(crate::callers::endpoints::DELETESONG, &id).await;
@@ -2428,7 +2489,8 @@ mod tests {
&song_directory,
&song_filename,
song_data,
&coverart_path,
&cover_directory,
&cover_filename,
coverart_data,
)
.await
+2 -1
View File
@@ -53,6 +53,7 @@ CREATE TABLE IF NOT EXISTS "song" (
CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
path TEXT NOT NULL,
directory TEXT NOT NULL,
filename TEXT NOT NULL,
song_id UUID NOT NULL
);
@@ -1,6 +1,6 @@
-- 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 "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, song_id) VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I', 'Coverart-1.jpg', '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 "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, song_id) VALUES('d96122cd-5ae9-4013-9934-60768d3006e9', 'I', 'tests/I', 'Coverart-2.jpg', '94cf7940-34ff-489f-9124-d0ec90a55af4');