mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Lots of stuff
This commit is contained in:
parent
1ad62e6f18
commit
7ffe996652
28 changed files with 2076 additions and 101 deletions
138
Cargo.lock
generated
138
Cargo.lock
generated
|
@ -2,12 +2,13 @@
|
||||||
name = "librespot"
|
name = "librespot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"byteorder 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"librespot-protocol 0.1.0",
|
"librespot-protocol 0.1.0",
|
||||||
"mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
"num 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"protobuf 0.0.10 (git+https://github.com/stepancheg/rust-protobuf.git)",
|
"portaudio 0.1.2 (git+https://github.com/mvdnes/portaudio-rs)",
|
||||||
|
"protobuf 1.0.0 (git+https://github.com/stepancheg/rust-protobuf.git)",
|
||||||
"protobuf_macros 0.1.0 (git+https://github.com/plietar/rust-protobuf-macros.git)",
|
"protobuf_macros 0.1.0 (git+https://github.com/plietar/rust-protobuf-macros.git)",
|
||||||
"rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"readall 0.1.0 (git+https://github.com/plietar/rust-readall.git)",
|
"readall 0.1.0 (git+https://github.com/plietar/rust-readall.git)",
|
||||||
|
@ -15,6 +16,7 @@ dependencies = [
|
||||||
"rust-gmp 0.2.0 (git+https://github.com/plietar/rust-gmp.git)",
|
"rust-gmp 0.2.0 (git+https://github.com/plietar/rust-gmp.git)",
|
||||||
"shannon 0.1.0 (git+https://github.com/plietar/rust-shannon.git)",
|
"shannon 0.1.0 (git+https://github.com/plietar/rust-shannon.git)",
|
||||||
"vergen 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
"vergen 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"vorbis 0.0.11 (git+https://github.com/plietar/vorbis-rs)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -22,24 +24,29 @@ name = "bitflags"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "0.3.9"
|
version = "0.3.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gcc"
|
name = "gcc"
|
||||||
version = "0.3.5"
|
version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -47,7 +54,7 @@ name = "librespot-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"protobuf 0.0.10 (git+https://github.com/stepancheg/rust-protobuf.git)",
|
"protobuf 1.0.0 (git+https://github.com/stepancheg/rust-protobuf.git)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -57,29 +64,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.1.24"
|
version = "0.1.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rustc-serialize 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ogg-sys"
|
||||||
|
version = "0.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"gcc 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portaudio"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "git+https://github.com/mvdnes/portaudio-rs#a6432fb11acebb5a2d9997fc0019eeb482ba435d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"portaudio_sys 0.1.0 (git+https://github.com/mvdnes/portaudio-rs)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portaudio_sys"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/mvdnes/portaudio-rs#a6432fb11acebb5a2d9997fc0019eeb482ba435d"
|
||||||
|
dependencies = [
|
||||||
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "0.0.10"
|
version = "1.0.0"
|
||||||
source = "git+https://github.com/stepancheg/rust-protobuf.git#41fde39aed305e0fb71ef6a8d92b35ee50550bde"
|
source = "git+https://github.com/stepancheg/rust-protobuf.git#d6e80593f38ce47dfa0c4912a3558fa33ee06143"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf_macros"
|
name = "protobuf_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/plietar/rust-protobuf-macros.git#e95dbc5bdf6c13787e2385d66d9d003afcaf9f17"
|
source = "git+https://github.com/plietar/rust-protobuf-macros.git#5fa976178a48b01bdf2da6d5e7929367e348ea04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -92,46 +133,51 @@ name = "rust-crypto"
|
||||||
version = "0.2.31"
|
version = "0.2.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"gcc 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rustc-serialize 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
"time 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-gmp"
|
name = "rust-gmp"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/plietar/rust-gmp.git#eaf298870d63712d18f8fab6bbbf0cb1e14dbb7f"
|
source = "git+https://github.com/plietar/rust-gmp.git#db2bb627165b12ebe18a41a941ac6284ce9b895d"
|
||||||
|
dependencies = [
|
||||||
|
"num 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-serialize"
|
name = "rustc-serialize"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shannon"
|
name = "shannon"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/plietar/rust-shannon.git#83a49c3397e1e546e6079cf54a0e5b2f85c6b13f"
|
source = "git+https://github.com/plietar/rust-shannon.git#c6be8a879a523a77d81c50df46faa891b76fea25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"byteorder 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"readall 0.1.0 (git+https://github.com/plietar/rust-readall.git)",
|
||||||
"shannon-sys 0.1.0 (git+https://github.com/plietar/rust-shannon.git)",
|
"shannon-sys 0.1.0 (git+https://github.com/plietar/rust-shannon.git)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shannon-sys"
|
name = "shannon-sys"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/plietar/rust-shannon.git#83a49c3397e1e546e6079cf54a0e5b2f85c6b13f"
|
source = "git+https://github.com/plietar/rust-shannon.git#c6be8a879a523a77d81c50df46faa891b76fea25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"gcc 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.1.25"
|
version = "0.1.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"gcc 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -139,7 +185,41 @@ name = "vergen"
|
||||||
version = "0.0.13"
|
version = "0.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"bitflags 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
"time 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vorbis"
|
||||||
|
version = "0.0.11"
|
||||||
|
source = "git+https://github.com/plietar/vorbis-rs#cff6b4222cebd0fb31bcbc2e14a7ba575548c703"
|
||||||
|
dependencies = [
|
||||||
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ogg-sys 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"vorbis-sys 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"vorbisfile-sys 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vorbis-sys"
|
||||||
|
version = "0.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"gcc 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ogg-sys 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vorbisfile-sys"
|
||||||
|
version = "0.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"gcc 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ogg-sys 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"vorbis-sys 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -17,18 +17,18 @@ rust-crypto = "*"
|
||||||
|
|
||||||
[dependencies.protobuf]
|
[dependencies.protobuf]
|
||||||
git = "https://github.com/stepancheg/rust-protobuf.git"
|
git = "https://github.com/stepancheg/rust-protobuf.git"
|
||||||
|
|
||||||
[dependencies.protobuf_macros]
|
[dependencies.protobuf_macros]
|
||||||
git = "https://github.com/plietar/rust-protobuf-macros.git"
|
git = "https://github.com/plietar/rust-protobuf-macros.git"
|
||||||
|
|
||||||
[dependencies.rust-gmp]
|
[dependencies.rust-gmp]
|
||||||
git = "https://github.com/plietar/rust-gmp.git"
|
git = "https://github.com/plietar/rust-gmp.git"
|
||||||
|
|
||||||
[dependencies.shannon]
|
[dependencies.shannon]
|
||||||
git = "https://github.com/plietar/rust-shannon.git"
|
git = "https://github.com/plietar/rust-shannon.git"
|
||||||
|
|
||||||
[dependencies.readall]
|
[dependencies.readall]
|
||||||
git = "https://github.com/plietar/rust-readall.git"
|
git = "https://github.com/plietar/rust-readall.git"
|
||||||
|
[dependencies.portaudio]
|
||||||
|
git = "https://github.com/mvdnes/portaudio-rs"
|
||||||
|
[dependencies.vorbis]
|
||||||
|
git = "https://github.com/plietar/vorbis-rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
vergen = "*"
|
vergen = "*"
|
||||||
|
|
|
@ -37,9 +37,18 @@ fn compile(prefix : &Path, files : &[&Path]) -> Result<(),ProtobufError>{
|
||||||
fn main() {
|
fn main() {
|
||||||
let root = PathBuf::from(&env::var("CARGO_MANIFEST_DIR").unwrap());
|
let root = PathBuf::from(&env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let proto = root.join("proto");
|
let proto = root.join("proto");
|
||||||
|
|
||||||
compile(&proto, &[
|
compile(&proto, &[
|
||||||
&proto.join("keyexchange.proto"),
|
&proto.join("keyexchange.proto"),
|
||||||
&proto.join("authentication.proto")
|
&proto.join("authentication.proto"),
|
||||||
|
&proto.join("mercury.proto"),
|
||||||
|
&proto.join("metadata.proto"),
|
||||||
|
&proto.join("playlist4changes.proto"),
|
||||||
|
&proto.join("playlist4content.proto"),
|
||||||
|
&proto.join("playlist4issues.proto"),
|
||||||
|
&proto.join("playlist4meta.proto"),
|
||||||
|
&proto.join("playlist4ops.proto"),
|
||||||
|
&proto.join("playlist4service.proto"),
|
||||||
]).unwrap();
|
]).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ message Album {
|
||||||
optional bytes gid = 1;
|
optional bytes gid = 1;
|
||||||
optional string name = 2;
|
optional string name = 2;
|
||||||
repeated Artist artist = 3;
|
repeated Artist artist = 3;
|
||||||
optional Type type = 4;
|
optional Type typ = 4;
|
||||||
optional string label = 5;
|
optional string label = 5;
|
||||||
optional Date date = 6;
|
optional Date date = 6;
|
||||||
optional sint32 popularity = 7;
|
optional sint32 popularity = 7;
|
||||||
|
@ -106,7 +106,7 @@ message Copyright {
|
||||||
P = 0;
|
P = 0;
|
||||||
C = 1;
|
C = 1;
|
||||||
}
|
}
|
||||||
optional Type type = 1;
|
optional Type typ = 1;
|
||||||
optional string text = 2;
|
optional string text = 2;
|
||||||
}
|
}
|
||||||
message Restriction {
|
message Restriction {
|
||||||
|
@ -122,7 +122,7 @@ message Restriction {
|
||||||
repeated Catalogue catalogue = 1;
|
repeated Catalogue catalogue = 1;
|
||||||
optional string countries_allowed = 2;
|
optional string countries_allowed = 2;
|
||||||
optional string countries_forbidden = 3;
|
optional string countries_forbidden = 3;
|
||||||
optional Type type = 4;
|
optional Type typ = 4;
|
||||||
repeated string usage = 5;
|
repeated string usage = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ message SalePeriod {
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExternalId {
|
message ExternalId {
|
||||||
optional string type = 1;
|
optional string typ = 1;
|
||||||
optional string id = 2;
|
optional string id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
80
protocol/proto/playlist4changes.proto
Normal file
80
protocol/proto/playlist4changes.proto
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import "playlist4content.proto";
|
||||||
|
import "playlist4issues.proto";
|
||||||
|
import "playlist4meta.proto";
|
||||||
|
import "playlist4ops.proto";
|
||||||
|
|
||||||
|
message ChangeInfo {
|
||||||
|
optional string user = 1;
|
||||||
|
optional int32 timestamp = 2;
|
||||||
|
optional bool admin = 3;
|
||||||
|
optional bool undo = 4;
|
||||||
|
optional bool redo = 5;
|
||||||
|
optional bool merge = 6;
|
||||||
|
optional bool compressed = 7;
|
||||||
|
optional bool migration = 8;
|
||||||
|
}
|
||||||
|
message Delta {
|
||||||
|
optional bytes base_version = 1;
|
||||||
|
repeated Op ops = 2;
|
||||||
|
optional ChangeInfo info = 4;
|
||||||
|
}
|
||||||
|
message Merge {
|
||||||
|
optional bytes base_version = 1;
|
||||||
|
optional bytes merge_version = 2;
|
||||||
|
optional ChangeInfo info = 4;
|
||||||
|
}
|
||||||
|
message ChangeSet {
|
||||||
|
enum Kind {
|
||||||
|
KIND_UNKNOWN = 0;
|
||||||
|
DELTA = 2;
|
||||||
|
MERGE = 3;
|
||||||
|
};
|
||||||
|
required Kind kind = 1;
|
||||||
|
optional Delta delta = 2;
|
||||||
|
optional Merge merge = 3;
|
||||||
|
}
|
||||||
|
message RevisionTaggedChangeSet {
|
||||||
|
required bytes revision = 1;
|
||||||
|
required ChangeSet change_set = 2;
|
||||||
|
}
|
||||||
|
message Diff {
|
||||||
|
required bytes from_revision = 1;
|
||||||
|
repeated Op ops = 2;
|
||||||
|
required bytes to_revision = 3;
|
||||||
|
}
|
||||||
|
message ListDump {
|
||||||
|
optional bytes latestRevision = 1;
|
||||||
|
optional int32 length = 2;
|
||||||
|
optional ListAttributes attributes = 3;
|
||||||
|
optional ListChecksum checksum = 4;
|
||||||
|
optional ListItems contents = 5;
|
||||||
|
repeated Delta pendingDeltas = 7;
|
||||||
|
}
|
||||||
|
message ListChanges {
|
||||||
|
optional bytes baseRevision = 1;
|
||||||
|
repeated Delta deltas = 2;
|
||||||
|
optional bool wantResultingRevisions = 3;
|
||||||
|
optional bool wantSyncResult = 4;
|
||||||
|
optional ListDump dump = 5;
|
||||||
|
repeated int32 nonces = 6;
|
||||||
|
}
|
||||||
|
message SelectedListContent {
|
||||||
|
optional bytes revision = 1;
|
||||||
|
optional int32 length = 2;
|
||||||
|
optional ListAttributes attributes = 3;
|
||||||
|
optional ListChecksum checksum = 4;
|
||||||
|
optional ListItems contents = 5;
|
||||||
|
optional Diff diff = 6;
|
||||||
|
|
||||||
|
optional Diff syncResult = 7;
|
||||||
|
repeated bytes resultingRevisions = 8;
|
||||||
|
|
||||||
|
optional bool multipleHeads = 9;
|
||||||
|
|
||||||
|
optional bool upToDate = 10;
|
||||||
|
|
||||||
|
repeated ClientResolveAction resolveAction = 12;
|
||||||
|
repeated ClientIssue issues = 13;
|
||||||
|
|
||||||
|
repeated int32 nonces = 14;
|
||||||
|
}
|
31
protocol/proto/playlist4content.proto
Normal file
31
protocol/proto/playlist4content.proto
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import "playlist4meta.proto";
|
||||||
|
import "playlist4issues.proto";
|
||||||
|
|
||||||
|
message Item {
|
||||||
|
required string uri = 1;
|
||||||
|
optional ItemAttributes attributes = 2;
|
||||||
|
}
|
||||||
|
message ListItems {
|
||||||
|
required int32 pos = 1;
|
||||||
|
required bool truncated = 2;
|
||||||
|
repeated Item items = 3;
|
||||||
|
}
|
||||||
|
message ContentRange {
|
||||||
|
required int32 pos = 1;
|
||||||
|
optional int32 length = 2;
|
||||||
|
}
|
||||||
|
message ListContentSelection {
|
||||||
|
optional bool wantRevision = 1;
|
||||||
|
optional bool wantLength = 2;
|
||||||
|
optional bool wantAttributes = 3;
|
||||||
|
optional bool wantChecksum = 4;
|
||||||
|
optional bool wantContent = 5;
|
||||||
|
optional ContentRange contentRange = 6;
|
||||||
|
optional bool wantDiff = 7;
|
||||||
|
optional bytes baseRevision = 8;
|
||||||
|
optional bytes hintRevision = 9;
|
||||||
|
optional bool wantNothingIfUpToDate = 10;
|
||||||
|
optional bool wantResolveAction = 12;
|
||||||
|
repeated ClientIssue issues = 13;
|
||||||
|
repeated ClientResolveAction resolveAction = 14;
|
||||||
|
}
|
40
protocol/proto/playlist4issues.proto
Normal file
40
protocol/proto/playlist4issues.proto
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
message ClientIssue {
|
||||||
|
enum Level {
|
||||||
|
LEVEL_UNKNOWN = 0;
|
||||||
|
LEVEL_DEBUG = 1;
|
||||||
|
LEVEL_INFO = 2;
|
||||||
|
LEVEL_NOTICE = 3;
|
||||||
|
LEVEL_WARNING = 4;
|
||||||
|
LEVEL_ERROR = 5;
|
||||||
|
}
|
||||||
|
enum Code {
|
||||||
|
CODE_UNKNOWN = 0;
|
||||||
|
CODE_INDEX_OUT_OF_BOUNDS = 1;
|
||||||
|
CODE_VERSION_MISMATCH = 2;
|
||||||
|
CODE_CACHED_CHANGE = 3;
|
||||||
|
CODE_OFFLINE_CHANGE = 4;
|
||||||
|
CODE_CONCURRENT_CHANGE = 5;
|
||||||
|
}
|
||||||
|
optional Level level = 1;
|
||||||
|
optional Code code = 2;
|
||||||
|
optional int32 repeatCount = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientResolveAction {
|
||||||
|
enum Code {
|
||||||
|
CODE_UNKNOWN = 0;
|
||||||
|
CODE_NO_ACTION = 1;
|
||||||
|
CODE_RETRY = 2;
|
||||||
|
CODE_RELOAD = 3;
|
||||||
|
CODE_DISCARD_LOCAL_CHANGES = 4;
|
||||||
|
CODE_SEND_DUMP = 5;
|
||||||
|
CODE_DISPLAY_ERROR_MESSAGE = 6;
|
||||||
|
}
|
||||||
|
enum Initiator {
|
||||||
|
INITIATOR_UNKNOWN = 0;
|
||||||
|
INITIATOR_SERVER = 1;
|
||||||
|
INITIATOR_CLIENT = 2;
|
||||||
|
}
|
||||||
|
optional Code code = 1;
|
||||||
|
optional Initiator initiator = 2;
|
||||||
|
}
|
58
protocol/proto/playlist4meta.proto
Normal file
58
protocol/proto/playlist4meta.proto
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
message ListChecksum {
|
||||||
|
required int32 version = 1;
|
||||||
|
optional bytes sha1 = 4;
|
||||||
|
}
|
||||||
|
message DownloadFormat {
|
||||||
|
enum Codec {
|
||||||
|
CODEC_UNKNOWN = 0;
|
||||||
|
OGG_VORBIS = 1;
|
||||||
|
FLAC = 2;
|
||||||
|
MPEG_1_LAYER_3 = 3;
|
||||||
|
}
|
||||||
|
required Codec codec = 1;
|
||||||
|
}
|
||||||
|
enum ListAttributeKind {
|
||||||
|
LIST_UNKNOWN = 0;
|
||||||
|
LIST_NAME = 1;
|
||||||
|
LIST_DESCRIPTION = 2;
|
||||||
|
LIST_PICTURE = 3;
|
||||||
|
LIST_COLLABORATIVE = 4;
|
||||||
|
LIST_PL3_VERSION = 5;
|
||||||
|
LIST_DELETED_BY_OWNER = 6;
|
||||||
|
LIST_RESTRICTED_COLLABORATIVE = 7;
|
||||||
|
}
|
||||||
|
message ListAttributes {
|
||||||
|
optional string name = 1;
|
||||||
|
optional string description = 2;
|
||||||
|
optional bytes picture = 3;
|
||||||
|
optional bool collaborative = 4;
|
||||||
|
optional string pl3_version = 5;
|
||||||
|
optional bool deleted_by_owner = 6;
|
||||||
|
optional bool restricted_collaborative = 7;
|
||||||
|
}
|
||||||
|
enum ItemAttributeKind {
|
||||||
|
ITEM_UNKNOWN = 0;
|
||||||
|
ITEM_ADDED_BY = 1;
|
||||||
|
ITEM_TIMESTAMP = 2;
|
||||||
|
ITEM_MESSAGE = 3;
|
||||||
|
ITEM_SEEN = 4;
|
||||||
|
ITEM_DOWNLOAD_COUNT = 5;
|
||||||
|
ITEM_DOWNLOAD_FORMAT = 6;
|
||||||
|
ITEM_SEVENDIGITAL_ID = 7;
|
||||||
|
ITEM_SEVENDIGITAL_LEFT = 8;
|
||||||
|
ITEM_SEEN_AT = 9;
|
||||||
|
}
|
||||||
|
message ItemAttributes {
|
||||||
|
optional string added_by = 1;
|
||||||
|
optional string message = 3;
|
||||||
|
optional bool seen = 4;
|
||||||
|
optional DownloadFormat download_format = 6;
|
||||||
|
optional string sevendigital_id = 7;
|
||||||
|
}
|
||||||
|
message StringAttribute {
|
||||||
|
required string key = 1;
|
||||||
|
required string value = 2;
|
||||||
|
}
|
||||||
|
message StringAttributes {
|
||||||
|
repeated StringAttribute attribute = 1;
|
||||||
|
}
|
68
protocol/proto/playlist4ops.proto
Normal file
68
protocol/proto/playlist4ops.proto
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import "playlist4content.proto";
|
||||||
|
import "playlist4meta.proto";
|
||||||
|
|
||||||
|
message Add {
|
||||||
|
optional int32 fromIndex = 1;
|
||||||
|
repeated Item items = 2;
|
||||||
|
optional ListChecksum list_checksum = 3;
|
||||||
|
optional bool addLast = 4;
|
||||||
|
optional bool addFirst = 5;
|
||||||
|
}
|
||||||
|
message Rem {
|
||||||
|
optional int32 fromIndex = 1;
|
||||||
|
optional int32 length = 2;
|
||||||
|
repeated Item items = 3;
|
||||||
|
optional ListChecksum list_checksum = 4;
|
||||||
|
optional ListChecksum items_checksum = 5;
|
||||||
|
optional ListChecksum uris_checksum = 6;
|
||||||
|
optional bool itemsAsKey = 7;
|
||||||
|
}
|
||||||
|
message Mov {
|
||||||
|
required int32 fromIndex = 1;
|
||||||
|
required int32 length = 2;
|
||||||
|
required int32 toIndex = 3;
|
||||||
|
optional ListChecksum list_checksum = 4;
|
||||||
|
optional ListChecksum items_checksum = 5;
|
||||||
|
optional ListChecksum uris_checksum = 6;
|
||||||
|
}
|
||||||
|
message ItemAttributesPartialState {
|
||||||
|
required ItemAttributes values = 1;
|
||||||
|
repeated ItemAttributeKind no_value = 2;
|
||||||
|
}
|
||||||
|
message ListAttributesPartialState {
|
||||||
|
required ListAttributes values = 1;
|
||||||
|
repeated ListAttributeKind no_value = 2;
|
||||||
|
}
|
||||||
|
message UpdateItemAttributes {
|
||||||
|
required int32 index = 1;
|
||||||
|
required ItemAttributesPartialState new_attributes = 2;
|
||||||
|
optional ItemAttributesPartialState old_attributes = 3;
|
||||||
|
optional ListChecksum list_checksum = 4;
|
||||||
|
optional ListChecksum old_attributes_checksum = 5;
|
||||||
|
}
|
||||||
|
message UpdateListAttributes {
|
||||||
|
required ListAttributesPartialState new_attributes = 1;
|
||||||
|
optional ListAttributesPartialState old_attributes = 2;
|
||||||
|
optional ListChecksum list_checksum = 3;
|
||||||
|
optional ListChecksum old_attributes_checksum = 4;
|
||||||
|
}
|
||||||
|
message Op {
|
||||||
|
enum Kind {
|
||||||
|
KIND_UNKNOWN = 0;
|
||||||
|
ADD = 2;
|
||||||
|
REM = 3;
|
||||||
|
MOV = 4;
|
||||||
|
UPDATE_ITEM_ATTRIBUTES = 5;
|
||||||
|
UPDATE_LIST_ATTRIBUTES = 6;
|
||||||
|
};
|
||||||
|
required Kind kind = 1;
|
||||||
|
optional Add add = 2;
|
||||||
|
optional Rem rem = 3;
|
||||||
|
optional Mov mov = 4;
|
||||||
|
optional UpdateItemAttributes update_item_attributes = 5;
|
||||||
|
optional UpdateListAttributes update_list_attributes = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OpList {
|
||||||
|
repeated Op ops = 1;
|
||||||
|
}
|
118
protocol/proto/playlist4service.proto
Normal file
118
protocol/proto/playlist4service.proto
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import "playlist4changes.proto";
|
||||||
|
import "playlist4content.proto";
|
||||||
|
|
||||||
|
message RequestContext {
|
||||||
|
optional bool administrative = 2;
|
||||||
|
optional bool migration = 4;
|
||||||
|
optional string tag = 7;
|
||||||
|
optional bool useStarredView = 8;
|
||||||
|
optional bool syncWithPublished = 9;
|
||||||
|
}
|
||||||
|
message GetCurrentRevisionArgs {
|
||||||
|
optional bytes uri = 1;
|
||||||
|
optional RequestContext context = 2;
|
||||||
|
}
|
||||||
|
message GetChangesInSequenceRangeArgs {
|
||||||
|
optional bytes uri = 1;
|
||||||
|
optional RequestContext context = 2;
|
||||||
|
optional int32 fromSequenceNumber = 3;
|
||||||
|
optional int32 toSequenceNumber = 4;
|
||||||
|
}
|
||||||
|
message GetChangesInSequenceRangeMatchingPl3VersionArgs {
|
||||||
|
optional bytes uri = 1;
|
||||||
|
optional RequestContext context = 2;
|
||||||
|
optional int32 fromSequenceNumber = 3;
|
||||||
|
optional int32 toSequenceNumber = 4;
|
||||||
|
optional string pl3Version = 5;
|
||||||
|
}
|
||||||
|
message GetChangesInSequenceRangeReturn {
|
||||||
|
repeated RevisionTaggedChangeSet result = 1;
|
||||||
|
}
|
||||||
|
message ObliterateListArgs {
|
||||||
|
optional bytes uri = 1;
|
||||||
|
optional RequestContext context = 2;
|
||||||
|
}
|
||||||
|
message UpdatePublishedArgs {
|
||||||
|
optional bytes publishedUri = 1;
|
||||||
|
optional RequestContext context = 2;
|
||||||
|
optional bytes uri = 3;
|
||||||
|
optional bool isPublished = 4;
|
||||||
|
}
|
||||||
|
message SynchronizeArgs {
|
||||||
|
optional bytes uri = 1;
|
||||||
|
optional RequestContext context = 2;
|
||||||
|
optional ListContentSelection selection = 3;
|
||||||
|
optional ListChanges changes = 4;
|
||||||
|
}
|
||||||
|
message GetSnapshotAtRevisionArgs {
|
||||||
|
optional bytes uri = 1;
|
||||||
|
optional RequestContext context = 2;
|
||||||
|
optional bytes revision = 3;
|
||||||
|
}
|
||||||
|
message SubscribeRequest {
|
||||||
|
repeated bytes uris = 1;
|
||||||
|
}
|
||||||
|
message UnsubscribeRequest {
|
||||||
|
repeated bytes uris = 1;
|
||||||
|
}
|
||||||
|
enum Playlist4InboxErrorKind {
|
||||||
|
INBOX_NOT_ALLOWED = 2;
|
||||||
|
INBOX_INVALID_USER = 3;
|
||||||
|
INBOX_INVALID_URI = 4;
|
||||||
|
INBOX_LIST_TOO_LONG = 5;
|
||||||
|
}
|
||||||
|
message Playlist4ServiceException {
|
||||||
|
optional string why = 1;
|
||||||
|
optional string symbol = 2;
|
||||||
|
optional bool permanent = 3;
|
||||||
|
optional string serviceErrorClass = 4;
|
||||||
|
optional Playlist4InboxErrorKind inboxErrorKind = 5;
|
||||||
|
}
|
||||||
|
message SynchronizeReturn {
|
||||||
|
optional SelectedListContent result = 1;
|
||||||
|
optional Playlist4ServiceException exception = 4;
|
||||||
|
}
|
||||||
|
enum Playlist4ServiceMethodKind {
|
||||||
|
METHOD_UNKNOWN = 0;
|
||||||
|
METHOD_GET_CURRENT_REVISION = 2;
|
||||||
|
METHOD_GET_CHANGES_IN_SEQUENCE_RANGE = 3;
|
||||||
|
METHOD_OBLITERATE_LIST = 4;
|
||||||
|
METHOD_SYNCHRONIZE = 5;
|
||||||
|
METHOD_UPDATE_PUBLISHED = 6;
|
||||||
|
METHOD_GET_CHANGES_IN_SEQUENCE_RANGE_MATCHING_PL3_VERSION = 7;
|
||||||
|
METHOD_GET_SNAPSHOT_AT_REVISION = 8;
|
||||||
|
}
|
||||||
|
message Playlist4ServiceCall {
|
||||||
|
optional Playlist4ServiceMethodKind kind = 1;
|
||||||
|
optional GetCurrentRevisionArgs getCurrentRevisionArgs = 2;
|
||||||
|
optional GetChangesInSequenceRangeArgs getChangesInSequenceRangeArgs = 3;
|
||||||
|
optional ObliterateListArgs obliterateListArgs = 4;
|
||||||
|
optional SynchronizeArgs synchronizeArgs = 5;
|
||||||
|
optional UpdatePublishedArgs updatePublishedArgs = 6;
|
||||||
|
optional GetChangesInSequenceRangeMatchingPl3VersionArgs getChangesInSequenceRangeMatchingPl3VersionArgs = 7;
|
||||||
|
optional GetSnapshotAtRevisionArgs getSnapshotAtRevisionArgs = 8;
|
||||||
|
}
|
||||||
|
message Playlist4ServiceReturn {
|
||||||
|
optional Playlist4ServiceMethodKind kind = 1;
|
||||||
|
optional Playlist4ServiceException exception = 2;
|
||||||
|
optional bytes getCurrentRevisionReturn = 3;
|
||||||
|
optional GetChangesInSequenceRangeReturn getChangesInSequenceRangeReturn = 4;
|
||||||
|
optional bool obliterateListReturn = 5;
|
||||||
|
optional SynchronizeReturn synchronizeReturn = 6;
|
||||||
|
optional bool updatePublishedReturn = 7;
|
||||||
|
optional GetChangesInSequenceRangeReturn getChangesInSequenceRangeMatchingPl3VersionReturn = 8;
|
||||||
|
//optional RevisionTaggedListSnapshot getSnapshotAtRevisionReturn = 9;
|
||||||
|
optional bytes getSnapshotAtRevisionReturn = 9;
|
||||||
|
}
|
||||||
|
message CreateListReply {
|
||||||
|
required bytes uri = 1;
|
||||||
|
optional bytes revision = 2;
|
||||||
|
}
|
||||||
|
message ModifyReply {
|
||||||
|
required bytes uri = 1;
|
||||||
|
optional bytes revision = 2;
|
||||||
|
}
|
||||||
|
message PlaylistModificationInfo {
|
||||||
|
optional bytes uri = 1;
|
||||||
|
optional bytes new_revision = 2;
|
||||||
|
}
|
|
@ -5,4 +5,13 @@ extern crate protobuf;
|
||||||
|
|
||||||
mod_path! keyexchange (concat!(env!("OUT_DIR"), "/keyexchange.rs"));
|
mod_path! keyexchange (concat!(env!("OUT_DIR"), "/keyexchange.rs"));
|
||||||
mod_path! authentication (concat!(env!("OUT_DIR"), "/authentication.rs"));
|
mod_path! authentication (concat!(env!("OUT_DIR"), "/authentication.rs"));
|
||||||
|
mod_path! mercury (concat!(env!("OUT_DIR"), "/mercury.rs"));
|
||||||
|
mod_path! metadata (concat!(env!("OUT_DIR"), "/metadata.rs"));
|
||||||
|
|
||||||
|
mod_path! playlist4changes (concat!(env!("OUT_DIR"), "/playlist4changes.rs"));
|
||||||
|
mod_path! playlist4content (concat!(env!("OUT_DIR"), "/playlist4content.rs"));
|
||||||
|
mod_path! playlist4issues (concat!(env!("OUT_DIR"), "/playlist4issues.rs"));
|
||||||
|
mod_path! playlist4meta (concat!(env!("OUT_DIR"), "/playlist4meta.rs"));
|
||||||
|
mod_path! playlist4ops (concat!(env!("OUT_DIR"), "/playlist4ops.rs"));
|
||||||
|
mod_path! playlist4service (concat!(env!("OUT_DIR"), "/playlist4service.rs"));
|
||||||
|
|
||||||
|
|
54
src/audio_decrypt.rs
Normal file
54
src/audio_decrypt.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use crypto::aes;
|
||||||
|
use crypto::symmetriccipher::SynchronousStreamCipher;
|
||||||
|
use readall::ReadAllExt;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use audio_key::AudioKey;
|
||||||
|
|
||||||
|
const AUDIO_AESIV : &'static [u8] = &[
|
||||||
|
0x72,0xe0,0x67,0xfb,0xdd,0xcb,0xcf,0x77,0xeb,0xe8,0xbc,0x64,0x3f,0x63,0x0d,0x93,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct AudioDecrypt<T : io::Read> {
|
||||||
|
cipher: Box<SynchronousStreamCipher + 'static>,
|
||||||
|
key: AudioKey,
|
||||||
|
reader: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T : io::Read> AudioDecrypt<T> {
|
||||||
|
pub fn new(key: AudioKey, mut reader: T) -> AudioDecrypt<T> {
|
||||||
|
let mut cipher = aes::ctr(aes::KeySize::KeySize128,
|
||||||
|
&key,
|
||||||
|
AUDIO_AESIV);
|
||||||
|
|
||||||
|
let mut buf = [0; 0xa7];
|
||||||
|
let mut buf2 = [0; 0xa7];
|
||||||
|
reader.read_all(&mut buf).unwrap();
|
||||||
|
cipher.process(&buf, &mut buf2);
|
||||||
|
|
||||||
|
AudioDecrypt {
|
||||||
|
cipher: cipher,
|
||||||
|
key: key,
|
||||||
|
reader: reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T : io::Read> io::Read for AudioDecrypt<T> {
|
||||||
|
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let mut buffer = vec![0u8; output.len()];
|
||||||
|
let len = try!(self.reader.read(&mut buffer));
|
||||||
|
|
||||||
|
self.cipher.process(&buffer[..len], &mut output[..len]);
|
||||||
|
|
||||||
|
Ok(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T : io::Read> io::Seek for AudioDecrypt<T> {
|
||||||
|
fn seek(&mut self, _pos: io::SeekFrom) -> io::Result<u64> {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "Cannot seek"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
149
src/audio_file.rs
Normal file
149
src/audio_file.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
use byteorder::{ByteOrder, BigEndian};
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::collections::BitSet;
|
||||||
|
use std::io;
|
||||||
|
use std::slice::bytes::copy_memory;
|
||||||
|
use std::sync::{mpsc, Arc, Condvar, Mutex};
|
||||||
|
|
||||||
|
use stream::{StreamRequest, StreamEvent};
|
||||||
|
use util::FileId;
|
||||||
|
|
||||||
|
const CHUNK_SIZE: usize = 0x40000;
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AudioFileRef(Arc<AudioFile>);
|
||||||
|
|
||||||
|
struct AudioFile {
|
||||||
|
file: FileId,
|
||||||
|
size: usize,
|
||||||
|
|
||||||
|
data: Mutex<AudioFileData>,
|
||||||
|
cond: Condvar
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AudioFileData {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
bitmap: BitSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFileRef {
|
||||||
|
pub fn new(file: FileId, streams: mpsc::Sender<StreamRequest>) -> AudioFileRef {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
streams.send(StreamRequest {
|
||||||
|
id: file,
|
||||||
|
offset: 0,
|
||||||
|
size: 1,
|
||||||
|
callback: tx
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
let size = {
|
||||||
|
let mut size = None;
|
||||||
|
for event in rx.iter() {
|
||||||
|
match event {
|
||||||
|
StreamEvent::Header(id, data) => {
|
||||||
|
if id == 0x3 {
|
||||||
|
size = Some(BigEndian::read_u32(&data) * 4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
StreamEvent::Data(_) => break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size.unwrap() as usize
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioFileRef(Arc::new(AudioFile {
|
||||||
|
file: file,
|
||||||
|
size: size,
|
||||||
|
|
||||||
|
data: Mutex::new(AudioFileData {
|
||||||
|
buffer: vec![0u8; size + (CHUNK_SIZE - size % CHUNK_SIZE)],
|
||||||
|
bitmap: BitSet::with_capacity(size / CHUNK_SIZE)
|
||||||
|
}),
|
||||||
|
cond: Condvar::new(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch(&self, streams: mpsc::Sender<StreamRequest>) {
|
||||||
|
let &AudioFileRef(ref inner) = self;
|
||||||
|
|
||||||
|
let mut index : usize = 0;
|
||||||
|
|
||||||
|
while index * CHUNK_SIZE < inner.size {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
streams.send(StreamRequest {
|
||||||
|
id: inner.file,
|
||||||
|
offset: (index * CHUNK_SIZE / 4) as u32,
|
||||||
|
size: (CHUNK_SIZE / 4) as u32,
|
||||||
|
callback: tx
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
let mut offset = 0;
|
||||||
|
for event in rx.iter() {
|
||||||
|
match event {
|
||||||
|
StreamEvent::Header(_,_) => (),
|
||||||
|
StreamEvent::Data(data) => {
|
||||||
|
let mut handle = inner.data.lock().unwrap();
|
||||||
|
copy_memory(&data, &mut handle.buffer[index * CHUNK_SIZE + offset..]);
|
||||||
|
offset += data.len();
|
||||||
|
|
||||||
|
if offset >= CHUNK_SIZE {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut handle = inner.data.lock().unwrap();
|
||||||
|
handle.bitmap.insert(index);
|
||||||
|
inner.cond.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AudioFileReader {
|
||||||
|
file: AudioFileRef,
|
||||||
|
position: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFileReader {
|
||||||
|
pub fn new(file: &AudioFileRef) -> AudioFileReader {
|
||||||
|
AudioFileReader {
|
||||||
|
file: file.clone(),
|
||||||
|
position: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for AudioFileReader {
|
||||||
|
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let index = self.position / CHUNK_SIZE;
|
||||||
|
let offset = self.position % CHUNK_SIZE;
|
||||||
|
let len = min(output.len(), CHUNK_SIZE-offset);
|
||||||
|
|
||||||
|
let &AudioFileRef(ref inner) = &self.file;
|
||||||
|
let mut handle = inner.data.lock().unwrap();
|
||||||
|
|
||||||
|
while !handle.bitmap.contains(&index) {
|
||||||
|
handle = inner.cond.wait(handle).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_memory(&handle.buffer[self.position..self.position+len], output);
|
||||||
|
self.position += len;
|
||||||
|
|
||||||
|
Ok(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Seek for AudioFileReader {
|
||||||
|
fn seek(&mut self, _pos: io::SeekFrom) -> io::Result<u64> {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "Cannot seek"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
109
src/audio_key.rs
Normal file
109
src/audio_key.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::io::{Cursor, Write};
|
||||||
|
use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
|
||||||
|
use readall::ReadAllExt;
|
||||||
|
|
||||||
|
use connection::Packet;
|
||||||
|
use util::{SpotifyId, FileId};
|
||||||
|
use util::Either::{Left, Right};
|
||||||
|
use subsystem::Subsystem;
|
||||||
|
|
||||||
|
pub struct AudioKeyRequest {
|
||||||
|
pub track: SpotifyId,
|
||||||
|
pub file: FileId,
|
||||||
|
pub callback: AudioKeyCallback,
|
||||||
|
}
|
||||||
|
pub type AudioKey = [u8; 16];
|
||||||
|
pub struct AudioKeyResponse(pub AudioKey);
|
||||||
|
pub type AudioKeyCallback = mpsc::Sender<AudioKeyResponse>;
|
||||||
|
|
||||||
|
type AudioKeyId = u32;
|
||||||
|
pub struct AudioKeyManager {
|
||||||
|
next_seq: AudioKeyId,
|
||||||
|
callbacks: HashMap<AudioKeyId, AudioKeyCallback>,
|
||||||
|
|
||||||
|
requests: mpsc::Receiver<AudioKeyRequest>,
|
||||||
|
packet_rx: mpsc::Receiver<Packet>,
|
||||||
|
packet_tx: mpsc::Sender<Packet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioKeyManager {
|
||||||
|
pub fn new(tx: mpsc::Sender<Packet>) -> (AudioKeyManager,
|
||||||
|
mpsc::Sender<AudioKeyRequest>,
|
||||||
|
mpsc::Sender<Packet>) {
|
||||||
|
let (req_tx, req_rx) = mpsc::channel();
|
||||||
|
let (pkt_tx, pkt_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
(AudioKeyManager {
|
||||||
|
next_seq: 1,
|
||||||
|
callbacks: HashMap::new(),
|
||||||
|
|
||||||
|
requests: req_rx,
|
||||||
|
packet_rx: pkt_rx,
|
||||||
|
packet_tx: tx
|
||||||
|
}, req_tx, pkt_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(&mut self, req: AudioKeyRequest) {
|
||||||
|
let seq = self.next_seq;
|
||||||
|
self.next_seq += 1;
|
||||||
|
|
||||||
|
let mut data : Vec<u8> = Vec::new();
|
||||||
|
data.write(&req.file).unwrap();
|
||||||
|
data.write(&req.track.to_raw()).unwrap();
|
||||||
|
data.write_u32::<BigEndian>(seq).unwrap();
|
||||||
|
data.write_u16::<BigEndian>(0x0000).unwrap();
|
||||||
|
|
||||||
|
self.packet_tx.send(Packet {
|
||||||
|
cmd: 0xc,
|
||||||
|
data: data
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
self.callbacks.insert(seq, req.callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packet(&mut self, packet: Packet) {
|
||||||
|
assert_eq!(packet.cmd, 0xd);
|
||||||
|
|
||||||
|
let mut data = Cursor::new(&packet.data as &[u8]);
|
||||||
|
let seq = data.read_u32::<BigEndian>().unwrap();
|
||||||
|
let mut key = [0u8; 16];
|
||||||
|
data.read_all(&mut key).unwrap();
|
||||||
|
|
||||||
|
match self.callbacks.remove(&seq) {
|
||||||
|
Some(callback) => callback.send(AudioKeyResponse(key)).unwrap(),
|
||||||
|
None => ()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Subsystem for AudioKeyManager {
|
||||||
|
fn run(mut self) {
|
||||||
|
loop {
|
||||||
|
match {
|
||||||
|
let requests = &self.requests;
|
||||||
|
let packets = &self.packet_rx;
|
||||||
|
|
||||||
|
select!{
|
||||||
|
r = requests.recv() => {
|
||||||
|
Left(r.unwrap())
|
||||||
|
},
|
||||||
|
p = packets.recv() => {
|
||||||
|
Right(p.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
Left(req) => {
|
||||||
|
self.request(req);
|
||||||
|
}
|
||||||
|
Right(pkt) => {
|
||||||
|
self.packet(pkt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use util;
|
use byteorder::{self, BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
use byteorder::{self, ReadBytesExt, WriteBytesExt, BigEndian, ByteOrder};
|
|
||||||
use keys::SharedKeys;
|
|
||||||
use readall::ReadAllExt;
|
use readall::ReadAllExt;
|
||||||
use shannon::ShannonStream;
|
use shannon::ShannonStream;
|
||||||
use std::convert;
|
use std::convert;
|
||||||
|
@ -9,6 +6,10 @@ use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::result;
|
use std::result;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use keys::SharedKeys;
|
||||||
|
use util;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
@ -37,6 +38,7 @@ pub struct PlainConnection {
|
||||||
stream: TcpStream
|
stream: TcpStream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct CipherConnection {
|
pub struct CipherConnection {
|
||||||
stream: ShannonStream<TcpStream>,
|
stream: ShannonStream<TcpStream>,
|
||||||
}
|
}
|
||||||
|
@ -106,5 +108,70 @@ impl CipherConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Packet {
|
||||||
|
pub cmd: u8,
|
||||||
|
pub data: Vec<u8>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SendThread {
|
||||||
|
connection: CipherConnection,
|
||||||
|
receiver: mpsc::Receiver<Packet>,
|
||||||
|
}
|
||||||
|
impl SendThread {
|
||||||
|
pub fn new(connection: CipherConnection)
|
||||||
|
-> (SendThread, mpsc::Sender<Packet>) {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
(SendThread {
|
||||||
|
connection: connection,
|
||||||
|
receiver: rx
|
||||||
|
}, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self) {
|
||||||
|
for req in self.receiver {
|
||||||
|
self.connection.send_encrypted_packet(
|
||||||
|
req.cmd, &req.data).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PacketDispatch {
|
||||||
|
pub main: mpsc::Sender<Packet>,
|
||||||
|
pub stream: mpsc::Sender<Packet>,
|
||||||
|
pub mercury: mpsc::Sender<Packet>,
|
||||||
|
pub audio_key: mpsc::Sender<Packet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RecvThread {
|
||||||
|
connection: CipherConnection,
|
||||||
|
dispatch: PacketDispatch
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecvThread {
|
||||||
|
pub fn new(connection: CipherConnection, dispatch: PacketDispatch)
|
||||||
|
-> RecvThread {
|
||||||
|
RecvThread {
|
||||||
|
connection: connection,
|
||||||
|
dispatch: dispatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self) {
|
||||||
|
loop {
|
||||||
|
let (cmd, data) = self.connection.recv_packet().unwrap();
|
||||||
|
let packet = Packet {
|
||||||
|
cmd: cmd,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
|
||||||
|
match packet.cmd {
|
||||||
|
0x09 => &self.dispatch.stream,
|
||||||
|
0xd | 0xe => &self.dispatch.audio_key,
|
||||||
|
0xb2...0xb6 => &self.dispatch.mercury,
|
||||||
|
_ => &self.dispatch.main,
|
||||||
|
}.send(packet).unwrap();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
12
src/keys.rs
12
src/keys.rs
|
@ -1,8 +1,8 @@
|
||||||
use rand;
|
|
||||||
use gmp::Mpz;
|
|
||||||
use num::FromPrimitive;
|
|
||||||
use crypto;
|
use crypto;
|
||||||
use crypto::mac::Mac;
|
use crypto::mac::Mac;
|
||||||
|
use gmp::Mpz;
|
||||||
|
use num::FromPrimitive;
|
||||||
|
use rand;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use util;
|
use util;
|
||||||
|
@ -29,7 +29,7 @@ pub struct PrivateKeys {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SharedKeys {
|
pub struct SharedKeys {
|
||||||
private: PrivateKeys,
|
//private: PrivateKeys,
|
||||||
challenge: Vec<u8>,
|
challenge: Vec<u8>,
|
||||||
send_key: Vec<u8>,
|
send_key: Vec<u8>,
|
||||||
recv_key: Vec<u8>
|
recv_key: Vec<u8>
|
||||||
|
@ -51,9 +51,11 @@ impl PrivateKeys {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
pub fn private_key(&self) -> Vec<u8> {
|
pub fn private_key(&self) -> Vec<u8> {
|
||||||
return self.private_key.to_bytes_be();
|
return self.private_key.to_bytes_be();
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
pub fn public_key(&self) -> Vec<u8> {
|
pub fn public_key(&self) -> Vec<u8> {
|
||||||
return self.public_key.to_bytes_be();
|
return self.public_key.to_bytes_be();
|
||||||
|
@ -78,7 +80,7 @@ impl PrivateKeys {
|
||||||
mac.input(server_packet);
|
mac.input(server_packet);
|
||||||
|
|
||||||
SharedKeys {
|
SharedKeys {
|
||||||
private: self,
|
//private: self,
|
||||||
challenge: mac.result().code().to_vec(),
|
challenge: mac.result().code().to_vec(),
|
||||||
send_key: data[0x14..0x34].to_vec(),
|
send_key: data[0x14..0x34].to_vec(),
|
||||||
recv_key: data[0x34..0x54].to_vec(),
|
recv_key: data[0x34..0x54].to_vec(),
|
||||||
|
|
59
src/main.rs
59
src/main.rs
|
@ -1,6 +1,6 @@
|
||||||
#![crate_name = "librespot"]
|
#![crate_name = "librespot"]
|
||||||
|
|
||||||
#![feature(plugin)]
|
#![feature(alloc,plugin,core,collections,std_misc,zero_one)]
|
||||||
|
|
||||||
#![plugin(protobuf_macros)]
|
#![plugin(protobuf_macros)]
|
||||||
#[macro_use] extern crate lazy_static;
|
#[macro_use] extern crate lazy_static;
|
||||||
|
@ -9,29 +9,45 @@ extern crate byteorder;
|
||||||
extern crate crypto;
|
extern crate crypto;
|
||||||
extern crate gmp;
|
extern crate gmp;
|
||||||
extern crate num;
|
extern crate num;
|
||||||
|
extern crate portaudio;
|
||||||
extern crate protobuf;
|
extern crate protobuf;
|
||||||
extern crate shannon;
|
extern crate shannon;
|
||||||
extern crate rand;
|
extern crate rand;
|
||||||
extern crate readall;
|
extern crate readall;
|
||||||
|
extern crate vorbis;
|
||||||
|
|
||||||
extern crate librespot_protocol;
|
extern crate librespot_protocol;
|
||||||
|
|
||||||
|
#[macro_use] mod util;
|
||||||
|
mod audio_decrypt;
|
||||||
|
mod audio_file;
|
||||||
|
mod audio_key;
|
||||||
mod connection;
|
mod connection;
|
||||||
mod keys;
|
mod keys;
|
||||||
|
mod mercury;
|
||||||
|
mod metadata;
|
||||||
|
mod player;
|
||||||
mod session;
|
mod session;
|
||||||
mod util;
|
mod stream;
|
||||||
|
mod subsystem;
|
||||||
|
|
||||||
|
use std::clone::Clone;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::{Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use session::{Session,Config};
|
use metadata::{MetadataCache, AlbumRef, ArtistRef, TrackRef};
|
||||||
|
use session::{Config, Session};
|
||||||
|
use util::SpotifyId;
|
||||||
|
use player::Player;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
let mut appkey_file = File::open(Path::new(&args.next().unwrap())).unwrap();
|
let mut appkey_file = File::open(Path::new(&args.next().unwrap())).unwrap();
|
||||||
let username = args.next().unwrap();
|
let username = args.next().unwrap();
|
||||||
let password = args.next().unwrap();
|
let password = args.next().unwrap();
|
||||||
|
let track_uri = args.next().unwrap();
|
||||||
|
let track_id = SpotifyId::from_base62(track_uri.split(':').nth(2).unwrap());
|
||||||
|
|
||||||
let mut appkey = Vec::new();
|
let mut appkey = Vec::new();
|
||||||
appkey_file.read_to_end(&mut appkey).unwrap();
|
appkey_file.read_to_end(&mut appkey).unwrap();
|
||||||
|
@ -41,8 +57,39 @@ fn main() {
|
||||||
user_agent: "ABC".to_string(),
|
user_agent: "ABC".to_string(),
|
||||||
device_id: "ABC".to_string()
|
device_id: "ABC".to_string()
|
||||||
};
|
};
|
||||||
let mut s = Session::new(config);
|
let session = Session::new(config);
|
||||||
|
session.login(username, password);
|
||||||
|
session.poll();
|
||||||
|
|
||||||
s.login(username, password);
|
let mut cache = MetadataCache::new(session.metadata.clone());
|
||||||
|
let track : TrackRef = cache.get(track_id);
|
||||||
|
|
||||||
|
let album : AlbumRef = {
|
||||||
|
let handle = track.wait();
|
||||||
|
let data = handle.unwrap();
|
||||||
|
eprintln!("{}", data.name);
|
||||||
|
cache.get(data.album)
|
||||||
|
};
|
||||||
|
|
||||||
|
let artists : Vec<ArtistRef> = {
|
||||||
|
let handle = album.wait();
|
||||||
|
let data = handle.unwrap();
|
||||||
|
eprintln!("{}", data.name);
|
||||||
|
data.artists.iter().map(|id| {
|
||||||
|
cache.get(*id)
|
||||||
|
}).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
for artist in artists {
|
||||||
|
let handle = artist.wait();
|
||||||
|
let data = handle.unwrap();
|
||||||
|
eprintln!("{}", data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Player::play(&session, track);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
session.poll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
214
src/mercury.rs
Normal file
214
src/mercury.rs
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
|
||||||
|
use protobuf::{self, Message};
|
||||||
|
use readall::ReadAllExt;
|
||||||
|
use std::collections::{HashMap, LinkedList};
|
||||||
|
use std::io::{Cursor, Read, Write};
|
||||||
|
use std::fmt;
|
||||||
|
use std::mem::replace;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use connection::Packet;
|
||||||
|
use librespot_protocol as protocol;
|
||||||
|
use subsystem::Subsystem;
|
||||||
|
use util::Either::{Left, Right};
|
||||||
|
|
||||||
|
pub enum MercuryMethod {
|
||||||
|
GET,
|
||||||
|
GETX,
|
||||||
|
SUB,
|
||||||
|
UNSUB,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MercuryRequest {
|
||||||
|
pub method: MercuryMethod,
|
||||||
|
pub url: String,
|
||||||
|
pub mime: Option<String>,
|
||||||
|
pub callback: MercuryCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MercuryResponse {
|
||||||
|
pub url: String,
|
||||||
|
pub payload: LinkedList<Vec<u8>>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MercuryCallback = Option<mpsc::Sender<MercuryResponse>>;
|
||||||
|
|
||||||
|
pub struct MercuryPending {
|
||||||
|
parts: LinkedList<Vec<u8>>,
|
||||||
|
partial: Option<Vec<u8>>,
|
||||||
|
callback: MercuryCallback,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MercuryManager {
|
||||||
|
next_seq: u32,
|
||||||
|
pending: HashMap<Vec<u8>, MercuryPending>,
|
||||||
|
|
||||||
|
requests: mpsc::Receiver<MercuryRequest>,
|
||||||
|
packet_tx: mpsc::Sender<Packet>,
|
||||||
|
packet_rx: mpsc::Receiver<Packet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MercuryMethod {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
formatter.write_str(match *self {
|
||||||
|
MercuryMethod::GET => "GET",
|
||||||
|
MercuryMethod::GETX => "GETX",
|
||||||
|
MercuryMethod::SUB => "SUB",
|
||||||
|
MercuryMethod::UNSUB => "UNSUB"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MercuryManager {
|
||||||
|
pub fn new(tx: mpsc::Sender<Packet>) -> (MercuryManager,
|
||||||
|
mpsc::Sender<MercuryRequest>,
|
||||||
|
mpsc::Sender<Packet>) {
|
||||||
|
let (req_tx, req_rx) = mpsc::channel();
|
||||||
|
let (pkt_tx, pkt_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
(MercuryManager {
|
||||||
|
next_seq: 0,
|
||||||
|
pending: HashMap::new(),
|
||||||
|
|
||||||
|
requests: req_rx,
|
||||||
|
packet_rx: pkt_rx,
|
||||||
|
packet_tx: tx,
|
||||||
|
}, req_tx, pkt_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(&mut self, req: MercuryRequest) {
|
||||||
|
let mut seq = [0u8; 4];
|
||||||
|
BigEndian::write_u32(&mut seq, self.next_seq);
|
||||||
|
self.next_seq += 1;
|
||||||
|
let data = self.encode_request(&seq, &req);
|
||||||
|
|
||||||
|
self.packet_tx.send(Packet {
|
||||||
|
cmd: 0xb2,
|
||||||
|
data: data
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
self.pending.insert(seq.to_vec(), MercuryPending{
|
||||||
|
parts: LinkedList::new(),
|
||||||
|
partial: None,
|
||||||
|
callback: req.callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_part(mut s: &mut Read) -> Vec<u8> {
|
||||||
|
let size = s.read_u16::<BigEndian>().unwrap() as usize;
|
||||||
|
let mut buffer = vec![0; size];
|
||||||
|
s.read_all(&mut buffer).unwrap();
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_request(&mut self, seq: &[u8]) {
|
||||||
|
let mut pending = self.pending.remove(seq).unwrap();
|
||||||
|
|
||||||
|
let header_data = match pending.parts.pop_front() {
|
||||||
|
Some(data) => data,
|
||||||
|
None => panic!("No header part !")
|
||||||
|
};
|
||||||
|
|
||||||
|
let header : protocol::mercury::MercuryReply =
|
||||||
|
protobuf::parse_from_bytes(&header_data).unwrap();
|
||||||
|
|
||||||
|
match pending.callback {
|
||||||
|
Some(ch) => {
|
||||||
|
ch.send(MercuryResponse{
|
||||||
|
url: header.get_url().to_string(),
|
||||||
|
payload: pending.parts
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_packet(&mut self, _cmd: u8, data: Vec<u8>) {
|
||||||
|
let mut packet = Cursor::new(data);
|
||||||
|
|
||||||
|
let seq = {
|
||||||
|
let seq_length = packet.read_u16::<BigEndian>().unwrap() as usize;
|
||||||
|
let mut seq = vec![0; seq_length];
|
||||||
|
packet.read_all(&mut seq).unwrap();
|
||||||
|
seq
|
||||||
|
};
|
||||||
|
let flags = packet.read_u8().unwrap();
|
||||||
|
let count = packet.read_u16::<BigEndian>().unwrap() as usize;
|
||||||
|
|
||||||
|
{
|
||||||
|
let pending : &mut MercuryPending = match self.pending.get_mut(&seq) {
|
||||||
|
Some(pending) => pending,
|
||||||
|
None => { return; }
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..count {
|
||||||
|
let mut part = Self::parse_part(&mut packet);
|
||||||
|
if pending.partial.is_some() {
|
||||||
|
let mut data = replace(&mut pending.partial, None).unwrap();
|
||||||
|
data.append(&mut part);
|
||||||
|
part = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == count -1 && (flags == 2) {
|
||||||
|
pending.partial = Some(part)
|
||||||
|
} else {
|
||||||
|
pending.parts.push_back(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags == 0x1 {
|
||||||
|
self.complete_request(&seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_request(&self, seq: &[u8], req: &MercuryRequest) -> Vec<u8> {
|
||||||
|
let mut packet = Vec::new();
|
||||||
|
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap();
|
||||||
|
packet.write_all(seq).unwrap();
|
||||||
|
packet.write_u8(1).unwrap(); // Flags: FINAL
|
||||||
|
packet.write_u16::<BigEndian>(1).unwrap(); // Part count. Only header
|
||||||
|
|
||||||
|
let mut header = protobuf_init!(protocol::mercury::MercuryRequest::new(), {
|
||||||
|
url: req.url.clone(),
|
||||||
|
method: req.method.to_string(),
|
||||||
|
});
|
||||||
|
req.mime.clone().map(|mime| header.set_mime(mime));
|
||||||
|
|
||||||
|
packet.write_u16::<BigEndian>(header.compute_size() as u16).unwrap();
|
||||||
|
header.write_to_writer(&mut packet).unwrap();
|
||||||
|
|
||||||
|
packet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Subsystem for MercuryManager {
|
||||||
|
fn run(mut self) {
|
||||||
|
loop {
|
||||||
|
match {
|
||||||
|
let requests = &self.requests;
|
||||||
|
let packets = &self.packet_rx;
|
||||||
|
|
||||||
|
select!{
|
||||||
|
r = requests.recv() => {
|
||||||
|
Left(r.unwrap())
|
||||||
|
},
|
||||||
|
p = packets.recv() => {
|
||||||
|
Right(p.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
Left(req) => {
|
||||||
|
self.request(req);
|
||||||
|
}
|
||||||
|
Right(pkt) => {
|
||||||
|
self.handle_packet(pkt.cmd, pkt.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
270
src/metadata.rs
Normal file
270
src/metadata.rs
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
use protobuf::{self, Message};
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::slice::bytes::copy_memory;
|
||||||
|
use std::sync::{mpsc, Arc, Condvar, Mutex, MutexGuard, Weak};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
|
use mercury::{MercuryRequest, MercuryMethod};
|
||||||
|
use subsystem::Subsystem;
|
||||||
|
use util::{SpotifyId, FileId};
|
||||||
|
|
||||||
|
pub trait MetadataTrait : Send + Any + 'static {
|
||||||
|
type Message: protobuf::MessageStatic;
|
||||||
|
fn from_msg(msg: &Self::Message) -> Self;
|
||||||
|
fn base_url() -> &'static str;
|
||||||
|
fn request(r: MetadataRef<Self>) -> MetadataRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Track {
|
||||||
|
pub name: String,
|
||||||
|
pub album: SpotifyId,
|
||||||
|
pub files: Vec<FileId>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetadataTrait for Track {
|
||||||
|
type Message = protocol::metadata::Track;
|
||||||
|
fn from_msg(msg: &Self::Message) -> Self {
|
||||||
|
Track {
|
||||||
|
name: msg.get_name().to_string(),
|
||||||
|
album: SpotifyId::from_raw(msg.get_album().get_gid()),
|
||||||
|
files: msg.get_file().iter()
|
||||||
|
.map(|file| {
|
||||||
|
let mut dst = [0u8; 20];
|
||||||
|
copy_memory(&file.get_gid(), &mut dst);
|
||||||
|
dst
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn base_url() -> &'static str {
|
||||||
|
"hm://metadata/3/track"
|
||||||
|
}
|
||||||
|
fn request(r: MetadataRef<Self>) -> MetadataRequest {
|
||||||
|
MetadataRequest::Track(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Album {
|
||||||
|
pub name: String,
|
||||||
|
pub artists: Vec<SpotifyId>,
|
||||||
|
pub covers: Vec<FileId>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetadataTrait for Album {
|
||||||
|
type Message = protocol::metadata::Album;
|
||||||
|
fn from_msg(msg: &Self::Message) -> Self {
|
||||||
|
Album {
|
||||||
|
name: msg.get_name().to_string(),
|
||||||
|
artists: msg.get_artist().iter()
|
||||||
|
.map(|a| SpotifyId::from_raw(a.get_gid()))
|
||||||
|
.collect(),
|
||||||
|
covers: msg.get_cover_group().get_image().iter()
|
||||||
|
.map(|image| {
|
||||||
|
let mut dst = [0u8; 20];
|
||||||
|
copy_memory(&image.get_file_id(), &mut dst);
|
||||||
|
dst
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn base_url() -> &'static str {
|
||||||
|
"hm://metadata/3/album"
|
||||||
|
}
|
||||||
|
fn request(r: MetadataRef<Self>) -> MetadataRequest {
|
||||||
|
MetadataRequest::Album(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Artist {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetadataTrait for Artist {
|
||||||
|
type Message = protocol::metadata::Artist;
|
||||||
|
fn from_msg(msg: &Self::Message) -> Self {
|
||||||
|
Artist {
|
||||||
|
name: msg.get_name().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn base_url() -> &'static str {
|
||||||
|
"hm://metadata/3/artist"
|
||||||
|
}
|
||||||
|
fn request(r: MetadataRef<Self>) -> MetadataRequest {
|
||||||
|
MetadataRequest::Artist(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MetadataState<T> {
|
||||||
|
Loading,
|
||||||
|
Loaded(T),
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Metadata<T: MetadataTrait> {
|
||||||
|
id: SpotifyId,
|
||||||
|
state: Mutex<MetadataState<T>>,
|
||||||
|
cond: Condvar
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MetadataRef<T> = Arc<Metadata<T>>;
|
||||||
|
|
||||||
|
pub type TrackRef = MetadataRef<Track>;
|
||||||
|
pub type AlbumRef = MetadataRef<Album>;
|
||||||
|
pub type ArtistRef = MetadataRef<Artist>;
|
||||||
|
|
||||||
|
pub struct MetadataCache {
|
||||||
|
metadata: mpsc::Sender<MetadataRequest>,
|
||||||
|
cache: HashMap<(SpotifyId, TypeId), Box<Any + 'static>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetadataCache {
|
||||||
|
pub fn new(metadata: mpsc::Sender<MetadataRequest>) -> MetadataCache {
|
||||||
|
MetadataCache {
|
||||||
|
metadata: metadata,
|
||||||
|
cache: HashMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<T: MetadataTrait>(&mut self, id: SpotifyId)
|
||||||
|
-> MetadataRef<T> {
|
||||||
|
let key = (id, TypeId::of::<T>());
|
||||||
|
|
||||||
|
self.cache.get(&key)
|
||||||
|
.and_then(|x| x.downcast_ref::<Weak<Metadata<T>>>())
|
||||||
|
.and_then(|x| x.upgrade())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let x : MetadataRef<T> = Arc::new(Metadata{
|
||||||
|
id: id,
|
||||||
|
state: Mutex::new(MetadataState::Loading),
|
||||||
|
cond: Condvar::new()
|
||||||
|
});
|
||||||
|
|
||||||
|
self.cache.insert(key, Box::new(x.downgrade()));
|
||||||
|
self.metadata.send(T::request(x.clone())).unwrap();
|
||||||
|
x
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T: MetadataTrait> Metadata<T> {
|
||||||
|
pub fn id(&self) -> SpotifyId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lock(&self) -> MutexGuard<MetadataState<T>> {
|
||||||
|
self.state.lock().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait(&self) -> MutexGuard<MetadataState<T>> {
|
||||||
|
let mut handle = self.lock();
|
||||||
|
while handle.is_loading() {
|
||||||
|
handle = self.cond.wait(handle).unwrap();
|
||||||
|
}
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&self, state: MetadataState<T>) {
|
||||||
|
let mut handle = self.lock();
|
||||||
|
*handle = state;
|
||||||
|
self.cond.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T: MetadataTrait + fmt::Debug> fmt::Debug for Metadata<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
write!(f, "Metadata<>({:?}, {:?})", self.id, *self.lock())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T: MetadataTrait> MetadataState<T> {
|
||||||
|
pub fn is_loading(&self) -> bool {
|
||||||
|
match *self {
|
||||||
|
MetadataState::Loading => true,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_loaded(&self) -> bool {
|
||||||
|
match *self {
|
||||||
|
MetadataState::Loaded(_) => true,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unwrap<'s>(&'s self) -> &'s T {
|
||||||
|
match *self {
|
||||||
|
MetadataState::Loaded(ref data) => data,
|
||||||
|
_ => panic!("Not loaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MetadataRequest {
|
||||||
|
Artist(ArtistRef),
|
||||||
|
Album(AlbumRef),
|
||||||
|
Track(TrackRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetadataManager {
|
||||||
|
requests: mpsc::Receiver<MetadataRequest>,
|
||||||
|
mercury: mpsc::Sender<MercuryRequest>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetadataManager {
|
||||||
|
pub fn new(mercury: mpsc::Sender<MercuryRequest>) -> (MetadataManager,
|
||||||
|
mpsc::Sender<MetadataRequest>) {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
(MetadataManager {
|
||||||
|
requests: rx,
|
||||||
|
mercury: mercury
|
||||||
|
}, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load<T: MetadataTrait> (&self, object: MetadataRef<T>) {
|
||||||
|
let mercury = self.mercury.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
mercury.send(MercuryRequest {
|
||||||
|
method: MercuryMethod::GET,
|
||||||
|
url: format!("{}/{}", T::base_url(), object.id.to_base16()),
|
||||||
|
mime: None,
|
||||||
|
callback: Some(tx)
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
let response = rx.recv().unwrap();
|
||||||
|
|
||||||
|
let msg : T::Message = protobuf::parse_from_bytes(
|
||||||
|
response.payload.front().unwrap()).unwrap();
|
||||||
|
|
||||||
|
object.set(MetadataState::Loaded(T::from_msg(&msg)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Subsystem for MetadataManager {
|
||||||
|
fn run(self) {
|
||||||
|
for req in self.requests.iter() {
|
||||||
|
match req {
|
||||||
|
MetadataRequest::Artist(artist) => {
|
||||||
|
self.load(artist)
|
||||||
|
}
|
||||||
|
MetadataRequest::Album(album) => {
|
||||||
|
self.load(album)
|
||||||
|
}
|
||||||
|
MetadataRequest::Track(track) => {
|
||||||
|
self.load(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
73
src/player.rs
Normal file
73
src/player.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use portaudio;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use vorbis;
|
||||||
|
|
||||||
|
use audio_key::{AudioKeyRequest, AudioKeyResponse};
|
||||||
|
use metadata::TrackRef;
|
||||||
|
use session::Session;
|
||||||
|
use audio_file::{AudioFileRef, AudioFileReader};
|
||||||
|
use audio_decrypt::AudioDecrypt;
|
||||||
|
|
||||||
|
pub struct Player;
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub fn play(session: &Session, track: TrackRef) {
|
||||||
|
let file_id = *track.wait().unwrap().files.first().unwrap();
|
||||||
|
|
||||||
|
let key = {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
session.audio_key.send(AudioKeyRequest {
|
||||||
|
track: track.id(),
|
||||||
|
file: file_id,
|
||||||
|
callback: tx
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
let AudioKeyResponse(key) = rx.recv().unwrap();
|
||||||
|
key
|
||||||
|
};
|
||||||
|
|
||||||
|
let reader = {
|
||||||
|
let file = AudioFileRef::new(file_id, session.stream.clone());
|
||||||
|
let f = file.clone();
|
||||||
|
let s = session.stream.clone();
|
||||||
|
thread::spawn( move || { f.fetch(s) });
|
||||||
|
AudioDecrypt::new(key, AudioFileReader::new(&file))
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
portaudio::initialize().unwrap();
|
||||||
|
|
||||||
|
let stream = portaudio::stream::Stream::<i16>::open_default(
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
44100.0,
|
||||||
|
portaudio::stream::FRAMES_PER_BUFFER_UNSPECIFIED,
|
||||||
|
None
|
||||||
|
).unwrap();
|
||||||
|
stream.start().unwrap();
|
||||||
|
|
||||||
|
let mut decoder = vorbis::Decoder::new(reader).unwrap();
|
||||||
|
|
||||||
|
for pkt in decoder.packets() {
|
||||||
|
match pkt {
|
||||||
|
Ok(packet) => {
|
||||||
|
match stream.write(&packet.data) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(portaudio::PaError::OutputUnderflowed)
|
||||||
|
=> eprintln!("Underflow"),
|
||||||
|
Err(e) => panic!("PA Error {}", e)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
Err(vorbis::VorbisError::Hole) => (),
|
||||||
|
Err(e) => panic!("Vorbis error {:?}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(stream);
|
||||||
|
|
||||||
|
portaudio::terminate().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
101
src/session.rs
101
src/session.rs
|
@ -1,13 +1,20 @@
|
||||||
use connection::{PlainConnection, CipherConnection};
|
use crypto::digest::Digest;
|
||||||
|
use crypto::sha1::Sha1;
|
||||||
|
use protobuf::{self, Message};
|
||||||
|
use rand::thread_rng;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use audio_key;
|
||||||
|
use connection::{PlainConnection, Packet, PacketDispatch, SendThread, RecvThread};
|
||||||
use keys::PrivateKeys;
|
use keys::PrivateKeys;
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
use mercury;
|
||||||
|
use metadata;
|
||||||
|
use stream;
|
||||||
|
use subsystem::Subsystem;
|
||||||
use util;
|
use util;
|
||||||
|
|
||||||
use crypto::sha1::Sha1;
|
|
||||||
use crypto::digest::Digest;
|
|
||||||
use protobuf::*;
|
|
||||||
use rand::thread_rng;
|
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub application_key: Vec<u8>,
|
pub application_key: Vec<u8>,
|
||||||
pub user_agent: String,
|
pub user_agent: String,
|
||||||
|
@ -16,7 +23,14 @@ pub struct Config {
|
||||||
|
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
config: Config,
|
config: Config,
|
||||||
connection: CipherConnection,
|
|
||||||
|
packet_rx: mpsc::Receiver<Packet>,
|
||||||
|
pub packet_tx: mpsc::Sender<Packet>,
|
||||||
|
|
||||||
|
pub audio_key: mpsc::Sender<audio_key::AudioKeyRequest>,
|
||||||
|
pub mercury: mpsc::Sender<mercury::MercuryRequest>,
|
||||||
|
pub metadata: mpsc::Sender<metadata::MetadataRequest>,
|
||||||
|
pub stream: mpsc::Sender<stream::StreamRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
|
@ -27,7 +41,6 @@ impl Session {
|
||||||
h.result_str()
|
h.result_str()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let keys = PrivateKeys::new();
|
let keys = PrivateKeys::new();
|
||||||
let mut connection = PlainConnection::connect().unwrap();
|
let mut connection = PlainConnection::connect().unwrap();
|
||||||
|
|
||||||
|
@ -68,7 +81,7 @@ impl Session {
|
||||||
connection.recv_packet().unwrap();
|
connection.recv_packet().unwrap();
|
||||||
|
|
||||||
let response : protocol::keyexchange::APResponseMessage =
|
let response : protocol::keyexchange::APResponseMessage =
|
||||||
parse_from_bytes(&init_server_packet[4..]).unwrap();
|
protobuf::parse_from_bytes(&init_server_packet[4..]).unwrap();
|
||||||
|
|
||||||
protobuf_bind!(response, {
|
protobuf_bind!(response, {
|
||||||
challenge => {
|
challenge => {
|
||||||
|
@ -90,13 +103,47 @@ impl Session {
|
||||||
|
|
||||||
connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap();
|
connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap();
|
||||||
|
|
||||||
|
let cipher_connection = connection.setup_cipher(shared_keys);
|
||||||
|
|
||||||
|
let (send_thread, tx) = SendThread::new(cipher_connection.clone());
|
||||||
|
|
||||||
|
let (main_tx, rx) = mpsc::channel();
|
||||||
|
let (mercury, mercury_req, mercury_pkt)
|
||||||
|
= mercury::MercuryManager::new(tx.clone());
|
||||||
|
let (metadata, metadata_req)
|
||||||
|
= metadata::MetadataManager::new(mercury_req.clone());
|
||||||
|
let (stream, stream_req, stream_pkt)
|
||||||
|
= stream::StreamManager::new(tx.clone());
|
||||||
|
let (audio_key, audio_key_req, audio_key_pkt)
|
||||||
|
= audio_key::AudioKeyManager::new(tx.clone());
|
||||||
|
|
||||||
|
let recv_thread = RecvThread::new(cipher_connection, PacketDispatch {
|
||||||
|
main: main_tx,
|
||||||
|
stream: stream_pkt,
|
||||||
|
mercury: mercury_pkt,
|
||||||
|
audio_key: audio_key_pkt
|
||||||
|
});
|
||||||
|
|
||||||
|
thread::spawn(move || send_thread.run());
|
||||||
|
thread::spawn(move || recv_thread.run());
|
||||||
|
|
||||||
|
mercury.start();
|
||||||
|
metadata.start();
|
||||||
|
stream.start();
|
||||||
|
audio_key.start();
|
||||||
|
|
||||||
Session {
|
Session {
|
||||||
config: config,
|
config: config,
|
||||||
connection: connection.setup_cipher(shared_keys)
|
packet_rx: rx,
|
||||||
|
packet_tx: tx,
|
||||||
|
mercury: mercury_req,
|
||||||
|
metadata: metadata_req,
|
||||||
|
stream: stream_req,
|
||||||
|
audio_key: audio_key_req,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn login(&mut self, username: String, password: String) {
|
pub fn login(&self, username: String, password: String) {
|
||||||
let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), {
|
let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), {
|
||||||
login_credentials => {
|
login_credentials => {
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -119,14 +166,32 @@ impl Session {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.connection.send_encrypted_packet(
|
self.packet_tx.send(Packet {
|
||||||
0xab,
|
cmd: 0xab,
|
||||||
&packet.write_to_bytes().unwrap()).unwrap();
|
data: packet.write_to_bytes().unwrap()
|
||||||
|
}).unwrap();
|
||||||
loop {
|
|
||||||
let (cmd, data) = self.connection.recv_packet().unwrap();
|
|
||||||
println!("{:x}", cmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn poll(&self) {
|
||||||
|
let packet = self.packet_rx.recv().unwrap();
|
||||||
|
|
||||||
|
match packet.cmd {
|
||||||
|
0x4 => { // PING
|
||||||
|
self.packet_tx.send(Packet {
|
||||||
|
cmd: 0x49,
|
||||||
|
data: packet.data
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
0x4a => { // PONG
|
||||||
|
}
|
||||||
|
0xac => { // AUTHENTICATED
|
||||||
|
eprintln!("Authentication succeedded");
|
||||||
|
}
|
||||||
|
0xad => {
|
||||||
|
eprintln!("Authentication failed");
|
||||||
|
}
|
||||||
|
_ => ()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
159
src/stream.rs
Normal file
159
src/stream.rs
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Cursor, Seek, SeekFrom, Write};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use connection::Packet;
|
||||||
|
use util::{ArcVec, FileId};
|
||||||
|
use util::Either::{Left, Right};
|
||||||
|
use subsystem::Subsystem;
|
||||||
|
|
||||||
|
pub type StreamCallback = mpsc::Sender<StreamEvent>;
|
||||||
|
pub struct StreamRequest {
|
||||||
|
pub id: FileId,
|
||||||
|
pub offset: u32,
|
||||||
|
pub size: u32,
|
||||||
|
pub callback: StreamCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StreamEvent {
|
||||||
|
Header(u8, ArcVec<u8>),
|
||||||
|
Data(ArcVec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelId = u16;
|
||||||
|
|
||||||
|
enum ChannelMode {
|
||||||
|
Header,
|
||||||
|
Data
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Channel {
|
||||||
|
mode: ChannelMode,
|
||||||
|
callback: StreamCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StreamManager {
|
||||||
|
next_id: ChannelId,
|
||||||
|
channels: HashMap<ChannelId, Channel>,
|
||||||
|
|
||||||
|
requests: mpsc::Receiver<StreamRequest>,
|
||||||
|
packet_rx: mpsc::Receiver<Packet>,
|
||||||
|
packet_tx: mpsc::Sender<Packet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamManager {
|
||||||
|
pub fn new(tx: mpsc::Sender<Packet>) -> (StreamManager,
|
||||||
|
mpsc::Sender<StreamRequest>,
|
||||||
|
mpsc::Sender<Packet>) {
|
||||||
|
let (req_tx, req_rx) = mpsc::channel();
|
||||||
|
let (pkt_tx, pkt_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
(StreamManager {
|
||||||
|
next_id: 0,
|
||||||
|
channels: HashMap::new(),
|
||||||
|
|
||||||
|
requests: req_rx,
|
||||||
|
packet_rx: pkt_rx,
|
||||||
|
packet_tx: tx
|
||||||
|
}, req_tx, pkt_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(&mut self, req: StreamRequest) {
|
||||||
|
let channel_id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
|
||||||
|
let mut data : Vec<u8> = Vec::new();
|
||||||
|
data.write_u16::<BigEndian>(channel_id).unwrap();
|
||||||
|
data.write_u8(0).unwrap();
|
||||||
|
data.write_u8(1).unwrap();
|
||||||
|
data.write_u16::<BigEndian>(0x0000).unwrap();
|
||||||
|
data.write_u32::<BigEndian>(0x00000000).unwrap();
|
||||||
|
data.write_u32::<BigEndian>(0x00009C40).unwrap();
|
||||||
|
data.write_u32::<BigEndian>(0x00020000).unwrap();
|
||||||
|
data.write(&req.id).unwrap();
|
||||||
|
data.write_u32::<BigEndian>(req.offset).unwrap();
|
||||||
|
data.write_u32::<BigEndian>(req.offset + req.size).unwrap();
|
||||||
|
|
||||||
|
self.packet_tx.send(Packet {
|
||||||
|
cmd: 0x8,
|
||||||
|
data: data
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
self.channels.insert(channel_id, Channel {
|
||||||
|
mode: ChannelMode::Header,
|
||||||
|
callback: req.callback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packet(&mut self, data: Vec<u8>) {
|
||||||
|
let data = ArcVec::new(data);
|
||||||
|
let mut packet = Cursor::new(&data as &[u8]);
|
||||||
|
|
||||||
|
let id : ChannelId = packet.read_u16::<BigEndian>().unwrap();
|
||||||
|
let channel = match self.channels.get_mut(&id) {
|
||||||
|
Some(ch) => ch,
|
||||||
|
None => { return; }
|
||||||
|
};
|
||||||
|
|
||||||
|
match channel.mode {
|
||||||
|
ChannelMode::Header => {
|
||||||
|
let mut length = 0;
|
||||||
|
|
||||||
|
while packet.position() < data.len() as u64 {
|
||||||
|
length = packet.read_u16::<BigEndian>().unwrap();
|
||||||
|
if length > 0 {
|
||||||
|
let header_id = packet.read_u8().unwrap();
|
||||||
|
channel.callback.send(StreamEvent::Header(
|
||||||
|
header_id,
|
||||||
|
data.clone()
|
||||||
|
.offset(packet.position() as usize)
|
||||||
|
.limit(length as usize - 1)
|
||||||
|
)).unwrap();
|
||||||
|
|
||||||
|
packet.seek(SeekFrom::Current(length as i64 - 1)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
channel.mode = ChannelMode::Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelMode::Data => {
|
||||||
|
if packet.position() < data.len() as u64 {
|
||||||
|
channel.callback.send(StreamEvent::Data(
|
||||||
|
data.clone().offset(packet.position() as usize))).unwrap();
|
||||||
|
} else {
|
||||||
|
// TODO: close the channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Subsystem for StreamManager {
|
||||||
|
fn run(mut self) {
|
||||||
|
loop {
|
||||||
|
match {
|
||||||
|
let requests = &self.requests;
|
||||||
|
let packets = &self.packet_rx;
|
||||||
|
|
||||||
|
select!{
|
||||||
|
r = requests.recv() => {
|
||||||
|
Left(r.unwrap())
|
||||||
|
},
|
||||||
|
p = packets.recv() => {
|
||||||
|
Right(p.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
Left(req) => self.request(req),
|
||||||
|
Right(pkt) => self.packet(pkt.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
9
src/subsystem.rs
Normal file
9
src/subsystem.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
pub trait Subsystem : Send + Sized + 'static {
|
||||||
|
fn run(self);
|
||||||
|
fn start(self) {
|
||||||
|
thread::spawn(move || self.run());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
29
src/util.rs
29
src/util.rs
|
@ -1,29 +0,0 @@
|
||||||
use rand::{Rng,Rand};
|
|
||||||
|
|
||||||
pub fn rand_vec<G: Rng, R: Rand>(rng: &mut G, size: usize) -> Vec<R> {
|
|
||||||
let mut vec = Vec::with_capacity(size);
|
|
||||||
|
|
||||||
for _ in 0..size {
|
|
||||||
vec.push(R::rand(rng));
|
|
||||||
}
|
|
||||||
|
|
||||||
return vec
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alloc_buffer(size: usize) -> Vec<u8> {
|
|
||||||
let mut vec = Vec::with_capacity(size);
|
|
||||||
unsafe {
|
|
||||||
vec.set_len(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod version {
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/version.rs"));
|
|
||||||
|
|
||||||
pub fn version_string() -> String {
|
|
||||||
format!("librespot-{}", short_sha())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
52
src/util/arcvec.rs
Normal file
52
src/util/arcvec.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ArcVec<T> {
|
||||||
|
data: Arc<Vec<T>>,
|
||||||
|
offset: usize,
|
||||||
|
length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T> ArcVec<T> {
|
||||||
|
pub fn new(data: Vec<T>) -> ArcVec<T> {
|
||||||
|
let length = data.len();
|
||||||
|
ArcVec {
|
||||||
|
data: Arc::new(data),
|
||||||
|
offset: 0,
|
||||||
|
length: length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn offset(mut self, offset: usize) -> ArcVec<T> {
|
||||||
|
assert!(offset <= self.length);
|
||||||
|
|
||||||
|
self.offset += offset;
|
||||||
|
self.length -= offset;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn limit(mut self, length: usize) -> ArcVec<T> {
|
||||||
|
assert!(length <= self.length);
|
||||||
|
self.length = length;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for ArcVec<T> {
|
||||||
|
type Target = [T];
|
||||||
|
|
||||||
|
fn deref(&self) -> &[T] {
|
||||||
|
&self.data[self.offset..self.offset+self.length]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T : fmt::Debug> fmt::Debug for ArcVec<T> {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
self.deref().fmt(formatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
94
src/util/int128.rs
Normal file
94
src/util/int128.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use std;
|
||||||
|
|
||||||
|
#[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub struct u128 {
|
||||||
|
high: u64,
|
||||||
|
low: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl u128 {
|
||||||
|
pub fn from_parts(high: u64, low: u64) -> u128 {
|
||||||
|
u128 { high: high, low: low }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parts(&self) -> (u64, u64) {
|
||||||
|
(self.high, self.low)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::num::Zero for u128 {
|
||||||
|
fn zero() -> u128 {
|
||||||
|
u128::from_parts(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add<u128> for u128 {
|
||||||
|
type Output = u128;
|
||||||
|
fn add(self, rhs: u128) -> u128 {
|
||||||
|
let low = self.low + rhs.low;
|
||||||
|
let high = self.high + rhs.high +
|
||||||
|
if low < self.low { 1 } else { 0 };
|
||||||
|
|
||||||
|
u128::from_parts(high, low)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <'a> std::ops::Add<&'a u128> for u128 {
|
||||||
|
type Output = u128;
|
||||||
|
fn add(self, rhs: &'a u128) -> u128 {
|
||||||
|
let low = self.low + rhs.low;
|
||||||
|
let high = self.high + rhs.high +
|
||||||
|
if low < self.low { 1 } else { 0 };
|
||||||
|
|
||||||
|
u128::from_parts(high, low)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<u8> for u128 {
|
||||||
|
fn from(n: u8) -> u128 {
|
||||||
|
u128::from_parts(0, n as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl std::ops::Mul<u128> for u128 {
|
||||||
|
type Output = u128;
|
||||||
|
|
||||||
|
fn mul(self, rhs: u128) -> u128 {
|
||||||
|
let top: [u64; 4] =
|
||||||
|
[self.high >> 32, self.high & 0xFFFFFFFF,
|
||||||
|
self.low >> 32, self.low & 0xFFFFFFFF];
|
||||||
|
|
||||||
|
let bottom : [u64; 4] =
|
||||||
|
[rhs.high >> 32, rhs.high & 0xFFFFFFFF,
|
||||||
|
rhs.low >> 32, rhs.low & 0xFFFFFFFF];
|
||||||
|
|
||||||
|
let mut rows = [std::num::Zero::zero(); 16];
|
||||||
|
for i in 0..4 {
|
||||||
|
for j in 0..4 {
|
||||||
|
let shift = i + j;
|
||||||
|
let product = top[3-i] * bottom[3-j];
|
||||||
|
let (high, low) = match shift {
|
||||||
|
0 => (0, product),
|
||||||
|
1 => (product >> 32, product << 32),
|
||||||
|
2 => (product, 0),
|
||||||
|
3 => (product << 32, 0),
|
||||||
|
_ => {
|
||||||
|
if product != 0 {
|
||||||
|
panic!("Overflow on mul {:?} {:?} ({} {})",
|
||||||
|
self, rhs, i, j)
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rows[j * 4 + i] = u128::from_parts(high, low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.iter().sum::<u128>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
68
src/util/mod.rs
Normal file
68
src/util/mod.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use rand::{Rng,Rand};
|
||||||
|
|
||||||
|
mod int128;
|
||||||
|
mod spotify_id;
|
||||||
|
mod arcvec;
|
||||||
|
|
||||||
|
pub use util::int128::u128;
|
||||||
|
pub use util::spotify_id::{SpotifyId, FileId};
|
||||||
|
pub use util::arcvec::ArcVec;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! eprintln(
|
||||||
|
($($arg:tt)*) => (
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
writeln!(&mut ::std::io::stderr(), $($arg)* ).unwrap()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! eprint(
|
||||||
|
($($arg:tt)*) => (
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
write!(&mut ::std::io::stderr(), $($arg)* ).unwrap()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn rand_vec<G: Rng, R: Rand>(rng: &mut G, size: usize) -> Vec<R> {
|
||||||
|
let mut vec = Vec::with_capacity(size);
|
||||||
|
|
||||||
|
for _ in 0..size {
|
||||||
|
vec.push(R::rand(rng));
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alloc_buffer(size: usize) -> Vec<u8> {
|
||||||
|
let mut vec = Vec::with_capacity(size);
|
||||||
|
unsafe {
|
||||||
|
vec.set_len(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod version {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/version.rs"));
|
||||||
|
|
||||||
|
pub fn version_string() -> String {
|
||||||
|
format!("librespot-{}", short_sha())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Either<S,T> {
|
||||||
|
Left(S),
|
||||||
|
Right(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hexdump(data: &[u8]) {
|
||||||
|
for b in data.iter() {
|
||||||
|
eprint!("{:02X} ", b);
|
||||||
|
}
|
||||||
|
eprintln!("");
|
||||||
|
}
|
||||||
|
|
79
src/util/spotify_id.rs
Normal file
79
src/util/spotify_id.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use std;
|
||||||
|
use util::u128;
|
||||||
|
use byteorder::{BigEndian,ByteOrder};
|
||||||
|
use std::ascii::AsciiExt;
|
||||||
|
|
||||||
|
pub type FileId = [u8; 20];
|
||||||
|
|
||||||
|
#[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)]
|
||||||
|
pub struct SpotifyId(u128);
|
||||||
|
|
||||||
|
const BASE62_DIGITS: &'static [u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef";
|
||||||
|
|
||||||
|
impl SpotifyId {
|
||||||
|
pub fn from_base16(id: &str) -> SpotifyId {
|
||||||
|
assert!(id.is_ascii());
|
||||||
|
let data = id.as_bytes();
|
||||||
|
|
||||||
|
let mut n : u128 = std::num::Zero::zero();
|
||||||
|
for c in data {
|
||||||
|
let d = BASE16_DIGITS.position_elem(c).unwrap() as u8;
|
||||||
|
n = n * u128::from(16);
|
||||||
|
n = n + u128::from(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotifyId(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_base62(id: &str) -> SpotifyId {
|
||||||
|
assert!(id.is_ascii());
|
||||||
|
let data = id.as_bytes();
|
||||||
|
|
||||||
|
let mut n : u128 = std::num::Zero::zero();
|
||||||
|
for c in data {
|
||||||
|
let d = BASE62_DIGITS.position_elem(c).unwrap() as u8;
|
||||||
|
n = n * u128::from(62);
|
||||||
|
n = n + u128::from(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotifyId(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_raw(data: &[u8]) -> SpotifyId {
|
||||||
|
assert_eq!(data.len(), 16);
|
||||||
|
|
||||||
|
let high = BigEndian::read_u64(&data[0..8]);
|
||||||
|
let low = BigEndian::read_u64(&data[8..16]);
|
||||||
|
|
||||||
|
SpotifyId(u128::from_parts(high, low))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_base16(&self) -> String {
|
||||||
|
let &SpotifyId(ref n) = self;
|
||||||
|
let (high, low) = n.parts();
|
||||||
|
|
||||||
|
let mut data = [0u8; 32];
|
||||||
|
for i in 0..16 {
|
||||||
|
data[31-i] = BASE16_DIGITS[(low.wrapping_shr(4 * i as u32) & 0xF) as usize];
|
||||||
|
}
|
||||||
|
for i in 0..16 {
|
||||||
|
data[15-i] = BASE16_DIGITS[(high.wrapping_shr(4 * i as u32) & 0xF) as usize];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::str::from_utf8(&data).unwrap().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_raw(&self) -> [u8; 16] {
|
||||||
|
let &SpotifyId(ref n) = self;
|
||||||
|
let (high, low) = n.parts();
|
||||||
|
|
||||||
|
let mut data = [0u8; 16];
|
||||||
|
|
||||||
|
BigEndian::write_u64(&mut data[0..8], high);
|
||||||
|
BigEndian::write_u64(&mut data[8..16], low);
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue