Add tidal provider

First draft of tidaldy that implements the crabidy provider trait.
This commit is contained in:
Hans Mündelein 2023-05-22 09:31:02 +02:00
parent 73cb9ddbda
commit 19f19cba2d
Signed by: hans
GPG Key ID: BA7B55E984CE74F4
9 changed files with 1623 additions and 206 deletions

440
Cargo.lock generated
View File

@ -14,6 +14,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "anyhow"
version = "1.0.71"
@ -90,9 +99,9 @@ dependencies = [
[[package]]
name = "base64"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105"
[[package]]
name = "bitflags"
@ -100,6 +109,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "bumpalo"
version = "3.12.2"
@ -148,6 +166,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "combine"
version = "3.8.1"
@ -161,6 +194,31 @@ dependencies = [
"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]]
name = "core-foundation"
version = "0.9.3"
@ -186,6 +244,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "cpufeatures"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [
"libc",
]
[[package]]
name = "crabidy"
version = "0.1.0"
@ -231,6 +298,16 @@ dependencies = [
"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]]
name = "cynic"
version = "3.0.0-beta.3"
@ -308,6 +385,16 @@ dependencies = [
"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]]
name = "either"
version = "1.8.1"
@ -369,7 +456,7 @@ dependencies = [
"futures-sink",
"nanorand",
"pin-project",
"spin",
"spin 0.9.8",
]
[[package]]
@ -441,6 +528,16 @@ dependencies = [
"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]]
name = "getrandom"
version = "0.2.9"
@ -450,7 +547,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
@ -489,6 +586,15 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "heck"
version = "0.4.1"
@ -593,6 +699,29 @@ dependencies = [
"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]]
name = "ident_case"
version = "1.0.1"
@ -669,6 +798,17 @@ dependencies = [
"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]]
name = "lazy_static"
version = "1.4.0"
@ -683,9 +823,9 @@ checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "linux-raw-sys"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "lock_api"
@ -732,7 +872,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.45.0",
]
@ -769,6 +909,16 @@ dependencies = [
"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]]
name = "num-traits"
version = "0.2.15"
@ -890,6 +1040,50 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "petgraph"
version = "0.6.3"
@ -1004,7 +1198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
dependencies = [
"bytes",
"heck",
"heck 0.4.1",
"itertools",
"lazy_static",
"log",
@ -1150,6 +1344,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls",
"serde",
"serde_json",
"serde_urlencoded",
@ -1163,6 +1358,21 @@ dependencies = [
"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]]
name = "rustix"
version = "0.37.19"
@ -1177,6 +1387,28 @@ dependencies = [
"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]]
name = "rustversion"
version = "1.0.12"
@ -1205,10 +1437,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "2.9.0"
name = "sct"
version = "0.7.0"
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 = [
"bitflags",
"core-foundation",
@ -1258,6 +1510,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1270,6 +1531,30 @@ dependencies = [
"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]]
name = "signal-hook"
version = "0.3.15"
@ -1325,6 +1610,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spin"
version = "0.9.8"
@ -1407,6 +1698,36 @@ dependencies = [
"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]]
name = "tinyvec"
version = "1.6.0"
@ -1497,6 +1818,49 @@ dependencies = [
"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]]
name = "tonic"
version = "0.9.2"
@ -1608,6 +1972,18 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "unicode-bidi"
version = "0.3.13"
@ -1650,6 +2026,18 @@ dependencies = [
"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]]
name = "url"
version = "2.3.1"
@ -1689,6 +2077,12 @@ dependencies = [
"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]]
name = "wasi"
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"
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]]
name = "windows-sys"
version = "0.42.0"
@ -1951,6 +2354,15 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
@ -1959,3 +2371,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "zeroize"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"

View File

@ -1,2 +1,2 @@
[workspace]
members = ["crabidy-core", "crabidy", "cbd-tui"]
members = ["crabidy-core", "crabidy", "cbd-tui", "tidaldy"]

View File

@ -1,4 +1,5 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("crabidy/v1/crabidy.proto")?;
tonic_build::compile_protos("crabidy/proto/crabidy.proto")?;
Ok(())
}

View File

@ -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 { }

View File

@ -6,214 +6,37 @@ use async_trait::async_trait;
#[async_trait]
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>;
fn get_root_list(&self) -> ItemList;
async fn get_item_list(&self, list_uuid: &str, depth: usize)
-> Result<ItemList, ProviderError>;
fn get_library_root(&self) -> proto::crabidy::LibraryNodeResponse;
async fn get_library_node(
&self,
list_uuid: &str,
) -> Result<proto::crabidy::LibraryNodeResponse, ProviderError>;
}
#[derive(Clone, Debug, Hash)]
pub enum ProviderError {
UnknownUser,
CouldNotLogin,
FetchError,
MalformedUuid,
Other,
}
#[derive(Clone, Debug)]
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 {
impl proto::crabidy::LibraryNodeResponse {
pub fn new() -> Self {
Self {
name: "/".to_string(),
uuid: "/".to_string(),
parent: "".to_string(),
tracks: None,
name: "/".to_string(),
children: Vec::new(),
parent: None,
state: proto::crabidy::LibraryNodeState::Unspecified as i32,
tracks: Vec::new(),
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,
}
}
}

24
tidaldy/Cargo.toml Normal file
View File

@ -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"] }

82
tidaldy/src/config.rs Normal file
View File

@ -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,
}

537
tidaldy/src/lib.rs Normal file
View File

@ -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(&params)
.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(&params)
.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(&params)
.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(&params)
.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(&params[..]),
)
.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);
}
}

404
tidaldy/src/models.rs Normal file
View File

@ -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,
}