diff --git a/Cargo.lock b/Cargo.lock index 274a7e6c..2693ccb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", ] diff --git a/Cargo.toml b/Cargo.toml index 2cedf810..da9182c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "*" diff --git a/protocol/build.rs b/protocol/build.rs index 73cc3f83..c205efcc 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -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(); } diff --git a/protocol/proto/metadata.proto b/protocol/proto/metadata.proto index fc27aede..16ff07c3 100644 --- a/protocol/proto/metadata.proto +++ b/protocol/proto/metadata.proto @@ -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; } diff --git a/protocol/proto/playlist4changes.proto b/protocol/proto/playlist4changes.proto new file mode 100644 index 00000000..c9764bfe --- /dev/null +++ b/protocol/proto/playlist4changes.proto @@ -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; +} diff --git a/protocol/proto/playlist4content.proto b/protocol/proto/playlist4content.proto new file mode 100644 index 00000000..e5d7bc47 --- /dev/null +++ b/protocol/proto/playlist4content.proto @@ -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; +} diff --git a/protocol/proto/playlist4issues.proto b/protocol/proto/playlist4issues.proto new file mode 100644 index 00000000..2fa21993 --- /dev/null +++ b/protocol/proto/playlist4issues.proto @@ -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; +} diff --git a/protocol/proto/playlist4meta.proto b/protocol/proto/playlist4meta.proto new file mode 100644 index 00000000..d6013b37 --- /dev/null +++ b/protocol/proto/playlist4meta.proto @@ -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; +} diff --git a/protocol/proto/playlist4ops.proto b/protocol/proto/playlist4ops.proto new file mode 100644 index 00000000..cd65a648 --- /dev/null +++ b/protocol/proto/playlist4ops.proto @@ -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; +} diff --git a/protocol/proto/playlist4service.proto b/protocol/proto/playlist4service.proto new file mode 100644 index 00000000..2f038f83 --- /dev/null +++ b/protocol/proto/playlist4service.proto @@ -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; +} diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 7bfdc7f7..bfb8406e 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -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")); diff --git a/src/audio_decrypt.rs b/src/audio_decrypt.rs new file mode 100644 index 00000000..dd938c99 --- /dev/null +++ b/src/audio_decrypt.rs @@ -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 { + cipher: Box, + key: AudioKey, + reader: T, +} + +impl AudioDecrypt { + pub fn new(key: AudioKey, mut reader: T) -> AudioDecrypt { + 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 io::Read for AudioDecrypt { + fn read(&mut self, output: &mut [u8]) -> io::Result { + 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 io::Seek for AudioDecrypt { + fn seek(&mut self, _pos: io::SeekFrom) -> io::Result { + Err(io::Error::new(io::ErrorKind::Other, "Cannot seek")) + } +} + + diff --git a/src/audio_file.rs b/src/audio_file.rs new file mode 100644 index 00000000..9c3a56be --- /dev/null +++ b/src/audio_file.rs @@ -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); + +struct AudioFile { + file: FileId, + size: usize, + + data: Mutex, + cond: Condvar +} + +struct AudioFileData { + buffer: Vec, + bitmap: BitSet, +} + +impl AudioFileRef { + pub fn new(file: FileId, streams: mpsc::Sender) -> 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) { + 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 { + 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 { + Err(io::Error::new(io::ErrorKind::Other, "Cannot seek")) + } +} + + diff --git a/src/audio_key.rs b/src/audio_key.rs new file mode 100644 index 00000000..10e02496 --- /dev/null +++ b/src/audio_key.rs @@ -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; + +type AudioKeyId = u32; +pub struct AudioKeyManager { + next_seq: AudioKeyId, + callbacks: HashMap, + + requests: mpsc::Receiver, + packet_rx: mpsc::Receiver, + packet_tx: mpsc::Sender, +} + +impl AudioKeyManager { + pub fn new(tx: mpsc::Sender) -> (AudioKeyManager, + mpsc::Sender, + mpsc::Sender) { + 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 = Vec::new(); + data.write(&req.file).unwrap(); + data.write(&req.track.to_raw()).unwrap(); + data.write_u32::(seq).unwrap(); + data.write_u16::(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::().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); + } + } + + } + } +} + diff --git a/src/connection.rs b/src/connection.rs index 06686cfd..ea5670c2 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -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, } @@ -106,5 +108,70 @@ impl CipherConnection { } } +pub struct Packet { + pub cmd: u8, + pub data: Vec +} +pub struct SendThread { + connection: CipherConnection, + receiver: mpsc::Receiver, +} +impl SendThread { + pub fn new(connection: CipherConnection) + -> (SendThread, mpsc::Sender) { + 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, + pub stream: mpsc::Sender, + pub mercury: mpsc::Sender, + pub audio_key: mpsc::Sender, +} + +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(); + + } + } +} diff --git a/src/keys.rs b/src/keys.rs index 3fb0a13d..8cfbdc64 100644 --- a/src/keys.rs +++ b/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, send_key: Vec, recv_key: Vec @@ -51,9 +51,11 @@ impl PrivateKeys { } } + /* pub fn private_key(&self) -> Vec { return self.private_key.to_bytes_be(); } + */ pub fn public_key(&self) -> Vec { 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 } diff --git a/src/main.rs b/src/main.rs index 97b5ae76..53e2e95c 100644 --- a/src/main.rs +++ b/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 = { + 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(); + } } diff --git a/src/mercury.rs b/src/mercury.rs new file mode 100644 index 00000000..8846f05c --- /dev/null +++ b/src/mercury.rs @@ -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, + pub callback: MercuryCallback +} + +#[derive(Debug)] +pub struct MercuryResponse { + pub url: String, + pub payload: LinkedList> +} + +pub type MercuryCallback = Option>; + +pub struct MercuryPending { + parts: LinkedList>, + partial: Option>, + callback: MercuryCallback, +} + +pub struct MercuryManager { + next_seq: u32, + pending: HashMap, MercuryPending>, + + requests: mpsc::Receiver, + packet_tx: mpsc::Sender, + packet_rx: mpsc::Receiver, +} + +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) -> (MercuryManager, + mpsc::Sender, + mpsc::Sender) { + 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 { + let size = s.read_u16::().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) { + let mut packet = Cursor::new(data); + + let seq = { + let seq_length = packet.read_u16::().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::().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 { + let mut packet = Vec::new(); + packet.write_u16::(seq.len() as u16).unwrap(); + packet.write_all(seq).unwrap(); + packet.write_u8(1).unwrap(); // Flags: FINAL + packet.write_u16::(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::(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); + } + } + + } + } +} + diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 00000000..7338d1df --- /dev/null +++ b/src/metadata.rs @@ -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) -> MetadataRequest; +} + +#[derive(Debug)] +pub struct Track { + pub name: String, + pub album: SpotifyId, + pub files: Vec +} + +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) -> MetadataRequest { + MetadataRequest::Track(r) + } +} + +#[derive(Debug)] +pub struct Album { + pub name: String, + pub artists: Vec, + pub covers: Vec +} + +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) -> 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) -> MetadataRequest { + MetadataRequest::Artist(r) + } +} + +#[derive(Debug)] +pub enum MetadataState { + Loading, + Loaded(T), + Error, +} + +pub struct Metadata { + id: SpotifyId, + state: Mutex>, + cond: Condvar +} + +pub type MetadataRef = Arc>; + +pub type TrackRef = MetadataRef; +pub type AlbumRef = MetadataRef; +pub type ArtistRef = MetadataRef; + +pub struct MetadataCache { + metadata: mpsc::Sender, + cache: HashMap<(SpotifyId, TypeId), Box> +} + +impl MetadataCache { + pub fn new(metadata: mpsc::Sender) -> MetadataCache { + MetadataCache { + metadata: metadata, + cache: HashMap::new() + } + } + + pub fn get(&mut self, id: SpotifyId) + -> MetadataRef { + let key = (id, TypeId::of::()); + + self.cache.get(&key) + .and_then(|x| x.downcast_ref::>>()) + .and_then(|x| x.upgrade()) + .unwrap_or_else(|| { + let x : MetadataRef = 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 Metadata { + pub fn id(&self) -> SpotifyId { + self.id + } + + pub fn lock(&self) -> MutexGuard> { + self.state.lock().unwrap() + } + + pub fn wait(&self) -> MutexGuard> { + let mut handle = self.lock(); + while handle.is_loading() { + handle = self.cond.wait(handle).unwrap(); + } + handle + } + + pub fn set(&self, state: MetadataState) { + let mut handle = self.lock(); + *handle = state; + self.cond.notify_all(); + } +} + +impl fmt::Debug for Metadata { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "Metadata<>({:?}, {:?})", self.id, *self.lock()) + } +} + +impl MetadataState { + 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, + mercury: mpsc::Sender +} + +impl MetadataManager { + pub fn new(mercury: mpsc::Sender) -> (MetadataManager, + mpsc::Sender) { + let (tx, rx) = mpsc::channel(); + (MetadataManager { + requests: rx, + mercury: mercury + }, tx) + } + + fn load (&self, object: MetadataRef) { + 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) + } + } + } + } +} + diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 00000000..1a27e31a --- /dev/null +++ b/src/player.rs @@ -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::::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(); + } +} + diff --git a/src/session.rs b/src/session.rs index 28bd5746..434af149 100644 --- a/src/session.rs +++ b/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, pub user_agent: String, @@ -16,7 +23,14 @@ pub struct Config { pub struct Session { config: Config, - connection: CipherConnection, + + packet_rx: mpsc::Receiver, + pub packet_tx: mpsc::Sender, + + pub audio_key: mpsc::Sender, + pub mercury: mpsc::Sender, + pub metadata: mpsc::Sender, + pub stream: mpsc::Sender, } 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"); + } + _ => () + }; } } diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 00000000..6398e72a --- /dev/null +++ b/src/stream.rs @@ -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; +pub struct StreamRequest { + pub id: FileId, + pub offset: u32, + pub size: u32, + pub callback: StreamCallback +} + +#[derive(Debug)] +pub enum StreamEvent { + Header(u8, ArcVec), + Data(ArcVec), +} + +type ChannelId = u16; + +enum ChannelMode { + Header, + Data +} + +struct Channel { + mode: ChannelMode, + callback: StreamCallback +} + +pub struct StreamManager { + next_id: ChannelId, + channels: HashMap, + + requests: mpsc::Receiver, + packet_rx: mpsc::Receiver, + packet_tx: mpsc::Sender, +} + +impl StreamManager { + pub fn new(tx: mpsc::Sender) -> (StreamManager, + mpsc::Sender, + mpsc::Sender) { + 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 = Vec::new(); + data.write_u16::(channel_id).unwrap(); + data.write_u8(0).unwrap(); + data.write_u8(1).unwrap(); + data.write_u16::(0x0000).unwrap(); + data.write_u32::(0x00000000).unwrap(); + data.write_u32::(0x00009C40).unwrap(); + data.write_u32::(0x00020000).unwrap(); + data.write(&req.id).unwrap(); + data.write_u32::(req.offset).unwrap(); + data.write_u32::(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) { + let data = ArcVec::new(data); + let mut packet = Cursor::new(&data as &[u8]); + + let id : ChannelId = packet.read_u16::().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::().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) + } + } + } +} + + diff --git a/src/subsystem.rs b/src/subsystem.rs new file mode 100644 index 00000000..896be425 --- /dev/null +++ b/src/subsystem.rs @@ -0,0 +1,9 @@ +use std::thread; + +pub trait Subsystem : Send + Sized + 'static { + fn run(self); + fn start(self) { + thread::spawn(move || self.run()); + } +} + diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 379b77b7..00000000 --- a/src/util.rs +++ /dev/null @@ -1,29 +0,0 @@ -use rand::{Rng,Rand}; - -pub fn rand_vec(rng: &mut G, size: usize) -> Vec { - 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 { - 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()) - } -} - diff --git a/src/util/arcvec.rs b/src/util/arcvec.rs new file mode 100644 index 00000000..a8b479a7 --- /dev/null +++ b/src/util/arcvec.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; +use std::fmt; +use std::ops::Deref; + +#[derive(Clone)] +pub struct ArcVec { + data: Arc>, + offset: usize, + length: usize, +} + +impl ArcVec { + pub fn new(data: Vec) -> ArcVec { + let length = data.len(); + ArcVec { + data: Arc::new(data), + offset: 0, + length: length + } + } + + pub fn offset(mut self, offset: usize) -> ArcVec { + assert!(offset <= self.length); + + self.offset += offset; + self.length -= offset; + + self + } + + pub fn limit(mut self, length: usize) -> ArcVec { + assert!(length <= self.length); + self.length = length; + + self + } +} + +impl Deref for ArcVec { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.data[self.offset..self.offset+self.length] + } +} + +impl fmt::Debug for ArcVec { + fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> { + self.deref().fmt(formatter) + } +} + diff --git a/src/util/int128.rs b/src/util/int128.rs new file mode 100644 index 00000000..ab881ee0 --- /dev/null +++ b/src/util/int128.rs @@ -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 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 for u128 { + fn from(n: u8) -> u128 { + u128::from_parts(0, n as u64) + } +} + + +impl std::ops::Mul 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::() + } +} + + diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 00000000..ff3d9b6d --- /dev/null +++ b/src/util/mod.rs @@ -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(rng: &mut G, size: usize) -> Vec { + 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 { + 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 { + Left(S), + Right(T) +} + +pub fn hexdump(data: &[u8]) { + for b in data.iter() { + eprint!("{:02X} ", b); + } + eprintln!(""); +} + diff --git a/src/util/spotify_id.rs b/src/util/spotify_id.rs new file mode 100644 index 00000000..9a84f511 --- /dev/null +++ b/src/util/spotify_id.rs @@ -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 + } +} +