Add tidal provider
First draft of tidaldy that implements the crabidy provider trait.
This commit is contained in:
parent
73cb9ddbda
commit
19f19cba2d
|
|
@ -14,6 +14,15 @@ version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.71"
|
version = "1.0.71"
|
||||||
|
|
@ -90,9 +99,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.0"
|
version = "0.21.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
|
checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
|
|
@ -100,6 +109,15 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.12.2"
|
version = "3.12.2"
|
||||||
|
|
@ -148,6 +166,21 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"time",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "3.8.1"
|
version = "3.8.1"
|
||||||
|
|
@ -161,6 +194,31 @@ dependencies = [
|
||||||
"unreachable",
|
"unreachable",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "confique"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1803b359c0d8e0c280ffe88a2da6d2a22b2e187148e1df1df4d6bdc335a3690"
|
||||||
|
dependencies = [
|
||||||
|
"confique-macro",
|
||||||
|
"json5",
|
||||||
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
|
"toml 0.5.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "confique-macro"
|
||||||
|
version = "0.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7305b4979ffd6d8b02006da5520b21c66bfab961cd688b9b6db00780f61448ce"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.3.3",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
|
@ -186,6 +244,15 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crabidy"
|
name = "crabidy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -231,6 +298,16 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cynic"
|
name = "cynic"
|
||||||
version = "3.0.0-beta.3"
|
version = "3.0.0-beta.3"
|
||||||
|
|
@ -308,6 +385,16 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
|
|
@ -369,7 +456,7 @@ dependencies = [
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"nanorand",
|
"nanorand",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"spin",
|
"spin 0.9.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -441,6 +528,16 @@ dependencies = [
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
|
@ -450,7 +547,7 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -489,6 +586,15 @@ version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -593,6 +699,29 @@ dependencies = [
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ident_case"
|
name = "ident_case"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -669,6 +798,17 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "json5"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -683,9 +823,9 @@ checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.3.7"
|
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 = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
|
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
|
|
@ -732,7 +872,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -769,6 +909,16 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
@ -890,6 +1040,50 @@ version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_derive"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_generator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_generator"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_meta",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.16",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_meta"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"pest",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
|
@ -1004,7 +1198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
|
checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"heck",
|
"heck 0.4.1",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1150,6 +1344,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
|
@ -1163,6 +1358,21 @@ dependencies = [
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.16.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"spin 0.5.2",
|
||||||
|
"untrusted",
|
||||||
|
"web-sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.37.19"
|
version = "0.37.19"
|
||||||
|
|
@ -1177,6 +1387,28 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"ring",
|
||||||
|
"rustls-webpki",
|
||||||
|
"sct",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.100.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
|
|
@ -1205,10 +1437,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "sct"
|
||||||
version = "2.9.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1"
|
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secrecy"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "2.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
|
|
@ -1258,6 +1510,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|
@ -1270,6 +1531,30 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
|
|
@ -1325,6 +1610,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
|
|
@ -1407,6 +1698,36 @@ dependencies = [
|
||||||
"syn 2.0.16",
|
"syn 2.0.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tidaldy"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"confique",
|
||||||
|
"crabidy-core",
|
||||||
|
"reqwest",
|
||||||
|
"secrecy",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"toml 0.7.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
@ -1497,6 +1818,49 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.19.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tonic"
|
name = "tonic"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
|
|
@ -1608,6 +1972,18 @@ version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
|
|
@ -1650,6 +2026,18 @@ dependencies = [
|
||||||
"void",
|
"void",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
|
@ -1689,6 +2077,12 @@ dependencies = [
|
||||||
"try-lock",
|
"try-lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
|
@ -1804,6 +2198,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
|
|
@ -1951,6 +2354,15 @@ version = "0.48.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -1959,3 +2371,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crabidy-core", "crabidy", "cbd-tui"]
|
members = ["crabidy-core", "crabidy", "cbd-tui", "tidaldy"]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tonic_build::compile_protos("crabidy/v1/crabidy.proto")?;
|
tonic_build::compile_protos("crabidy/v1/crabidy.proto")?;
|
||||||
|
tonic_build::compile_protos("crabidy/proto/crabidy.proto")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package crabidy;
|
||||||
|
|
||||||
|
|
||||||
|
service Library {
|
||||||
|
rpc GetLibraryNode (LibraryNodeRequest) returns (LibraryNodeResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To signal whether it's loading data (for frontend only probably)
|
||||||
|
enum LibraryNodeState {
|
||||||
|
LIBRARY_NODE_STATE_UNSPECIFIED = 0;
|
||||||
|
LIBRARY_NODE_STATE_PENDING = 1;
|
||||||
|
LIBRARY_NODE_STATE_DONE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Track {
|
||||||
|
// Including provider
|
||||||
|
string uuid = 1;
|
||||||
|
string artist = 2;
|
||||||
|
string title = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LibraryNodeRequest {
|
||||||
|
string uuid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LibraryNodeResponse {
|
||||||
|
// Including provider
|
||||||
|
string uuid = 1;
|
||||||
|
string name = 2;
|
||||||
|
repeated string children = 3;
|
||||||
|
optional string parent = 4;
|
||||||
|
LibraryNodeState state = 5;
|
||||||
|
repeated Track tracks = 6;
|
||||||
|
bool is_queable = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Queue {
|
||||||
|
rpc QueueTrack (QueueTrackRequest) returns (EmptyResponse);
|
||||||
|
rpc QueueLibraryNode (QueueNodeRequest) returns (EmptyResponse);
|
||||||
|
rpc ReplaceWithTrack (QueueTrackRequest) returns (EmptyResponse);
|
||||||
|
rpc ReplaceWithNode (QueueNodeRequest) returns (EmptyResponse);
|
||||||
|
rpc AppendTrack (QueueTrackRequest) returns (EmptyResponse);
|
||||||
|
rpc AppendNode (QueueNodeRequest) returns (EmptyResponse);
|
||||||
|
rpc RemoveTracks (RemoveTracksRequest) returns (EmptyResponse);
|
||||||
|
rpc SetCurrentTrack (SetCurrentTrackRequest) returns (EmptyResponse);
|
||||||
|
rpc GetQueueUpdates (QueueUpdatesRequest) returns (stream QueueUpdateResponse);
|
||||||
|
rpc GetQueue(EmptyRequest) returns (CurrentQueue);
|
||||||
|
rpc SaveQueue (QueueSaveRequest) returns (EmptyResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message CurrentQueue {
|
||||||
|
uint64 timestamp =1;
|
||||||
|
uint32 current = 2;
|
||||||
|
repeated Track tracks = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueuePositionChange {
|
||||||
|
uint64 timestamp = 1;
|
||||||
|
uint32 new_position = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueueTrackRequest {
|
||||||
|
string uuid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueueNodeRequest {
|
||||||
|
string uuid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveTracksRequest {
|
||||||
|
repeated uint32 positions = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetCurrentTrackRequest {
|
||||||
|
uint32 position = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message QueueUpdatesRequest {
|
||||||
|
uint64 timestamp =2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueueUpdateResponse {
|
||||||
|
oneof QueueUpdateResult{
|
||||||
|
CurrentQueue full = 1;
|
||||||
|
QueuePositionChange position_change = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueueSaveRequest {
|
||||||
|
string name = 1;
|
||||||
|
// inside the configured path of crabidy
|
||||||
|
string path = 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
service Playback {
|
||||||
|
rpc TogglePlay (EmptyRequest) returns (EmptyResponse);
|
||||||
|
rpc Stop (EmptyRequest) returns (EmptyResponse);
|
||||||
|
rpc GetActiveTrack (EmptyRequest) returns (ActiveTrack);
|
||||||
|
rpc GetTrackUpdates (ActiveTrackFilter) returns (stream ActiveTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum TrackPlayState {
|
||||||
|
TRACK_PLAY_STATE_STOPPED = 0;
|
||||||
|
TRACK_PLAY_STATE_LOADING = 1;
|
||||||
|
TRACK_PLAY_STATE_PLAYING = 2;
|
||||||
|
TRACK_PLAY_STATE_PAUSED = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ActiveTrackFilter {
|
||||||
|
// defines how many of the update messages should be skipped
|
||||||
|
// before they are sent back via GetTrackUpdates
|
||||||
|
uint32 updates_skipped = 1;
|
||||||
|
repeated string type_whitelist = 2;
|
||||||
|
repeated string type_blacklist = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ActiveTrack {
|
||||||
|
optional Track track = 1;
|
||||||
|
TrackPlayState play_state = 2;
|
||||||
|
uint32 completion = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EmptyRequest { }
|
||||||
|
message EmptyResponse { }
|
||||||
|
|
@ -6,214 +6,37 @@ use async_trait::async_trait;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ProviderClient: std::fmt::Debug + Send + Sync {
|
pub trait ProviderClient: std::fmt::Debug + Send + Sync {
|
||||||
|
async fn init(raw_toml_settings: &str) -> Result<Self, ProviderError>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
fn settings(&self) -> String;
|
||||||
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError>;
|
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError>;
|
||||||
fn get_root_list(&self) -> ItemList;
|
fn get_library_root(&self) -> proto::crabidy::LibraryNodeResponse;
|
||||||
async fn get_item_list(&self, list_uuid: &str, depth: usize)
|
async fn get_library_node(
|
||||||
-> Result<ItemList, ProviderError>;
|
&self,
|
||||||
|
list_uuid: &str,
|
||||||
|
) -> Result<proto::crabidy::LibraryNodeResponse, ProviderError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash)]
|
#[derive(Clone, Debug, Hash)]
|
||||||
pub enum ProviderError {
|
pub enum ProviderError {
|
||||||
UnknownUser,
|
UnknownUser,
|
||||||
|
CouldNotLogin,
|
||||||
FetchError,
|
FetchError,
|
||||||
|
MalformedUuid,
|
||||||
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
impl proto::crabidy::LibraryNodeResponse {
|
||||||
pub struct ItemList {
|
|
||||||
pub name: String,
|
|
||||||
pub uuid: String,
|
|
||||||
pub parent: String,
|
|
||||||
pub tracks: Option<Vec<Track>>,
|
|
||||||
pub children: Vec<ItemList>,
|
|
||||||
pub is_queable: bool,
|
|
||||||
pub ephemeral: bool,
|
|
||||||
pub is_partial: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemList {
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: "/".to_string(),
|
|
||||||
uuid: "/".to_string(),
|
uuid: "/".to_string(),
|
||||||
parent: "".to_string(),
|
name: "/".to_string(),
|
||||||
tracks: None,
|
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
|
parent: None,
|
||||||
|
state: proto::crabidy::LibraryNodeState::Unspecified as i32,
|
||||||
|
tracks: Vec::new(),
|
||||||
is_queable: false,
|
is_queable: false,
|
||||||
ephemeral: false,
|
|
||||||
is_partial: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn replace_sublist(&mut self, sublist: &Self) {
|
|
||||||
if self.uuid == sublist.uuid {
|
|
||||||
self.name = sublist.name.clone();
|
|
||||||
self.tracks = sublist.tracks.clone();
|
|
||||||
self.children = sublist.children.clone();
|
|
||||||
self.is_queable = sublist.is_queable;
|
|
||||||
self.ephemeral = sublist.ephemeral;
|
|
||||||
self.is_partial = sublist.is_partial;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for child in self.children.iter_mut() {
|
|
||||||
child.replace_sublist(sublist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn flatten(&self) -> Vec<Track> {
|
|
||||||
let mut tracks = Vec::new();
|
|
||||||
if let Some(own_tracks) = &self.tracks {
|
|
||||||
tracks.extend(own_tracks.clone());
|
|
||||||
}
|
|
||||||
for child in self.children.iter() {
|
|
||||||
tracks.extend(child.flatten());
|
|
||||||
}
|
|
||||||
tracks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct ItemListFilter {
|
|
||||||
uuid_filter: Option<String>,
|
|
||||||
name_filter: Option<String>,
|
|
||||||
provider_filter: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
enum PlayState {
|
|
||||||
Buffering,
|
|
||||||
Playing,
|
|
||||||
Paused,
|
|
||||||
Stopped,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Track {
|
|
||||||
pub title: String,
|
|
||||||
pub uuid: String,
|
|
||||||
pub duration: Option<i32>,
|
|
||||||
pub album: Option<Album>,
|
|
||||||
pub artist: Option<Artist>,
|
|
||||||
pub provider: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct InputTrack {
|
|
||||||
pub uuid: String,
|
|
||||||
pub provider: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Album {
|
|
||||||
pub title: String,
|
|
||||||
pub release_date: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Artist {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash)]
|
|
||||||
pub enum QueueError {
|
|
||||||
ItemListNotQueuable,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Queue {
|
|
||||||
pub tracks: Vec<Track>,
|
|
||||||
current: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Queue {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
tracks: vec![],
|
|
||||||
current: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.current = 0;
|
|
||||||
self.tracks = vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replace_with_track(&mut self, track: Track) {
|
|
||||||
self.current = 0;
|
|
||||||
self.tracks = vec![track];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replace_with_item_list(&mut self, item_list: &ItemList) -> Result<(), QueueError> {
|
|
||||||
if !item_list.is_queable {
|
|
||||||
return Err(QueueError::ItemListNotQueuable);
|
|
||||||
};
|
|
||||||
self.current = 0;
|
|
||||||
self.tracks = item_list.flatten();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current(&mut self, current: usize) -> bool {
|
|
||||||
if current < self.tracks.len() {
|
|
||||||
self.current = current as i32;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current(&self) -> Option<&Track> {
|
|
||||||
self.tracks.get(self.current as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(&mut self) -> Option<&Track> {
|
|
||||||
if (self.current as usize) < self.tracks.len() {
|
|
||||||
self.current += 1;
|
|
||||||
Some(&self.tracks[(self.current - 1) as usize])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn previous(&mut self) -> Option<&Track> {
|
|
||||||
if self.current > 0 {
|
|
||||||
self.current -= 1;
|
|
||||||
Some(&self.tracks[self.current as usize])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append_track(&mut self, track: Track) {
|
|
||||||
self.tracks.push(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append_playlist(&mut self, playlist: &[Track]) {
|
|
||||||
self.tracks.extend(playlist.to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn queue_track(&mut self, track: Track) {
|
|
||||||
self.tracks.insert(self.current as usize, track);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn queue_playlist(&mut self, playlist: &[Track]) {
|
|
||||||
let tail: Vec<Track> = self
|
|
||||||
.tracks
|
|
||||||
.splice((self.current as usize).., playlist.to_vec())
|
|
||||||
.collect();
|
|
||||||
self.tracks.extend(tail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ActiveTrack {
|
|
||||||
track: Option<Track>,
|
|
||||||
completion: i32,
|
|
||||||
play_state: PlayState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveTrack {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
track: None,
|
|
||||||
completion: 0,
|
|
||||||
play_state: PlayState::Stopped,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "tidaldy"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.68"
|
||||||
|
base64 = "0.21.0"
|
||||||
|
chrono = "0.4.24"
|
||||||
|
confique = "0.2.3"
|
||||||
|
crabidy-core = { path = "../crabidy-core" }
|
||||||
|
reqwest = { version = "0.11.17", features = ["json", "rustls"] }
|
||||||
|
secrecy = { version = "0.8.0", features = ["serde"] }
|
||||||
|
serde = { version = "1.0.162", features = ["derive"] }
|
||||||
|
serde_json = "1.0.96"
|
||||||
|
serde_urlencoded = "0.7.1"
|
||||||
|
thiserror = "1.0.40"
|
||||||
|
tokio = { version = "1.28.1", features = ["full", "time"] }
|
||||||
|
toml = "0.7.4"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.28.1", features = ["full"] }
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::iter::zip;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub base_url: String,
|
||||||
|
pub hifi_url: String,
|
||||||
|
pub audio_quality: AudioQuality,
|
||||||
|
pub login: LoginConfig,
|
||||||
|
pub oauth: OauthConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
let client_id_key = b"abcdefghijklmnop";
|
||||||
|
let hidden_id_from_fulltext_search = "\u{1b}7W<-01\u{3}\nX\u{1f}(=\u{1}[\u{4}".as_bytes();
|
||||||
|
let mut client_id_bytes = Vec::new();
|
||||||
|
for (k, c) in zip(client_id_key, hidden_id_from_fulltext_search) {
|
||||||
|
client_id_bytes.push(*c ^ *k);
|
||||||
|
}
|
||||||
|
let client_id = String::from_utf8(client_id_bytes).unwrap();
|
||||||
|
|
||||||
|
let client_secret_key = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR";
|
||||||
|
let hidden_secret_from_fulltext_search = "7((\u{c}! \u{16}\"9\u{1b}\u{1d}\u{1f}=8!2'D\u{6}\u{1f}-\"=\u{15}\u{e}\u{16}7 70\u{15}q0$\u{4}&9/z|<5eo".as_bytes();
|
||||||
|
let mut client_secret_bytes = Vec::new();
|
||||||
|
for (k, c) in zip(client_secret_key, hidden_secret_from_fulltext_search) {
|
||||||
|
client_secret_bytes.push(*c ^ *k);
|
||||||
|
}
|
||||||
|
let client_secret = String::from_utf8(client_secret_bytes).unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
base_url: "https://api.tidal.com/v1".to_string(),
|
||||||
|
hifi_url: "https://api.tidalhifi.com/v1".to_string(),
|
||||||
|
audio_quality: AudioQuality::Lossless,
|
||||||
|
login: LoginConfig {
|
||||||
|
device_code: None,
|
||||||
|
user_id: None,
|
||||||
|
country_code: None,
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: None,
|
||||||
|
expires_after: None,
|
||||||
|
},
|
||||||
|
oauth: OauthConfig {
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
base_url: "https://auth.tidal.com/v1/oauth2".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct LoginConfig {
|
||||||
|
pub device_code: Option<String>,
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
pub country_code: Option<String>,
|
||||||
|
pub access_token: Option<String>,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub expires_after: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct OauthConfig {
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
pub base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum AudioQuality {
|
||||||
|
Low,
|
||||||
|
High,
|
||||||
|
Lossless,
|
||||||
|
HiRes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("failed to write config file")]
|
||||||
|
Write,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,537 @@
|
||||||
|
/// Lots of stuff and especially the auth handling is shamelessly copied from
|
||||||
|
/// https://github.com/MinisculeGirraffe/tdl
|
||||||
|
use reqwest::Client as HttpClient;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use tokio::time::{sleep, Duration, Instant};
|
||||||
|
pub mod config;
|
||||||
|
pub mod models;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
pub use models::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Client {
|
||||||
|
http_client: HttpClient,
|
||||||
|
settings: config::Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl crabidy_core::ProviderClient for Client {
|
||||||
|
async fn init(raw_toml_settings: &str) -> Result<Self, crabidy_core::ProviderError> {
|
||||||
|
let settings: config::Settings = if let Ok(settings) = toml::from_str(raw_toml_settings) {
|
||||||
|
settings
|
||||||
|
} else {
|
||||||
|
let settings = config::Settings::default();
|
||||||
|
println!(
|
||||||
|
"could not parse toml settings: {:#?} using default settings instead: {:#?}",
|
||||||
|
raw_toml_settings, settings
|
||||||
|
);
|
||||||
|
settings
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut client = Self::new(settings)?;
|
||||||
|
if let Ok(_) = client.login_config().await {
|
||||||
|
return Ok(client);
|
||||||
|
}
|
||||||
|
if let Ok(_) = client.login_web().await {
|
||||||
|
return Ok(client);
|
||||||
|
}
|
||||||
|
Err(crabidy_core::ProviderError::CouldNotLogin)
|
||||||
|
}
|
||||||
|
fn settings(&self) -> String {
|
||||||
|
toml::to_string_pretty(&self.settings).unwrap()
|
||||||
|
}
|
||||||
|
async fn get_urls_for_track(
|
||||||
|
&self,
|
||||||
|
track_uuid: &str,
|
||||||
|
) -> Result<Vec<String>, crabidy_core::ProviderError> {
|
||||||
|
let Ok(playback) = self.get_track_playback(track_uuid).await else {
|
||||||
|
return Err(crabidy_core::ProviderError::FetchError)
|
||||||
|
};
|
||||||
|
let Ok(manifest) = playback.get_manifest() else {
|
||||||
|
return Err(crabidy_core::ProviderError::FetchError)
|
||||||
|
};
|
||||||
|
Ok(manifest.urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_library_root(&self) -> crabidy_core::proto::crabidy::LibraryNodeResponse {
|
||||||
|
let global_root = crabidy_core::proto::crabidy::LibraryNodeResponse::new();
|
||||||
|
let children = vec!["userplaylists".to_string()];
|
||||||
|
crabidy_core::proto::crabidy::LibraryNodeResponse {
|
||||||
|
uuid: "tidal".to_string(),
|
||||||
|
name: "tidal".to_string(),
|
||||||
|
parent: Some(format!("{}", global_root.uuid)),
|
||||||
|
state: crabidy_core::proto::crabidy::LibraryNodeState::Done as i32,
|
||||||
|
tracks: Vec::new(),
|
||||||
|
children,
|
||||||
|
is_queable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_library_node(
|
||||||
|
&self,
|
||||||
|
uuid: &str,
|
||||||
|
) -> Result<crabidy_core::proto::crabidy::LibraryNodeResponse, crabidy_core::ProviderError>
|
||||||
|
{
|
||||||
|
let Some(user_id) = self.settings.login.user_id.clone() else {
|
||||||
|
return Err(crabidy_core::ProviderError::UnknownUser)
|
||||||
|
};
|
||||||
|
let (module, uuid) = split_uuid(uuid);
|
||||||
|
let node = match module.as_str() {
|
||||||
|
"userplaylists" => {
|
||||||
|
let global_root = crabidy_core::proto::crabidy::LibraryNodeResponse::new();
|
||||||
|
let mut node = crabidy_core::proto::crabidy::LibraryNodeResponse {
|
||||||
|
uuid: "userplaylists".to_string(),
|
||||||
|
name: "playlists".to_string(),
|
||||||
|
parent: Some(format!("{}", global_root.uuid)),
|
||||||
|
state: crabidy_core::proto::crabidy::LibraryNodeState::Unspecified as i32,
|
||||||
|
tracks: Vec::new(),
|
||||||
|
children: Vec::new(),
|
||||||
|
is_queable: false,
|
||||||
|
};
|
||||||
|
for playlist in self
|
||||||
|
.get_users_playlists_and_favorite_playlists(&user_id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
node.children.push(playlist.playlist.uuid);
|
||||||
|
}
|
||||||
|
node
|
||||||
|
}
|
||||||
|
"playlist" => {
|
||||||
|
let mut node: crabidy_core::proto::crabidy::LibraryNodeResponse =
|
||||||
|
self.get_playlist(&uuid).await?.into();
|
||||||
|
let tracks: Vec<crabidy_core::proto::crabidy::Track> = self
|
||||||
|
.get_playlist_tracks(&uuid)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.into())
|
||||||
|
.collect();
|
||||||
|
node.tracks = tracks;
|
||||||
|
node.parent = Some("userplaylists".to_string());
|
||||||
|
node
|
||||||
|
}
|
||||||
|
_ => return Err(crabidy_core::ProviderError::MalformedUuid),
|
||||||
|
};
|
||||||
|
Ok(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_uuid(uuid: &str) -> (String, String) {
|
||||||
|
let mut split = uuid.splitn(2, ':');
|
||||||
|
(
|
||||||
|
split.next().unwrap().to_string(),
|
||||||
|
split.next().unwrap().to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(settings: config::Settings) -> Result<Self, ClientError> {
|
||||||
|
let http_client = HttpClient::builder()
|
||||||
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59")
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
http_client,
|
||||||
|
settings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_id(&self) -> Option<String> {
|
||||||
|
self.settings.login.user_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn make_request<T: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
uri: &str,
|
||||||
|
query: Option<&[(&str, String)]>,
|
||||||
|
) -> Result<T, ClientError> {
|
||||||
|
let Some(ref access_token) = self.settings.login.access_token.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No access token found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let Some(country_code) = self.settings.login.country_code.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No country code found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let country_param = ("countryCode", country_code);
|
||||||
|
let mut params: Vec<&(&str, String)> = vec![&country_param];
|
||||||
|
if let Some(query) = query {
|
||||||
|
params.extend(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: T = self
|
||||||
|
.http_client
|
||||||
|
.get(format!("{}/{}", self.settings.hifi_url, uri))
|
||||||
|
.bearer_auth(access_token)
|
||||||
|
.query(¶ms)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn make_paginated_request<T: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
uri: &str,
|
||||||
|
query: Option<&[(&str, String)]>,
|
||||||
|
) -> Result<Vec<T>, ClientError> {
|
||||||
|
let Some(ref access_token) = self.settings.login.access_token.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No access token found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let Some(country_code) = self.settings.login.country_code.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No country code found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let country_param = ("countryCode", country_code);
|
||||||
|
let limit = 50;
|
||||||
|
let mut offset = 0;
|
||||||
|
let limit_param = ("limit", limit.to_string());
|
||||||
|
let mut params: Vec<&(&str, String)> = vec![&country_param, &limit_param];
|
||||||
|
if let Some(query) = query {
|
||||||
|
params.extend(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response: Page<T> = self
|
||||||
|
.http_client
|
||||||
|
.get(format!("{}/{}", self.settings.hifi_url, uri))
|
||||||
|
.bearer_auth(access_token)
|
||||||
|
.query(¶ms)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
let mut items = Vec::with_capacity(response.total_number_of_items);
|
||||||
|
items.extend(response.items);
|
||||||
|
while response.offset + limit < response.total_number_of_items {
|
||||||
|
offset += limit;
|
||||||
|
let offset_param = ("offset", offset.to_string());
|
||||||
|
let mut params: Vec<&(&str, String)> =
|
||||||
|
vec![&country_param, &limit_param, &offset_param];
|
||||||
|
if let Some(query) = query {
|
||||||
|
params.extend(query);
|
||||||
|
}
|
||||||
|
response = self
|
||||||
|
.http_client
|
||||||
|
.get(format!("{}/{}", self.settings.hifi_url, uri))
|
||||||
|
.bearer_auth(access_token)
|
||||||
|
.query(¶ms)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
items.extend(response.items);
|
||||||
|
}
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn make_explorer_request(
|
||||||
|
&self,
|
||||||
|
uri: &str,
|
||||||
|
query: Option<&[(&str, String)]>,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let Some(ref access_token) = self.settings.login.access_token.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No access token found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let Some(country_code) = self.settings.login.country_code.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No country code found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let country_param = ("countryCode", country_code);
|
||||||
|
let mut params: Vec<&(&str, String)> = vec![&country_param];
|
||||||
|
if let Some(query) = query {
|
||||||
|
params.extend(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.http_client
|
||||||
|
.get(format!("{}/{}", self.settings.hifi_url, uri))
|
||||||
|
.bearer_auth(access_token)
|
||||||
|
.query(¶ms)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
println!("{:?}", response);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search(&self, query: &str) -> Result<(), ClientError> {
|
||||||
|
let query = vec![("query", query.to_string())];
|
||||||
|
self.make_explorer_request(&format!("search/artists"), Some(&query))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_playlist_tracks(
|
||||||
|
&self,
|
||||||
|
playlist_uuid: &str,
|
||||||
|
) -> Result<Vec<Track>, ClientError> {
|
||||||
|
Ok(self
|
||||||
|
.make_paginated_request(&format!("playlists/{}/tracks", playlist_uuid), None)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_playlist(&self, playlist_uuid: &str) -> Result<Playlist, ClientError> {
|
||||||
|
Ok(self
|
||||||
|
.make_request(&format!("playlists/{}", playlist_uuid), None)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_users_playlists(&self, user_id: u64) -> Result<Vec<Playlist>, ClientError> {
|
||||||
|
Ok(self
|
||||||
|
.make_paginated_request(&format!("users/{}/playlists", user_id), None)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_users_playlists_and_favorite_playlists(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
) -> Result<Vec<PlaylistAndFavorite>, ClientError> {
|
||||||
|
Ok(self
|
||||||
|
.make_paginated_request(
|
||||||
|
&format!("users/{}/playlistsAndFavoritePlaylists", user_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn explore_get_users_playlists_and_favorite_playlists(
|
||||||
|
&self,
|
||||||
|
user_id: u64,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let limit = 50;
|
||||||
|
let offset = 0;
|
||||||
|
let limit_param = ("limit", limit.to_string());
|
||||||
|
let offset_param = ("offset", offset.to_string());
|
||||||
|
let params: Vec<(&str, String)> = vec![limit_param, offset_param];
|
||||||
|
self.make_explorer_request(
|
||||||
|
&format!("users/{}/playlistsAndFavoritePlaylists", user_id),
|
||||||
|
Some(¶ms[..]),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_users_favorites(&self, user_id: u64) -> Result<(), ClientError> {
|
||||||
|
self.make_explorer_request(
|
||||||
|
&format!("users/{}/favorites", user_id),
|
||||||
|
None,
|
||||||
|
// Some(&query),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user(&self, user_id: u64) -> Result<(), ClientError> {
|
||||||
|
self.make_explorer_request(
|
||||||
|
&format!("users/{}", user_id),
|
||||||
|
None,
|
||||||
|
// Some(&query),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_track_playback(&self, track_id: &str) -> Result<TrackPlayback, ClientError> {
|
||||||
|
let query = vec![
|
||||||
|
("audioquality", "LOSSLESS".to_string()),
|
||||||
|
("playbackmode", "STREAM".to_string()),
|
||||||
|
("assetpresentation", "FULL".to_string()),
|
||||||
|
];
|
||||||
|
self.make_request(
|
||||||
|
&format!("tracks/{}/playbackinfopostpaywall", track_id),
|
||||||
|
Some(&query),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_track(&self, track_id: String) -> Result<Track, ClientError> {
|
||||||
|
self.make_request(&format!("tracks/{}", track_id), None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_web(&mut self) -> Result<(), ClientError> {
|
||||||
|
let code_response = self.get_device_code().await?;
|
||||||
|
let now = Instant::now();
|
||||||
|
println!("{}", code_response.verification_uri_complete);
|
||||||
|
while now.elapsed().as_secs() <= code_response.expires_in {
|
||||||
|
let login = self.check_auth_status(&code_response.device_code).await;
|
||||||
|
if login.is_err() {
|
||||||
|
sleep(Duration::from_secs(code_response.interval)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let timestamp = chrono::Utc::now().timestamp() as u64;
|
||||||
|
|
||||||
|
let login_results = login?;
|
||||||
|
self.settings.login.device_code = Some(code_response.device_code);
|
||||||
|
self.settings.login.access_token = Some(login_results.access_token);
|
||||||
|
self.settings.login.refresh_token = login_results.refresh_token;
|
||||||
|
self.settings.login.expires_after = Some(login_results.expires_in + timestamp);
|
||||||
|
self.settings.login.user_id = Some(login_results.user.user_id);
|
||||||
|
self.settings.login.country_code = Some(login_results.user.country_code);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("login attempt expired");
|
||||||
|
Err(ClientError::ConnectionError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_config(&mut self) -> Result<(), ClientError> {
|
||||||
|
let Some(access_token) = self.settings.login.access_token.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No access token found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
//return if our session is still valid
|
||||||
|
if self
|
||||||
|
.http_client
|
||||||
|
.get(format!("{}/sessions", self.settings.base_url))
|
||||||
|
.bearer_auth(access_token)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status()
|
||||||
|
.is_success()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
//otherwise refresh our token
|
||||||
|
let refresh = self.refresh_access_token().await?;
|
||||||
|
let now = chrono::Utc::now().timestamp() as u64;
|
||||||
|
|
||||||
|
self.settings.login.expires_after = Some(refresh.expires_in + now);
|
||||||
|
self.settings.login.access_token = Some(refresh.access_token);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_access_token(&self) -> Result<RefreshResponse, ClientError> {
|
||||||
|
let Some(refresh_token) = self.settings.login.refresh_token.clone() else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"No refresh token found".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let data = DeviceAuthRequest {
|
||||||
|
client_id: self.settings.oauth.client_id.clone(),
|
||||||
|
client_secret: Some(self.settings.oauth.client_secret.clone()),
|
||||||
|
refresh_token: Some(refresh_token.to_string()),
|
||||||
|
grant_type: Some("refresh_token".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let body = serde_urlencoded::to_string(&data)?;
|
||||||
|
|
||||||
|
let req = self
|
||||||
|
.http_client
|
||||||
|
.post("https://auth.tidal.com/v1/oauth2/token")
|
||||||
|
.body(body)
|
||||||
|
.basic_auth(
|
||||||
|
self.settings.oauth.client_id.clone(),
|
||||||
|
Some(self.settings.oauth.client_secret.clone()),
|
||||||
|
)
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if req.status().is_success() {
|
||||||
|
let res = req.json::<RefreshResponse>().await?;
|
||||||
|
Ok(res)
|
||||||
|
} else {
|
||||||
|
Err(ClientError::AuthError(
|
||||||
|
"Failed to refresh access token".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get_device_code(&self) -> Result<DeviceAuthResponse, ClientError> {
|
||||||
|
let req = DeviceAuthRequest {
|
||||||
|
client_id: self.settings.oauth.client_id.clone(),
|
||||||
|
scope: Some("r_usr+w_usr+w_sub".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let payload = serde_urlencoded::to_string(&req)?;
|
||||||
|
let res = self
|
||||||
|
.http_client
|
||||||
|
.post(format!(
|
||||||
|
"{}/device_authorization",
|
||||||
|
&self.settings.oauth.base_url
|
||||||
|
))
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body(payload)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !res.status().is_success() {
|
||||||
|
return Err(ClientError::AuthError(res.status().to_string()));
|
||||||
|
}
|
||||||
|
let code: DeviceAuthResponse = res.json().await?;
|
||||||
|
Ok(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_auth_status(
|
||||||
|
&self,
|
||||||
|
device_code: &str,
|
||||||
|
) -> Result<RefreshResponse, ClientError> {
|
||||||
|
let req = DeviceAuthRequest {
|
||||||
|
client_id: self.settings.oauth.client_id.clone(),
|
||||||
|
device_code: Some(device_code.to_string()),
|
||||||
|
scope: Some("r_usr+w_usr+w_sub".to_string()),
|
||||||
|
grant_type: Some("urn:ietf:params:oauth:grant-type:device_code".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let payload = serde_urlencoded::to_string(&req)?;
|
||||||
|
let res = self
|
||||||
|
.http_client
|
||||||
|
.post(format!("{}/token", self.settings.oauth.base_url))
|
||||||
|
.basic_auth(
|
||||||
|
self.settings.oauth.client_id.clone(),
|
||||||
|
Some(self.settings.oauth.client_secret.clone()),
|
||||||
|
)
|
||||||
|
.body(payload)
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if !res.status().is_success() {
|
||||||
|
if res.status().is_client_error() {
|
||||||
|
return Err(ClientError::AuthError(format!(
|
||||||
|
"Failed to check auth status: {}",
|
||||||
|
res.status().canonical_reason().unwrap_or("")
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
return Err(ClientError::AuthError(
|
||||||
|
"Failed to check auth status".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let refresh = res.json::<RefreshResponse>().await?;
|
||||||
|
Ok(refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn setup() -> Client {
|
||||||
|
let settings = crate::config::Settings::default();
|
||||||
|
Client::new(settings).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_device_code() {
|
||||||
|
let client = setup();
|
||||||
|
println!("{:#?}", client);
|
||||||
|
let response = client.get_device_code().await.unwrap();
|
||||||
|
assert!(!response.device_code.is_empty());
|
||||||
|
assert_eq!(response.device_code.len(), 36);
|
||||||
|
assert!(!response.user_code.is_empty());
|
||||||
|
assert_eq!(response.user_code.len(), 5);
|
||||||
|
assert!(!response.verification_uri.is_empty());
|
||||||
|
assert!(!response.verification_uri_complete.is_empty());
|
||||||
|
assert!(response.expires_in == 300);
|
||||||
|
assert!(response.interval != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
use std::{str::FromStr, string::FromUtf8Error};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub trait Paginated {
|
||||||
|
fn offset(&self) -> usize;
|
||||||
|
fn limit(&self) -> usize;
|
||||||
|
fn total(&self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Page<T> {
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
pub offset: usize,
|
||||||
|
pub total_number_of_items: usize,
|
||||||
|
pub items: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ArtistSearchPage {
|
||||||
|
pub limit: i64,
|
||||||
|
pub offset: i64,
|
||||||
|
pub total_number_of_items: i64,
|
||||||
|
pub items: Vec<Item>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginated for ArtistSearchPage {
|
||||||
|
fn offset(&self) -> usize {
|
||||||
|
self.offset as usize
|
||||||
|
}
|
||||||
|
fn limit(&self) -> usize {
|
||||||
|
self.limit as usize
|
||||||
|
}
|
||||||
|
fn total(&self) -> usize {
|
||||||
|
self.total_number_of_items as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Item {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
pub artist_types: Vec<String>,
|
||||||
|
pub url: String,
|
||||||
|
pub picture: Value,
|
||||||
|
pub popularity: i64,
|
||||||
|
pub artist_roles: Vec<ArtistRole>,
|
||||||
|
pub mixes: Mixes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ArtistRole {
|
||||||
|
pub category_id: i64,
|
||||||
|
pub category: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ClientError {
|
||||||
|
#[error("connecting to the tidal api servers failed")]
|
||||||
|
ConnectionError,
|
||||||
|
#[error("internal reqwest error")]
|
||||||
|
HttpClientError(#[from] reqwest::Error),
|
||||||
|
#[error("internal serde url error")]
|
||||||
|
SerdeUrlError(#[from] serde_urlencoded::ser::Error),
|
||||||
|
#[error("authentication failed")]
|
||||||
|
AuthError(String),
|
||||||
|
#[error("base64 decoding failed")]
|
||||||
|
Base64DecodeError(#[from] base64::DecodeError),
|
||||||
|
#[error("utf8 decoding failed")]
|
||||||
|
Utf8DecodeError(#[from] FromUtf8Error),
|
||||||
|
#[error("json decoding failed")]
|
||||||
|
JsonDecodeError(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ClientError> for crabidy_core::ProviderError {
|
||||||
|
fn from(err: ClientError) -> Self {
|
||||||
|
match err {
|
||||||
|
ClientError::ConnectionError => Self::FetchError,
|
||||||
|
ClientError::HttpClientError(_) => Self::FetchError,
|
||||||
|
ClientError::SerdeUrlError(_) => Self::FetchError,
|
||||||
|
_ => Self::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
pub struct DeviceAuthRequest {
|
||||||
|
pub client_id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub client_secret: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub grant_type: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub device_code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
|
pub struct DeviceAuthResponse {
|
||||||
|
pub device_code: String,
|
||||||
|
pub user_code: String,
|
||||||
|
pub verification_uri: String,
|
||||||
|
pub verification_uri_complete: String,
|
||||||
|
pub expires_in: u64,
|
||||||
|
pub interval: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct RefreshResponse {
|
||||||
|
pub user: UserResponse,
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub token_type: String,
|
||||||
|
pub expires_in: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
|
pub struct UserResponse {
|
||||||
|
pub user_id: String,
|
||||||
|
pub country_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TrackPlayback {
|
||||||
|
pub track_id: i64,
|
||||||
|
pub asset_presentation: String,
|
||||||
|
pub audio_mode: String,
|
||||||
|
pub audio_quality: String,
|
||||||
|
pub manifest_mime_type: String,
|
||||||
|
pub manifest_hash: String,
|
||||||
|
pub manifest: String,
|
||||||
|
pub album_replay_gain: f64,
|
||||||
|
pub album_peak_amplitude: f64,
|
||||||
|
pub track_replay_gain: f64,
|
||||||
|
pub track_peak_amplitude: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackPlayback {
|
||||||
|
pub fn get_manifest(&self) -> Result<PlaybackManifest, ClientError> {
|
||||||
|
PlaybackManifest::from_str(&self.manifest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Track {
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
pub duration: u64,
|
||||||
|
pub replay_gain: f64,
|
||||||
|
pub peak: f64,
|
||||||
|
pub allow_streaming: bool,
|
||||||
|
pub stream_ready: bool,
|
||||||
|
pub stream_start_date: Option<String>,
|
||||||
|
pub premium_streaming_only: bool,
|
||||||
|
pub track_number: u64,
|
||||||
|
pub volume_number: u64,
|
||||||
|
pub version: Value,
|
||||||
|
pub popularity: u64,
|
||||||
|
pub copyright: Option<String>,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub isrc: Option<String>,
|
||||||
|
pub editable: bool,
|
||||||
|
pub explicit: bool,
|
||||||
|
pub audio_quality: String,
|
||||||
|
pub audio_modes: Vec<String>,
|
||||||
|
pub artist: Artist,
|
||||||
|
pub artists: Vec<Artist>,
|
||||||
|
pub album: Album,
|
||||||
|
pub mixes: Mixes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Track> for crabidy_core::proto::crabidy::Track {
|
||||||
|
fn from(track: Track) -> Self {
|
||||||
|
Self {
|
||||||
|
uuid: track.id.to_string(),
|
||||||
|
title: track.title,
|
||||||
|
artist: track.artist.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Track> for crabidy_core::proto::crabidy::Track {
|
||||||
|
fn from(track: &Track) -> Self {
|
||||||
|
Self {
|
||||||
|
uuid: track.id.to_string(),
|
||||||
|
title: track.title.clone(),
|
||||||
|
artist: track.artist.name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Artist {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub type_field: String,
|
||||||
|
pub picture: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Artist2 {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub type_field: String,
|
||||||
|
pub picture: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Album {
|
||||||
|
pub id: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub cover: String,
|
||||||
|
pub vibrant_color: String,
|
||||||
|
pub video_cover: Value,
|
||||||
|
pub release_date: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Mixes {
|
||||||
|
#[serde(rename = "TRACK_MIX")]
|
||||||
|
pub track_mix: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
|
pub struct PlaybackManifest {
|
||||||
|
pub mime_type: String,
|
||||||
|
pub codecs: String,
|
||||||
|
pub encryption_type: EncryptionType,
|
||||||
|
pub key_id: Option<String>,
|
||||||
|
pub urls: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PlaybackManifest {
|
||||||
|
type Err = ClientError;
|
||||||
|
fn from_str(input: &str) -> Result<PlaybackManifest, Self::Err> {
|
||||||
|
let decode = base64::decode(input)?;
|
||||||
|
let json = String::from_utf8(decode)?;
|
||||||
|
let parsed: PlaybackManifest = serde_json::from_str(&json)?;
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum EncryptionType {
|
||||||
|
#[serde(rename = "NONE")]
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct User {
|
||||||
|
pub id: i64,
|
||||||
|
pub username: String,
|
||||||
|
pub profile_name: String,
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub email_verified: bool,
|
||||||
|
pub country_code: String,
|
||||||
|
pub created: String,
|
||||||
|
pub newsletter: bool,
|
||||||
|
#[serde(rename = "acceptedEULA")]
|
||||||
|
pub accepted_eula: bool,
|
||||||
|
pub gender: Value,
|
||||||
|
pub date_of_birth: String,
|
||||||
|
pub facebook_uid: i64,
|
||||||
|
pub apple_uid: Value,
|
||||||
|
pub partner: i64,
|
||||||
|
pub tidal_id: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PlaylistAndFavorite {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub type_field: String,
|
||||||
|
pub created: String,
|
||||||
|
pub playlist: Playlist,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlaylistAndFavorite> for crabidy_core::proto::crabidy::LibraryNodeResponse {
|
||||||
|
fn from(a: PlaylistAndFavorite) -> Self {
|
||||||
|
a.playlist.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Playlist {
|
||||||
|
pub uuid: String,
|
||||||
|
pub title: String,
|
||||||
|
pub number_of_tracks: Option<usize>,
|
||||||
|
pub number_of_videos: Option<usize>,
|
||||||
|
pub creator: Option<Creator>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub duration: Option<u32>,
|
||||||
|
pub last_updated: Option<String>,
|
||||||
|
pub created: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub type_field: Option<String>,
|
||||||
|
pub public_playlist: bool,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub image: Option<String>,
|
||||||
|
pub popularity: Option<i64>,
|
||||||
|
pub square_image: Option<String>,
|
||||||
|
pub promoted_artists: Option<Vec<Value>>,
|
||||||
|
pub last_item_added_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Playlist> for crabidy_core::proto::crabidy::LibraryNodeResponse {
|
||||||
|
fn from(a: Playlist) -> Self {
|
||||||
|
crabidy_core::proto::crabidy::LibraryNodeResponse {
|
||||||
|
name: a.title,
|
||||||
|
uuid: format!("playlist:{}", a.uuid),
|
||||||
|
tracks: Vec::new(),
|
||||||
|
parent: None,
|
||||||
|
state: crabidy_core::proto::crabidy::LibraryNodeState::Done as i32,
|
||||||
|
children: Vec::new(),
|
||||||
|
is_queable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Creator {
|
||||||
|
pub id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PlaylistTracksPage {
|
||||||
|
pub limit: usize,
|
||||||
|
pub offset: usize,
|
||||||
|
pub total_number_of_items: usize,
|
||||||
|
pub items: Vec<PlaylistTrack>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginated for PlaylistTracksPage {
|
||||||
|
fn offset(&self) -> usize {
|
||||||
|
self.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limit(&self) -> usize {
|
||||||
|
self.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total(&self) -> usize {
|
||||||
|
self.total_number_of_items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PlaylistTrack {
|
||||||
|
pub id: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub duration: i64,
|
||||||
|
pub replay_gain: f64,
|
||||||
|
pub peak: f64,
|
||||||
|
pub allow_streaming: bool,
|
||||||
|
pub stream_ready: bool,
|
||||||
|
pub stream_start_date: Option<String>,
|
||||||
|
pub premium_streaming_only: bool,
|
||||||
|
pub track_number: i64,
|
||||||
|
pub volume_number: i64,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub popularity: i64,
|
||||||
|
pub copyright: String,
|
||||||
|
pub description: Value,
|
||||||
|
pub url: String,
|
||||||
|
pub isrc: String,
|
||||||
|
pub editable: bool,
|
||||||
|
pub explicit: bool,
|
||||||
|
pub audio_quality: String,
|
||||||
|
pub audio_modes: Vec<String>,
|
||||||
|
pub artist: Artist,
|
||||||
|
pub artists: Vec<Artist>,
|
||||||
|
pub album: Album,
|
||||||
|
pub mixes: Mixes,
|
||||||
|
pub date_added: String,
|
||||||
|
pub index: i64,
|
||||||
|
pub item_uuid: String,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue