feat: everything works again. now to break it again
This commit is contained in:
parent
1dfd27f6ae
commit
7a659ced4d
23 changed files with 910 additions and 267 deletions
85
Cargo.lock
generated
85
Cargo.lock
generated
|
|
@ -338,6 +338,26 @@ version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "audiotags"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f47bdf1acc4fa4113f8aa72bb3bfe62a61443c61d0d997b723ba61ce102c79b"
|
||||||
|
dependencies = [
|
||||||
|
"audiotags-dev-macro",
|
||||||
|
"id3",
|
||||||
|
"metaflac",
|
||||||
|
"mp4ameta",
|
||||||
|
"readme-rustdocifier",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "audiotags-dev-macro"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b79298591161f312f06327df7963063ee07466be303dcc3084a44ec293cb36e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -966,6 +986,7 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
|
@ -1371,7 +1392,6 @@ dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"flate2",
|
"flate2",
|
||||||
"tokio",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1595,6 +1615,17 @@ version = "2.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metaflac"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1470d3cc1bb0d692af5eb3afb594330b8ba09fd91c32c4e1c6322172a5ba750"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"hex",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "migration"
|
name = "migration"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -1645,6 +1676,22 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mp4ameta"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb23d62e8eb5299a3f79657c70ea9269eac8f6239a76952689bcd06a74057e81"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"mp4ameta_proc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mp4ameta_proc"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multer"
|
name = "multer"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|
@ -1839,9 +1886,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-float"
|
name = "ordered-float"
|
||||||
version = "3.9.1"
|
version = "3.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06"
|
checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
@ -2268,11 +2315,11 @@ dependencies = [
|
||||||
name = "rave"
|
name = "rave"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"audiotags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"entities",
|
"entities",
|
||||||
"futures-lite",
|
"futures",
|
||||||
"id3",
|
|
||||||
"md5",
|
"md5",
|
||||||
"migration",
|
"migration",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -2293,6 +2340,12 @@ dependencies = [
|
||||||
"url-escape",
|
"url-escape",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "readme-rustdocifier"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
|
|
@ -2304,14 +2357,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.9.6"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff"
|
checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.3.9",
|
"regex-automata 0.4.1",
|
||||||
"regex-syntax 0.7.5",
|
"regex-syntax 0.8.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2325,13 +2378,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.3.9"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
|
checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.7.5",
|
"regex-syntax 0.8.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2342,9 +2395,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.7.5"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
|
|
@ -2708,9 +2761,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.19"
|
version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
|
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry"
|
name = "sentry"
|
||||||
|
|
|
||||||
12
Justfile
12
Justfile
|
|
@ -15,3 +15,15 @@ run: mount
|
||||||
refresh:
|
refresh:
|
||||||
sea migrate fresh
|
sea migrate fresh
|
||||||
sea generate entity -o ./entities/src --with-serde both --date-time-crate time --lib
|
sea generate entity -o ./entities/src --with-serde both --date-time-crate time --lib
|
||||||
|
|
||||||
|
scan:
|
||||||
|
curl -vv "http://localhost:1234/rest/startScan?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0" | xq
|
||||||
|
|
||||||
|
scanStatus:
|
||||||
|
curl -vv "http://localhost:1234/rest/getScanStatus?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0" | xq
|
||||||
|
|
||||||
|
getAlbumList2:
|
||||||
|
curl -vv "http://localhost:1234/rest/getAlbumList2?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0&type=newest" | xq
|
||||||
|
|
||||||
|
getAlbum ALBUM:
|
||||||
|
curl -vv "http://localhost:1234/rest/getAlbum?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0&id={{ ALBUM }}" | xq
|
||||||
|
|
@ -11,7 +11,7 @@ pub struct Model {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub artist_id: Option<i64>,
|
pub artist_id: Option<i64>,
|
||||||
pub cover_art_id: Option<i64>,
|
pub cover_art_id: Option<i64>,
|
||||||
pub song_count: i32,
|
pub song_count: i64,
|
||||||
pub duration: i64,
|
pub duration: i64,
|
||||||
pub play_count: i64,
|
pub play_count: i64,
|
||||||
pub created: TimeDateTimeWithTimeZone,
|
pub created: TimeDateTimeWithTimeZone,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ pub struct Model {
|
||||||
pub cover_art_id: Option<i64>,
|
pub cover_art_id: Option<i64>,
|
||||||
pub artist_image_url: Option<String>,
|
pub artist_image_url: Option<String>,
|
||||||
pub album_count: i32,
|
pub album_count: i32,
|
||||||
pub starred: bool,
|
pub starred: Option<TimeDateTimeWithTimeZone>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,13 @@ pub struct Model {
|
||||||
pub is_dir: bool,
|
pub is_dir: bool,
|
||||||
pub cover_art_id: Option<i64>,
|
pub cover_art_id: Option<i64>,
|
||||||
pub created: TimeDateTimeWithTimeZone,
|
pub created: TimeDateTimeWithTimeZone,
|
||||||
|
pub starred: Option<TimeDateTimeWithTimeZone>,
|
||||||
pub duration: i64,
|
pub duration: i64,
|
||||||
pub bit_rate: Option<i64>,
|
pub bit_rate: Option<i64>,
|
||||||
|
pub track_number: Option<i32>,
|
||||||
pub size: i64,
|
pub size: i64,
|
||||||
|
pub play_count: i64,
|
||||||
|
pub disc_number: i32,
|
||||||
pub suffix: String,
|
pub suffix: String,
|
||||||
pub content_type: String,
|
pub content_type: String,
|
||||||
pub is_video: bool,
|
pub is_video: bool,
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,8 @@ impl MigrationTrait for Migration {
|
||||||
)
|
)
|
||||||
.col(
|
.col(
|
||||||
ColumnDef::new(Artist::Starred)
|
ColumnDef::new(Artist::Starred)
|
||||||
.boolean()
|
.timestamp_with_time_zone()
|
||||||
.not_null()
|
.null(),
|
||||||
.default(false),
|
|
||||||
)
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ impl MigrationTrait for Migration {
|
||||||
.col(ColumnDef::new(Album::Name).string().not_null())
|
.col(ColumnDef::new(Album::Name).string().not_null())
|
||||||
.col(ColumnDef::new(Album::ArtistId).big_integer().null())
|
.col(ColumnDef::new(Album::ArtistId).big_integer().null())
|
||||||
.col(ColumnDef::new(Album::CoverArtId).big_integer().null())
|
.col(ColumnDef::new(Album::CoverArtId).big_integer().null())
|
||||||
.col(ColumnDef::new(Album::SongCount).integer().not_null())
|
.col(ColumnDef::new(Album::SongCount).big_integer().not_null())
|
||||||
.col(ColumnDef::new(Album::Duration).big_integer().not_null())
|
.col(ColumnDef::new(Album::Duration).big_integer().not_null())
|
||||||
.col(
|
.col(
|
||||||
ColumnDef::new(Album::PlayCount)
|
ColumnDef::new(Album::PlayCount)
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,27 @@ impl MigrationTrait for Migration {
|
||||||
.timestamp_with_time_zone()
|
.timestamp_with_time_zone()
|
||||||
.not_null(),
|
.not_null(),
|
||||||
)
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Track::Starred)
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
.col(ColumnDef::new(Track::Duration).big_integer().not_null())
|
.col(ColumnDef::new(Track::Duration).big_integer().not_null())
|
||||||
.col(ColumnDef::new(Track::BitRate).big_integer().null())
|
.col(ColumnDef::new(Track::BitRate).big_integer().null())
|
||||||
|
.col(ColumnDef::new(Track::TrackNumber).integer().null())
|
||||||
.col(ColumnDef::new(Track::Size).big_integer().not_null())
|
.col(ColumnDef::new(Track::Size).big_integer().not_null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Track::PlayCount)
|
||||||
|
.big_integer()
|
||||||
|
.not_null()
|
||||||
|
.default(0),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Track::DiscNumber)
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.default(1),
|
||||||
|
)
|
||||||
.col(ColumnDef::new(Track::Suffix).string().not_null())
|
.col(ColumnDef::new(Track::Suffix).string().not_null())
|
||||||
.col(ColumnDef::new(Track::ContentType).string().not_null())
|
.col(ColumnDef::new(Track::ContentType).string().not_null())
|
||||||
.col(
|
.col(
|
||||||
|
|
@ -111,8 +129,12 @@ pub enum Track {
|
||||||
AlbumId,
|
AlbumId,
|
||||||
ArtistId,
|
ArtistId,
|
||||||
IsDir,
|
IsDir,
|
||||||
|
TrackNumber,
|
||||||
CoverArtId,
|
CoverArtId,
|
||||||
Created,
|
Created,
|
||||||
|
Starred,
|
||||||
|
PlayCount,
|
||||||
|
DiscNumber,
|
||||||
Duration,
|
Duration,
|
||||||
BitRate,
|
BitRate,
|
||||||
Size,
|
Size,
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ sea-orm = { workspace = true }
|
||||||
entities = { workspace = true }
|
entities = { workspace = true }
|
||||||
migration = { workspace = true }
|
migration = { workspace = true }
|
||||||
once_cell = { version = "1.18.0", features = ["parking_lot"] }
|
once_cell = { version = "1.18.0", features = ["parking_lot"] }
|
||||||
futures-lite = "1.13.0"
|
futures = "0.3"
|
||||||
id3 = { version = "1.8.0", features = ["tokio"] }
|
audiotags = "0.4.1"
|
||||||
tracing-appender = "0.2.2"
|
tracing-appender = "0.2.2"
|
||||||
sentry = { version = "0.31.7", default-features = false, features = [
|
sentry = { version = "0.31.7", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
#![warn(clippy::pedantic, clippy::nursery)]
|
#![warn(clippy::pedantic, clippy::nursery)]
|
||||||
#![deny(clippy::unwrap_used)]
|
#![deny(clippy::unwrap_used)]
|
||||||
#![allow(clippy::module_name_repetitions, clippy::too_many_lines)]
|
#![allow(
|
||||||
|
clippy::module_name_repetitions,
|
||||||
|
clippy::too_many_lines,
|
||||||
|
clippy::cast_possible_truncation
|
||||||
|
)]
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::Authentication,
|
authentication::Authentication,
|
||||||
subsonic::{Error, SubsonicResponse},
|
subsonic::{Album as AlbumId3, Child, Error, SubsonicResponse},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
use entities::prelude::{Album, Track};
|
use entities::prelude::{Album, Artist, Genre, Track};
|
||||||
use poem::web::{Data, Query};
|
use poem::web::{Data, Query};
|
||||||
use poem_ext::db::DbTxn;
|
use poem_ext::db::DbTxn;
|
||||||
use sea_orm::{EntityTrait, ModelTrait};
|
use sea_orm::{EntityTrait, ModelTrait};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{error, instrument, warn};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
|
|
@ -25,10 +25,27 @@ pub async fn get_album(
|
||||||
Err(e) => return e,
|
Err(e) => return e,
|
||||||
}
|
}
|
||||||
|
|
||||||
let album = Album::find_by_id(params.id).one(&**txn).await;
|
let album_id = match params.id.strip_prefix("al-").ok_or_else(|| {
|
||||||
|
Error::RequiredParameterMissing(Some("Album IDs must be formatted as `al-{}`".to_string()))
|
||||||
|
}) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => return SubsonicResponse::new_error(e),
|
||||||
|
};
|
||||||
|
let album_id = match album_id.parse::<i64>() {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error parsing album ID: {e}"
|
||||||
|
);
|
||||||
|
return SubsonicResponse::new_error(Error::Generic(None));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let album = Album::find_by_id(album_id).one(&**txn).await;
|
||||||
let Ok(Some(album)) = album else {
|
let Ok(Some(album)) = album else {
|
||||||
match album {
|
match album {
|
||||||
Ok(Some(_)) => unreachable!("Some(album) covered by `let .. else`"),
|
Ok(Some(_)) => unreachable!("Ok(Some(_)) covered by `let .. else`"),
|
||||||
Ok(None) => return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)),
|
Ok(None) => return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
|
|
@ -40,6 +57,43 @@ pub async fn get_album(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let artist = album.find_related(Artist).one(&**txn).await;
|
||||||
|
let Ok(artist) = artist else {
|
||||||
|
match artist {
|
||||||
|
Ok(_) => unreachable!("Ok(_) covered by `let .. else`"),
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error getting artist: {e}"
|
||||||
|
);
|
||||||
|
return SubsonicResponse::new_error(Error::Generic(None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let genre = if let Some(genre_ids) = &album.genre_ids {
|
||||||
|
if genre_ids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let genre = Genre::find_by_id(genre_ids[0]).one(&**txn).await;
|
||||||
|
|
||||||
|
match genre {
|
||||||
|
Ok(Some(genre)) => Some(genre),
|
||||||
|
Ok(None) => None,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error getting genre: {e}"
|
||||||
|
);
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let tracks = album.find_related(Track).all(&**txn).await;
|
let tracks = album.find_related(Track).all(&**txn).await;
|
||||||
|
|
||||||
let tracks = match tracks {
|
let tracks = match tracks {
|
||||||
|
|
@ -53,11 +107,17 @@ pub async fn get_album(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
todo!("finish implementing get_album")
|
let album = AlbumId3::new(album, artist, genre);
|
||||||
// SubsonicResponse::new_album(album, tracks)
|
let mut tracks = tracks
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| Child::new(&t, &album))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
tracks.sort_by_cached_key(|tr| tr.track.unwrap_or_default());
|
||||||
|
|
||||||
|
SubsonicResponse::new_album(album, tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct GetAlbumParams {
|
pub struct GetAlbumParams {
|
||||||
pub id: i32,
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ use poem::{
|
||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
use poem_ext::db::DbTxn;
|
use poem_ext::db::DbTxn;
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
|
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::instrument;
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::Authentication,
|
authentication::Authentication,
|
||||||
random_types::SortType,
|
random_types::SortType,
|
||||||
subsonic::{Error, SubsonicResponse},
|
subsonic::{Album as AlbumId3, Error, SubsonicResponse},
|
||||||
utils::{self},
|
utils::{self},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -51,30 +51,96 @@ pub async fn get_album_list(
|
||||||
};
|
};
|
||||||
|
|
||||||
let album_list = match params.r#type {
|
let album_list = match params.r#type {
|
||||||
SortType::Random => get_album_list_random(txn, params).await,
|
SortType::Random => get_album_list_random(txn.clone(), params).await,
|
||||||
SortType::Newest => get_album_list_newest(txn, params).await,
|
SortType::Newest => get_album_list_newest(txn.clone(), params).await,
|
||||||
SortType::Highest => get_album_list_highest(txn, params).await,
|
SortType::Highest => get_album_list_highest(txn.clone(), params).await,
|
||||||
SortType::Frequent => get_album_list_frequent(txn, params).await,
|
SortType::Frequent => get_album_list_frequent(txn.clone(), params).await,
|
||||||
SortType::Recent => get_album_list_recent(txn, params).await,
|
SortType::Recent => get_album_list_recent(txn.clone(), params).await,
|
||||||
SortType::AlphabeticalByName => get_album_list_alphabetical_by_name(txn, params).await,
|
SortType::AlphabeticalByName => {
|
||||||
SortType::AlphabeticalByArtist => get_album_list_alphabetical_by_artist(txn, params).await,
|
get_album_list_alphabetical_by_name(txn.clone(), params).await
|
||||||
SortType::Starred => get_album_list_starred(txn, params).await,
|
}
|
||||||
SortType::ByYear => get_album_list_by_year(txn, params).await,
|
SortType::AlphabeticalByArtist => {
|
||||||
SortType::ByGenre => get_album_list_by_genre(txn, params).await,
|
get_album_list_alphabetical_by_artist(txn.clone(), params).await
|
||||||
|
}
|
||||||
|
SortType::Starred => get_album_list_starred(txn.clone(), params).await,
|
||||||
|
SortType::ByYear => get_album_list_by_year(txn.clone(), params).await,
|
||||||
|
SortType::ByGenre => get_album_list_by_genre(txn.clone(), params).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
todo!()
|
let album_list = match album_list {
|
||||||
|
Ok(a) => albums_to_album_id3(txn, &a).await,
|
||||||
|
Err(e) => return SubsonicResponse::new_error(e),
|
||||||
|
};
|
||||||
|
|
||||||
// match album_list {
|
match album_list {
|
||||||
// Ok(a) => {
|
Ok(a) => {
|
||||||
// if req.uri().path().contains("getAlbumList2") {
|
if req.uri().path().contains("getAlbumList2") {
|
||||||
// SubsonicResponse::new_album_list2(a)
|
SubsonicResponse::new_album_list2(a)
|
||||||
// } else {
|
} else {
|
||||||
// SubsonicResponse::new_album_list(a)
|
SubsonicResponse::new_album_list(a)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// Err(e) => SubsonicResponse::new_error(e),
|
Err(e) => SubsonicResponse::new_error(e),
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn albums_to_album_id3(conn: DbTxn, albums: &[album::Model]) -> Result<Vec<AlbumId3>, Error> {
|
||||||
|
let mut album_id3_futures = Vec::with_capacity(albums.len());
|
||||||
|
|
||||||
|
for album in albums {
|
||||||
|
album_id3_futures.push(album_to_album_id3(conn.clone(), album));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = futures::future::join_all(album_id3_futures).await;
|
||||||
|
|
||||||
|
let mut album_id3s = Vec::with_capacity(result.len());
|
||||||
|
|
||||||
|
for album in result {
|
||||||
|
album_id3s.push(album?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(album_id3s)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn album_to_album_id3(conn: DbTxn, album: &album::Model) -> Result<AlbumId3, Error> {
|
||||||
|
let artist = album.find_related(Artist).one(&*conn).await;
|
||||||
|
let Ok(artist) = artist else {
|
||||||
|
match artist {
|
||||||
|
Ok(_) => unreachable!("Ok(_) covered by `let .. else`"),
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error getting artist: {e}"
|
||||||
|
);
|
||||||
|
return Err(Error::Generic(None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let genre = if let Some(genre_ids) = &album.genre_ids {
|
||||||
|
if genre_ids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let genre = Genre::find_by_id(genre_ids[0]).one(&*conn).await;
|
||||||
|
|
||||||
|
match genre {
|
||||||
|
Ok(Some(genre)) => Some(genre),
|
||||||
|
Ok(None) => None,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error getting genre: {e}"
|
||||||
|
);
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AlbumId3::new(album.clone(), artist, genre))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(_conn, _params))]
|
#[instrument(skip(_conn, _params))]
|
||||||
|
|
@ -138,6 +204,7 @@ async fn get_album_list_recent(
|
||||||
params: GetAlbumListParams,
|
params: GetAlbumListParams,
|
||||||
) -> Result<Vec<album::Model>, Error> {
|
) -> Result<Vec<album::Model>, Error> {
|
||||||
let albums = Album::find()
|
let albums = Album::find()
|
||||||
|
.filter(album::Column::Played.is_not_null())
|
||||||
.order_by_desc(album::Column::Played)
|
.order_by_desc(album::Column::Played)
|
||||||
.limit(params.size)
|
.limit(params.size)
|
||||||
.offset(params.offset)
|
.offset(params.offset)
|
||||||
|
|
@ -153,7 +220,7 @@ async fn get_album_list_alphabetical_by_name(
|
||||||
params: GetAlbumListParams,
|
params: GetAlbumListParams,
|
||||||
) -> Result<Vec<album::Model>, Error> {
|
) -> Result<Vec<album::Model>, Error> {
|
||||||
let albums = Album::find()
|
let albums = Album::find()
|
||||||
.order_by_desc(album::Column::Name)
|
.order_by_asc(album::Column::Name)
|
||||||
.limit(params.size)
|
.limit(params.size)
|
||||||
.offset(params.offset)
|
.offset(params.offset)
|
||||||
.all(&*conn)
|
.all(&*conn)
|
||||||
|
|
@ -170,7 +237,7 @@ async fn get_album_list_alphabetical_by_artist(
|
||||||
let albums = Album::find()
|
let albums = Album::find()
|
||||||
.filter(album::Column::ArtistId.is_not_null())
|
.filter(album::Column::ArtistId.is_not_null())
|
||||||
.find_also_related(Artist)
|
.find_also_related(Artist)
|
||||||
.order_by_desc(artist::Column::Name)
|
.order_by_asc(artist::Column::Name)
|
||||||
.limit(params.size)
|
.limit(params.size)
|
||||||
.offset(params.offset)
|
.offset(params.offset)
|
||||||
.all(&*conn)
|
.all(&*conn)
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
|
use entities::prelude::Track;
|
||||||
use poem::{
|
use poem::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
web::{Data, Query},
|
web::{Data, Query},
|
||||||
IntoResponse, Response,
|
IntoResponse, Response,
|
||||||
};
|
};
|
||||||
use poem_ext::db::DbTxn;
|
use poem_ext::db::DbTxn;
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::instrument;
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::Authentication,
|
authentication::Authentication,
|
||||||
|
subsonic::{Error, SubsonicResponse},
|
||||||
utils::{self},
|
utils::{self},
|
||||||
};
|
};
|
||||||
|
|
||||||
const SONG: &[u8] = include_bytes!("../../../../data.mp3");
|
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
pub async fn stream(
|
pub async fn stream(
|
||||||
Data(txn): Data<&DbTxn>,
|
Data(txn): Data<&DbTxn>,
|
||||||
auth: Authentication,
|
auth: Authentication,
|
||||||
Query(_params): Query<StreamParams>,
|
Query(params): Query<StreamParams>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), auth).await;
|
||||||
|
|
||||||
|
|
@ -27,16 +28,64 @@ pub async fn stream(
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e.into_response(),
|
Err(e) => return e.into_response(),
|
||||||
}
|
}
|
||||||
|
let Some(id) = params.id.strip_prefix("tr-") else {
|
||||||
|
return SubsonicResponse::new_error(Error::RequiredParameterMissing(Some(
|
||||||
|
"Track IDs must be formatted as `tr-{}`".to_string(),
|
||||||
|
)))
|
||||||
|
.into_response();
|
||||||
|
};
|
||||||
|
let id = match id.parse::<i64>() {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error parsing track ID: {e}"
|
||||||
|
);
|
||||||
|
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let track = Track::find_by_id(id).one(&**txn).await;
|
||||||
|
|
||||||
|
let track = match track {
|
||||||
|
Ok(Some(track)) => track,
|
||||||
|
Ok(None) => {
|
||||||
|
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error fetching track: {e}"
|
||||||
|
);
|
||||||
|
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = track.path;
|
||||||
|
|
||||||
|
let song = tokio::fs::read(path).await;
|
||||||
|
|
||||||
|
let song = match song {
|
||||||
|
Ok(song) => song,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Error reading track: {e}"
|
||||||
|
);
|
||||||
|
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
poem::Response::builder()
|
poem::Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header("Content-Type", "audio/mpeg")
|
.header("Content-Type", "audio/mpeg")
|
||||||
.body(SONG)
|
.body(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
pub struct StreamParams {
|
pub struct StreamParams {
|
||||||
pub id: i32,
|
pub id: String,
|
||||||
#[serde(rename = "maxBitRate", default)]
|
#[serde(rename = "maxBitRate", default)]
|
||||||
pub max_bit_rate: Option<i32>,
|
pub max_bit_rate: Option<i32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
||||||
164
rave/src/scan.rs
164
rave/src/scan.rs
|
|
@ -1,24 +1,20 @@
|
||||||
use color_eyre::{Report, Result};
|
use color_eyre::{Report, Result};
|
||||||
use entities::{
|
use entities::{
|
||||||
album, artist, genre, music_folder,
|
genre, music_folder,
|
||||||
prelude::{Album, Artist, Genre, MusicFolder, Track},
|
prelude::{Genre, MusicFolder},
|
||||||
track,
|
|
||||||
};
|
};
|
||||||
use futures_lite::StreamExt;
|
use futures::StreamExt;
|
||||||
use id3::{Tag, TagLike};
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelBehavior, ColumnTrait, ConnectOptions, Database, DatabaseTransaction, EntityTrait,
|
ColumnTrait, ConnectOptions, Database, DatabaseTransaction, EntityTrait, QueryFilter, Set,
|
||||||
QueryFilter, Set, TransactionTrait,
|
TransactionTrait,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
convert::Into,
|
|
||||||
ops::ControlFlow,
|
ops::ControlFlow,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use time::OffsetDateTime;
|
use tokio::sync::RwLock;
|
||||||
use tokio::{fs::File, sync::RwLock};
|
|
||||||
use tracing::{debug, error, info, instrument};
|
use tracing::{debug, error, info, instrument};
|
||||||
|
|
||||||
mod walk;
|
mod walk;
|
||||||
|
|
@ -88,6 +84,10 @@ async fn scan() {
|
||||||
do_entry(de, txn, state.clone()).await; // test without multithreading
|
do_entry(de, txn, state.clone()).await; // test without multithreading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Go over everything and consolidate duplicates?
|
||||||
|
// This is a bit of a pain, but it's probably worth it
|
||||||
|
// Since if we want to use multithreading, we need to do this
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut stat = STATUS.write().await;
|
let mut stat = STATUS.write().await;
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ async fn scan() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const VALID_EXTENSIONS: &[&str] = &["mp3"];
|
const VALID_EXTENSIONS: &[&str] = &["mp3", "flac"];
|
||||||
|
|
||||||
async fn do_entry(de: walk::DirEntry, txn: DatabaseTransaction, state: Arc<RwLock<ScanState>>) {
|
async fn do_entry(de: walk::DirEntry, txn: DatabaseTransaction, state: Arc<RwLock<ScanState>>) {
|
||||||
if let Err(e) = handle_entry(&txn, de.clone(), state).await {
|
if let Err(e) = handle_entry(&txn, de.clone(), state).await {
|
||||||
|
|
@ -175,99 +175,22 @@ async fn handle_entry(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let stem = stem.to_string_lossy();
|
let track = match ext.as_ref() {
|
||||||
|
"mp3" => mp3::handle(tx, path.clone(), stem.to_string_lossy(), state.clone()).await?,
|
||||||
|
"flac" => flac::handle(tx, path.clone(), stem.to_string_lossy(), state.clone()).await?,
|
||||||
|
|
||||||
let meta = { File::open(&path).await?.metadata().await? };
|
_ => unreachable!("We already checked the extension"),
|
||||||
|
};
|
||||||
|
|
||||||
let tag = Tag::async_read_from_path(&path).await?;
|
debug!("Inserted track {:?}", track.id);
|
||||||
|
|
||||||
let artist = find_artist(tx, &tag).await?;
|
|
||||||
|
|
||||||
let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
|
||||||
|
|
||||||
let mut am = track::ActiveModel::new();
|
|
||||||
|
|
||||||
am.title = Set(tag.title().unwrap_or(&stem).to_string());
|
|
||||||
am.album_id = Set(Some(album.id));
|
|
||||||
am.artist_id = Set(artist.as_ref().map(|c| c.id));
|
|
||||||
am.content_type = Set("audio/mpeg".to_string());
|
|
||||||
am.suffix = Set("mp3".to_string());
|
|
||||||
am.path = Set(path.to_string_lossy().to_string());
|
|
||||||
am.is_dir = Set(false);
|
|
||||||
am.is_video = Set(false);
|
|
||||||
am.duration = Set(tag.duration().map(Into::into).unwrap_or_default());
|
|
||||||
am.created = Set(OffsetDateTime::now_utc());
|
|
||||||
am.size = Set(meta
|
|
||||||
.len()
|
|
||||||
.try_into()
|
|
||||||
.expect("Failed to convert meta len to i64"));
|
|
||||||
|
|
||||||
let model = Track::insert(am).exec_with_returning(tx).await?;
|
|
||||||
|
|
||||||
debug!("Inserted track {:?}", model.id);
|
|
||||||
|
|
||||||
// TODO: figure out how to scan. steal from Gonic if we have to :3
|
// TODO: figure out how to scan. steal from Gonic if we have to :3
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(tx, tag, state))]
|
mod flac;
|
||||||
async fn find_album(
|
mod mp3;
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
artist_id: Option<i64>,
|
|
||||||
tag: &Tag,
|
|
||||||
state: Arc<RwLock<ScanState>>,
|
|
||||||
) -> Result<album::Model, Report> {
|
|
||||||
let Some(album_name) = tag.album() else {
|
|
||||||
return Err(Report::msg("Couldn't get album name from tag"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// if not, search by name
|
|
||||||
let search = Album::find()
|
|
||||||
.filter(album::Column::Name.like(album_name))
|
|
||||||
.one(tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// if we found one, return it
|
|
||||||
if let Some(search) = search {
|
|
||||||
return Ok(search);
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, create it
|
|
||||||
let mut am = album::ActiveModel::new();
|
|
||||||
|
|
||||||
am.name = Set(album_name.to_string());
|
|
||||||
am.music_folder_id = Set(state.read().await.music_folder_id);
|
|
||||||
am.artist_id = Set(artist_id);
|
|
||||||
am.year = Set(tag.year());
|
|
||||||
|
|
||||||
let genre = tag.genre_parsed();
|
|
||||||
if let Some(genre) = genre {
|
|
||||||
let genre_id = find_or_create_genre(tx, genre.as_ref()).await?;
|
|
||||||
am.genre_ids = Set(Some(vec![genre_id]));
|
|
||||||
}
|
|
||||||
am.song_count = Set(tag
|
|
||||||
.total_tracks()
|
|
||||||
.map(|v| i32::try_from(v).expect("Failed to convert total tracks to i32"))
|
|
||||||
.unwrap_or_default());
|
|
||||||
am.duration = Set(tag.duration().map(Into::into).unwrap_or_default());
|
|
||||||
am.created = Set(OffsetDateTime::now_utc());
|
|
||||||
|
|
||||||
let model = Album::insert(am).exec_with_returning(tx).await;
|
|
||||||
|
|
||||||
// if we failed to insert, return the error
|
|
||||||
let Ok(model) = model else {
|
|
||||||
let err = model.expect_err("somehow not err");
|
|
||||||
error!(
|
|
||||||
error = &err as &dyn std::error::Error,
|
|
||||||
"Failed to insert album {err}"
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(Report::new(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(tx))]
|
#[instrument(skip(tx))]
|
||||||
async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result<i64, Report> {
|
async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result<i64, Report> {
|
||||||
|
|
@ -305,55 +228,8 @@ async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result<i6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(tx, tag))]
|
|
||||||
async fn find_artist(tx: &DatabaseTransaction, tag: &Tag) -> Result<Option<artist::Model>, Report> {
|
|
||||||
let artist_to_search = match (tag.album_artist(), tag.artists()) {
|
|
||||||
(Some(tag_artist), None) => Some(tag_artist.to_string()),
|
|
||||||
(None, Some(tag_artists)) => Some(tag_artists.join(", ")),
|
|
||||||
(Some(tag_artist), Some(tag_artists)) => {
|
|
||||||
let mut artists = tag_artists.clone();
|
|
||||||
artists.push(tag_artist);
|
|
||||||
Some(artists.join(", "))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match &artist_to_search {
|
|
||||||
Some(artist_to_search) => {
|
|
||||||
let artist_to_search = artist_to_search.trim();
|
|
||||||
let attempt = Artist::find()
|
|
||||||
.filter(artist::Column::Name.like(artist_to_search))
|
|
||||||
.one(tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Some(attempt) = attempt {
|
|
||||||
Ok(Some(attempt))
|
|
||||||
} else {
|
|
||||||
let am = artist::ActiveModel {
|
|
||||||
name: Set(artist_to_search.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let model = Artist::insert(am).exec_with_returning(tx).await;
|
|
||||||
|
|
||||||
let Ok(model) = model else {
|
|
||||||
let err = model.expect_err("somehow not err");
|
|
||||||
error!(
|
|
||||||
error = &err as &dyn std::error::Error,
|
|
||||||
"Failed to insert artist {err}"
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(Report::new(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(model))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct ScanState {
|
pub struct ScanState {
|
||||||
pub music_folder_id: i64,
|
pub music_folder_id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
190
rave/src/scan/flac.rs
Normal file
190
rave/src/scan/flac.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use audiotags::{AudioTagEdit, FlacTag, Tag};
|
||||||
|
use color_eyre::Report;
|
||||||
|
use entities::{
|
||||||
|
album, artist,
|
||||||
|
prelude::{Album, Artist, Track},
|
||||||
|
track,
|
||||||
|
};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelBehavior, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, Set,
|
||||||
|
};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tokio::{fs::File, sync::RwLock};
|
||||||
|
use tracing::{debug, error, instrument};
|
||||||
|
|
||||||
|
use super::ScanState;
|
||||||
|
|
||||||
|
#[instrument(skip(tx, state))]
|
||||||
|
pub async fn handle(
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
path: PathBuf,
|
||||||
|
stem: Cow<'_, str>,
|
||||||
|
state: Arc<RwLock<ScanState>>,
|
||||||
|
) -> Result<track::Model, Report> {
|
||||||
|
let meta = { File::open(&path).await?.metadata().await? };
|
||||||
|
|
||||||
|
let tag = {
|
||||||
|
let tag = Tag::default().read_from_path(&path)?;
|
||||||
|
|
||||||
|
FlacTag::from(tag)
|
||||||
|
};
|
||||||
|
|
||||||
|
let artist = find_artist(tx, &tag).await?;
|
||||||
|
|
||||||
|
let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
||||||
|
|
||||||
|
let mut am = track::ActiveModel::new();
|
||||||
|
|
||||||
|
am.title = Set(tag.title().unwrap_or(&stem).to_string());
|
||||||
|
am.album_id = Set(Some(album.id));
|
||||||
|
am.artist_id = Set(artist.as_ref().map(|c| c.id));
|
||||||
|
am.content_type = Set("audio/mpeg".to_string());
|
||||||
|
am.suffix = Set("mp3".to_string());
|
||||||
|
am.path = Set(path.to_string_lossy().to_string());
|
||||||
|
am.is_dir = Set(false);
|
||||||
|
am.track_number = Set(tag
|
||||||
|
.track_number()
|
||||||
|
.map(|v| i32::try_from(v).expect("failed to convert track to i32")));
|
||||||
|
if let Some(disc_number) = tag
|
||||||
|
.disc_number()
|
||||||
|
.map(|v| i32::try_from(v).expect("failed to convert disc to i32"))
|
||||||
|
{
|
||||||
|
am.disc_number = Set(disc_number);
|
||||||
|
}
|
||||||
|
am.is_video = Set(false);
|
||||||
|
am.duration = {
|
||||||
|
let duration = tag.duration();
|
||||||
|
|
||||||
|
let duration = duration.unwrap_or_default();
|
||||||
|
|
||||||
|
debug!("Duration: {duration}");
|
||||||
|
let duration = duration.round().rem_euclid(2f64.powi(32)) as i64;
|
||||||
|
debug!("math'd duration: {duration}");
|
||||||
|
|
||||||
|
Set(duration)
|
||||||
|
};
|
||||||
|
am.created = Set(OffsetDateTime::now_utc());
|
||||||
|
am.size = Set(meta
|
||||||
|
.len()
|
||||||
|
.try_into()
|
||||||
|
.expect("Failed to convert meta len to i64"));
|
||||||
|
|
||||||
|
let model = Track::insert(am).exec_with_returning(tx).await?;
|
||||||
|
|
||||||
|
Ok(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(tx, tag, state))]
|
||||||
|
async fn find_album(
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
artist_id: Option<i64>,
|
||||||
|
tag: &FlacTag,
|
||||||
|
state: Arc<RwLock<ScanState>>,
|
||||||
|
) -> Result<album::Model, Report> {
|
||||||
|
let Some(album) = tag.album() else {
|
||||||
|
return Err(Report::msg("Couldn't get album name from tag"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// if not, search by name
|
||||||
|
let search = Album::find()
|
||||||
|
.filter(album::Column::Name.like(album.title))
|
||||||
|
.one(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// if we found one, return it
|
||||||
|
if let Some(search) = search {
|
||||||
|
return Ok(search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, create it
|
||||||
|
let mut am = album::ActiveModel::new();
|
||||||
|
|
||||||
|
am.name = Set(album.title.to_string());
|
||||||
|
am.music_folder_id = Set(state.read().await.music_folder_id);
|
||||||
|
am.artist_id = Set(artist_id);
|
||||||
|
am.year = Set(tag.year());
|
||||||
|
|
||||||
|
let genre = tag.genre();
|
||||||
|
if let Some(genre) = genre {
|
||||||
|
let genre_id = super::find_or_create_genre(tx, genre).await?;
|
||||||
|
am.genre_ids = Set(Some(vec![genre_id]));
|
||||||
|
}
|
||||||
|
am.song_count = Set(tag
|
||||||
|
.total_tracks()
|
||||||
|
.map(|v| i64::try_from(v).expect("Failed to convert total tracks to i32"))
|
||||||
|
.unwrap_or_default());
|
||||||
|
am.duration = Set(tag
|
||||||
|
.duration()
|
||||||
|
.map(|c| c.round().rem_euclid(2f64.powi(32)) as i64) // TODO: figure out how to do this properly
|
||||||
|
.unwrap_or_default());
|
||||||
|
am.created = Set(OffsetDateTime::now_utc());
|
||||||
|
|
||||||
|
let model = Album::insert(am).exec_with_returning(tx).await;
|
||||||
|
|
||||||
|
// if we failed to insert, return the error
|
||||||
|
let Ok(model) = model else {
|
||||||
|
let err = model.expect_err("somehow not err");
|
||||||
|
error!(
|
||||||
|
error = &err as &dyn std::error::Error,
|
||||||
|
"Failed to insert album {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Report::new(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(tx, tag))]
|
||||||
|
async fn find_artist(
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
tag: &FlacTag,
|
||||||
|
) -> Result<Option<artist::Model>, Report> {
|
||||||
|
let artist_to_search = match (tag.album_artist(), tag.artists()) {
|
||||||
|
(Some(tag_artist), None) => Some(tag_artist.to_string()),
|
||||||
|
(None, Some(tag_artists)) => Some(tag_artists.join(", ")),
|
||||||
|
(Some(tag_artist), Some(tag_artists)) => {
|
||||||
|
let mut artists = tag_artists.clone();
|
||||||
|
artists.push(tag_artist);
|
||||||
|
Some(artists.join(", "))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &artist_to_search {
|
||||||
|
Some(artist_to_search) => {
|
||||||
|
let artist_to_search = artist_to_search.trim();
|
||||||
|
let attempt = Artist::find()
|
||||||
|
.filter(artist::Column::Name.like(artist_to_search))
|
||||||
|
.one(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(attempt) = attempt {
|
||||||
|
Ok(Some(attempt))
|
||||||
|
} else {
|
||||||
|
let am = artist::ActiveModel {
|
||||||
|
name: Set(artist_to_search.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let model = Artist::insert(am).exec_with_returning(tx).await;
|
||||||
|
|
||||||
|
let Ok(model) = model else {
|
||||||
|
let err = model.expect_err("somehow not err");
|
||||||
|
error!(
|
||||||
|
error = &err as &dyn std::error::Error,
|
||||||
|
"Failed to insert artist {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Report::new(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(model))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
190
rave/src/scan/mp3.rs
Normal file
190
rave/src/scan/mp3.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use audiotags::{AudioTagEdit, Id3v2Tag, Tag};
|
||||||
|
use color_eyre::Report;
|
||||||
|
use entities::{
|
||||||
|
album, artist,
|
||||||
|
prelude::{Album, Artist, Track},
|
||||||
|
track,
|
||||||
|
};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelBehavior, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, Set,
|
||||||
|
};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tokio::{fs::File, sync::RwLock};
|
||||||
|
use tracing::{debug, error, instrument};
|
||||||
|
|
||||||
|
use super::ScanState;
|
||||||
|
|
||||||
|
#[instrument(skip(tx, state))]
|
||||||
|
pub async fn handle(
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
path: PathBuf,
|
||||||
|
stem: Cow<'_, str>,
|
||||||
|
state: Arc<RwLock<ScanState>>,
|
||||||
|
) -> Result<track::Model, Report> {
|
||||||
|
let meta = { File::open(&path).await?.metadata().await? };
|
||||||
|
|
||||||
|
let tag = {
|
||||||
|
let tag = Tag::default().read_from_path(&path)?;
|
||||||
|
|
||||||
|
audiotags::Id3v2Tag::from(tag)
|
||||||
|
};
|
||||||
|
|
||||||
|
let artist = find_artist(tx, &tag).await?;
|
||||||
|
|
||||||
|
let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
||||||
|
|
||||||
|
let mut am = track::ActiveModel::new();
|
||||||
|
|
||||||
|
am.title = Set(tag.title().unwrap_or(&stem).to_string());
|
||||||
|
am.album_id = Set(Some(album.id));
|
||||||
|
am.artist_id = Set(artist.as_ref().map(|c| c.id));
|
||||||
|
am.content_type = Set("audio/mpeg".to_string());
|
||||||
|
am.suffix = Set("mp3".to_string());
|
||||||
|
am.path = Set(path.to_string_lossy().to_string());
|
||||||
|
am.is_dir = Set(false);
|
||||||
|
am.track_number = Set(tag
|
||||||
|
.track_number()
|
||||||
|
.map(|v| i32::try_from(v).expect("failed to convert track to i32")));
|
||||||
|
if let Some(disc_number) = tag
|
||||||
|
.disc_number()
|
||||||
|
.map(|v| i32::try_from(v).expect("failed to convert disc to i32"))
|
||||||
|
{
|
||||||
|
am.disc_number = Set(disc_number);
|
||||||
|
}
|
||||||
|
am.is_video = Set(false);
|
||||||
|
am.duration = {
|
||||||
|
let duration = tag.duration();
|
||||||
|
|
||||||
|
let duration = duration.unwrap_or_default();
|
||||||
|
|
||||||
|
debug!("Duration: {duration}");
|
||||||
|
let duration = duration.round().rem_euclid(2f64.powi(32)) as i64;
|
||||||
|
debug!("math'd duration: {duration}");
|
||||||
|
|
||||||
|
Set(duration)
|
||||||
|
};
|
||||||
|
am.created = Set(OffsetDateTime::now_utc());
|
||||||
|
am.size = Set(meta
|
||||||
|
.len()
|
||||||
|
.try_into()
|
||||||
|
.expect("Failed to convert meta len to i64"));
|
||||||
|
|
||||||
|
let model = Track::insert(am).exec_with_returning(tx).await?;
|
||||||
|
|
||||||
|
Ok(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(tx, tag, state))]
|
||||||
|
async fn find_album(
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
artist_id: Option<i64>,
|
||||||
|
tag: &Id3v2Tag,
|
||||||
|
state: Arc<RwLock<ScanState>>,
|
||||||
|
) -> Result<album::Model, Report> {
|
||||||
|
let Some(album) = tag.album() else {
|
||||||
|
return Err(Report::msg("Couldn't get album name from tag"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// if not, search by name
|
||||||
|
let search = Album::find()
|
||||||
|
.filter(album::Column::Name.like(album.title))
|
||||||
|
.one(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// if we found one, return it
|
||||||
|
if let Some(search) = search {
|
||||||
|
return Ok(search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, create it
|
||||||
|
let mut am = album::ActiveModel::new();
|
||||||
|
|
||||||
|
am.name = Set(album.title.to_string());
|
||||||
|
am.music_folder_id = Set(state.read().await.music_folder_id);
|
||||||
|
am.artist_id = Set(artist_id);
|
||||||
|
am.year = Set(tag.year());
|
||||||
|
|
||||||
|
let genre = tag.genre();
|
||||||
|
if let Some(genre) = genre {
|
||||||
|
let genre_id = super::find_or_create_genre(tx, genre).await?;
|
||||||
|
am.genre_ids = Set(Some(vec![genre_id]));
|
||||||
|
}
|
||||||
|
am.song_count = Set(tag
|
||||||
|
.total_tracks()
|
||||||
|
.map(|v| i64::try_from(v).expect("Failed to convert total tracks to i32"))
|
||||||
|
.unwrap_or_default());
|
||||||
|
am.duration = Set(tag
|
||||||
|
.duration()
|
||||||
|
.map(|c| c.round().rem_euclid(2f64.powi(32)) as i64) // TODO: figure out how to do this properly
|
||||||
|
.unwrap_or_default());
|
||||||
|
am.created = Set(OffsetDateTime::now_utc());
|
||||||
|
|
||||||
|
let model = Album::insert(am).exec_with_returning(tx).await;
|
||||||
|
|
||||||
|
// if we failed to insert, return the error
|
||||||
|
let Ok(model) = model else {
|
||||||
|
let err = model.expect_err("somehow not err");
|
||||||
|
error!(
|
||||||
|
error = &err as &dyn std::error::Error,
|
||||||
|
"Failed to insert album {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Report::new(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(tx, tag))]
|
||||||
|
async fn find_artist(
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
tag: &Id3v2Tag,
|
||||||
|
) -> Result<Option<artist::Model>, Report> {
|
||||||
|
let artist_to_search = match (tag.album_artist(), tag.artists()) {
|
||||||
|
(Some(tag_artist), None) => Some(tag_artist.to_string()),
|
||||||
|
(None, Some(tag_artists)) => Some(tag_artists.join(", ")),
|
||||||
|
(Some(tag_artist), Some(tag_artists)) => {
|
||||||
|
let mut artists = tag_artists.clone();
|
||||||
|
artists.push(tag_artist);
|
||||||
|
Some(artists.join(", "))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &artist_to_search {
|
||||||
|
Some(artist_to_search) => {
|
||||||
|
let artist_to_search = artist_to_search.trim();
|
||||||
|
let attempt = Artist::find()
|
||||||
|
.filter(artist::Column::Name.like(artist_to_search))
|
||||||
|
.one(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(attempt) = attempt {
|
||||||
|
Ok(Some(attempt))
|
||||||
|
} else {
|
||||||
|
let am = artist::ActiveModel {
|
||||||
|
name: Set(artist_to_search.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let model = Artist::insert(am).exec_with_returning(tx).await;
|
||||||
|
|
||||||
|
let Ok(model) = model else {
|
||||||
|
let err = model.expect_err("somehow not err");
|
||||||
|
error!(
|
||||||
|
error = &err as &dyn std::error::Error,
|
||||||
|
"Failed to insert artist {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Report::new(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(model))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,18 +7,20 @@ use std::{
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures_lite::{future::Boxed as BoxedFut, stream, Future, FutureExt, Stream, StreamExt};
|
use futures::{
|
||||||
|
future::BoxFuture as BoxedFut,
|
||||||
|
stream::{self, BoxStream},
|
||||||
|
Future, FutureExt, Stream, StreamExt,
|
||||||
|
};
|
||||||
|
|
||||||
use tokio::fs::{read_dir, ReadDir};
|
use tokio::fs::{read_dir, ReadDir};
|
||||||
|
|
||||||
pub use tokio::io::Result;
|
pub use tokio::io::Result;
|
||||||
pub type DirEntry = Arc<tokio::fs::DirEntry>;
|
pub type DirEntry = Arc<tokio::fs::DirEntry>;
|
||||||
|
|
||||||
type BoxStream = futures_lite::stream::Boxed<Result<DirEntry>>;
|
|
||||||
|
|
||||||
pub struct WalkDir {
|
pub struct WalkDir {
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
entries: BoxStream,
|
entries: BoxStream<'static, Result<DirEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -34,7 +36,7 @@ impl WalkDir {
|
||||||
root: root.as_ref().to_path_buf(),
|
root: root.as_ref().to_path_buf(),
|
||||||
entries: walk_dir(
|
entries: walk_dir(
|
||||||
root,
|
root,
|
||||||
None::<Box<dyn FnMut(DirEntry) -> BoxedFut<Filtering> + Send>>,
|
None::<Box<dyn FnMut(DirEntry) -> BoxedFut<'static, Filtering> + Send>>,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +63,10 @@ impl Stream for WalkDir {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn walk_dir<F, Fut>(root: impl AsRef<Path>, filter: Option<F>) -> BoxStream
|
fn walk_dir<F, Fut>(
|
||||||
|
root: impl AsRef<Path>,
|
||||||
|
filter: Option<F>,
|
||||||
|
) -> BoxStream<'static, Result<DirEntry>>
|
||||||
where
|
where
|
||||||
F: FnMut(DirEntry) -> Fut + Send + 'static,
|
F: FnMut(DirEntry) -> Fut + Send + 'static,
|
||||||
Fut: Future<Output = Filtering> + Send,
|
Fut: Future<Output = Filtering> + Send,
|
||||||
|
|
@ -90,7 +95,10 @@ enum State<F> {
|
||||||
|
|
||||||
type UnfoldState<F> = (Result<DirEntry>, State<F>);
|
type UnfoldState<F> = (Result<DirEntry>, State<F>);
|
||||||
|
|
||||||
fn walk<F, Fut>(mut dirs: Vec<ReadDir>, filter: Option<F>) -> BoxedFut<Option<UnfoldState<F>>>
|
fn walk<F, Fut>(
|
||||||
|
mut dirs: Vec<ReadDir>,
|
||||||
|
filter: Option<F>,
|
||||||
|
) -> BoxedFut<'static, Option<UnfoldState<F>>>
|
||||||
where
|
where
|
||||||
F: FnMut(DirEntry) -> Fut + Send + 'static,
|
F: FnMut(DirEntry) -> Fut + Send + 'static,
|
||||||
Fut: Future<Output = Filtering> + Send,
|
Fut: Future<Output = Filtering> + Send,
|
||||||
|
|
@ -116,7 +124,7 @@ fn walk_entry<F, Fut>(
|
||||||
entry: DirEntry,
|
entry: DirEntry,
|
||||||
mut dirs: Vec<ReadDir>,
|
mut dirs: Vec<ReadDir>,
|
||||||
mut filter: Option<F>,
|
mut filter: Option<F>,
|
||||||
) -> BoxedFut<Option<UnfoldState<F>>>
|
) -> BoxedFut<'static, Option<UnfoldState<F>>>
|
||||||
where
|
where
|
||||||
F: FnMut(DirEntry) -> Fut + Send + 'static,
|
F: FnMut(DirEntry) -> Fut + Send + 'static,
|
||||||
Fut: Future<Output = Filtering> + Send,
|
Fut: Future<Output = Filtering> + Send,
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ mod error;
|
||||||
mod types;
|
mod types;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use types::album::Album;
|
pub use types::album::Album;
|
||||||
|
pub use types::child::Child;
|
||||||
pub use types::music_folder::MusicFolder;
|
pub use types::music_folder::MusicFolder;
|
||||||
pub use types::track::Track;
|
|
||||||
|
|
||||||
impl IntoResponse for SubsonicResponse {
|
impl IntoResponse for SubsonicResponse {
|
||||||
fn into_response(self) -> poem::Response {
|
fn into_response(self) -> poem::Response {
|
||||||
|
|
@ -53,8 +53,11 @@ impl SubsonicResponse {
|
||||||
Self::new(SubResponseType::AlbumList2 { albums })
|
Self::new(SubResponseType::AlbumList2 { albums })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_album(album: Album, songs: Vec<Track>) -> Self {
|
pub fn new_album(album: Album, songs: Vec<Child>) -> Self {
|
||||||
Self::new(SubResponseType::Album { album, songs })
|
Self::new(SubResponseType::Album {
|
||||||
|
album: Box::new(album),
|
||||||
|
songs,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_empty() -> Self {
|
pub fn new_empty() -> Self {
|
||||||
|
|
@ -107,9 +110,9 @@ pub enum SubResponseType {
|
||||||
#[serde(rename = "album")]
|
#[serde(rename = "album")]
|
||||||
Album {
|
Album {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
album: Album,
|
album: Box<Album>,
|
||||||
#[serde(flatten)]
|
#[serde(rename = "song")]
|
||||||
songs: Vec<Track>,
|
songs: Vec<Child>,
|
||||||
},
|
},
|
||||||
#[serde(rename = "scanStatus")]
|
#[serde(rename = "scanStatus")]
|
||||||
ScanStatus {
|
ScanStatus {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
pub mod album;
|
pub mod album;
|
||||||
|
pub mod child;
|
||||||
pub mod music_folder;
|
pub mod music_folder;
|
||||||
pub mod track;
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,61 @@
|
||||||
// use entities::*;
|
use entities::{album, artist, genre};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use time::format_description::well_known::Iso8601;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
id: String,
|
#[serde(rename = "@id")]
|
||||||
name: String,
|
pub id: String,
|
||||||
artist: Option<String>,
|
#[serde(rename = "@name")]
|
||||||
#[serde(rename = "artistId")]
|
pub name: String,
|
||||||
artist_id: Option<String>,
|
#[serde(rename = "@artist", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub artist: Option<String>,
|
||||||
|
#[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub artist_id: Option<String>,
|
||||||
|
#[serde(rename = "@coverArt", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cover_art_id: Option<String>,
|
||||||
|
#[serde(rename = "@songCount")]
|
||||||
|
pub song_count: u64,
|
||||||
|
#[serde(rename = "@duration")]
|
||||||
|
pub duration: u64,
|
||||||
|
#[serde(rename = "@playCount")]
|
||||||
|
pub play_count: u64,
|
||||||
|
#[serde(rename = "@created")]
|
||||||
|
pub created: String,
|
||||||
|
#[serde(rename = "@starred", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub starred: Option<String>,
|
||||||
|
#[serde(rename = "@year", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub year: Option<u32>,
|
||||||
|
#[serde(rename = "@genre", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub genre: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Album {
|
impl Album {
|
||||||
// pub fn new(
|
#[allow(clippy::cast_sign_loss)]
|
||||||
// album: album::Model,
|
#[must_use]
|
||||||
// artists: Option<artist::Model>,
|
pub fn new(
|
||||||
// cover_art: Option<cover_art::Model>,
|
album: album::Model,
|
||||||
// genres: Vec<genre::Model>,
|
artist: Option<artist::Model>,
|
||||||
// ) -> Self {
|
genre: Option<genre::Model>,
|
||||||
// Self {
|
) -> Self {
|
||||||
// id: album.id.to_string(),
|
Self {
|
||||||
// name: album.name,
|
id: format!("al-{}", album.id),
|
||||||
// artist: artist.map(|a| a.name),
|
name: album.name,
|
||||||
// artist_id: artist.map(|a| format!("ar-{}", a.id)),
|
artist: artist.clone().map(|c| c.name),
|
||||||
// }
|
artist_id: artist.map(|c| format!("ar-{}", c.id)),
|
||||||
// }
|
cover_art_id: album.cover_art_id.map(|c| format!("ca-{c}")),
|
||||||
// }
|
song_count: album.song_count as u64,
|
||||||
|
duration: album.duration as u64,
|
||||||
|
play_count: album.play_count as u64,
|
||||||
|
created: album
|
||||||
|
.created
|
||||||
|
.format(&Iso8601::DEFAULT)
|
||||||
|
.expect("Failed to format time"),
|
||||||
|
starred: album
|
||||||
|
.starred
|
||||||
|
.map(|c| c.format(&Iso8601::DEFAULT).expect("Failed to format time")),
|
||||||
|
year: album.year.map(|c| c as u32),
|
||||||
|
genre: genre.map(|c| c.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
86
rave/src/subsonic/types/child.rs
Normal file
86
rave/src/subsonic/types/child.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
use entities::track;
|
||||||
|
use serde::Serialize;
|
||||||
|
use time::format_description::well_known::Iso8601;
|
||||||
|
|
||||||
|
use crate::subsonic::Album;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct Child {
|
||||||
|
#[serde(rename = "@id")]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "@isDir")]
|
||||||
|
pub is_dir: bool,
|
||||||
|
#[serde(rename = "@title")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(rename = "@album", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub album: Option<String>,
|
||||||
|
#[serde(rename = "@track", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub track: Option<u32>,
|
||||||
|
#[serde(rename = "@year", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub year: Option<u32>,
|
||||||
|
#[serde(rename = "@coverArtId", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cover_art_id: Option<String>,
|
||||||
|
#[serde(rename = "@size")]
|
||||||
|
pub size: u64,
|
||||||
|
#[serde(rename = "@contentType")]
|
||||||
|
pub content_type: String,
|
||||||
|
#[serde(rename = "@suffix")]
|
||||||
|
pub suffix: String,
|
||||||
|
#[serde(rename = "@starred", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub starred: Option<String>,
|
||||||
|
#[serde(rename = "@duration")]
|
||||||
|
pub duration: u64,
|
||||||
|
#[serde(rename = "@bitRate", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bit_rate: Option<u64>,
|
||||||
|
#[serde(rename = "@path")]
|
||||||
|
pub path: String,
|
||||||
|
#[serde(rename = "@playCount")]
|
||||||
|
pub play_count: u64,
|
||||||
|
#[serde(rename = "@discNumber")]
|
||||||
|
pub disc_number: u32,
|
||||||
|
#[serde(rename = "@created")]
|
||||||
|
pub created: String,
|
||||||
|
#[serde(rename = "@albumId", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub album_id: Option<String>,
|
||||||
|
#[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub artist_id: Option<String>,
|
||||||
|
#[serde(rename = "@type")]
|
||||||
|
pub child_type: String,
|
||||||
|
#[serde(rename = "@isVideo")]
|
||||||
|
pub is_video: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Child {
|
||||||
|
#[allow(clippy::cast_sign_loss)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(track: &track::Model, album: &Album) -> Self {
|
||||||
|
Self {
|
||||||
|
id: format!("tr-{}", track.id),
|
||||||
|
is_dir: false,
|
||||||
|
title: track.title.clone(),
|
||||||
|
album: Some(album.name.clone()),
|
||||||
|
track: track.track_number.map(|c| c as u32),
|
||||||
|
year: album.year,
|
||||||
|
cover_art_id: track.cover_art_id.map(|c| format!("ca-{c}")),
|
||||||
|
size: track.size as u64,
|
||||||
|
content_type: track.content_type.clone(),
|
||||||
|
suffix: track.suffix.clone(),
|
||||||
|
starred: track
|
||||||
|
.starred
|
||||||
|
.map(|c| c.format(&Iso8601::DEFAULT).expect("Failed to format date")),
|
||||||
|
duration: track.duration as u64,
|
||||||
|
bit_rate: track.bit_rate.map(|c| c as u64),
|
||||||
|
path: track.path.clone(),
|
||||||
|
play_count: track.play_count as u64,
|
||||||
|
disc_number: track.disc_number as u32,
|
||||||
|
created: track
|
||||||
|
.created
|
||||||
|
.format(&Iso8601::DEFAULT)
|
||||||
|
.expect("Failed to format date"),
|
||||||
|
album_id: Some(format!("al-{}", album.id)),
|
||||||
|
artist_id: album.artist_id.clone(),
|
||||||
|
child_type: "music".to_string(),
|
||||||
|
is_video: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,29 +1,19 @@
|
||||||
use entities::music_folder::Model;
|
use entities::music_folder::Model;
|
||||||
use serde::{ser::SerializeStruct, Serialize};
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct MusicFolder {
|
pub struct MusicFolder {
|
||||||
pub(crate) id: i64,
|
#[serde(rename = "@id")]
|
||||||
pub(crate) name: String,
|
pub id: String,
|
||||||
|
#[serde(rename = "@name")]
|
||||||
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Model> for MusicFolder {
|
impl From<Model> for MusicFolder {
|
||||||
fn from(value: Model) -> Self {
|
fn from(value: Model) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: format!("mf-{}", value.id),
|
||||||
name: value.name,
|
name: value.name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for MusicFolder {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
let mut s = serializer.serialize_struct("MusicFolder", 2)?;
|
|
||||||
s.serialize_field("id", &format!("mf-{}", self.id))?;
|
|
||||||
s.serialize_field("name", &self.name)?;
|
|
||||||
s.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct Track {}
|
|
||||||
Loading…
Reference in a new issue