Get songs endpoint (#162)

* Added test_migrations directory

* Added skeleton endpoint to get songs

* Code formatting

* Added more code

* Added TODO

* Code formatting

* Updated album in album json file

* Changing directory name

* Test refactoring

* Forgot to include this

* Added test for getting songs and added function for test_migrations

* Adding test migrations

* Removing placeholder

* Renamed

* Renamed

* Fixed what caused test failure

* Created migration with cli command

Creating test_migrations/20250725213448_migration_name.sql

* Migration changes

* Removing migration

* More migration changes

* Made endpoint available

* Migration changes

* Got the test to work

* Cleaned up test

* Code formatting

* More cleanup

* Version bump
This commit was merged in pull request #162.
This commit is contained in:
KD
2025-07-25 18:20:28 -04:00
committed by GitHub
parent a987e438c4
commit dbda9a3897
19 changed files with 201 additions and 5 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"dev": {
"migrations": "./migrations"
},
"test": {
"migrations": "./test_migrations"
}
}
Generated
+1 -1
View File
@@ -752,7 +752,7 @@ dependencies = [
[[package]]
name = "icarus"
version = "0.1.93"
version = "0.1.94"
dependencies = [
"axum",
"common-multipart-rfc7578",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "icarus"
version = "0.1.93"
version = "0.1.94"
edition = "2024"
rust-version = "1.88"
+1
View File
@@ -16,6 +16,7 @@ pub mod endpoints {
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
pub const CREATESONG: &str = "/api/v2/song";
pub const GETSONGS: &str = "/api/v2/song";
pub const CREATECOVERART: &str = "/api/v2/coverart";
}
+43
View File
@@ -92,6 +92,13 @@ pub mod request {
pub user_id: uuid::Uuid,
}
}
pub mod get_songs {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Params {
pub id: Option<uuid::Uuid>,
}
}
}
pub mod response {
@@ -158,6 +165,14 @@ pub mod response {
pub data: Vec<uuid::Uuid>,
}
}
pub mod get_songs {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<icarus_models::song::Song>,
}
}
}
// TODO: Might make a distinction between year and date in a song's tag at some point
@@ -968,4 +983,32 @@ pub mod endpoint {
}
}
}
pub async fn get_songs(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::get_songs::Params>,
) -> (
axum::http::StatusCode,
Json<super::response::get_songs::Response>,
) {
let mut response = super::response::get_songs::Response::default();
match params.id {
Some(id) => match super::song_db::get_song(&pool, &id).await {
Ok(song) => {
response.message = String::from(super::super::response::SUCCESSFUL);
response.data.push(song);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
None => {
response.message = String::from("Invalid parameters");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
}
+81 -2
View File
@@ -115,12 +115,17 @@ pub mod init {
crate::callers::endpoints::CREATECOVERART,
post(crate::callers::coverart::endpoint::create_coverart),
)
.route(
crate::callers::endpoints::GETSONGS,
get(crate::callers::song::endpoint::get_songs),
)
}
pub async fn app() -> axum::Router {
let pool = crate::db::create_pool()
.await
.expect("Failed to create pool");
// TODO: Look into handling this. Seems redundant to run migrations multiple times
crate::db::migrations(&pool).await;
routes()
@@ -227,6 +232,15 @@ mod tests {
Err("Error parsing".into())
}
}
pub async fn migrations(pool: &sqlx::PgPool) {
// Run migrations using the sqlx::migrate! macro
// Assumes your test migrations are in a ./test_migrations folder relative to Cargo.toml
sqlx::migrate!("./test_migrations")
.run(pool)
.await
.expect("Failed to run migrations");
}
}
mod init {
@@ -247,7 +261,7 @@ mod tests {
) -> Result<axum::response::Response, std::convert::Infallible> {
// Create multipart form
let mut form = MultipartForm::default();
let _ = form.add_file("flac", "tests/IAmWe/track01.flac");
let _ = form.add_file("flac", "tests/I/track01.flac");
// Create request
let content_type = form.content_type();
@@ -331,7 +345,7 @@ mod tests {
app: &axum::Router,
) -> Result<axum::response::Response, std::convert::Infallible> {
let mut form = MultipartForm::default();
let _ = form.add_file("jpg", "tests/IAmWe/Coverart.jpg");
let _ = form.add_file("jpg", "tests/I/Coverart.jpg");
// Create request
let content_type = form.content_type();
@@ -1799,4 +1813,69 @@ mod tests {
let _ = db_mgr::drop_database(&tm_pool, &db_name).await;
}
pub mod after_song_queue {
use tower::ServiceExt;
#[tokio::test]
async fn test_get_songs() {
let tm_pool = super::db_mgr::get_pool().await.unwrap();
let db_name = super::db_mgr::generate_db_name().await;
match super::db_mgr::create_database(&tm_pool, &db_name).await {
Ok(_) => {
println!("Success");
}
Err(err) => {
assert!(false, "Error: {:?}", err);
}
}
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
super::db_mgr::migrations(&pool).await;
let app = super::init::app(pool).await;
let mut id = uuid::Uuid::nil();
match uuid::Uuid::parse_str("44cf7940-34ff-489f-9124-d0ec90a55af9") {
Ok(val) => {
id = val;
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
};
let uri = format!("{}?id={id}", crate::callers::endpoints::GETSONGS);
match app
.clone()
.oneshot(
axum::http::Request::builder()
.method(axum::http::Method::GET)
.uri(uri)
.header(axum::http::header::CONTENT_TYPE, "application/json")
.body(axum::body::Body::empty())
.unwrap(),
)
.await
{
Ok(response) => {
let resp = super::get_resp_data::<
crate::callers::song::response::get_songs::Response,
>(response)
.await;
assert_eq!(false, resp.data.is_empty(), "Should not be empty");
let song = resp.data[0].clone();
assert_eq!(id, song.id, "Id does not match {song:?}");
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
}
}
}
+62
View File
@@ -0,0 +1,62 @@
-- Add migration script here
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Table to store queued songs to process
CREATE TABLE IF NOT EXISTS "songQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
filename TEXT NOT NULL,
status TEXT CHECK (status IN ('pending', 'ready', 'processing', 'done')),
data BYTEA NULL,
user_id UUID NULL
);
-- Table to store queued metadata
CREATE TABLE IF NOT EXISTS "metadataQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
metadata jsonb NOT NULL,
created_at timestamptz DEFAULT now(),
song_queue_id UUID NOT NULL
);
-- Table to store queued coverart
CREATE TABLE IF NOT EXISTS "coverartQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
data BYTEA NULL,
song_queue_id UUID NULL
);
-- Create an index for better query performance
CREATE INDEX metadata_queue_data_metadata ON "metadataQueue" USING gin (metadata);
-- Table to store a song's info
CREATE TABLE IF NOT EXISTS "song" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
artist TEXT NOT NULL,
album_artist TEXT NOT NULL,
album TEXT NOT NULL,
genre TEXT NOT NULL,
year INT NOT NULL,
track INT NOT NULL,
disc INT NOT NULL,
track_count INT NOT NULL,
disc_count INT NOT NULL,
duration INT NOT NULL,
audio_type TEXT NOT NULL,
date_created timestamptz DEFAULT now(),
filename TEXT NOT NULL,
directory TEXT NOT NULL,
user_id UUID NULL
);
CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
path TEXT NOT NULL,
song_id UUID NOT NULL
);
-- Might not to disable constraints on fields
-- 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');
-- Re-enable the constraints on the fields
@@ -0,0 +1,3 @@
-- Add migration script here
INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('44cf7940-34ff-489f-9124-d0ec90a55af9', 'Hypocrite Like The Rest', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 1, 1, 9, 1, 139, 'flac', '2020-01-01 13:00:00-05', 'track01.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
INSERT INTO "coverart" VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I/Coverart.jpg', '44cf7940-34ff-489f-9124-d0ec90a55af9');

Before

Width:  |  Height:  |  Size: 6.7 MiB

After

Width:  |  Height:  |  Size: 6.7 MiB

@@ -1,5 +1,5 @@
{
"album": "I Am We",
"album": "I",
"album_artist": "Kuoth",
"disc_count": 1,
"genre": "Alternative Hip-Hop",
Binary file not shown.