Compare commits

..

No commits in common. "86a4bd9762d955bbd3de965826a6ecb2af749c73" and "f9ade790274c5a2d0c4c6d3fb992bf6195adfc6f" have entirely different histories.

20 changed files with 317 additions and 935 deletions

269
Cargo.lock generated
View file

@ -76,9 +76,9 @@ dependencies = [
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.4" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@ -90,15 +90,15 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.4" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
version = "0.2.2" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [ dependencies = [
"utf8parse", "utf8parse",
] ]
@ -114,9 +114,9 @@ dependencies = [
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.1" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"windows-sys 0.48.0", "windows-sys 0.48.0",
@ -161,7 +161,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -172,7 +172,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -264,9 +264,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "3.4.0" version = "3.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@ -275,9 +275,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli-decompressor" name = "brotli-decompressor"
version = "2.5.0" version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@ -291,9 +291,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "bytes" name = "bytes"
@ -333,9 +333,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.4.6" version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -343,9 +343,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.4.6" version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -362,7 +362,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -527,7 +527,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -549,7 +549,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [ dependencies = [
"darling_core 0.20.3", "darling_core 0.20.3",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -629,7 +629,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -640,9 +640,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.4" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [ dependencies = [
"errno-dragonfly", "errno-dragonfly",
"libc", "libc",
@ -688,9 +688,9 @@ dependencies = [
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.0.1" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]] [[package]]
name = "finl_unicode" name = "finl_unicode"
@ -710,12 +710,13 @@ dependencies = [
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.0" version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"pin-project",
"spin 0.9.8", "spin 0.9.8",
] ]
@ -816,7 +817,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -909,9 +910,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.1" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
dependencies = [ dependencies = [
"ahash", "ahash",
"allocator-api2", "allocator-api2",
@ -923,7 +924,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [ dependencies = [
"hashbrown 0.14.1", "hashbrown 0.14.0",
] ]
[[package]] [[package]]
@ -1116,19 +1117,19 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.0.2" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.1", "hashbrown 0.14.0",
] ]
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.33.0" version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa511b2e298cd49b1856746f6bb73e17036bcd66b25f5e92cdcdbec9bd75686" checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea"
dependencies = [ dependencies = [
"console", "console",
"lazy_static", "lazy_static",
@ -1233,9 +1234,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.8" version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -1266,19 +1267,18 @@ dependencies = [
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca"
dependencies = [ dependencies = [
"cfg-if",
"digest", "digest",
] ]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.6.4" version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]] [[package]]
name = "mime" name = "mime"
@ -1455,7 +1455,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -1560,9 +1560,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33"
dependencies = [ dependencies = [
"memchr", "memchr",
"thiserror", "thiserror",
@ -1571,9 +1571,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_derive" name = "pest_derive"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a"
dependencies = [ dependencies = [
"pest", "pest",
"pest_generator", "pest_generator",
@ -1581,22 +1581,22 @@ dependencies = [
[[package]] [[package]]
name = "pest_generator" name = "pest_generator"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141"
dependencies = [ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
name = "pest_meta" name = "pest_meta"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"pest", "pest",
@ -1633,7 +1633,7 @@ dependencies = [
"phf_shared", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -1645,6 +1645,26 @@ dependencies = [
"siphasher 0.3.11", "siphasher 0.3.11",
] ]
[[package]]
name = "pin-project"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.13" version = "0.2.13"
@ -1727,7 +1747,7 @@ checksum = "f69f8d22fa3f34f3083d9a4375c038732c7a7e964de1beb81c544da92dfc40b8"
dependencies = [ dependencies = [
"ahash", "ahash",
"equivalent", "equivalent",
"hashbrown 0.14.1", "hashbrown 0.14.0",
"parking_lot", "parking_lot",
] ]
@ -1787,9 +1807,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.9.6" version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1799,9 +1819,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.3.9" version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1822,9 +1842,9 @@ checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.22" version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"base64 0.21.4", "base64 0.21.4",
@ -1851,7 +1871,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls", "tokio-rustls",
@ -2005,7 +2024,7 @@ dependencies = [
"regex", "regex",
"relative-path", "relative-path",
"rustc_version", "rustc_version",
"syn 2.0.38", "syn 2.0.37",
"unicode-ident", "unicode-ident",
] ]
@ -2026,9 +2045,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.17" version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"errno", "errno",
@ -2077,7 +2096,7 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]] [[package]]
name = "rustypipe" name = "rustypipe"
version = "0.1.0" version = "0.1.0"
source = "git+https://code.thetadev.de/ThetaDev/rustypipe.git#e247b0c5d9874313d8a350142694bc90db805a11" source = "git+https://code.thetadev.de/ThetaDev/rustypipe.git#d6de428549bfe7881e5ac52857d4bc1fa2db7f5d"
dependencies = [ dependencies = [
"base64 0.21.4", "base64 0.21.4",
"fancy-regex", "fancy-regex",
@ -2167,9 +2186,9 @@ dependencies = [
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.19" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]] [[package]]
name = "serde" name = "serde"
@ -2188,7 +2207,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -2256,7 +2275,7 @@ dependencies = [
"darling 0.20.3", "darling 0.20.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -2272,9 +2291,9 @@ dependencies = [
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.8" version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
@ -2283,9 +2302,9 @@ dependencies = [
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
] ]
@ -2341,7 +2360,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -2402,9 +2421,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx" name = "sqlx"
version = "0.7.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721"
dependencies = [ dependencies = [
"sqlx-core", "sqlx-core",
"sqlx-macros", "sqlx-macros",
@ -2415,9 +2434,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-core" name = "sqlx-core"
version = "0.7.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53"
dependencies = [ dependencies = [
"ahash", "ahash",
"atoi", "atoi",
@ -2435,7 +2454,7 @@ dependencies = [
"futures-util", "futures-util",
"hashlink", "hashlink",
"hex", "hex",
"indexmap 2.0.2", "indexmap 2.0.0",
"log", "log",
"memchr", "memchr",
"once_cell", "once_cell",
@ -2483,9 +2502,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-macros" name = "sqlx-macros"
version = "0.7.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2496,9 +2515,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-macros-core" name = "sqlx-macros-core"
version = "0.7.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"either", "either",
@ -2522,9 +2541,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-mysql" name = "sqlx-mysql"
version = "0.7.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.21.4", "base64 0.21.4",
@ -2566,9 +2585,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-postgres" name = "sqlx-postgres"
version = "0.7.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.21.4", "base64 0.21.4",
@ -2607,9 +2626,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-sqlite" name = "sqlx-sqlite"
version = "0.7.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2"
dependencies = [ dependencies = [
"atoi", "atoi",
"flume", "flume",
@ -2665,7 +2684,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -2687,36 +2706,15 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.38" version = "2.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.8.0" version = "3.8.0"
@ -2745,22 +2743,22 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.49" version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.49" version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -2775,9 +2773,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.29" version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
@ -2788,15 +2786,15 @@ dependencies = [
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.2" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.15" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
dependencies = [ dependencies = [
"time-core", "time-core",
] ]
@ -2862,7 +2860,6 @@ dependencies = [
"rspotify", "rspotify",
"rstest", "rstest",
"rustypipe", "rustypipe",
"serde_plain",
"siphasher 1.0.0", "siphasher 1.0.0",
"sqlx", "sqlx",
"sqlx-database-tester", "sqlx-database-tester",
@ -2915,7 +2912,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -3002,7 +2999,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [ dependencies = [
"indexmap 2.0.2", "indexmap 2.0.0",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
@ -3036,7 +3033,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -3234,7 +3231,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3268,7 +3265,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.37",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3484,9 +3481,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.16" version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -146,10 +146,6 @@ impl Album {
Id::Db(self.id) Id::Db(self.id)
} }
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError> pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,

View file

@ -99,10 +99,6 @@ impl Artist {
Id::Db(self.id) Id::Db(self.id)
} }
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError> pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,

View file

@ -102,10 +102,6 @@ impl Playlist {
Id::Db(self.id) Id::Db(self.id)
} }
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError> pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
@ -635,10 +631,6 @@ impl PlaylistUpdate<'_> {
} }
impl PlaylistSlim { impl PlaylistSlim {
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError> pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,

View file

@ -138,10 +138,6 @@ impl Track {
Id::Db(self.id) Id::Db(self.id)
} }
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError> pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
@ -515,10 +511,6 @@ impl TrackUpdate<'_> {
} }
impl TrackSlim { impl TrackSlim {
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError> pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,

View file

@ -55,10 +55,6 @@ impl User {
Id::Db(self.id) Id::Db(self.id)
} }
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError> pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
@ -320,12 +316,6 @@ impl UserUpdate<'_> {
} }
} }
impl UserItem {
pub fn src_id(&self) -> SrcId {
SrcId(&self.src_id, self.service)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -21,7 +21,6 @@ quick_cache.workspace = true
regex.workspace = true regex.workspace = true
rspotify.workspace = true rspotify.workspace = true
rustypipe.workspace = true rustypipe.workspace = true
serde_plain.workspace = true
siphasher.workspace = true siphasher.workspace = true
sqlx.workspace = true sqlx.workspace = true
strsim.workspace = true strsim.workspace = true

View file

@ -17,8 +17,6 @@ pub enum ExtractorError {
}, },
#[error("could not find a match for track [{id}]")] #[error("could not find a match for track [{id}]")]
NoMatch { id: SrcIdOwned }, NoMatch { id: SrcIdOwned },
#[error("no id found")]
NoId,
#[error("{0}")] #[error("{0}")]
Other(Cow<'static, str>), Other(Cow<'static, str>),
} }

View file

@ -1,4 +1,5 @@
#![warn(clippy::dbg_macro, clippy::todo)] #![warn(clippy::dbg_macro, clippy::todo)]
#![allow(dead_code)]
pub mod error; pub mod error;
mod model; mod model;
@ -7,7 +8,6 @@ mod util;
pub use model::GetStatus; pub use model::GetStatus;
use services::SpotifyExtractor; use services::SpotifyExtractor;
use tiraya_utils::config::CONFIG;
use std::{collections::HashSet, sync::Arc}; use std::{collections::HashSet, sync::Arc};
@ -28,6 +28,13 @@ use crate::{
services::YouTubeExtractor, services::YouTubeExtractor,
}; };
const ARTIST_STALE: Duration = Duration::hours(24);
const PLAYLIST_STALE: Duration = Duration::hours(24);
const USER_STALE: Duration = Duration::hours(24);
pub(crate) const CONCURRENCY: usize = 4;
pub(crate) const DB_CONCURRENCY: usize = 8;
pub(crate) const ITEM_LIMIT: usize = 2000;
pub struct Extractor(Arc<ExtractorInner>); pub struct Extractor(Arc<ExtractorInner>);
pub struct ExtractorInner { pub struct ExtractorInner {
@ -50,11 +57,7 @@ impl Extractor {
let yt = YouTubeExtractor::new(core.clone())?; let yt = YouTubeExtractor::new(core.clone())?;
Ok(Self( Ok(Self(
ExtractorInner { ExtractorInner {
sp: if CONFIG.extractor.spotify.enable { sp: SpotifyExtractor::new(core.clone(), yt.clone())?,
Some(SpotifyExtractor::new(core.clone(), yt.clone())?)
} else {
None
},
yt, yt,
core, core,
} }
@ -68,12 +71,6 @@ impl Extractor {
.await .await
.to_optional() .to_optional()
.unwrap(); .unwrap();
// Get srcid from fetched artist if available (could be an alias)
let srcid = artist
.as_ref()
.map(|a| a.src_id().to_owned())
.unwrap_or_else(|| id.to_owned());
let last_update = if let Some(artist) = artist { let last_update = if let Some(artist) = artist {
self.core.artist_cache.insert(id.to_owned(), artist.id); self.core.artist_cache.insert(id.to_owned(), artist.id);
@ -83,7 +80,7 @@ impl Extractor {
let age = OffsetDateTime::now_utc() - last_sync_at.assume_utc(); let age = OffsetDateTime::now_utc() - last_sync_at.assume_utc();
// Check if artist in DB is still fresh // Check if artist in DB is still fresh
if age < Duration::hours(CONFIG.extractor.artist_stale_h.into()) { if age < ARTIST_STALE {
return Ok(GetResult::stored(artist)); return Ok(GetResult::stored(artist));
} else { } else {
match last_sync_data { match last_sync_data {
@ -115,8 +112,7 @@ impl Extractor {
None None
}; };
self.update_artist(srcid.as_srcid(), last_update.as_ref()) self.update_artist(id, last_update.as_ref()).await
.await
} }
async fn update_artist( async fn update_artist(
@ -126,7 +122,6 @@ impl Extractor {
) -> Result<GetResult<Artist>, ExtractorError> { ) -> Result<GetResult<Artist>, ExtractorError> {
let res = match id.1 { let res = match id.1 {
MusicService::YouTube => self.yt.update_artist(id.0, last_update).await, MusicService::YouTube => self.yt.update_artist(id.0, last_update).await,
MusicService::Spotify => self.sp()?.update_artist(id.0).await,
_ => { _ => {
return Err(ExtractorError::Unsupported { return Err(ExtractorError::Unsupported {
typ: EntityType::Artist, typ: EntityType::Artist,
@ -177,7 +172,7 @@ impl Extractor {
} }
} }
}) })
.buffer_unordered(CONFIG.core.db_concurrency) .buffer_unordered(DB_CONCURRENCY)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await; .await;
@ -216,7 +211,6 @@ impl Extractor {
let res = match id.1 { let res = match id.1 {
MusicService::YouTube => self.yt.fetch_album(id.0).await?, MusicService::YouTube => self.yt.fetch_album(id.0).await?,
MusicService::Spotify => self.sp()?.fetch_album(id.0).await?,
_ => { _ => {
return Err(ExtractorError::Unsupported { return Err(ExtractorError::Unsupported {
typ: EntityType::Album, typ: EntityType::Album,
@ -271,7 +265,7 @@ impl Extractor {
let age = OffsetDateTime::now_utc() - last_sync_at.assume_utc(); let age = OffsetDateTime::now_utc() - last_sync_at.assume_utc();
// Check if playlist< in DB is still fresh // Check if playlist< in DB is still fresh
if age < Duration::hours(CONFIG.extractor.playlist_stale_h.into()) { if age < PLAYLIST_STALE {
return Ok(GetResult::stored(playlist)); return Ok(GetResult::stored(playlist));
} else { } else {
match last_sync_data { match last_sync_data {
@ -370,8 +364,8 @@ impl Extractor {
{ {
let age = OffsetDateTime::now_utc() - last_sync_at.assume_utc(); let age = OffsetDateTime::now_utc() - last_sync_at.assume_utc();
// Check if playlist in DB is still fresh // Check if playlist< in DB is still fresh
if age < Duration::hours(CONFIG.extractor.user_stale_h.into()) { if age < USER_STALE {
return Ok(GetResult::stored(user)); return Ok(GetResult::stored(user));
} else { } else {
match last_sync_data { match last_sync_data {
@ -416,7 +410,6 @@ impl Extractor {
) -> Result<GetResult<User>, ExtractorError> { ) -> Result<GetResult<User>, ExtractorError> {
let res = match id.1 { let res = match id.1 {
MusicService::YouTube => self.yt.update_user(id.0, last_update).await, MusicService::YouTube => self.yt.update_user(id.0, last_update).await,
MusicService::Spotify => self.sp()?.update_user(id.0, last_update).await,
_ => { _ => {
return Err(ExtractorError::Unsupported { return Err(ExtractorError::Unsupported {
typ: EntityType::User, typ: EntityType::User,

View file

@ -8,12 +8,6 @@ use crate::{error::ExtractorError, model::ExtractorCore};
/* /*
/// Tiraya extractor implementation for a specific music streaming service /// Tiraya extractor implementation for a specific music streaming service
///
/// Terminology for the individual methods:
/// - **update** Check if an item was updated on the source, download and import the
/// changes if necessary
/// - **fetch** Download and import the item from source if it is not present in the database
/// - **import** Take a data object from a service and store it in the database
pub(crate) trait ServiceExtractor { pub(crate) trait ServiceExtractor {
/// Create a new instance of the service extractor /// Create a new instance of the service extractor
fn new(core: Arc<ExtractorCore>) -> Result<Self, ExtractorError>; fn new(core: Arc<ExtractorCore>) -> Result<Self, ExtractorError>;

View file

@ -1,25 +1,21 @@
use futures::{stream::TryStreamExt, StreamExt}; use futures::stream::{StreamExt, TryStreamExt};
use path_macro::path; use path_macro::path;
use rspotify::{ use rspotify::{
model::{ model::{FullTrack, PlayableItem, PlaylistId, PublicUser, TrackId},
AlbumId, ArtistId, FullTrack, Market, PlayableItem, PlaylistId, PublicUser,
SimplifiedPlaylist, TrackId, UserId,
},
prelude::{BaseClient, Id}, prelude::{BaseClient, Id},
ClientCredsSpotify, ClientCredsSpotify,
}; };
use std::{collections::HashSet, sync::Arc}; use std::{collections::HashSet, sync::Arc};
use time::OffsetDateTime; use time::OffsetDateTime;
use tiraya_db::models::{ use tiraya_db::models::{
Artist, MusicService, Playlist, PlaylistEntry, PlaylistImgType, PlaylistNew, PlaylistType, MusicService, Playlist, PlaylistEntry, PlaylistImgType, PlaylistNew, PlaylistType, SrcId,
SrcId, SrcIdOwned, Track, User, UserNew, UserType, SrcIdOwned,
}; };
use tiraya_utils::config::CONFIG; use tiraya_utils::config::CONFIG;
use crate::{ use crate::{
error::ExtractorError, error::ExtractorError,
model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate}, model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate},
util::ArtistIdName,
}; };
use super::YouTubeExtractor; use super::YouTubeExtractor;
@ -29,41 +25,32 @@ pub struct SpotifyExtractor {
core: Arc<ExtractorCore>, core: Arc<ExtractorCore>,
sp: ClientCredsSpotify, sp: ClientCredsSpotify,
yt: YouTubeExtractor, yt: YouTubeExtractor,
market: Market,
} }
const PAGE_LIMIT: u32 = 50;
impl SpotifyExtractor { impl SpotifyExtractor {
pub fn new(core: Arc<ExtractorCore>, yt: YouTubeExtractor) -> Result<Self, ExtractorError> { pub fn new(
let sp = ClientCredsSpotify::with_config( core: Arc<ExtractorCore>,
rspotify::Credentials { yt: YouTubeExtractor,
id: CONFIG.extractor.spotify.client_id.to_owned(), ) -> Result<Option<Self>, ExtractorError> {
secret: Some(CONFIG.extractor.spotify.client_secret.to_owned()), if let Some(cfg) = &CONFIG.extractor.spotify {
}, let sp = ClientCredsSpotify::with_config(
rspotify::Config { rspotify::Credentials {
cache_path: path!(CONFIG.extractor.data_dir / "spotify_token_cache.json"), id: cfg.client_id.to_owned(),
token_cached: true, secret: Some(cfg.client_secret.to_owned()),
pagination_chunks: PAGE_LIMIT, },
..Default::default() rspotify::Config {
}, cache_path: path!(CONFIG.extractor.data_dir / "spotify_token_cache.json"),
); token_cached: true,
..Default::default()
},
);
let market_str = &CONFIG.extractor.spotify.market; Ok(Some(Self { core, sp, yt }))
let market = Market::Country(serde_plain::from_str(market_str).map_err(|_| { } else {
ExtractorError::Other(format!("invalid spotify market `{market_str}`").into()) Ok(None)
})?); }
Ok(Self {
core,
sp,
yt,
market,
})
} }
/// Since Rspotify does not initialize the token by itself it needs to be loaded
/// before making a request
async fn load_token(&self) -> Result<(), ExtractorError> { async fn load_token(&self) -> Result<(), ExtractorError> {
let mut token = self.sp.token.lock().await.unwrap(); let mut token = self.sp.token.lock().await.unwrap();
if token.is_none() { if token.is_none() {
@ -80,93 +67,14 @@ impl SpotifyExtractor {
Ok(()) Ok(())
} }
pub async fn update_artist(&self, src_id: &str) -> Result<GetResult<i32>, ExtractorError> {
// Info: since we are matching Spotify artists with YTM artists, this function
// will only be called on the initial request of the artist, not on updates
// (hence no last_update parameter).
self.load_token().await?;
let artist_id = ArtistId::from_id(src_id)?;
let mut top_tracks = self
.sp
.artist_top_tracks(artist_id, Some(self.market))
.await?;
let mut get_track = || -> Result<FullTrack, ExtractorError> {
let i = top_tracks
.iter()
.enumerate()
.find(|(_, t)| {
t.artists.len() == 1
&& t.artists[0]
.id
.as_ref()
.map(|id| id.id() == src_id)
.unwrap_or_default()
})
.or_else(|| {
top_tracks.iter().enumerate().find(|(_, t)| {
t.artists.iter().any(|a| {
a.id.as_ref()
.map(|id| id.id() == src_id)
.unwrap_or_default()
})
})
})
.ok_or_else(|| ExtractorError::InvalidData {
id: SrcIdOwned(src_id.to_owned(), MusicService::Spotify),
msg: "no top tracks found".into(),
})?
.0;
Ok(top_tracks.remove(i))
};
let a_id = SrcId(src_id, MusicService::Spotify);
for _ in 0..3 {
let track = get_track()?;
match self.import_track(track).await {
Ok(_) => {
if let Some(a_id) = Artist::get_id(a_id, &self.core.db).await? {
let src_id = Artist::get_src_id(a_id, &self.core.db)
.await?
.ok_or(ExtractorError::NoId)?;
if src_id.1 != MusicService::YouTube {
return Err(ExtractorError::Other("artist id not from yt".into()));
}
return self.yt.update_artist(&src_id.0, None).await;
}
}
Err(ExtractorError::NoMatch { .. }) => {}
Err(e) => return Err(e),
}
}
Err(ExtractorError::NoMatch {
id: a_id.to_owned(),
})
}
pub async fn fetch_album(&self, src_id: &str) -> Result<(i32, bool), ExtractorError> {
self.load_token().await?;
let album = self.sp.album(AlbumId::from_id(src_id)?, None).await?;
let artist = album
.artists
.into_iter()
.next()
.map(|artist| artist.name)
.unwrap_or_default();
let album_id = self
.yt
.match_album(SrcId(src_id, MusicService::Spotify), &album.name, &artist)
.await?;
Ok((album_id, false))
}
pub async fn fetch_track(&self, src_id: &str) -> Result<i32, ExtractorError> { pub async fn fetch_track(&self, src_id: &str) -> Result<i32, ExtractorError> {
self.load_token().await?; self.load_token().await?;
let track = self.sp.track(TrackId::from_id(src_id)?, None).await?; let track = self.sp.track(TrackId::from_id(src_id)?, None).await?;
self.import_track(track).await self.import_track(track)
.await?
.ok_or_else(|| ExtractorError::NoMatch {
id: SrcIdOwned(src_id.to_owned(), MusicService::Spotify),
})
} }
pub async fn update_playlist( pub async fn update_playlist(
@ -193,7 +101,7 @@ impl SpotifyExtractor {
.into_iter() .into_iter()
.max_by_key(|img| img.height.unwrap_or_default()) .max_by_key(|img| img.height.unwrap_or_default())
.map(|img| img.url); .map(|img| img.url);
let owner_id = self.import_user(playlist.owner).await?; let owner_id = self.import_user(&playlist.owner).await?;
let n_playlist = PlaylistNew { let n_playlist = PlaylistNew {
src_id, src_id,
@ -222,7 +130,6 @@ impl SpotifyExtractor {
None None
}) })
}) })
.take(CONFIG.extractor.playlist_item_limit_matchmaker)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await?; .await?;
@ -262,167 +169,32 @@ impl SpotifyExtractor {
Ok(GetResult::fetched(playlist_id)) Ok(GetResult::fetched(playlist_id))
} }
pub async fn update_user( async fn import_user(&self, user: &PublicUser) -> Result<i32, ExtractorError> {
&self,
src_id: &str,
last_update: Option<&SyncLastUpdate>,
) -> Result<GetResult<i32>, ExtractorError> {
self.load_token().await?;
let sp_user_id = UserId::from_id(src_id)?;
// First page of playlists
let playlists_p1 = self
.sp
.user_playlists_manual(sp_user_id.clone_static(), Some(PAGE_LIMIT), Some(0))
.await?;
let current_state = playlists_p1
.items
.first()
.map(|pl| LastUpdateState::Version(pl.id.id().to_owned()))
.unwrap_or_default();
if let Some(last_update) = last_update {
if last_update.state.is_current(&current_state) {
User::set_last_sync(last_update.id, current_state.into(), &self.core.db).await?;
return Ok(GetResult::stored(last_update.id));
}
}
let user = self.sp.user(sp_user_id.clone_static()).await?;
let user_id = self.import_user(user).await?;
self.import_playlists(playlists_p1.items).await?;
if playlists_p1.next.is_some() {
let mut offset = PAGE_LIMIT;
while offset < CONFIG.extractor.user_playlist_limit as u32 {
let playlists = self
.sp
.user_playlists_manual(
sp_user_id.clone_static(),
Some(PAGE_LIMIT),
Some(offset),
)
.await?;
self.import_playlists(playlists.items).await?;
if playlists.next.is_none() {
break;
}
offset += PAGE_LIMIT;
}
}
User::set_last_sync(user_id, current_state.into(), &self.core.db).await?;
Ok(GetResult::fetched(user_id))
}
async fn import_user(&self, user: PublicUser) -> Result<i32, ExtractorError> {
let id_owned = SrcIdOwned(user.id.id().to_owned(), MusicService::Spotify);
let image_url = user
.images
.into_iter()
.max_by_key(|img| img.height.unwrap_or_default())
.map(|img| img.url);
let name = user.display_name.as_deref().unwrap_or(user.id.id());
self.core self.core
.user_cache .import_user_id(
.get_or_insert_async(&id_owned, async { SrcId(user.id.id(), MusicService::Spotify),
let user = UserNew { user.display_name.as_deref().unwrap_or(user.id.id()),
src_id: &id_owned.0, false,
service: id_owned.1, )
name,
user_type: UserType::Remote,
image_url: image_url.as_deref(),
..Default::default()
};
let user_id = user.upsert(&self.core.db).await?;
tracing::debug!("imported user id [{}] {}", id_owned, name);
Ok(user_id)
})
.await .await
} }
async fn import_playlist(&self, playlist: SimplifiedPlaylist) -> Result<i32, ExtractorError> { async fn import_track(&self, track: FullTrack) -> Result<Option<i32>, ExtractorError> {
if let Some(id) = Playlist::get_id(
SrcId(playlist.id.id(), MusicService::Spotify),
&self.core.db,
)
.await?
{
return Ok(id);
}
let image_url = playlist
.images
.into_iter()
.max_by_key(|img| img.height.unwrap_or_default())
.map(|img| img.url);
let owner_id = self.import_user(playlist.owner).await?;
let playlist_n = PlaylistNew {
src_id: playlist.id.id(),
service: MusicService::Spotify,
name: &playlist.name,
owner_id: Some(owner_id),
playlist_type: PlaylistType::Remote,
image_url: image_url.as_deref(),
image_type: Some(PlaylistImgType::Custom).filter(|_| image_url.is_some()),
..Default::default()
};
let playlist_id = playlist_n.upsert(&self.core.db).await?;
tracing::debug!(
"imported playlist item [sp:{}] {}",
playlist.id.id(),
playlist.name
);
Ok(playlist_id)
}
async fn import_playlists(
&self,
playlists: impl IntoIterator<Item = SimplifiedPlaylist>,
) -> Result<(), ExtractorError> {
futures::stream::iter(playlists.into_iter().map(Ok))
.try_for_each_concurrent(CONFIG.core.db_concurrency, |pl| async {
self.import_playlist(pl).await?;
Ok(())
})
.await
}
async fn import_track(&self, track: FullTrack) -> Result<i32, ExtractorError> {
let src_id = SrcId(
track
.id
.as_ref()
.ok_or(ExtractorError::Other("no Spotify track id".into()))?
.id(),
MusicService::Spotify,
);
if let Some(track_id) = Track::get_id(src_id, &self.core.db).await? {
return Ok(track_id);
}
let artists = track
.artists
.into_iter()
.filter_map(|a| {
a.id.as_ref().map(|a_id| ArtistIdName {
id: a_id.id().to_owned(),
name: a.name,
})
})
.collect::<Vec<_>>();
self.yt self.yt
.match_track( .match_track(
src_id, SrcId(
track
.id
.ok_or(ExtractorError::Other("no Spotify track id".into()))?
.id(),
MusicService::Spotify,
),
&track.name, &track.name,
&artists, track
.artists
.first()
.map(|a| a.name.as_str())
.unwrap_or_default(),
track.external_ids.get("isrc").map(|s| s.as_str()), track.external_ids.get("isrc").map(|s| s.as_str()),
) )
.await .await
@ -431,17 +203,11 @@ impl SpotifyExtractor {
async fn import_tracks( async fn import_tracks(
&self, &self,
tracks: impl IntoIterator<Item = FullTrack>, tracks: impl IntoIterator<Item = FullTrack>,
) -> Result<(), ExtractorError> { ) -> Result<Vec<Option<i32>>, ExtractorError> {
futures::stream::iter(tracks.into_iter().map(Ok)) futures::stream::iter(tracks)
.try_for_each_concurrent(CONFIG.extractor.youtube.concurrency, |track| async move { .map(|track| async { self.import_track(track).await })
match self.import_track(track).await { .buffered(CONFIG.extractor.youtube.concurrency)
Ok(_) => Ok(()), .try_collect()
Err(e) => match e {
ExtractorError::NoMatch { .. } => Ok(()),
_ => Err(e),
},
}
})
.await .await
} }
} }

View file

@ -3,29 +3,29 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use path_macro::path;
use rustypipe::model as rpmodel; use rustypipe::model as rpmodel;
use rustypipe::{ use rustypipe::param::search_filter::SearchFilter;
client::RustyPipe, model::richtext::ToPlaintext, param::search_filter::SearchFilter, use rustypipe::report::FileReporter;
report::FileReporter, use rustypipe::{client::RustyPipe, model::richtext::ToPlaintext};
};
use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; use time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
use tiraya_db::models::{PlaylistEntry, User};
use tiraya_db::{ use tiraya_db::{
error::OptionalRes, error::OptionalRes,
models::{ models::{
Album, AlbumNew, AlbumType, AlbumUpdate, Artist, ArtistId, ArtistNew, DatePrecision, Id, Album, AlbumNew, AlbumType, AlbumUpdate, Artist, ArtistNew, DatePrecision, Id,
MusicService, Playlist, PlaylistEntry, PlaylistImgType, PlaylistNew, PlaylistType, SrcId, MusicService, Playlist, PlaylistImgType, PlaylistNew, PlaylistType, SrcId, SrcIdOwned,
SrcIdOwned, Track, TrackNew, TrackUpdate, User, UserNew, UserType, Track, TrackNew, TrackUpdate, UserNew, UserType,
}, },
}; };
use tiraya_utils::config::CONFIG; use tiraya_utils::config::CONFIG;
use crate::util::ArtistSrcidName; use crate::util::ImgFormat;
use crate::ITEM_LIMIT;
use crate::{ use crate::{
error::ExtractorError, error::ExtractorError,
model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate}, model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate},
util::{self, ArtistIdName, ImgFormat}, util::{self, ArtistIdName},
GetStatus, CONCURRENCY, DB_CONCURRENCY,
}; };
const YTM_CHANNEL_SUFFIX: &str = " - Topic"; const YTM_CHANNEL_SUFFIX: &str = " - Topic";
@ -50,9 +50,7 @@ impl YouTubeExtractor {
&CONFIG.extractor.youtube.country, &CONFIG.extractor.youtube.country,
)?) )?)
.storage_dir(&CONFIG.extractor.data_dir) .storage_dir(&CONFIG.extractor.data_dir)
.reporter(Box::new(FileReporter::new(path!( .reporter(Box::new(FileReporter::new(&CONFIG.extractor.data_dir)))
CONFIG.extractor.data_dir / "rustypipe_reports"
))))
.n_http_retries(CONFIG.extractor.youtube.n_retries) .n_http_retries(CONFIG.extractor.youtube.n_retries)
.build()?, .build()?,
}) })
@ -127,8 +125,8 @@ impl YouTubeExtractor {
// Insert all albums // Insert all albums
futures::stream::iter(artist.albums) futures::stream::iter(artist.albums)
.map(Ok) .map(Ok)
.try_for_each_concurrent(CONFIG.core.db_concurrency, |album| async { .try_for_each_concurrent(DB_CONCURRENCY, |album| async {
self.import_album_item(album).await.map(|_| ()) self.import_album_item(album).await
}) })
.await?; .await?;
@ -202,8 +200,8 @@ impl YouTubeExtractor {
// Insert album variants# // Insert album variants#
let has_variants = !album.variants.is_empty(); let has_variants = !album.variants.is_empty();
futures::stream::iter(album.variants.into_iter().map(Ok::<_, ExtractorError>)) futures::stream::iter(album.variants.into_iter().map(Ok::<_, ExtractorError>))
.try_for_each_concurrent(CONFIG.core.db_concurrency, |album| async { .try_for_each_concurrent(DB_CONCURRENCY, |album| async {
self.import_album_item(album).await.map(|_| ()) self.import_album_item(album).await
}) })
.await?; .await?;
@ -223,39 +221,12 @@ impl YouTubeExtractor {
} }
pub async fn fetch_track(&self, src_id: &str) -> Result<i32, ExtractorError> { pub async fn fetch_track(&self, src_id: &str) -> Result<i32, ExtractorError> {
self._fetch_track(src_id, true).await
}
async fn _fetch_track(&self, src_id: &str, with_details: bool) -> Result<i32, ExtractorError> {
let query = self.rp.query(); let query = self.rp.query();
let (track, details) = if with_details { let (track, details) =
let (track, details) = tokio::join!(query.music_details(src_id), self.get_track_details(src_id));
tokio::join!(query.music_details(src_id), self.get_track_details(src_id)); let track = track?.track;
(track?.track, details) self.import_track_item(track, None, &[], details, false)
} else { .await
(query.music_details(src_id).await?.track, None)
};
let redirected_id = if track.id != src_id {
Some(track.id.clone())
} else {
None
};
let track_id = self
.import_track_item(track, None, &[], details, false)
.await?;
if let Some(redirected_id) = redirected_id {
tracing::info!(
"track [yt:{src_id}] got redirected to [yt:{redirected_id}], added alias"
);
Track::add_alias(
track_id,
SrcId(&redirected_id, MusicService::YouTube),
&self.core.db,
)
.await?;
}
Ok(track_id)
} }
pub async fn update_playlist( pub async fn update_playlist(
@ -302,9 +273,7 @@ impl YouTubeExtractor {
let playlist_id = n_playlist.upsert(&self.core.db).await?; let playlist_id = n_playlist.upsert(&self.core.db).await?;
let mut m_tracks = m_playlist.tracks; let mut m_tracks = m_playlist.tracks;
m_tracks m_tracks.extend_limit(&query, ITEM_LIMIT).await?;
.extend_limit(&query, CONFIG.extractor.playlist_item_limit)
.await?;
let new_entries = m_tracks let new_entries = m_tracks
.items .items
@ -377,7 +346,7 @@ impl YouTubeExtractor {
channel channel
.content .content
.extend_limit(self.rp.query(), CONFIG.extractor.user_playlist_limit) .extend_limit(self.rp.query(), ITEM_LIMIT)
.await?; .await?;
let image_url = util::yt_image_url(&channel.avatar, ImgFormat::Avatar); let image_url = util::yt_image_url(&channel.avatar, ImgFormat::Avatar);
@ -412,40 +381,38 @@ impl YouTubeExtractor {
pub async fn match_track( pub async fn match_track(
&self, &self,
src_id: SrcId<'_>, src_id: SrcId<'_>,
name: &str, title: &str,
artists: &[ArtistIdName], artist: &str,
isrc: Option<&str>, isrc: Option<&str>,
) -> Result<i32, ExtractorError> { ) -> Result<Option<i32>, ExtractorError> {
let track_id = self.get_matching_track(src_id, name, artists, isrc).await?; if let Some(track_id) = Track::get_id(src_id, &self.core.db).await? {
return Ok(Some(track_id));
Track::add_alias(track_id, src_id, &self.core.db).await?; }
if let Some(isrc) = isrc.and_then(util::normalize_isrc) {
let t_upd = TrackUpdate { if let Some((track_id, _)) = self.get_matching_track(title, artist, isrc).await? {
isrc: Some(Some(&isrc)), Track::add_alias(track_id, src_id, &self.core.db).await?;
..Default::default() if let Some(isrc) = isrc {
}; let t_upd = TrackUpdate {
t_upd.update(Id::Db(track_id), &self.core.db).await?; isrc: Some(Some(isrc)),
..Default::default()
};
t_upd.update(Id::Db(track_id), &self.core.db).await?;
}
tracing::info!("matched track {title} [{src_id}]");
Ok(Some(track_id))
} else {
Ok(None)
} }
Ok(track_id)
} }
async fn get_matching_track( async fn get_matching_track(
&self, &self,
src_id: SrcId<'_>, title: &str,
name: &str, artist: &str,
artists: &[ArtistIdName],
isrc: Option<&str>, isrc: Option<&str>,
) -> Result<i32, ExtractorError> { ) -> Result<Option<(i32, f32)>, ExtractorError> {
let artist = &artists let t1 = self.core.matchmaker.parse_title(title);
.first()
.ok_or_else(|| ExtractorError::NoMatch {
id: src_id.to_owned(),
})?
.name;
let t1 = self.core.matchmaker.parse_title(name);
// Attempt to find the track by searching YouTube for its ISRC id
// If the title of the first search result matches, return this track.
if let Some(isrc) = isrc { if let Some(isrc) = isrc {
let filter = SearchFilter::new() let filter = SearchFilter::new()
.item_type(rustypipe::param::search_filter::ItemType::Video) .item_type(rustypipe::param::search_filter::ItemType::Video)
@ -455,38 +422,17 @@ impl YouTubeExtractor {
let t2 = self.core.matchmaker.parse_title(&video.name); let t2 = self.core.matchmaker.parse_title(&video.name);
let score = self.core.matchmaker.match_name(&t1, &t2); let score = self.core.matchmaker.match_name(&t1, &t2);
if self.core.matchmaker.is_match(score) { if self.core.matchmaker.is_match(score) {
tracing::info!( return Ok(Some((self.fetch_track(&video.id).await?, score)));
"matched track `{name}` [{src_id}] => `{0}` [yt:{1}] (ISRC, {score:.2})",
video.name,
video.id
);
// Get the found track either from YT or the database
let track = if let Some(track) =
Track::get(Id::Src(&video.id, MusicService::YouTube), &self.core.db)
.await
.to_optional()?
{
track
} else {
let track_id = self._fetch_track(&video.id, false).await?;
Track::get(Id::Db(track_id), &self.core.db).await?
};
self.store_matching_artists(artists, src_id.1, &track.artists)
.await?;
return Ok(track.id);
} }
} }
} }
let a1 = self.core.matchmaker.parse_artist(artist); let a1 = self.core.matchmaker.parse_artist(artist);
// Find the track by searching for YT Music tracks/videos
let search = self let search = self
.rp .rp
.query() .query()
.music_search(format!("{name} {artist}")) .music_search(format!("{title} {artist}"))
.await?; .await?;
let best_match = search let best_match = search
.tracks .tracks
@ -508,170 +454,15 @@ impl YouTubeExtractor {
.max_by(|a, b| a.1.total_cmp(&b.1)); .max_by(|a, b| a.1.total_cmp(&b.1));
if let Some((track, score)) = best_match { if let Some((track, score)) = best_match {
if self.core.matchmaker.is_match(score) { if self.core.matchmaker.is_match(score) {
tracing::info!( return Ok(Some((
"matched track `{name}` [{src_id}] => `{0}` [yt:{1}] (search, {score:.2})", self.import_track_item(track, None, &[], None, false)
track.name, .await?,
track.id score,
); )));
return self.import_track_item(track, None, &[], None, false).await;
}
tracing::info!(
"could not match track `{name}` [{src_id}] => `{0}` [yt:{1}] (search, {score:.2})",
track.name,
track.id
);
} else {
tracing::info!("could not match track `{name}` [{src_id}] (search)");
}
Err(ExtractorError::NoMatch {
id: src_id.to_owned(),
})
}
/// Try to match the artists from a different music service to the ones of a track
/// already stored in the database.
///
/// Returns the artist id matched to the first given artist
async fn store_matching_artists(
&self,
artists_to_match: &[ArtistIdName],
service: MusicService,
yt_artists: &[ArtistId],
) -> Result<(), ExtractorError> {
let mut yt_artists = yt_artists
.iter()
.filter_map(|a| {
a.id.as_ref().map(|id| {
(
ArtistSrcidName {
id: id.as_srcid(),
name: &a.name,
},
self.core.matchmaker.parse_artist(&a.name),
)
})
})
.collect::<Vec<_>>();
for atm in artists_to_match {
let atm_srcid = SrcIdOwned(atm.id.to_owned(), service);
// Check if the artist has already been matched
let atm_id_res = self
.core
.artist_cache
.get_or_insert_async(&atm_srcid, async {
Artist::get_id(atm_srcid.as_srcid(), &self.core.db)
.await?
.ok_or(ExtractorError::NoId)
})
.await;
match atm_id_res {
Ok(atm_id) => {
// Artist is already matched
let yt_src_id = Artist::get_src_id(atm_id, &self.core.db)
.await?
.ok_or(ExtractorError::NoId)?;
if let Some((idx, _)) = yt_artists
.iter()
.enumerate()
.find(|(_, (a, _))| a.id == yt_src_id)
{
yt_artists.swap_remove(idx);
}
}
Err(ExtractorError::NoId) => {
// Match artist
let a1 = self.core.matchmaker.parse_artist(&atm.name);
let (ib, score) = yt_artists
.iter()
.map(|(_, a2)| self.core.matchmaker.match_name(&a1, a2))
.enumerate()
.max_by(|(_, a), (_, b)| a.total_cmp(b))
.unwrap_or_default();
if score > 0.8 {
let yta = yt_artists.swap_remove(ib).0;
let yta_id = Artist::get_id(yta.id, &self.core.db)
.await?
.ok_or(ExtractorError::NoId)?;
Artist::add_alias(yta_id, atm_srcid.as_srcid(), &self.core.db).await?;
tracing::info!(
"matched artist `{}` [{}] => `{}` [{}] ({score:.2})",
yta.name,
yta.id,
atm.name,
atm_srcid
);
self.core.artist_cache.insert(atm_srcid, yta_id);
}
}
Err(e) => return Err(e),
} }
} }
Ok(())
}
/// Try to match an album from a different streaming service to a YouTube track Ok(None)
pub async fn match_album(
&self,
src_id: SrcId<'_>,
name: &str,
artist: &str,
) -> Result<i32, ExtractorError> {
let albums = self
.rp
.query()
.music_search_albums(&format!("{name} {artist}"))
.await?
.items
.items;
let t1 = self.core.matchmaker.parse_title(name);
let a1 = self.core.matchmaker.parse_artist(name);
let best_match = albums
.into_iter()
.map(|album| {
let t2 = self.core.matchmaker.parse_title(&album.name);
let a2 = self.core.matchmaker.parse_title(
&album
.artists
.first()
.map(|a| a.name.to_owned())
.unwrap_or_default(),
);
let score = self.core.matchmaker.match_track(&t1, &a1, &t2, &a2, false);
(album, score)
})
.max_by(|a, b| a.1.total_cmp(&b.1));
if let Some((album, score)) = best_match {
if self.core.matchmaker.is_match(score) {
tracing::info!(
"matched album `{name}` [{src_id}] => `{0}` [yt:{1}] (search, {score:.2})",
album.name,
album.id
);
let album_id = album.id.clone();
let import_res = self.import_album_item(album).await?;
if import_res.status == GetStatus::Fetched {
return self.fetch_album(&album_id).await.map(|res| res.0);
} else {
return Ok(import_res.c);
}
}
tracing::info!(
"could not match album `{name}` [{src_id}] => `{0}` [yt:{1}] (search, {score:.2})",
album.name,
album.id
);
} else {
tracing::info!("could not match album `{name}` [{src_id}] (search)");
}
Err(ExtractorError::NoMatch {
id: src_id.to_owned(),
})
} }
} }
@ -724,7 +515,22 @@ impl YouTubeExtractor {
let release_date_precision = let release_date_precision =
details.as_ref().and_then(|d| d.release_date).map(|d| d.1); details.as_ref().and_then(|d| d.release_date).map(|d| d.1);
if let Some(t_album) = track.album { if track.is_video {
// Create a pseudo-album for music videos
let n_album = AlbumNew {
src_id: &track.id,
service: MusicService::YouTube,
name: &track.name,
release_date,
release_date_precision,
album_type: Some(AlbumType::Mv),
by_va: track.by_va,
image_url: image_url.as_deref(),
dirty: false,
..Default::default()
};
n_album.upsert(&self.core.db).await?
} else if let Some(t_album) = track.album {
let n_album = AlbumNew { let n_album = AlbumNew {
src_id: &t_album.id, src_id: &t_album.id,
service: MusicService::YouTube, service: MusicService::YouTube,
@ -736,24 +542,10 @@ impl YouTubeExtractor {
}; };
n_album.upsert(&self.core.db).await? n_album.upsert(&self.core.db).await?
} else { } else {
if !track.is_video { return Err(ExtractorError::InvalidData {
tracing::warn!("track [yt:{}] has no album, but is no MV", track.id); id: SrcIdOwned(track.id, MusicService::YouTube),
} msg: "unknown album".into(),
});
// Create a pseudo-album for music videos
let n_album = AlbumNew {
src_id: &track.id,
service: MusicService::YouTube,
name: &track.name,
release_date,
release_date_precision,
album_type: Some(AlbumType::Mv).filter(|_| track.is_video),
by_va: track.by_va,
image_url: image_url.as_deref(),
dirty: false,
..Default::default()
};
n_album.upsert(&self.core.db).await?
} }
} }
}; };
@ -796,7 +588,7 @@ impl YouTubeExtractor {
self.import_track_item(track, album_id, album_artists, None, hidden) self.import_track_item(track, album_id, album_artists, None, hidden)
.await .await
}) })
.buffered(CONFIG.core.db_concurrency) .buffered(DB_CONCURRENCY)
.try_collect() .try_collect()
.await .await
} }
@ -827,7 +619,7 @@ impl YouTubeExtractor {
) -> Result<Vec<i32>, ExtractorError> { ) -> Result<Vec<i32>, ExtractorError> {
futures::stream::iter(artists) futures::stream::iter(artists)
.map(|aid| async { self.import_artist_id(aid).await }) .map(|aid| async { self.import_artist_id(aid).await })
.buffered(CONFIG.core.db_concurrency) .buffered(CONCURRENCY)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await .await
.map_err(ExtractorError::from) .map_err(ExtractorError::from)
@ -861,22 +653,20 @@ impl YouTubeExtractor {
) -> Result<Vec<i32>, ExtractorError> { ) -> Result<Vec<i32>, ExtractorError> {
futures::stream::iter(artists) futures::stream::iter(artists)
.map(|artist| async { self.import_artist_item(artist).await }) .map(|artist| async { self.import_artist_item(artist).await })
.buffered(CONFIG.core.db_concurrency) .buffered(CONCURRENCY)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await .await
.map_err(ExtractorError::from) .map_err(ExtractorError::from)
} }
/// Import a YT Music album item (album from artist page) /// Import a YT Music album item (album from artist page)
async fn import_album_item( async fn import_album_item(&self, album: rpmodel::AlbumItem) -> Result<(), ExtractorError> {
&self,
album: rpmodel::AlbumItem,
) -> Result<GetResult<i32>, ExtractorError> {
// Return if the album was already imported // Return if the album was already imported
if let Some(album_id) = if Album::get_id_clean(SrcId(&album.id, MusicService::YouTube), &self.core.db)
Album::get_id_clean(SrcId(&album.id, MusicService::YouTube), &self.core.db).await? .await?
.is_some()
{ {
return Ok(GetResult::stored(album_id)); return Ok(());
} }
let (artists, ul_artists) = self let (artists, ul_artists) = self
@ -905,7 +695,7 @@ impl YouTubeExtractor {
tx.commit().await?; tx.commit().await?;
tracing::debug!("imported album item [yt:{}] {}", album.id, album.name); tracing::debug!("imported album item [yt:{}] {}", album.id, album.name);
Ok(GetResult::fetched(album_id)) Ok(())
} }
/// Import a YT Music playlist item /// Import a YT Music playlist item
@ -945,7 +735,7 @@ impl YouTubeExtractor {
) -> Result<Vec<i32>, ExtractorError> { ) -> Result<Vec<i32>, ExtractorError> {
futures::stream::iter(playlists) futures::stream::iter(playlists)
.map(|playlists| async { self.import_playlist_item(playlists).await }) .map(|playlists| async { self.import_playlist_item(playlists).await })
.buffered(CONFIG.core.db_concurrency) .buffered(CONCURRENCY)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await .await
.map_err(ExtractorError::from) .map_err(ExtractorError::from)
@ -989,8 +779,8 @@ impl YouTubeExtractor {
playlists: impl IntoIterator<Item = rpmodel::PlaylistItem>, playlists: impl IntoIterator<Item = rpmodel::PlaylistItem>,
) -> Result<Vec<i32>, ExtractorError> { ) -> Result<Vec<i32>, ExtractorError> {
futures::stream::iter(playlists) futures::stream::iter(playlists)
.map(|pl| async { self.import_playlist_item_yt(pl).await }) .map(|playlists| async { self.import_playlist_item_yt(playlists).await })
.buffered(CONFIG.core.db_concurrency) .buffered(CONCURRENCY)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await .await
.map_err(ExtractorError::from) .map_err(ExtractorError::from)
@ -1198,13 +988,11 @@ mod tests {
.match_track( .match_track(
SrcId("5awNIWVrh2ISfvPd5IUZNh", MusicService::Spotify), SrcId("5awNIWVrh2ISfvPd5IUZNh", MusicService::Spotify),
"PTT (Paint The Town)", "PTT (Paint The Town)",
&[ArtistIdName { "LOONA",
id: "52zMTJCKluDlFwMQWmccY7".to_owned(),
name: "LOONA".to_owned(),
}],
Some("KRA382152284"), Some("KRA382152284"),
) )
.await .await
.unwrap()
.unwrap(); .unwrap();
let track = Track::get(Id::Db(track_id), &pool).await.unwrap(); let track = Track::get(Id::Db(track_id), &pool).await.unwrap();
insta::assert_ron_snapshot!(track, { insta::assert_ron_snapshot!(track, {

View file

@ -163,7 +163,10 @@ impl Matchmaker {
} }
fn parse(&self, s: &str, artist: bool) -> ParsedName { fn parse(&self, s: &str, artist: bool) -> ParsedName {
let s = s.to_ascii_lowercase().replace("feat.", "|"); let mut s = s.to_ascii_lowercase();
if !artist {
s = s.replace("feat.", "|")
}
let mut parts = Vec::new(); let mut parts = Vec::new();
let mut n_parts = 0; let mut n_parts = 0;
@ -189,12 +192,18 @@ impl Matchmaker {
if brace_level == 0 { if brace_level == 0 {
do_split = true; do_split = true;
} }
} else if (space && SEPARATORS.contains(&c)) || SEPARATORS_NOSPACE.contains(&c) { } else if ((artist || space) && SEPARATORS.contains(&c))
|| SEPARATORS_NOSPACE.contains(&c)
{
do_split = true; do_split = true;
} else if c.is_whitespace() { } else if c.is_whitespace() {
// Dont create double spaces if artist {
if !space && !buf.is_empty() { do_split = true;
buf += " "; } else {
// Dont create double spaces
if !space && !buf.is_empty() {
buf += " ";
}
} }
} else { } else {
buf.push(c); buf.push(c);
@ -547,33 +556,4 @@ mod tests {
"'{a}' does not match '{b}': score {score} < 0.5\npA: {parsed_a:?}\npB: {parsed_b:?}" "'{a}' does not match '{b}': score {score} < 0.5\npA: {parsed_a:?}\npB: {parsed_b:?}"
); );
} }
#[rstest]
#[case("Fantasy", "Max-Antoine", "Fantasy", "Max-Antoine Meisters", false)]
#[case(
"Armada",
"Two Steps From Hell",
"Two Steps From Hell - Armada",
"ThePrimeAres2",
true
)]
fn match_tracks(
#[case] t1: &str,
#[case] a1: &str,
#[case] t2: &str,
#[case] a2: &str,
#[case] is_video: bool,
) {
let mm = Matchmaker::new(KEYWORDS.values(), MATCH_THR);
let parsed_t1 = mm.parse_title(t1);
let parsed_a1 = mm.parse_artist(a1);
let parsed_t2 = mm.parse_title(t2);
let parsed_a2 = mm.parse_artist(a2);
let score = mm.match_track(&parsed_t1, &parsed_a1, &parsed_t2, &parsed_a2, is_video);
assert!(
mm.is_match(score),
"'{t1}' does not match '{t2}': score {score} < 0.5\npA: {parsed_t1:?}\npB: {parsed_t2:?}"
);
}
} }

View file

@ -9,15 +9,10 @@ use regex::Regex;
use rustypipe::model as rpmodel; use rustypipe::model as rpmodel;
use siphasher::sip128::{Hasher128, SipHasher}; use siphasher::sip128::{Hasher128, SipHasher};
use time::{Date, Duration, OffsetDateTime}; use time::{Date, Duration, OffsetDateTime};
use tiraya_db::models::{AlbumType, DatePrecision, SrcId, SyncData, SyncError}; use tiraya_db::models::{AlbumType, DatePrecision, SyncData, SyncError};
use crate::error::ExtractorSourceError; use crate::error::ExtractorSourceError;
pub struct ArtistSrcidName<'a> {
pub id: SrcId<'a>,
pub name: &'a str,
}
#[derive(Clone)] #[derive(Clone)]
pub struct ArtistIdName { pub struct ArtistIdName {
pub id: String, pub id: String,
@ -182,21 +177,6 @@ impl AlbumHasher {
} }
} }
pub fn normalize_isrc(isrc: &str) -> Option<String> {
static ISRC_RE: Lazy<Regex> =
Lazy::new(|| Regex::new("^[A-z]{2}[- ]?[A-z0-9]{3}[- ]?[0-9]{2}[- ]?[0-9]{5}$").unwrap());
if ISRC_RE.is_match(isrc) {
Some(
isrc.chars()
.filter(|c| *c != ' ' && *c != '-')
.map(|c| c.to_ascii_uppercase())
.collect(),
)
} else {
None
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -265,12 +245,4 @@ mod tests {
let hash = hasher.finish(); let hash = hasher.finish();
assert_eq!(hash, expect, "got {hash:x?}"); assert_eq!(hash, expect, "got {hash:x?}");
} }
#[rstest]
#[case("NL 1AP 19 35969", Some("NL1AP1935969"))]
#[case("nl 1AP 19 35969", Some("NL1AP1935969"))]
#[case("DE-ZC6-2342630", Some("DEZC62342630"))]
fn t_normalize_isrc(#[case] isrc: &str, #[case] expect: Option<&str>) {
assert_eq!(normalize_isrc(isrc).as_deref(), expect);
}
} }

View file

@ -204,7 +204,7 @@ mod sp {
/* /*
#[sqlx_database_tester::test(pool(variable = "pool", migrations = "../db/migrations"))] #[sqlx_database_tester::test(pool(variable = "pool", migrations = "../db/migrations"))]
async fn update_playlist() { async fn update_playlist() {
let xtr = Extractor::new(pool.clone()).unwrap(); let xtr = xtr(pool.clone()).await;
// 7Cdbc4geOdYRdsne9iOH49 // 7Cdbc4geOdYRdsne9iOH49
@ -215,14 +215,5 @@ mod sp {
let playlist_tracks = Playlist::get_tracks(res.c, &pool).await.unwrap().0; let playlist_tracks = Playlist::get_tracks(res.c, &pool).await.unwrap().0;
dbg!(&playlist_tracks); dbg!(&playlist_tracks);
}
#[sqlx_database_tester::test(pool(variable = "pool", migrations = "../db/migrations"))]
async fn update_user() {
setup_log();
let xtr = Extractor::new(pool.clone()).unwrap();
xtr.get_user(SrcId("1251642561", MusicService::Spotify)).await.unwrap();
}*/ }*/
} }

View file

@ -2,7 +2,7 @@ use std::{str::FromStr, time::Instant};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use sqlx::PgPool; use sqlx::PgPool;
use tiraya_db::models::{Playlist, SrcIdOwned, User}; use tiraya_db::models::{Playlist, SrcIdOwned};
use tiraya_extractor::{Extractor, GetStatus}; use tiraya_extractor::{Extractor, GetStatus};
#[derive(Parser)] #[derive(Parser)]
@ -14,10 +14,7 @@ struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
Artist { id: String }, Artist { id: String },
Album { id: String },
Track { id: String },
Playlist { id: String }, Playlist { id: String },
User { id: String },
} }
#[tokio::main] #[tokio::main]
@ -43,50 +40,21 @@ async fn run() {
if artist_res.status == GetStatus::Fetched { if artist_res.status == GetStatus::Fetched {
ext.fetch_artist_albums(artist_res.c.id).await.unwrap(); ext.fetch_artist_albums(artist_res.c.id).await.unwrap();
} }
dbg!(artist_res.c); dbg!(artist_res.c);
} }
Commands::Album { id } => {
let src_id = SrcIdOwned::from_str(&id).unwrap();
let album = ext.get_album(src_id.as_srcid()).await.unwrap().c;
dbg!(album);
}
Commands::Track { id } => {
let src_id = SrcIdOwned::from_str(&id).unwrap();
let track = ext.get_track(src_id.as_srcid()).await.unwrap().c;
dbg!(track);
}
Commands::Playlist { id } => { Commands::Playlist { id } => {
let src_id = SrcIdOwned::from_str(&id).unwrap(); let src_id = SrcIdOwned::from_str(&id).unwrap();
let playlist = ext.get_playlist(src_id.as_srcid()).await.unwrap(); let playlist = ext.get_playlist(src_id.as_srcid()).await.unwrap();
let tracks = Playlist::get_tracks(playlist.c.id, &pool).await.unwrap().0; let tracks = Playlist::get_tracks(playlist.c.id, &pool).await.unwrap().0;
for (i, e) in tracks.iter().enumerate() { for (i, e) in tracks.iter().enumerate() {
if let Some(track) = &e.track { if let Some(track) = &e.track {
println!( println!("{} [{}] {}", i + 1, track.src_id, track.name);
"{} [{}] {} - {}",
i + 1,
track.src_id,
track.name,
track
.artists
.first()
.map(|a| a.name.as_str())
.unwrap_or_default()
);
} else { } else {
println!("{} n/a", i + 1) println!("{} n/a", i + 1)
} }
} }
} }
Commands::User { id } => {
let src_id = SrcIdOwned::from_str(&id).unwrap();
let user = ext.get_user(src_id.as_srcid()).await.unwrap().c;
let playlists = User::playlists(user.id, &pool).await.unwrap();
dbg!(user);
for (i, playlist) in playlists.iter().enumerate() {
println!("{} {}", i + 1, playlist.name)
}
}
} }
println!("Time: {}ms", t_start.elapsed().as_millis()); println!("Time: {}ms", t_start.elapsed().as_millis());
} }

View file

@ -41,18 +41,6 @@ pub struct ConfigExtractor {
/// # Number of hours after a playlist is considered stale and needs to be updated from the source /// # Number of hours after a playlist is considered stale and needs to be updated from the source
#[default(24)] #[default(24)]
pub playlist_stale_h: u32, pub playlist_stale_h: u32,
/// # Number of hours after a user is considered stale and needs to be updated from the source
#[default(24)]
pub user_stale_h: u32,
/// Maximum number of playlist items to be extracted
#[default(2000)]
pub playlist_item_limit: usize,
/// Maximum number of playlist items to be extracted and matched to YTM tracks
#[default(500)]
pub playlist_item_limit_matchmaker: usize,
/// Maximum number of user playlists to be extracted
#[default(200)]
pub user_playlist_limit: usize,
/// Directory for caching service tokens and other extractor data /// Directory for caching service tokens and other extractor data
#[default("extractor_data")] #[default("extractor_data")]
pub data_dir: PathBuf, pub data_dir: PathBuf,
@ -72,7 +60,7 @@ pub struct ConfigExtractor {
pub match_threshold: f32, pub match_threshold: f32,
pub youtube: ConfigExtractorYouTube, pub youtube: ConfigExtractorYouTube,
pub spotify: ConfigExtractorSpotify, pub spotify: Option<ConfigExtractorSpotify>,
} }
#[derive(Debug, Serialize, Deserialize, SmartDefault)] #[derive(Debug, Serialize, Deserialize, SmartDefault)]
@ -92,18 +80,13 @@ pub struct ConfigExtractorYouTube {
pub concurrency: usize, pub concurrency: usize,
} }
#[derive(Debug, Serialize, Deserialize, SmartDefault)] #[derive(Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct ConfigExtractorSpotify { pub struct ConfigExtractorSpotify {
pub enable: bool,
/// Spotify client credentials /// Spotify client credentials
/// ///
/// Setup under <https://developer.spotify.com/dashboard> /// Setup under <https://developer.spotify.com/dashboard>
pub client_id: String, pub client_id: String,
pub client_secret: String, pub client_secret: String,
/// Spotify content country
#[default("US")]
pub market: String,
} }
const DEFAULT_CONFIG_FILE: &str = "config.toml"; const DEFAULT_CONFIG_FILE: &str = "config.toml";

View file

@ -1,3 +1 @@
#![warn(clippy::dbg_macro, clippy::todo)]
pub mod config; pub mod config;

View file

@ -1,5 +1,6 @@
--- ---
source: crates/utils/src/config.rs source: crates/utils/src/config.rs
assertion_line: 117
expression: cfg expression: cfg
--- ---
Config( Config(
@ -16,18 +17,15 @@ Config(
artist_playlist_excluded_types: [ artist_playlist_excluded_types: [
"inst", "inst",
], ],
match_threshold: 0.5,
youtube: ConfigExtractorYouTube( youtube: ConfigExtractorYouTube(
language: "en", language: "en",
country: "US", country: "US",
n_retries: 2, n_retries: 2,
concurrency: 4, concurrency: 4,
), ),
spotify: ConfigExtractorSpotify( spotify: Some(ConfigExtractorSpotify(
enable: true,
client_id: "abc123", client_id: "abc123",
client_secret: "supersecret", client_secret: "supersecret",
market: None, )),
),
), ),
) )

View file

@ -11,16 +11,10 @@ db_concurrency = 8
# Music data extractor settings # Music data extractor settings
[extractor] [extractor]
# Number of hours after an item is considered stale and needs to be updated from the source # Number of hours after an item is considered stale
# and needs to be updated from the source
artist_stale_h = 24 artist_stale_h = 24
playlist_stale_h = 24 playlist_stale_h = 24
user_stale_h = 24
# Maximum number of playlist items to be extracted
playlist_item_limit = 2000
# Maximum number of playlist items to be extracted and matched to YTM tracks
playlist_item_limit_matchmaker = 500
# Maximum number of user playlists to be extracted
user_playlist_limit = 200
# Directory for caching service tokens and other extractor data # Directory for caching service tokens and other extractor data
data_dir = "extractor_data" data_dir = "extractor_data"
# Set of track types (keys from `track_type_keywords`) excluded from artist playlists # Set of track types (keys from `track_type_keywords`) excluded from artist playlists
@ -60,10 +54,7 @@ n_retries = 2
concurrency = 4 concurrency = 4
[extractor.spotify] [extractor.spotify]
enable = true
# Spotify client credentials # Spotify client credentials
# Setup under https://developer.spotify.com/dashboard # Setup under https://developer.spotify.com/dashboard
client_id = "abc123" client_id = "abc123"
client_secret = "supersecret" client_secret = "supersecret"
# Spotify content country (Default: country from user account)
# market = "US"