Compare commits

..

No commits in common. "24e706cb1914435c1d7af363cf5d5e908f642889" and "4ff2d82bd303bba8f85552028ed6a5e53fd72ea4" have entirely different histories.

26 changed files with 342 additions and 725 deletions

View file

@ -1,2 +1 @@
DATABASE_URL="postgres://postgres:1234@localhost/tiraya" DATABASE_URL="postgres://postgres:1234@localhost/tiraya"
RUST_LOG="tiraya_extractor=info"

200
Cargo.lock generated
View file

@ -31,9 +31,9 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.1" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -374,7 +374,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -396,7 +396,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [ dependencies = [
"darling_core 0.20.3", "darling_core 0.20.3",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -659,7 +659,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -719,12 +719,6 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.21" version = "0.3.21"
@ -780,9 +774,9 @@ dependencies = [
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.3" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]] [[package]]
name = "hex" name = "hex"
@ -938,9 +932,9 @@ dependencies = [
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.32.0" version = "1.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea" checksum = "a0770b0a3d4c70567f0d58331f3088b0e4c4f56c9b8d764efe654b4a5d46de3a"
dependencies = [ dependencies = [
"console", "console",
"lazy_static", "lazy_static",
@ -1148,16 +1142,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@ -1254,7 +1238,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -1283,12 +1267,6 @@ dependencies = [
"range-ext", "range-ext",
] ]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -1370,7 +1348,7 @@ dependencies = [
"pest_meta", "pest_meta",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -1419,7 +1397,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -1488,9 +1466,9 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.30.0" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@ -1591,12 +1569,6 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "relative-path"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.20" version = "0.11.20"
@ -1694,53 +1666,17 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rstest"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199"
dependencies = [
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605"
dependencies = [
"cfg-if",
"glob",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn 2.0.37",
"unicode-ident",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.14" version = "0.38.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
dependencies = [ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"errno", "errno",
@ -1771,9 +1707,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.101.6" version = "0.101.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted", "untrusted",
@ -1782,11 +1718,12 @@ dependencies = [
[[package]] [[package]]
name = "rustypipe" name = "rustypipe"
version = "0.1.0" version = "0.1.0"
source = "git+https://code.thetadev.de/ThetaDev/rustypipe.git#127596687b0f5a29be65adfe80c40059edc4cc50" source = "git+https://code.thetadev.de/ThetaDev/rustypipe.git#abd3317a10535203adab0bda236c4ace66435b63"
dependencies = [ dependencies = [
"base64 0.21.4", "base64 0.21.4",
"fancy-regex", "fancy-regex",
"futures", "futures",
"log",
"once_cell", "once_cell",
"phf", "phf",
"quick-js-dtp", "quick-js-dtp",
@ -1802,7 +1739,6 @@ dependencies = [
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
"tracing",
"url", "url",
"urlencoding", "urlencoding",
] ]
@ -1870,12 +1806,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "semver"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.188" version = "1.0.188"
@ -1893,7 +1823,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -1952,14 +1882,14 @@ dependencies = [
"darling 0.20.3", "darling 0.20.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
name = "sha1" name = "sha1"
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 = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
@ -1977,15 +1907,6 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "signature" name = "signature"
version = "2.1.0" version = "2.1.0"
@ -2025,9 +1946,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.11.1" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]] [[package]]
name = "socket2" name = "socket2"
@ -2350,9 +2271,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.37" version = "2.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" checksum = "91e02e55d62894af2a08aca894c6577281f76769ba47c94d5756bec8ac6e7373"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2397,12 +2318,12 @@ name = "testbed"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"env_logger",
"rustypipe", "rustypipe",
"sqlx", "sqlx",
"tiraya-db", "tiraya-db",
"tiraya-extractor", "tiraya-extractor",
"tokio", "tokio",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -2422,17 +2343,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
]
[[package]]
name = "thread_local"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [
"cfg-if",
"once_cell",
] ]
[[package]] [[package]]
@ -2508,10 +2419,10 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"hex-literal", "hex-literal",
"log",
"once_cell", "once_cell",
"quick_cache", "quick_cache",
"regex", "regex",
"rstest",
"rustypipe", "rustypipe",
"siphasher 1.0.0", "siphasher 1.0.0",
"sqlx", "sqlx",
@ -2521,7 +2432,6 @@ dependencies = [
"time", "time",
"tiraya-db", "tiraya-db",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
@ -2549,7 +2459,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -2575,9 +2485,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.9" version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@ -2614,7 +2524,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
] ]
[[package]] [[package]]
@ -2624,32 +2534,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@ -2742,12 +2626,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@ -2806,7 +2684,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2840,7 +2718,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.36",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2894,9 +2772,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.6" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]

View file

@ -17,8 +17,6 @@ thiserror = "1.0.36"
anyhow = "1.0.71" anyhow = "1.0.71"
dotenvy = "0.15.7" dotenvy = "0.15.7"
log = "0.4.17" log = "0.4.17"
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
env_logger = "0.10.0" env_logger = "0.10.0"
path_macro = "1.0.0" path_macro = "1.0.0"
hex-literal = "0.4.1" hex-literal = "0.4.1"

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "select t.id, t.src_id, t.service as \"service: _\", t.name, t.duration,\n t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,\n t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,\n jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)\n filter (where a.src_id is not null) as \"artists: _\"\nfrom tracks t\n left join artists_tracks art on art.track_id = t.id\n left join artists a on a.id = art.artist_id\n join track_aliases ta on ta.track_id=t.id\nwhere ta.src_id=$1 and ta.service=$2\ngroup by t.id", "query": "select t.id, t.src_id, t.service as \"service: _\", t.name, t.duration, t.duration_ms,\n t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,\n t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,\n jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)\n filter (where a.src_id is not null) as \"artists: _\"\nfrom tracks t\n left join artists_tracks art on art.track_id = t.id\n left join artists a on a.id = art.artist_id\nwhere t.src_id=$1 and t.service=$2\ngroup by t.id",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -42,71 +42,76 @@
}, },
{ {
"ordinal": 5, "ordinal": 5,
"name": "duration_ms",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "size", "name": "size",
"type_info": "Int8" "type_info": "Int8"
}, },
{ {
"ordinal": 6, "ordinal": 7,
"name": "loudness", "name": "loudness",
"type_info": "Float4" "type_info": "Float4"
}, },
{ {
"ordinal": 7, "ordinal": 8,
"name": "album_id", "name": "album_id",
"type_info": "Int4" "type_info": "Int4"
}, },
{ {
"ordinal": 8, "ordinal": 9,
"name": "album_pos", "name": "album_pos",
"type_info": "Int2" "type_info": "Int2"
}, },
{ {
"ordinal": 9, "ordinal": 10,
"name": "ul_artists", "name": "ul_artists",
"type_info": "TextArray" "type_info": "TextArray"
}, },
{ {
"ordinal": 10, "ordinal": 11,
"name": "isrc", "name": "isrc",
"type_info": "Varchar" "type_info": "Varchar"
}, },
{ {
"ordinal": 11, "ordinal": 12,
"name": "description", "name": "description",
"type_info": "Text" "type_info": "Text"
}, },
{ {
"ordinal": 12, "ordinal": 13,
"name": "created_at", "name": "created_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 13, "ordinal": 14,
"name": "updated_at", "name": "updated_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 14, "ordinal": 15,
"name": "primary_track", "name": "primary_track",
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"ordinal": 15, "ordinal": 16,
"name": "downloaded_at", "name": "downloaded_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 16, "ordinal": 17,
"name": "last_streamed_at", "name": "last_streamed_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 17, "ordinal": 18,
"name": "n_streams", "name": "n_streams",
"type_info": "Int4" "type_info": "Int4"
}, },
{ {
"ordinal": 18, "ordinal": 19,
"name": "artists: _", "name": "artists: _",
"type_info": "Jsonb" "type_info": "Jsonb"
} }
@ -135,6 +140,7 @@
false, false,
false, false,
true, true,
false,
true, true,
true, true,
false, false,
@ -151,5 +157,5 @@
null null
] ]
}, },
"hash": "240c8386d19e97338971891b871bc2f5ed7a6edcdf6305d5a1bbbccc19793057" "hash": "1075ed43809289b5b81d7cda88d66a270ff6085a0e280cc70e5a2a3c45fccf12"
} }

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "insert into tracks (src_id, service, name, duration, duration_ms,\n size, loudness, album_id, album_pos, ul_artists, isrc, description, primary_track)\nvalues ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)\non conflict (src_id, service) do update set\n name = excluded.name,\n duration = case when tracks.duration_ms and not excluded.duration_ms\n then tracks.duration else coalesce(excluded.duration, tracks.duration) end,\n duration_ms = excluded.duration_ms or tracks.duration_ms,\n size = coalesce(excluded.size, tracks.size),\n loudness = coalesce(excluded.loudness, tracks.loudness),\n album_id = excluded.album_id,\n album_pos = coalesce(excluded.album_pos, tracks.album_pos),\n ul_artists = coalesce(excluded.ul_artists, tracks.ul_artists),\n isrc = coalesce(excluded.isrc, tracks.isrc),\n description = coalesce(excluded.description, tracks.description),\n primary_track = coalesce(excluded.primary_track, tracks.primary_track)\nreturning id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "music_service",
"kind": {
"Enum": [
"yt",
"ty",
"sp",
"mx"
]
}
}
},
"Text",
"Int4",
"Bool",
"Int8",
"Float4",
"Int4",
"Int2",
"TextArray",
"Varchar",
"Text",
"Bool"
]
},
"nullable": [
false
]
},
"hash": "147c40380556124f815737fece63fa626ec7b9a214c371bf89feaa90824a629e"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "select t.id, t.src_id, t.service as \"service: _\", t.name, t.duration,\n t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,\n t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,\n jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)\n filter (where a.src_id is not null) as \"artists: _\"\nfrom tracks t\n left join artists_tracks art on art.track_id = t.id\n left join artists a on a.id = art.artist_id\nwhere t.src_id=$1 and t.service=$2\ngroup by t.id", "query": "select t.id, t.src_id, t.service as \"service: _\", t.name, t.duration, t.duration_ms,\n t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,\n t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,\n jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)\n filter (where a.src_id is not null) as \"artists: _\"\nfrom tracks t\n left join artists_tracks art on art.track_id = t.id\n left join artists a on a.id = art.artist_id\n join track_aliases ta on ta.track_id=t.id\nwhere ta.src_id=$1 and ta.service=$2\ngroup by t.id",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -42,71 +42,76 @@
}, },
{ {
"ordinal": 5, "ordinal": 5,
"name": "duration_ms",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "size", "name": "size",
"type_info": "Int8" "type_info": "Int8"
}, },
{ {
"ordinal": 6, "ordinal": 7,
"name": "loudness", "name": "loudness",
"type_info": "Float4" "type_info": "Float4"
}, },
{ {
"ordinal": 7, "ordinal": 8,
"name": "album_id", "name": "album_id",
"type_info": "Int4" "type_info": "Int4"
}, },
{ {
"ordinal": 8, "ordinal": 9,
"name": "album_pos", "name": "album_pos",
"type_info": "Int2" "type_info": "Int2"
}, },
{ {
"ordinal": 9, "ordinal": 10,
"name": "ul_artists", "name": "ul_artists",
"type_info": "TextArray" "type_info": "TextArray"
}, },
{ {
"ordinal": 10, "ordinal": 11,
"name": "isrc", "name": "isrc",
"type_info": "Varchar" "type_info": "Varchar"
}, },
{ {
"ordinal": 11, "ordinal": 12,
"name": "description", "name": "description",
"type_info": "Text" "type_info": "Text"
}, },
{ {
"ordinal": 12, "ordinal": 13,
"name": "created_at", "name": "created_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 13, "ordinal": 14,
"name": "updated_at", "name": "updated_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 14, "ordinal": 15,
"name": "primary_track", "name": "primary_track",
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"ordinal": 15, "ordinal": 16,
"name": "downloaded_at", "name": "downloaded_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 16, "ordinal": 17,
"name": "last_streamed_at", "name": "last_streamed_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 17, "ordinal": 18,
"name": "n_streams", "name": "n_streams",
"type_info": "Int4" "type_info": "Int4"
}, },
{ {
"ordinal": 18, "ordinal": 19,
"name": "artists: _", "name": "artists: _",
"type_info": "Jsonb" "type_info": "Jsonb"
} }
@ -135,6 +140,7 @@
false, false,
false, false,
true, true,
false,
true, true,
true, true,
false, false,
@ -151,5 +157,5 @@
null null
] ]
}, },
"hash": "b74d96a7a07be63547f6292beb8e2ed946f04184555c3092d1a502d2ba7622bb" "hash": "22bd3df069a5e7fa8c7882a2b38c2d2e4be1b49a61ea77c4ac2770064cdc3323"
} }

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "select t.id, t.src_id, t.service as \"service: _\", t.name, t.duration,\n t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,\n t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,\n jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)\n filter (where a.src_id is not null) as \"artists: _\"\nfrom tracks t\n left join artists_tracks art on art.track_id = t.id\n left join artists a on a.id = art.artist_id\nwhere t.id=$1\ngroup by t.id", "query": "select t.id, t.src_id, t.service as \"service: _\", t.name, t.duration, t.duration_ms,\n t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,\n t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,\n jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)\n filter (where a.src_id is not null) as \"artists: _\"\nfrom tracks t\n left join artists_tracks art on art.track_id = t.id\n left join artists a on a.id = art.artist_id\nwhere t.id=$1\ngroup by t.id",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -42,71 +42,76 @@
}, },
{ {
"ordinal": 5, "ordinal": 5,
"name": "duration_ms",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "size", "name": "size",
"type_info": "Int8" "type_info": "Int8"
}, },
{ {
"ordinal": 6, "ordinal": 7,
"name": "loudness", "name": "loudness",
"type_info": "Float4" "type_info": "Float4"
}, },
{ {
"ordinal": 7, "ordinal": 8,
"name": "album_id", "name": "album_id",
"type_info": "Int4" "type_info": "Int4"
}, },
{ {
"ordinal": 8, "ordinal": 9,
"name": "album_pos", "name": "album_pos",
"type_info": "Int2" "type_info": "Int2"
}, },
{ {
"ordinal": 9, "ordinal": 10,
"name": "ul_artists", "name": "ul_artists",
"type_info": "TextArray" "type_info": "TextArray"
}, },
{ {
"ordinal": 10, "ordinal": 11,
"name": "isrc", "name": "isrc",
"type_info": "Varchar" "type_info": "Varchar"
}, },
{ {
"ordinal": 11, "ordinal": 12,
"name": "description", "name": "description",
"type_info": "Text" "type_info": "Text"
}, },
{ {
"ordinal": 12, "ordinal": 13,
"name": "created_at", "name": "created_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 13, "ordinal": 14,
"name": "updated_at", "name": "updated_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 14, "ordinal": 15,
"name": "primary_track", "name": "primary_track",
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"ordinal": 15, "ordinal": 16,
"name": "downloaded_at", "name": "downloaded_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 16, "ordinal": 17,
"name": "last_streamed_at", "name": "last_streamed_at",
"type_info": "Timestamp" "type_info": "Timestamp"
}, },
{ {
"ordinal": 17, "ordinal": 18,
"name": "n_streams", "name": "n_streams",
"type_info": "Int4" "type_info": "Int4"
}, },
{ {
"ordinal": 18, "ordinal": 19,
"name": "artists: _", "name": "artists: _",
"type_info": "Jsonb" "type_info": "Jsonb"
} }
@ -122,6 +127,7 @@
false, false,
false, false,
true, true,
false,
true, true,
true, true,
false, false,
@ -138,5 +144,5 @@
null null
] ]
}, },
"hash": "ebd526014759ba1316f26ba42a0b66aef00cae6c86a6d5f4747a99414d42c2b1" "hash": "dbb478aab35ec0b538845d633a84399984847f42b62fd980c022385a8c6e69e7"
} }

View file

@ -1,45 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "insert into tracks (src_id, service, name, duration,\n size, loudness, album_id, album_pos, ul_artists, isrc, description, primary_track)\nvalues ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)\non conflict (src_id, service) do update set\n name = excluded.name,\n duration = coalesce(excluded.duration, tracks.duration),\n size = coalesce(excluded.size, tracks.size),\n loudness = coalesce(excluded.loudness, tracks.loudness),\n album_id = excluded.album_id,\n album_pos = coalesce(excluded.album_pos, tracks.album_pos),\n ul_artists = coalesce(excluded.ul_artists, tracks.ul_artists),\n isrc = coalesce(excluded.isrc, tracks.isrc),\n description = coalesce(excluded.description, tracks.description),\n primary_track = coalesce(excluded.primary_track, tracks.primary_track)\nreturning id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "music_service",
"kind": {
"Enum": [
"yt",
"ty",
"sp",
"mx"
]
}
}
},
"Text",
"Int4",
"Int8",
"Float4",
"Int4",
"Int2",
"TextArray",
"Varchar",
"Text",
"Bool"
]
},
"nullable": [
false
]
},
"hash": "f3e06fbfd2fc3bff86b80c754109bbbd11d8c2decf4d72760c68a2f6d9d22c67"
}

View file

@ -23,6 +23,7 @@ CREATE TABLE tracks (
service music_service NOT NULL, service music_service NOT NULL,
name text NOT NULL, name text NOT NULL,
duration integer, duration integer,
duration_ms bool NOT NULL DEFAULT false,
size bigint, size bigint,
loudness float4, loudness float4,
album_pos smallint, album_pos smallint,
@ -42,7 +43,8 @@ COMMENT ON COLUMN tracks.id IS E'Internal track ID';
COMMENT ON COLUMN tracks.src_id IS E'Track ID from the source'; COMMENT ON COLUMN tracks.src_id IS E'Track ID from the source';
COMMENT ON COLUMN tracks.service IS E'Service providing the track'; COMMENT ON COLUMN tracks.service IS E'Service providing the track';
COMMENT ON COLUMN tracks.name IS E'Track name'; COMMENT ON COLUMN tracks.name IS E'Track name';
COMMENT ON COLUMN tracks.duration IS E'Duration of the track in seconds'; COMMENT ON COLUMN tracks.duration IS E'Duration of the track in milliseconds';
COMMENT ON COLUMN tracks.duration_ms IS E'True if the duration is in millisecond resolution';
COMMENT ON COLUMN tracks.size IS E'File size in bytes'; COMMENT ON COLUMN tracks.size IS E'File size in bytes';
COMMENT ON COLUMN tracks.loudness IS E'Track loudness in dB'; COMMENT ON COLUMN tracks.loudness IS E'Track loudness in dB';
COMMENT ON COLUMN tracks.album_pos IS E'Position of the track in its album'; COMMENT ON COLUMN tracks.album_pos IS E'Position of the track in its album';
@ -329,7 +331,7 @@ CREATE TRIGGER albums_set_updated_at
EXECUTE PROCEDURE set_updated_at(); EXECUTE PROCEDURE set_updated_at();
CREATE TRIGGER tracks_set_updated_at CREATE TRIGGER tracks_set_updated_at
BEFORE UPDATE OF id,src_id,service,name,duration,size,loudness,album_pos,album_id,ul_artists,isrc,description BEFORE UPDATE OF id,src_id,service,name,duration,duration_ms,size,loudness,album_pos,album_id,ul_artists,isrc,description
ON tracks ON tracks
FOR EACH ROW FOR EACH ROW
EXECUTE PROCEDURE set_updated_at(); EXECUTE PROCEDURE set_updated_at();

View file

@ -5,7 +5,7 @@ use time::{Date, PrimitiveDateTime};
use super::{ use super::{
artist::{ArtistId, ArtistJsonb}, artist::{ArtistId, ArtistJsonb},
map_artists, AlbumType, DatePrecision, Id, MusicService, SrcId, SrcIdOwned, TrackSlim, map_artists, AlbumType, Artist, DatePrecision, Id, MusicService, SrcId, SrcIdOwned, TrackSlim,
TrackSlimRow, TrackSlimRow,
}; };
use crate::{ use crate::{
@ -254,10 +254,11 @@ group by b.id"#,
pub async fn add_artists( pub async fn add_artists(
id: i32, id: i32,
artists: &[i32], artists: &[Id<'_>],
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> { ) -> Result<(), DatabaseError> {
for artist_id in artists { for artist_id in artists {
let artist_id = Artist::resolve_id(*artist_id, tx).await?;
sqlx::query!( sqlx::query!(
r#"insert into artists_albums (album_id, artist_id) values ($1, $2) r#"insert into artists_albums (album_id, artist_id) values ($1, $2)
on conflict (album_id, artist_id) do nothing"#, on conflict (album_id, artist_id) do nothing"#,
@ -272,7 +273,7 @@ on conflict (album_id, artist_id) do nothing"#,
pub async fn set_artists( pub async fn set_artists(
id: i32, id: i32,
artists: &[i32], artists: &[Id<'_>],
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> { ) -> Result<(), DatabaseError> {
sqlx::query!(r#"delete from artists_albums where album_id=$1"#, id) sqlx::query!(r#"delete from artists_albums where album_id=$1"#, id)
@ -639,7 +640,7 @@ mod tests {
album_hash: Some(&hex!("badeaffe")), album_hash: Some(&hex!("badeaffe")),
..Default::default() ..Default::default()
}; };
let album_artists = [ids::ARTIST_ID_LEA, ids::ARTIST_ID_CYRIL]; let album_artists = [ids::ARTIST_LEA, ids::ARTIST_CYRIL];
// Create // Create
let mut c_tx = pool.begin().await.unwrap(); let mut c_tx = pool.begin().await.unwrap();

View file

@ -7,7 +7,7 @@ expression: tracks_iwwus
src_id: "hWFarQmaQAQ", src_id: "hWFarQmaQAQ",
service: yt, service: yt,
name: "Immer wenn wir uns sehn (\"Das schönste Mädchen der Welt\", Soundtrack)", name: "Immer wenn wir uns sehn (\"Das schönste Mädchen der Welt\", Soundtrack)",
duration: Some(186), duration: Some(186000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),

View file

@ -7,7 +7,7 @@ expression: tracks_vakuum
src_id: "2txScm52-QI", src_id: "2txScm52-QI",
service: yt, service: yt,
name: "Die Segel sind gesetzt", name: "Die Segel sind gesetzt",
duration: Some(186), duration: Some(186000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -29,7 +29,7 @@ expression: tracks_vakuum
src_id: "oZKv47vyqQU", src_id: "oZKv47vyqQU",
service: yt, service: yt,
name: "Monster", name: "Monster",
duration: Some(224), duration: Some(224000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -51,7 +51,7 @@ expression: tracks_vakuum
src_id: "7WXlMU9ItnA", src_id: "7WXlMU9ItnA",
service: yt, service: yt,
name: "Dach", name: "Dach",
duration: Some(231), duration: Some(231000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -73,7 +73,7 @@ expression: tracks_vakuum
src_id: "ySChj_9rT5Y", src_id: "ySChj_9rT5Y",
service: yt, service: yt,
name: "Kennst du das", name: "Kennst du das",
duration: Some(191), duration: Some(191000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -95,7 +95,7 @@ expression: tracks_vakuum
src_id: "revpIT2HiNs", src_id: "revpIT2HiNs",
service: yt, service: yt,
name: "Wohin willst du", name: "Wohin willst du",
duration: Some(255), duration: Some(255000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -117,7 +117,7 @@ expression: tracks_vakuum
src_id: "LeEgBsYfjLU", src_id: "LeEgBsYfjLU",
service: yt, service: yt,
name: "Vakuum", name: "Vakuum",
duration: Some(220), duration: Some(220000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -139,7 +139,7 @@ expression: tracks_vakuum
src_id: "-i5XjMkQN8M", src_id: "-i5XjMkQN8M",
service: yt, service: yt,
name: "Melodie", name: "Melodie",
duration: Some(251), duration: Some(251000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -161,7 +161,7 @@ expression: tracks_vakuum
src_id: "DhlIZkoPsxg", src_id: "DhlIZkoPsxg",
service: yt, service: yt,
name: "Du & Ich", name: "Du & Ich",
duration: Some(238), duration: Some(238000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -183,7 +183,7 @@ expression: tracks_vakuum
src_id: "LCoomBMOkgU", src_id: "LCoomBMOkgU",
service: yt, service: yt,
name: "Schwerelos", name: "Schwerelos",
duration: Some(198), duration: Some(198000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -205,7 +205,7 @@ expression: tracks_vakuum
src_id: "c6Ot-Z3HEBo", src_id: "c6Ot-Z3HEBo",
service: yt, service: yt,
name: "Lichtermeer", name: "Lichtermeer",
duration: Some(164), duration: Some(164000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -227,7 +227,7 @@ expression: tracks_vakuum
src_id: "ybm_4hQG0ok", src_id: "ybm_4hQG0ok",
service: yt, service: yt,
name: "Nachtzug", name: "Nachtzug",
duration: Some(290), duration: Some(290000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -249,7 +249,7 @@ expression: tracks_vakuum
src_id: "DJKmtK5PmSY", src_id: "DJKmtK5PmSY",
service: yt, service: yt,
name: "Rückenwind", name: "Rückenwind",
duration: Some(263), duration: Some(263000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),

View file

@ -7,7 +7,7 @@ expression: tracks
src_id: "LeEgBsYfjLU", src_id: "LeEgBsYfjLU",
service: yt, service: yt,
name: "Vakuum", name: "Vakuum",
duration: Some(220), duration: Some(220000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -29,7 +29,7 @@ expression: tracks
src_id: "LCoomBMOkgU", src_id: "LCoomBMOkgU",
service: yt, service: yt,
name: "Schwerelos", name: "Schwerelos",
duration: Some(198), duration: Some(198000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -51,7 +51,7 @@ expression: tracks
src_id: "c6Ot-Z3HEBo", src_id: "c6Ot-Z3HEBo",
service: yt, service: yt,
name: "Lichtermeer", name: "Lichtermeer",
duration: Some(164), duration: Some(164000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -73,7 +73,7 @@ expression: tracks
src_id: "hWFarQmaQAQ", src_id: "hWFarQmaQAQ",
service: yt, service: yt,
name: "Immer wenn wir uns sehn (\"Das schönste Mädchen der Welt\", Soundtrack)", name: "Immer wenn wir uns sehn (\"Das schönste Mädchen der Welt\", Soundtrack)",
duration: Some(186), duration: Some(186000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -99,7 +99,7 @@ expression: tracks
src_id: "2txScm52-QI", src_id: "2txScm52-QI",
service: yt, service: yt,
name: "Die Segel sind gesetzt", name: "Die Segel sind gesetzt",
duration: Some(186), duration: Some(186000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),

View file

@ -7,7 +7,7 @@ expression: tracks
src_id: "2txScm52-QI", src_id: "2txScm52-QI",
service: yt, service: yt,
name: "Die Segel sind gesetzt", name: "Die Segel sind gesetzt",
duration: Some(186), duration: Some(186000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -29,7 +29,7 @@ expression: tracks
src_id: "oZKv47vyqQU", src_id: "oZKv47vyqQU",
service: yt, service: yt,
name: "Monster", name: "Monster",
duration: Some(224), duration: Some(224000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -51,7 +51,7 @@ expression: tracks
src_id: "7WXlMU9ItnA", src_id: "7WXlMU9ItnA",
service: yt, service: yt,
name: "Dach", name: "Dach",
duration: Some(231), duration: Some(231000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -73,7 +73,7 @@ expression: tracks
src_id: "ySChj_9rT5Y", src_id: "ySChj_9rT5Y",
service: yt, service: yt,
name: "Kennst du das", name: "Kennst du das",
duration: Some(191), duration: Some(191000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -95,7 +95,7 @@ expression: tracks
src_id: "revpIT2HiNs", src_id: "revpIT2HiNs",
service: yt, service: yt,
name: "Wohin willst du", name: "Wohin willst du",
duration: Some(255), duration: Some(255000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -117,7 +117,7 @@ expression: tracks
src_id: "LeEgBsYfjLU", src_id: "LeEgBsYfjLU",
service: yt, service: yt,
name: "Vakuum", name: "Vakuum",
duration: Some(220), duration: Some(220000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -139,7 +139,7 @@ expression: tracks
src_id: "-i5XjMkQN8M", src_id: "-i5XjMkQN8M",
service: yt, service: yt,
name: "Melodie", name: "Melodie",
duration: Some(251), duration: Some(251000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -161,7 +161,7 @@ expression: tracks
src_id: "DhlIZkoPsxg", src_id: "DhlIZkoPsxg",
service: yt, service: yt,
name: "Du & Ich", name: "Du & Ich",
duration: Some(238), duration: Some(238000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -183,7 +183,7 @@ expression: tracks
src_id: "LCoomBMOkgU", src_id: "LCoomBMOkgU",
service: yt, service: yt,
name: "Schwerelos", name: "Schwerelos",
duration: Some(198), duration: Some(198000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -205,7 +205,7 @@ expression: tracks
src_id: "c6Ot-Z3HEBo", src_id: "c6Ot-Z3HEBo",
service: yt, service: yt,
name: "Lichtermeer", name: "Lichtermeer",
duration: Some(164), duration: Some(164000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -227,7 +227,7 @@ expression: tracks
src_id: "ybm_4hQG0ok", src_id: "ybm_4hQG0ok",
service: yt, service: yt,
name: "Nachtzug", name: "Nachtzug",
duration: Some(290), duration: Some(290000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -249,7 +249,7 @@ expression: tracks
src_id: "DJKmtK5PmSY", src_id: "DJKmtK5PmSY",
service: yt, service: yt,
name: "Rückenwind", name: "Rückenwind",
duration: Some(263), duration: Some(263000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),
@ -271,7 +271,7 @@ expression: tracks
src_id: "hWFarQmaQAQ", src_id: "hWFarQmaQAQ",
service: yt, service: yt,
name: "Immer wenn wir uns sehn (\"Das schönste Mädchen der Welt\", Soundtrack)", name: "Immer wenn wir uns sehn (\"Das schönste Mädchen der Welt\", Soundtrack)",
duration: Some(186), duration: Some(186000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),

View file

@ -10,7 +10,7 @@ expression: tracks
src_id: "WSBUeFdXiSs", src_id: "WSBUeFdXiSs",
service: yt, service: yt,
name: "Leicht", name: "Leicht",
duration: Some(206), duration: Some(206000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC-2mb3G26qV676d-iXbOTVQ"), id: Some("yt:UC-2mb3G26qV676d-iXbOTVQ"),
@ -36,7 +36,7 @@ expression: tracks
src_id: "OCgE2GSL1Pk", src_id: "OCgE2GSL1Pk",
service: yt, service: yt,
name: "Smoke Signals", name: "Smoke Signals",
duration: Some(197), duration: Some(197000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UCQ6yypykkyPLM5FVhOm4Eog"), id: Some("yt:UCQ6yypykkyPLM5FVhOm4Eog"),
@ -62,7 +62,7 @@ expression: tracks
src_id: "6485PhOtHzY", src_id: "6485PhOtHzY",
service: yt, service: yt,
name: "Lieblingsmensch", name: "Lieblingsmensch",
duration: Some(190), duration: Some(190000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UCIh4j8fXWf2U0ro0qnGU8Mg"), id: Some("yt:UCIh4j8fXWf2U0ro0qnGU8Mg"),

View file

@ -8,6 +8,7 @@ Track(
service: yt, service: yt,
name: "empty", name: "empty",
duration: None, duration: None,
duration_ms: false,
artists: [], artists: [],
size: None, size: None,
loudness: None, loudness: None,

View file

@ -7,7 +7,8 @@ Track(
src_id: "g0iRiJ_ck48", src_id: "g0iRiJ_ck48",
service: yt, service: yt,
name: "Aulë und Yavanna", name: "Aulë und Yavanna",
duration: Some(216), duration: Some(216000),
duration_ms: false,
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),

View file

@ -6,7 +6,7 @@ TrackSlim(
src_id: "g0iRiJ_ck48", src_id: "g0iRiJ_ck48",
service: yt, service: yt,
name: "Aulë und Yavanna", name: "Aulë und Yavanna",
duration: Some(216), duration: Some(216000),
artists: [ artists: [
ArtistId( ArtistId(
id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"), id: Some("yt:UC_MxOdawj_BStPs4CKBYD0Q"),

View file

@ -6,7 +6,7 @@ use time::{Date, PrimitiveDateTime};
use super::{ use super::{
album::AlbumId, album::AlbumId,
artist::{ArtistId, ArtistJsonb}, artist::{ArtistId, ArtistJsonb},
map_artists, AlbumType, Id, MusicService, SrcId, SrcIdOwned, map_artists, AlbumType, Artist, Id, MusicService, SrcId, SrcIdOwned,
}; };
use crate::{ use crate::{
error::{DatabaseError, OptionalRes}, error::{DatabaseError, OptionalRes},
@ -20,6 +20,7 @@ pub struct Track {
pub service: MusicService, pub service: MusicService,
pub name: String, pub name: String,
pub duration: Option<i32>, pub duration: Option<i32>,
pub duration_ms: bool,
pub artists: Vec<ArtistId>, pub artists: Vec<ArtistId>,
pub size: Option<i64>, pub size: Option<i64>,
pub loudness: Option<f32>, pub loudness: Option<f32>,
@ -42,6 +43,7 @@ struct TrackRow {
service: MusicService, service: MusicService,
name: String, name: String,
duration: Option<i32>, duration: Option<i32>,
duration_ms: bool,
size: Option<i64>, size: Option<i64>,
loudness: Option<f32>, loudness: Option<f32>,
album_id: i32, album_id: i32,
@ -65,6 +67,7 @@ pub struct TrackNew<'a> {
pub service: MusicService, pub service: MusicService,
pub name: &'a str, pub name: &'a str,
pub duration: Option<i32>, pub duration: Option<i32>,
pub duration_ms: bool,
pub size: Option<i64>, pub size: Option<i64>,
pub loudness: Option<f32>, pub loudness: Option<f32>,
pub album_id: i32, pub album_id: i32,
@ -80,6 +83,7 @@ pub struct TrackNew<'a> {
pub struct TrackUpdate<'a> { pub struct TrackUpdate<'a> {
pub name: Option<&'a str>, pub name: Option<&'a str>,
pub duration: Option<Option<i32>>, pub duration: Option<Option<i32>>,
pub duration_ms: Option<bool>,
pub size: Option<Option<i64>>, pub size: Option<Option<i64>>,
pub loudness: Option<Option<f32>>, pub loudness: Option<Option<f32>>,
pub album_id: Option<i32>, pub album_id: Option<i32>,
@ -148,7 +152,7 @@ impl Track {
Id::Db(id) => { Id::Db(id) => {
sqlx::query_as!( sqlx::query_as!(
TrackRow, TrackRow,
r#"select t.id, t.src_id, t.service as "service: _", t.name, t.duration, r#"select t.id, t.src_id, t.service as "service: _", t.name, t.duration, t.duration_ms,
t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at, t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,
t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams, t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,
jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq) jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)
@ -166,7 +170,7 @@ group by t.id"#,
Id::Src(src_id, srv) => { Id::Src(src_id, srv) => {
let res = sqlx::query_as!( let res = sqlx::query_as!(
TrackRow, TrackRow,
r#"select t.id, t.src_id, t.service as "service: _", t.name, t.duration, r#"select t.id, t.src_id, t.service as "service: _", t.name, t.duration, t.duration_ms,
t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at, t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,
t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams, t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,
jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq) jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)
@ -188,7 +192,7 @@ group by t.id"#,
None => { None => {
sqlx::query_as!( sqlx::query_as!(
TrackRow, TrackRow,
r#"select t.id, t.src_id, t.service as "service: _", t.name, t.duration, r#"select t.id, t.src_id, t.service as "service: _", t.name, t.duration, t.duration_ms,
t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at, t.size, t.loudness, t.album_id, t.album_pos, t.ul_artists, t.isrc, t.description, t.created_at, t.updated_at,
t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams, t.primary_track, t.downloaded_at, t.last_streamed_at, t.n_streams,
jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq) jsonb_agg(json_build_object('id', a.src_id, 'sv', a.service, 'n', a.name) order by art.seq)
@ -280,7 +284,7 @@ where ta.src_id=$1 and ta.service=$2"#,
pub async fn set_artists( pub async fn set_artists(
id: i32, id: i32,
artists: &[i32], artists: &[Id<'_>],
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> { ) -> Result<(), DatabaseError> {
sqlx::query!(r#"delete from artists_tracks where track_id=$1"#, id) sqlx::query!(r#"delete from artists_tracks where track_id=$1"#, id)
@ -288,6 +292,7 @@ where ta.src_id=$1 and ta.service=$2"#,
.await?; .await?;
for artist_id in artists { for artist_id in artists {
let artist_id = Artist::resolve_id(*artist_id, tx).await?;
sqlx::query!( sqlx::query!(
r#"insert into artists_tracks (track_id, artist_id) values ($1, $2)"#, r#"insert into artists_tracks (track_id, artist_id) values ($1, $2)"#,
id, id,
@ -347,12 +352,14 @@ impl TrackNew<'_> {
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
let res = sqlx::query!( let res = sqlx::query!(
r#"insert into tracks (src_id, service, name, duration, r#"insert into tracks (src_id, service, name, duration, duration_ms,
size, loudness, album_id, album_pos, ul_artists, isrc, description, primary_track) size, loudness, album_id, album_pos, ul_artists, isrc, description, primary_track)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
on conflict (src_id, service) do update set on conflict (src_id, service) do update set
name = excluded.name, name = excluded.name,
duration = coalesce(excluded.duration, tracks.duration), duration = case when tracks.duration_ms and not excluded.duration_ms
then tracks.duration else coalesce(excluded.duration, tracks.duration) end,
duration_ms = excluded.duration_ms or tracks.duration_ms,
size = coalesce(excluded.size, tracks.size), size = coalesce(excluded.size, tracks.size),
loudness = coalesce(excluded.loudness, tracks.loudness), loudness = coalesce(excluded.loudness, tracks.loudness),
album_id = excluded.album_id, album_id = excluded.album_id,
@ -366,6 +373,7 @@ returning id"#,
self.service as MusicService, self.service as MusicService,
self.name, self.name,
self.duration, self.duration,
self.duration_ms,
self.size, self.size,
self.loudness, self.loudness,
self.album_id, self.album_id,
@ -402,6 +410,14 @@ impl TrackUpdate<'_> {
query.push_bind(duration); query.push_bind(duration);
n += 1; n += 1;
} }
if let Some(duration_ms) = &self.duration_ms {
if n != 0 {
query.push(", ");
}
query.push("duration_ms=");
query.push_bind(duration_ms);
n += 1;
}
if let Some(size) = &self.size { if let Some(size) = &self.size {
if n != 0 { if n != 0 {
query.push(", "); query.push(", ");
@ -583,6 +599,7 @@ impl From<TrackRow> for Track {
service: value.service, service: value.service,
name: value.name, name: value.name,
duration: value.duration, duration: value.duration,
duration_ms: value.duration_ms,
artists: map_artists(value.artists, value.ul_artists), artists: map_artists(value.artists, value.ul_artists),
size: value.size, size: value.size,
loudness: value.loudness, loudness: value.loudness,
@ -637,7 +654,8 @@ mod tests {
src_id: "g0iRiJ_ck48", src_id: "g0iRiJ_ck48",
service: MusicService::YouTube, service: MusicService::YouTube,
name: "Aulë und Yavanna", name: "Aulë und Yavanna",
duration: Some(216), duration: Some(216000),
duration_ms: false,
size: Some(3_439_414), size: Some(3_439_414),
loudness: Some(6.1513805), loudness: Some(6.1513805),
album_id: 1, album_id: 1,
@ -647,7 +665,7 @@ mod tests {
description: Some("Hello World"), description: Some("Hello World"),
primary_track: Some(true), primary_track: Some(true),
}; };
let track_artists = [ids::ARTIST_ID_LEA, ids::ARTIST_ID_CYRIL]; let track_artists = [ids::ARTIST_LEA, ids::ARTIST_CYRIL];
// Create // Create
let mut c_tx = pool.begin().await.unwrap(); let mut c_tx = pool.begin().await.unwrap();
@ -686,6 +704,7 @@ mod tests {
let clear = TrackUpdate { let clear = TrackUpdate {
name: Some("empty"), name: Some("empty"),
duration: Some(None), duration: Some(None),
duration_ms: Some(false),
size: Some(None), size: Some(None),
loudness: Some(None), loudness: Some(None),
album_id: None, album_id: None,

View file

@ -36,25 +36,25 @@ INSERT INTO artists_albums (artist_id,album_id) VALUES
(8,6), (8,6),
(9,7); (9,7);
INSERT INTO tracks (src_id,service,"name",duration,"size",loudness,album_pos,album_id,ul_artists,isrc,created_at,updated_at,primary_track,downloaded_at,last_streamed_at,n_streams) VALUES INSERT INTO tracks (src_id,service,"name",duration,duration_ms,"size",loudness,album_pos,album_id,ul_artists,isrc,created_at,updated_at,primary_track,downloaded_at,last_streamed_at,n_streams) VALUES
('2txScm52-QI','yt','Die Segel sind gesetzt',186,NULL,NULL,1,1,'{}',NULL,'2023-08-30 22:40:30.15406','2023-08-30 22:49:15.955139',NULL,NULL,NULL,0), ('2txScm52-QI','yt','Die Segel sind gesetzt',186000,false,NULL,NULL,1,1,'{}',NULL,'2023-08-30 22:40:30.15406','2023-08-30 22:49:15.955139',NULL,NULL,NULL,0),
('oZKv47vyqQU','yt','Monster',224,NULL,NULL,2,1,'{}',NULL,'2023-08-30 22:41:22.905632','2023-08-30 22:49:15.957959',NULL,NULL,NULL,0), ('oZKv47vyqQU','yt','Monster',224000,false,NULL,NULL,2,1,'{}',NULL,'2023-08-30 22:41:22.905632','2023-08-30 22:49:15.957959',NULL,NULL,NULL,0),
('7WXlMU9ItnA','yt','Dach',231,NULL,NULL,3,1,'{}',NULL,'2023-08-30 22:42:00.093845','2023-08-30 22:49:15.959538',NULL,NULL,NULL,0), ('7WXlMU9ItnA','yt','Dach',231000,false,NULL,NULL,3,1,'{}',NULL,'2023-08-30 22:42:00.093845','2023-08-30 22:49:15.959538',NULL,NULL,NULL,0),
('ySChj_9rT5Y','yt','Kennst du das',191,NULL,NULL,4,1,'{}',NULL,'2023-08-30 22:42:57.462304','2023-08-30 22:49:15.961008',NULL,NULL,NULL,0), ('ySChj_9rT5Y','yt','Kennst du das',191000,false,NULL,NULL,4,1,'{}',NULL,'2023-08-30 22:42:57.462304','2023-08-30 22:49:15.961008',NULL,NULL,NULL,0),
('revpIT2HiNs','yt','Wohin willst du',255,NULL,NULL,5,1,'{}',NULL,'2023-08-30 22:43:39.594609','2023-08-30 22:49:15.962497',NULL,NULL,NULL,0), ('revpIT2HiNs','yt','Wohin willst du',255000,false,NULL,NULL,5,1,'{}',NULL,'2023-08-30 22:43:39.594609','2023-08-30 22:49:15.962497',NULL,NULL,NULL,0),
('LeEgBsYfjLU','yt','Vakuum',220,NULL,NULL,6,1,'{}',NULL,'2023-08-30 22:44:14.749107','2023-08-30 22:49:15.963926',NULL,NULL,NULL,0), ('LeEgBsYfjLU','yt','Vakuum',220000,false,NULL,NULL,6,1,'{}',NULL,'2023-08-30 22:44:14.749107','2023-08-30 22:49:15.963926',NULL,NULL,NULL,0),
('-i5XjMkQN8M','yt','Melodie',251,NULL,NULL,7,1,'{}',NULL,'2023-08-30 22:44:44.585095','2023-08-30 22:49:15.96526',NULL,NULL,NULL,0), ('-i5XjMkQN8M','yt','Melodie',251000,false,NULL,NULL,7,1,'{}',NULL,'2023-08-30 22:44:44.585095','2023-08-30 22:49:15.96526',NULL,NULL,NULL,0),
('DhlIZkoPsxg','yt','Du & Ich',238,NULL,NULL,8,1,'{}',NULL,'2023-08-30 22:45:20.711259','2023-08-30 22:49:15.966532',NULL,NULL,NULL,0), ('DhlIZkoPsxg','yt','Du & Ich',238000,false,NULL,NULL,8,1,'{}',NULL,'2023-08-30 22:45:20.711259','2023-08-30 22:49:15.966532',NULL,NULL,NULL,0),
('LCoomBMOkgU','yt','Schwerelos',198,NULL,NULL,9,1,'{}',NULL,'2023-08-30 22:46:13.885768','2023-08-30 22:49:15.967865',NULL,NULL,NULL,0), ('LCoomBMOkgU','yt','Schwerelos',198000,false,NULL,NULL,9,1,'{}',NULL,'2023-08-30 22:46:13.885768','2023-08-30 22:49:15.967865',NULL,NULL,NULL,0),
('c6Ot-Z3HEBo','yt','Lichtermeer',164,NULL,NULL,10,1,'{}',NULL,'2023-08-30 22:46:41.10766','2023-08-30 22:49:15.969191',NULL,NULL,NULL,0), ('c6Ot-Z3HEBo','yt','Lichtermeer',164000,false,NULL,NULL,10,1,'{}',NULL,'2023-08-30 22:46:41.10766','2023-08-30 22:49:15.969191',NULL,NULL,NULL,0),
('ybm_4hQG0ok','yt','Nachtzug',290,NULL,NULL,11,1,'{}',NULL,'2023-08-30 22:47:28.033803','2023-08-30 22:49:15.97051',NULL,NULL,NULL,0), ('ybm_4hQG0ok','yt','Nachtzug',290000,false,NULL,NULL,11,1,'{}',NULL,'2023-08-30 22:47:28.033803','2023-08-30 22:49:15.97051',NULL,NULL,NULL,0),
('DJKmtK5PmSY','yt','Rückenwind',263,NULL,NULL,12,1,'{}',NULL,'2023-08-30 22:47:54.822762','2023-08-30 22:49:15.971789',NULL,NULL,NULL,0), ('DJKmtK5PmSY','yt','Rückenwind',263000,false,NULL,NULL,12,1,'{}',NULL,'2023-08-30 22:47:54.822762','2023-08-30 22:49:15.971789',NULL,NULL,NULL,0),
('hWFarQmaQAQ','yt','Immer wenn wir uns sehn ("Das schönste Mädchen der Welt", Soundtrack)',186,NULL,NULL,1,2,'{}',NULL,'2023-08-30 23:00:09.653056','2023-08-30 23:00:09.653056',NULL,NULL,NULL,0), ('hWFarQmaQAQ','yt','Immer wenn wir uns sehn ("Das schönste Mädchen der Welt", Soundtrack)',186000,false,NULL,NULL,1,2,'{}',NULL,'2023-08-30 23:00:09.653056','2023-08-30 23:00:09.653056',NULL,NULL,NULL,0),
('tXb7WTkhE1c','yt','Das schönste Mädchen der Welt ("Das schönste Mädchen der Welt", Soundtrack)',142,NULL,NULL,1,3,'{}',NULL,'2023-08-30 23:00:09.653056','2023-08-30 23:00:09.653056',NULL,NULL,NULL,0), ('tXb7WTkhE1c','yt','Das schönste Mädchen der Welt ("Das schönste Mädchen der Welt", Soundtrack)',142000,false,NULL,NULL,1,3,'{}',NULL,'2023-08-30 23:00:09.653056','2023-08-30 23:00:09.653056',NULL,NULL,NULL,0),
('ZeerrnuLi5E','yt','Black Mamba',229,NULL,NULL,1,4,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0), ('ZeerrnuLi5E','yt','Black Mamba',229000,false,NULL,NULL,1,4,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0),
('OCgE2GSL1Pk','yt','Smoke Signals',197,NULL,NULL,NULL,5,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0), ('OCgE2GSL1Pk','yt','Smoke Signals',197000,false,NULL,NULL,NULL,5,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0),
('6485PhOtHzY','yt','Lieblingsmensch',190,NULL,NULL,1,6,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0), ('6485PhOtHzY','yt','Lieblingsmensch',190000,false,NULL,NULL,1,6,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0),
('WSBUeFdXiSs','yt','Leicht',206,NULL,NULL,NULL,7,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0); ('WSBUeFdXiSs','yt','Leicht',206000,false,NULL,NULL,NULL,7,'{}',NULL,'2023-09-09 22:53:50.360159','2023-09-09 22:53:50.360159',NULL,NULL,NULL,0);
INSERT INTO artists_tracks (artist_id,track_id) VALUES INSERT INTO artists_tracks (artist_id,track_id) VALUES
(1,1), (1,1),

View file

@ -15,7 +15,7 @@ futures.workspace = true
time.workspace = true time.workspace = true
once_cell.workspace = true once_cell.workspace = true
regex.workspace = true regex.workspace = true
tracing.workspace = true log.workspace = true
quick_cache.workspace = true quick_cache.workspace = true
siphasher.workspace = true siphasher.workspace = true
hex-literal.workspace = true hex-literal.workspace = true
@ -25,6 +25,5 @@ tiraya-db.workspace = true
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio.workspace = true
sqlx-database-tester.workspace = true sqlx-database-tester.workspace = true
rstest.workspace = true
env_logger.workspace = true env_logger.workspace = true
test-log.workspace = true test-log.workspace = true

View file

@ -0,0 +1 @@
{"desktop_client":null,"music_client":{"last_update":"2023-09-20T17:47:33Z","data":{"version":"1.20230913.01.00-canary_control"}},"deobf":null}

View file

@ -3,20 +3,22 @@
pub mod error; pub mod error;
mod util; mod util;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, hash::Hasher, sync::Arc};
use error::ExtractorError; use error::ExtractorError;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use hex_literal::hex;
use quick_cache::sync::Cache; use quick_cache::sync::Cache;
use rustypipe::{client::RustyPipe, model::richtext::ToPlaintext}; use rustypipe::client::RustyPipe;
use siphasher::sip128::{Hasher128, SipHasher};
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use time::{Date, Duration, OffsetDateTime}; use time::{Date, Duration, OffsetDateTime};
use tiraya_db::{ use tiraya_db::{
error::OptionalRes, error::OptionalRes,
models::{ models::{
Album, AlbumNew, AlbumType, AlbumUpdate, Artist, ArtistNew, DatePrecision, EntityType, Id, Album, AlbumNew, AlbumType, Artist, ArtistNew, DatePrecision, EntityType, Id, MusicService,
MusicService, Playlist, PlaylistImgType, PlaylistNew, PlaylistType, SrcId, SrcIdOwned, Playlist, PlaylistImgType, PlaylistNew, PlaylistType, SrcId, SrcIdOwned, SyncData, Track,
SyncData, Track, TrackNew, TrackUpdate, TrackNew,
}, },
}; };
@ -35,7 +37,6 @@ const ARTIST_STALE: Duration = Duration::hours(24);
const CONCURRENCY: usize = 4; const CONCURRENCY: usize = 4;
const DB_CONCURRENCY: usize = 8; const DB_CONCURRENCY: usize = 8;
#[derive(Debug)]
struct SyncLastUpdate { struct SyncLastUpdate {
id: i32, id: i32,
state: LastUpdateState, state: LastUpdateState,
@ -90,7 +91,6 @@ impl Extractor {
) )
} }
#[tracing::instrument(skip(self))]
pub async fn get_artist(&self, id: SrcId<'_>) -> Result<GetResult<Artist>, ExtractorError> { pub async fn get_artist(&self, id: SrcId<'_>) -> Result<GetResult<Artist>, ExtractorError> {
let artist = Artist::get(id.id(), &self.db).await.to_optional().unwrap(); let artist = Artist::get(id.id(), &self.db).await.to_optional().unwrap();
let last_update = if let Some(artist) = artist { let last_update = if let Some(artist) = artist {
@ -143,7 +143,6 @@ impl Extractor {
} }
} }
#[tracing::instrument(skip(self))]
async fn update_yt_artist( async fn update_yt_artist(
&self, &self,
id: &str, id: &str,
@ -194,7 +193,7 @@ impl Extractor {
let top_track_ids = let top_track_ids =
futures::stream::iter(artist.tracks.into_iter().filter(|t| !t.is_video)) futures::stream::iter(artist.tracks.into_iter().filter(|t| !t.is_video))
.map(|track| async { self.import_yt_track(track, None, &[]).await }) .map(|track| async move { self.import_yt_track(track, None).await })
.buffered(CONCURRENCY) .buffered(CONCURRENCY)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await; .await;
@ -203,14 +202,14 @@ impl Extractor {
.filter_map(|id| match id { .filter_map(|id| match id {
Ok(id) => Some(id), Ok(id) => Some(id),
Err(e) => { Err(e) => {
tracing::error!("could not import artist track: {}", e); log::error!("could not import artist track: {}", e);
None None
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let related_artist_ids = futures::stream::iter(artist.similar_artists) let related_artist_ids = futures::stream::iter(artist.similar_artists)
.map(|artist| async { self.import_yt_artist_item(artist).await }) .map(|artist| async move { self.import_yt_artist_item(artist).await })
.buffered(DB_CONCURRENCY) .buffered(DB_CONCURRENCY)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await; .await;
@ -219,14 +218,14 @@ impl Extractor {
.filter_map(|id| match id { .filter_map(|id| match id {
Ok(id) => Some(id), Ok(id) => Some(id),
Err(e) => { Err(e) => {
tracing::error!("could not import related artist: {}", e); log::error!("could not import related artist: {}", e);
None None
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let related_playlist_ids = futures::stream::iter(artist.playlists) let related_playlist_ids = futures::stream::iter(artist.playlists)
.map(|pl| async { self.import_yt_playlist_item(pl).await }) .map(|pl| async move { self.import_yt_playlist_item(pl).await })
.buffered(DB_CONCURRENCY) .buffered(DB_CONCURRENCY)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await; .await;
@ -235,7 +234,7 @@ impl Extractor {
.filter_map(|id| match id { .filter_map(|id| match id {
Ok(id) => Some(id), Ok(id) => Some(id),
Err(e) => { Err(e) => {
tracing::error!("could not import artist playlist: {}", e); log::error!("could not import artist playlist: {}", e);
None None
} }
}) })
@ -277,7 +276,7 @@ impl Extractor {
// Insert all albums // Insert all albums
futures::stream::iter(artist.albums) futures::stream::iter(artist.albums)
.map(Ok) .map(Ok)
.try_for_each_concurrent(DB_CONCURRENCY, |album| async { .try_for_each_concurrent(DB_CONCURRENCY, |album| async move {
self.import_yt_album_item(album).await self.import_yt_album_item(album).await
}) })
.await?; .await?;
@ -289,32 +288,35 @@ impl Extractor {
}; };
Artist::set_last_sync(artist_id, this_update_state.into(), &self.db).await?; Artist::set_last_sync(artist_id, this_update_state.into(), &self.db).await?;
tracing::info!("fetched artist [yt:{}] {}", artist.id, artist.name);
Ok(GetResult::fetched(artist_id)) Ok(GetResult::fetched(artist_id))
} }
#[tracing::instrument(skip(self))] pub async fn update_yt_artist_albums(&self, id: i32) -> Result<(), ExtractorError> {
pub async fn fetch_yt_artist_albums(&self, id: i32) -> Result<(), ExtractorError> { let dirty_albums = Artist::dirty_album_ids(id, &self.db).await?;
for _ in 0..2 { let more_albums = futures::stream::iter(dirty_albums)
let dirty_albums = Artist::dirty_album_ids(id, &self.db).await?; .map(|id| async move {
let has_more = futures::stream::iter(dirty_albums) match self.import_yt_album(&id.src_id).await {
.map(|id| async move { Ok(more) => more,
match self.fetch_yt_album(&id.src_id).await { Err(e) => {
Ok(more) => more.1, log::error!("could not import album [yt:{}]: {}", id.src_id, e);
Err(e) => { Vec::new()
tracing::error!("could not import album [yt:{}]: {}", id.src_id, e);
false
}
} }
}) }
.buffer_unordered(DB_CONCURRENCY) })
.collect::<Vec<_>>() .buffer_unordered(DB_CONCURRENCY)
.await; .collect::<Vec<_>>()
if has_more.iter().all(|x| !x) { .await;
break;
} futures::stream::iter(more_albums.into_iter().flatten())
} .for_each_concurrent(DB_CONCURRENCY, |album| async move {
tracing::info!("updated artist albums for #{id}"); match self.import_yt_album(&album.id).await {
Ok(_) => {}
Err(e) => {
log::error!("could not import album [yt:{}]: {}", album.id, e);
}
}
})
.await;
Ok(()) Ok(())
} }
@ -333,7 +335,6 @@ impl Extractor {
&self, &self,
track: rpmodel::TrackItem, track: rpmodel::TrackItem,
album_id: Option<i32>, album_id: Option<i32>,
album_artists: &[ArtistIdName],
) -> Result<i32, ExtractorError> { ) -> Result<i32, ExtractorError> {
if album_id.is_none() { if album_id.is_none() {
if let Some(id) = if let Some(id) =
@ -343,9 +344,7 @@ impl Extractor {
} }
} }
let (artists, ul_artists) = self let (artists, ul_artists) = util::split_yt_artists(track.artists);
.split_yt_artists(track.artists, track.artist_id, album_artists)
.await;
let album_id = match album_id { let album_id = match album_id {
Some(album_id) => album_id, Some(album_id) => album_id,
@ -389,7 +388,8 @@ impl Extractor {
src_id: &track.id, src_id: &track.id,
service: MusicService::YouTube, service: MusicService::YouTube,
name: &track.name, name: &track.name,
duration: track.duration.and_then(|v| v.try_into().ok()), duration: track.duration.and_then(|v| (v * 1000).try_into().ok()),
duration_ms: false,
album_id, album_id,
album_pos: track.track_nr.and_then(|v| v.try_into().ok()), album_pos: track.track_nr.and_then(|v| v.try_into().ok()),
ul_artists: ul_artists.as_deref(), ul_artists: ul_artists.as_deref(),
@ -400,42 +400,39 @@ impl Extractor {
Track::set_artists(track_id, &artist_ids, &mut tx).await?; Track::set_artists(track_id, &artist_ids, &mut tx).await?;
tx.commit().await?; tx.commit().await?;
tracing::debug!("imported track [yt:{}] {}", track.id, track.name);
Ok(track_id) Ok(track_id)
} }
/// Import a list of YT Music artist ids
async fn import_yt_artist_ids( async fn import_yt_artist_ids(
&self, &self,
artists: &[ArtistIdName], artists: &[ArtistIdName],
) -> Result<Vec<i32>, ExtractorError> { ) -> Result<Vec<Id<'_>>, ExtractorError> {
futures::stream::iter(artists) futures::stream::iter(artists)
.map(|aid| async { self.import_yt_artist_id(aid).await }) .map(|aid| async move { self.import_yt_artist_id(aid).await.map(Id::Db) })
.buffered(CONCURRENCY) .buffered(CONCURRENCY)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await .await
.map_err(ExtractorError::from) .map_err(ExtractorError::from)
} }
/// Import a YT Music artist id (artist with ID and name)
async fn import_yt_artist_id(&self, aid: &ArtistIdName) -> Result<i32, ExtractorError> { async fn import_yt_artist_id(&self, aid: &ArtistIdName) -> Result<i32, ExtractorError> {
let id_owned = SrcIdOwned(aid.id.to_owned(), MusicService::YouTube); let id_owned = SrcIdOwned(aid.id.to_owned(), MusicService::YouTube);
self.artist_cache self.artist_cache
.get_or_insert_async(&id_owned, async { .get_or_insert_async(&id_owned, async move {
let artist = ArtistNew { let artist = ArtistNew {
src_id: &aid.id, src_id: &aid.id,
service: MusicService::YouTube, service: MusicService::YouTube,
name: &aid.name, name: &aid.name,
..Default::default() ..Default::default()
}; };
let artist_id = artist.upsert_recessive(&self.db).await?; artist
tracing::debug!("imported artist id [yt:{}] {}", aid.id, aid.name); .upsert_recessive(&self.db)
Ok(artist_id) .await
.map_err(ExtractorError::from)
}) })
.await .await
} }
/// Import a YT Music artist item (artist with name, image and subscriber count)
async fn import_yt_artist_item( async fn import_yt_artist_item(
&self, &self,
artist: rpmodel::ArtistItem, artist: rpmodel::ArtistItem,
@ -452,13 +449,11 @@ impl Extractor {
..Default::default() ..Default::default()
}; };
let artist_id = n_artist.upsert_recessive(&self.db).await?; let artist_id = n_artist.upsert_recessive(&self.db).await?;
tracing::debug!("imported artist item [yt:{}] {}", artist.id, artist.name);
self.artist_cache self.artist_cache
.insert(SrcIdOwned(artist.id, MusicService::YouTube), artist_id); .insert(SrcIdOwned(artist.id, MusicService::YouTube), artist_id);
Ok(artist_id) Ok(artist_id)
} }
/// Import a YT Music album item (album from artist page)
async fn import_yt_album_item(&self, album: rpmodel::AlbumItem) -> Result<(), ExtractorError> { async fn import_yt_album_item(&self, album: rpmodel::AlbumItem) -> Result<(), ExtractorError> {
// Return if the album was already imported // Return if the album was already imported
if Album::get_id_clean(SrcId(&album.id, MusicService::YouTube), &self.db) if Album::get_id_clean(SrcId(&album.id, MusicService::YouTube), &self.db)
@ -468,9 +463,7 @@ impl Extractor {
return Ok(()); return Ok(());
} }
let (artists, ul_artists) = self let (artists, ul_artists) = util::split_yt_artists(album.artists);
.split_yt_artists(album.artists, album.artist_id, &[])
.await;
let artist_ids = self.import_yt_artist_ids(&artists).await?; let artist_ids = self.import_yt_artist_ids(&artists).await?;
let image_url = util::get_image_url(&album.cover, false); let image_url = util::get_image_url(&album.cover, false);
@ -493,11 +486,9 @@ impl Extractor {
Album::add_artists(album_id, &artist_ids, &mut tx).await?; Album::add_artists(album_id, &artist_ids, &mut tx).await?;
tx.commit().await?; tx.commit().await?;
tracing::debug!("imported album item [yt:{}] {}", album.id, album.name);
Ok(()) Ok(())
} }
/// Import a YT Music playlist item
async fn import_yt_playlist_item( async fn import_yt_playlist_item(
&self, &self,
pl: rpmodel::MusicPlaylistItem, pl: rpmodel::MusicPlaylistItem,
@ -528,46 +519,30 @@ impl Extractor {
image_type: Some(PlaylistImgType::Custom), image_type: Some(PlaylistImgType::Custom),
..Default::default() ..Default::default()
}; };
let playlist_id = playlist_n.upsert(&self.db).await?; playlist_n
.upsert(&self.db)
tracing::debug!("imported playlist item [yt:{}] {}", pl.id, pl.name); .await
Ok(playlist_id) .map_err(ExtractorError::from)
} }
/// Fetch and import a YT Music album and return its ID and whether the album contains variants async fn import_yt_album(&self, id: &str) -> Result<Vec<rpmodel::AlbumItem>, ExtractorError> {
async fn fetch_yt_album(&self, id: &str) -> Result<(i32, bool), ExtractorError> {
// Return if the album was already imported
if let Some(album_id) =
Album::get_id_clean(SrcId(id, MusicService::YouTube), &self.db).await?
{
return Ok((album_id, false));
}
let album = self.rp.query().music_album(id).await?; let album = self.rp.query().music_album(id).await?;
let (artists, ul_artists) = self let (artists, ul_artists) = util::split_yt_artists(album.artists);
.split_yt_artists(album.artists, album.artist_id, &[])
.await;
let image_url = util::get_image_url(&album.cover, false); let image_url = util::get_image_url(&album.cover, false);
let artist_ids = self.import_yt_artist_ids(&artists).await?; let artist_ids = self.import_yt_artist_ids(&artists).await?;
// Get album hash // Get album hash
let mut hasher = util::AlbumHasher::new(); let mut hasher = SipHasher::new_with_key(&hex!("e0060fd1ea207d8f43d2bf9bcae63f65"));
for track in &album.tracks { for track in &album.tracks {
hasher.add_track(&track.name, track.duration.unwrap_or_default()); hasher.write(track.name.as_bytes());
hasher.write_u32(track.duration.unwrap_or_default());
} }
let album_hash = hasher.finish(); let album_hash = hasher.finish128().as_bytes();
let hidden = if let Some(id) = artist_ids.first() {
!Album::ids_from_hash(*id, &album_hash, &self.db)
.await?
.is_empty()
} else {
false
};
// Insert album // Insert album
// TODO: accurate release date
let album_n = AlbumNew { let album_n = AlbumNew {
src_id: &album.id, src_id: &album.id,
service: MusicService::YouTube, service: MusicService::YouTube,
@ -580,7 +555,6 @@ impl Extractor {
ul_artists: ul_artists.as_deref(), ul_artists: ul_artists.as_deref(),
by_va: album.by_va, by_va: album.by_va,
image_url: image_url.as_deref(), image_url: image_url.as_deref(),
hidden,
album_hash: Some(&album_hash), album_hash: Some(&album_hash),
..Default::default() ..Default::default()
}; };
@ -589,176 +563,18 @@ impl Extractor {
Album::set_artists(album_id, &artist_ids, &mut tx).await?; Album::set_artists(album_id, &artist_ids, &mut tx).await?;
tx.commit().await?; tx.commit().await?;
let first_track_id = album.tracks.first().map(|t| t.id.to_owned());
// Insert tracks // Insert tracks
futures::stream::iter(album.tracks.into_iter().map(Ok::<_, ExtractorError>)) futures::stream::iter(album.tracks.into_iter().map(Ok::<_, ExtractorError>))
.try_for_each_concurrent(DB_CONCURRENCY, |track| async { .try_for_each_concurrent(DB_CONCURRENCY, |track| async move {
self.import_yt_track(track, Some(album_id), &artists) self.import_yt_track(track, Some(album_id)).await?;
.await?;
Ok(()) Ok(())
}) })
.await?; .await?;
// Insert album variants
let has_variants = !album.variants.is_empty();
futures::stream::iter(album.variants.into_iter().map(Ok::<_, ExtractorError>))
.try_for_each_concurrent(DB_CONCURRENCY, |album| async {
self.import_yt_album_item(album).await
})
.await?;
// Fetch more accurate album date
if let Some(first_track_id) = first_track_id {
self.fetch_track_details(album_id, album.year, &first_track_id, true)
.await?;
}
// Mark album clean // Mark album clean
Album::mark_dirty(album_id, false, &self.db).await?; Album::mark_dirty(album_id, false, &self.db).await?;
tracing::info!("imported album [yt:{}] {}", album.id, album.name); Ok(album.variants)
Ok((album_id, has_variants))
}
/// Fetch the description text and upload date of a YT video and store it in the database.
async fn fetch_track_details(
&self,
album_id: i32,
release_year: Option<u16>,
yt_id: &str,
ytm_track: bool,
) -> Result<(), ExtractorError> {
let details = self.rp.query().video_details(yt_id).await?;
let description = details.description.to_plaintext();
let release_date_detail = if ytm_track {
util::extract_yt_release_date(&description, details.publish_date)
} else {
details.publish_date.map(|d| (d.date(), DatePrecision::Day))
};
let release_date = release_date_detail.filter(|d| match release_year {
Some(year) => d.0.year() == i32::from(year),
None => true,
});
let t_upd = TrackUpdate {
description: Some(Some(&description)),
..Default::default()
};
t_upd
.update(Id::Src(yt_id, MusicService::YouTube), &self.db)
.await?;
if let Some(release_date) = release_date {
let b_upd = AlbumUpdate {
release_date: Some(Some(release_date.0)),
release_date_precision: Some(Some(release_date.1)),
..Default::default()
};
b_upd.update(Id::Db(album_id), &self.db).await?;
}
Ok(())
}
/// Get the name of a YouTube artist with the given ID (either from DB or the RSS feed)
async fn get_yt_artist_name(&self, id: &str) -> Result<String, ExtractorError> {
let artist = Artist::get(Id::Src(id, MusicService::YouTube), &self.db)
.await
.to_optional()?;
if let Some(artist) = artist {
Ok(artist.name)
} else {
let feed = self.rp.query().channel_rss(id).await?;
let name = feed.name.strip_suffix(" - Topic").unwrap_or(&feed.name);
self.import_yt_artist_id(&ArtistIdName {
id: id.to_owned(),
name: name.to_owned(),
})
.await?;
Ok(name.to_owned())
}
}
/// Convert track/album artists scraped from YouTube into the format used by Tiraya
///
/// YouTube Music's metadata is incomplete and there are a lot of tracks with
/// unlinked artists (the user is only shown a list of artists that are not
/// clickable links).
///
/// The YouTube interface also includes the "Go to artist" button in the dropdown
/// menu which may be present even if there are only unlinked artists shown.
///
/// So we can apply a few tricks to improve the metadata:
///
/// - If the artist ID from the dropdown is not included in the list of artists,
/// fetch its name and add it to the beginning
/// - Add the album artists (if given) to the list of artists if they appear in the
/// list of unlinked artists
///
/// Example albums for testing:
/// - https://music.youtube.com/browse/MPREb_GXN2zsSMUKJ
/// - https://music.youtube.com/browse/MPREb_98weK02o4DO
/// - https://music.youtube.com/browse/MPREb_1pFivcTlTls
async fn split_yt_artists(
&self,
artists: Vec<rpmodel::ArtistId>,
artist_id: Option<String>,
album_artists: &[ArtistIdName],
) -> (Vec<ArtistIdName>, Option<Vec<String>>) {
let mut ul_artists = Vec::new();
let mut add_aid = true;
let mut artists = artists
.into_iter()
.filter_map(|a| match a.id {
Some(id) => {
if Some(&id) == artist_id.as_ref() {
add_aid = false;
}
Some(ArtistIdName { id, name: a.name })
}
None => {
ul_artists.push(a.name);
None
}
})
.collect::<Vec<_>>();
// Artist from artist_id field is not contained in artists list
if add_aid {
if let Some(artist_id) = artist_id {
if let Ok(name) = self.get_yt_artist_name(&artist_id).await {
util::extract_ul_artist(&mut ul_artists, &name);
artists.insert(
0,
ArtistIdName {
id: artist_id,
name,
},
);
}
}
}
// Try to match album artists(s)
if !ul_artists.is_empty() {
for aa in album_artists {
if util::extract_ul_artist(&mut ul_artists, &aa.name) {
artists.insert(0, aa.clone());
}
}
}
(
artists,
if ul_artists.is_empty() {
None
} else {
Some(ul_artists)
},
)
} }
} }
@ -777,7 +593,7 @@ mod tests {
use super::*; use super::*;
#[tokio::test] #[tokio::test]
// #[test_tracing::test] // #[test_log::test]
async fn import_album() { async fn import_album() {
sqlx_database_tester::dotenv::dotenv().unwrap(); sqlx_database_tester::dotenv::dotenv().unwrap();
let url = std::env::var("DATABASE_URL").unwrap(); let url = std::env::var("DATABASE_URL").unwrap();
@ -790,7 +606,7 @@ mod tests {
.unwrap(); .unwrap();
if artist_res.fetched { if artist_res.fetched {
extractor extractor
.fetch_yt_artist_albums(artist_res.c.id) .update_yt_artist_albums(artist_res.c.id)
.await .await
.unwrap(); .unwrap();
} }

View file

@ -1,19 +1,39 @@
use std::{borrow::Cow, hash::Hasher}; use std::borrow::Cow;
use hex_literal::hex;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rustypipe::model as rpmodel; use rustypipe::model as rpmodel;
use siphasher::sip128::{Hasher128, SipHasher}; use tiraya_db::models::{AlbumType, SyncData, SyncError};
use time::{Date, Duration, OffsetDateTime};
use tiraya_db::models::{AlbumType, DatePrecision, SyncData, SyncError};
#[derive(Clone)]
pub struct ArtistIdName { pub struct ArtistIdName {
pub id: String, pub id: String,
pub name: String, pub name: String,
} }
pub fn split_yt_artists(
artists: Vec<rpmodel::ArtistId>,
) -> (Vec<ArtistIdName>, Option<Vec<String>>) {
let mut ul_artists = Vec::new();
let artists = artists
.into_iter()
.filter_map(|a| match a.id {
Some(id) => Some(ArtistIdName { id, name: a.name }),
None => {
ul_artists.push(a.name);
None
}
})
.collect();
(
artists,
if ul_artists.is_empty() {
None
} else {
Some(ul_artists)
},
)
}
static YTM_IMAGE_REGEX: Lazy<Regex> = static YTM_IMAGE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^https://[a-z\d]+.googleusercontent.com/[\w\d_/-]+").unwrap()); Lazy::new(|| Regex::new(r"^https://[a-z\d]+.googleusercontent.com/[\w\d_/-]+").unwrap());
@ -67,96 +87,11 @@ pub fn map_album_type(album_type: rpmodel::AlbumType) -> AlbumType {
} }
} }
static RELEASE_DATE_REGEX: Lazy<Regex> = /*
Lazy::new(|| Regex::new(r"Released on: (\d{4}-\d{2}-\d{2})").unwrap());
const YMD_FORMAT: &[time::format_description::FormatItem] =
time::macros::format_description!("[year]-[month]-[day]");
pub fn extract_yt_release_date(
description: &str,
upload_date: Option<OffsetDateTime>,
) -> Option<(Date, DatePrecision)> {
RELEASE_DATE_REGEX
.captures(description)
.and_then(|cap| {
let raw_date = &cap[1];
Date::parse(raw_date, YMD_FORMAT).ok()
})
.map(|release_date| {
if let Some(upload_date) = upload_date {
// Prefer the video upload date if it lies within 4 days of the release date
let upload_date = upload_date.date();
let diff = (upload_date - release_date).abs();
if diff < Duration::days(4) {
return (upload_date, DatePrecision::Day);
}
}
(
release_date,
// If a date component is unknown, YT will show a 1 in that place
if release_date.day() == 1 {
if release_date.month() == time::Month::January {
DatePrecision::Year
} else {
DatePrecision::Month
}
} else {
DatePrecision::Day
},
)
})
.or_else(|| upload_date.map(|d| (d.date(), DatePrecision::Day)))
}
/// Try to extract the given artist name from a list of unlinked artists
///
/// Returns true if the artist was extracted
pub fn extract_ul_artist(ul_artists: &mut [String], name: &str) -> bool {
// TODO: add support for other languages
static END_RE: Lazy<Regex> = Lazy::new(|| Regex::new(",? *(and|&)? *$").unwrap());
static START_RE: Lazy<Regex> = Lazy::new(|| Regex::new("^,? *(and|&)? *").unwrap());
for ul_a in ul_artists.iter_mut() {
if let Some((a, b)) = ul_a.split_once(name) {
// Trim end of first part
if a.is_empty() {
*ul_a = START_RE.replace(b, "").to_string()
} else {
*ul_a = END_RE.replace(a, "").to_string() + b;
}
return true;
}
}
false
}
pub struct AlbumHasher(SipHasher);
impl AlbumHasher {
pub fn new() -> Self {
Self(SipHasher::new_with_key(&hex!(
"e0060fd1ea207d8f43d2bf9bcae63f65"
)))
}
pub fn add_track(&mut self, name: &str, duration: u32) {
self.0.write(name.as_bytes());
self.0.write_u32(duration);
}
pub fn finish(&self) -> [u8; 16] {
self.0.finish128().as_bytes()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use rstest::rstest;
use time::macros::{date, datetime};
/*
fn get_image_url_album() { fn get_image_url_album() {
let images = [rpmodel::Thumbnail { let images = [rpmodel::Thumbnail {
url: "https://lh3.googleusercontent.com/46FajCeiWBIdYQmkVkDvNVpprs-ihk9hVulFc8v-pUPEfgUU2uzGmc45vO-sZCYQiEasgo21cbengDYIAQ=w226-h226-l90-rj", url: "https://lh3.googleusercontent.com/46FajCeiWBIdYQmkVkDvNVpprs-ihk9hVulFc8v-pUPEfgUU2uzGmc45vO-sZCYQiEasgo21cbengDYIAQ=w226-h226-l90-rj",
@ -164,57 +99,5 @@ mod tests {
height: 226, height: 226,
}]; }];
} }
*/
#[rstest]
#[case("Released on: 2016-04-22", Some(datetime!(2016-05-10 0:0 UTC)), Some((date!(2016-04-22), DatePrecision::Day)))]
#[case("", Some(datetime!(2016-05-10 0:0 UTC)), Some((date!(2016-05-10), DatePrecision::Day)))]
#[case("Released on: 2016-04-22", Some(datetime!(2016-4-20 0:0 UTC)), Some((date!(2016-04-20), DatePrecision::Day)))]
#[case("Released on: 2016-04-20", Some(datetime!(2016-4-22 0:0 UTC)), Some((date!(2016-04-22), DatePrecision::Day)))]
#[case("Released on: 2016-04-01", Some(datetime!(2016-4-22 0:0 UTC)), Some((date!(2016-04-01), DatePrecision::Month)))]
#[case("Released on: 2016-01-01", Some(datetime!(2016-4-22 0:0 UTC)), Some((date!(2016-01-01), DatePrecision::Year)))]
#[case("Released on: 2016-04-20", None, Some((date!(2016-04-20), DatePrecision::Day)))]
#[case("", None, None)]
fn t_extract_yt_release_date(
#[case] description: &str,
#[case] upload_date: Option<OffsetDateTime>,
#[case] expect: Option<(Date, DatePrecision)>,
) {
let res = extract_yt_release_date(description, upload_date);
assert_eq!(res, expect);
}
#[rstest]
#[case(&["Aurelio Fierro, Furio Rendine, Vincenzo de Crescenzo"], "Aurelio Fierro", &["Furio Rendine, Vincenzo de Crescenzo"])]
#[case(&["Miley Cyrus, Swae Lee, & Mike WiLL Made-It"], "Swae Lee", &["Miley Cyrus, & Mike WiLL Made-It"])]
#[case(&["Miley Cyrus, Swae Lee, & Mike WiLL Made-It"], "Mike WiLL Made-It", &["Miley Cyrus, Swae Lee"])]
#[case(&["Miley Cyrus, Swae Lee, & Mike WiLL Made-It"], "foobar", &["Miley Cyrus, Swae Lee, & Mike WiLL Made-It"])]
#[case(&["Miley Cyrus"], "Miley Cyrus", &[""])]
fn t_extract_ul_artist(
#[case] ul_artists: &[&str],
#[case] name: &str,
#[case] expect: &[&str],
) {
let mut art = ul_artists.iter().map(|s| s.to_string()).collect::<Vec<_>>();
assert_eq!(extract_ul_artist(&mut art, name), ul_artists != expect);
assert_eq!(art, expect);
}
#[rstest]
#[case(&[("Lieblingsmensch", 190)], hex!("290deeaedd84dbdb71d2f62979eeedc8"))]
#[case(&[
("Ich wache auf", 221),
("Waldbrand", 208),
("Verlernt", 223),
("In Farbe", 221),
("Stadt im Hinterland", 197)
], hex!("ffbd4cad41df8e99f3d0a3d629f5a5d5"))]
fn album_hash(#[case] tracks: &[(&str, u32)], #[case] expect: [u8; 16]) {
let mut hasher = AlbumHasher::new();
for t in tracks {
hasher.add_track(t.0, t.1);
}
let hash = hasher.finish();
assert_eq!(hash, expect, "got {hash:x?}");
}
} }
*/

View file

@ -17,4 +17,4 @@ rustypipe.workspace = true
sqlx.workspace = true sqlx.workspace = true
dotenvy.workspace = true dotenvy.workspace = true
tokio.workspace = true tokio.workspace = true
tracing-subscriber.workspace = true env_logger.workspace = true

View file

@ -6,7 +6,7 @@ use tiraya_extractor::Extractor;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dotenvy::dotenv().unwrap(); dotenvy::dotenv().unwrap();
tracing_subscriber::fmt::init(); env_logger::init();
let url = std::env::var("DATABASE_URL").unwrap(); let url = std::env::var("DATABASE_URL").unwrap();
let pool = PgPool::connect(&url).await.unwrap(); let pool = PgPool::connect(&url).await.unwrap();
@ -20,7 +20,7 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
if artist_res.fetched { if artist_res.fetched {
ext.fetch_yt_artist_albums(artist_res.c.id).await.unwrap(); ext.update_yt_artist_albums(artist_res.c.id).await.unwrap();
} }
dbg!(artist_res.c); dbg!(artist_res.c);