Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 598996cb8d | |||
| 21a97c150c | |||
| 679a4ad322 | |||
| 9aefeace23 | |||
| a0c6ae65a3 | |||
| c7230d3b32 | |||
| 703fc49f32 | |||
| e98e055de0 | |||
| a8603361c0 | |||
| dbda9a3897 |
@@ -68,7 +68,9 @@ jobs:
|
||||
env:
|
||||
ROOT_DIRECTORY: "/tmp"
|
||||
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
|
||||
run: cargo test --verbose
|
||||
run: |
|
||||
cat .env.sample | head -1 > .env
|
||||
cargo test --verbose --
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dev": {
|
||||
"migrations": "./migrations"
|
||||
},
|
||||
"test": {
|
||||
"migrations": "./test_migrations"
|
||||
}
|
||||
}
|
||||
Generated
+201
-9
@@ -17,12 +17,27 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
@@ -93,6 +108,29 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
@@ -108,6 +146,12 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -222,6 +266,17 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -588,8 +643,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -630,6 +687,16 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hdrhistogram"
|
||||
version = "7.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -752,18 +819,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icarus"
|
||||
version = "0.1.93"
|
||||
version = "0.1.102"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"base64 0.21.7",
|
||||
"common-multipart-rfc7578",
|
||||
"futures",
|
||||
"icarus_envy",
|
||||
"icarus_meta",
|
||||
"icarus_models",
|
||||
"josekit",
|
||||
"jsonwebtoken",
|
||||
"mime_guess",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -776,8 +849,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icarus_envy"
|
||||
version = "0.3.0"
|
||||
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_envy.git?tag=v0.3.0-devel-d73fba9899-006#d73fba9899372b0655a90cb426645930135152da"
|
||||
version = "0.3.2"
|
||||
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_envy.git?tag=v0.3.2#d84a8144aedf02e1b459d67c4023a7e0833f89fd"
|
||||
dependencies = [
|
||||
"const_format",
|
||||
"dotenvy",
|
||||
@@ -786,16 +859,17 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "icarus_meta"
|
||||
version = "0.3.0"
|
||||
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_meta.git?tag=v0.3.0-devel-f4b71de969-680#f4b71de9692029a706b1ce82c39f6715c560158e"
|
||||
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_meta.git?tag=v0.3.0#f87c049ab3139995230485b33038a0425f2e7430"
|
||||
dependencies = [
|
||||
"lofty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icarus_models"
|
||||
version = "0.4.5"
|
||||
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.4.5-devel-655d05dabb-111#655d05dabbdadb9b28940564a1eb82470aa4f166"
|
||||
version = "0.5.5"
|
||||
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.5.5#f43bcaa314694c25cea65dbfcfbabc8fc7bf395a"
|
||||
dependencies = [
|
||||
"josekit",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -926,6 +1000,23 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "josekit"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"flate2",
|
||||
"openssl",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@@ -936,6 +1027,21 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -1123,6 +1229,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
@@ -1285,6 +1401,16 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -1455,6 +1581,49 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.16",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.8"
|
||||
@@ -1570,6 +1739,7 @@ version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -1654,6 +1824,18 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
@@ -1717,7 +1899,7 @@ version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
@@ -1793,7 +1975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@@ -1837,7 +2019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"crc",
|
||||
@@ -2125,9 +2307,13 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"hdrhistogram",
|
||||
"indexmap",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -2264,6 +2450,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
|
||||
+13
-7
@@ -1,25 +1,31 @@
|
||||
[package]
|
||||
name = "icarus"
|
||||
version = "0.1.93"
|
||||
version = "0.1.102"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
|
||||
[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" }
|
||||
tower = { version = "0.5.2" }
|
||||
tower = { version = "0.5.2", features = ["full"] }
|
||||
tokio = { version = "1.45.1", features = ["full"] }
|
||||
tokio-util = { version = "0.7.15" }
|
||||
tower-http = { version = "0.6.6", features = ["timeout"] }
|
||||
tokio-util = { version = "0.7.15", features = ["io"] }
|
||||
tower-http = { version = "0.6.6", features = ["cors", "timeout"] }
|
||||
tracing-subscriber = "0.3.19"
|
||||
futures = { version = "0.3.31" }
|
||||
mime_guess = { version = "2.0.5" }
|
||||
uuid = { version = "1.17.0", 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"] }
|
||||
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0-devel-f4b71de969-680" }
|
||||
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.4.5-devel-655d05dabb-111" }
|
||||
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.0-devel-d73fba9899-006" }
|
||||
thiserror = "1.0"
|
||||
base64 = "0.21"
|
||||
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" }
|
||||
|
||||
[dev-dependencies]
|
||||
common-multipart-rfc7578 = { version = "0.7.0" }
|
||||
|
||||
+24
-7
@@ -17,7 +17,7 @@ services:
|
||||
main_db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- main-api-network
|
||||
- icarus-network
|
||||
restart: unless-stopped # Optional: Restart policy
|
||||
|
||||
# --- Web API auth ---
|
||||
@@ -39,11 +39,30 @@ services:
|
||||
auth_db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- auth-api-network
|
||||
- icarus-network
|
||||
# Optional: Mount local code for development (live reload)
|
||||
# volumes:
|
||||
# - ./path/to/your/web-api-repo:/app
|
||||
|
||||
# --- songparser service ---
|
||||
songparser:
|
||||
build:
|
||||
context: ../songparser
|
||||
ssh: ["default"]
|
||||
dockerfile: Dockerfile
|
||||
container_name: songparser
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ../songparser/.env
|
||||
depends_on:
|
||||
- api
|
||||
- main_db
|
||||
- auth_api
|
||||
- auth_db
|
||||
networks:
|
||||
- icarus-network
|
||||
|
||||
|
||||
# PostgreSQL Database Service
|
||||
# --- icarus web api db ---
|
||||
main_db:
|
||||
@@ -69,7 +88,7 @@ services:
|
||||
start_period: 10s
|
||||
restart: always # Optional: Restart policy
|
||||
networks:
|
||||
- main-api-network
|
||||
- icarus-network
|
||||
|
||||
# --- icarus web auth api db ---
|
||||
auth_db:
|
||||
@@ -95,7 +114,7 @@ services:
|
||||
start_period: 10s
|
||||
restart: always # Optional: Restart policy
|
||||
networks:
|
||||
- auth-api-network
|
||||
- icarus-network
|
||||
|
||||
# Define the named volume for data persistence
|
||||
volumes:
|
||||
@@ -106,7 +125,5 @@ volumes:
|
||||
|
||||
# Define the network (optional, but good practice)
|
||||
networks:
|
||||
main-api-network:
|
||||
driver: bridge
|
||||
auth-api-network:
|
||||
icarus-network:
|
||||
driver: bridge
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// use std::collections::BTreeMap;
|
||||
// use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
http::{Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use jsonwebtoken::{DecodingKey, Validation, decode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use serde_json::{json, Value};
|
||||
use thiserror::Error;
|
||||
// use time::OffsetDateTime;
|
||||
|
||||
fn deserialize_i64_from_f64<'de, D>(deserializer: D) -> Result<i64, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let val = f64::deserialize(deserializer)?;
|
||||
// Handle NaN and infinity cases
|
||||
if val.is_nan() || val.is_infinite() {
|
||||
return Err(serde::de::Error::custom("invalid float value"));
|
||||
}
|
||||
// Round to nearest integer and convert
|
||||
let rounded = val.round();
|
||||
// Check if the rounded value can fit in i64
|
||||
if rounded < (i64::MIN as f64) || rounded > (i64::MAX as f64) {
|
||||
return Err(serde::de::Error::custom("float out of i64 range"));
|
||||
}
|
||||
Ok(rounded as i64)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserClaims {
|
||||
pub iss: String,
|
||||
pub aud: String, // Audience
|
||||
pub sub: String, // Subject (user ID)
|
||||
#[serde(deserialize_with = "deserialize_i64_from_f64")]
|
||||
pub exp: i64, // Expiration time (UTC timestamp)
|
||||
#[serde(deserialize_with = "deserialize_i64_from_f64")]
|
||||
pub iat: i64, // Issued at (UTC timestamp)
|
||||
// pub azp: String,
|
||||
// pub gty: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub roles: Option<Vec<String>>, // Optional roles
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum JwtError {
|
||||
#[error("Token creation failed")]
|
||||
TokenCreation,
|
||||
#[error("Token verification failed")]
|
||||
TokenVerification,
|
||||
#[error("Invalid token")]
|
||||
InvalidToken,
|
||||
#[error("Token expired")]
|
||||
ExpiredToken,
|
||||
#[error("Invalid key")]
|
||||
InvalidKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub status: &'static str,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub async fn auth<B>(
|
||||
cookie_jar: CookieJar,
|
||||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
|
||||
println!("Cookie: {cookie_jar:?}");
|
||||
|
||||
let token = cookie_jar
|
||||
.get("token")
|
||||
.map(|cookie| cookie.value().to_string())
|
||||
.or_else(|| {
|
||||
req.headers()
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|auth_header| auth_header.to_str().ok())
|
||||
.and_then(|auth_value| {
|
||||
println!("Auth value: {auth_value:?}");
|
||||
auth_value.strip_prefix("Bearer ").map(String::from)
|
||||
})
|
||||
});
|
||||
|
||||
let token = token.ok_or_else(|| {
|
||||
let json_error = ErrorResponse {
|
||||
status: "fail",
|
||||
message: "You are not logged in, please provide token".to_string(),
|
||||
};
|
||||
(StatusCode::UNAUTHORIZED, Json(json_error))
|
||||
})?;
|
||||
|
||||
let secret_key = icarus_envy::environment::get_secret_main_key().await;
|
||||
|
||||
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.set_audience(&["icarus"]); // Must match exactly what's in the token
|
||||
let claims = decode::<UserClaims>(
|
||||
&token,
|
||||
// TODO: Replace with code to get secret from env
|
||||
&DecodingKey::from_secret(secret_key.as_ref()),
|
||||
&validation,
|
||||
)
|
||||
.map_err(|err| {
|
||||
eprintln!("Error: {err:?}");
|
||||
let json_error = ErrorResponse {
|
||||
status: "fail",
|
||||
message: "Invalid token - Error decoding claims".to_string(),
|
||||
};
|
||||
(StatusCode::UNAUTHORIZED, Json(json_error))
|
||||
})?
|
||||
.claims;
|
||||
|
||||
println!("Claims: {claims:?}");
|
||||
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
@@ -45,6 +45,13 @@ pub mod request {
|
||||
pub coverart_queue_id: uuid::Uuid,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get_coverart {
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Params {
|
||||
pub id: Option<uuid::Uuid>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod response {
|
||||
@@ -99,6 +106,14 @@ pub mod response {
|
||||
pub data: Vec<uuid::Uuid>,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get_coverart {
|
||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Response {
|
||||
pub message: String,
|
||||
pub data: Vec<icarus_models::coverart::CoverArt>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod db {
|
||||
@@ -321,6 +336,128 @@ pub mod cov_db {
|
||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_coverart(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &uuid::Uuid,
|
||||
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT id, title, path, song_id FROM "coverart" WHERE id = $1;
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error querying data: {e:?}");
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(row) => Ok(icarus_models::coverart::CoverArt {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
title: row
|
||||
.try_get("title")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
path: row
|
||||
.try_get("path")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
data: Vec::new(),
|
||||
song_id: row
|
||||
.try_get("song_id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
}),
|
||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_coverart_with_song_id(
|
||||
pool: &sqlx::PgPool,
|
||||
song_id: &uuid::Uuid,
|
||||
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT id, title, path, song_id FROM "coverart" WHERE song_id = $1;
|
||||
"#,
|
||||
)
|
||||
.bind(song_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error querying data: {e:?}");
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(row) => Ok(icarus_models::coverart::CoverArt {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
title: row
|
||||
.try_get("title")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
path: row
|
||||
.try_get("path")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
data: Vec::new(),
|
||||
song_id: row
|
||||
.try_get("song_id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
}),
|
||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_coverart(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &uuid::Uuid,
|
||||
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM "coverart"
|
||||
WHERE id = $1
|
||||
RETURNING id, title, path, song_id
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error deleting data: {e:?}");
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(row) => Ok(icarus_models::coverart::CoverArt {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
title: row
|
||||
.try_get("title")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
path: row
|
||||
.try_get("path")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
song_id: row
|
||||
.try_get("song_id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
data: Vec::new(),
|
||||
}),
|
||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod endpoint {
|
||||
@@ -507,7 +644,10 @@ pub mod endpoint {
|
||||
|
||||
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);
|
||||
|
||||
@@ -560,4 +700,68 @@ pub mod endpoint {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_coverart(
|
||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||
axum::extract::Query(params): axum::extract::Query<super::request::get_coverart::Params>,
|
||||
) -> (
|
||||
axum::http::StatusCode,
|
||||
axum::Json<super::response::get_coverart::Response>,
|
||||
) {
|
||||
let mut response = super::response::get_coverart::Response::default();
|
||||
|
||||
match params.id {
|
||||
Some(id) => match super::cov_db::get_coverart(&pool, &id).await {
|
||||
Ok(coverart) => {
|
||||
response.data.push(coverart);
|
||||
response.message = String::from(super::super::response::SUCCESSFUL);
|
||||
(axum::http::StatusCode::OK, axum::Json(response))
|
||||
}
|
||||
Err(err) => {
|
||||
response.message = err.to_string();
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
response.message = String::from("Invalid parameters");
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(data) => {
|
||||
let bytes = axum::body::Bytes::from(data);
|
||||
let mut response = bytes.into_response();
|
||||
let headers = response.headers_mut();
|
||||
// TODO: Address hard coding
|
||||
headers.insert(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"audio/jpg".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{id}.jpg\"")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
(axum::http::StatusCode::OK, response)
|
||||
}
|
||||
Err(_err) => (
|
||||
axum::http::StatusCode::NOT_FOUND,
|
||||
axum::response::Response::default(),
|
||||
),
|
||||
},
|
||||
Err(_err) => (
|
||||
axum::http::StatusCode::NOT_FOUND,
|
||||
axum::response::Response::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,14 @@ pub mod endpoints {
|
||||
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
|
||||
|
||||
pub const CREATESONG: &str = "/api/v2/song";
|
||||
pub const GETSONGS: &str = "/api/v2/song";
|
||||
pub const GETALLSONGS: &str = "/api/v2/song/all";
|
||||
pub const STREAMSONG: &str = "/api/v2/song/stream/{id}";
|
||||
pub const DOWNLOADSONG: &str = "/api/v2/song/download/{id}";
|
||||
pub const DELETESONG: &str = "/api/v2/song/{id}";
|
||||
pub const CREATECOVERART: &str = "/api/v2/coverart";
|
||||
pub const GETCOVERART: &str = "/api/v2/coverart";
|
||||
pub const DOWNLOADCOVERART: &str = "/api/v2/coverart/download/{id}";
|
||||
}
|
||||
|
||||
pub mod response {
|
||||
|
||||
@@ -92,6 +92,13 @@ pub mod request {
|
||||
pub user_id: uuid::Uuid,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get_songs {
|
||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Params {
|
||||
pub id: Option<uuid::Uuid>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod response {
|
||||
@@ -158,6 +165,28 @@ pub mod response {
|
||||
pub data: Vec<uuid::Uuid>,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get_songs {
|
||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Response {
|
||||
pub message: String,
|
||||
pub data: Vec<icarus_models::song::Song>,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod delete_song {
|
||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct SongAndCoverArt {
|
||||
pub song: icarus_models::song::Song,
|
||||
pub coverart: icarus_models::coverart::CoverArt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Response {
|
||||
pub message: String,
|
||||
pub data: Vec<SongAndCoverArt>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Might make a distinction between year and date in a song's tag at some point
|
||||
@@ -321,6 +350,207 @@ pub mod song_db {
|
||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all_songs(
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result<Vec<icarus_models::song::Song>, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT * FROM "song";
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error querying data: {e:?}");
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(rows) => {
|
||||
let mut songs: Vec<icarus_models::song::Song> = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
let date_created_time: time::OffsetDateTime = row
|
||||
.try_get("date_created")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap();
|
||||
|
||||
let song = icarus_models::song::Song {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
title: row
|
||||
.try_get("title")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
artist: row
|
||||
.try_get("artist")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
album_artist: row
|
||||
.try_get("album_artist")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
album: row
|
||||
.try_get("album")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
genre: row
|
||||
.try_get("genre")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
year: row
|
||||
.try_get("year")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
track: row
|
||||
.try_get("track")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
disc: row
|
||||
.try_get("disc")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
track_count: row
|
||||
.try_get("track_count")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
disc_count: row
|
||||
.try_get("disc_count")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
duration: row
|
||||
.try_get("duration")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
audio_type: row
|
||||
.try_get("audio_type")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
filename: row
|
||||
.try_get("filename")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
directory: row
|
||||
.try_get("directory")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
date_created: date_created_time.to_string(),
|
||||
user_id: row
|
||||
.try_get("user_id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
data: Vec::new(),
|
||||
};
|
||||
|
||||
songs.push(song);
|
||||
}
|
||||
|
||||
Ok(songs)
|
||||
}
|
||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_song(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &uuid::Uuid,
|
||||
) -> Result<icarus_models::song::Song, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
// icarus_models::song::Song,
|
||||
r#"
|
||||
DELETE FROM "song"
|
||||
WHERE id = $1
|
||||
RETURNING id, title, artist, album, album_artist, genre, year, disc, track, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error deleting data: {e:?}")
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(row) => {
|
||||
let date_created_time: time::OffsetDateTime = row
|
||||
.try_get("date_created")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap();
|
||||
|
||||
Ok(icarus_models::song::Song {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
title: row
|
||||
.try_get("title")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
artist: row
|
||||
.try_get("artist")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
album_artist: row
|
||||
.try_get("album_artist")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
album: row
|
||||
.try_get("album")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
genre: row
|
||||
.try_get("genre")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
year: row
|
||||
.try_get("year")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
track: row
|
||||
.try_get("track")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
disc: row
|
||||
.try_get("disc")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
track_count: row
|
||||
.try_get("track_count")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
disc_count: row
|
||||
.try_get("disc_count")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
duration: row
|
||||
.try_get("duration")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
audio_type: row
|
||||
.try_get("audio_type")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
filename: row
|
||||
.try_get("filename")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
directory: row
|
||||
.try_get("directory")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
date_created: date_created_time.to_string(),
|
||||
user_id: row
|
||||
.try_get("user_id")
|
||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||
.unwrap(),
|
||||
data: Vec::new(),
|
||||
})
|
||||
}
|
||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod song_queue {
|
||||
@@ -730,6 +960,7 @@ pub mod endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Rename
|
||||
pub async fn download_flac(
|
||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||
@@ -968,4 +1199,232 @@ pub mod endpoint {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_songs(
|
||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||
axum::extract::Query(params): axum::extract::Query<super::request::get_songs::Params>,
|
||||
) -> (
|
||||
axum::http::StatusCode,
|
||||
Json<super::response::get_songs::Response>,
|
||||
) {
|
||||
let mut response = super::response::get_songs::Response::default();
|
||||
|
||||
match params.id {
|
||||
Some(id) => match super::song_db::get_song(&pool, &id).await {
|
||||
Ok(song) => {
|
||||
response.message = String::from(super::super::response::SUCCESSFUL);
|
||||
response.data.push(song);
|
||||
(axum::http::StatusCode::OK, axum::Json(response))
|
||||
}
|
||||
Err(err) => {
|
||||
response.message = err.to_string();
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
response.message = String::from("Invalid parameters");
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all_songs(
|
||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||
) -> (
|
||||
axum::http::StatusCode,
|
||||
axum::Json<super::response::get_songs::Response>,
|
||||
) {
|
||||
let mut response = super::response::get_songs::Response::default();
|
||||
|
||||
match super::song_db::get_all_songs(&pool).await {
|
||||
Ok(songs) => {
|
||||
response.message = String::from(super::super::response::SUCCESSFUL);
|
||||
response.data = songs;
|
||||
(axum::http::StatusCode::OK, axum::Json(response))
|
||||
}
|
||||
Err(err) => {
|
||||
response.message = err.to_string();
|
||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stream_song(
|
||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match super::song_db::get_song(&pool, &id).await {
|
||||
Ok(song) => {
|
||||
let song_path = song.song_path().unwrap();
|
||||
let path = std::path::Path::new(&song_path);
|
||||
|
||||
if !path.starts_with(&song.directory) || !path.exists() {
|
||||
return Err((axum::http::StatusCode::NOT_FOUND, "File not found"));
|
||||
}
|
||||
|
||||
let file = match tokio::fs::File::open(&path).await {
|
||||
Ok(file) => file,
|
||||
Err(_) => return Err((axum::http::StatusCode::NOT_FOUND, "File not found")),
|
||||
};
|
||||
|
||||
let file_size = match file.metadata().await {
|
||||
Ok(meta) => meta.len(),
|
||||
Err(_) => {
|
||||
return Err((
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not read file",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
let stream = tokio_util::io::ReaderStream::new(file);
|
||||
|
||||
let rep = axum::response::Response::builder()
|
||||
.header("content-type", mime.to_string())
|
||||
.header("accept-ranges", "bytes")
|
||||
.header("content-length", file_size.to_string())
|
||||
.body(axum::body::Body::from_stream(stream))
|
||||
.unwrap();
|
||||
|
||||
Ok(rep)
|
||||
}
|
||||
Err(_err) => Err((
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not find file",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
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(data) => {
|
||||
let bytes = axum::body::Bytes::from(data);
|
||||
let mut response = bytes.into_response();
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"audio/flac".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{id}.flac\"")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
(axum::http::StatusCode::OK, response)
|
||||
}
|
||||
Err(_err) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::response::Response::default(),
|
||||
),
|
||||
},
|
||||
Err(_err) => (
|
||||
axum::http::StatusCode::NOT_FOUND,
|
||||
axum::response::Response::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_song(
|
||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||
) -> (
|
||||
axum::http::StatusCode,
|
||||
axum::Json<super::response::delete_song::Response>,
|
||||
) {
|
||||
let mut response = super::response::delete_song::Response::default();
|
||||
|
||||
match super::song_db::get_song(&pool, &id).await {
|
||||
Ok(song) => {
|
||||
match super::super::coverart::cov_db::get_coverart_with_song_id(&pool, &song.id)
|
||||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(response),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.message =
|
||||
String::from("Could not locate coverart on the filesystem");
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(response),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
response.message = err.to_string();
|
||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
response.message = err.to_string();
|
||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+730
-23
@@ -1,3 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod callers;
|
||||
|
||||
pub mod db {
|
||||
@@ -44,76 +45,154 @@ async fn main() {
|
||||
}
|
||||
|
||||
pub mod init {
|
||||
use axum::routing::{get, patch, post};
|
||||
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},
|
||||
};
|
||||
|
||||
pub async fn routes() -> axum::Router {
|
||||
axum::Router::new()
|
||||
.route(crate::ROOT, get(crate::root))
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUESONG,
|
||||
post(crate::callers::song::endpoint::queue_song),
|
||||
post(crate::callers::song::endpoint::queue_song).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUESONG,
|
||||
patch(crate::callers::song::endpoint::update_song_queue_status),
|
||||
patch(crate::callers::song::endpoint::update_song_queue_status).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUESONGLINKUSERID,
|
||||
patch(crate::callers::song::endpoint::link_user_id),
|
||||
patch(crate::callers::song::endpoint::link_user_id).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUESONGDATA,
|
||||
get(crate::callers::song::endpoint::download_flac),
|
||||
get(crate::callers::song::endpoint::download_flac).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::NEXTQUEUESONG,
|
||||
get(crate::callers::song::endpoint::fetch_queue_song),
|
||||
get(crate::callers::song::endpoint::fetch_queue_song).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUESONGUPDATE,
|
||||
patch(crate::callers::song::endpoint::update_song_queue),
|
||||
patch(crate::callers::song::endpoint::update_song_queue).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUESONGDATAWIPE,
|
||||
patch(crate::callers::song::endpoint::wipe_data_from_song_queue),
|
||||
patch(crate::callers::song::endpoint::wipe_data_from_song_queue).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUEMETADATA,
|
||||
post(crate::callers::metadata::endpoint::queue_metadata),
|
||||
post(crate::callers::metadata::endpoint::queue_metadata).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUEMETADATA,
|
||||
get(crate::callers::metadata::endpoint::fetch_metadata),
|
||||
get(crate::callers::metadata::endpoint::fetch_metadata).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUECOVERART,
|
||||
post(crate::callers::coverart::endpoint::queue),
|
||||
post(crate::callers::coverart::endpoint::queue).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUECOVERARTDATA,
|
||||
get(crate::callers::coverart::endpoint::fetch_coverart_with_data),
|
||||
get(crate::callers::coverart::endpoint::fetch_coverart_with_data).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUECOVERART,
|
||||
get(crate::callers::coverart::endpoint::fetch_coverart_no_data),
|
||||
get(crate::callers::coverart::endpoint::fetch_coverart_no_data).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUECOVERARTLINK,
|
||||
patch(crate::callers::coverart::endpoint::link),
|
||||
patch(crate::callers::coverart::endpoint::link).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::QUEUECOVERARTDATAWIPE,
|
||||
patch(crate::callers::coverart::endpoint::wipe_data_from_coverart_queue),
|
||||
patch(crate::callers::coverart::endpoint::wipe_data_from_coverart_queue)
|
||||
.route_layer(axum::middleware::from_fn(
|
||||
crate::auth::auth::<axum::body::Body>,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::CREATESONG,
|
||||
post(crate::callers::song::endpoint::create_metadata),
|
||||
post(crate::callers::song::endpoint::create_metadata).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::CREATECOVERART,
|
||||
post(crate::callers::coverart::endpoint::create_coverart),
|
||||
post(crate::callers::coverart::endpoint::create_coverart).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::GETSONGS,
|
||||
get(crate::callers::song::endpoint::get_songs).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::GETCOVERART,
|
||||
get(crate::callers::coverart::endpoint::get_coverart).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::DOWNLOADCOVERART,
|
||||
get(crate::callers::coverart::endpoint::download_coverart).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::STREAMSONG,
|
||||
get(crate::callers::song::endpoint::stream_song).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::DOWNLOADSONG,
|
||||
get(crate::callers::song::endpoint::download_song).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::DELETESONG,
|
||||
delete(crate::callers::song::endpoint::delete_song).route_layer(
|
||||
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
crate::callers::endpoints::GETALLSONGS,
|
||||
get(crate::callers::song::endpoint::get_all_songs),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -121,13 +200,21 @@ pub mod init {
|
||||
let pool = crate::db::create_pool()
|
||||
.await
|
||||
.expect("Failed to create pool");
|
||||
// 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]);
|
||||
|
||||
routes()
|
||||
.await
|
||||
.layer(axum::Extension(pool))
|
||||
.layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024))
|
||||
.layer(TimeoutLayer::new(Duration::from_secs(300)))
|
||||
.layer(cors)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +314,15 @@ mod tests {
|
||||
Err("Error parsing".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn migrations(pool: &sqlx::PgPool) {
|
||||
// Run migrations using the sqlx::migrate! macro
|
||||
// Assumes your test migrations are in a ./test_migrations folder relative to Cargo.toml
|
||||
sqlx::migrate!("./test_migrations")
|
||||
.run(pool)
|
||||
.await
|
||||
.expect("Failed to run migrations");
|
||||
}
|
||||
}
|
||||
|
||||
mod init {
|
||||
@@ -241,13 +337,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_fields() -> (String, String, String) {
|
||||
(
|
||||
String::from("What a twist!"),
|
||||
String::from("icarus_test"),
|
||||
String::from("icarus"),
|
||||
)
|
||||
}
|
||||
|
||||
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 (message, issuer, audience) = token_fields();
|
||||
|
||||
let token_resource = icarus_models::token::TokenResource {
|
||||
message: message,
|
||||
issuer: issuer,
|
||||
audiences: vec![audience],
|
||||
id: TEST_USER_ID,
|
||||
};
|
||||
|
||||
match icarus_models::token::create_token(&key, &token_resource, time::Duration::hours(1)) {
|
||||
Ok((access_token, _some_time)) => Ok(access_token),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn bearer_auth() -> String {
|
||||
let token = match test_token().await {
|
||||
Ok(access_token) => access_token,
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
format!("Bearer {token}")
|
||||
}
|
||||
|
||||
// TODO: Put the *_req() functions in their own module
|
||||
async fn song_queue_req(
|
||||
app: &axum::Router,
|
||||
) -> Result<axum::response::Response, std::convert::Infallible> {
|
||||
// Create multipart form
|
||||
let mut form = MultipartForm::default();
|
||||
let _ = form.add_file("flac", "tests/IAmWe/track01.flac");
|
||||
let _ = form.add_file("flac", "tests/I/track01.flac");
|
||||
|
||||
// Create request
|
||||
let content_type = form.content_type();
|
||||
@@ -256,6 +391,7 @@ mod tests {
|
||||
.method(axum::http::Method::POST)
|
||||
.uri(crate::callers::endpoints::QUEUESONG)
|
||||
.header(axum::http::header::CONTENT_TYPE, content_type)
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from_stream(body))
|
||||
.unwrap();
|
||||
app.clone().oneshot(req).await
|
||||
@@ -275,6 +411,7 @@ mod tests {
|
||||
.method(axum::http::Method::PATCH)
|
||||
.uri(crate::callers::endpoints::QUEUESONGLINKUSERID)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap();
|
||||
|
||||
@@ -288,6 +425,7 @@ mod tests {
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(crate::callers::endpoints::NEXTQUEUESONG)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap();
|
||||
app.clone().oneshot(fetch_req).await
|
||||
@@ -303,6 +441,7 @@ mod tests {
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap();
|
||||
|
||||
@@ -321,6 +460,7 @@ mod tests {
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(axum::http::header::CONTENT_TYPE, "audio/flac")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap();
|
||||
|
||||
@@ -331,7 +471,7 @@ mod tests {
|
||||
app: &axum::Router,
|
||||
) -> Result<axum::response::Response, std::convert::Infallible> {
|
||||
let mut form = MultipartForm::default();
|
||||
let _ = form.add_file("jpg", "tests/IAmWe/Coverart.jpg");
|
||||
let _ = form.add_file("jpg", "tests/I/Coverart-1.jpg");
|
||||
|
||||
// Create request
|
||||
let content_type = form.content_type();
|
||||
@@ -341,6 +481,7 @@ mod tests {
|
||||
.method(axum::http::Method::POST)
|
||||
.uri(crate::callers::endpoints::QUEUECOVERART)
|
||||
.header(axum::http::header::CONTENT_TYPE, content_type)
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from_stream(body))
|
||||
.unwrap();
|
||||
|
||||
@@ -358,6 +499,7 @@ mod tests {
|
||||
.method(axum::http::Method::POST)
|
||||
.uri(crate::callers::endpoints::QUEUEMETADATA)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap();
|
||||
|
||||
@@ -378,6 +520,7 @@ mod tests {
|
||||
.method(axum::http::Method::PATCH)
|
||||
.uri(crate::callers::endpoints::QUEUECOVERARTLINK)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap();
|
||||
|
||||
@@ -397,6 +540,7 @@ mod tests {
|
||||
.method(axum::http::Method::POST)
|
||||
.uri(crate::callers::endpoints::CREATECOVERART)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap();
|
||||
app.clone().oneshot(req).await
|
||||
@@ -413,6 +557,7 @@ mod tests {
|
||||
.method(axum::http::Method::POST)
|
||||
.uri(crate::callers::endpoints::CREATESONG)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap();
|
||||
|
||||
@@ -432,6 +577,7 @@ mod tests {
|
||||
.method(axum::http::Method::PATCH)
|
||||
.uri(crate::callers::endpoints::QUEUESONG)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap();
|
||||
|
||||
@@ -452,6 +598,7 @@ mod tests {
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap();
|
||||
|
||||
@@ -473,9 +620,7 @@ mod tests {
|
||||
let song_queue_id = resp.data[0];
|
||||
assert_eq!(false, song_queue_id.is_nil(), "Should not be empty");
|
||||
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
// match super::get_resp_data::<crate::callers::song::response::link_user_id::Response>(response).await {
|
||||
let user_id = super::TEST_USER_ID;
|
||||
|
||||
match super::song_queue_link_req(&app, &song_queue_id, &user_id).await {
|
||||
Ok(response) => {
|
||||
@@ -632,6 +777,11 @@ mod tests {
|
||||
serde_json::from_slice(&body).unwrap()
|
||||
}
|
||||
|
||||
pub async fn format_url_with_value(endpoint: &str, value: &uuid::Uuid) -> String {
|
||||
let last = endpoint.len() - 5;
|
||||
format!("{}/{value}", &endpoint[0..last])
|
||||
}
|
||||
|
||||
// TODO: Change the name of the function to be more expressive and put into it's own module
|
||||
pub mod payload_data {
|
||||
pub async fn queue_metadata_payload_data(song_queue_id: &uuid::Uuid) -> serde_json::Value {
|
||||
@@ -737,7 +887,7 @@ mod tests {
|
||||
assert_eq!(false, resp.data[0].is_nil(), "Should not be empty");
|
||||
|
||||
let song_queue_id = &resp.data[0];
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
let user_id = TEST_USER_ID;
|
||||
println!("User Id: {user_id:?}");
|
||||
|
||||
match song_queue_link_req(&app, &song_queue_id, &user_id).await {
|
||||
@@ -911,6 +1061,10 @@ mod tests {
|
||||
.method(axum::http::Method::PATCH)
|
||||
.uri(uri)
|
||||
.header(axum::http::header::CONTENT_TYPE, content_type)
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::from_stream(body))
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -1373,6 +1527,10 @@ mod tests {
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(axum::http::header::CONTENT_TYPE, "image/jpeg")
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -1641,6 +1799,7 @@ mod tests {
|
||||
.method(axum::http::Method::PATCH)
|
||||
.uri(crate::callers::endpoints::QUEUESONGDATAWIPE)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -1761,6 +1920,10 @@ mod tests {
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"application/json",
|
||||
)
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::from(payload.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -1799,4 +1962,548 @@ mod tests {
|
||||
|
||||
let _ = db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
|
||||
pub mod zzz_after_song_queue {
|
||||
use futures::StreamExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_songs() {
|
||||
let tm_pool = super::db_mgr::get_pool().await.unwrap();
|
||||
let db_name = super::db_mgr::generate_db_name().await;
|
||||
|
||||
match super::db_mgr::create_database(&tm_pool, &db_name).await {
|
||||
Ok(_) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
|
||||
super::db_mgr::migrations(&pool).await;
|
||||
|
||||
let app = super::init::app(pool).await;
|
||||
|
||||
let (id, _, _, _) = test_data::song_id().await.unwrap();
|
||||
|
||||
let uri = format!("{}?id={id}", crate::callers::endpoints::GETSONGS);
|
||||
|
||||
match app
|
||||
.clone()
|
||||
.oneshot(
|
||||
axum::http::Request::builder()
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
super::bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let resp = super::get_resp_data::<
|
||||
crate::callers::song::response::get_songs::Response,
|
||||
>(response)
|
||||
.await;
|
||||
assert_eq!(false, resp.data.is_empty(), "Should not be empty");
|
||||
|
||||
let song = resp.data[0].clone();
|
||||
assert_eq!(id, song.id, "Id does not match {song:?}");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_coverart() {
|
||||
let tm_pool = super::db_mgr::get_pool().await.unwrap();
|
||||
let db_name = super::db_mgr::generate_db_name().await;
|
||||
|
||||
match super::db_mgr::create_database(&tm_pool, &db_name).await {
|
||||
Ok(_) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
|
||||
super::db_mgr::migrations(&pool).await;
|
||||
|
||||
let app = super::init::app(pool).await;
|
||||
|
||||
let id = test_data::coverart_id().await.unwrap();
|
||||
|
||||
let uri = format!("{}?id={id}", crate::callers::endpoints::GETCOVERART);
|
||||
|
||||
match app
|
||||
.clone()
|
||||
.oneshot(
|
||||
axum::http::Request::builder()
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
super::bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let resp = super::get_resp_data::<
|
||||
crate::callers::coverart::response::get_coverart::Response,
|
||||
>(response)
|
||||
.await;
|
||||
assert_eq!(false, resp.data.is_empty(), "Should not be empty");
|
||||
|
||||
let coverart = resp.data[0].clone();
|
||||
assert_eq!(id, coverart.id, "Id does not match {coverart:?}");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
|
||||
pub mod test_data {
|
||||
pub async fn song_id() -> Result<(uuid::Uuid, String, String, String), uuid::Error> {
|
||||
match uuid::Uuid::parse_str("44cf7940-34ff-489f-9124-d0ec90a55af9") {
|
||||
Ok(id) => Ok((
|
||||
id,
|
||||
String::from("tests/I/"),
|
||||
String::from("track01.flac"),
|
||||
String::from("tests/I/Coverart-1.jpg"),
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn other_song_id() -> Result<(uuid::Uuid, 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"),
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn coverart_id() -> Result<uuid::Uuid, uuid::Error> {
|
||||
uuid::Uuid::parse_str("996122cd-5ae9-4013-9934-60768d3006ed")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_song() {
|
||||
let tm_pool = super::db_mgr::get_pool().await.unwrap();
|
||||
let db_name = super::db_mgr::generate_db_name().await;
|
||||
|
||||
match super::db_mgr::create_database(&tm_pool, &db_name).await {
|
||||
Ok(_) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
|
||||
super::db_mgr::migrations(&pool).await;
|
||||
|
||||
let app = super::init::app(pool).await;
|
||||
|
||||
let (id, _, _, _) = test_data::song_id().await.unwrap();
|
||||
|
||||
let my_url = crate::callers::endpoints::STREAMSONG;
|
||||
let last = my_url.len() - 5;
|
||||
let uri = format!("{}/{id}", &my_url[0..last]);
|
||||
|
||||
match app
|
||||
.clone()
|
||||
.oneshot(
|
||||
axum::http::Request::builder()
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(&uri)
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
super::bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let e = response.into_body();
|
||||
let mut data = e.into_data_stream();
|
||||
while let Some(chunk) = data.next().await {
|
||||
match chunk {
|
||||
Ok(_data) => {}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_song() {
|
||||
let tm_pool = super::db_mgr::get_pool().await.unwrap();
|
||||
let db_name = super::db_mgr::generate_db_name().await;
|
||||
|
||||
match super::db_mgr::create_database(&tm_pool, &db_name).await {
|
||||
Ok(_) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
|
||||
super::db_mgr::migrations(&pool).await;
|
||||
|
||||
let app = super::init::app(pool).await;
|
||||
|
||||
let (id, _, _, _) = test_data::song_id().await.unwrap();
|
||||
|
||||
let uri =
|
||||
super::format_url_with_value(crate::callers::endpoints::DOWNLOADSONG, &id).await;
|
||||
|
||||
match app
|
||||
.clone()
|
||||
.oneshot(
|
||||
axum::http::Request::builder()
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(&uri)
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
super::bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let e = response.into_body();
|
||||
let mut data = e.into_data_stream();
|
||||
while let Some(chunk) = data.next().await {
|
||||
match chunk {
|
||||
Ok(_data) => {}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_coverart() {
|
||||
let tm_pool = super::db_mgr::get_pool().await.unwrap();
|
||||
let db_name = super::db_mgr::generate_db_name().await;
|
||||
|
||||
match super::db_mgr::create_database(&tm_pool, &db_name).await {
|
||||
Ok(_) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
|
||||
super::db_mgr::migrations(&pool).await;
|
||||
|
||||
let app = super::init::app(pool).await;
|
||||
|
||||
let id = test_data::coverart_id().await.unwrap();
|
||||
|
||||
let uri =
|
||||
super::format_url_with_value(crate::callers::endpoints::DOWNLOADCOVERART, &id)
|
||||
.await;
|
||||
|
||||
match app
|
||||
.clone()
|
||||
.oneshot(
|
||||
axum::http::Request::builder()
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(&uri)
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
super::bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let e = response.into_body();
|
||||
let mut data = e.into_data_stream();
|
||||
while let Some(chunk) = data.next().await {
|
||||
match chunk {
|
||||
Ok(_data) => {}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
|
||||
async fn get_test_data(
|
||||
song_directory: &String,
|
||||
song_filename: &String,
|
||||
coverart_path: &String,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), std::io::Error> {
|
||||
let song = icarus_models::song::Song {
|
||||
directory: song_directory.clone(),
|
||||
filename: song_filename.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let coverart = icarus_models::coverart::CoverArt {
|
||||
path: coverart_path.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match song.to_data() {
|
||||
Ok(song_data) => match coverart.to_data() {
|
||||
Ok(coverart_data) => Ok((song_data, coverart_data)),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_test_again(
|
||||
song_directory: &String,
|
||||
song_filename: &String,
|
||||
song_data: Vec<u8>,
|
||||
coverart_path: &String,
|
||||
coverart_data: Vec<u8>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let song = icarus_models::song::Song {
|
||||
directory: song_directory.clone(),
|
||||
filename: song_filename.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let coverart = icarus_models::coverart::CoverArt {
|
||||
path: coverart_path.clone(),
|
||||
..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) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_last_delete_song() {
|
||||
let tm_pool = super::db_mgr::get_pool().await.unwrap();
|
||||
let db_name = super::db_mgr::generate_db_name().await;
|
||||
|
||||
match super::db_mgr::create_database(&tm_pool, &db_name).await {
|
||||
Ok(_) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
|
||||
super::db_mgr::migrations(&pool).await;
|
||||
|
||||
let app = super::init::app(pool).await;
|
||||
|
||||
let (id, song_directory, song_filename, coverart_path) =
|
||||
test_data::other_song_id().await.unwrap();
|
||||
let (song_data, coverart_data) =
|
||||
get_test_data(&song_directory, &song_filename, &coverart_path)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let uri =
|
||||
super::format_url_with_value(crate::callers::endpoints::DELETESONG, &id).await;
|
||||
|
||||
match app
|
||||
.clone()
|
||||
.oneshot(
|
||||
axum::http::Request::builder()
|
||||
.method(axum::http::Method::DELETE)
|
||||
.uri(&uri)
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
super::bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let resp = super::get_resp_data::<
|
||||
crate::callers::song::response::delete_song::Response,
|
||||
>(response)
|
||||
.await;
|
||||
assert_eq!(false, resp.data.is_empty(), "Response has no data");
|
||||
|
||||
let song_and_coverart = &resp.data[0];
|
||||
assert_eq!(
|
||||
id, song_and_coverart.song.id,
|
||||
"Song Ids do not match {id:?} {:?}",
|
||||
song_and_coverart.song.id
|
||||
);
|
||||
|
||||
match save_test_again(
|
||||
&song_directory,
|
||||
&song_filename,
|
||||
song_data,
|
||||
&coverart_path,
|
||||
coverart_data,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_all_songs() {
|
||||
let tm_pool = super::db_mgr::get_pool().await.unwrap();
|
||||
let db_name = super::db_mgr::generate_db_name().await;
|
||||
|
||||
match super::db_mgr::create_database(&tm_pool, &db_name).await {
|
||||
Ok(_) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
|
||||
super::db_mgr::migrations(&pool).await;
|
||||
|
||||
let app = super::init::app(pool).await;
|
||||
|
||||
match app
|
||||
.clone()
|
||||
.oneshot(
|
||||
axum::http::Request::builder()
|
||||
.method(axum::http::Method::GET)
|
||||
.uri(crate::callers::endpoints::GETALLSONGS)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
axum::http::header::AUTHORIZATION,
|
||||
super::bearer_auth().await,
|
||||
)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let resp = super::get_resp_data::<
|
||||
crate::callers::song::response::get_songs::Response,
|
||||
>(response)
|
||||
.await;
|
||||
assert_eq!(false, resp.data.is_empty(), "Should not be empty");
|
||||
|
||||
let songs = &resp.data;
|
||||
assert_eq!(
|
||||
2,
|
||||
songs.len(),
|
||||
"Returned song count does not match. Returned song count {:?} song count {}",
|
||||
songs.len(),
|
||||
2
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Error: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Add migration script here
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- Table to store queued songs to process
|
||||
CREATE TABLE IF NOT EXISTS "songQueue" (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
filename TEXT NOT NULL,
|
||||
status TEXT CHECK (status IN ('pending', 'ready', 'processing', 'done')),
|
||||
data BYTEA NULL,
|
||||
user_id UUID NULL
|
||||
);
|
||||
|
||||
-- Table to store queued metadata
|
||||
CREATE TABLE IF NOT EXISTS "metadataQueue" (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
metadata jsonb NOT NULL,
|
||||
created_at timestamptz DEFAULT now(),
|
||||
song_queue_id UUID NOT NULL
|
||||
);
|
||||
|
||||
-- Table to store queued coverart
|
||||
CREATE TABLE IF NOT EXISTS "coverartQueue" (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
data BYTEA NULL,
|
||||
song_queue_id UUID NULL
|
||||
);
|
||||
|
||||
-- Create an index for better query performance
|
||||
CREATE INDEX metadata_queue_data_metadata ON "metadataQueue" USING gin (metadata);
|
||||
|
||||
-- Table to store a song's info
|
||||
CREATE TABLE IF NOT EXISTS "song" (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
artist TEXT NOT NULL,
|
||||
album_artist TEXT NOT NULL,
|
||||
album TEXT NOT NULL,
|
||||
genre TEXT NOT NULL,
|
||||
year INT NOT NULL,
|
||||
track INT NOT NULL,
|
||||
disc INT NOT NULL,
|
||||
track_count INT NOT NULL,
|
||||
disc_count INT NOT NULL,
|
||||
duration INT NOT NULL,
|
||||
audio_type TEXT NOT NULL,
|
||||
date_created timestamptz DEFAULT now(),
|
||||
filename TEXT NOT NULL,
|
||||
directory TEXT NOT NULL,
|
||||
user_id UUID NULL
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "coverart" (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
song_id UUID NOT NULL
|
||||
);
|
||||
@@ -0,0 +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 "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');
|
||||
|
Before Width: | Height: | Size: 6.7 MiB After Width: | Height: | Size: 6.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 MiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"album": "I Am We",
|
||||
"album": "I",
|
||||
"album_artist": "Kuoth",
|
||||
"disc_count": 1,
|
||||
"genre": "Alternative Hip-Hop",
|
||||
Binary file not shown.
Reference in New Issue
Block a user