Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50d0ea630f | |||
| d273c801bf | |||
| bcd960d9a1 | |||
| dbbbf02acf | |||
| 01b34c9848 | |||
| 1b9bd4359c | |||
| d05502dd8e | |||
| 147a50c867 | |||
| a80e21b918 | |||
| 731aae5240 | |||
| 79e909bfc0 | |||
| 7940527418 | |||
| f859eaf0a2 | |||
| 22080a2f29 | |||
| c630cb84b5 | |||
| ca38465f2d | |||
| fd4e661b3c | |||
| 6995ed483c | |||
| 2cc5e99590 | |||
| 09d7fa32c8 | |||
| 27ef175828 | |||
| 1871ebe40b | |||
| 1a5a9fcaaf | |||
| 8b36bc8c49 | |||
| 003e5b77c1 | |||
| 02d9ef013e | |||
| 351b4acb1c | |||
| 381dd49b56 | |||
| a9ba1c5193 | |||
| fb360c8269 | |||
| e461e78661 | |||
| 2023fc81f8 | |||
| 48a40a4821 | |||
| 8bf99be22c | |||
| 8c12c8c761 | |||
| caaec248db | |||
| 309d76785a | |||
| c83a37c20a | |||
| 032801b903 | |||
|
b844f06e82
|
|||
|
517a01d60c
|
|||
| 1ae2f88beb | |||
| 805df87f5b | |||
| 598996cb8d | |||
| 21a97c150c | |||
| 679a4ad322 | |||
| 9aefeace23 | |||
| a0c6ae65a3 | |||
| c7230d3b32 | |||
| 703fc49f32 | |||
| e98e055de0 | |||
| a8603361c0 | |||
| dbda9a3897 | |||
| a987e438c4 | |||
| a150d34c6c |
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
APP_ENV=development
|
||||||
|
BACKEND_PORT=8000
|
||||||
|
FRONTEND_URL=http://localhost:4200
|
||||||
|
RUST_LOG=debug
|
||||||
|
ALLOWED_ORIGINS=https://soaricarus.com,https://www.soaricarus.com
|
||||||
|
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
|
||||||
|
ROOT_DIRECTORY=/home/icarus/mydata
|
||||||
|
POSTGRES_MAIN_USER=icarus
|
||||||
|
POSTGRES_MAIN_PASSWORD=password
|
||||||
|
POSTGRES_MAIN_DB=icarus_db
|
||||||
|
POSTGRES_MAIN_HOST=main_db
|
||||||
|
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:5432/${POSTGRES_MAIN_DB}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
name: Release Tagging
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "v0.2" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Important for git describe --tags
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: 1.95
|
||||||
|
components: cargo
|
||||||
|
|
||||||
|
- name: Extract Version from Cargo.toml
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '^version = "' Cargo.toml | sed -E 's/version = "([^"]+)"/\1/')
|
||||||
|
PROJECT_COMMIT_HASH=$(git rev-parse HEAD | cut -c 1-10)
|
||||||
|
BRANCH_REF="${GITHUB_REF}"
|
||||||
|
BRANCH_NAME=$(echo "$BRANCH_REF" | cut -d '/' -f 3)
|
||||||
|
PROJECT_TAG_RELEASE="v$VERSION-$BRANCH_NAME-$PROJECT_COMMIT_HASH"
|
||||||
|
echo "::set-output name=project_tag_release::$PROJECT_TAG_RELEASE-961"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
echo "Hash: $PROJECT_COMMIT_HASH"
|
||||||
|
echo "Branch: $BRANCH_NAME"
|
||||||
|
echo "Tag Release: $PROJECT_TAG_RELEASE"
|
||||||
|
|
||||||
|
- name: Print version
|
||||||
|
id: print_version
|
||||||
|
run: |
|
||||||
|
echo "Printing version"
|
||||||
|
echo "Version: ${{ steps.version.outputs.project_tag_release }}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.version.outputs.project_tag_release }}
|
||||||
|
release_name: Release ${{ steps.version.outputs.project_tag_release }}
|
||||||
|
body: |
|
||||||
|
Release of version ${{ steps.version.outputs.project_tag_release }}
|
||||||
@@ -2,16 +2,16 @@ 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:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:17.5
|
image: postgres:18.3-alpine
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||||
@@ -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.95
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: 1.88.0
|
toolchain: 1.95
|
||||||
components: clippy, rustfmt
|
components: clippy, rustfmt
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
@@ -68,7 +68,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
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: cargo test --verbose
|
run: |
|
||||||
|
cat .env.sample | head -6 > .env
|
||||||
|
cargo test --verbose --
|
||||||
|
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: cargo clippy -- -D warnings
|
run: cargo clippy -- -D warnings
|
||||||
|
|||||||
@@ -7,3 +7,7 @@
|
|||||||
|
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
Storage/
|
Storage/
|
||||||
|
|
||||||
|
*.sln
|
||||||
|
*.cs
|
||||||
|
*.csproj
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"dev": {
|
||||||
|
"migrations": "./migrations"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"migrations": "./test_migrations"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1176
-604
File diff suppressed because it is too large
Load Diff
+26
-18
@@ -1,27 +1,35 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "icarus"
|
name = "icarus"
|
||||||
version = "0.1.93"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.95"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8.4", features = ["multipart"] }
|
axum = { version = "0.8.9", features = ["multipart"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
axum-extra = { version = "0.12.6", features = ["cookie"] }
|
||||||
serde_json = { version = "1.0.140" }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
tower = { version = "0.5.2" }
|
serde_json = { version = "1.0.149" }
|
||||||
tokio = { version = "1.45.1", features = ["full"] }
|
tokio = { version = "1.52.3", features = ["full"] }
|
||||||
tokio-util = { version = "0.7.15" }
|
tokio-util = { version = "0.7.18", features = ["io"] }
|
||||||
tower-http = { version = "0.6.6", features = ["timeout"] }
|
tower = { version = "0.5.3", features = ["full"] }
|
||||||
tracing-subscriber = "0.3.19"
|
tower-http = { version = "0.6.10", features = ["cors", "timeout"] }
|
||||||
futures = { version = "0.3.31" }
|
tracing-subscriber = "0.3.23"
|
||||||
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
futures = { version = "0.3.32" }
|
||||||
|
mime_guess = { version = "2.0.5" }
|
||||||
|
uuid = { version = "1.23.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.47", features = ["formatting", "macros", "parsing", "serde"] }
|
||||||
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0-devel-f4b71de969-680" }
|
thiserror = "2.0.18"
|
||||||
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.4.5-devel-655d05dabb-111" }
|
base64 = "0.22.1"
|
||||||
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.0-devel-d73fba9899-006" }
|
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
|
||||||
|
josekit = { version = "0.10.3" }
|
||||||
|
utoipa = { version = "5.5.0", features = ["axum_extras"] }
|
||||||
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
|
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.6.0" }
|
||||||
|
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.11.1" }
|
||||||
|
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.7.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.8" }
|
||||||
tempfile = { version = "3.20.0" }
|
tempfile = { version = "3.27.0" }
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Build the application
|
# Stage 1: Build the application
|
||||||
FROM rust:1.88 as builder
|
FROM rust:1.95 as builder
|
||||||
|
|
||||||
# Set the working directory inside the container
|
# Set the working directory inside the container
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -45,7 +45,7 @@ RUN --mount=type=ssh \
|
|||||||
|
|
||||||
# Stage 2: Create the final, smaller runtime image
|
# Stage 2: Create the final, smaller runtime image
|
||||||
# Use a minimal base image like debian-slim or even distroless for security/size
|
# Use a minimal base image like debian-slim or even distroless for security/size
|
||||||
FROM ubuntu:24.04
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
# Install runtime dependencies if needed (e.g., SSL certificates)
|
# Install runtime dependencies if needed (e.g., SSL certificates)
|
||||||
RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
|
||||||
@@ -63,7 +63,7 @@ COPY --from=builder /usr/src/app/.env .
|
|||||||
COPY --from=builder /usr/src/app/migrations ./migrations
|
COPY --from=builder /usr/src/app/migrations ./migrations
|
||||||
|
|
||||||
# Expose the port your Axum app listens on (e.g., 3000 or 8000)
|
# Expose the port your Axum app listens on (e.g., 3000 or 8000)
|
||||||
EXPOSE 3000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Set the command to run your application
|
# Set the command to run your application
|
||||||
# Ensure this matches the binary name copied above
|
# Ensure this matches the binary name copied above
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,17 +1,33 @@
|
|||||||
|
# Icarus
|
||||||
|
Web API for the Icarus project.
|
||||||
|
|
||||||
|
|
||||||
# Getting Started
|
### Requires
|
||||||
## Docker
|
`icarus_auth` v0.7.x
|
||||||
Make sure `icarus_auth` is located in the root of the parent directory if using docker.
|
`songparser` v0.5.x
|
||||||
|
|
||||||
Create a `.env` file for both projects - `icarus_auth` and `icarus` - in the root of each project.
|
### Compatible with
|
||||||
|
`icarus-dm` v0.9.x
|
||||||
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
Quickest way to get started is with docker. Make sure `icarus_auth` and `songparser` repositories
|
||||||
|
are located in the root of the parent directory. Check the respective repositories to ensure they
|
||||||
|
are setup correctly before configuring `Icarus`.
|
||||||
|
|
||||||
|
Copy the `.env.docker.sample` file to `.env`. Ensure that the `ROOT_DIRECTORY` variable is populated
|
||||||
|
and exists on the docker image's filesystem. The credentials for the database doesn't need to be
|
||||||
|
changed for development, but if deploying it, it should be modified.
|
||||||
|
|
||||||
Build containers
|
Build containers
|
||||||
```
|
```
|
||||||
docker compose build --ssh default api auth_api
|
docker compose build --ssh default
|
||||||
```
|
```
|
||||||
|
|
||||||
Bring it up
|
Bring it up
|
||||||
```
|
```
|
||||||
docker compose up -d --force-recreate api auth_api
|
docker compose up -d --force-recreate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To view the OpenAPI spec, run the project and access `/swagger-ui`. If running through docker,
|
||||||
|
the url would be something like `http://localhost:8000/swagger-ui`.
|
||||||
|
|||||||
+30
-14
@@ -9,15 +9,14 @@ services:
|
|||||||
container_name: icarus # Optional: Give the container a specific name
|
container_name: icarus # Optional: Give the container a specific name
|
||||||
ports:
|
ports:
|
||||||
# Map host port 8000 to container port 3000 (adjust as needed)
|
# Map host port 8000 to container port 3000 (adjust as needed)
|
||||||
# Format: "HOST_PORT:CONTAINER_PORT"
|
- "8000:8000"
|
||||||
- "8000:3000"
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
depends_on:
|
depends_on:
|
||||||
main_db:
|
main_db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- main-api-network
|
- icarus-network
|
||||||
restart: unless-stopped # Optional: Restart policy
|
restart: unless-stopped # Optional: Restart policy
|
||||||
|
|
||||||
# --- Web API auth ---
|
# --- Web API auth ---
|
||||||
@@ -29,7 +28,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
|
||||||
@@ -39,15 +38,34 @@ services:
|
|||||||
auth_db:
|
auth_db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- auth-api-network
|
- icarus-network
|
||||||
# Optional: Mount local code for development (live reload)
|
# Optional: Mount local code for development (live reload)
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./path/to/your/web-api-repo:/app
|
# - ./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
|
# PostgreSQL Database Service
|
||||||
# --- icarus web api db ---
|
# --- icarus web api db ---
|
||||||
main_db:
|
main_db:
|
||||||
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller)
|
image: postgres:18.3-alpine # Use an official Postgres image (Alpine variant is smaller)
|
||||||
container_name: icarus_db # Optional: Give the container a specific name
|
container_name: icarus_db # Optional: Give the container a specific name
|
||||||
environment:
|
environment:
|
||||||
# These MUST match the user, password, and database name in the DATABASE_URL above
|
# These MUST match the user, password, and database name in the DATABASE_URL above
|
||||||
@@ -56,7 +74,7 @@ services:
|
|||||||
POSTGRES_DB: ${POSTGRES_MAIN_DB:-icarus_db}
|
POSTGRES_DB: ${POSTGRES_MAIN_DB:-icarus_db}
|
||||||
volumes:
|
volumes:
|
||||||
# Persist database data using a named volume
|
# Persist database data using a named volume
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql
|
||||||
ports:
|
ports:
|
||||||
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
|
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
@@ -69,11 +87,11 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
restart: always # Optional: Restart policy
|
restart: always # Optional: Restart policy
|
||||||
networks:
|
networks:
|
||||||
- main-api-network
|
- icarus-network
|
||||||
|
|
||||||
# --- icarus web auth api db ---
|
# --- icarus web auth api db ---
|
||||||
auth_db:
|
auth_db:
|
||||||
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller)
|
image: postgres:18.3-alpine # Use an official Postgres image (Alpine variant is smaller)
|
||||||
container_name: icarus_auth_db # Optional: Give the container a specific name
|
container_name: icarus_auth_db # Optional: Give the container a specific name
|
||||||
environment:
|
environment:
|
||||||
# These MUST match the user, password, and database name in the DATABASE_URL above
|
# These MUST match the user, password, and database name in the DATABASE_URL above
|
||||||
@@ -82,7 +100,7 @@ services:
|
|||||||
POSTGRES_DB: ${POSTGRES_AUTH_DB:-icarus_auth_db}
|
POSTGRES_DB: ${POSTGRES_AUTH_DB:-icarus_auth_db}
|
||||||
volumes:
|
volumes:
|
||||||
# Persist database data using a named volume
|
# Persist database data using a named volume
|
||||||
- postgres_data_auth:/var/lib/postgresql/data
|
- postgres_data_auth:/var/lib/postgresql
|
||||||
ports:
|
ports:
|
||||||
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
|
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
@@ -95,7 +113,7 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
restart: always # Optional: Restart policy
|
restart: always # Optional: Restart policy
|
||||||
networks:
|
networks:
|
||||||
- auth-api-network
|
- icarus-network
|
||||||
|
|
||||||
# Define the named volume for data persistence
|
# Define the named volume for data persistence
|
||||||
volumes:
|
volumes:
|
||||||
@@ -106,7 +124,5 @@ volumes:
|
|||||||
|
|
||||||
# Define the network (optional, but good practice)
|
# Define the network (optional, but good practice)
|
||||||
networks:
|
networks:
|
||||||
main-api-network:
|
icarus-network:
|
||||||
driver: bridge
|
|
||||||
auth-api-network:
|
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS "metadataQueue" (
|
|||||||
CREATE TABLE IF NOT EXISTS "coverartQueue" (
|
CREATE TABLE IF NOT EXISTS "coverartQueue" (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
data BYTEA NULL,
|
data BYTEA NULL,
|
||||||
|
file_type TEXT NOT NULL,
|
||||||
song_queue_id UUID NULL
|
song_queue_id UUID NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,8 +56,8 @@ CREATE TABLE IF NOT EXISTS "song" (
|
|||||||
CREATE TABLE IF NOT EXISTS "coverart" (
|
CREATE TABLE IF NOT EXISTS "coverart" (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
-- TODO: Separate path later
|
directory TEXT NOT NULL,
|
||||||
path TEXT NOT NULL,
|
filename TEXT NOT NULL,
|
||||||
|
file_type TEXT NOT NULL,
|
||||||
song_id UUID NOT NULL
|
song_id UUID NOT NULL
|
||||||
-- TODO: Add type later
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
http::{Request, StatusCode},
|
||||||
|
middleware::Next,
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
|
use jsonwebtoken::{DecodingKey, Validation, decode};
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub status: &'static str,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth<B>(
|
||||||
|
cookie_jar: CookieJar,
|
||||||
|
req: Request<axum::body::Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
|
||||||
|
let token = cookie_jar
|
||||||
|
.get("token")
|
||||||
|
.map(|cookie| cookie.value().to_string())
|
||||||
|
.or_else(|| {
|
||||||
|
req.headers()
|
||||||
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
|
.and_then(|auth_header| auth_header.to_str().ok())
|
||||||
|
.and_then(|auth_value| auth_value.strip_prefix("Bearer ").map(String::from))
|
||||||
|
});
|
||||||
|
|
||||||
|
let token = token.ok_or_else(|| {
|
||||||
|
let json_error = ErrorResponse {
|
||||||
|
status: "fail",
|
||||||
|
message: "You are not logged in, please provide token".to_string(),
|
||||||
|
};
|
||||||
|
(StatusCode::UNAUTHORIZED, Json(json_error))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let secret_key = icarus_envy::environment::get_secret_main_key().await.value;
|
||||||
|
|
||||||
|
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||||
|
validation.set_audience(&["icarus"]); // Must match exactly what's in the token
|
||||||
|
let _claims = decode::<icarus_models::token::UserClaims>(
|
||||||
|
&token,
|
||||||
|
&DecodingKey::from_secret(secret_key.as_ref()),
|
||||||
|
&validation,
|
||||||
|
)
|
||||||
|
.map_err(|err| {
|
||||||
|
eprintln!("Error: {err:?}");
|
||||||
|
let json_error = ErrorResponse {
|
||||||
|
status: "fail",
|
||||||
|
message: "Invalid token - Error decoding claims".to_string(),
|
||||||
|
};
|
||||||
|
(StatusCode::UNAUTHORIZED, Json(json_error))
|
||||||
|
})?
|
||||||
|
.claims;
|
||||||
|
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
||||||
+173
-472
@@ -1,480 +1,58 @@
|
|||||||
// TODO: Separate queue and coverart endpoints
|
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct CoverArtQueue {
|
|
||||||
pub id: uuid::Uuid,
|
|
||||||
pub song_queue_id: uuid::Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod request {
|
pub mod request {
|
||||||
|
|
||||||
pub mod link {
|
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Request {
|
|
||||||
pub coverart_id: uuid::Uuid,
|
|
||||||
pub song_queue_id: uuid::Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod fetch_coverart_no_data {
|
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Params {
|
|
||||||
pub id: Option<uuid::Uuid>,
|
|
||||||
pub song_queue_id: Option<uuid::Uuid>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod fetch_coverart_with_data {
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Params {
|
|
||||||
pub id: Option<uuid::Uuid>,
|
|
||||||
pub song_queue_id: Option<uuid::Uuid>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod create_coverart {
|
pub mod create_coverart {
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
pub song_id: uuid::Uuid,
|
pub song_id: uuid::Uuid,
|
||||||
pub coverart_queue_id: uuid::Uuid,
|
pub coverart_queue_id: uuid::Uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod wipe_data_from_coverart_queue {
|
pub mod get_coverart {
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
pub struct Request {
|
pub struct Params {
|
||||||
pub coverart_queue_id: uuid::Uuid,
|
pub id: Option<uuid::Uuid>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod response {
|
pub mod response {
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub message: String,
|
|
||||||
pub data: Vec<uuid::Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod link {
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Id {
|
|
||||||
pub coverart_id: uuid::Uuid,
|
|
||||||
pub song_queue_id: uuid::Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub message: String,
|
|
||||||
pub data: Vec<Id>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod fetch_coverart_no_data {
|
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub message: String,
|
|
||||||
pub data: Vec<super::super::CoverArtQueue>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod fetch_coverart_with_data {
|
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub message: String,
|
|
||||||
pub data: Vec<Vec<u8>>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod create_coverart {
|
pub mod create_coverart {
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub data: Vec<icarus_models::coverart::CoverArt>,
|
pub data: Vec<icarus_models::coverart::CoverArt>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod wipe_data_from_coverart_queue {
|
pub mod get_coverart {
|
||||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub data: Vec<uuid::Uuid>,
|
pub data: Vec<icarus_models::coverart::CoverArt>,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod db {
|
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
pub async fn insert(pool: &sqlx::PgPool, data: &Vec<u8>) -> Result<uuid::Uuid, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
INSERT INTO "coverartQueue" (data) VALUES($1) RETURNING id;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(data)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error inserting: {e:?}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => {
|
|
||||||
let id: uuid::Uuid = row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap();
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
coverart_id: &uuid::Uuid,
|
|
||||||
song_queue_id: &uuid::Uuid,
|
|
||||||
) -> Result<i32, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
UPDATE "coverartQueue" SET song_queue_id = $1 WHERE id = $2;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(song_queue_id)
|
|
||||||
.bind(coverart_id)
|
|
||||||
.execute(pool)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => Ok(0),
|
|
||||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_coverart_queue_with_id(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
id: &uuid::Uuid,
|
|
||||||
) -> Result<super::CoverArtQueue, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT id, song_queue_id FROM "coverartQueue" WHERE id = $1;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error querying data: {e:?}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => Ok(super::CoverArtQueue {
|
|
||||||
id: row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
song_queue_id: row
|
|
||||||
.try_get("song_queue_id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
}),
|
|
||||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_coverart_queue_with_song_queue_id(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
song_queue_id: &uuid::Uuid,
|
|
||||||
) -> Result<super::CoverArtQueue, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT id, song_queue_id FROM "coverartQueue" WHERE song_queue_id = $1;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(song_queue_id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error querying data: {e:?}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => Ok(super::CoverArtQueue {
|
|
||||||
id: row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
song_queue_id: row
|
|
||||||
.try_get("song_queue_id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
}),
|
|
||||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_coverart_queue_data_with_id(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
id: &uuid::Uuid,
|
|
||||||
) -> Result<Vec<u8>, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT data FROM "coverartQueue" WHERE id = $1;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error querying data: {e:?}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => Ok(row
|
|
||||||
.try_get("data")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap()),
|
|
||||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_coverart_queue_data_with_song_queue_id(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
song_queue_id: &uuid::Uuid,
|
|
||||||
) -> Result<Vec<u8>, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT data FROM "coverartQueue" WHERE song_queue_id = $1;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(song_queue_id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error querying data: {e}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => Ok(row
|
|
||||||
.try_get("data")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap()),
|
|
||||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wipe_data(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
coverart_queue_id: &uuid::Uuid,
|
|
||||||
) -> Result<uuid::Uuid, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
UPDATE "coverartQueue" SET data = NULL WHERE id = $1 RETURNING id;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(coverart_queue_id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error updating query: {e}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => Ok(row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap()),
|
|
||||||
Err(_) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod cov_db {
|
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
pub async fn create(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
coverart: &icarus_models::coverart::CoverArt,
|
|
||||||
song_id: &uuid::Uuid,
|
|
||||||
) -> Result<uuid::Uuid, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
INSERT INTO "coverart" (title, path, song_id) VALUES($1, $2, $3) RETURNING id;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(&coverart.title)
|
|
||||||
.bind(&coverart.path)
|
|
||||||
.bind(song_id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error inserting: {e}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => {
|
|
||||||
let id: uuid::Uuid = row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap();
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod endpoint {
|
pub mod endpoint {
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
pub async fn queue(
|
use crate::repo;
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
use crate::repo::queue as repo_queue;
|
||||||
mut multipart: axum::extract::Multipart,
|
|
||||||
) -> (
|
|
||||||
axum::http::StatusCode,
|
|
||||||
axum::Json<super::response::Response>,
|
|
||||||
) {
|
|
||||||
let mut response = super::response::Response::default();
|
|
||||||
|
|
||||||
match multipart.next_field().await {
|
/// Endpoint to create cover art
|
||||||
Ok(Some(field)) => {
|
#[utoipa::path(
|
||||||
let name = field.name().unwrap().to_string();
|
post,
|
||||||
let file_name = field.file_name().unwrap().to_string();
|
path = super::super::endpoints::CREATECOVERART,
|
||||||
let content_type = field.content_type().unwrap().to_string();
|
request_body(
|
||||||
let data = field.bytes().await.unwrap();
|
content = super::request::create_coverart::Request,
|
||||||
let raw_data = data.to_vec();
|
description = "Data required to create cover art",
|
||||||
|
content_type = "application/json"
|
||||||
println!(
|
|
||||||
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
|
|
||||||
file_name,
|
|
||||||
name,
|
|
||||||
content_type,
|
|
||||||
data.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
match super::db::insert(&pool, &raw_data).await {
|
|
||||||
Ok(id) => {
|
|
||||||
response.message = String::from("Successful");
|
|
||||||
response.data.push(id);
|
|
||||||
(axum::http::StatusCode::OK, axum::Json(response))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)),
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn link(
|
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
|
||||||
axum::Json(payload): axum::Json<super::request::link::Request>,
|
|
||||||
) -> (
|
|
||||||
axum::http::StatusCode,
|
|
||||||
axum::Json<super::response::link::Response>,
|
|
||||||
) {
|
|
||||||
let mut response = super::response::link::Response::default();
|
|
||||||
let id = payload.coverart_id;
|
|
||||||
let song_id = payload.song_queue_id;
|
|
||||||
|
|
||||||
match super::db::update(&pool, &id, &song_id).await {
|
|
||||||
Ok(_o) => {
|
|
||||||
response.data.push(super::response::link::Id {
|
|
||||||
song_queue_id: song_id,
|
|
||||||
coverart_id: id,
|
|
||||||
});
|
|
||||||
|
|
||||||
(axum::http::StatusCode::OK, axum::Json(response))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_coverart_no_data(
|
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
|
||||||
axum::extract::Query(params): axum::extract::Query<
|
|
||||||
super::request::fetch_coverart_no_data::Params,
|
|
||||||
>,
|
|
||||||
) -> (
|
|
||||||
axum::http::StatusCode,
|
|
||||||
axum::Json<super::response::fetch_coverart_no_data::Response>,
|
|
||||||
) {
|
|
||||||
let mut response = super::response::fetch_coverart_no_data::Response::default();
|
|
||||||
|
|
||||||
match params.id {
|
|
||||||
Some(id) => match super::db::get_coverart_queue_with_id(&pool, &id).await {
|
|
||||||
Ok(cover_art_queue) => {
|
|
||||||
response.message = String::from("Successful");
|
|
||||||
response.data.push(cover_art_queue);
|
|
||||||
(axum::http::StatusCode::OK, axum::Json(response))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => match params.song_queue_id {
|
|
||||||
Some(song_queue_id) => {
|
|
||||||
match super::db::get_coverart_queue_with_song_queue_id(&pool, &song_queue_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(cover_art_queue) => {
|
|
||||||
response.message = String::from("Successful");
|
|
||||||
response.data.push(cover_art_queue);
|
|
||||||
(axum::http::StatusCode::OK, axum::Json(response))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
response.message = String::from("No valid id provided");
|
|
||||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_coverart_with_data(
|
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
|
||||||
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
|
||||||
) -> (axum::http::StatusCode, axum::response::Response) {
|
|
||||||
match super::db::get_coverart_queue_data_with_id(&pool, &id).await {
|
|
||||||
Ok(data) => {
|
|
||||||
let bytes = axum::body::Bytes::from(data);
|
|
||||||
let mut response = bytes.into_response();
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
// TODO: Address this hard coding for the coverart content type
|
|
||||||
headers.insert(axum::http::header::CONTENT_TYPE, "image".parse().unwrap());
|
|
||||||
// TODO: Make the conent disposition more dynamic
|
|
||||||
headers.insert(
|
|
||||||
axum::http::header::CONTENT_DISPOSITION,
|
|
||||||
format!("attachment; filename=\"{id}.jpg\"")
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
(axum::http::StatusCode::OK, response)
|
|
||||||
}
|
|
||||||
Err(_err) => (
|
|
||||||
axum::http::StatusCode::BAD_REQUEST,
|
|
||||||
axum::response::Response::default(),
|
|
||||||
),
|
),
|
||||||
}
|
responses(
|
||||||
}
|
(status = 200, description = "Cover art created", body = super::response::create_coverart::Response),
|
||||||
|
(status = 400, description = "Failure in creating cover art", body = super::response::create_coverart::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub async fn create_coverart(
|
pub async fn create_coverart(
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
axum::Json(payload): axum::Json<super::request::create_coverart::Request>,
|
axum::Json(payload): axum::Json<super::request::create_coverart::Request>,
|
||||||
@@ -485,29 +63,53 @@ pub mod endpoint {
|
|||||||
let mut response = super::response::create_coverart::Response::default();
|
let mut response = super::response::create_coverart::Response::default();
|
||||||
let id = payload.coverart_queue_id;
|
let id = payload.coverart_queue_id;
|
||||||
|
|
||||||
match super::db::get_coverart_queue_data_with_id(&pool, &id).await {
|
match repo_queue::coverart::get_coverart_queue_data_with_id(&pool, &id).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let song_id = payload.song_id;
|
let song_id = payload.song_id;
|
||||||
match crate::callers::song::song_db::get_song(&pool, &song_id).await {
|
match crate::repo::song::get_song(&pool, &song_id).await {
|
||||||
Ok(song) => {
|
Ok(song) => {
|
||||||
let directory = icarus_envy::environment::get_root_directory().await;
|
let directory = icarus_envy::environment::get_root_directory().await.value;
|
||||||
let dir = std::path::Path::new(&directory);
|
let file_type =
|
||||||
|
icarus_meta::detection::coverart::file_type_from_data(&data).unwrap();
|
||||||
|
let coverart_type = if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::JPEG_TYPE
|
||||||
|
{
|
||||||
|
icarus_models::types::CoverArtType::JpegExtension
|
||||||
|
} else if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::JPG_TYPE
|
||||||
|
{
|
||||||
|
icarus_models::types::CoverArtType::JpgExtension
|
||||||
|
} else if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::PNG_TYPE
|
||||||
|
{
|
||||||
|
icarus_models::types::CoverArtType::PngExtension
|
||||||
|
} else {
|
||||||
|
response.message = String::from("Invalid CoverArt type");
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(response),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let filename =
|
||||||
|
icarus_models::coverart::generate_filename(coverart_type, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// TODO: Make this random and the file extension should not be hard coded
|
|
||||||
let filename = format!("{}-coverart.jpeg", &song.filename[..8]);
|
|
||||||
let save_path = dir.join(&filename);
|
|
||||||
let path = String::from(save_path.to_str().unwrap());
|
|
||||||
let mut coverart =
|
let mut coverart =
|
||||||
icarus_models::coverart::init::init_coverart_only_path(path);
|
icarus_models::coverart::init::init_coverart_dir_and_filename(
|
||||||
|
&directory, &filename,
|
||||||
|
);
|
||||||
coverart.title = song.album.clone();
|
coverart.title = song.album.clone();
|
||||||
|
coverart.file_type = file_type.file_type;
|
||||||
coverart.data = data;
|
coverart.data = data;
|
||||||
|
|
||||||
let mut file = std::fs::File::create(&save_path).unwrap();
|
match coverart.save_to_filesystem() {
|
||||||
file.write_all(&coverart.data).unwrap();
|
Ok(_) => {
|
||||||
|
match repo::coverart::create(&pool, &coverart, &song.id).await {
|
||||||
match super::cov_db::create(&pool, &coverart, &song.id).await {
|
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
|
coverart.song_id = song_id;
|
||||||
coverart.id = id;
|
coverart.id = id;
|
||||||
|
println!("Cover Art created");
|
||||||
|
|
||||||
response.message = String::from("Successful");
|
response.message = String::from("Successful");
|
||||||
response.data.push(coverart);
|
response.data.push(coverart);
|
||||||
|
|
||||||
@@ -519,6 +121,15 @@ pub mod endpoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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::BAD_REQUEST, axum::Json(response))
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
@@ -532,21 +143,32 @@ pub mod endpoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wipe_data_from_coverart_queue(
|
/// Endpoint to get cover art with criteria
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = super::super::endpoints::GETCOVERART,
|
||||||
|
params(
|
||||||
|
("id" = uuid::Uuid, Path, description = "Cover art Id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Cover art retrieved", body = super::response::get_coverart::Response),
|
||||||
|
(status = 400, description = "Error retrieving cover art", body = super::response::get_coverart::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_coverart(
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>,
|
axum::extract::Query(params): axum::extract::Query<super::request::get_coverart::Params>,
|
||||||
) -> (
|
) -> (
|
||||||
axum::http::StatusCode,
|
axum::http::StatusCode,
|
||||||
axum::Json<super::response::wipe_data_from_coverart_queue::Response>,
|
axum::Json<super::response::get_coverart::Response>,
|
||||||
) {
|
) {
|
||||||
let mut response = super::response::wipe_data_from_coverart_queue::Response::default();
|
let mut response = super::response::get_coverart::Response::default();
|
||||||
let coverart_queue_id = payload.coverart_queue_id;
|
|
||||||
|
|
||||||
match super::db::get_coverart_queue_with_id(&pool, &coverart_queue_id).await {
|
match params.id {
|
||||||
Ok(coverart_queue) => match super::db::wipe_data(&pool, &coverart_queue.id).await {
|
Some(id) => match repo::coverart::get_coverart(&pool, &id).await {
|
||||||
Ok(id) => {
|
Ok(coverart) => {
|
||||||
response.message = String::from("Success");
|
response.data.push(coverart);
|
||||||
response.data.push(id);
|
response.message = String::from(super::super::response::SUCCESSFUL);
|
||||||
(axum::http::StatusCode::OK, axum::Json(response))
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -554,10 +176,89 @@ pub mod endpoint {
|
|||||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
None => {
|
||||||
|
response.message = String::from("Invalid parameters");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to download cover art
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = super::super::endpoints::DOWNLOADCOVERART,
|
||||||
|
params(
|
||||||
|
("id" = uuid::Uuid, Path, description = "Cover art Id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Cover art downloading", body = Vec<u8>),
|
||||||
|
(status = 404, description = "Cover art not found", body = Vec<u8>)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn download_coverart(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
) -> (axum::http::StatusCode, axum::response::Response) {
|
||||||
|
match repo::coverart::get_coverart(&pool, &id).await {
|
||||||
|
Ok(coverart) => match icarus_models::coverart::io::to_data(&coverart) {
|
||||||
|
Ok(data) => {
|
||||||
|
let (file_type, img_type) =
|
||||||
|
match icarus_meta::detection::coverart::file_type_from_data(&data) {
|
||||||
|
Ok(file_type) => {
|
||||||
|
if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::JPEG_TYPE
|
||||||
|
{
|
||||||
|
(file_type, icarus_models::types::CoverArtType::JpegExtension)
|
||||||
|
} else if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::JPG_TYPE
|
||||||
|
{
|
||||||
|
(file_type, icarus_models::types::CoverArtType::JpgExtension)
|
||||||
|
} else if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::PNG_TYPE
|
||||||
|
{
|
||||||
|
(file_type, icarus_models::types::CoverArtType::PngExtension)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::response::Response::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
response.message = err.to_string();
|
eprintln!("Error: {err:?}");
|
||||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
return (
|
||||||
}
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::response::Response::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let bytes = axum::body::Bytes::from(data);
|
||||||
|
let mut response = bytes.into_response();
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
let filename =
|
||||||
|
icarus_models::coverart::generate_filename(img_type, true).unwrap();
|
||||||
|
headers.insert(
|
||||||
|
axum::http::header::CONTENT_TYPE,
|
||||||
|
file_type.mime.parse().unwrap(),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
axum::http::header::CONTENT_DISPOSITION,
|
||||||
|
format!("attachment; filename=\"{filename}\"")
|
||||||
|
.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(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
// TODO: Explicitly make this module target queueing a song's metadata
|
|
||||||
pub mod request {
|
|
||||||
|
|
||||||
pub mod queue_metadata {
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow)]
|
|
||||||
pub struct Request {
|
|
||||||
pub song_queue_id: uuid::Uuid,
|
|
||||||
pub album: String,
|
|
||||||
pub album_artist: String,
|
|
||||||
pub artist: String,
|
|
||||||
pub disc: i32,
|
|
||||||
pub disc_count: i32,
|
|
||||||
pub duration: i64,
|
|
||||||
pub genre: String,
|
|
||||||
pub title: String,
|
|
||||||
pub track: i32,
|
|
||||||
pub track_count: i32,
|
|
||||||
pub year: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
pub async fn to_json_value(&self) -> serde_json::Value {
|
|
||||||
serde_json::json!(
|
|
||||||
{
|
|
||||||
"song_queue_id": &self.song_queue_id,
|
|
||||||
"album": &self.album,
|
|
||||||
"album_artist": &self.album_artist,
|
|
||||||
"genre": &self.genre,
|
|
||||||
"year": &self.year,
|
|
||||||
"track_count": &self.track_count,
|
|
||||||
"disc_count": &self.disc_count,
|
|
||||||
"title": &self.title,
|
|
||||||
"artist": &self.artist,
|
|
||||||
"disc": &self.disc,
|
|
||||||
"track": &self.track,
|
|
||||||
"duration": &self.duration,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod fetch_metadata {
|
|
||||||
#[derive(
|
|
||||||
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, sqlx::Decode,
|
|
||||||
)]
|
|
||||||
pub struct Params {
|
|
||||||
pub id: Option<uuid::Uuid>,
|
|
||||||
pub song_queue_id: Option<uuid::Uuid>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod response {
|
|
||||||
|
|
||||||
pub mod queue_metadata {
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub message: String,
|
|
||||||
pub data: Vec<uuid::Uuid>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod fetch_metadata {
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub message: String,
|
|
||||||
pub data: Vec<crate::callers::metadata::metadata_queue::MetadataQueue>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod metadata_queue {
|
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
|
|
||||||
pub struct InsertedData {
|
|
||||||
pub id: uuid::Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)]
|
|
||||||
pub struct MetadataQueue {
|
|
||||||
pub id: uuid::Uuid,
|
|
||||||
pub metadata: serde_json::Value,
|
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
|
||||||
pub created_at: time::OffsetDateTime,
|
|
||||||
pub song_queue_id: uuid::Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
metadata: &serde_json::Value,
|
|
||||||
song_queue_id: &uuid::Uuid,
|
|
||||||
) -> Result<uuid::Uuid, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
INSERT INTO "metadataQueue" (metadata, song_queue_id) VALUES($1, $2) RETURNING id;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(metadata)
|
|
||||||
.bind(song_queue_id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error inserting: {e}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => {
|
|
||||||
let id: uuid::Uuid = row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap();
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_with_song_queue_id(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
song_queue_id: &uuid::Uuid,
|
|
||||||
) -> Result<MetadataQueue, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT * FROM "metadataQueue" WHERE song_queue_id = $1;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(song_queue_id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error inserting: {e}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => Ok(MetadataQueue {
|
|
||||||
id: row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
metadata: row
|
|
||||||
.try_get("metadata")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
created_at: row
|
|
||||||
.try_get("created_at")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
song_queue_id: row
|
|
||||||
.try_get("song_queue_id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
}),
|
|
||||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_with_id(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
id: &uuid::Uuid,
|
|
||||||
) -> Result<MetadataQueue, sqlx::Error> {
|
|
||||||
let result = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT id, metadata, created_at, song_queue_id FROM "metadataQueue" WHERE id = $1;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Error inserting: {e}");
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(row) => {
|
|
||||||
let data: serde_json::Value = row
|
|
||||||
.try_get("metadata")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap();
|
|
||||||
Ok(MetadataQueue {
|
|
||||||
id: row
|
|
||||||
.try_get("id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
metadata: data,
|
|
||||||
created_at: row
|
|
||||||
.try_get("created_at")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
song_queue_id: row
|
|
||||||
.try_get("song_queue_id")
|
|
||||||
.map_err(|_e| sqlx::Error::RowNotFound)
|
|
||||||
.unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(_err) => Err(sqlx::Error::RowNotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod endpoint {
|
|
||||||
use axum::{Json, http::StatusCode};
|
|
||||||
|
|
||||||
pub async fn queue_metadata(
|
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
|
||||||
Json(payload): Json<super::request::queue_metadata::Request>,
|
|
||||||
) -> (StatusCode, Json<super::response::queue_metadata::Response>) {
|
|
||||||
let mut results: Vec<uuid::Uuid> = Vec::new();
|
|
||||||
let mut response = super::response::queue_metadata::Response::default();
|
|
||||||
let meta = payload.to_json_value().await;
|
|
||||||
match super::metadata_queue::insert(&pool, &meta, &payload.song_queue_id).await {
|
|
||||||
Ok(metadata_queue_id) => {
|
|
||||||
results.push(metadata_queue_id);
|
|
||||||
response.data = results;
|
|
||||||
response.message = if response.data.is_empty() {
|
|
||||||
String::from("Error")
|
|
||||||
} else {
|
|
||||||
String::from("Success")
|
|
||||||
};
|
|
||||||
|
|
||||||
(StatusCode::OK, Json(response))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(StatusCode::BAD_REQUEST, Json(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_metadata(
|
|
||||||
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
|
||||||
axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>,
|
|
||||||
) -> (StatusCode, Json<super::response::fetch_metadata::Response>) {
|
|
||||||
let mut response = super::response::fetch_metadata::Response::default();
|
|
||||||
|
|
||||||
match params.id {
|
|
||||||
Some(id) => {
|
|
||||||
println!("Something works {id}");
|
|
||||||
|
|
||||||
match super::metadata_queue::get_with_id(&pool, &id).await {
|
|
||||||
Ok(item) => {
|
|
||||||
response.message = String::from("Successful");
|
|
||||||
response.data.push(item);
|
|
||||||
(StatusCode::OK, Json(response))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(StatusCode::BAD_REQUEST, Json(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => match params.song_queue_id {
|
|
||||||
Some(song_queue_id) => {
|
|
||||||
println!("Song queue Id is probably not nil");
|
|
||||||
match super::metadata_queue::get_with_song_queue_id(&pool, &song_queue_id).await
|
|
||||||
{
|
|
||||||
Ok(item) => {
|
|
||||||
response.message = String::from("Successful");
|
|
||||||
response.data.push(item);
|
|
||||||
(StatusCode::OK, Json(response))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
response.message = err.to_string();
|
|
||||||
(StatusCode::BAD_REQUEST, Json(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
println!("What is going on?");
|
|
||||||
(StatusCode::BAD_REQUEST, Json(response))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+14
-12
@@ -1,24 +1,26 @@
|
|||||||
pub mod coverart;
|
pub mod coverart;
|
||||||
pub mod metadata;
|
pub mod queue;
|
||||||
pub mod song;
|
pub mod song;
|
||||||
|
|
||||||
pub mod endpoints {
|
pub mod endpoints {
|
||||||
pub const QUEUESONG: &str = "/api/v2/song/queue";
|
pub const ROOT: &str = "/";
|
||||||
pub const QUEUESONGLINKUSERID: &str = "/api/v2/song/queue/link";
|
|
||||||
pub const QUEUESONGDATA: &str = "/api/v2/song/queue/{id}";
|
|
||||||
pub const QUEUESONGUPDATE: &str = "/api/v2/song/queue/{id}";
|
|
||||||
pub const NEXTQUEUESONG: &str = "/api/v2/song/queue/next";
|
|
||||||
pub const QUEUEMETADATA: &str = "/api/v2/song/metadata/queue";
|
|
||||||
pub const QUEUECOVERART: &str = "/api/v2/coverart/queue";
|
|
||||||
pub const QUEUECOVERARTDATA: &str = "/api/v2/coverart/queue/data/{id}";
|
|
||||||
pub const QUEUECOVERARTLINK: &str = "/api/v2/coverart/queue/link";
|
|
||||||
pub const QUEUESONGDATAWIPE: &str = "/api/v2/song/queue/data/wipe";
|
|
||||||
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
|
|
||||||
|
|
||||||
pub const CREATESONG: &str = "/api/v2/song";
|
pub const CREATESONG: &str = "/api/v2/song";
|
||||||
|
pub const GETSONGS: &str = "/api/v2/song";
|
||||||
|
pub const 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 CREATECOVERART: &str = "/api/v2/coverart";
|
||||||
|
pub const GETCOVERART: &str = "/api/v2/coverart";
|
||||||
|
pub const DOWNLOADCOVERART: &str = "/api/v2/coverart/download/{id}";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod response {
|
pub mod response {
|
||||||
pub const SUCCESSFUL: &str = "SUCCESSFUL";
|
pub const SUCCESSFUL: &str = "SUCCESSFUL";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Basic handler that responds with a static string
|
||||||
|
pub async fn root() -> &'static str {
|
||||||
|
"Hello, World!"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct CoverArtQueue {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub file_type: String,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
pub mod queue {
|
||||||
|
#[derive(utoipa::ToSchema)]
|
||||||
|
pub struct Request {
|
||||||
|
/// Filename
|
||||||
|
pub file: String,
|
||||||
|
#[schema(rename = "type")]
|
||||||
|
/// File type. Should be a file and not a value
|
||||||
|
pub file_type: String,
|
||||||
|
/// Raw data of the cover art file
|
||||||
|
pub value: Vec<u8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod link {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Request {
|
||||||
|
pub coverart_id: uuid::Uuid,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_no_data {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Params {
|
||||||
|
pub id: Option<uuid::Uuid>,
|
||||||
|
pub song_queue_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_with_data {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Params {
|
||||||
|
pub id: Option<uuid::Uuid>,
|
||||||
|
pub song_queue_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wipe_data_from_coverart_queue {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Request {
|
||||||
|
pub coverart_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
pub mod queue {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod link {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Id {
|
||||||
|
pub coverart_id: uuid::Uuid,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<Id>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_no_data {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<super::super::CoverArtQueue>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_with_data {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wipe_data_from_coverart_queue {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod helper {
|
||||||
|
pub fn is_coverart_file_type_valid(file_type: &String) -> bool {
|
||||||
|
let valid_file_types = vec![
|
||||||
|
String::from(icarus_meta::detection::coverart::constants::JPEG_TYPE),
|
||||||
|
String::from(icarus_meta::detection::coverart::constants::JPG_TYPE),
|
||||||
|
String::from(icarus_meta::detection::coverart::constants::PNG_TYPE),
|
||||||
|
];
|
||||||
|
|
||||||
|
for valid_file_type in valid_file_types {
|
||||||
|
if valid_file_type == *file_type {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod endpoint {
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
|
use crate::repo::queue as repo;
|
||||||
|
|
||||||
|
/// Endpoint to queue cover art
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = super::super::endpoints::QUEUECOVERART,
|
||||||
|
request_body(
|
||||||
|
content = super::request::queue::Request,
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful", body = super::response::queue::Response),
|
||||||
|
(status = 400, description = "Error queueing cover art", body = super::response::queue::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn queue(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
mut multipart: axum::extract::Multipart,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::queue::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::queue::Response::default();
|
||||||
|
|
||||||
|
match multipart.next_field().await {
|
||||||
|
Ok(Some(field)) => {
|
||||||
|
let name = field.name().unwrap().to_string();
|
||||||
|
let file_name = field.file_name().unwrap().to_string();
|
||||||
|
let content_type = field.content_type().unwrap().to_string();
|
||||||
|
let data = field.bytes().await.unwrap();
|
||||||
|
let raw_data = data.to_vec();
|
||||||
|
let file_type =
|
||||||
|
match icarus_meta::detection::coverart::file_type_from_data(&raw_data) {
|
||||||
|
Ok(file_type) => file_type,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error: {err:?}");
|
||||||
|
response.message = err.to_string();
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !super::helper::is_coverart_file_type_valid(&file_type.file_type) {
|
||||||
|
response.message = format!("CoverArt file type not supported: {file_type:?}");
|
||||||
|
(
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(response),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Received file '{}' (name = '{}', content-type = '{}', size = {}, file-type = {:?})",
|
||||||
|
file_name,
|
||||||
|
name,
|
||||||
|
content_type,
|
||||||
|
data.len(),
|
||||||
|
file_type
|
||||||
|
);
|
||||||
|
|
||||||
|
match repo::coverart::insert(&pool, &raw_data, &file_type.file_type).await {
|
||||||
|
Ok(id) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(id);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)),
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to link queued cover art
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = super::super::endpoints::QUEUECOVERARTLINK,
|
||||||
|
request_body(
|
||||||
|
content = super::request::link::Request,
|
||||||
|
description = "Linking queued cover art to queued song",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued cover art linked", body = super::response::link::Response),
|
||||||
|
(status = 400, description = "Linkage failed", body = super::response::link::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn link(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::link::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::link::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::link::Response::default();
|
||||||
|
let id = payload.coverart_id;
|
||||||
|
let song_id = payload.song_queue_id;
|
||||||
|
|
||||||
|
match repo::coverart::update(&pool, &id, &song_id).await {
|
||||||
|
Ok(_o) => {
|
||||||
|
response.data.push(super::response::link::Id {
|
||||||
|
song_queue_id: song_id,
|
||||||
|
coverart_id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to fetch cover art details
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = super::super::endpoints::QUEUECOVERART,
|
||||||
|
params(
|
||||||
|
("id" = uuid::Uuid, Path, description = "Queued cover art Id"),
|
||||||
|
("song_queue_id" = uuid::Uuid, Path, description = "Queued song Id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued song linked", body = super::response::fetch_coverart_no_data::Response),
|
||||||
|
(status = 400, description = "Linkage failed", body = super::response::fetch_coverart_no_data::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn fetch_coverart_no_data(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Query(params): axum::extract::Query<
|
||||||
|
super::request::fetch_coverart_no_data::Params,
|
||||||
|
>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::fetch_coverart_no_data::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::fetch_coverart_no_data::Response::default();
|
||||||
|
|
||||||
|
match params.id {
|
||||||
|
Some(id) => match repo::coverart::get_coverart_queue_with_id(&pool, &id).await {
|
||||||
|
Ok(cover_art_queue) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(cover_art_queue);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => match params.song_queue_id {
|
||||||
|
Some(song_queue_id) => match repo::coverart::get_coverart_queue_with_song_queue_id(
|
||||||
|
&pool,
|
||||||
|
&song_queue_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(cover_art_queue) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(cover_art_queue);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
response.message = String::from("No valid id provided");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to fetch the queued cover art data
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = super::super::endpoints::QUEUECOVERARTDATA,
|
||||||
|
params(("id" = uuid::Uuid, Path, description = "Queued cover art Id")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued cover art data", body = Vec<u8>),
|
||||||
|
(status = 400, description = "Error fetching queued cover art data", body = Vec<u8>)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn fetch_coverart_with_data(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
) -> (axum::http::StatusCode, axum::response::Response) {
|
||||||
|
match repo::coverart::get_coverart_queue_data_with_id(&pool, &id).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let file_type =
|
||||||
|
icarus_meta::detection::coverart::file_type_from_data(&data).unwrap();
|
||||||
|
let bytes = axum::body::Bytes::from(data);
|
||||||
|
let mut response = bytes.into_response();
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
headers.insert(
|
||||||
|
axum::http::header::CONTENT_TYPE,
|
||||||
|
file_type.mime.parse().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let coverart_type = if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::JPEG_TYPE
|
||||||
|
{
|
||||||
|
icarus_models::types::CoverArtType::JpegExtension
|
||||||
|
} else if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::JPG_TYPE
|
||||||
|
{
|
||||||
|
icarus_models::types::CoverArtType::JpgExtension
|
||||||
|
} else if file_type.file_type
|
||||||
|
== icarus_meta::detection::coverart::constants::PNG_TYPE
|
||||||
|
{
|
||||||
|
icarus_models::types::CoverArtType::PngExtension
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::response::Response::default(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let filename =
|
||||||
|
icarus_models::coverart::generate_filename(coverart_type, true).unwrap();
|
||||||
|
headers.insert(
|
||||||
|
axum::http::header::CONTENT_DISPOSITION,
|
||||||
|
format!("attachment; filename=\"{filename}\"")
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, response)
|
||||||
|
}
|
||||||
|
Err(_err) => (
|
||||||
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
|
axum::response::Response::default(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to wipe data from the cover art queue
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = super::super::endpoints::QUEUECOVERARTDATAWIPE,
|
||||||
|
request_body(
|
||||||
|
content = super::request::wipe_data_from_coverart_queue::Request,
|
||||||
|
description = "Data required to wipe the data from the cover art queue",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Data wiped from cover art queue", body = super::response::wipe_data_from_coverart_queue::Response),
|
||||||
|
(status = 400, description = "Error wiping the data", body = super::response::wipe_data_from_coverart_queue::Response),
|
||||||
|
(status = 404, description = "Cover art not found", body = super::response::wipe_data_from_coverart_queue::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn wipe_data_from_coverart_queue(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::wipe_data_from_coverart_queue::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::wipe_data_from_coverart_queue::Response::default();
|
||||||
|
let coverart_queue_id = payload.coverart_queue_id;
|
||||||
|
|
||||||
|
match repo::coverart::get_coverart_queue_with_id(&pool, &coverart_queue_id).await {
|
||||||
|
Ok(coverart_queue) => {
|
||||||
|
match repo::coverart::wipe_data(&pool, &coverart_queue.id).await {
|
||||||
|
Ok(id) => {
|
||||||
|
response.message = String::from("Success");
|
||||||
|
response.data.push(id);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
pub mod request {
|
||||||
|
pub mod queue_metadata {
|
||||||
|
#[derive(
|
||||||
|
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema,
|
||||||
|
)]
|
||||||
|
pub struct Request {
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
pub album: String,
|
||||||
|
pub album_artist: String,
|
||||||
|
pub artist: String,
|
||||||
|
pub disc: i32,
|
||||||
|
pub disc_count: i32,
|
||||||
|
pub duration: i64,
|
||||||
|
pub genre: String,
|
||||||
|
pub title: String,
|
||||||
|
pub track: i32,
|
||||||
|
pub track_count: i32,
|
||||||
|
pub year: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub async fn to_json_value(&self) -> serde_json::Value {
|
||||||
|
serde_json::json!(
|
||||||
|
{
|
||||||
|
"song_queue_id": &self.song_queue_id,
|
||||||
|
"album": &self.album,
|
||||||
|
"album_artist": &self.album_artist,
|
||||||
|
"genre": &self.genre,
|
||||||
|
"year": &self.year,
|
||||||
|
"track_count": &self.track_count,
|
||||||
|
"disc_count": &self.disc_count,
|
||||||
|
"title": &self.title,
|
||||||
|
"artist": &self.artist,
|
||||||
|
"disc": &self.disc,
|
||||||
|
"track": &self.track,
|
||||||
|
"duration": &self.duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_metadata {
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
serde::Deserialize,
|
||||||
|
serde::Serialize,
|
||||||
|
sqlx::FromRow,
|
||||||
|
sqlx::Decode,
|
||||||
|
utoipa::ToSchema,
|
||||||
|
)]
|
||||||
|
pub struct Params {
|
||||||
|
pub id: Option<uuid::Uuid>,
|
||||||
|
pub song_queue_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
pub mod queue_metadata {
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_metadata {
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<crate::repo::queue::metadata::MetadataQueue>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Module for metadata related endpoints
|
||||||
|
pub mod endpoint {
|
||||||
|
use crate::repo::queue as repo_queue;
|
||||||
|
|
||||||
|
/// Endpoint to create queued metadata
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = super::super::endpoints::QUEUEMETADATA,
|
||||||
|
request_body(
|
||||||
|
content = super::request::queue_metadata::Request,
|
||||||
|
description = "Data required to create queued metadata",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued metadata created", body = super::response::queue_metadata::Response),
|
||||||
|
(status = 400, description = "Error creating queued metadata", body = super::response::queue_metadata::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn queue_metadata(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::queue_metadata::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::queue_metadata::Response>,
|
||||||
|
) {
|
||||||
|
let mut results: Vec<uuid::Uuid> = Vec::new();
|
||||||
|
let mut response = super::response::queue_metadata::Response::default();
|
||||||
|
let meta = payload.to_json_value().await;
|
||||||
|
match repo_queue::metadata::insert(&pool, &meta, &payload.song_queue_id).await {
|
||||||
|
Ok(metadata_queue_id) => {
|
||||||
|
results.push(metadata_queue_id);
|
||||||
|
response.data = results;
|
||||||
|
response.message = if response.data.is_empty() {
|
||||||
|
String::from("Error")
|
||||||
|
} else {
|
||||||
|
String::from(super::super::super::response::SUCCESSFUL)
|
||||||
|
};
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to get queued metadata
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = super::super::endpoints::QUEUEMETADATA,
|
||||||
|
params(
|
||||||
|
("id" = uuid::Uuid, Path, description = "Id of queued metadata"),
|
||||||
|
("song_queue_id" = uuid::Uuid, Path, description = "Id of queued song")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued metadata retrieved", body = super::response::fetch_metadata::Response),
|
||||||
|
(status = 400, description = "Error retrieving queued metadata", body = super::response::fetch_metadata::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn fetch_metadata(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::fetch_metadata::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::fetch_metadata::Response::default();
|
||||||
|
|
||||||
|
match params.id {
|
||||||
|
Some(id) => {
|
||||||
|
println!("Something works {id}");
|
||||||
|
|
||||||
|
match repo_queue::metadata::get_with_id(&pool, &id).await {
|
||||||
|
Ok(item) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(item);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => match params.song_queue_id {
|
||||||
|
Some(song_queue_id) => {
|
||||||
|
println!("Song queue Id is probably not nil");
|
||||||
|
match repo_queue::metadata::get_with_song_queue_id(&pool, &song_queue_id).await
|
||||||
|
{
|
||||||
|
Ok(item) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(item);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("What is going on?");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
pub mod coverart;
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod song;
|
||||||
|
|
||||||
|
pub mod endpoints {
|
||||||
|
pub const QUEUESONG: &str = "/api/v2/song/queue";
|
||||||
|
pub const QUEUESONGLINKUSERID: &str = "/api/v2/song/queue/link";
|
||||||
|
pub const QUEUESONGDATA: &str = "/api/v2/song/queue/{id}";
|
||||||
|
pub const QUEUESONGUPDATE: &str = "/api/v2/song/queue/{id}";
|
||||||
|
pub const NEXTQUEUESONG: &str = "/api/v2/song/queue/next";
|
||||||
|
pub const QUEUEMETADATA: &str = "/api/v2/song/metadata/queue";
|
||||||
|
pub const QUEUECOVERART: &str = "/api/v2/coverart/queue";
|
||||||
|
pub const QUEUECOVERARTDATA: &str = "/api/v2/coverart/queue/data/{id}";
|
||||||
|
pub const QUEUECOVERARTLINK: &str = "/api/v2/coverart/queue/link";
|
||||||
|
pub const QUEUESONGDATAWIPE: &str = "/api/v2/song/queue/data/wipe";
|
||||||
|
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
|
||||||
|
}
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
pub mod request {
|
||||||
|
pub mod song_queue {
|
||||||
|
#[derive(utoipa::ToSchema)]
|
||||||
|
pub struct SongQueueRequest {
|
||||||
|
/// Filename
|
||||||
|
pub file: String,
|
||||||
|
#[schema(rename = "type")]
|
||||||
|
/// File type. Should be a file and not a value
|
||||||
|
pub file_type: String,
|
||||||
|
/// Raw data of the flac file
|
||||||
|
pub value: Vec<u8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod update_status {
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Request {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wipe_data_from_song_queue {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Request {
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod link_user_id {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Request {
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
pub user_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
pub mod song_queue {
|
||||||
|
/// Song queue response
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
/// Id of the queued song
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_queue_song {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<crate::repo::queue::song::dbtype::SongQueue>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod update_status {
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct ChangedStatus {
|
||||||
|
pub old_status: String,
|
||||||
|
pub new_status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<ChangedStatus>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod update_song_queue {
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wipe_data_from_song_queue {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod link_user_id {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_song_valid(data: &[u8]) -> Result<bool, std::io::Error> {
|
||||||
|
match icarus_meta::detection::song::file_type_from_data(data) {
|
||||||
|
Ok(file_type) => {
|
||||||
|
if file_type.file_type == icarus_meta::detection::song::constants::FLAC_TYPE {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod endpoint {
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
|
use crate::repo::queue as repo;
|
||||||
|
|
||||||
|
/// Endpoint to queue a song. Starts the process and places the song in a queue
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = super::super::endpoints::QUEUESONG,
|
||||||
|
request_body(
|
||||||
|
content = super::request::song_queue::SongQueueRequest,
|
||||||
|
description = "Multipart form data for uploading song",
|
||||||
|
content_type = "multipart/form-data"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Song queued", body = super::response::song_queue::Response),
|
||||||
|
(status = 400, description = "Invalid request passed", body = super::response::song_queue::Response),
|
||||||
|
(status = 500, description = "Error queueing song", body = super::response::song_queue::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn queue_song(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
mut multipart: axum::extract::Multipart,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::song_queue::Response>,
|
||||||
|
) {
|
||||||
|
let mut results: Vec<uuid::Uuid> = Vec::new();
|
||||||
|
let mut response = super::response::song_queue::Response::default();
|
||||||
|
|
||||||
|
match multipart.next_field().await {
|
||||||
|
Ok(multipart_field) => {
|
||||||
|
if let Some(field) = multipart_field {
|
||||||
|
let name = field.name().unwrap().to_string();
|
||||||
|
let file_name = field.file_name().unwrap().to_string();
|
||||||
|
let content_type = field.content_type().unwrap().to_string();
|
||||||
|
let data = field.bytes().await.unwrap();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
|
||||||
|
file_name,
|
||||||
|
name,
|
||||||
|
content_type,
|
||||||
|
data.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let raw_data: Vec<u8> = data.to_vec();
|
||||||
|
match super::is_song_valid(&raw_data).await {
|
||||||
|
Ok(valid) => {
|
||||||
|
if valid {
|
||||||
|
match repo::song::insert(
|
||||||
|
&pool,
|
||||||
|
&raw_data,
|
||||||
|
&file_name,
|
||||||
|
&crate::repo::queue::song::status::PENDING.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(queued_song) => {
|
||||||
|
results.push(queued_song);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.message = String::from("Invalid song type");
|
||||||
|
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.data = results;
|
||||||
|
response.message = if response.data.is_empty() {
|
||||||
|
String::from("Error")
|
||||||
|
} else {
|
||||||
|
String::from(super::super::super::response::SUCCESSFUL)
|
||||||
|
};
|
||||||
|
(axum::http::StatusCode::CREATED, axum::Json(response))
|
||||||
|
} else {
|
||||||
|
response.message = String::from("No field found in multipart");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to link a user id to a queued song
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = super::super::endpoints::QUEUESONGLINKUSERID,
|
||||||
|
request_body(
|
||||||
|
content = super::request::link_user_id::Request,
|
||||||
|
description = "User Id and queued song id",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued song linked", body = super::response::link_user_id::Response),
|
||||||
|
(status = 400, description = "Linkage failed", body = super::response::link_user_id::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn link_user_id(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::link_user_id::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::link_user_id::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::link_user_id::Response::default();
|
||||||
|
|
||||||
|
match repo::song::get_song_queue(&pool, &payload.song_queue_id).await {
|
||||||
|
Ok(song_queue) => {
|
||||||
|
match repo::song::link_user_id(&pool, &song_queue.id, &payload.user_id).await {
|
||||||
|
Ok(user_id) => {
|
||||||
|
response.message = String::from(crate::callers::response::SUCCESSFUL);
|
||||||
|
response.data.push(user_id);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to fetch the next queued song as long as it is available
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = super::super::endpoints::NEXTQUEUESONG,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued song is present and available", body = super::response::fetch_queue_song::Response),
|
||||||
|
(status = 400, description = "Linkage failed", body = super::response::fetch_queue_song::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn fetch_queue_song(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::fetch_queue_song::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::fetch_queue_song::Response::default();
|
||||||
|
|
||||||
|
match repo::song::get_most_recent_and_update(&pool).await {
|
||||||
|
Ok(item) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(item);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to download the queued song
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = super::super::endpoints::QUEUESONGDATA,
|
||||||
|
params(("id" = uuid::Uuid, Path, description = "Queued song Id")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued song linked", body = Vec<u8>),
|
||||||
|
(status = 400, description = "Linkage failed", body = Vec<u8>)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn download_queued_song(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
) -> (axum::http::StatusCode, axum::response::Response) {
|
||||||
|
println!("Id: {id}");
|
||||||
|
|
||||||
|
match repo::song::get_data(&pool, &id).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let by = axum::body::Bytes::from(data);
|
||||||
|
let mut response = by.into_response();
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
headers.insert(
|
||||||
|
axum::http::header::CONTENT_TYPE,
|
||||||
|
"audio/flac".parse().unwrap(),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
axum::http::header::CONTENT_DISPOSITION,
|
||||||
|
format!("attachment; filename=\"{id}.flac\"")
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, response)
|
||||||
|
}
|
||||||
|
Err(_err) => (
|
||||||
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
|
axum::response::Response::default(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to update the status of a queued song
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = super::super::endpoints::QUEUESONG,
|
||||||
|
request_body(
|
||||||
|
content = super::request::update_status::Request,
|
||||||
|
description = "Update the status of a queued song",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Status has been updated", body = super::response::update_status::Response),
|
||||||
|
(status = 400, description = "Error updating status of queued song", body = super::response::update_status::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn update_song_queue_status(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::update_status::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::update_status::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::update_status::Response::default();
|
||||||
|
|
||||||
|
if repo::song::status::is_valid(&payload.status).await {
|
||||||
|
let id = payload.id;
|
||||||
|
if !id.is_nil() {
|
||||||
|
match repo::song::get_status_of_song_queue(&pool, &id).await {
|
||||||
|
Ok(old) => {
|
||||||
|
match repo::song::update_song_queue_status(&pool, &payload.status, &id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(new) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response
|
||||||
|
.data
|
||||||
|
.push(super::response::update_status::ChangedStatus {
|
||||||
|
old_status: old,
|
||||||
|
new_status: new,
|
||||||
|
});
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.message = String::from("Id is nil");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.message = String::from("Status not valid");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to update the queued song data
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = super::super::endpoints::QUEUESONGUPDATE,
|
||||||
|
request_body(
|
||||||
|
content = super::request::song_queue::SongQueueRequest,
|
||||||
|
description = "Multipart form data for uploading song",
|
||||||
|
content_type = "multipart/form-data"
|
||||||
|
),
|
||||||
|
params(("id" = uuid::Uuid, Path, description = "Queued song Id")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued song updated", body = super::response::update_song_queue::Response),
|
||||||
|
(status = 400, description = "Error updating queued song", body = super::response::update_song_queue::Response),
|
||||||
|
(status = 404, description = "Queued song not found", body = super::response::update_song_queue::Response),
|
||||||
|
(status = 500, description = "Error updating queued song", body = super::response::update_song_queue::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn update_song_queue(
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
mut multipart: axum::extract::Multipart,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::update_song_queue::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::update_song_queue::Response::default();
|
||||||
|
|
||||||
|
if let Some(field) = multipart.next_field().await.unwrap() {
|
||||||
|
let name = field.name().unwrap().to_string();
|
||||||
|
let file_name = field.file_name().unwrap().to_string();
|
||||||
|
let content_type = match field.content_type() {
|
||||||
|
Some(ct) => ct.to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = field.bytes().await.unwrap();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
|
||||||
|
file_name,
|
||||||
|
name,
|
||||||
|
content_type,
|
||||||
|
data.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let raw_data: Vec<u8> = data.to_vec();
|
||||||
|
match super::is_song_valid(&raw_data).await {
|
||||||
|
Ok(valid) => {
|
||||||
|
if valid {
|
||||||
|
match repo::song::update(&pool, &raw_data, &id).await {
|
||||||
|
Ok(_) => {
|
||||||
|
response.message =
|
||||||
|
String::from(super::super::super::response::SUCCESSFUL);
|
||||||
|
response.data.push(id);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(response),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.message = String::from("Invalid song type");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(response),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.message = String::from("No data provided");
|
||||||
|
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint to wipe the data from a queued song
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = super::super::endpoints::QUEUESONGDATAWIPE,
|
||||||
|
request_body(
|
||||||
|
content = super::request::wipe_data_from_song_queue::Request,
|
||||||
|
description = "Pass the queued song Id to wipe the data",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Queued song data wiped", body = super::response::wipe_data_from_song_queue::Response),
|
||||||
|
(status = 404, description = "Queued song cannot be found", body = super::response::wipe_data_from_song_queue::Response)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn wipe_data_from_song_queue(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::wipe_data_from_song_queue::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::wipe_data_from_song_queue::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::wipe_data_from_song_queue::Response::default();
|
||||||
|
let id = payload.song_queue_id;
|
||||||
|
|
||||||
|
match repo::song::get_song_queue(&pool, &id).await {
|
||||||
|
Ok(song_queue) => match repo::song::wipe_data(&pool, &song_queue.id).await {
|
||||||
|
Ok(wiped_id) => {
|
||||||
|
response.message = String::from("Success");
|
||||||
|
response.data.push(wiped_id);
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+314
-808
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
/// Full hosting address that the application will be broadcasting
|
||||||
|
pub fn get_full() -> String {
|
||||||
|
format!("{ADDRESS}:{PORT}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ADDRESS: &str = "0.0.0.0";
|
||||||
|
pub const PORT: &str = "8000";
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use axum::routing::{delete, get, patch, post};
|
||||||
|
use tower_http::timeout::TimeoutLayer;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
use crate::callers::coverart as coverart_caller;
|
||||||
|
use crate::callers::queue::coverart as coverart_queue_callers;
|
||||||
|
use crate::callers::queue::metadata as metadata_queue_caller;
|
||||||
|
use crate::callers::queue::song as song_queue_callers;
|
||||||
|
use crate::callers::song as song_caller;
|
||||||
|
use coverart_caller::endpoint as coverart_endpoints;
|
||||||
|
use coverart_caller::response as coverart_responses;
|
||||||
|
use coverart_queue_callers::endpoint as coverart_queue_endpoints;
|
||||||
|
use coverart_queue_callers::response as coverart_queue_responses;
|
||||||
|
use metadata_queue_caller::endpoint as metadata_queue_endpoints;
|
||||||
|
use metadata_queue_caller::response as metadata_queue_responses;
|
||||||
|
use song_caller::endpoint as song_endpoints;
|
||||||
|
use song_caller::response as song_responses;
|
||||||
|
use song_queue_callers::endpoint as song_queue_endpoints;
|
||||||
|
use song_queue_callers::response as song_queue_responses;
|
||||||
|
|
||||||
|
mod cors {
|
||||||
|
pub async fn configure_cors() -> tower_http::cors::CorsLayer {
|
||||||
|
let cors = tower_http::cors::CorsLayer::new()
|
||||||
|
.allow_methods([
|
||||||
|
axum::http::Method::GET,
|
||||||
|
axum::http::Method::POST,
|
||||||
|
axum::http::Method::PUT,
|
||||||
|
axum::http::Method::DELETE,
|
||||||
|
]) // Specify allowed methods:cite[2]
|
||||||
|
.allow_headers([
|
||||||
|
axum::http::header::CONTENT_TYPE,
|
||||||
|
axum::http::header::AUTHORIZATION,
|
||||||
|
]) // Specify allowed headers:cite[2]
|
||||||
|
.allow_credentials(true) // If you need to send cookies or authentication headers:cite[2]
|
||||||
|
.max_age(std::time::Duration::from_secs(3600)); // Cache the preflight response for 1 hour:cite[2]
|
||||||
|
|
||||||
|
// Dynamically set the allowed origin based on the environment
|
||||||
|
match std::env::var(icarus_envy::keys::APP_ENV).as_deref() {
|
||||||
|
Ok("production") => {
|
||||||
|
// In production, allow only your specific, trusted origins
|
||||||
|
let allowed_origins_env = icarus_envy::environment::get_allowed_origins().await;
|
||||||
|
match icarus_envy::utility::delimitize(&allowed_origins_env) {
|
||||||
|
Ok(alwd) => {
|
||||||
|
let allowed_origins: Vec<axum::http::HeaderValue> = alwd
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.parse::<axum::http::HeaderValue>().unwrap())
|
||||||
|
.collect();
|
||||||
|
cors.allow_origin(allowed_origins)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error getting allowed origins: Error: {err:?}");
|
||||||
|
std::process::exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Development (default): Allow localhost origins
|
||||||
|
cors.allow_origin(vec![
|
||||||
|
"http://localhost:8000".parse().unwrap(),
|
||||||
|
"http://127.0.0.1:8000".parse().unwrap(),
|
||||||
|
"http://localhost:4200".parse().unwrap(),
|
||||||
|
"http://127.0.0.1:4200".parse().unwrap(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(utoipa::OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(song_queue_endpoints::queue_song, song_queue_endpoints::link_user_id, song_queue_endpoints::fetch_queue_song, song_queue_endpoints::download_queued_song,
|
||||||
|
song_queue_endpoints::update_song_queue_status, song_queue_endpoints::update_song_queue, song_endpoints::create_metadata, song_queue_endpoints::wipe_data_from_song_queue, song_endpoints::get_songs, song_endpoints::get_all_songs, song_endpoints::stream_song, song_endpoints::download_song,
|
||||||
|
song_endpoints::delete_song, coverart_queue_endpoints::queue, coverart_queue_endpoints::link, coverart_queue_endpoints::fetch_coverart_no_data,
|
||||||
|
coverart_queue_endpoints::fetch_coverart_with_data, coverart_endpoints::create_coverart, coverart_queue_endpoints::wipe_data_from_coverart_queue,
|
||||||
|
coverart_endpoints::get_coverart, coverart_endpoints::download_coverart,
|
||||||
|
metadata_queue_endpoints::queue_metadata, metadata_queue_endpoints::fetch_metadata),
|
||||||
|
components(schemas(song_queue_callers::response::song_queue::Response, song_queue_callers::response::link_user_id::Response, song_queue_callers::response::fetch_queue_song::Response,
|
||||||
|
song_queue_responses::update_status::Response, song_queue_callers::response::update_song_queue::Response, song_responses::create_metadata::Response,
|
||||||
|
song_queue_callers::response::wipe_data_from_song_queue::Response, song_responses::get_songs::Response, song_responses::delete_song::Response,
|
||||||
|
coverart_queue_responses::queue::Response, coverart_queue_responses::link::Response, coverart_queue_responses::fetch_coverart_no_data::Response,
|
||||||
|
coverart_queue_responses::fetch_coverart_with_data::Response, coverart_responses::create_coverart::Response,
|
||||||
|
coverart_queue_responses::wipe_data_from_coverart_queue::Response, coverart_responses::get_coverart::Response,
|
||||||
|
metadata_queue_responses::queue_metadata::Response, metadata_queue_responses::fetch_metadata::Response)),
|
||||||
|
tags(
|
||||||
|
(name = "Icarus API", description = "Web API to manage music")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
struct ApiDoc;
|
||||||
|
|
||||||
|
pub async fn routes() -> axum::Router {
|
||||||
|
axum::Router::new()
|
||||||
|
.route(crate::callers::endpoints::ROOT, get(crate::callers::root))
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUESONG,
|
||||||
|
post(crate::callers::queue::song::endpoint::queue_song).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUESONG,
|
||||||
|
patch(crate::callers::queue::song::endpoint::update_song_queue_status).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUESONGLINKUSERID,
|
||||||
|
patch(crate::callers::queue::song::endpoint::link_user_id).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUESONGDATA,
|
||||||
|
get(crate::callers::queue::song::endpoint::download_queued_song).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::NEXTQUEUESONG,
|
||||||
|
get(crate::callers::queue::song::endpoint::fetch_queue_song).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUESONGUPDATE,
|
||||||
|
patch(crate::callers::queue::song::endpoint::update_song_queue).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUESONGDATAWIPE,
|
||||||
|
patch(crate::callers::queue::song::endpoint::wipe_data_from_song_queue).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUEMETADATA,
|
||||||
|
post(crate::callers::queue::metadata::endpoint::queue_metadata).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUEMETADATA,
|
||||||
|
get(crate::callers::queue::metadata::endpoint::fetch_metadata).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUECOVERART,
|
||||||
|
post(crate::callers::queue::coverart::endpoint::queue).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUECOVERARTDATA,
|
||||||
|
get(crate::callers::queue::coverart::endpoint::fetch_coverart_with_data).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUECOVERART,
|
||||||
|
get(crate::callers::queue::coverart::endpoint::fetch_coverart_no_data).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUECOVERARTLINK,
|
||||||
|
patch(crate::callers::queue::coverart::endpoint::link).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::queue::endpoints::QUEUECOVERARTDATAWIPE,
|
||||||
|
patch(crate::callers::queue::coverart::endpoint::wipe_data_from_coverart_queue)
|
||||||
|
.route_layer(axum::middleware::from_fn(
|
||||||
|
crate::auth::auth::<axum::body::Body>,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::CREATESONG,
|
||||||
|
post(crate::callers::song::endpoint::create_metadata).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::CREATECOVERART,
|
||||||
|
post(crate::callers::coverart::endpoint::create_coverart).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::GETSONGS,
|
||||||
|
get(crate::callers::song::endpoint::get_songs).route_layer(axum::middleware::from_fn(
|
||||||
|
crate::auth::auth::<axum::body::Body>,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::GETCOVERART,
|
||||||
|
get(crate::callers::coverart::endpoint::get_coverart).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::DOWNLOADCOVERART,
|
||||||
|
get(crate::callers::coverart::endpoint::download_coverart).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::STREAMSONG,
|
||||||
|
get(crate::callers::song::endpoint::stream_song).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::DOWNLOADSONG,
|
||||||
|
get(crate::callers::song::endpoint::download_song).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::DELETESONG,
|
||||||
|
delete(crate::callers::song::endpoint::delete_song).route_layer(
|
||||||
|
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
crate::callers::endpoints::GETALLSONGS,
|
||||||
|
get(crate::callers::song::endpoint::get_all_songs),
|
||||||
|
)
|
||||||
|
.layer(cors::configure_cors().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn app() -> axum::Router {
|
||||||
|
let pool = crate::db::create_pool()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create pool");
|
||||||
|
|
||||||
|
crate::db::migrations(&pool).await;
|
||||||
|
|
||||||
|
let cors = cors::configure_cors().await;
|
||||||
|
|
||||||
|
routes()
|
||||||
|
.await
|
||||||
|
.merge(
|
||||||
|
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
|
||||||
|
.url("/api-docs/openapi.json", ApiDoc::openapi()),
|
||||||
|
)
|
||||||
|
.layer(axum::Extension(pool))
|
||||||
|
.layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024))
|
||||||
|
.layer(TimeoutLayer::with_status_code(
|
||||||
|
axum::http::StatusCode::OK,
|
||||||
|
Duration::from_secs(300),
|
||||||
|
))
|
||||||
|
.layer(cors)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod host;
|
||||||
|
pub mod init;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
|
||||||
|
pub mod connection_settings {
|
||||||
|
pub const MAXCONN: u32 = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_pool() -> Result<sqlx::PgPool, sqlx::Error> {
|
||||||
|
let database_url = icarus_envy::environment::get_db_url().await.value;
|
||||||
|
println!("Database url: {database_url}");
|
||||||
|
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.max_connections(connection_settings::MAXCONN)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn migrations(pool: &sqlx::PgPool) {
|
||||||
|
// Run migrations using the sqlx::migrate! macro
|
||||||
|
// Assumes your migrations are in a ./migrations folder relative to Cargo.toml
|
||||||
|
sqlx::migrate!("./migrations")
|
||||||
|
.run(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run migrations");
|
||||||
|
}
|
||||||
+990
-373
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
|||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
coverart: &icarus_models::coverart::CoverArt,
|
||||||
|
song_id: &uuid::Uuid,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "coverart" (title, directory, filename, file_type, song_id) VALUES($1, $2, $3, $4, $5) RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&coverart.title)
|
||||||
|
.bind(&coverart.directory)
|
||||||
|
.bind(&coverart.filename)
|
||||||
|
.bind(&coverart.file_type)
|
||||||
|
.bind(song_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, title, directory, filename, file_type, song_id FROM "coverart" WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(icarus_models::coverart::CoverArt {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
title: row
|
||||||
|
.try_get("title")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
directory: row
|
||||||
|
.try_get("directory")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
file_type: row
|
||||||
|
.try_get("file_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_id: row
|
||||||
|
.try_get("song_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_with_song_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song_id: &uuid::Uuid,
|
||||||
|
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, title, directory, filename, file_type, song_id FROM "coverart" WHERE song_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(icarus_models::coverart::CoverArt {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
title: row
|
||||||
|
.try_get("title")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
directory: row
|
||||||
|
.try_get("directory")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
file_type: row
|
||||||
|
.try_get("file_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
data: Vec::new(),
|
||||||
|
song_id: row
|
||||||
|
.try_get("song_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_coverart(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM "coverart"
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, title, directory, filename, file_type, song_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error deleting data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(icarus_models::coverart::CoverArt {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
title: row
|
||||||
|
.try_get("title")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
directory: row
|
||||||
|
.try_get("directory")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
file_type: row
|
||||||
|
.try_get("file_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_id: row
|
||||||
|
.try_get("song_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
data: Vec::new(),
|
||||||
|
}),
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod coverart;
|
||||||
|
pub mod queue;
|
||||||
|
pub mod song;
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
pub async fn insert(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
data: &Vec<u8>,
|
||||||
|
file_type: &str,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "coverartQueue" (data, file_type) VALUES($1, $2) RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(data)
|
||||||
|
.bind(file_type)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
coverart_id: &uuid::Uuid,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<i32, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "coverartQueue" SET song_queue_id = $1 WHERE id = $2;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.bind(coverart_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(0),
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_with_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<crate::callers::queue::coverart::CoverArtQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, file_type, song_queue_id FROM "coverartQueue" WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(crate::callers::queue::coverart::CoverArtQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
file_type: row
|
||||||
|
.try_get("file_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_with_song_queue_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<crate::callers::queue::coverart::CoverArtQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, file_type, song_queue_id FROM "coverartQueue" WHERE song_queue_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(crate::callers::queue::coverart::CoverArtQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
file_type: row
|
||||||
|
.try_get("file_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_data_with_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<Vec<u8>, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT data FROM "coverartQueue" WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("data")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_data_with_song_queue_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<Vec<u8>, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT data FROM "coverartQueue" WHERE song_queue_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("data")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wipe_data(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
coverart_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "coverartQueue" SET data = NULL WHERE id = $1 RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(coverart_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error updating query: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
|
||||||
|
pub struct InsertedData {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct MetadataQueue {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub metadata: serde_json::Value,
|
||||||
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
|
pub created_at: time::OffsetDateTime,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
metadata: &serde_json::Value,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "metadataQueue" (metadata, song_queue_id) VALUES($1, $2) RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(metadata)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_with_song_queue_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<MetadataQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT * FROM "metadataQueue" WHERE song_queue_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(MetadataQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
metadata: row
|
||||||
|
.try_get("metadata")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
created_at: row
|
||||||
|
.try_get("created_at")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_with_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<MetadataQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, metadata, created_at, song_queue_id FROM "metadataQueue" WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let data: serde_json::Value = row
|
||||||
|
.try_get("metadata")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(MetadataQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
metadata: data,
|
||||||
|
created_at: row
|
||||||
|
.try_get("created_at")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod coverart;
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod song;
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
pub mod status {
|
||||||
|
pub const PENDING: &str = "pending";
|
||||||
|
pub const READY: &str = "ready";
|
||||||
|
pub const PROCESSING: &str = "processing";
|
||||||
|
pub const DONE: &str = "done";
|
||||||
|
|
||||||
|
pub async fn is_valid(status: &str) -> bool {
|
||||||
|
status == PENDING || status == PROCESSING || status == DONE || status == READY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod dbtype {
|
||||||
|
/// Maps to the songQueue table. Does not contain the data field
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct SongQueue {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub filename: String,
|
||||||
|
pub status: String,
|
||||||
|
pub user_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
data: &Vec<u8>,
|
||||||
|
filename: &String,
|
||||||
|
status: &String,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "songQueue" (data, filename, status) VALUES($1, $2, $3) RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(data)
|
||||||
|
.bind(filename)
|
||||||
|
.bind(status)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
data: &Vec<u8>,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<Vec<u8>, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "songQueue" SET data = $1 WHERE id = $2 RETURNING data;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(data)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("data")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_most_recent_and_update(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
) -> Result<dbtype::SongQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "songQueue"
|
||||||
|
SET status = $1
|
||||||
|
WHERE id = (
|
||||||
|
SELECT id FROM "songQueue"
|
||||||
|
WHERE status = $2
|
||||||
|
ORDER BY id
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
RETURNING id, filename, status, user_id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(status::PROCESSING)
|
||||||
|
.bind(status::READY)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let user_id_result = row.try_get("user_id");
|
||||||
|
let song_queue = dbtype::SongQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
status: row
|
||||||
|
.try_get("status")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
user_id: match user_id_result {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => uuid::Uuid::nil(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(song_queue)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_status_of_song_queue(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<String, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, status FROM "songQueue" WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error selecting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("status")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_song_queue_status(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
status: &String,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<String, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "songQueue" SET status = $1 WHERE id = $2 RETURNING status;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(status)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error updating record {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("status")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn link_user_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
user_id: &uuid::Uuid,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "songQueue" SET user_id = $1 WHERE id = $2 RETURNING user_id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error updating record {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("user_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_song_queue(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<dbtype::SongQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, filename, status, user_id FROM "songQueue" WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let user_id_result = row.try_get("user_id");
|
||||||
|
let song_queue = dbtype::SongQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
status: row
|
||||||
|
.try_get("status")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
user_id: match user_id_result {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => uuid::Uuid::nil(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(song_queue)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wipe_data(pool: &sqlx::PgPool, id: &uuid::Uuid) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "songQueue" SET data = NULL WHERE id = $1 RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error updating record: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_data(pool: &sqlx::PgPool, id: &uuid::Uuid) -> Result<Vec<u8>, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT data FROM "songQueue"
|
||||||
|
WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let data = row
|
||||||
|
.try_get("data")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
pub async fn insert(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song: &icarus_models::song::Song,
|
||||||
|
) -> Result<(time::OffsetDateTime, uuid::Uuid), sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "song" (title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, filename, directory, user_id)
|
||||||
|
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING date_created, id;
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(&song.title)
|
||||||
|
.bind(&song.artist)
|
||||||
|
.bind(&song.album_artist)
|
||||||
|
.bind(&song.album)
|
||||||
|
.bind(&song.genre)
|
||||||
|
.bind(song.year)
|
||||||
|
.bind(song.track)
|
||||||
|
.bind(song.disc)
|
||||||
|
.bind(song.track_count)
|
||||||
|
.bind(song.disc_count)
|
||||||
|
.bind(song.duration)
|
||||||
|
.bind(&song.audio_type)
|
||||||
|
.bind(&song.filename)
|
||||||
|
.bind(&song.directory)
|
||||||
|
.bind(song.user_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting query: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
let date_created_time: time::OffsetDateTime = row
|
||||||
|
.try_get("date_created")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
let date_created = date_created_time;
|
||||||
|
|
||||||
|
Ok((date_created, id))
|
||||||
|
}
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_song(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<icarus_models::song::Song, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT * FROM "song" WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let date_created_time: time::OffsetDateTime = row
|
||||||
|
.try_get("date_created")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(icarus_models::song::Song {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
title: row
|
||||||
|
.try_get("title")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
artist: row
|
||||||
|
.try_get("artist")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
album_artist: row
|
||||||
|
.try_get("album_artist")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
album: row
|
||||||
|
.try_get("album")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
genre: row
|
||||||
|
.try_get("genre")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
year: row
|
||||||
|
.try_get("year")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
track: row
|
||||||
|
.try_get("track")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
disc: row
|
||||||
|
.try_get("disc")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
track_count: row
|
||||||
|
.try_get("track_count")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
disc_count: row
|
||||||
|
.try_get("disc_count")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
duration: row
|
||||||
|
.try_get("duration")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
audio_type: row
|
||||||
|
.try_get("audio_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
directory: row
|
||||||
|
.try_get("directory")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
date_created: Some(date_created_time),
|
||||||
|
user_id: row
|
||||||
|
.try_get("user_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
data: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_songs(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
) -> Result<Vec<icarus_models::song::Song>, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT * FROM "song";
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(rows) => {
|
||||||
|
let mut songs: Vec<icarus_models::song::Song> = Vec::new();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let date_created_time: time::OffsetDateTime = row
|
||||||
|
.try_get("date_created")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let song = icarus_models::song::Song {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
title: row
|
||||||
|
.try_get("title")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
artist: row
|
||||||
|
.try_get("artist")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
album_artist: row
|
||||||
|
.try_get("album_artist")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
album: row
|
||||||
|
.try_get("album")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
genre: row
|
||||||
|
.try_get("genre")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
year: row
|
||||||
|
.try_get("year")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
track: row
|
||||||
|
.try_get("track")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
disc: row
|
||||||
|
.try_get("disc")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
track_count: row
|
||||||
|
.try_get("track_count")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
disc_count: row
|
||||||
|
.try_get("disc_count")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
duration: row
|
||||||
|
.try_get("duration")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
audio_type: row
|
||||||
|
.try_get("audio_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
directory: row
|
||||||
|
.try_get("directory")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
date_created: Some(date_created_time),
|
||||||
|
user_id: row
|
||||||
|
.try_get("user_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
data: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
songs.push(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(songs)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_song(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<icarus_models::song::Song, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
// icarus_models::song::Song,
|
||||||
|
r#"
|
||||||
|
DELETE FROM "song"
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, title, artist, album, album_artist, genre, year, disc, track, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error deleting data: {e:?}")
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let date_created_time: time::OffsetDateTime = row
|
||||||
|
.try_get("date_created")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(icarus_models::song::Song {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
title: row
|
||||||
|
.try_get("title")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
artist: row
|
||||||
|
.try_get("artist")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
album_artist: row
|
||||||
|
.try_get("album_artist")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
album: row
|
||||||
|
.try_get("album")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
genre: row
|
||||||
|
.try_get("genre")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
year: row
|
||||||
|
.try_get("year")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
track: row
|
||||||
|
.try_get("track")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
disc: row
|
||||||
|
.try_get("disc")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
track_count: row
|
||||||
|
.try_get("track_count")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
disc_count: row
|
||||||
|
.try_get("disc_count")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
duration: row
|
||||||
|
.try_get("duration")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
audio_type: row
|
||||||
|
.try_get("audio_type")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
filename: row
|
||||||
|
.try_get("filename")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
directory: row
|
||||||
|
.try_get("directory")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
date_created: Some(date_created_time),
|
||||||
|
user_id: row
|
||||||
|
.try_get("user_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
data: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
-- 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,
|
||||||
|
file_type TEXT NOT 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,
|
||||||
|
directory TEXT NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
file_type 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" (id, title, directory, filename, file_type, song_id) VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I', 'Coverart-1.jpg', 'jpeg', '44cf7940-34ff-489f-9124-d0ec90a55af9');
|
||||||
|
|
||||||
|
INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('94cf7940-34ff-489f-9124-d0ec90a55af4', 'It''s Too Late', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 2, 1, 9, 1, 116, 'flac', '2020-01-01 13:01:00-05', 'track02.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
|
||||||
|
INSERT INTO "coverart" (id, title, directory, filename, file_type, song_id) VALUES('d96122cd-5ae9-4013-9934-60768d3006e9', 'I', 'tests/I', 'Coverart-2.jpg', 'jpeg', '94cf7940-34ff-489f-9124-d0ec90a55af4');
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 MiB |
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"album": "I",
|
||||||
|
"album_artist": "Kuoth",
|
||||||
|
"disc_count": 1,
|
||||||
|
"genre": "Alternative Hip-Hop",
|
||||||
|
"year": 2020,
|
||||||
|
"track_count": 9,
|
||||||
|
"tracks": [
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Hypocrite Like the Rest",
|
||||||
|
"duration": 139,
|
||||||
|
"track": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "It's Too Late",
|
||||||
|
"duration": 116,
|
||||||
|
"track": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Hell Might be Nice",
|
||||||
|
"duration": 229,
|
||||||
|
"track": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Dark Side of Progress",
|
||||||
|
"duration": 187,
|
||||||
|
"track": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Distractions",
|
||||||
|
"duration": 141,
|
||||||
|
"track": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "But You Won't Listen",
|
||||||
|
"duration": 184,
|
||||||
|
"track": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Guidance",
|
||||||
|
"duration": 121,
|
||||||
|
"track": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "M",
|
||||||
|
"duration": 98,
|
||||||
|
"track": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Social Acceptance",
|
||||||
|
"duration": 169,
|
||||||
|
"track": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 271 KiB |
Reference in New Issue
Block a user