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"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"byteorder 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.1.10 (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.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"librespot-protocol 0.1.0",
|
||||
"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)",
|
||||
"protobuf 0.0.10 (git+https://github.com/stepancheg/rust-protobuf.git)",
|
||||
"num 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"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)",
|
||||
"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)",
|
||||
|
@ -15,6 +16,7 @@ dependencies = [
|
|||
"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)",
|
||||
"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]]
|
||||
|
@ -22,24 +24,29 @@ name = "bitflags"
|
|||
version = "0.1.1"
|
||||
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]]
|
||||
name = "byteorder"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "gcc"
|
||||
version = "0.3.5"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "0.1.10"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
|
@ -47,7 +54,7 @@ name = "librespot-protocol"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"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]]
|
||||
|
@ -57,29 +64,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"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]]
|
||||
name = "protobuf"
|
||||
version = "0.0.10"
|
||||
source = "git+https://github.com/stepancheg/rust-protobuf.git#41fde39aed305e0fb71ef6a8d92b35ee50550bde"
|
||||
version = "1.0.0"
|
||||
source = "git+https://github.com/stepancheg/rust-protobuf.git#d6e80593f38ce47dfa0c4912a3558fa33ee06143"
|
||||
|
||||
[[package]]
|
||||
name = "protobuf_macros"
|
||||
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]]
|
||||
name = "rand"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
|
@ -92,46 +133,51 @@ name = "rust-crypto"
|
|||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.1.7 (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.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)",
|
||||
"time 0.1.25 (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.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-gmp"
|
||||
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]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "shannon"
|
||||
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 = [
|
||||
"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)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shannon-sys"
|
||||
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 = [
|
||||
"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]]
|
||||
name = "time"
|
||||
version = "0.1.25"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.1.7 (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.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -139,7 +185,41 @@ name = "vergen"
|
|||
version = "0.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.25 (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.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]
|
||||
git = "https://github.com/stepancheg/rust-protobuf.git"
|
||||
|
||||
[dependencies.protobuf_macros]
|
||||
git = "https://github.com/plietar/rust-protobuf-macros.git"
|
||||
|
||||
[dependencies.rust-gmp]
|
||||
git = "https://github.com/plietar/rust-gmp.git"
|
||||
|
||||
[dependencies.shannon]
|
||||
git = "https://github.com/plietar/rust-shannon.git"
|
||||
|
||||
[dependencies.readall]
|
||||
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]
|
||||
vergen = "*"
|
||||
|
|
|
@ -37,9 +37,18 @@ fn compile(prefix : &Path, files : &[&Path]) -> Result<(),ProtobufError>{
|
|||
fn main() {
|
||||
let root = PathBuf::from(&env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let proto = root.join("proto");
|
||||
|
||||
compile(&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();
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ message Album {
|
|||
optional bytes gid = 1;
|
||||
optional string name = 2;
|
||||
repeated Artist artist = 3;
|
||||
optional Type type = 4;
|
||||
optional Type typ = 4;
|
||||
optional string label = 5;
|
||||
optional Date date = 6;
|
||||
optional sint32 popularity = 7;
|
||||
|
@ -106,7 +106,7 @@ message Copyright {
|
|||
P = 0;
|
||||
C = 1;
|
||||
}
|
||||
optional Type type = 1;
|
||||
optional Type typ = 1;
|
||||
optional string text = 2;
|
||||
}
|
||||
message Restriction {
|
||||
|
@ -122,7 +122,7 @@ message Restriction {
|
|||
repeated Catalogue catalogue = 1;
|
||||
optional string countries_allowed = 2;
|
||||
optional string countries_forbidden = 3;
|
||||
optional Type type = 4;
|
||||
optional Type typ = 4;
|
||||
repeated string usage = 5;
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,7 @@ message SalePeriod {
|
|||
}
|
||||
|
||||
message ExternalId {
|
||||
optional string type = 1;
|
||||
optional string typ = 1;
|
||||
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! 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, ReadBytesExt, WriteBytesExt, BigEndian, ByteOrder};
|
||||
use keys::SharedKeys;
|
||||
use byteorder::{self, BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
|
||||
use readall::ReadAllExt;
|
||||
use shannon::ShannonStream;
|
||||
use std::convert;
|
||||
|
@ -9,6 +6,10 @@ use std::io;
|
|||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::result;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use keys::SharedKeys;
|
||||
use util;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
|
@ -37,6 +38,7 @@ pub struct PlainConnection {
|
|||
stream: TcpStream
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CipherConnection {
|
||||
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::mac::Mac;
|
||||
use gmp::Mpz;
|
||||
use num::FromPrimitive;
|
||||
use rand;
|
||||
use std::io::Write;
|
||||
|
||||
use util;
|
||||
|
@ -29,7 +29,7 @@ pub struct PrivateKeys {
|
|||
}
|
||||
|
||||
pub struct SharedKeys {
|
||||
private: PrivateKeys,
|
||||
//private: PrivateKeys,
|
||||
challenge: Vec<u8>,
|
||||
send_key: Vec<u8>,
|
||||
recv_key: Vec<u8>
|
||||
|
@ -51,9 +51,11 @@ impl PrivateKeys {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn private_key(&self) -> Vec<u8> {
|
||||
return self.private_key.to_bytes_be();
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn public_key(&self) -> Vec<u8> {
|
||||
return self.public_key.to_bytes_be();
|
||||
|
@ -78,7 +80,7 @@ impl PrivateKeys {
|
|||
mac.input(server_packet);
|
||||
|
||||
SharedKeys {
|
||||
private: self,
|
||||
//private: self,
|
||||
challenge: mac.result().code().to_vec(),
|
||||
send_key: data[0x14..0x34].to_vec(),
|
||||
recv_key: data[0x34..0x54].to_vec(),
|
||||
|
|
59
src/main.rs
59
src/main.rs
|
@ -1,6 +1,6 @@
|
|||
#![crate_name = "librespot"]
|
||||
|
||||
#![feature(plugin)]
|
||||
#![feature(alloc,plugin,core,collections,std_misc,zero_one)]
|
||||
|
||||
#![plugin(protobuf_macros)]
|
||||
#[macro_use] extern crate lazy_static;
|
||||
|
@ -9,29 +9,45 @@ extern crate byteorder;
|
|||
extern crate crypto;
|
||||
extern crate gmp;
|
||||
extern crate num;
|
||||
extern crate portaudio;
|
||||
extern crate protobuf;
|
||||
extern crate shannon;
|
||||
extern crate rand;
|
||||
extern crate readall;
|
||||
extern crate vorbis;
|
||||
|
||||
extern crate librespot_protocol;
|
||||
|
||||
#[macro_use] mod util;
|
||||
mod audio_decrypt;
|
||||
mod audio_file;
|
||||
mod audio_key;
|
||||
mod connection;
|
||||
mod keys;
|
||||
mod mercury;
|
||||
mod metadata;
|
||||
mod player;
|
||||
mod session;
|
||||
mod util;
|
||||
mod stream;
|
||||
mod subsystem;
|
||||
|
||||
use std::clone::Clone;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::io::{Read, Write};
|
||||
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() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let mut appkey_file = File::open(Path::new(&args.next().unwrap())).unwrap();
|
||||
let username = 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();
|
||||
appkey_file.read_to_end(&mut appkey).unwrap();
|
||||
|
@ -41,8 +57,39 @@ fn main() {
|
|||
user_agent: "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 librespot_protocol as protocol;
|
||||
use mercury;
|
||||
use metadata;
|
||||
use stream;
|
||||
use subsystem::Subsystem;
|
||||
use util;
|
||||
|
||||
use crypto::sha1::Sha1;
|
||||
use crypto::digest::Digest;
|
||||
use protobuf::*;
|
||||
use rand::thread_rng;
|
||||
|
||||
pub struct Config {
|
||||
pub application_key: Vec<u8>,
|
||||
pub user_agent: String,
|
||||
|
@ -16,7 +23,14 @@ pub struct Config {
|
|||
|
||||
pub struct Session {
|
||||
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 {
|
||||
|
@ -27,7 +41,6 @@ impl Session {
|
|||
h.result_str()
|
||||
};
|
||||
|
||||
|
||||
let keys = PrivateKeys::new();
|
||||
let mut connection = PlainConnection::connect().unwrap();
|
||||
|
||||
|
@ -68,7 +81,7 @@ impl Session {
|
|||
connection.recv_packet().unwrap();
|
||||
|
||||
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, {
|
||||
challenge => {
|
||||
|
@ -90,13 +103,47 @@ impl Session {
|
|||
|
||||
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 {
|
||||
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(), {
|
||||
login_credentials => {
|
||||
username: username,
|
||||
|
@ -119,14 +166,32 @@ impl Session {
|
|||
}
|
||||
});
|
||||
|
||||
self.connection.send_encrypted_packet(
|
||||
0xab,
|
||||
&packet.write_to_bytes().unwrap()).unwrap();
|
||||
self.packet_tx.send(Packet {
|
||||
cmd: 0xab,
|
||||
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