Re-implement indexing; Implement ReplayGain
This commit is contained in:
parent
22bb854862
commit
8771669a47
|
@ -474,6 +474,34 @@ version = "2.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.8"
|
||||
|
@ -486,12 +514,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.16"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
|
@ -503,6 +528,12 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.8"
|
||||
|
@ -591,15 +622,19 @@ dependencies = [
|
|||
"chrono",
|
||||
"dirs",
|
||||
"kdl",
|
||||
"lofty",
|
||||
"miette",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"owo-colors",
|
||||
"rayon",
|
||||
"replaygain",
|
||||
"sea-orm",
|
||||
"sea-orm-migration",
|
||||
"sea-query",
|
||||
"symphonia",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
|
@ -649,6 +684,12 @@ version = "2.5.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "extended"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.1"
|
||||
|
@ -661,6 +702,16 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.0"
|
||||
|
@ -1061,6 +1112,32 @@ dependencies = [
|
|||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lofty"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f75066eb1d25a7047fb2667edb410ae2592439ed81546f95c28b0a1c7d7d3818"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"data-encoding",
|
||||
"flate2",
|
||||
"lofty_attr",
|
||||
"log",
|
||||
"ogg_pager",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lofty_attr"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "764b60e1ddd07e5665a6a17636a95cd7d8f3b86c73503a69c32979d05f72f3cf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
|
@ -1232,6 +1309,12 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
|
@ -1282,6 +1365,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ogg_pager"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c949d63b387b25c332f6e39d1762dd4b405008289dd7681f02c258b1294653ca"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
|
@ -1585,6 +1677,26 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
|
@ -1676,6 +1788,12 @@ dependencies = [
|
|||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "replaygain"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de46745c4722b3312b44f1aa45c9eb813059cab25705de6c69180f3715c4decc"
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.42"
|
||||
|
@ -2413,28 +2531,32 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941"
|
||||
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"symphonia-bundle-flac",
|
||||
"symphonia-bundle-mp3",
|
||||
"symphonia-codec-aac",
|
||||
"symphonia-codec-adpcm",
|
||||
"symphonia-codec-alac",
|
||||
"symphonia-codec-pcm",
|
||||
"symphonia-codec-vorbis",
|
||||
"symphonia-core",
|
||||
"symphonia-format-caf",
|
||||
"symphonia-format-isomp4",
|
||||
"symphonia-format-mkv",
|
||||
"symphonia-format-ogg",
|
||||
"symphonia-format-wav",
|
||||
"symphonia-format-riff",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-bundle-flac"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f23b0482a7cb18fcdf9981ab0b78df800ef0080187d294650023c462439058d"
|
||||
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
|
@ -2444,11 +2566,10 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia-bundle-mp3"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a"
|
||||
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
|
@ -2456,10 +2577,31 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-adpcm"
|
||||
version = "0.5.3"
|
||||
name = "symphonia-codec-aac"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "870e7dc1865d818c7b6318879d060553a73a3b2a3b8443dff90910f10ac41150"
|
||||
checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-adpcm"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-alac"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
|
@ -2467,9 +2609,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia-codec-pcm"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47f1fbd220a06a641c8ce2ddad10f5ef6ee5cc0c54d9044d25d43b0d3119deaa"
|
||||
checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
|
@ -2477,9 +2619,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia-codec-vorbis"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3953397e3506aa01350c4205817e4f95b58d476877a42f0458d07b665749e203"
|
||||
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
|
@ -2488,9 +2630,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142"
|
||||
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
|
@ -2500,10 +2642,34 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-mkv"
|
||||
version = "0.5.3"
|
||||
name = "symphonia-format-caf"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5c61dfc851ad25d4043d8c231d8617e8f7cd02a6cc0edad21ade21848d58895"
|
||||
checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-isomp4"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-mkv"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -2514,9 +2680,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia-format-ogg"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bf1a00ccd11452d44048a0368828040f778ae650418dbd9d8765b7ee2574c8d"
|
||||
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
|
@ -2525,11 +2691,12 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-wav"
|
||||
version = "0.5.3"
|
||||
name = "symphonia-format-riff"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da76614728fa27c003bdcdfbac51396bd8fcbf94c95fe8e62f1d2bac58ef03a4"
|
||||
checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50"
|
||||
dependencies = [
|
||||
"extended",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
|
@ -2537,9 +2704,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0"
|
||||
checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
|
@ -2549,9 +2716,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "symphonia-utils-xiph"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a450ca645b80d69aff8b35576cbfdc7f20940b29998202aab910045714c951f8"
|
||||
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
|
||||
dependencies = [
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
|
@ -2651,12 +2818,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.30"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
|
||||
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
|
@ -2671,10 +2839,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
|||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.15"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
|
||||
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
|
@ -2778,20 +2947,20 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
|
|
|
@ -10,17 +10,21 @@ adler = "1.0.2"
|
|||
chrono = "0.4.31"
|
||||
dirs = "5.0.1"
|
||||
kdl = "4.6.0"
|
||||
lofty = "0.18.2"
|
||||
miette = { version = "5.10.0", features = ["fancy"] }
|
||||
mime = "0.3.17"
|
||||
mime_guess = "2.0.4"
|
||||
owo-colors = "3.5.0"
|
||||
rayon = "1.8.1"
|
||||
replaygain = "1.0.1"
|
||||
sea-orm = { version = "0.12.4", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"] }
|
||||
sea-orm-migration = "0.12.4"
|
||||
sea-query = "0.30.2"
|
||||
symphonia = { version = "0.5.3", features = ["flac", "mp3", "vorbis", "ogg", "wav"] }
|
||||
symphonia = { version = "0.5.4", features = ["all"] }
|
||||
thiserror = "1.0.56"
|
||||
time = "0.3.34"
|
||||
tokio = { version = "1.33.0", features = ["full"] }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-core = "0.1.32"
|
||||
tracing-subscriber = "0.3.17"
|
||||
tracing-subscriber = "0.3.18"
|
||||
walkdir = "2.4.0"
|
||||
|
|
|
@ -8,7 +8,7 @@ use super::{
|
|||
use kdl::{KdlDocument, KdlNode};
|
||||
use miette::miette;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Source {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use std::{fmt::Debug, num::TryFromIntError, sync::PoisonError};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
num::{ParseFloatError, ParseIntError, TryFromIntError},
|
||||
sync::PoisonError,
|
||||
};
|
||||
|
||||
use kdl::KdlError;
|
||||
use miette::Diagnostic;
|
||||
|
@ -9,12 +13,16 @@ use tokio::task::JoinError;
|
|||
pub enum EleanorError {
|
||||
#[error("Couldn't unlock mutex")]
|
||||
LockFailed,
|
||||
#[error("Failed to convert types")]
|
||||
CastError,
|
||||
#[error("Failed to convert from integer: {0}")]
|
||||
TryFromIntError(#[from] TryFromIntError),
|
||||
#[error("Failed to parse integer: {0}")]
|
||||
ParseIntError(#[from] ParseIntError),
|
||||
#[error("Failed to parse float")]
|
||||
ParseFloatError(#[from] ParseFloatError),
|
||||
#[error("Failed to create probe: {0}")]
|
||||
SymponiaError(#[from] symphonia::core::errors::Error),
|
||||
#[error("Failed to read song metadata: {0}")]
|
||||
LoftyError(#[from] lofty::LoftyError),
|
||||
#[error("Database error: {0}")]
|
||||
DatabaseError(#[from] sea_orm::DbErr),
|
||||
#[error("An IO error occured: {0}")]
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
use std::{ffi::OsStr, fs::File, hash::Hasher, path::Path};
|
||||
|
||||
use adler::Adler32;
|
||||
use lofty::{AudioFile, ItemKey, TaggedFileExt};
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use rayon::prelude::*;
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, Set};
|
||||
use symphonia::{
|
||||
core::{
|
||||
formats::{FormatOptions, FormatReader},
|
||||
io::{MediaSourceStream, MediaSourceStreamOptions},
|
||||
meta::{Limit, MetadataOptions},
|
||||
probe::Hint,
|
||||
},
|
||||
default::get_probe,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::debug;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::backend::replaygain::{format_gain, ReplayGain};
|
||||
|
||||
use super::{
|
||||
config::Source,
|
||||
error::EleanorError,
|
||||
model::{library, library::Column, sources},
|
||||
};
|
||||
|
||||
/// Get audio packets, ignoring metadata
|
||||
fn get_packets(path: &Path) -> Result<Box<dyn FormatReader>> {
|
||||
let file = Box::new(File::open(path).into_diagnostic()?);
|
||||
|
||||
let probe = get_probe();
|
||||
|
||||
let ext = path.extension().and_then(OsStr::to_str).unwrap_or("");
|
||||
|
||||
let source = MediaSourceStream::new(file, MediaSourceStreamOptions::default());
|
||||
probe
|
||||
.format(
|
||||
Hint::new().with_extension(ext),
|
||||
source,
|
||||
&FormatOptions::default(),
|
||||
&MetadataOptions {
|
||||
limit_metadata_bytes: Limit::Maximum(0),
|
||||
limit_visual_bytes: Limit::Maximum(0),
|
||||
},
|
||||
)
|
||||
.into_diagnostic()
|
||||
.map(|v| v.format)
|
||||
}
|
||||
|
||||
fn hash_packets(data: &mut Box<dyn FormatReader>) -> u64 {
|
||||
let mut adler = Adler32::new();
|
||||
|
||||
while let Ok(packet) = data.next_packet() {
|
||||
adler.write(&packet.data);
|
||||
}
|
||||
|
||||
adler.finish()
|
||||
}
|
||||
|
||||
fn index_song(
|
||||
file: &DirEntry,
|
||||
source: &Source,
|
||||
force: bool,
|
||||
indexed_ts: OffsetDateTime,
|
||||
) -> Result<Option<library::ActiveModel>, EleanorError> {
|
||||
// Re-index previously indexed files
|
||||
if !force {
|
||||
let modified = file
|
||||
.metadata()
|
||||
.into_diagnostic()?
|
||||
.modified()?
|
||||
.duration_since(indexed_ts.into())
|
||||
.is_ok();
|
||||
|
||||
if modified {
|
||||
debug!("Skipping file {}", file.path().display());
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Indexing file {}", file.path().display());
|
||||
|
||||
let tagged_file = lofty::read_from_path(file.path())?;
|
||||
|
||||
let tags = tagged_file.primary_tag().or(tagged_file.first_tag());
|
||||
let properties = tagged_file.properties();
|
||||
|
||||
// Hash audio packets
|
||||
let mut packets = get_packets(file.path())?;
|
||||
let hash = hash_packets(&mut packets);
|
||||
|
||||
let rg_track_gain = tags
|
||||
.and_then(|t| t.get_string(&ItemKey::ReplayGainTrackGain))
|
||||
.and_then(|v| format_gain(v).ok());
|
||||
let rg_track_peak = tags
|
||||
.and_then(|t| t.get_string(&ItemKey::ReplayGainTrackPeak))
|
||||
.and_then(|v| format_gain(v).ok());
|
||||
let rg_album_gain = tags
|
||||
.and_then(|t| t.get_string(&ItemKey::ReplayGainAlbumGain))
|
||||
.and_then(|v| format_gain(v).ok());
|
||||
let rg_album_peak = tags
|
||||
.and_then(|t| t.get_string(&ItemKey::ReplayGainAlbumPeak))
|
||||
.and_then(|v| format_gain(v).ok());
|
||||
|
||||
// Check for existing ReplayGain tags.
|
||||
let mut rg = if let (Some(track_gain), Some(track_peak)) = (rg_track_gain, rg_track_peak) {
|
||||
Ok(ReplayGain {
|
||||
track_gain,
|
||||
track_peak,
|
||||
album_gain: None,
|
||||
album_peak: None,
|
||||
})
|
||||
} else {
|
||||
// Calculate replaygain values for the audio track
|
||||
let mut packets = get_packets(file.path())?;
|
||||
ReplayGain::try_calculate(&mut packets)
|
||||
};
|
||||
|
||||
// Set album gain and peak, if present in metadata.
|
||||
if let Ok(rg) = &mut rg {
|
||||
rg.album_gain = rg_album_gain;
|
||||
rg.album_peak = rg_album_peak;
|
||||
}
|
||||
|
||||
let song: library::ActiveModel = library::ActiveModel {
|
||||
path: Set(file
|
||||
.path()
|
||||
.parent()
|
||||
.and_then(Path::to_str)
|
||||
.ok_or(miette!("Couldn't get path for file {:?}", file))?
|
||||
.to_string()),
|
||||
filename: Set(file
|
||||
.file_name()
|
||||
.to_str()
|
||||
.ok_or(miette!("Couldn't get filename for file {:?}", file))?
|
||||
.to_string()),
|
||||
source_id: Set(source.id),
|
||||
hash: Set(hash.try_into()?),
|
||||
artist: Set(tags
|
||||
.and_then(lofty::Accessor::artist)
|
||||
.map(|t| t.to_string())),
|
||||
album_artist: Set(tags
|
||||
.and_then(|t| t.get_string(&lofty::ItemKey::AlbumArtist))
|
||||
.map(|t| t.to_string())),
|
||||
name: Set(tags.and_then(lofty::Accessor::title).map(|t| t.to_string())),
|
||||
album: Set(tags.and_then(lofty::Accessor::album).map(|t| t.to_string())),
|
||||
genres: Set(tags.and_then(lofty::Accessor::genre).map(|t| t.to_string())),
|
||||
track: Set(tags.and_then(lofty::Accessor::track).map(|t| t as i32)),
|
||||
disc: Set(tags.and_then(lofty::Accessor::disk).map(|t| t as i32)),
|
||||
year: Set(tags.and_then(lofty::Accessor::year).map(|t| t as i32)),
|
||||
duration: Set(properties.duration().as_millis().try_into()?),
|
||||
rg_track_gain: Set(rg.as_ref().map(|v| f64::from(v.track_gain)).ok()),
|
||||
rg_track_peak: Set(rg.as_ref().map(|v| f64::from(v.track_peak)).ok()),
|
||||
rg_album_gain: Set(rg
|
||||
.as_ref()
|
||||
.map(|v| v.album_gain.map(f64::from))
|
||||
.ok()
|
||||
.flatten()),
|
||||
rg_album_peak: Set(rg
|
||||
.as_ref()
|
||||
.map(|v| v.album_peak.map(f64::from))
|
||||
.ok()
|
||||
.flatten()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(Some(song))
|
||||
}
|
||||
|
||||
pub async fn index_source(
|
||||
source: Source,
|
||||
force: bool,
|
||||
db: &DatabaseConnection,
|
||||
) -> Result<(), EleanorError> {
|
||||
// Get timestamp of last successful scan for current source, or fall back to
|
||||
// a timestamp that's unlikely to be encountered
|
||||
let indexed_ts = sources::Entity::find()
|
||||
.filter(sources::Column::Id.eq(source.id))
|
||||
.column(sources::Column::LastIndexed)
|
||||
.all(db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
.and_then(|v| v.last_indexed)
|
||||
.unwrap_or(String::from("0"))
|
||||
.parse::<i64>()?;
|
||||
|
||||
let indexed_ts = OffsetDateTime::from_unix_timestamp(indexed_ts).into_diagnostic()?;
|
||||
|
||||
let songs: Vec<library::ActiveModel> = WalkDir::new(&source.path)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<_>>()
|
||||
.par_iter()
|
||||
.filter(|e| !e.file_type().is_dir()) // Exclude directories
|
||||
.filter(|e| {
|
||||
mime_guess::from_path(e.path())
|
||||
.first()
|
||||
.is_some_and(|v| v.type_() == mime::AUDIO) // Exclude non-audio files
|
||||
})
|
||||
.map(|file| index_song(file, &source, force, indexed_ts))
|
||||
.collect::<Result<Vec<_>, EleanorError>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
// Write metadata to database
|
||||
library::Entity::insert_many(songs)
|
||||
.on_conflict(
|
||||
sea_query::OnConflict::column(Column::Hash)
|
||||
.update_columns([
|
||||
Column::Artist,
|
||||
Column::AlbumArtist,
|
||||
Column::Name,
|
||||
Column::Album,
|
||||
Column::Duration,
|
||||
Column::Genres,
|
||||
Column::Track,
|
||||
Column::Disc,
|
||||
Column::Year,
|
||||
Column::RgTrackGain,
|
||||
Column::RgTrackPeak,
|
||||
Column::RgAlbumGain,
|
||||
Column::RgAlbumPeak,
|
||||
])
|
||||
.clone(),
|
||||
)
|
||||
.on_empty_do_nothing()
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
// Update last indexed timestamp
|
||||
sources::Entity::insert(sources::ActiveModel {
|
||||
id: Set(source.id),
|
||||
last_indexed: Set(Some(
|
||||
time::OffsetDateTime::now_utc().unix_timestamp().to_string(),
|
||||
)),
|
||||
})
|
||||
.on_conflict(
|
||||
sea_query::OnConflict::column(sources::Column::Id)
|
||||
.update_column(sources::Column::LastIndexed)
|
||||
.clone(),
|
||||
)
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -29,6 +29,7 @@ impl MigrationTrait for Migration {
|
|||
.col(ColumnDef::new(Song::Duration).integer().not_null())
|
||||
.col(ColumnDef::new(Song::Genres).string())
|
||||
.col(ColumnDef::new(Song::Track).integer())
|
||||
.col(ColumnDef::new(Song::Disc).integer())
|
||||
.col(ColumnDef::new(Song::Year).integer())
|
||||
.col(ColumnDef::new(Song::RGTrackGain).double())
|
||||
.col(ColumnDef::new(Song::RGTrackPeak).double())
|
||||
|
@ -69,6 +70,7 @@ pub enum Song {
|
|||
Genres,
|
||||
/// Number of the track in the album
|
||||
Track,
|
||||
Disc,
|
||||
Year,
|
||||
RGTrackGain,
|
||||
RGTrackPeak,
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Source::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Source::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Source::LastIndexed).string())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Source::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
pub enum Source {
|
||||
#[iden = "sources"]
|
||||
Table,
|
||||
Id,
|
||||
LastIndexed,
|
||||
}
|
|
@ -3,6 +3,7 @@ use sea_orm_migration::prelude::*;
|
|||
mod m20220803_000001_create_library;
|
||||
mod m20220803_000001_create_playlist_entries;
|
||||
mod m20220803_000001_create_playlists;
|
||||
mod m20240223_185340_create_sources;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
@ -11,8 +12,9 @@ impl MigratorTrait for Migrator {
|
|||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20220803_000001_create_library::Migration),
|
||||
Box::new(m20220803_000001_create_playlists::Migration),
|
||||
Box::new(m20220803_000001_create_playlist_entries::Migration),
|
||||
Box::new(m20220803_000001_create_playlists::Migration),
|
||||
Box::new(m20240223_185340_create_sources::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod indexing;
|
||||
mod kdl_utils;
|
||||
pub mod logging;
|
||||
#[allow(clippy::pedantic)]
|
||||
mod migrator;
|
||||
#[allow(clippy::pedantic)]
|
||||
pub mod model;
|
||||
// pub mod playback;
|
||||
pub mod error;
|
||||
mod kdl_utils;
|
||||
pub mod logging;
|
||||
pub mod playback;
|
||||
pub mod replaygain;
|
||||
pub mod utils;
|
||||
|
||||
use std::fs::{create_dir_all, File};
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct Model {
|
|||
pub id: i32,
|
||||
pub path: String,
|
||||
pub filename: String,
|
||||
pub source_id: i32,
|
||||
pub source_id: u32,
|
||||
pub hash: u32,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
|
@ -18,6 +18,7 @@ pub struct Model {
|
|||
pub duration: u32,
|
||||
pub genres: Option<String>,
|
||||
pub track: Option<i32>,
|
||||
pub disc: Option<i32>,
|
||||
pub year: Option<i32>,
|
||||
#[sea_orm(column_type = "Double", nullable)]
|
||||
pub rg_track_gain: Option<f64>,
|
||||
|
|
|
@ -5,3 +5,4 @@ pub mod prelude;
|
|||
pub mod library;
|
||||
pub mod playlist_entries;
|
||||
pub mod playlists;
|
||||
pub mod sources;
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
pub use super::library::Entity as Library;
|
||||
pub use super::playlist_entries::Entity as PlaylistEntries;
|
||||
pub use super::playlists::Entity as Playlists;
|
||||
pub use super::sources::Entity as Sources;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "sources")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: u32,
|
||||
pub last_indexed: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
use miette::miette;
|
||||
|
||||
use symphonia::core::{audio::SampleBuffer, codecs::DecoderOptions, formats::FormatReader};
|
||||
|
||||
use super::error::EleanorError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplayGain {
|
||||
pub track_gain: f32,
|
||||
pub track_peak: f32,
|
||||
pub album_gain: Option<f32>,
|
||||
pub album_peak: Option<f32>,
|
||||
}
|
||||
|
||||
impl ReplayGain {
|
||||
pub(crate) fn try_calculate(audio: &mut Box<dyn FormatReader>) -> Result<Self, EleanorError> {
|
||||
let track = audio
|
||||
.default_track()
|
||||
.ok_or_else(|| miette!("No default track was found"))?;
|
||||
|
||||
let params = &track.codec_params;
|
||||
|
||||
let (sample_rate, channels) = (params.sample_rate, params.channels);
|
||||
let Some(sample_rate) = sample_rate else {
|
||||
return Err(miette!("Sample rate must be known").into());
|
||||
};
|
||||
|
||||
// Only stereo is supported.
|
||||
if !channels.is_some_and(|x| x.count() == 2) {
|
||||
return Err(miette!("Unsupported channel configuration: {:?}", channels).into());
|
||||
}
|
||||
|
||||
// Only 44.1kHz and 48kHz are supported.
|
||||
let Some(mut rg) = replaygain::ReplayGain::new(sample_rate as usize) else {
|
||||
return Err(miette!("Unsupported sample rate: {:?}", sample_rate).into());
|
||||
};
|
||||
|
||||
let mut decoder =
|
||||
symphonia::default::get_codecs().make(params, &DecoderOptions::default())?;
|
||||
|
||||
let track_id = track.id;
|
||||
|
||||
let mut samples: Vec<f32> = vec![];
|
||||
let mut sample_buf = None;
|
||||
loop {
|
||||
let packet = match audio.next_packet() {
|
||||
Ok(packet) => packet,
|
||||
Err(symphonia::core::errors::Error::IoError(ref packet_error))
|
||||
if packet_error.kind() == std::io::ErrorKind::UnexpectedEof =>
|
||||
{
|
||||
// End of audio stream
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Skip packets belonging to other audio tracks
|
||||
if packet.track_id() != track_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
match decoder.decode(&packet) {
|
||||
Ok(buffer) => {
|
||||
if sample_buf.is_none() {
|
||||
let spec = *buffer.spec();
|
||||
let duration = buffer.capacity() as u64;
|
||||
|
||||
sample_buf = Some(SampleBuffer::<f32>::new(duration, spec));
|
||||
}
|
||||
if let Some(target) = &mut sample_buf {
|
||||
target.copy_interleaved_ref(buffer);
|
||||
}
|
||||
}
|
||||
Err(symphonia::core::errors::Error::DecodeError(_)) => (),
|
||||
Err(_) => break,
|
||||
}
|
||||
|
||||
if let Some(buf) = &mut sample_buf {
|
||||
samples.extend(buf.samples());
|
||||
}
|
||||
}
|
||||
|
||||
if samples.is_empty() {
|
||||
return Err(miette!("No samples were decoded from input audio").into());
|
||||
};
|
||||
|
||||
rg.process_samples(&samples);
|
||||
|
||||
let (track_gain, track_peak) = rg.finish();
|
||||
|
||||
Ok(Self {
|
||||
track_gain,
|
||||
track_peak,
|
||||
album_gain: None,
|
||||
album_peak: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_gain(gain: &str) -> Result<f32, EleanorError> {
|
||||
gain.chars()
|
||||
.filter(|c| c.is_numeric() || matches!(*c, '-' | '+' | '.'))
|
||||
.collect::<String>()
|
||||
.parse::<f32>()
|
||||
.map_err(EleanorError::from)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rg_parse_expected() {
|
||||
for (input, expected) in [("-8.97 dB", -8.97), ("12.75 dB", 12.75), ("0.00 dB", 0.0)] {
|
||||
assert_eq!(format_gain(input).unwrap(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rg_parse_malformed() {
|
||||
for (input, expected) in [
|
||||
("-8.5712 dB", -8.5712),
|
||||
("+2.3 dB", 2.3),
|
||||
(" 13.12 dB", 13.12),
|
||||
("006.66 Db", 6.66),
|
||||
("0.93dB", 0.93),
|
||||
("24.12 deutscheBahn", 24.12),
|
||||
] {
|
||||
assert_eq!(format_gain(input).unwrap(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rg_parse_invalid() {
|
||||
for input in ["", "akjdfnhlkjfh", "🥺", " DB "] {
|
||||
assert!(format_gain(input).is_err())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue