Lots of stuff

This commit is contained in:
Paul Lietar 2015-06-23 15:38:29 +01:00
parent 1ad62e6f18
commit 7ffe996652
28 changed files with 2076 additions and 101 deletions

138
Cargo.lock generated
View file

@ -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)",
]

View file

@ -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 = "*"

View file

@ -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();
}

View file

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

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View file

@ -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
View 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
View 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
View 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);
}
}
}
}
}

View file

@ -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();
}
}
}

View file

@ -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(),
@ -94,7 +96,7 @@ impl SharedKeys {
pub fn send_key(&self) -> &[u8] {
&self.send_key
}
pub fn recv_key(&self) -> &[u8] {
&self.recv_key
}

View file

@ -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
View 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
View 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
View 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();
}
}

View file

@ -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
View 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
View 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());
}
}

View file

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