Compare commits

...

11 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
19 changed files with 612 additions and 571 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 SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/icarus/mydata ROOT_DIRECTORY=/home/icarus/mydata
POSTGRES_MAIN_USER=icarus 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 SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/icarus/mydata ROOT_DIRECTORY=/home/icarus/mydata
POSTGRES_MAIN_USER=icarus 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 - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.88.0 toolchain: 1.90.0
components: cargo components: cargo
- name: Extract Version from Cargo.toml - name: Extract Version from Cargo.toml
+6 -6
View File
@@ -2,9 +2,9 @@ name: Rust CI
on: on:
push: push:
branches: [ "v0.2" ] branches: [ "main" ]
pull_request: pull_request:
branches: [ "v0.2" ] branches: [ "main" ]
jobs: jobs:
build: build:
@@ -25,12 +25,12 @@ jobs:
--health-retries 5 --health-retries 5
steps: 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 uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.88.0 toolchain: 1.90.0
components: clippy, rustfmt components: clippy, rustfmt
override: true override: true
@@ -69,7 +69,7 @@ jobs:
ROOT_DIRECTORY: "/tmp" ROOT_DIRECTORY: "/tmp"
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}" DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
run: | run: |
cat .env.sample | head -1 > .env cat .env.sample | head -6 > .env
cargo test --verbose -- cargo test --verbose --
- name: Run clippy - name: Run clippy
+4
View File
@@ -7,3 +7,7 @@
.DS_STORE .DS_STORE
Storage/ Storage/
*.sln
*.cs
*.csproj
Generated
+321 -344
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -1,35 +1,35 @@
[package] [package]
name = "icarus" name = "icarus"
version = "0.1.102" version = "0.3.1"
edition = "2024" edition = "2024"
rust-version = "1.88" rust-version = "1.90"
[dependencies] [dependencies]
axum = { version = "0.8.4", features = ["multipart"] } axum = { version = "0.8.6", features = ["multipart"] }
axum-extra = { version = "0.10.1", features = ["cookie"] } axum-extra = { version = "0.10.3", features = ["cookie"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.140" } serde_json = { version = "1.0.145" }
tower = { version = "0.5.2", features = ["full"] } tower = { version = "0.5.2", features = ["full"] }
tokio = { version = "1.45.1", features = ["full"] } tokio = { version = "1.47.1", features = ["full"] }
tokio-util = { version = "0.7.15", features = ["io"] } tokio-util = { version = "0.7.16", features = ["io"] }
tower-http = { version = "0.6.6", features = ["cors", "timeout"] } tower-http = { version = "0.6.6", features = ["cors", "timeout"] }
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.20"
futures = { version = "0.3.31" } futures = { version = "0.3.31" }
mime_guess = { version = "2.0.5" } 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"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] }
time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] } time = { version = "0.3.44", features = ["formatting", "macros", "parsing", "serde"] }
thiserror = "1.0" thiserror = "2.0.17"
base64 = "0.21" base64 = "0.22.1"
jsonwebtoken = { version = "9.3.1" } jsonwebtoken = { version = "9.3.1" }
josekit = { version = "0.10.1" } 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.3.0" } 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.5.6" } 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.3.2" } icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.5.0" }
[dev-dependencies] [dev-dependencies]
common-multipart-rfc7578 = { version = "0.7.0" } common-multipart-rfc7578 = { version = "0.7.0" }
url = { version = "2.5.4" } url = { version = "2.5.7" }
tempfile = { version = "3.20.0" } tempfile = { version = "3.23.0" }
+1 -1
View File
@@ -1,5 +1,5 @@
# Stage 1: Build the application # Stage 1: Build the application
FROM rust:1.88 as builder FROM rust:1.90 as builder
# Set the working directory inside the container # Set the working directory inside the container
WORKDIR /usr/src/app 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.
+8
View File
@@ -2,6 +2,14 @@
Web API for the Icarus project. Web API for the Icarus project.
### Requires
`icarus_auth` v0.6.1
`songparser` v0.4.2
### Compatible with
`icarus-dm` v0.8.2
## 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
are located in the root of the parent directory. Check the respective repositories to ensure they are located in the root of the parent directory. Check the respective repositories to ensure they
+2 -2
View File
@@ -10,7 +10,7 @@ services:
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" # Format: "HOST_PORT:CONTAINER_PORT"
- "8000:3000" - "8000:8000"
env_file: env_file:
- .env - .env
depends_on: depends_on:
@@ -29,7 +29,7 @@ services:
container_name: auth_api container_name: auth_api
restart: unless-stopped restart: unless-stopped
ports: 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:
# Environment variables your API needs, e.g., database connection # Environment variables your API needs, e.g., database connection
# Add other necessary environment variables # 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" ( 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,
song_id UUID NOT NULL song_id UUID NOT NULL
-- TODO: Add type later -- TODO: Add type later
); );
+1 -6
View File
@@ -1,6 +1,3 @@
// use std::collections::BTreeMap;
// use std::sync::Arc;
use axum::{ use axum::{
Json, Json,
http::{Request, StatusCode}, http::{Request, StatusCode},
@@ -10,9 +7,7 @@ use axum::{
use axum_extra::extract::cookie::CookieJar; use axum_extra::extract::cookie::CookieJar;
use jsonwebtoken::{DecodingKey, Validation, decode}; use jsonwebtoken::{DecodingKey, Validation, decode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// use serde_json::{json, Value};
use thiserror::Error; use thiserror::Error;
// use time::OffsetDateTime;
fn deserialize_i64_from_f64<'de, D>(deserializer: D) -> Result<i64, D::Error> fn deserialize_i64_from_f64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where where
@@ -95,7 +90,7 @@ pub async fn auth<B>(
(StatusCode::UNAUTHORIZED, Json(json_error)) (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); let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_audience(&["icarus"]); // Must match exactly what's in the token validation.set_audience(&["icarus"]); // Must match exactly what's in the token
+51 -33
View File
@@ -325,11 +325,12 @@ pub mod cov_db {
) -> Result<uuid::Uuid, sqlx::Error> { ) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query( let result = sqlx::query(
r#" 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.title)
.bind(&coverart.path) .bind(&coverart.directory)
.bind(&coverart.filename)
.bind(song_id) .bind(song_id)
.fetch_one(pool) .fetch_one(pool)
.await .await
@@ -355,7 +356,7 @@ pub mod cov_db {
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> { ) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query( let result = sqlx::query(
r#" 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) .bind(id)
@@ -375,15 +376,19 @@ pub mod cov_db {
.try_get("title") .try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
path: row directory: row
.try_get("path") .try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
data: Vec::new(),
song_id: row song_id: row
.try_get("song_id") .try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
..Default::default()
}), }),
Err(_) => Err(sqlx::Error::RowNotFound), Err(_) => Err(sqlx::Error::RowNotFound),
} }
@@ -395,7 +400,7 @@ pub mod cov_db {
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> { ) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query( let result = sqlx::query(
r#" 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) .bind(song_id)
@@ -415,8 +420,12 @@ pub mod cov_db {
.try_get("title") .try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
path: row directory: row
.try_get("path") .try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
data: Vec::new(), data: Vec::new(),
@@ -437,7 +446,7 @@ pub mod cov_db {
r#" r#"
DELETE FROM "coverart" DELETE FROM "coverart"
WHERE id = $1 WHERE id = $1
RETURNING id, title, path, song_id RETURNING id, title, directory, filename, song_id
"#, "#,
) )
.bind(id) .bind(id)
@@ -457,8 +466,12 @@ pub mod cov_db {
.try_get("title") .try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
path: row directory: row
.try_get("path") .try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
song_id: row song_id: row
@@ -473,8 +486,6 @@ pub mod cov_db {
} }
pub mod endpoint { pub mod endpoint {
use std::io::Write;
use axum::response::IntoResponse; use axum::response::IntoResponse;
/// Endpoint to queue cover art /// Endpoint to queue cover art
@@ -702,35 +713,42 @@ pub mod endpoint {
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::callers::song::song_db::get_song(&pool, &song_id).await {
Ok(song) => { Ok(song) => {
let directory = icarus_envy::environment::get_root_directory().await; let directory = icarus_envy::environment::get_root_directory().await.value;
let dir = std::path::Path::new(&directory);
// TODO: Make this random and the file extension should not be hard coded // TODO: Make this random and the file extension should not be hard coded
let filename = format!("{}-coverart.jpeg", &song.filename[..8]); 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.data = data; coverart.data = data;
let mut file = std::fs::File::create(&save_path).unwrap(); match coverart.save_to_filesystem() {
file.write_all(&coverart.data).unwrap(); 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 { response.message = String::from("Successful");
Ok(id) => { response.data.push(coverart);
// TODO: Populate song_id
coverart.id = id;
println!("Cover Art created");
response.message = String::from("Successful"); (axum::http::StatusCode::OK, axum::Json(response))
response.data.push(coverart); }
Err(err) => {
(axum::http::StatusCode::OK, axum::Json(response)) response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} }
Err(err) => { Err(err) => {
response.message = err.to_string(); response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
} }
} }
} }
@@ -848,7 +866,7 @@ pub mod endpoint {
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 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) => { Ok(data) => {
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();
+65 -93
View File
@@ -81,11 +81,7 @@ pub mod request {
duration: self.duration, duration: self.duration,
audio_type: self.audio_type.clone(), audio_type: self.audio_type.clone(),
user_id: self.user_id, user_id: self.user_id,
// TODO: Change the type of this in icarus_models lib ..Default::default()
date_created: String::new(),
filename: String::new(),
data: Vec::new(),
directory: String::new(),
} }
} }
} }
@@ -224,7 +220,7 @@ pub mod song_db {
pub async fn insert( pub async fn insert(
pool: &sqlx::PgPool, pool: &sqlx::PgPool,
song: &icarus_models::song::Song, song: &icarus_models::song::Song,
) -> Result<(String, uuid::Uuid), sqlx::Error> { ) -> Result<(time::OffsetDateTime, uuid::Uuid), sqlx::Error> {
let result = sqlx::query( let result = sqlx::query(
r#" r#"
INSERT INTO "song" (title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, filename, directory, user_id) INSERT INTO "song" (title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, filename, directory, user_id)
@@ -262,7 +258,7 @@ pub mod song_db {
.try_get("date_created") .try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(); .unwrap();
let date_created = date_created_time.to_string(); let date_created = date_created_time;
Ok((date_created, id)) Ok((date_created, id))
} }
@@ -354,7 +350,7 @@ pub mod song_db {
.try_get("directory") .try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
date_created: date_created_time.to_string(), date_created: Some(date_created_time),
user_id: row user_id: row
.try_get("user_id") .try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
@@ -451,7 +447,7 @@ pub mod song_db {
.try_get("directory") .try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
date_created: date_created_time.to_string(), date_created: Some(date_created_time),
user_id: row user_id: row
.try_get("user_id") .try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
@@ -555,7 +551,7 @@ pub mod song_db {
.try_get("directory") .try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(), .unwrap(),
date_created: date_created_time.to_string(), date_created: Some(date_created_time),
user_id: row user_id: row
.try_get("user_id") .try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound) .map_err(|_e| sqlx::Error::RowNotFound)
@@ -571,11 +567,6 @@ pub mod song_db {
mod song_queue { mod song_queue {
use sqlx::Row; use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
}
// TODO: Move this somewhere else at some point // TODO: Move this somewhere else at some point
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)] #[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct SongQueue { pub struct SongQueue {
@@ -874,8 +865,6 @@ mod song_queue {
pub mod endpoint { pub mod endpoint {
use axum::{Json, http::StatusCode, response::IntoResponse}; use axum::{Json, http::StatusCode, response::IntoResponse};
use std::io::Write;
use crate::callers::song::song_queue; use crate::callers::song::song_queue;
/// Endpoint to queue a song. Starts the process and places the song in a queue /// Endpoint to queue a song. Starts the process and places the song in a queue
@@ -912,11 +901,6 @@ pub mod endpoint {
data.len() 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 raw_data: Vec<u8> = data.to_vec();
let queue_repo = song_queue::insert( let queue_repo = song_queue::insert(
&pool, &pool,
@@ -1207,9 +1191,11 @@ pub mod endpoint {
if payload.is_valid() { if payload.is_valid() {
let mut song = payload.to_song(); let mut song = payload.to_song();
song.filename = song.filename = icarus_models::song::generate_filename(
song.generate_filename(icarus_models::types::MusicTypes::FlacExtension, true); icarus_models::types::MusicTypes::FlacExtension,
song.directory = icarus_envy::environment::get_root_directory().await; true,
);
song.directory = icarus_envy::environment::get_root_directory().await.value;
match song_queue::get_data(&pool, &payload.song_queue_id).await { match song_queue::get_data(&pool, &payload.song_queue_id).await {
Ok(data) => { Ok(data) => {
@@ -1227,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) { (axum::http::StatusCode::OK, axum::Json(response))
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))
}
} }
} Err(err) => {
response.message = format!("{:?} song {:?}", err.to_string(), song);
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
Err(err) => { Err(err) => {
let song_path = song.song_path(); response.message = err.to_string();
response.message = format!(
"{err:?} Song directory: {} Filename: {} Save Path: {:?} Song Path: {:?}",
song.directory, song.filename, save_path, song_path
);
( (
axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response), axum::Json(response),
@@ -1466,7 +1435,7 @@ pub mod endpoint {
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::song_db::get_song(&pool, &id).await { 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) => { Ok(data) => {
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();
@@ -1522,49 +1491,52 @@ pub mod endpoint {
.await .await
{ {
Ok(coverart) => { Ok(coverart) => {
let coverart_path = std::path::Path::new(&coverart.path); let coverart_path_str = match coverart.get_path() {
if coverart_path.exists() { Ok(path) => path,
match song.song_path() { Err(err) => {
Ok(song_path) => { response.message = err.to_string();
match super::song_db::delete_song(&pool, &song.id).await { return (
Ok(deleted_song) => { axum::http::StatusCode::INTERNAL_SERVER_ERROR,
match super::super::coverart::cov_db::delete_coverart( axum::Json(response),
&pool, );
&coverart.id, }
) };
.await let coverart_path = std::path::Path::new(&coverart_path_str);
{
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))
}
}
}
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) => { Err(err) => {
response.message = err.to_string(); response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response)) (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
} }
} }
} }
Err(err) => { Err(err) => {
response.message = err.to_string(); response.message = err.to_string();
( (
+87 -61
View File
@@ -10,7 +10,7 @@ pub mod db {
} }
pub async fn create_pool() -> Result<sqlx::PgPool, sqlx::Error> { 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}"); println!("Database url: {database_url}");
PgPoolOptions::new() PgPoolOptions::new()
@@ -45,16 +45,12 @@ async fn main() {
} }
pub mod init { pub mod init {
use axum::routing::{delete, get, patch, post};
use std::time::Duration; use std::time::Duration;
use axum::routing::{delete, get, patch, post};
use tower_http::timeout::TimeoutLayer; use tower_http::timeout::TimeoutLayer;
use utoipa::OpenApi; use utoipa::OpenApi;
use axum::http::{
HeaderValue, Method,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
};
use crate::callers::coverart as coverart_caller; use crate::callers::coverart as coverart_caller;
use crate::callers::metadata as metadata_caller; use crate::callers::metadata as metadata_caller;
use crate::callers::song as song_caller; use crate::callers::song as song_caller;
@@ -65,6 +61,54 @@ pub mod init {
use song_caller::endpoint as song_endpoints; use song_caller::endpoint as song_endpoints;
use song_caller::response as song_responses; 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)] #[derive(utoipa::OpenApi)]
#[openapi( #[openapi(
paths(song_endpoints::queue_song, song_endpoints::link_user_id, song_endpoints::fetch_queue_song, song_endpoints::download_flac, paths(song_endpoints::queue_song, song_endpoints::link_user_id, song_endpoints::fetch_queue_song, song_endpoints::download_flac,
@@ -226,6 +270,7 @@ pub mod init {
crate::callers::endpoints::GETALLSONGS, crate::callers::endpoints::GETALLSONGS,
get(crate::callers::song::endpoint::get_all_songs), get(crate::callers::song::endpoint::get_all_songs),
) )
.layer(cors::configure_cors().await)
} }
pub async fn app() -> axum::Router { pub async fn app() -> axum::Router {
@@ -235,11 +280,7 @@ pub mod init {
// TODO: Look into handling this. Seems redundant to run migrations multiple times // TODO: Look into handling this. Seems redundant to run migrations multiple times
crate::db::migrations(&pool).await; crate::db::migrations(&pool).await;
let cors = tower_http::cors::CorsLayer::new() let cors = cors::configure_cors().await;
.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]);
routes() routes()
.await .await
@@ -265,7 +306,7 @@ fn get_address() -> String {
// TODO: Move elsewhere // TODO: Move elsewhere
fn get_port() -> String { fn get_port() -> String {
String::from("3000") String::from("8000")
} }
// TODO: Move elsewhere // TODO: Move elsewhere
@@ -294,7 +335,7 @@ mod tests {
pub const LIMIT: usize = 6; pub const LIMIT: usize = 6;
pub async fn get_pool() -> Result<sqlx::PgPool, sqlx::Error> { 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(); let tm_options = sqlx::postgres::PgConnectOptions::from_str(&tm_db_url).unwrap();
sqlx::PgPool::connect_with(tm_options).await sqlx::PgPool::connect_with(tm_options).await
} }
@@ -307,7 +348,7 @@ mod tests {
} }
pub async fn connect_to_db(db_name: &str) -> Result<sqlx::PgPool, sqlx::Error> { 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); let options = sqlx::postgres::PgConnectOptions::from_str(&db_url)?.database(db_name);
sqlx::PgPool::connect_with(options).await sqlx::PgPool::connect_with(options).await
} }
@@ -334,7 +375,7 @@ mod tests {
} }
pub async fn get_database_name() -> Result<String, Box<dyn std::error::Error>> { 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)?; let parsed_url = url::Url::parse(&database_url)?;
if parsed_url.scheme() == "postgres" || parsed_url.scheme() == "postgresql" { if parsed_url.scheme() == "postgres" || parsed_url.scheme() == "postgresql" {
@@ -384,7 +425,7 @@ mod tests {
pub const TEST_USER_ID: uuid::Uuid = uuid::uuid!("cc938368-615a-4694-b2ca-6e122fa31c52"); pub const TEST_USER_ID: uuid::Uuid = uuid::uuid!("cc938368-615a-4694-b2ca-6e122fa31c52");
pub async fn test_token() -> Result<String, josekit::JoseError> { 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 (message, issuer, audience) = token_fields();
let token_resource = icarus_models::token::TokenResource { let token_resource = icarus_models::token::TokenResource {
@@ -1729,8 +1770,6 @@ mod tests {
song song
); );
eprintln!("Song: {:?}", song);
match sequence_flow::queue_coverart_flow(&app, &song_queue_id).await { match sequence_flow::queue_coverart_flow(&app, &song_queue_id).await {
Ok(response) => { Ok(response) => {
let resp = get_resp_data::< let resp = get_resp_data::<
@@ -2130,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") { match uuid::Uuid::parse_str("94cf7940-34ff-489f-9124-d0ec90a55af4") {
Ok(id) => Ok(( Ok(id) => Ok((
id, id,
String::from("tests/I/"), (String::from("tests/I/"), String::from("track02.flac")),
String::from("track02.flac"), (String::from("tests/I/"), String::from("Coverart-2.jpg")),
String::from("tests/I/Coverart-2.jpg"),
)), )),
Err(err) => Err(err), Err(err) => Err(err),
} }
@@ -2330,7 +2368,8 @@ mod tests {
async fn get_test_data( async fn get_test_data(
song_directory: &String, song_directory: &String,
song_filename: &String, song_filename: &String,
coverart_path: &String, coverart_directory: &String,
coverart_filename: &String,
) -> Result<(Vec<u8>, Vec<u8>), std::io::Error> { ) -> Result<(Vec<u8>, Vec<u8>), std::io::Error> {
let song = icarus_models::song::Song { let song = icarus_models::song::Song {
directory: song_directory.clone(), directory: song_directory.clone(),
@@ -2339,12 +2378,13 @@ mod tests {
}; };
let coverart = icarus_models::coverart::CoverArt { let coverart = icarus_models::coverart::CoverArt {
path: coverart_path.clone(), directory: coverart_directory.clone(),
filename: coverart_filename.clone(),
..Default::default() ..Default::default()
}; };
match song.to_data() { match icarus_models::song::io::to_data(&song) {
Ok(song_data) => match coverart.to_data() { Ok(song_data) => match icarus_models::coverart::io::to_data(&coverart) {
Ok(coverart_data) => Ok((song_data, coverart_data)), Ok(coverart_data) => Ok((song_data, coverart_data)),
Err(err) => Err(err), Err(err) => Err(err),
}, },
@@ -2356,45 +2396,26 @@ mod tests {
song_directory: &String, song_directory: &String,
song_filename: &String, song_filename: &String,
song_data: Vec<u8>, song_data: Vec<u8>,
coverart_path: &String, coverart_directory: &String,
coverart_filename: &String,
coverart_data: Vec<u8>, coverart_data: Vec<u8>,
) -> Result<(), std::io::Error> { ) -> Result<(), std::io::Error> {
let song = icarus_models::song::Song { let song = icarus_models::song::Song {
directory: song_directory.clone(), directory: song_directory.clone(),
filename: song_filename.clone(), filename: song_filename.clone(),
data: song_data,
..Default::default() ..Default::default()
}; };
let coverart = icarus_models::coverart::CoverArt { let coverart = icarus_models::coverart::CoverArt {
path: coverart_path.clone(), directory: coverart_directory.clone(),
filename: coverart_filename.clone(),
data: coverart_data,
..Default::default() ..Default::default()
}; };
use std::io::Write; match song.save_to_filesystem() {
Ok(_) => match coverart.save_to_filesystem() {
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) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
@@ -2421,12 +2442,16 @@ mod tests {
let app = super::init::app(pool).await; 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(); test_data::other_song_id().await.unwrap();
let (song_data, coverart_data) = let (song_data, coverart_data) = get_test_data(
get_test_data(&song_directory, &song_filename, &coverart_path) &song_directory,
.await &song_filename,
.unwrap(); &cover_directory,
&cover_filename,
)
.await
.unwrap();
let uri = let uri =
super::format_url_with_value(crate::callers::endpoints::DELETESONG, &id).await; super::format_url_with_value(crate::callers::endpoints::DELETESONG, &id).await;
@@ -2464,7 +2489,8 @@ mod tests {
&song_directory, &song_directory,
&song_filename, &song_filename,
song_data, song_data,
&coverart_path, &cover_directory,
&cover_filename,
coverart_data, coverart_data,
) )
.await .await
+2 -1
View File
@@ -53,6 +53,7 @@ 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,
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, 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 "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');