mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2026-05-16 02:05:30 +01:00
applied platformio structure
This commit is contained in:
109
lib/spotify/cspot/src/AccessKeyFetcher.cpp
Normal file
109
lib/spotify/cspot/src/AccessKeyFetcher.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "AccessKeyFetcher.h"
|
||||
|
||||
#include <cstring> // for strrchr
|
||||
#include <initializer_list> // for initializer_list
|
||||
#include <map> // for operator!=, operator==
|
||||
#include <type_traits> // for remove_extent_t
|
||||
#include <vector> // for vector
|
||||
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "BellUtils.h" // for BELL_SLEEP_MS
|
||||
#include "CSpotContext.h" // for Context
|
||||
#include "HTTPClient.h"
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "MercurySession.h" // for MercurySession, MercurySession::Res...
|
||||
#include "NanoPBExtensions.h" // for bell::nanopb::encode...
|
||||
#include "NanoPBHelper.h" // for pbEncode and pbDecode
|
||||
#include "Packet.h" // for cspot
|
||||
#include "TimeProvider.h" // for TimeProvider
|
||||
#include "Utils.h" // for string_format
|
||||
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
#include "cJSON.h"
|
||||
#else
|
||||
#include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
|
||||
#include "nlohmann/json_fwd.hpp" // for json
|
||||
#endif
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
static std::string SCOPES =
|
||||
"streaming,user-library-read,user-library-modify,user-top-read,user-read-"
|
||||
"recently-played"; // Required access scopes
|
||||
|
||||
AccessKeyFetcher::AccessKeyFetcher(std::shared_ptr<cspot::Context> ctx)
|
||||
: ctx(ctx) {}
|
||||
|
||||
bool AccessKeyFetcher::isExpired() {
|
||||
if (accessKey.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ctx->timeProvider->getSyncedTimestamp() > expiresAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string AccessKeyFetcher::getAccessKey() {
|
||||
if (!isExpired()) {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
updateAccessKey();
|
||||
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
void AccessKeyFetcher::updateAccessKey() {
|
||||
if (keyPending) {
|
||||
// Already pending refresh request
|
||||
return;
|
||||
}
|
||||
|
||||
keyPending = true;
|
||||
|
||||
// Max retry of 3, can receive different hash cat types
|
||||
int retryCount = 3;
|
||||
bool success = false;
|
||||
|
||||
do {
|
||||
CSPOT_LOG(info, "Access token expired, fetching new one...");
|
||||
|
||||
auto credentials = "grant_type=client_credentials&client_id=" + ctx->config.clientId + "&client_secret=" + ctx->config.clientSecret;
|
||||
std::vector<uint8_t> body(credentials.begin(), credentials.end());
|
||||
|
||||
auto response = bell::HTTPClient::post(
|
||||
"https://accounts.spotify.com/api/token",
|
||||
{ {"Content-Type", "application/x-www-form-urlencoded"} }, body);
|
||||
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* root = cJSON_Parse(response->body().data());
|
||||
if (!cJSON_GetObjectItem(root, "error")) {
|
||||
accessKey = std::string(cJSON_GetObjectItem(root, "access_token")->valuestring);
|
||||
int expiresIn = cJSON_GetObjectItem(root, "expires_in")->valueint;
|
||||
cJSON_Delete(root);
|
||||
#else
|
||||
auto root = nlohmann::json::parse(response->bytes());
|
||||
if (!root.contains("error")) {
|
||||
accessKey = std::string(root["access_token"]);
|
||||
int expiresIn = root["expires_in"];
|
||||
#endif
|
||||
// Successfully received an auth token
|
||||
CSPOT_LOG(info, "Access token sucessfully fetched");
|
||||
success = true;
|
||||
|
||||
this->expiresAt =
|
||||
ctx->timeProvider->getSyncedTimestamp() + (expiresIn * 1000);
|
||||
}
|
||||
else {
|
||||
CSPOT_LOG(error, "Failed to fetch access token");
|
||||
BELL_SLEEP_MS(3000);
|
||||
}
|
||||
|
||||
retryCount--;
|
||||
} while (retryCount >= 0 && !success);
|
||||
|
||||
keyPending = false;
|
||||
}
|
||||
42
lib/spotify/cspot/src/ApResolve.cpp
Normal file
42
lib/spotify/cspot/src/ApResolve.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "ApResolve.h"
|
||||
|
||||
#include <initializer_list> // for initializer_list
|
||||
#include <map> // for operator!=, operator==
|
||||
#include <memory> // for allocator, unique_ptr
|
||||
#include <string_view> // for string_view
|
||||
#include <vector> // for vector
|
||||
|
||||
#include "HTTPClient.h" // for HTTPClient, HTTPClient::Response
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
#include "cJSON.h"
|
||||
#else
|
||||
#include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
|
||||
#include "nlohmann/json_fwd.hpp" // for json
|
||||
#endif
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
ApResolve::ApResolve(std::string apOverride) {
|
||||
this->apOverride = apOverride;
|
||||
}
|
||||
|
||||
std::string ApResolve::fetchFirstApAddress() {
|
||||
if (apOverride != "") {
|
||||
return apOverride;
|
||||
}
|
||||
|
||||
auto request = bell::HTTPClient::get("https://apresolve.spotify.com/");
|
||||
std::string_view responseStr = request->body();
|
||||
|
||||
// parse json with nlohmann
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* json = cJSON_Parse(responseStr.data());
|
||||
auto ap_string = std::string(
|
||||
cJSON_GetArrayItem(cJSON_GetObjectItem(json, "ap_list"), 0)->valuestring);
|
||||
cJSON_Delete(json);
|
||||
return ap_string;
|
||||
#else
|
||||
auto json = nlohmann::json::parse(responseStr);
|
||||
return json["ap_list"][0];
|
||||
#endif
|
||||
}
|
||||
130
lib/spotify/cspot/src/AuthChallenges.cpp
Normal file
130
lib/spotify/cspot/src/AuthChallenges.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "AuthChallenges.h"
|
||||
|
||||
#include <algorithm> // for copy
|
||||
#include <climits> // for CHAR_BIT
|
||||
#include <random> // for default_random_engine, independent_bits_en...
|
||||
|
||||
#include "NanoPBHelper.h" // for pbPutString, pbEncode, pbDecode
|
||||
#include "pb.h" // for pb_byte_t
|
||||
#include "pb_decode.h" // for pb_release
|
||||
|
||||
using namespace cspot;
|
||||
using random_bytes_engine =
|
||||
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
|
||||
|
||||
AuthChallenges::AuthChallenges() {
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
this->clientHello = {};
|
||||
this->apResponse = {};
|
||||
this->authRequest = {};
|
||||
this->clientResPlaintext = {};
|
||||
}
|
||||
|
||||
AuthChallenges::~AuthChallenges() {
|
||||
// Destruct the protobufs
|
||||
pb_release(ClientHello_fields, &clientHello);
|
||||
pb_release(APResponseMessage_fields, &apResponse);
|
||||
pb_release(ClientResponsePlaintext_fields, &clientResPlaintext);
|
||||
pb_release(ClientResponseEncrypted_fields, &authRequest);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AuthChallenges::prepareAuthPacket(
|
||||
std::vector<uint8_t>& authData, int authType, const std::string& deviceId,
|
||||
const std::string& username) {
|
||||
// prepare authentication request proto
|
||||
pbPutString(username, authRequest.login_credentials.username);
|
||||
|
||||
std::copy(authData.begin(), authData.end(),
|
||||
authRequest.login_credentials.auth_data.bytes);
|
||||
authRequest.login_credentials.auth_data.size = authData.size();
|
||||
|
||||
authRequest.login_credentials.typ = (AuthenticationType)authType;
|
||||
authRequest.system_info.cpu_family = CpuFamily_CPU_UNKNOWN;
|
||||
authRequest.system_info.os = Os_OS_UNKNOWN;
|
||||
|
||||
auto infoStr = std::string("cspot-player");
|
||||
pbPutString(infoStr, authRequest.system_info.system_information_string);
|
||||
pbPutString(deviceId, authRequest.system_info.device_id);
|
||||
|
||||
auto versionStr = std::string("cspot-1.1");
|
||||
pbPutString(versionStr, authRequest.version_string);
|
||||
authRequest.has_version_string = true;
|
||||
|
||||
return pbEncode(ClientResponseEncrypted_fields, &authRequest);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AuthChallenges::solveApHello(
|
||||
std::vector<uint8_t>& helloPacket, std::vector<uint8_t>& data) {
|
||||
// Decode the response
|
||||
auto skipSize = std::vector<uint8_t>(data.begin() + 4, data.end());
|
||||
|
||||
pb_release(APResponseMessage_fields, &apResponse);
|
||||
pbDecode(apResponse, APResponseMessage_fields, skipSize);
|
||||
|
||||
auto diffieKey = std::vector<uint8_t>(
|
||||
apResponse.challenge.login_crypto_challenge.diffie_hellman.gs,
|
||||
apResponse.challenge.login_crypto_challenge.diffie_hellman.gs + 96);
|
||||
|
||||
// Compute the diffie hellman shared key based on the response
|
||||
auto sharedKey = this->crypto->dhCalculateShared(diffieKey);
|
||||
|
||||
// Init client packet + Init server packets are required for the hmac challenge
|
||||
data.insert(data.begin(), helloPacket.begin(), helloPacket.end());
|
||||
|
||||
// Solve the hmac challenge
|
||||
auto resultData = std::vector<uint8_t>(0);
|
||||
for (int x = 1; x < 6; x++) {
|
||||
auto challengeVector = std::vector<uint8_t>(1);
|
||||
challengeVector[0] = x;
|
||||
|
||||
challengeVector.insert(challengeVector.begin(), data.begin(), data.end());
|
||||
auto digest = crypto->sha1HMAC(sharedKey, challengeVector);
|
||||
resultData.insert(resultData.end(), digest.begin(), digest.end());
|
||||
}
|
||||
|
||||
auto lastVec =
|
||||
std::vector<uint8_t>(resultData.begin(), resultData.begin() + 0x14);
|
||||
|
||||
// Digest generated!
|
||||
auto digest = crypto->sha1HMAC(lastVec, data);
|
||||
clientResPlaintext.login_crypto_response.has_diffie_hellman = true;
|
||||
|
||||
std::copy(digest.begin(), digest.end(),
|
||||
clientResPlaintext.login_crypto_response.diffie_hellman.hmac);
|
||||
|
||||
// Get send and receive keys
|
||||
this->shanSendKey = std::vector<uint8_t>(resultData.begin() + 0x14,
|
||||
resultData.begin() + 0x34);
|
||||
this->shanRecvKey = std::vector<uint8_t>(resultData.begin() + 0x34,
|
||||
resultData.begin() + 0x54);
|
||||
|
||||
return pbEncode(ClientResponsePlaintext_fields, &clientResPlaintext);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AuthChallenges::prepareClientHello() {
|
||||
// Prepare protobuf message
|
||||
this->crypto->dhInit();
|
||||
|
||||
// Copy the public key into diffiehellman hello packet
|
||||
std::copy(this->crypto->publicKey.begin(), this->crypto->publicKey.end(),
|
||||
clientHello.login_crypto_hello.diffie_hellman.gc);
|
||||
|
||||
clientHello.login_crypto_hello.diffie_hellman.server_keys_known = 1;
|
||||
clientHello.build_info.product = Product_PRODUCT_CLIENT;
|
||||
clientHello.build_info.platform = Platform2_PLATFORM_LINUX_X86;
|
||||
clientHello.build_info.version = SPOTIFY_VERSION;
|
||||
clientHello.feature_set.autoupdate2 = true;
|
||||
clientHello.cryptosuites_supported[0] = Cryptosuite_CRYPTO_SUITE_SHANNON;
|
||||
clientHello.padding[0] = 0x1E;
|
||||
|
||||
clientHello.has_feature_set = true;
|
||||
clientHello.login_crypto_hello.has_diffie_hellman = true;
|
||||
clientHello.has_padding = true;
|
||||
clientHello.has_feature_set = true;
|
||||
|
||||
// Generate the random nonce
|
||||
auto nonce = crypto->generateVectorWithRandomData(16);
|
||||
std::copy(nonce.begin(), nonce.end(), clientHello.client_nonce);
|
||||
|
||||
return pbEncode(ClientHello_fields, &clientHello);
|
||||
}
|
||||
159
lib/spotify/cspot/src/CDNAudioFile.cpp
Normal file
159
lib/spotify/cspot/src/CDNAudioFile.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "CDNAudioFile.h"
|
||||
|
||||
#include <string.h> // for memcpy
|
||||
#include <functional> // for __base
|
||||
#include <initializer_list> // for initializer_list
|
||||
#include <map> // for operator!=, operator==
|
||||
#include <string_view> // for string_view
|
||||
#include <type_traits> // for remove_extent_t
|
||||
|
||||
#include "AccessKeyFetcher.h" // for AccessKeyFetcher
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "Crypto.h"
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "Packet.h" // for cspot
|
||||
#include "SocketStream.h" // for SocketStream
|
||||
#include "Utils.h" // for bigNumAdd, bytesToHexString, string...
|
||||
#include "WrappedSemaphore.h" // for WrappedSemaphore
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
#include "cJSON.h"
|
||||
#else
|
||||
#include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
|
||||
#include "nlohmann/json_fwd.hpp" // for json
|
||||
#endif
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
CDNAudioFile::CDNAudioFile(const std::string& cdnUrl,
|
||||
const std::vector<uint8_t>& audioKey)
|
||||
: cdnUrl(cdnUrl), audioKey(audioKey) {
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
}
|
||||
|
||||
size_t CDNAudioFile::getPosition() {
|
||||
return this->position;
|
||||
}
|
||||
|
||||
void CDNAudioFile::seek(size_t newPos) {
|
||||
this->enableRequestMargin = true;
|
||||
this->position = newPos;
|
||||
}
|
||||
|
||||
void CDNAudioFile::openStream() {
|
||||
CSPOT_LOG(info, "Opening HTTP stream to %s", this->cdnUrl.c_str());
|
||||
|
||||
// Open connection, read first 128 bytes
|
||||
this->httpConnection = bell::HTTPClient::get(
|
||||
this->cdnUrl,
|
||||
{bell::HTTPClient::RangeHeader::range(0, OPUS_HEADER_SIZE - 1)});
|
||||
|
||||
this->httpConnection->stream().read((char*)header.data(), OPUS_HEADER_SIZE);
|
||||
this->totalFileSize =
|
||||
this->httpConnection->totalLength() - SPOTIFY_OPUS_HEADER;
|
||||
|
||||
this->decrypt(header.data(), OPUS_HEADER_SIZE, 0);
|
||||
|
||||
// Location must be dividable by 16
|
||||
size_t footerStartLocation =
|
||||
(this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) -
|
||||
(this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) % 16;
|
||||
|
||||
this->footer = std::vector<uint8_t>(
|
||||
this->totalFileSize - footerStartLocation + SPOTIFY_OPUS_HEADER);
|
||||
this->httpConnection->get(
|
||||
cdnUrl, {bell::HTTPClient::RangeHeader::last(footer.size())});
|
||||
|
||||
this->httpConnection->stream().read((char*)footer.data(),
|
||||
this->footer.size());
|
||||
|
||||
this->decrypt(footer.data(), footer.size(), footerStartLocation);
|
||||
CSPOT_LOG(info, "Header and footer bytes received");
|
||||
this->position = 0;
|
||||
this->lastRequestPosition = 0;
|
||||
this->lastRequestCapacity = 0;
|
||||
}
|
||||
|
||||
size_t CDNAudioFile::readBytes(uint8_t* dst, size_t bytes) {
|
||||
size_t offsetPosition = position + SPOTIFY_OPUS_HEADER;
|
||||
size_t actualFileSize = this->totalFileSize + SPOTIFY_OPUS_HEADER;
|
||||
|
||||
if (position + bytes >= this->totalFileSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// // Opus tries to read header, use prefetched data
|
||||
if (offsetPosition < OPUS_HEADER_SIZE &&
|
||||
bytes + offsetPosition <= OPUS_HEADER_SIZE) {
|
||||
memcpy(dst, this->header.data() + offsetPosition, bytes);
|
||||
position += bytes;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// // Opus tries to read footer, use prefetched data
|
||||
if (offsetPosition >= (actualFileSize - this->footer.size())) {
|
||||
size_t toReadBytes = bytes;
|
||||
|
||||
if ((position + bytes) > this->totalFileSize) {
|
||||
// Tries to read outside of bounds, truncate
|
||||
toReadBytes = this->totalFileSize - position;
|
||||
}
|
||||
|
||||
size_t footerOffset =
|
||||
offsetPosition - (actualFileSize - this->footer.size());
|
||||
memcpy(dst, this->footer.data() + footerOffset, toReadBytes);
|
||||
|
||||
position += toReadBytes;
|
||||
return toReadBytes;
|
||||
}
|
||||
|
||||
// Data not in the headers. Make sense of whats going on.
|
||||
// Position in bounds :)
|
||||
if (offsetPosition >= this->lastRequestPosition &&
|
||||
offsetPosition < this->lastRequestPosition + this->lastRequestCapacity) {
|
||||
size_t toRead = bytes;
|
||||
|
||||
if ((toRead + offsetPosition) >
|
||||
this->lastRequestPosition + lastRequestCapacity) {
|
||||
toRead = this->lastRequestPosition + lastRequestCapacity - offsetPosition;
|
||||
}
|
||||
|
||||
memcpy(dst, this->httpBuffer.data() + offsetPosition - lastRequestPosition,
|
||||
toRead);
|
||||
position += toRead;
|
||||
|
||||
return toRead;
|
||||
} else {
|
||||
size_t requestPosition = (offsetPosition) - ((offsetPosition) % 16);
|
||||
if (this->enableRequestMargin && requestPosition > SEEK_MARGIN_SIZE) {
|
||||
requestPosition = (offsetPosition - SEEK_MARGIN_SIZE) -
|
||||
((offsetPosition - SEEK_MARGIN_SIZE) % 16);
|
||||
this->enableRequestMargin = false;
|
||||
}
|
||||
|
||||
this->httpConnection->get(
|
||||
cdnUrl, {bell::HTTPClient::RangeHeader::range(
|
||||
requestPosition, requestPosition + HTTP_BUFFER_SIZE - 1)});
|
||||
this->lastRequestPosition = requestPosition;
|
||||
this->lastRequestCapacity = this->httpConnection->contentLength();
|
||||
|
||||
this->httpConnection->stream().read((char*)this->httpBuffer.data(),
|
||||
lastRequestCapacity);
|
||||
this->decrypt(this->httpBuffer.data(), lastRequestCapacity,
|
||||
|
||||
this->lastRequestPosition);
|
||||
|
||||
return readBytes(dst, bytes);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
size_t CDNAudioFile::getSize() {
|
||||
return this->totalFileSize;
|
||||
}
|
||||
|
||||
void CDNAudioFile::decrypt(uint8_t* dst, size_t nbytes, size_t pos) {
|
||||
auto calculatedIV = bigNumAdd(audioAESIV, pos / 16);
|
||||
|
||||
this->crypto->aesCTRXcrypt(this->audioKey, calculatedIV, dst, nbytes);
|
||||
}
|
||||
269
lib/spotify/cspot/src/LoginBlob.cpp
Normal file
269
lib/spotify/cspot/src/LoginBlob.cpp
Normal file
@@ -0,0 +1,269 @@
|
||||
#include "LoginBlob.h"
|
||||
|
||||
#include <stdio.h> // for sprintf
|
||||
#include <initializer_list> // for initializer_list
|
||||
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "ConstantParameters.h" // for brandName, cspot, protoc...
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "protobuf/authentication.pb.h" // for AuthenticationType_AUTHE...
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
#include "cJSON.h"
|
||||
#else
|
||||
#include "nlohmann/detail/json_pointer.hpp" // for json_pointer<>::string_t
|
||||
#include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
|
||||
#include "nlohmann/json_fwd.hpp" // for json
|
||||
#endif
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
LoginBlob::LoginBlob(std::string name) {
|
||||
char hash[32];
|
||||
sprintf(hash, "%016zu", std::hash<std::string>{}(name));
|
||||
// base is 142137fd329622137a14901634264e6f332e2411
|
||||
this->deviceId = std::string("142137fd329622137a149016") + std::string(hash);
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
this->name = name;
|
||||
|
||||
this->crypto->dhInit();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> LoginBlob::decodeBlob(
|
||||
const std::vector<uint8_t>& blob, const std::vector<uint8_t>& sharedKey) {
|
||||
// 0:16 - iv; 17:-20 - blob; -20:0 - checksum
|
||||
auto iv = std::vector<uint8_t>(blob.begin(), blob.begin() + 16);
|
||||
auto encrypted = std::vector<uint8_t>(blob.begin() + 16, blob.end() - 20);
|
||||
auto checksum = std::vector<uint8_t>(blob.end() - 20, blob.end());
|
||||
|
||||
// baseKey = sha1(sharedKey) 0:16
|
||||
crypto->sha1Init();
|
||||
|
||||
crypto->sha1Update(sharedKey);
|
||||
auto baseKey = crypto->sha1FinalBytes();
|
||||
baseKey = std::vector<uint8_t>(baseKey.begin(), baseKey.begin() + 16);
|
||||
|
||||
auto checksumMessage = std::string("checksum");
|
||||
auto checksumKey = crypto->sha1HMAC(
|
||||
baseKey,
|
||||
std::vector<uint8_t>(checksumMessage.begin(), checksumMessage.end()));
|
||||
|
||||
auto encryptionMessage = std::string("encryption");
|
||||
auto encryptionKey = crypto->sha1HMAC(
|
||||
baseKey,
|
||||
std::vector<uint8_t>(encryptionMessage.begin(), encryptionMessage.end()));
|
||||
|
||||
auto mac = crypto->sha1HMAC(checksumKey, encrypted);
|
||||
|
||||
// Check checksum
|
||||
if (mac != checksum) {
|
||||
CSPOT_LOG(error, "Mac doesn't match!");
|
||||
}
|
||||
|
||||
encryptionKey =
|
||||
std::vector<uint8_t>(encryptionKey.begin(), encryptionKey.begin() + 16);
|
||||
crypto->aesCTRXcrypt(encryptionKey, iv, encrypted.data(), encrypted.size());
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
uint32_t LoginBlob::readBlobInt(const std::vector<uint8_t>& data) {
|
||||
auto lo = data[blobSkipPosition];
|
||||
if ((int)(lo & 0x80) == 0) {
|
||||
this->blobSkipPosition += 1;
|
||||
return lo;
|
||||
}
|
||||
|
||||
auto hi = data[blobSkipPosition + 1];
|
||||
this->blobSkipPosition += 2;
|
||||
|
||||
return (uint32_t)((lo & 0x7f) | (hi << 7));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> LoginBlob::decodeBlobSecondary(
|
||||
const std::vector<uint8_t>& blob, const std::string& username,
|
||||
const std::string& deviceId) {
|
||||
auto encryptedString = std::string(blob.begin(), blob.end());
|
||||
auto blobData = crypto->base64Decode(encryptedString);
|
||||
|
||||
crypto->sha1Init();
|
||||
crypto->sha1Update(std::vector<uint8_t>(deviceId.begin(), deviceId.end()));
|
||||
auto secret = crypto->sha1FinalBytes();
|
||||
auto pkBaseKey = crypto->pbkdf2HmacSha1(
|
||||
secret, std::vector<uint8_t>(username.begin(), username.end()), 256, 20);
|
||||
|
||||
crypto->sha1Init();
|
||||
crypto->sha1Update(pkBaseKey);
|
||||
auto key = std::vector<uint8_t>({0x00, 0x00, 0x00, 0x14}); // len of base key
|
||||
auto baseKeyHashed = crypto->sha1FinalBytes();
|
||||
key.insert(key.begin(), baseKeyHashed.begin(), baseKeyHashed.end());
|
||||
|
||||
crypto->aesECBdecrypt(key, blobData);
|
||||
|
||||
auto l = blobData.size();
|
||||
|
||||
for (int i = 0; i < l - 16; i++) {
|
||||
blobData[l - i - 1] ^= blobData[l - i - 17];
|
||||
}
|
||||
|
||||
return blobData;
|
||||
}
|
||||
|
||||
void LoginBlob::loadZeroconf(const std::vector<uint8_t>& blob,
|
||||
const std::vector<uint8_t>& sharedKey,
|
||||
const std::string& deviceId,
|
||||
const std::string& username) {
|
||||
|
||||
auto partDecoded = this->decodeBlob(blob, sharedKey);
|
||||
auto loginData = this->decodeBlobSecondary(partDecoded, username, deviceId);
|
||||
|
||||
// Parse blob
|
||||
blobSkipPosition = 1;
|
||||
blobSkipPosition += readBlobInt(loginData);
|
||||
blobSkipPosition += 1;
|
||||
this->authType = readBlobInt(loginData);
|
||||
blobSkipPosition += 1;
|
||||
auto authSize = readBlobInt(loginData);
|
||||
this->username = username;
|
||||
this->authData =
|
||||
std::vector<uint8_t>(loginData.begin() + blobSkipPosition,
|
||||
loginData.begin() + blobSkipPosition + authSize);
|
||||
}
|
||||
|
||||
void LoginBlob::loadUserPass(const std::string& username,
|
||||
const std::string& password) {
|
||||
this->username = username;
|
||||
this->authData = std::vector<uint8_t>(password.begin(), password.end());
|
||||
this->authType =
|
||||
static_cast<uint32_t>(AuthenticationType_AUTHENTICATION_USER_PASS);
|
||||
}
|
||||
|
||||
void LoginBlob::loadJson(const std::string& json) {
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* root = cJSON_Parse(json.c_str());
|
||||
this->authType = cJSON_GetObjectItem(root, "authType")->valueint;
|
||||
this->username = cJSON_GetObjectItem(root, "username")->valuestring;
|
||||
std::string authDataObject =
|
||||
cJSON_GetObjectItem(root, "authData")->valuestring;
|
||||
this->authData = crypto->base64Decode(authDataObject);
|
||||
cJSON_Delete(root);
|
||||
#else
|
||||
auto root = nlohmann::json::parse(json);
|
||||
this->authType = root["authType"];
|
||||
this->username = root["username"];
|
||||
std::string authDataObject = root["authData"];
|
||||
|
||||
this->authData = crypto->base64Decode(authDataObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string LoginBlob::toJson() {
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* json_obj = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json_obj, "authData",
|
||||
crypto->base64Encode(authData).c_str());
|
||||
cJSON_AddNumberToObject(json_obj, "authType", this->authType);
|
||||
cJSON_AddStringToObject(json_obj, "username", this->username.c_str());
|
||||
|
||||
char* str = cJSON_PrintUnformatted(json_obj);
|
||||
cJSON_Delete(json_obj);
|
||||
std::string json_objStr(str);
|
||||
free(str);
|
||||
|
||||
return json_objStr;
|
||||
#else
|
||||
nlohmann::json obj;
|
||||
obj["authData"] = crypto->base64Encode(authData);
|
||||
obj["authType"] = this->authType;
|
||||
obj["username"] = this->username;
|
||||
|
||||
return obj.dump();
|
||||
#endif
|
||||
}
|
||||
|
||||
void LoginBlob::loadZeroconfQuery(
|
||||
std::map<std::string, std::string>& queryParams) {
|
||||
// Get all urlencoded params
|
||||
auto username = queryParams["userName"];
|
||||
auto blobString = queryParams["blob"];
|
||||
auto clientKeyString = queryParams["clientKey"];
|
||||
auto deviceName = queryParams["deviceName"];
|
||||
|
||||
// client key and bytes are urlencoded
|
||||
auto clientKeyBytes = crypto->base64Decode(clientKeyString);
|
||||
auto blobBytes = crypto->base64Decode(blobString);
|
||||
|
||||
// Generated secret based on earlier generated DH
|
||||
auto secretKey = crypto->dhCalculateShared(clientKeyBytes);
|
||||
|
||||
this->loadZeroconf(blobBytes, secretKey, deviceId, username);
|
||||
}
|
||||
|
||||
std::string LoginBlob::buildZeroconfInfo() {
|
||||
// Encode publicKey into base64
|
||||
|
||||
auto encodedKey = crypto->base64Encode(crypto->publicKey);
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* json_obj = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(json_obj, "status", 101);
|
||||
cJSON_AddStringToObject(json_obj, "statusString", "OK");
|
||||
cJSON_AddStringToObject(json_obj, "version", cspot::protocolVersion);
|
||||
cJSON_AddStringToObject(json_obj, "libraryVersion", cspot::swVersion);
|
||||
cJSON_AddStringToObject(json_obj, "accountReq", "PREMIUM");
|
||||
cJSON_AddStringToObject(json_obj, "brandDisplayName", cspot::brandName);
|
||||
cJSON_AddStringToObject(json_obj, "modelDisplayName", name.c_str());
|
||||
cJSON_AddStringToObject(json_obj, "voiceSupport", "NO");
|
||||
cJSON_AddStringToObject(json_obj, "availability", this->username.c_str());
|
||||
cJSON_AddNumberToObject(json_obj, "productID", 0);
|
||||
cJSON_AddStringToObject(json_obj, "tokenType", "default");
|
||||
cJSON_AddStringToObject(json_obj, "groupStatus", "NONE");
|
||||
cJSON_AddStringToObject(json_obj, "resolverVersion", "0");
|
||||
cJSON_AddStringToObject(json_obj, "scope",
|
||||
"streaming,client-authorization-universal");
|
||||
cJSON_AddStringToObject(json_obj, "activeUser", "");
|
||||
cJSON_AddStringToObject(json_obj, "deviceID", deviceId.c_str());
|
||||
cJSON_AddStringToObject(json_obj, "remoteName", name.c_str());
|
||||
cJSON_AddStringToObject(json_obj, "publicKey", encodedKey.c_str());
|
||||
cJSON_AddStringToObject(json_obj, "deviceType", "deviceType");
|
||||
|
||||
char* str = cJSON_PrintUnformatted(json_obj);
|
||||
cJSON_Delete(json_obj);
|
||||
std::string json_objStr(str);
|
||||
free(str);
|
||||
|
||||
return json_objStr;
|
||||
#else
|
||||
nlohmann::json obj;
|
||||
obj["status"] = 101;
|
||||
obj["statusString"] = "OK";
|
||||
obj["version"] = cspot::protocolVersion;
|
||||
obj["spotifyError"] = 0;
|
||||
obj["libraryVersion"] = cspot::swVersion;
|
||||
obj["accountReq"] = "PREMIUM";
|
||||
obj["brandDisplayName"] = cspot::brandName;
|
||||
obj["modelDisplayName"] = name;
|
||||
obj["voiceSupport"] = "NO";
|
||||
obj["availability"] = this->username;
|
||||
obj["productID"] = 0;
|
||||
obj["tokenType"] = "default";
|
||||
obj["groupStatus"] = "NONE";
|
||||
obj["resolverVersion"] = "0";
|
||||
obj["scope"] = "streaming,client-authorization-universal";
|
||||
obj["activeUser"] = "";
|
||||
obj["deviceID"] = deviceId;
|
||||
obj["remoteName"] = name;
|
||||
obj["publicKey"] = encodedKey;
|
||||
obj["deviceType"] = "SPEAKER";
|
||||
|
||||
return obj.dump();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string LoginBlob::getDeviceId() {
|
||||
return this->deviceId;
|
||||
}
|
||||
std::string LoginBlob::getDeviceName() {
|
||||
return this->name;
|
||||
}
|
||||
std::string LoginBlob::getUserName() {
|
||||
return this->username;
|
||||
}
|
||||
347
lib/spotify/cspot/src/MercurySession.cpp
Normal file
347
lib/spotify/cspot/src/MercurySession.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
#include "MercurySession.h"
|
||||
|
||||
#include <string.h> // for memcpy
|
||||
#include <memory> // for shared_ptr
|
||||
#include <mutex> // for scoped_lock
|
||||
#include <stdexcept> // for runtime_error
|
||||
#include <type_traits> // for remove_extent_t, __underlying_type_impl<>:...
|
||||
#include <utility> // for pair
|
||||
#ifndef _WIN32
|
||||
#include <arpa/inet.h> // for htons, ntohs, htonl, ntohl
|
||||
#endif
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "BellTask.h" // for Task
|
||||
#include "BellUtils.h" // for BELL_SLEEP_MS
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "NanoPBHelper.h" // for pbPutString, pbDecode, pbEncode
|
||||
#include "PlainConnection.h" // for PlainConnection
|
||||
#include "ShannonConnection.h" // for ShannonConnection
|
||||
#include "TimeProvider.h" // for TimeProvider
|
||||
#include "Utils.h" // for extract, pack, hton64
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
MercurySession::MercurySession(std::shared_ptr<TimeProvider> timeProvider)
|
||||
: bell::Task("mercury_dispatcher", 4 * 1024, 3, 1) {
|
||||
this->timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
MercurySession::~MercurySession() {
|
||||
std::scoped_lock lock(this->isRunningMutex);
|
||||
}
|
||||
|
||||
void MercurySession::runTask() {
|
||||
isRunning = true;
|
||||
std::scoped_lock lock(this->isRunningMutex);
|
||||
|
||||
this->executeEstabilishedCallback = true;
|
||||
while (isRunning) {
|
||||
cspot::Packet packet = {};
|
||||
try {
|
||||
packet = shanConn->recvPacket();
|
||||
CSPOT_LOG(info, "Received packet, command: %d", packet.command);
|
||||
|
||||
if (static_cast<RequestType>(packet.command) == RequestType::PING) {
|
||||
timeProvider->syncWithPingPacket(packet.data);
|
||||
|
||||
this->lastPingTimestamp = timeProvider->getSyncedTimestamp();
|
||||
this->shanConn->sendPacket(0x49, packet.data);
|
||||
} else {
|
||||
this->packetQueue.push(packet);
|
||||
}
|
||||
} catch (const std::runtime_error& e) {
|
||||
CSPOT_LOG(error, "Error while receiving packet: %s", e.what());
|
||||
failAllPending();
|
||||
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
reconnect();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::reconnect() {
|
||||
isReconnecting = true;
|
||||
|
||||
try {
|
||||
this->conn = nullptr;
|
||||
this->shanConn = nullptr;
|
||||
|
||||
this->connectWithRandomAp();
|
||||
this->authenticate(this->authBlob);
|
||||
|
||||
CSPOT_LOG(info, "Reconnection successful");
|
||||
|
||||
BELL_SLEEP_MS(100);
|
||||
|
||||
lastPingTimestamp = timeProvider->getSyncedTimestamp();
|
||||
isReconnecting = false;
|
||||
|
||||
this->executeEstabilishedCallback = true;
|
||||
} catch (...) {
|
||||
CSPOT_LOG(error, "Cannot reconnect, will retry in 5s");
|
||||
BELL_SLEEP_MS(5000);
|
||||
|
||||
if (isRunning) {
|
||||
return reconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::setConnectedHandler(
|
||||
ConnectionEstabilishedCallback callback) {
|
||||
this->connectionReadyCallback = callback;
|
||||
}
|
||||
|
||||
bool MercurySession::triggerTimeout() {
|
||||
if (!isRunning)
|
||||
return true;
|
||||
auto currentTimestamp = timeProvider->getSyncedTimestamp();
|
||||
|
||||
if (currentTimestamp - this->lastPingTimestamp > PING_TIMEOUT_MS) {
|
||||
CSPOT_LOG(debug, "Reconnection required, no ping received");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MercurySession::unregister(uint64_t sequenceId) {
|
||||
auto callback = this->callbacks.find(sequenceId);
|
||||
|
||||
if (callback != this->callbacks.end()) {
|
||||
this->callbacks.erase(callback);
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::unregisterAudioKey(uint32_t sequenceId) {
|
||||
auto callback = this->audioKeyCallbacks.find(sequenceId);
|
||||
|
||||
if (callback != this->audioKeyCallbacks.end()) {
|
||||
this->audioKeyCallbacks.erase(callback);
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::disconnect() {
|
||||
CSPOT_LOG(info, "Disconnecting mercury session");
|
||||
this->isRunning = false;
|
||||
conn->close();
|
||||
std::scoped_lock lock(this->isRunningMutex);
|
||||
}
|
||||
|
||||
std::string MercurySession::getCountryCode() {
|
||||
return this->countryCode;
|
||||
}
|
||||
|
||||
void MercurySession::handlePacket() {
|
||||
Packet packet = {};
|
||||
|
||||
this->packetQueue.wtpop(packet, 200);
|
||||
|
||||
if (executeEstabilishedCallback && this->connectionReadyCallback != nullptr) {
|
||||
executeEstabilishedCallback = false;
|
||||
this->connectionReadyCallback();
|
||||
}
|
||||
|
||||
switch (static_cast<RequestType>(packet.command)) {
|
||||
case RequestType::COUNTRY_CODE_RESPONSE: {
|
||||
this->countryCode = std::string();
|
||||
this->countryCode.resize(2);
|
||||
memcpy(this->countryCode.data(), packet.data.data(), 2);
|
||||
CSPOT_LOG(debug, "Received country code %s", this->countryCode.c_str());
|
||||
break;
|
||||
}
|
||||
case RequestType::AUDIO_KEY_FAILURE_RESPONSE:
|
||||
case RequestType::AUDIO_KEY_SUCCESS_RESPONSE: {
|
||||
// this->lastRequestTimestamp = -1;
|
||||
|
||||
// First four bytes mark the sequence id
|
||||
auto seqId = ntohl(extract<uint32_t>(packet.data, 0));
|
||||
|
||||
if (this->audioKeyCallbacks.count(seqId) > 0) {
|
||||
auto success = static_cast<RequestType>(packet.command) ==
|
||||
RequestType::AUDIO_KEY_SUCCESS_RESPONSE;
|
||||
this->audioKeyCallbacks[seqId](success, packet.data);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case RequestType::SEND:
|
||||
case RequestType::SUB:
|
||||
case RequestType::UNSUB: {
|
||||
CSPOT_LOG(debug, "Received mercury packet");
|
||||
|
||||
auto response = this->decodeResponse(packet.data);
|
||||
if (this->callbacks.count(response.sequenceId) > 0) {
|
||||
auto seqId = response.sequenceId;
|
||||
this->callbacks[response.sequenceId](response);
|
||||
this->callbacks.erase(this->callbacks.find(seqId));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RequestType::SUBRES: {
|
||||
auto response = decodeResponse(packet.data);
|
||||
|
||||
auto uri = std::string(response.mercuryHeader.uri);
|
||||
if (this->subscriptions.count(uri) > 0) {
|
||||
this->subscriptions[uri](response);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::failAllPending() {
|
||||
Response response = {};
|
||||
response.fail = true;
|
||||
|
||||
// Fail all callbacks
|
||||
for (auto& it : this->callbacks) {
|
||||
it.second(response);
|
||||
}
|
||||
|
||||
// Fail all subscriptions
|
||||
for (auto& it : this->subscriptions) {
|
||||
it.second(response);
|
||||
}
|
||||
|
||||
// Remove references
|
||||
this->subscriptions = {};
|
||||
this->callbacks = {};
|
||||
}
|
||||
|
||||
MercurySession::Response MercurySession::decodeResponse(
|
||||
const std::vector<uint8_t>& data) {
|
||||
Response response = {};
|
||||
response.parts = {};
|
||||
|
||||
auto sequenceLength = ntohs(extract<uint16_t>(data, 0));
|
||||
response.sequenceId = hton64(extract<uint64_t>(data, 2));
|
||||
|
||||
auto partsNumber = ntohs(extract<uint16_t>(data, 11));
|
||||
|
||||
auto headerSize = ntohs(extract<uint16_t>(data, 13));
|
||||
auto headerBytes =
|
||||
std::vector<uint8_t>(data.begin() + 15, data.begin() + 15 + headerSize);
|
||||
|
||||
auto pos = 15 + headerSize;
|
||||
while (pos < data.size()) {
|
||||
auto partSize = ntohs(extract<uint16_t>(data, pos));
|
||||
|
||||
response.parts.push_back(std::vector<uint8_t>(
|
||||
data.begin() + pos + 2, data.begin() + pos + 2 + partSize));
|
||||
pos += 2 + partSize;
|
||||
}
|
||||
|
||||
pbDecode(response.mercuryHeader, Header_fields, headerBytes);
|
||||
response.fail = false;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
uint64_t MercurySession::executeSubscription(RequestType method,
|
||||
const std::string& uri,
|
||||
ResponseCallback callback,
|
||||
ResponseCallback subscription,
|
||||
DataParts& payload) {
|
||||
CSPOT_LOG(debug, "Executing Mercury Request, type %s",
|
||||
RequestTypeMap[method].c_str());
|
||||
|
||||
// Encode header
|
||||
pbPutString(uri, tempMercuryHeader.uri);
|
||||
pbPutString(RequestTypeMap[method], tempMercuryHeader.method);
|
||||
|
||||
tempMercuryHeader.has_method = true;
|
||||
tempMercuryHeader.has_uri = true;
|
||||
|
||||
// GET and SEND are actually the same. Therefore the override
|
||||
// The difference between them is only in header's method
|
||||
if (method == RequestType::GET) {
|
||||
method = RequestType::SEND;
|
||||
}
|
||||
|
||||
if (method == RequestType::SUB) {
|
||||
this->subscriptions.insert({uri, subscription});
|
||||
}
|
||||
|
||||
auto headerBytes = pbEncode(Header_fields, &tempMercuryHeader);
|
||||
|
||||
this->callbacks.insert({sequenceId, callback});
|
||||
|
||||
// Structure: [Sequence size] [SequenceId] [0x1] [Payloads number]
|
||||
// [Header size] [Header] [Payloads (size + data)]
|
||||
|
||||
// Pack sequenceId
|
||||
auto sequenceIdBytes = pack<uint64_t>(hton64(this->sequenceId));
|
||||
auto sequenceSizeBytes = pack<uint16_t>(htons(sequenceIdBytes.size()));
|
||||
|
||||
sequenceIdBytes.insert(sequenceIdBytes.begin(), sequenceSizeBytes.begin(),
|
||||
sequenceSizeBytes.end());
|
||||
sequenceIdBytes.push_back(0x01);
|
||||
|
||||
auto payloadNum = pack<uint16_t>(htons(payload.size() + 1));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), payloadNum.begin(),
|
||||
payloadNum.end());
|
||||
|
||||
auto headerSizePayload = pack<uint16_t>(htons(headerBytes.size()));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(),
|
||||
headerSizePayload.end());
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerBytes.begin(),
|
||||
headerBytes.end());
|
||||
|
||||
// Encode all the payload parts
|
||||
for (int x = 0; x < payload.size(); x++) {
|
||||
headerSizePayload = pack<uint16_t>(htons(payload[x].size()));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(),
|
||||
headerSizePayload.end());
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), payload[x].begin(),
|
||||
payload[x].end());
|
||||
}
|
||||
|
||||
// Bump sequence id
|
||||
this->sequenceId += 1;
|
||||
|
||||
try {
|
||||
this->shanConn->sendPacket(
|
||||
static_cast<std::underlying_type<RequestType>::type>(method),
|
||||
sequenceIdBytes);
|
||||
} catch (...) {
|
||||
// @TODO: handle disconnect
|
||||
}
|
||||
|
||||
return this->sequenceId - 1;
|
||||
}
|
||||
|
||||
uint32_t MercurySession::requestAudioKey(const std::vector<uint8_t>& trackId,
|
||||
const std::vector<uint8_t>& fileId,
|
||||
AudioKeyCallback audioCallback) {
|
||||
auto buffer = fileId;
|
||||
|
||||
// Store callback
|
||||
this->audioKeyCallbacks.insert({this->audioKeySequence, audioCallback});
|
||||
|
||||
// Structure: [FILEID] [TRACKID] [4 BYTES SEQUENCE ID] [0x00, 0x00]
|
||||
buffer.insert(buffer.end(), trackId.begin(), trackId.end());
|
||||
auto audioKeySequenceBuffer = pack<uint32_t>(htonl(this->audioKeySequence));
|
||||
buffer.insert(buffer.end(), audioKeySequenceBuffer.begin(),
|
||||
audioKeySequenceBuffer.end());
|
||||
auto suffix = std::vector<uint8_t>({0x00, 0x00});
|
||||
buffer.insert(buffer.end(), suffix.begin(), suffix.end());
|
||||
|
||||
// Bump audio key sequence
|
||||
this->audioKeySequence += 1;
|
||||
|
||||
// Used for broken connection detection
|
||||
// this->lastRequestTimestamp = timeProvider->getSyncedTimestamp();
|
||||
try {
|
||||
this->shanConn->sendPacket(
|
||||
static_cast<uint8_t>(RequestType::AUDIO_KEY_REQUEST_COMMAND), buffer);
|
||||
} catch (...) {
|
||||
// @TODO: Handle disconnect
|
||||
}
|
||||
return audioKeySequence - 1;
|
||||
}
|
||||
205
lib/spotify/cspot/src/PlainConnection.cpp
Normal file
205
lib/spotify/cspot/src/PlainConnection.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
#include "PlainConnection.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <netdb.h> // for addrinfo, freeaddrinfo, getaddrinfo
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h> // for IPPROTO_IP, IPPROTO_TCP
|
||||
#include <netinet/tcp.h> // for TCP_NODELAY
|
||||
#include <sys/errno.h> // for EAGAIN, EINTR, ETIMEDOUT, errno
|
||||
#include <sys/socket.h> // for setsockopt, connect, recv, send, shutdown
|
||||
#include <sys/time.h> // for timeval
|
||||
#include <cstring> // for memset
|
||||
#include <stdexcept> // for runtime_error
|
||||
#else
|
||||
#include <ws2tcpip.h>
|
||||
#endif
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "Packet.h" // for cspot
|
||||
#include "Utils.h" // for extract, pack
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
static int getErrno() {
|
||||
#ifdef _WIN32
|
||||
int code = WSAGetLastError();
|
||||
if (code == WSAETIMEDOUT)
|
||||
return ETIMEDOUT;
|
||||
if (code == WSAEINTR)
|
||||
return EINTR;
|
||||
return code;
|
||||
#else
|
||||
return errno;
|
||||
#endif
|
||||
}
|
||||
|
||||
PlainConnection::PlainConnection() {
|
||||
this->apSock = -1;
|
||||
};
|
||||
|
||||
PlainConnection::~PlainConnection() {
|
||||
this->close();
|
||||
};
|
||||
|
||||
void PlainConnection::connect(const std::string& apAddress) {
|
||||
struct addrinfo h, *airoot, *ai;
|
||||
std::string hostname = apAddress.substr(0, apAddress.find(":"));
|
||||
std::string portStr =
|
||||
apAddress.substr(apAddress.find(":") + 1, apAddress.size());
|
||||
memset(&h, 0, sizeof(h));
|
||||
h.ai_family = AF_INET;
|
||||
h.ai_socktype = SOCK_STREAM;
|
||||
h.ai_protocol = IPPROTO_IP;
|
||||
|
||||
// Lookup host
|
||||
if (getaddrinfo(hostname.c_str(), portStr.c_str(), &h, &airoot)) {
|
||||
CSPOT_LOG(error, "getaddrinfo failed");
|
||||
}
|
||||
|
||||
// find the right ai, connect to server
|
||||
for (ai = airoot; ai; ai = ai->ai_next) {
|
||||
if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
|
||||
continue;
|
||||
|
||||
this->apSock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
|
||||
if (this->apSock < 0)
|
||||
continue;
|
||||
|
||||
if (::connect(this->apSock, (struct sockaddr*)ai->ai_addr,
|
||||
ai->ai_addrlen) != -1) {
|
||||
#ifdef _WIN32
|
||||
uint32_t tv = 3000;
|
||||
#else
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 3;
|
||||
tv.tv_usec = 0;
|
||||
#endif
|
||||
setsockopt(this->apSock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv,
|
||||
sizeof tv);
|
||||
setsockopt(this->apSock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv,
|
||||
sizeof tv);
|
||||
|
||||
int flag = 1;
|
||||
setsockopt(this->apSock, /* socket affected */
|
||||
IPPROTO_TCP, /* set option at TCP level */
|
||||
TCP_NODELAY, /* name of option */
|
||||
(char*)&flag, /* the cast is historical cruft */
|
||||
sizeof(int)); /* length of option value */
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
closesocket(this->apSock);
|
||||
#else
|
||||
::close(this->apSock);
|
||||
#endif
|
||||
apSock = -1;
|
||||
throw std::runtime_error("Can't connect to spotify servers");
|
||||
}
|
||||
|
||||
freeaddrinfo(airoot);
|
||||
CSPOT_LOG(debug, "Connected to spotify server");
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlainConnection::recvPacket() {
|
||||
// Read packet size
|
||||
std::vector<uint8_t> packetBuffer(4);
|
||||
readBlock(packetBuffer.data(), 4);
|
||||
uint32_t packetSize = ntohl(extract<uint32_t>(packetBuffer, 0));
|
||||
|
||||
packetBuffer.resize(packetSize, 0);
|
||||
|
||||
// Read actual data
|
||||
readBlock(packetBuffer.data() + 4, packetSize - 4);
|
||||
|
||||
return packetBuffer;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlainConnection::sendPrefixPacket(
|
||||
const std::vector<uint8_t>& prefix, const std::vector<uint8_t>& data) {
|
||||
// Calculate full packet length
|
||||
uint32_t actualSize = prefix.size() + data.size() + sizeof(uint32_t);
|
||||
|
||||
// Packet structure [PREFIX] + [SIZE] + [DATA]
|
||||
auto sizeRaw = pack<uint32_t>(htonl(actualSize));
|
||||
sizeRaw.insert(sizeRaw.begin(), prefix.begin(), prefix.end());
|
||||
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
|
||||
|
||||
// Actually write it to the server
|
||||
writeBlock(sizeRaw);
|
||||
|
||||
return sizeRaw;
|
||||
}
|
||||
|
||||
void PlainConnection::readBlock(const uint8_t* dst, size_t size) {
|
||||
unsigned int idx = 0;
|
||||
ssize_t n;
|
||||
int retries = 0;
|
||||
|
||||
while (idx < size) {
|
||||
READ:
|
||||
if ((n = recv(this->apSock, (char*)&dst[idx], size - idx, 0)) <= 0) {
|
||||
switch (getErrno()) {
|
||||
case EAGAIN:
|
||||
case ETIMEDOUT:
|
||||
if (timeoutHandler()) {
|
||||
CSPOT_LOG(error, "Connection lost, will need to reconnect...");
|
||||
throw std::runtime_error("Reconnection required");
|
||||
}
|
||||
goto READ;
|
||||
case EINTR:
|
||||
break;
|
||||
default:
|
||||
if (retries++ > 4)
|
||||
throw std::runtime_error("Error in read");
|
||||
goto READ;
|
||||
}
|
||||
}
|
||||
idx += n;
|
||||
}
|
||||
}
|
||||
|
||||
size_t PlainConnection::writeBlock(const std::vector<uint8_t>& data) {
|
||||
unsigned int idx = 0;
|
||||
ssize_t n;
|
||||
|
||||
int retries = 0;
|
||||
|
||||
while (idx < data.size()) {
|
||||
WRITE:
|
||||
if ((n = send(this->apSock, (char*)&data[idx],
|
||||
data.size() - idx < 64 ? data.size() - idx : 64, 0)) <= 0) {
|
||||
switch (getErrno()) {
|
||||
case EAGAIN:
|
||||
case ETIMEDOUT:
|
||||
if (timeoutHandler()) {
|
||||
throw std::runtime_error("Reconnection required");
|
||||
}
|
||||
goto WRITE;
|
||||
case EINTR:
|
||||
break;
|
||||
default:
|
||||
if (retries++ > 4)
|
||||
throw std::runtime_error("Error in write");
|
||||
goto WRITE;
|
||||
}
|
||||
}
|
||||
idx += n;
|
||||
}
|
||||
|
||||
return data.size();
|
||||
}
|
||||
|
||||
void PlainConnection::close() {
|
||||
if (this->apSock < 0)
|
||||
return;
|
||||
|
||||
CSPOT_LOG(info, "Closing socket...");
|
||||
shutdown(this->apSock, SHUT_RDWR);
|
||||
#ifdef _WIN32
|
||||
closesocket(this->apSock);
|
||||
#else
|
||||
::close(this->apSock);
|
||||
#endif
|
||||
this->apSock = -1;
|
||||
}
|
||||
202
lib/spotify/cspot/src/PlaybackState.cpp
Normal file
202
lib/spotify/cspot/src/PlaybackState.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
#include "PlaybackState.h"
|
||||
|
||||
#include <string.h> // for strdup, memcpy, strcpy, strlen
|
||||
#include <cstdint> // for uint8_t
|
||||
#include <cstdlib> // for free, NULL, realloc, rand
|
||||
#include <cstring>
|
||||
#include <memory> // for shared_ptr
|
||||
#include <type_traits> // for remove_extent_t
|
||||
#include <utility> // for swap
|
||||
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "CSpotContext.h" // for Context::ConfigState, Context (ptr o...
|
||||
#include "ConstantParameters.h" // for protocolVersion, swVersion
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "NanoPBHelper.h" // for pbEncode, pbPutString
|
||||
#include "Packet.h" // for cspot
|
||||
#include "pb.h" // for pb_bytes_array_t, PB_BYTES_ARRAY_T_A...
|
||||
#include "pb_decode.h" // for pb_release
|
||||
#include "protobuf/spirc.pb.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
PlaybackState::PlaybackState(std::shared_ptr<cspot::Context> ctx) {
|
||||
this->ctx = ctx;
|
||||
innerFrame = {};
|
||||
remoteFrame = {};
|
||||
|
||||
// Prepare callbacks for decoding of remote frame track data
|
||||
remoteFrame.state.track.funcs.decode = &TrackReference::pbDecodeTrackList;
|
||||
remoteFrame.state.track.arg = &remoteTracks;
|
||||
|
||||
innerFrame.ident = strdup(ctx->config.deviceId.c_str());
|
||||
innerFrame.protocol_version = strdup(protocolVersion);
|
||||
|
||||
// Prepare default state
|
||||
innerFrame.state.has_position_ms = true;
|
||||
innerFrame.state.position_ms = 0;
|
||||
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusStop;
|
||||
innerFrame.state.has_status = true;
|
||||
|
||||
innerFrame.state.position_measured_at = 0;
|
||||
innerFrame.state.has_position_measured_at = true;
|
||||
|
||||
innerFrame.state.shuffle = false;
|
||||
innerFrame.state.has_shuffle = true;
|
||||
|
||||
innerFrame.state.repeat = false;
|
||||
innerFrame.state.has_repeat = true;
|
||||
|
||||
innerFrame.device_state.sw_version = strdup(swVersion);
|
||||
|
||||
innerFrame.device_state.is_active = false;
|
||||
innerFrame.device_state.has_is_active = true;
|
||||
|
||||
innerFrame.device_state.can_play = true;
|
||||
innerFrame.device_state.has_can_play = true;
|
||||
|
||||
innerFrame.device_state.volume = ctx->config.volume;
|
||||
innerFrame.device_state.has_volume = true;
|
||||
|
||||
innerFrame.device_state.name = strdup(ctx->config.deviceName.c_str());
|
||||
|
||||
// Prepare player's capabilities
|
||||
addCapability(CapabilityType_kCanBePlayer, 1);
|
||||
addCapability(CapabilityType_kDeviceType, 4);
|
||||
addCapability(CapabilityType_kGaiaEqConnectId, 1);
|
||||
addCapability(CapabilityType_kSupportsLogout, 0);
|
||||
addCapability(CapabilityType_kSupportsPlaylistV2, 1);
|
||||
addCapability(CapabilityType_kIsObservable, 1);
|
||||
addCapability(CapabilityType_kVolumeSteps, 64);
|
||||
addCapability(CapabilityType_kSupportedContexts, -1,
|
||||
std::vector<std::string>({"album", "playlist", "search",
|
||||
"inbox", "toplist", "starred",
|
||||
"publishedstarred", "track"}));
|
||||
addCapability(CapabilityType_kSupportedTypes, -1,
|
||||
std::vector<std::string>(
|
||||
{"audio/track", "audio/episode", "audio/episode+track"}));
|
||||
innerFrame.device_state.capabilities_count = 8;
|
||||
}
|
||||
|
||||
PlaybackState::~PlaybackState() {
|
||||
pb_release(Frame_fields, &innerFrame);
|
||||
pb_release(Frame_fields, &remoteFrame);
|
||||
}
|
||||
|
||||
void PlaybackState::setPlaybackState(const PlaybackState::State state) {
|
||||
switch (state) {
|
||||
case State::Loading:
|
||||
// Prepare the playback at position 0
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPause;
|
||||
innerFrame.state.position_ms = 0;
|
||||
innerFrame.state.position_measured_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
break;
|
||||
case State::Playing:
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPlay;
|
||||
innerFrame.state.position_measured_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
break;
|
||||
case State::Stopped:
|
||||
break;
|
||||
case State::Paused:
|
||||
// Update state and recalculate current song position
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPause;
|
||||
uint32_t diff = ctx->timeProvider->getSyncedTimestamp() -
|
||||
innerFrame.state.position_measured_at;
|
||||
this->updatePositionMs(innerFrame.state.position_ms + diff);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackState::syncWithRemote() {
|
||||
innerFrame.state.context_uri = (char*)realloc(
|
||||
innerFrame.state.context_uri, strlen(remoteFrame.state.context_uri) + 1);
|
||||
|
||||
strcpy(innerFrame.state.context_uri, remoteFrame.state.context_uri);
|
||||
|
||||
innerFrame.state.has_playing_track_index = true;
|
||||
innerFrame.state.playing_track_index = remoteFrame.state.playing_track_index;
|
||||
}
|
||||
|
||||
bool PlaybackState::isActive() {
|
||||
return innerFrame.device_state.is_active;
|
||||
}
|
||||
|
||||
void PlaybackState::setActive(bool isActive) {
|
||||
innerFrame.device_state.is_active = isActive;
|
||||
if (isActive) {
|
||||
innerFrame.device_state.became_active_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
innerFrame.device_state.has_became_active_at = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackState::updatePositionMs(uint32_t position) {
|
||||
innerFrame.state.position_ms = position;
|
||||
innerFrame.state.position_measured_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
}
|
||||
|
||||
void PlaybackState::setVolume(uint32_t volume) {
|
||||
innerFrame.device_state.volume = volume;
|
||||
ctx->config.volume = volume;
|
||||
}
|
||||
|
||||
bool PlaybackState::decodeRemoteFrame(std::vector<uint8_t>& data) {
|
||||
pb_release(Frame_fields, &remoteFrame);
|
||||
|
||||
remoteTracks.clear();
|
||||
|
||||
pbDecode(remoteFrame, Frame_fields, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlaybackState::encodeCurrentFrame(MessageType typ) {
|
||||
// Prepare current frame info
|
||||
innerFrame.version = 1;
|
||||
innerFrame.seq_nr = this->seqNum;
|
||||
innerFrame.typ = typ;
|
||||
innerFrame.state_update_id = ctx->timeProvider->getSyncedTimestamp();
|
||||
innerFrame.has_version = true;
|
||||
innerFrame.has_seq_nr = true;
|
||||
innerFrame.recipient_count = 0;
|
||||
innerFrame.has_state = true;
|
||||
innerFrame.has_device_state = true;
|
||||
innerFrame.has_typ = true;
|
||||
innerFrame.has_state_update_id = true;
|
||||
|
||||
this->seqNum += 1;
|
||||
|
||||
return pbEncode(Frame_fields, &innerFrame);
|
||||
}
|
||||
|
||||
// Wraps messy nanopb setters. @TODO: find a better way to handle this
|
||||
void PlaybackState::addCapability(CapabilityType typ, int intValue,
|
||||
std::vector<std::string> stringValue) {
|
||||
innerFrame.device_state.capabilities[capabilityIndex].has_typ = true;
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].typ = typ;
|
||||
|
||||
if (intValue != -1) {
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue[0] =
|
||||
intValue;
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count =
|
||||
1;
|
||||
} else {
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count =
|
||||
0;
|
||||
}
|
||||
|
||||
for (int x = 0; x < stringValue.size(); x++) {
|
||||
pbPutString(stringValue[x],
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex]
|
||||
.stringValue[x]);
|
||||
}
|
||||
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex]
|
||||
.stringValue_count = stringValue.size();
|
||||
|
||||
this->capabilityIndex += 1;
|
||||
}
|
||||
107
lib/spotify/cspot/src/Session.cpp
Normal file
107
lib/spotify/cspot/src/Session.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "Session.h"
|
||||
|
||||
#include <limits.h> // for CHAR_BIT
|
||||
#include <cstdint> // for uint8_t
|
||||
#include <functional> // for __base
|
||||
#include <memory> // for shared_ptr, unique_ptr, make_unique
|
||||
#include <random> // for default_random_engine, independent_bi...
|
||||
#include <type_traits> // for remove_extent_t
|
||||
#include <utility> // for move
|
||||
|
||||
#include "ApResolve.h" // for ApResolve, cspot
|
||||
#include "AuthChallenges.h" // for AuthChallenges
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "LoginBlob.h" // for LoginBlob
|
||||
#include "Packet.h" // for Packet
|
||||
#include "PlainConnection.h" // for PlainConnection, timeoutCallback
|
||||
#include "ShannonConnection.h" // for ShannonConnection
|
||||
|
||||
#include "NanoPBHelper.h" // for pbPutString, pbEncode, pbDecode
|
||||
#include "pb_decode.h"
|
||||
#include "protobuf/authentication.pb.h"
|
||||
|
||||
using random_bytes_engine =
|
||||
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
Session::Session() {
|
||||
this->challenges = std::make_unique<cspot::AuthChallenges>();
|
||||
}
|
||||
|
||||
Session::~Session() {}
|
||||
|
||||
void Session::connect(std::unique_ptr<cspot::PlainConnection> connection) {
|
||||
this->conn = std::move(connection);
|
||||
conn->timeoutHandler = [this]() {
|
||||
return this->triggerTimeout();
|
||||
};
|
||||
auto helloPacket = this->conn->sendPrefixPacket(
|
||||
{0x00, 0x04}, this->challenges->prepareClientHello());
|
||||
auto apResponse = this->conn->recvPacket();
|
||||
CSPOT_LOG(info, "Received APHello response");
|
||||
|
||||
auto solvedHello = this->challenges->solveApHello(helloPacket, apResponse);
|
||||
|
||||
conn->sendPrefixPacket({}, solvedHello);
|
||||
CSPOT_LOG(debug, "Received shannon keys");
|
||||
|
||||
// Generates the public and priv key
|
||||
this->shanConn = std::make_shared<ShannonConnection>();
|
||||
|
||||
// Init shanno-encrypted connection
|
||||
this->shanConn->wrapConnection(this->conn, challenges->shanSendKey,
|
||||
challenges->shanRecvKey);
|
||||
}
|
||||
|
||||
void Session::connectWithRandomAp() {
|
||||
auto apResolver = std::make_unique<ApResolve>("");
|
||||
auto conn = std::make_unique<cspot::PlainConnection>();
|
||||
conn->timeoutHandler = [this]() {
|
||||
return this->triggerTimeout();
|
||||
};
|
||||
|
||||
auto apAddr = apResolver->fetchFirstApAddress();
|
||||
|
||||
CSPOT_LOG(debug, "Connecting with AP <%s>", apAddr.c_str());
|
||||
conn->connect(apAddr);
|
||||
|
||||
this->connect(std::move(conn));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob) {
|
||||
// save auth blob for reconnection purposes
|
||||
authBlob = blob;
|
||||
// prepare authentication request proto
|
||||
auto data = challenges->prepareAuthPacket(blob->authData, blob->authType,
|
||||
deviceId, blob->username);
|
||||
|
||||
// Send login request
|
||||
this->shanConn->sendPacket(LOGIN_REQUEST_COMMAND, data);
|
||||
|
||||
auto packet = this->shanConn->recvPacket();
|
||||
switch (packet.command) {
|
||||
case AUTH_SUCCESSFUL_COMMAND: {
|
||||
APWelcome welcome;
|
||||
CSPOT_LOG(debug, "Authorization successful");
|
||||
pbDecode(welcome, APWelcome_fields, packet.data);
|
||||
return std::vector<uint8_t>(welcome.reusable_auth_credentials.bytes,
|
||||
welcome.reusable_auth_credentials.bytes +
|
||||
welcome.reusable_auth_credentials.size);
|
||||
break;
|
||||
}
|
||||
case AUTH_DECLINED_COMMAND: {
|
||||
CSPOT_LOG(error, "Authorization declined");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
CSPOT_LOG(error, "Unknown auth fail code %d", packet.command);
|
||||
}
|
||||
|
||||
return std::vector<uint8_t>(0);
|
||||
}
|
||||
|
||||
void Session::close() {
|
||||
this->conn->close();
|
||||
}
|
||||
393
lib/spotify/cspot/src/Shannon.cpp
Normal file
393
lib/spotify/cspot/src/Shannon.cpp
Normal file
@@ -0,0 +1,393 @@
|
||||
#include "Shannon.h"
|
||||
|
||||
#include <limits.h> // for CHAR_BIT
|
||||
#include <stddef.h> // for size_t
|
||||
|
||||
using std::size_t;
|
||||
|
||||
static inline uint32_t rotl(uint32_t n, unsigned int c) {
|
||||
const unsigned int mask =
|
||||
(CHAR_BIT * sizeof(n) - 1); // assumes width is a power of 2.
|
||||
// assert ( (c<=mask) &&"rotate by type width or more");
|
||||
c &= mask;
|
||||
return (n << c) | (n >> ((-c) & mask));
|
||||
}
|
||||
|
||||
static inline uint32_t rotr(uint32_t n, unsigned int c) {
|
||||
const unsigned int mask =
|
||||
(CHAR_BIT * sizeof(n) - 1); // assumes width is a power of 2.
|
||||
// assert ( (c<=mask) &&"rotate by type width or more");
|
||||
c &= mask;
|
||||
return (n >> c) | (n << ((-c) & mask));
|
||||
}
|
||||
|
||||
uint32_t Shannon::sbox1(uint32_t w) {
|
||||
w ^= rotl(w, 5) | rotl(w, 7);
|
||||
w ^= rotl(w, 19) | rotl(w, 22);
|
||||
return w;
|
||||
}
|
||||
|
||||
uint32_t Shannon::sbox2(uint32_t w) {
|
||||
w ^= rotl(w, 7) | rotl(w, 22);
|
||||
w ^= rotl(w, 5) | rotl(w, 19);
|
||||
return w;
|
||||
}
|
||||
|
||||
void Shannon::cycle() {
|
||||
uint32_t t;
|
||||
int i;
|
||||
|
||||
/* nonlinear feedback function */
|
||||
t = this->R[12] ^ this->R[13] ^ this->konst;
|
||||
t = Shannon::sbox1(t) ^ rotl(this->R[0], 1);
|
||||
/* shift register */
|
||||
for (i = 1; i < N; ++i)
|
||||
this->R[i - 1] = this->R[i];
|
||||
this->R[N - 1] = t;
|
||||
t = Shannon::sbox2(this->R[2] ^ this->R[15]);
|
||||
this->R[0] ^= t;
|
||||
this->sbuf = t ^ this->R[8] ^ this->R[12];
|
||||
}
|
||||
|
||||
void Shannon::crcfunc(uint32_t i) {
|
||||
uint32_t t;
|
||||
int j;
|
||||
|
||||
/* Accumulate CRC of input */
|
||||
t = this->CRC[0] ^ this->CRC[2] ^ this->CRC[15] ^ i;
|
||||
for (j = 1; j < N; ++j)
|
||||
this->CRC[j - 1] = this->CRC[j];
|
||||
this->CRC[N - 1] = t;
|
||||
}
|
||||
|
||||
void Shannon::macfunc(uint32_t i) {
|
||||
this->crcfunc(i);
|
||||
this->R[KEYP] ^= i;
|
||||
}
|
||||
|
||||
void Shannon::initState() {
|
||||
int i;
|
||||
|
||||
/* Register initialised to Fibonacci numbers; Counter zeroed. */
|
||||
this->R[0] = 1;
|
||||
this->R[1] = 1;
|
||||
for (i = 2; i < N; ++i)
|
||||
this->R[i] = this->R[i - 1] + this->R[i - 2];
|
||||
this->konst = Shannon::INITKONST;
|
||||
}
|
||||
void Shannon::saveState() {
|
||||
int i;
|
||||
for (i = 0; i < Shannon::N; ++i)
|
||||
this->initR[i] = this->R[i];
|
||||
}
|
||||
void Shannon::reloadState() {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < Shannon::N; ++i)
|
||||
this->R[i] = this->initR[i];
|
||||
}
|
||||
void Shannon::genkonst() {
|
||||
this->konst = this->R[0];
|
||||
}
|
||||
void Shannon::diffuse() {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < Shannon::FOLD; ++i)
|
||||
this->cycle();
|
||||
}
|
||||
|
||||
#define Byte(x, i) ((uint32_t)(((x) >> (8 * (i))) & 0xFF))
|
||||
#define BYTE2WORD(b) \
|
||||
((((uint32_t)(b)[3] & 0xFF) << 24) | (((uint32_t)(b)[2] & 0xFF) << 16) | \
|
||||
(((uint32_t)(b)[1] & 0xFF) << 8) | (((uint32_t)(b)[0] & 0xFF)))
|
||||
#define WORD2BYTE(w, b) \
|
||||
{ \
|
||||
(b)[3] = Byte(w, 3); \
|
||||
(b)[2] = Byte(w, 2); \
|
||||
(b)[1] = Byte(w, 1); \
|
||||
(b)[0] = Byte(w, 0); \
|
||||
}
|
||||
#define XORWORD(w, b) \
|
||||
{ \
|
||||
(b)[3] ^= Byte(w, 3); \
|
||||
(b)[2] ^= Byte(w, 2); \
|
||||
(b)[1] ^= Byte(w, 1); \
|
||||
(b)[0] ^= Byte(w, 0); \
|
||||
}
|
||||
|
||||
#define XORWORD(w, b) \
|
||||
{ \
|
||||
(b)[3] ^= Byte(w, 3); \
|
||||
(b)[2] ^= Byte(w, 2); \
|
||||
(b)[1] ^= Byte(w, 1); \
|
||||
(b)[0] ^= Byte(w, 0); \
|
||||
}
|
||||
|
||||
/* Load key material into the register
|
||||
*/
|
||||
#define ADDKEY(k) this->R[KEYP] ^= (k);
|
||||
|
||||
void Shannon::loadKey(const std::vector<uint8_t>& key) {
|
||||
int i, j;
|
||||
uint32_t k;
|
||||
uint8_t xtra[4];
|
||||
size_t keylen = key.size();
|
||||
/* start folding in key */
|
||||
for (i = 0; i < (keylen & ~0x3); i += 4) {
|
||||
k = BYTE2WORD(&key[i]);
|
||||
ADDKEY(k);
|
||||
this->cycle();
|
||||
}
|
||||
|
||||
/* if there were any extra key bytes, zero pad to a word */
|
||||
if (i < keylen) {
|
||||
for (j = 0 /* i unchanged */; i < keylen; ++i)
|
||||
xtra[j++] = key[i];
|
||||
for (/* j unchanged */; j < 4; ++j)
|
||||
xtra[j] = 0;
|
||||
k = BYTE2WORD(xtra);
|
||||
ADDKEY(k);
|
||||
this->cycle();
|
||||
}
|
||||
|
||||
/* also fold in the length of the key */
|
||||
ADDKEY(keylen);
|
||||
this->cycle();
|
||||
|
||||
/* save a copy of the register */
|
||||
for (i = 0; i < N; ++i)
|
||||
this->CRC[i] = this->R[i];
|
||||
|
||||
/* now diffuse */
|
||||
this->diffuse();
|
||||
|
||||
/* now xor the copy back -- makes key loading irreversible */
|
||||
for (i = 0; i < N; ++i)
|
||||
this->R[i] ^= this->CRC[i];
|
||||
}
|
||||
|
||||
void Shannon::key(const std::vector<uint8_t>& key) {
|
||||
this->initState();
|
||||
this->loadKey(key);
|
||||
this->genkonst(); /* in case we proceed to stream generation */
|
||||
this->saveState();
|
||||
this->nbuf = 0;
|
||||
}
|
||||
|
||||
void Shannon::nonce(const std::vector<uint8_t>& nonce) {
|
||||
this->reloadState();
|
||||
this->konst = Shannon::INITKONST;
|
||||
this->loadKey(nonce);
|
||||
this->genkonst();
|
||||
this->nbuf = 0;
|
||||
}
|
||||
|
||||
void Shannon::stream(std::vector<uint8_t>& bufVec) {
|
||||
uint8_t* endbuf;
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t* buf = bufVec.data();
|
||||
/* handle any previously buffered bytes */
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
*buf++ ^= this->sbuf & 0xFF;
|
||||
this->sbuf >>= 8;
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
|
||||
/* handle whole words */
|
||||
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
|
||||
while (buf < endbuf) {
|
||||
this->cycle();
|
||||
XORWORD(this->sbuf, buf);
|
||||
buf += 4;
|
||||
}
|
||||
|
||||
/* handle any trailing bytes */
|
||||
nbytes &= 0x03;
|
||||
if (nbytes != 0) {
|
||||
this->cycle();
|
||||
this->nbuf = 32;
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
*buf++ ^= this->sbuf & 0xFF;
|
||||
this->sbuf >>= 8;
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Shannon::maconly(std::vector<uint8_t>& bufVec) {
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t* buf = bufVec.data();
|
||||
|
||||
uint8_t* endbuf;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
if (this->nbuf != 0) {
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
this->mbuf ^= (*buf++) << (32 - this->nbuf);
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
if (this->nbuf != 0) /* not a whole word yet */
|
||||
return;
|
||||
/* LFSR already cycled */
|
||||
this->macfunc(this->mbuf);
|
||||
}
|
||||
|
||||
/* handle whole words */
|
||||
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
|
||||
while (buf < endbuf) {
|
||||
this->cycle();
|
||||
this->macfunc(BYTE2WORD(buf));
|
||||
buf += 4;
|
||||
}
|
||||
|
||||
/* handle any trailing bytes */
|
||||
nbytes &= 0x03;
|
||||
if (nbytes != 0) {
|
||||
this->cycle();
|
||||
this->mbuf = 0;
|
||||
this->nbuf = 32;
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
this->mbuf ^= (*buf++) << (32 - this->nbuf);
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Shannon::encrypt(std::vector<uint8_t>& bufVec) {
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t* buf = bufVec.data();
|
||||
uint8_t* endbuf;
|
||||
uint32_t t = 0;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
if (this->nbuf != 0) {
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
this->mbuf ^= *buf << (32 - this->nbuf);
|
||||
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
|
||||
++buf;
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
if (this->nbuf != 0) /* not a whole word yet */
|
||||
return;
|
||||
/* LFSR already cycled */
|
||||
this->macfunc(this->mbuf);
|
||||
}
|
||||
|
||||
/* handle whole words */
|
||||
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
|
||||
while (buf < endbuf) {
|
||||
this->cycle();
|
||||
t = BYTE2WORD(buf);
|
||||
this->macfunc(t);
|
||||
t ^= this->sbuf;
|
||||
WORD2BYTE(t, buf);
|
||||
buf += 4;
|
||||
}
|
||||
|
||||
/* handle any trailing bytes */
|
||||
nbytes &= 0x03;
|
||||
if (nbytes != 0) {
|
||||
this->cycle();
|
||||
this->mbuf = 0;
|
||||
this->nbuf = 32;
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
this->mbuf ^= *buf << (32 - this->nbuf);
|
||||
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
|
||||
++buf;
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Shannon::decrypt(std::vector<uint8_t>& bufVec) {
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t* buf = bufVec.data();
|
||||
uint8_t* endbuf;
|
||||
uint32_t t = 0;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
if (this->nbuf != 0) {
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
|
||||
this->mbuf ^= *buf << (32 - this->nbuf);
|
||||
++buf;
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
if (this->nbuf != 0) /* not a whole word yet */
|
||||
return;
|
||||
/* LFSR already cycled */
|
||||
this->macfunc(this->mbuf);
|
||||
}
|
||||
|
||||
/* handle whole words */
|
||||
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
|
||||
while (buf < endbuf) {
|
||||
this->cycle();
|
||||
t = BYTE2WORD(buf) ^ this->sbuf;
|
||||
this->macfunc(t);
|
||||
WORD2BYTE(t, buf);
|
||||
buf += 4;
|
||||
}
|
||||
|
||||
/* handle any trailing bytes */
|
||||
nbytes &= 0x03;
|
||||
if (nbytes != 0) {
|
||||
this->cycle();
|
||||
this->mbuf = 0;
|
||||
this->nbuf = 32;
|
||||
while (this->nbuf != 0 && nbytes != 0) {
|
||||
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
|
||||
this->mbuf ^= *buf << (32 - this->nbuf);
|
||||
++buf;
|
||||
this->nbuf -= 8;
|
||||
--nbytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Shannon::finish(std::vector<uint8_t>& bufVec) {
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t* buf = bufVec.data();
|
||||
int i;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
if (this->nbuf != 0) {
|
||||
/* LFSR already cycled */
|
||||
this->macfunc(this->mbuf);
|
||||
}
|
||||
|
||||
/* perturb the MAC to mark end of input.
|
||||
* Note that only the stream register is updated, not the CRC. This is an
|
||||
* action that can't be duplicated by passing in plaintext, hence
|
||||
* defeating any kind of extension attack.
|
||||
*/
|
||||
this->cycle();
|
||||
ADDKEY(INITKONST ^ (this->nbuf << 3));
|
||||
this->nbuf = 0;
|
||||
|
||||
/* now add the CRC to the stream register and diffuse it */
|
||||
for (i = 0; i < N; ++i)
|
||||
this->R[i] ^= this->CRC[i];
|
||||
this->diffuse();
|
||||
|
||||
/* produce output from the stream buffer */
|
||||
while (nbytes > 0) {
|
||||
this->cycle();
|
||||
if (nbytes >= 4) {
|
||||
WORD2BYTE(this->sbuf, buf);
|
||||
nbytes -= 4;
|
||||
buf += 4;
|
||||
} else {
|
||||
for (i = 0; i < nbytes; ++i)
|
||||
buf[i] = Byte(this->sbuf, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
107
lib/spotify/cspot/src/ShannonConnection.cpp
Normal file
107
lib/spotify/cspot/src/ShannonConnection.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "ShannonConnection.h"
|
||||
|
||||
#include <type_traits> // for remove_extent_t
|
||||
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "Packet.h" // for Packet, cspot
|
||||
#include "PlainConnection.h" // for PlainConnection
|
||||
#include "Shannon.h" // for Shannon
|
||||
#include "Utils.h" // for pack, extract
|
||||
#ifndef _WIN32
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
ShannonConnection::ShannonConnection() {}
|
||||
|
||||
ShannonConnection::~ShannonConnection() {}
|
||||
|
||||
void ShannonConnection::wrapConnection(
|
||||
std::shared_ptr<cspot::PlainConnection> conn, std::vector<uint8_t>& sendKey,
|
||||
std::vector<uint8_t>& recvKey) {
|
||||
this->conn = conn;
|
||||
|
||||
this->sendCipher = std::make_unique<Shannon>();
|
||||
this->recvCipher = std::make_unique<Shannon>();
|
||||
|
||||
// Set keys
|
||||
this->sendCipher->key(sendKey);
|
||||
this->recvCipher->key(recvKey);
|
||||
|
||||
// Set initial nonce
|
||||
this->sendCipher->nonce(pack<uint32_t>(htonl(0)));
|
||||
this->recvCipher->nonce(pack<uint32_t>(htonl(0)));
|
||||
}
|
||||
|
||||
void ShannonConnection::sendPacket(uint8_t cmd, std::vector<uint8_t>& data) {
|
||||
std::scoped_lock lock(this->writeMutex);
|
||||
auto rawPacket = this->cipherPacket(cmd, data);
|
||||
|
||||
// Shannon encrypt the packet and write it to sock
|
||||
this->sendCipher->encrypt(rawPacket);
|
||||
this->conn->writeBlock(rawPacket);
|
||||
|
||||
// Generate mac
|
||||
std::vector<uint8_t> mac(MAC_SIZE);
|
||||
this->sendCipher->finish(mac);
|
||||
|
||||
// Update the nonce
|
||||
this->sendNonce += 1;
|
||||
this->sendCipher->nonce(pack<uint32_t>(htonl(this->sendNonce)));
|
||||
|
||||
// Write the mac to sock
|
||||
this->conn->writeBlock(mac);
|
||||
}
|
||||
|
||||
cspot::Packet ShannonConnection::recvPacket() {
|
||||
std::scoped_lock lock(this->readMutex);
|
||||
|
||||
std::vector<uint8_t> data(3);
|
||||
// Receive 3 bytes, cmd + int16 size
|
||||
this->conn->readBlock(data.data(), 3);
|
||||
this->recvCipher->decrypt(data);
|
||||
|
||||
auto readSize = ntohs(extract<uint16_t>(data, 1));
|
||||
auto packetData = std::vector<uint8_t>(readSize);
|
||||
|
||||
// Read and decode if the packet has an actual body
|
||||
if (readSize > 0) {
|
||||
this->conn->readBlock(packetData.data(), readSize);
|
||||
this->recvCipher->decrypt(packetData);
|
||||
}
|
||||
|
||||
// Read mac
|
||||
std::vector<uint8_t> mac(MAC_SIZE);
|
||||
this->conn->readBlock(mac.data(), MAC_SIZE);
|
||||
|
||||
// Generate mac
|
||||
std::vector<uint8_t> mac2(MAC_SIZE);
|
||||
this->recvCipher->finish(mac2);
|
||||
|
||||
if (mac != mac2) {
|
||||
CSPOT_LOG(error, "Shannon read: Mac doesn't match");
|
||||
}
|
||||
|
||||
// Update the nonce
|
||||
this->recvNonce += 1;
|
||||
this->recvCipher->nonce(pack<uint32_t>(htonl(this->recvNonce)));
|
||||
uint8_t cmd = 0;
|
||||
if (data.size() > 0) {
|
||||
cmd = data[0];
|
||||
}
|
||||
// data[0] == cmd
|
||||
return Packet{cmd, packetData};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ShannonConnection::cipherPacket(
|
||||
uint8_t cmd, std::vector<uint8_t>& data) {
|
||||
// Generate packet structure, [Command] [Size] [Raw data]
|
||||
auto sizeRaw = pack<uint16_t>(htons(uint16_t(data.size())));
|
||||
|
||||
sizeRaw.insert(sizeRaw.begin(), cmd);
|
||||
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
|
||||
|
||||
return sizeRaw;
|
||||
}
|
||||
310
lib/spotify/cspot/src/SpircHandler.cpp
Normal file
310
lib/spotify/cspot/src/SpircHandler.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
#include "SpircHandler.h"
|
||||
|
||||
#include <cstdint> // for uint8_t
|
||||
#include <memory> // for shared_ptr, make_unique, unique_ptr
|
||||
#include <type_traits> // for remove_extent_t
|
||||
#include <utility> // for move
|
||||
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "CSpotContext.h" // for Context::ConfigState, Context (ptr only)
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "MercurySession.h" // for MercurySession, MercurySession::Response
|
||||
#include "NanoPBHelper.h" // for pbDecode
|
||||
#include "Packet.h" // for cspot
|
||||
#include "PlaybackState.h" // for PlaybackState, PlaybackState::State
|
||||
#include "TrackPlayer.h" // for TrackPlayer
|
||||
#include "TrackQueue.h"
|
||||
#include "TrackReference.h" // for TrackReference
|
||||
#include "Utils.h" // for stringHexToBytes
|
||||
#include "pb_decode.h" // for pb_release
|
||||
#include "protobuf/spirc.pb.h" // for Frame, State, Frame_fields, MessageTy...
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
SpircHandler::SpircHandler(std::shared_ptr<cspot::Context> ctx) {
|
||||
this->playbackState = std::make_shared<PlaybackState>(ctx);
|
||||
this->trackQueue = std::make_shared<cspot::TrackQueue>(ctx, playbackState);
|
||||
|
||||
auto EOFCallback = [this]() {
|
||||
if (trackQueue->isFinished()) {
|
||||
sendEvent(EventType::DEPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
auto trackLoadedCallback = [this](std::shared_ptr<QueuedTrack> track,
|
||||
bool paused = false) {
|
||||
playbackState->setPlaybackState(paused ? PlaybackState::State::Paused
|
||||
: PlaybackState::State::Playing);
|
||||
playbackState->updatePositionMs(track->requestedPosition);
|
||||
|
||||
this->notify();
|
||||
|
||||
// Send playback start event, pause/unpause per request
|
||||
sendEvent(EventType::PLAYBACK_START, (int)track->requestedPosition);
|
||||
sendEvent(EventType::PLAY_PAUSE, paused);
|
||||
};
|
||||
|
||||
this->ctx = ctx;
|
||||
this->trackPlayer = std::make_shared<TrackPlayer>(
|
||||
ctx, trackQueue, EOFCallback, trackLoadedCallback);
|
||||
|
||||
// Subscribe to mercury on session ready
|
||||
ctx->session->setConnectedHandler([this]() { this->subscribeToMercury(); });
|
||||
}
|
||||
|
||||
void SpircHandler::subscribeToMercury() {
|
||||
auto responseLambda = [this](MercurySession::Response& res) {
|
||||
if (res.fail)
|
||||
return;
|
||||
|
||||
sendCmd(MessageType_kMessageTypeHello);
|
||||
CSPOT_LOG(debug, "Sent kMessageTypeHello!");
|
||||
|
||||
// Assign country code
|
||||
this->ctx->config.countryCode = this->ctx->session->getCountryCode();
|
||||
};
|
||||
auto subscriptionLambda = [this](MercurySession::Response& res) {
|
||||
if (res.fail)
|
||||
return;
|
||||
CSPOT_LOG(debug, "Received subscription response");
|
||||
|
||||
this->handleFrame(res.parts[0]);
|
||||
};
|
||||
|
||||
ctx->session->executeSubscription(
|
||||
MercurySession::RequestType::SUB,
|
||||
"hm://remote/user/" + ctx->config.username + "/", responseLambda,
|
||||
subscriptionLambda);
|
||||
}
|
||||
|
||||
void SpircHandler::loadTrackFromURI(const std::string& uri) {}
|
||||
|
||||
void SpircHandler::notifyAudioEnded() {
|
||||
playbackState->updatePositionMs(0);
|
||||
notify();
|
||||
trackPlayer->resetState(true);
|
||||
}
|
||||
|
||||
void SpircHandler::notifyAudioReachedPlayback() {
|
||||
int offset = 0;
|
||||
|
||||
// get HEAD track
|
||||
auto currentTrack = trackQueue->consumeTrack(nullptr, offset);
|
||||
|
||||
// Do not execute when meta is already updated
|
||||
if (trackQueue->notifyPending) {
|
||||
trackQueue->notifyPending = false;
|
||||
|
||||
playbackState->updatePositionMs(currentTrack->requestedPosition);
|
||||
|
||||
// Reset position in queued track
|
||||
currentTrack->requestedPosition = 0;
|
||||
} else {
|
||||
trackQueue->skipTrack(TrackQueue::SkipDirection::NEXT, false);
|
||||
playbackState->updatePositionMs(0);
|
||||
|
||||
// we moved to next track, re-acquire currentTrack again
|
||||
currentTrack = trackQueue->consumeTrack(nullptr, offset);
|
||||
}
|
||||
|
||||
this->notify();
|
||||
|
||||
sendEvent(EventType::TRACK_INFO, currentTrack->trackInfo);
|
||||
}
|
||||
|
||||
void SpircHandler::updatePositionMs(uint32_t position) {
|
||||
playbackState->updatePositionMs(position);
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircHandler::disconnect() {
|
||||
this->trackQueue->stopTask();
|
||||
this->trackPlayer->stop();
|
||||
this->ctx->session->disconnect();
|
||||
}
|
||||
|
||||
void SpircHandler::handleFrame(std::vector<uint8_t>& data) {
|
||||
// Decode received spirc frame
|
||||
playbackState->decodeRemoteFrame(data);
|
||||
|
||||
switch (playbackState->remoteFrame.typ) {
|
||||
case MessageType_kMessageTypeNotify: {
|
||||
CSPOT_LOG(debug, "Notify frame");
|
||||
|
||||
// Pause the playback if another player took control
|
||||
if (playbackState->isActive() &&
|
||||
playbackState->remoteFrame.device_state.is_active) {
|
||||
CSPOT_LOG(debug, "Another player took control, pausing playback");
|
||||
playbackState->setActive(false);
|
||||
|
||||
this->trackPlayer->stop();
|
||||
sendEvent(EventType::DISC);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeSeek: {
|
||||
this->trackPlayer->seekMs(playbackState->remoteFrame.position);
|
||||
|
||||
playbackState->updatePositionMs(playbackState->remoteFrame.position);
|
||||
|
||||
notify();
|
||||
|
||||
sendEvent(EventType::SEEK, (int)playbackState->remoteFrame.position);
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeVolume:
|
||||
playbackState->setVolume(playbackState->remoteFrame.volume);
|
||||
this->notify();
|
||||
sendEvent(EventType::VOLUME, (int)playbackState->remoteFrame.volume);
|
||||
break;
|
||||
case MessageType_kMessageTypePause:
|
||||
setPause(true);
|
||||
break;
|
||||
case MessageType_kMessageTypePlay:
|
||||
setPause(false);
|
||||
break;
|
||||
case MessageType_kMessageTypeNext:
|
||||
if (nextSong()) {
|
||||
sendEvent(EventType::NEXT);
|
||||
}
|
||||
break;
|
||||
case MessageType_kMessageTypePrev:
|
||||
if (previousSong()) {
|
||||
sendEvent(EventType::PREV);
|
||||
}
|
||||
break;
|
||||
case MessageType_kMessageTypeLoad: {
|
||||
this->trackPlayer->start();
|
||||
|
||||
CSPOT_LOG(debug, "Load frame %d!", playbackState->remoteTracks.size());
|
||||
|
||||
if (playbackState->remoteTracks.size() == 0) {
|
||||
CSPOT_LOG(info, "No tracks in frame, stopping playback");
|
||||
break;
|
||||
}
|
||||
|
||||
playbackState->setActive(true);
|
||||
|
||||
playbackState->updatePositionMs(playbackState->remoteFrame.position);
|
||||
playbackState->setPlaybackState(PlaybackState::State::Playing);
|
||||
|
||||
playbackState->syncWithRemote();
|
||||
|
||||
// Update track list in case we have a new one
|
||||
trackQueue->updateTracks(playbackState->remoteFrame.state.position_ms,
|
||||
true);
|
||||
|
||||
this->notify();
|
||||
|
||||
// Stop the current track, if any
|
||||
trackPlayer->resetState();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeReplace: {
|
||||
CSPOT_LOG(debug, "Got replace frame %d",
|
||||
playbackState->remoteTracks.size());
|
||||
playbackState->syncWithRemote();
|
||||
|
||||
// 1st track is the current one, but update the position
|
||||
bool cleared = trackQueue->updateTracks(
|
||||
playbackState->remoteFrame.state.position_ms +
|
||||
ctx->timeProvider->getSyncedTimestamp() -
|
||||
playbackState->innerFrame.state.position_measured_at,
|
||||
false);
|
||||
|
||||
this->notify();
|
||||
|
||||
// need to re-load all if streaming track is completed
|
||||
if (cleared) {
|
||||
sendEvent(EventType::FLUSH);
|
||||
trackPlayer->resetState();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeShuffle: {
|
||||
CSPOT_LOG(debug, "Got shuffle frame");
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeRepeat: {
|
||||
CSPOT_LOG(debug, "Got repeat frame");
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SpircHandler::setRemoteVolume(int volume) {
|
||||
playbackState->setVolume(volume);
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircHandler::notify() {
|
||||
this->sendCmd(MessageType_kMessageTypeNotify);
|
||||
}
|
||||
|
||||
bool SpircHandler::skipSong(TrackQueue::SkipDirection dir) {
|
||||
bool skipped = trackQueue->skipTrack(dir);
|
||||
|
||||
// Reset track state
|
||||
trackPlayer->resetState(!skipped);
|
||||
|
||||
// send NEXT or PREV event only when successful
|
||||
return skipped;
|
||||
}
|
||||
|
||||
bool SpircHandler::nextSong() {
|
||||
return skipSong(TrackQueue::SkipDirection::NEXT);
|
||||
}
|
||||
|
||||
bool SpircHandler::previousSong() {
|
||||
return skipSong(TrackQueue::SkipDirection::PREV);
|
||||
}
|
||||
|
||||
std::shared_ptr<TrackPlayer> SpircHandler::getTrackPlayer() {
|
||||
return this->trackPlayer;
|
||||
}
|
||||
|
||||
void SpircHandler::sendCmd(MessageType typ) {
|
||||
// Serialize current player state
|
||||
auto encodedFrame = playbackState->encodeCurrentFrame(typ);
|
||||
|
||||
auto responseLambda = [=](MercurySession::Response& res) {
|
||||
};
|
||||
auto parts = MercurySession::DataParts({encodedFrame});
|
||||
ctx->session->execute(MercurySession::RequestType::SEND,
|
||||
"hm://remote/user/" + ctx->config.username + "/",
|
||||
responseLambda, parts);
|
||||
}
|
||||
void SpircHandler::setEventHandler(EventHandler handler) {
|
||||
this->eventHandler = handler;
|
||||
}
|
||||
|
||||
void SpircHandler::setPause(bool isPaused) {
|
||||
if (isPaused) {
|
||||
CSPOT_LOG(debug, "External pause command");
|
||||
playbackState->setPlaybackState(PlaybackState::State::Paused);
|
||||
} else {
|
||||
CSPOT_LOG(debug, "External play command");
|
||||
|
||||
playbackState->setPlaybackState(PlaybackState::State::Playing);
|
||||
}
|
||||
notify();
|
||||
sendEvent(EventType::PLAY_PAUSE, isPaused);
|
||||
}
|
||||
|
||||
void SpircHandler::sendEvent(EventType type) {
|
||||
auto event = std::make_unique<Event>();
|
||||
event->eventType = type;
|
||||
event->data = {};
|
||||
eventHandler(std::move(event));
|
||||
}
|
||||
|
||||
void SpircHandler::sendEvent(EventType type, EventData data) {
|
||||
auto event = std::make_unique<Event>();
|
||||
event->eventType = type;
|
||||
event->data = data;
|
||||
eventHandler(std::move(event));
|
||||
}
|
||||
24
lib/spotify/cspot/src/TimeProvider.cpp
Normal file
24
lib/spotify/cspot/src/TimeProvider.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "TimeProvider.h"
|
||||
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "Utils.h" // for extract, getCurrentTimestamp
|
||||
#ifndef _WIN32
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
TimeProvider::TimeProvider() {}
|
||||
|
||||
void TimeProvider::syncWithPingPacket(const std::vector<uint8_t>& pongPacket) {
|
||||
CSPOT_LOG(debug, "Time synced with spotify servers");
|
||||
// Spotify's timestamp is in seconds since unix time - convert to millis.
|
||||
uint64_t remoteTimestamp =
|
||||
((uint64_t)ntohl(extract<uint32_t>(pongPacket, 0))) * 1000;
|
||||
this->timestampDiff = remoteTimestamp - getCurrentTimestamp();
|
||||
}
|
||||
|
||||
unsigned long long TimeProvider::getSyncedTimestamp() {
|
||||
return getCurrentTimestamp() + this->timestampDiff;
|
||||
}
|
||||
313
lib/spotify/cspot/src/TrackPlayer.cpp
Normal file
313
lib/spotify/cspot/src/TrackPlayer.cpp
Normal file
@@ -0,0 +1,313 @@
|
||||
#include "TrackPlayer.h"
|
||||
|
||||
#include <mutex> // for mutex, scoped_lock
|
||||
#include <string> // for string
|
||||
#include <type_traits> // for remove_extent_t
|
||||
#include <vector> // for vector, vector<>::value_type
|
||||
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "BellUtils.h" // for BELL_SLEEP_MS
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "Packet.h" // for cspot
|
||||
#include "TrackQueue.h" // for CDNTrackStream, CDNTrackStream::TrackInfo
|
||||
#include "WrappedSemaphore.h" // for WrappedSemaphore
|
||||
|
||||
#ifdef BELL_VORBIS_FLOAT
|
||||
#define VORBIS_SEEK(file, position) \
|
||||
(ov_time_seek(file, (double)position / 1000))
|
||||
#define VORBIS_READ(file, buffer, bufferSize, section) \
|
||||
(ov_read(file, buffer, bufferSize, 0, 2, 1, section))
|
||||
#else
|
||||
#define VORBIS_SEEK(file, position) (ov_time_seek(file, position))
|
||||
#define VORBIS_READ(file, buffer, bufferSize, section) \
|
||||
(ov_read(file, buffer, bufferSize, section))
|
||||
#endif
|
||||
|
||||
namespace cspot {
|
||||
struct Context;
|
||||
struct TrackReference;
|
||||
} // namespace cspot
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
static size_t vorbisReadCb(void* ptr, size_t size, size_t nmemb,
|
||||
TrackPlayer* self) {
|
||||
return self->_vorbisRead(ptr, size, nmemb);
|
||||
}
|
||||
|
||||
static int vorbisCloseCb(TrackPlayer* self) {
|
||||
return self->_vorbisClose();
|
||||
}
|
||||
|
||||
static int vorbisSeekCb(TrackPlayer* self, int64_t offset, int whence) {
|
||||
|
||||
return self->_vorbisSeek(offset, whence);
|
||||
}
|
||||
|
||||
static long vorbisTellCb(TrackPlayer* self) {
|
||||
return self->_vorbisTell();
|
||||
}
|
||||
|
||||
TrackPlayer::TrackPlayer(std::shared_ptr<cspot::Context> ctx,
|
||||
std::shared_ptr<cspot::TrackQueue> trackQueue,
|
||||
EOFCallback eof, TrackLoadedCallback trackLoaded)
|
||||
: bell::Task("cspot_player", 48 * 1024, 5, 1) {
|
||||
this->ctx = ctx;
|
||||
this->eofCallback = eof;
|
||||
this->trackLoaded = trackLoaded;
|
||||
this->trackQueue = trackQueue;
|
||||
this->playbackSemaphore = std::make_unique<bell::WrappedSemaphore>(5);
|
||||
|
||||
// Initialize vorbis callbacks
|
||||
vorbisFile = {};
|
||||
vorbisCallbacks = {
|
||||
(decltype(ov_callbacks::read_func))&vorbisReadCb,
|
||||
(decltype(ov_callbacks::seek_func))&vorbisSeekCb,
|
||||
(decltype(ov_callbacks::close_func))&vorbisCloseCb,
|
||||
(decltype(ov_callbacks::tell_func))&vorbisTellCb,
|
||||
};
|
||||
}
|
||||
|
||||
TrackPlayer::~TrackPlayer() {
|
||||
isRunning = false;
|
||||
resetState();
|
||||
std::scoped_lock lock(runningMutex);
|
||||
}
|
||||
|
||||
void TrackPlayer::start() {
|
||||
if (!isRunning) {
|
||||
isRunning = true;
|
||||
startTask();
|
||||
}
|
||||
}
|
||||
|
||||
void TrackPlayer::stop() {
|
||||
isRunning = false;
|
||||
resetState();
|
||||
std::scoped_lock lock(runningMutex);
|
||||
}
|
||||
|
||||
void TrackPlayer::resetState(bool paused) {
|
||||
// Mark for reset
|
||||
this->pendingReset = true;
|
||||
this->currentSongPlaying = false;
|
||||
this->startPaused = paused;
|
||||
|
||||
std::scoped_lock lock(dataOutMutex);
|
||||
|
||||
CSPOT_LOG(info, "Resetting state");
|
||||
}
|
||||
|
||||
void TrackPlayer::seekMs(size_t ms) {
|
||||
if (inFuture) {
|
||||
// We're in the middle of the next track, so we need to reset the player in order to seek
|
||||
resetState();
|
||||
}
|
||||
|
||||
CSPOT_LOG(info, "Seeking...");
|
||||
this->pendingSeekPositionMs = ms;
|
||||
}
|
||||
|
||||
void TrackPlayer::runTask() {
|
||||
std::scoped_lock lock(runningMutex);
|
||||
|
||||
std::shared_ptr<QueuedTrack> track, newTrack = nullptr;
|
||||
|
||||
int trackOffset = 0;
|
||||
bool eof = false;
|
||||
bool endOfQueueReached = false;
|
||||
|
||||
while (isRunning) {
|
||||
// Ensure we even have any tracks to play
|
||||
if (!this->trackQueue->hasTracks() ||
|
||||
(!pendingReset && endOfQueueReached && trackQueue->isFinished())) {
|
||||
this->trackQueue->playableSemaphore->twait(300);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Last track was interrupted, reset to default
|
||||
if (pendingReset) {
|
||||
track = nullptr;
|
||||
pendingReset = false;
|
||||
inFuture = false;
|
||||
}
|
||||
|
||||
endOfQueueReached = false;
|
||||
|
||||
// Wait 800ms. If next reset is requested in meantime, restart the queue.
|
||||
// Gets rid of excess actions during rapid queueing
|
||||
BELL_SLEEP_MS(50);
|
||||
|
||||
if (pendingReset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newTrack = trackQueue->consumeTrack(track, trackOffset);
|
||||
|
||||
if (newTrack == nullptr) {
|
||||
if (trackOffset == -1) {
|
||||
// Reset required
|
||||
track = nullptr;
|
||||
}
|
||||
|
||||
BELL_SLEEP_MS(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
track = newTrack;
|
||||
|
||||
inFuture = trackOffset > 0;
|
||||
|
||||
if (track->state != QueuedTrack::State::READY) {
|
||||
track->loadedSemaphore->twait(5000);
|
||||
|
||||
if (track->state != QueuedTrack::State::READY) {
|
||||
CSPOT_LOG(error, "Track failed to load, skipping it");
|
||||
this->eofCallback();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
CSPOT_LOG(info, "Got track ID=%s", track->identifier.c_str());
|
||||
|
||||
currentSongPlaying = true;
|
||||
|
||||
{
|
||||
std::scoped_lock lock(playbackMutex);
|
||||
|
||||
currentTrackStream = track->getAudioFile();
|
||||
|
||||
// Open the stream
|
||||
currentTrackStream->openStream();
|
||||
|
||||
if (pendingReset || !currentSongPlaying) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trackOffset == 0 && pendingSeekPositionMs == 0) {
|
||||
this->trackLoaded(track, startPaused);
|
||||
startPaused = false;
|
||||
}
|
||||
|
||||
int32_t r =
|
||||
ov_open_callbacks(this, &vorbisFile, NULL, 0, vorbisCallbacks);
|
||||
|
||||
if (pendingSeekPositionMs > 0) {
|
||||
track->requestedPosition = pendingSeekPositionMs;
|
||||
}
|
||||
|
||||
if (track->requestedPosition > 0) {
|
||||
VORBIS_SEEK(&vorbisFile, track->requestedPosition);
|
||||
}
|
||||
|
||||
eof = false;
|
||||
track->loading = true;
|
||||
|
||||
CSPOT_LOG(info, "Playing");
|
||||
|
||||
while (!eof && currentSongPlaying) {
|
||||
// Execute seek if needed
|
||||
if (pendingSeekPositionMs > 0) {
|
||||
uint32_t seekPosition = pendingSeekPositionMs;
|
||||
|
||||
// Reset the pending seek position
|
||||
pendingSeekPositionMs = 0;
|
||||
|
||||
// Seek to the new position
|
||||
VORBIS_SEEK(&vorbisFile, seekPosition);
|
||||
}
|
||||
|
||||
long ret = VORBIS_READ(&vorbisFile, (char*)&pcmBuffer[0],
|
||||
pcmBuffer.size(), ¤tSection);
|
||||
|
||||
if (ret == 0) {
|
||||
CSPOT_LOG(info, "EOF");
|
||||
// and done :)
|
||||
eof = true;
|
||||
} else if (ret < 0) {
|
||||
CSPOT_LOG(error, "An error has occured in the stream %d", ret);
|
||||
currentSongPlaying = false;
|
||||
} else {
|
||||
if (this->dataCallback != nullptr) {
|
||||
auto toWrite = ret;
|
||||
|
||||
while (!eof && currentSongPlaying && !pendingReset && toWrite > 0) {
|
||||
int written = 0;
|
||||
{
|
||||
std::scoped_lock dataOutLock(dataOutMutex);
|
||||
// If reset happened during playback, return
|
||||
if (!currentSongPlaying || pendingReset)
|
||||
break;
|
||||
|
||||
written = dataCallback(pcmBuffer.data() + (ret - toWrite),
|
||||
toWrite, track->identifier);
|
||||
}
|
||||
if (written == 0) {
|
||||
BELL_SLEEP_MS(50);
|
||||
}
|
||||
toWrite -= written;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ov_clear(&vorbisFile);
|
||||
|
||||
CSPOT_LOG(info, "Playing done");
|
||||
|
||||
// always move back to LOADING (ensure proper seeking after last track has been loaded)
|
||||
currentTrackStream = nullptr;
|
||||
track->loading = false;
|
||||
}
|
||||
|
||||
if (eof) {
|
||||
if (trackQueue->isFinished()) {
|
||||
endOfQueueReached = true;
|
||||
}
|
||||
|
||||
this->eofCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t TrackPlayer::_vorbisRead(void* ptr, size_t size, size_t nmemb) {
|
||||
if (this->currentTrackStream == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return this->currentTrackStream->readBytes((uint8_t*)ptr, nmemb * size);
|
||||
}
|
||||
|
||||
size_t TrackPlayer::_vorbisClose() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int TrackPlayer::_vorbisSeek(int64_t offset, int whence) {
|
||||
if (this->currentTrackStream == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
switch (whence) {
|
||||
case 0:
|
||||
this->currentTrackStream->seek(offset); // Spotify header offset
|
||||
break;
|
||||
case 1:
|
||||
this->currentTrackStream->seek(this->currentTrackStream->getPosition() +
|
||||
offset);
|
||||
break;
|
||||
case 2:
|
||||
this->currentTrackStream->seek(this->currentTrackStream->getSize() +
|
||||
offset);
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
long TrackPlayer::_vorbisTell() {
|
||||
if (this->currentTrackStream == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return this->currentTrackStream->getPosition();
|
||||
}
|
||||
|
||||
void TrackPlayer::setDataCallback(DataCallback callback) {
|
||||
this->dataCallback = callback;
|
||||
}
|
||||
636
lib/spotify/cspot/src/TrackQueue.cpp
Normal file
636
lib/spotify/cspot/src/TrackQueue.cpp
Normal file
@@ -0,0 +1,636 @@
|
||||
#include "TrackQueue.h"
|
||||
#include <pb_decode.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "AccessKeyFetcher.h"
|
||||
#include "BellTask.h"
|
||||
#include "CDNAudioFile.h"
|
||||
#include "CSpotContext.h"
|
||||
#include "HTTPClient.h"
|
||||
#include "Logger.h"
|
||||
#include "Utils.h"
|
||||
#include "WrappedSemaphore.h"
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
#include "cJSON.h"
|
||||
#else
|
||||
#include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
|
||||
#include "nlohmann/json_fwd.hpp" // for json
|
||||
#endif
|
||||
#include "protobuf/metadata.pb.h"
|
||||
|
||||
using namespace cspot;
|
||||
namespace TrackDataUtils {
|
||||
bool countryListContains(char* countryList, const char* country) {
|
||||
uint16_t countryList_length = strlen(countryList);
|
||||
for (int x = 0; x < countryList_length; x += 2) {
|
||||
if (countryList[x] == country[0] && countryList[x + 1] == country[1]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool doRestrictionsApply(Restriction* restrictions, int count,
|
||||
const char* country) {
|
||||
for (int x = 0; x < count; x++) {
|
||||
if (restrictions[x].countries_allowed != nullptr) {
|
||||
return !countryListContains(restrictions[x].countries_allowed, country);
|
||||
}
|
||||
|
||||
if (restrictions[x].countries_forbidden != nullptr) {
|
||||
return countryListContains(restrictions[x].countries_forbidden, country);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool canPlayTrack(Track& trackInfo, int altIndex, const char* country) {
|
||||
if (altIndex < 0) {
|
||||
|
||||
} else {
|
||||
for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count;
|
||||
x++) {
|
||||
if (trackInfo.alternative[altIndex].restriction[x].countries_allowed !=
|
||||
nullptr) {
|
||||
return countryListContains(
|
||||
trackInfo.alternative[altIndex].restriction[x].countries_allowed,
|
||||
country);
|
||||
}
|
||||
|
||||
if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden !=
|
||||
nullptr) {
|
||||
return !countryListContains(
|
||||
trackInfo.alternative[altIndex].restriction[x].countries_forbidden,
|
||||
country);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace TrackDataUtils
|
||||
|
||||
void TrackInfo::loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid) {
|
||||
// Generate ID based on GID
|
||||
trackId = bytesToHexString(gid);
|
||||
|
||||
name = std::string(pbTrack->name);
|
||||
|
||||
if (pbTrack->artist_count > 0) {
|
||||
// Handle artist data
|
||||
artist = std::string(pbTrack->artist[0].name);
|
||||
}
|
||||
|
||||
if (pbTrack->has_album) {
|
||||
// Handle album data
|
||||
album = std::string(pbTrack->album.name);
|
||||
|
||||
if (pbTrack->album.has_cover_group &&
|
||||
pbTrack->album.cover_group.image_count > 0) {
|
||||
auto imageId =
|
||||
pbArrayToVector(pbTrack->album.cover_group.image[0].file_id);
|
||||
imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId);
|
||||
}
|
||||
}
|
||||
|
||||
number = pbTrack->has_number ? pbTrack->number : 0;
|
||||
discNumber = pbTrack->has_disc_number ? pbTrack->disc_number : 0;
|
||||
duration = pbTrack->duration;
|
||||
}
|
||||
|
||||
void TrackInfo::loadPbEpisode(Episode* pbEpisode,
|
||||
const std::vector<uint8_t>& gid) {
|
||||
// Generate ID based on GID
|
||||
trackId = bytesToHexString(gid);
|
||||
|
||||
name = std::string(pbEpisode->name);
|
||||
|
||||
if (pbEpisode->covers->image_count > 0) {
|
||||
// Handle episode info
|
||||
auto imageId = pbArrayToVector(pbEpisode->covers->image[0].file_id);
|
||||
imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId);
|
||||
}
|
||||
|
||||
number = pbEpisode->has_number ? pbEpisode->number : 0;
|
||||
discNumber = 0;
|
||||
duration = pbEpisode->duration;
|
||||
}
|
||||
|
||||
QueuedTrack::QueuedTrack(TrackReference& ref,
|
||||
std::shared_ptr<cspot::Context> ctx,
|
||||
uint32_t requestedPosition)
|
||||
: requestedPosition(requestedPosition), ctx(ctx) {
|
||||
this->ref = ref;
|
||||
|
||||
loadedSemaphore = std::make_shared<bell::WrappedSemaphore>();
|
||||
state = State::QUEUED;
|
||||
}
|
||||
|
||||
QueuedTrack::~QueuedTrack() {
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
|
||||
if (pendingMercuryRequest != 0) {
|
||||
ctx->session->unregister(pendingMercuryRequest);
|
||||
}
|
||||
|
||||
if (pendingAudioKeyRequest != 0) {
|
||||
ctx->session->unregisterAudioKey(pendingAudioKeyRequest);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<cspot::CDNAudioFile> QueuedTrack::getAudioFile() {
|
||||
if (state != State::READY) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_shared<cspot::CDNAudioFile>(cdnUrl, audioKey);
|
||||
}
|
||||
|
||||
void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) {
|
||||
int alternativeCount, filesCount = 0;
|
||||
bool canPlay = false;
|
||||
AudioFile* selectedFiles = nullptr;
|
||||
|
||||
const char* countryCode = ctx->config.countryCode.c_str();
|
||||
|
||||
if (ref.type == TrackReference::Type::TRACK) {
|
||||
CSPOT_LOG(info, "Track name: %s", pbTrack->name);
|
||||
CSPOT_LOG(info, "Track duration: %d", pbTrack->duration);
|
||||
|
||||
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
|
||||
pbTrack->restriction_count);
|
||||
|
||||
// Check if we can play the track, if not, try alternatives
|
||||
if (TrackDataUtils::doRestrictionsApply(
|
||||
pbTrack->restriction, pbTrack->restriction_count, countryCode)) {
|
||||
// Go through alternatives
|
||||
for (int x = 0; x < pbTrack->alternative_count; x++) {
|
||||
if (!TrackDataUtils::doRestrictionsApply(
|
||||
pbTrack->alternative[x].restriction,
|
||||
pbTrack->alternative[x].restriction_count, countryCode)) {
|
||||
selectedFiles = pbTrack->alternative[x].file;
|
||||
filesCount = pbTrack->alternative[x].file_count;
|
||||
trackId = pbArrayToVector(pbTrack->alternative[x].gid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We can play the track
|
||||
selectedFiles = pbTrack->file;
|
||||
filesCount = pbTrack->file_count;
|
||||
trackId = pbArrayToVector(pbTrack->gid);
|
||||
}
|
||||
|
||||
if (trackId.size() > 0) {
|
||||
// Load track information
|
||||
trackInfo.loadPbTrack(pbTrack, trackId);
|
||||
}
|
||||
} else {
|
||||
// Handle episodes
|
||||
CSPOT_LOG(info, "Episode name: %s", pbEpisode->name);
|
||||
CSPOT_LOG(info, "Episode duration: %d", pbEpisode->duration);
|
||||
|
||||
CSPOT_LOG(debug, "episodeInfo.restriction.size() = %d",
|
||||
pbEpisode->restriction_count);
|
||||
|
||||
// Check if we can play the episode
|
||||
if (!TrackDataUtils::doRestrictionsApply(pbEpisode->restriction,
|
||||
pbEpisode->restriction_count,
|
||||
countryCode)) {
|
||||
selectedFiles = pbEpisode->file;
|
||||
filesCount = pbEpisode->file_count;
|
||||
trackId = pbArrayToVector(pbEpisode->gid);
|
||||
|
||||
// Load track information
|
||||
trackInfo.loadPbEpisode(pbEpisode, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
// Find playable file
|
||||
for (int x = 0; x < filesCount; x++) {
|
||||
CSPOT_LOG(debug, "File format: %d", selectedFiles[x].format);
|
||||
if (selectedFiles[x].format == ctx->config.audioFormat) {
|
||||
fileId = pbArrayToVector(selectedFiles[x].file_id);
|
||||
break; // If file found stop searching
|
||||
}
|
||||
|
||||
// Fallback to OGG Vorbis 96kbps
|
||||
if (fileId.size() == 0 &&
|
||||
selectedFiles[x].format == AudioFormat_OGG_VORBIS_96) {
|
||||
fileId = pbArrayToVector(selectedFiles[x].file_id);
|
||||
}
|
||||
}
|
||||
|
||||
// No viable files found for playback
|
||||
if (fileId.size() == 0) {
|
||||
CSPOT_LOG(info, "File not available for playback");
|
||||
|
||||
// no alternatives for song
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign track identifier
|
||||
identifier = bytesToHexString(fileId);
|
||||
|
||||
state = State::KEY_REQUIRED;
|
||||
}
|
||||
|
||||
void QueuedTrack::stepLoadAudioFile(
|
||||
std::mutex& trackListMutex,
|
||||
std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
|
||||
// Request audio key
|
||||
this->pendingAudioKeyRequest = ctx->session->requestAudioKey(
|
||||
trackId, fileId,
|
||||
[this, &trackListMutex, updateSemaphore](
|
||||
bool success, const std::vector<uint8_t>& audioKey) {
|
||||
std::scoped_lock lock(trackListMutex);
|
||||
|
||||
if (success) {
|
||||
CSPOT_LOG(info, "Got audio key");
|
||||
this->audioKey =
|
||||
std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
|
||||
|
||||
state = State::CDN_REQUIRED;
|
||||
} else {
|
||||
CSPOT_LOG(error, "Failed to get audio key");
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
}
|
||||
updateSemaphore->give();
|
||||
});
|
||||
|
||||
state = State::PENDING_KEY;
|
||||
}
|
||||
|
||||
void QueuedTrack::stepLoadCDNUrl(const std::string& accessKey) {
|
||||
if (accessKey.size() == 0) {
|
||||
// Wait for access key
|
||||
return;
|
||||
}
|
||||
|
||||
// Request CDN URL
|
||||
CSPOT_LOG(info, "Received access key, fetching CDN URL...");
|
||||
|
||||
try {
|
||||
|
||||
std::string requestUrl = string_format(
|
||||
"https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
|
||||
"%s?alt=json&product=9",
|
||||
bytesToHexString(fileId).c_str());
|
||||
|
||||
auto req = bell::HTTPClient::get(
|
||||
requestUrl, {bell::HTTPClient::ValueHeader(
|
||||
{"Authorization", "Bearer " + accessKey})});
|
||||
|
||||
// Wait for response
|
||||
std::string_view result = req->body();
|
||||
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* jsonResult = cJSON_Parse(result.data());
|
||||
cdnUrl = cJSON_GetArrayItem(cJSON_GetObjectItem(jsonResult, "cdnurl"), 0)
|
||||
->valuestring;
|
||||
cJSON_Delete(jsonResult);
|
||||
#else
|
||||
auto jsonResult = nlohmann::json::parse(result);
|
||||
cdnUrl = jsonResult["cdnurl"][0];
|
||||
#endif
|
||||
|
||||
CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
|
||||
state = State::READY;
|
||||
loadedSemaphore->give();
|
||||
} catch (...) {
|
||||
CSPOT_LOG(error, "Cannot fetch CDN URL");
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
}
|
||||
}
|
||||
|
||||
void QueuedTrack::expire() {
|
||||
if (state != State::QUEUED) {
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
}
|
||||
}
|
||||
|
||||
void QueuedTrack::stepLoadMetadata(
|
||||
Track* pbTrack, Episode* pbEpisode, std::mutex& trackListMutex,
|
||||
std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
|
||||
|
||||
// Prepare request ID
|
||||
std::string requestUrl = string_format(
|
||||
"hm://metadata/3/%s/%s",
|
||||
ref.type == TrackReference::Type::TRACK ? "track" : "episode",
|
||||
bytesToHexString(ref.gid).c_str());
|
||||
|
||||
auto responseHandler = [this, pbTrack, pbEpisode, &trackListMutex,
|
||||
updateSemaphore](MercurySession::Response& res) {
|
||||
std::scoped_lock lock(trackListMutex);
|
||||
|
||||
if (res.parts.size() == 0) {
|
||||
// Invalid metadata, cannot proceed
|
||||
state = State::FAILED;
|
||||
updateSemaphore->give();
|
||||
loadedSemaphore->give();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the metadata
|
||||
if (ref.type == TrackReference::Type::TRACK) {
|
||||
pb_release(Track_fields, pbTrack);
|
||||
pbDecode(*pbTrack, Track_fields, res.parts[0]);
|
||||
} else {
|
||||
pb_release(Episode_fields, pbEpisode);
|
||||
pbDecode(*pbEpisode, Episode_fields, res.parts[0]);
|
||||
}
|
||||
|
||||
// Parse received metadata
|
||||
stepParseMetadata(pbTrack, pbEpisode);
|
||||
|
||||
updateSemaphore->give();
|
||||
};
|
||||
// Execute the request
|
||||
pendingMercuryRequest = ctx->session->execute(
|
||||
MercurySession::RequestType::GET, requestUrl, responseHandler);
|
||||
|
||||
// Set the state to pending
|
||||
state = State::PENDING_META;
|
||||
}
|
||||
|
||||
TrackQueue::TrackQueue(std::shared_ptr<cspot::Context> ctx,
|
||||
std::shared_ptr<cspot::PlaybackState> state)
|
||||
: bell::Task("CSpotTrackQueue", 1024 * 32, 2, 1),
|
||||
playbackState(state),
|
||||
ctx(ctx) {
|
||||
accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
|
||||
processSemaphore = std::make_shared<bell::WrappedSemaphore>();
|
||||
playableSemaphore = std::make_shared<bell::WrappedSemaphore>();
|
||||
|
||||
// Assign encode callback to track list
|
||||
playbackState->innerFrame.state.track.funcs.encode =
|
||||
&TrackReference::pbEncodeTrackList;
|
||||
playbackState->innerFrame.state.track.arg = ¤tTracks;
|
||||
pbTrack = Track_init_zero;
|
||||
pbEpisode = Episode_init_zero;
|
||||
|
||||
// Start the task
|
||||
startTask();
|
||||
};
|
||||
|
||||
TrackQueue::~TrackQueue() {
|
||||
stopTask();
|
||||
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
pb_release(Track_fields, &pbTrack);
|
||||
pb_release(Episode_fields, &pbEpisode);
|
||||
}
|
||||
|
||||
TrackInfo TrackQueue::getTrackInfo(std::string_view identifier) {
|
||||
for (auto& track : preloadedTracks) {
|
||||
if (track->identifier == identifier)
|
||||
return track->trackInfo;
|
||||
}
|
||||
return TrackInfo{};
|
||||
}
|
||||
|
||||
void TrackQueue::runTask() {
|
||||
isRunning = true;
|
||||
|
||||
std::scoped_lock lock(runningMutex);
|
||||
|
||||
std::deque<std::shared_ptr<QueuedTrack>> trackQueue;
|
||||
|
||||
while (isRunning) {
|
||||
processSemaphore->twait(100);
|
||||
|
||||
// Make sure we have the newest access key
|
||||
accessKey = accessKeyFetcher->getAccessKey();
|
||||
|
||||
int loadedIndex = currentTracksIndex;
|
||||
|
||||
// No tracks loaded yet
|
||||
if (loadedIndex < 0) {
|
||||
continue;
|
||||
} else {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
trackQueue = preloadedTracks;
|
||||
}
|
||||
|
||||
for (auto& track : trackQueue) {
|
||||
if (track) {
|
||||
this->processTrack(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TrackQueue::stopTask() {
|
||||
if (isRunning) {
|
||||
isRunning = false;
|
||||
processSemaphore->give();
|
||||
std::scoped_lock lock(runningMutex);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QueuedTrack> TrackQueue::consumeTrack(
|
||||
std::shared_ptr<QueuedTrack> prevTrack, int& offset) {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
if (currentTracksIndex == -1 || currentTracksIndex >= currentTracks.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// No previous track, return head
|
||||
if (prevTrack == nullptr) {
|
||||
offset = 0;
|
||||
|
||||
return preloadedTracks[0];
|
||||
}
|
||||
|
||||
// if (currentTracksIndex + preloadedTracks.size() >= currentTracks.size()) {
|
||||
// offset = -1;
|
||||
|
||||
// // Last track in queue
|
||||
// return nullptr;
|
||||
// }
|
||||
|
||||
auto prevTrackIter =
|
||||
std::find(preloadedTracks.begin(), preloadedTracks.end(), prevTrack);
|
||||
|
||||
if (prevTrackIter != preloadedTracks.end()) {
|
||||
// Get offset of next track
|
||||
offset = prevTrackIter - preloadedTracks.begin() + 1;
|
||||
} else {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
if (offset >= preloadedTracks.size()) {
|
||||
// Last track in preloaded queue
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Return the current track
|
||||
return preloadedTracks[offset];
|
||||
}
|
||||
|
||||
void TrackQueue::processTrack(std::shared_ptr<QueuedTrack> track) {
|
||||
switch (track->state) {
|
||||
case QueuedTrack::State::QUEUED:
|
||||
track->stepLoadMetadata(&pbTrack, &pbEpisode, tracksMutex,
|
||||
processSemaphore);
|
||||
break;
|
||||
case QueuedTrack::State::KEY_REQUIRED:
|
||||
track->stepLoadAudioFile(tracksMutex, processSemaphore);
|
||||
break;
|
||||
case QueuedTrack::State::CDN_REQUIRED:
|
||||
track->stepLoadCDNUrl(accessKey);
|
||||
|
||||
if (track->state == QueuedTrack::State::READY) {
|
||||
if (preloadedTracks.size() < MAX_TRACKS_PRELOAD) {
|
||||
// Queue a new track to preload
|
||||
queueNextTrack(preloadedTracks.size());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Do not perform any action
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool TrackQueue::queueNextTrack(int offset, uint32_t positionMs) {
|
||||
const int requestedRefIndex = offset + currentTracksIndex;
|
||||
|
||||
if (requestedRefIndex < 0 || requestedRefIndex >= currentTracks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// in case we re-queue current track, make sure position is updated (0)
|
||||
if (offset == 0 && preloadedTracks.size() &&
|
||||
preloadedTracks[0]->ref == currentTracks[currentTracksIndex]) {
|
||||
preloadedTracks.pop_front();
|
||||
}
|
||||
|
||||
if (offset <= 0) {
|
||||
preloadedTracks.push_front(std::make_shared<QueuedTrack>(
|
||||
currentTracks[requestedRefIndex], ctx, positionMs));
|
||||
} else {
|
||||
preloadedTracks.push_back(std::make_shared<QueuedTrack>(
|
||||
currentTracks[requestedRefIndex], ctx, positionMs));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TrackQueue::skipTrack(SkipDirection dir, bool expectNotify) {
|
||||
bool skipped = true;
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
if (dir == SkipDirection::PREV) {
|
||||
uint64_t position =
|
||||
!playbackState->innerFrame.state.has_position_ms
|
||||
? 0
|
||||
: playbackState->innerFrame.state.position_ms +
|
||||
ctx->timeProvider->getSyncedTimestamp() -
|
||||
playbackState->innerFrame.state.position_measured_at;
|
||||
|
||||
if (currentTracksIndex > 0 && position < 3000) {
|
||||
queueNextTrack(-1);
|
||||
|
||||
if (preloadedTracks.size() > MAX_TRACKS_PRELOAD) {
|
||||
preloadedTracks.pop_back();
|
||||
}
|
||||
|
||||
currentTracksIndex--;
|
||||
} else {
|
||||
queueNextTrack(0);
|
||||
}
|
||||
} else {
|
||||
if (currentTracks.size() > currentTracksIndex + 1) {
|
||||
preloadedTracks.pop_front();
|
||||
|
||||
if (!queueNextTrack(preloadedTracks.size() + 1)) {
|
||||
CSPOT_LOG(info, "Failed to queue next track");
|
||||
}
|
||||
|
||||
currentTracksIndex++;
|
||||
} else {
|
||||
skipped = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (skipped) {
|
||||
// Update frame data
|
||||
playbackState->innerFrame.state.playing_track_index = currentTracksIndex;
|
||||
|
||||
if (expectNotify) {
|
||||
// Reset position to zero
|
||||
notifyPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
bool TrackQueue::hasTracks() {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
return currentTracks.size() > 0;
|
||||
}
|
||||
|
||||
bool TrackQueue::isFinished() {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
return currentTracksIndex >= currentTracks.size() - 1;
|
||||
}
|
||||
|
||||
bool TrackQueue::updateTracks(uint32_t requestedPosition, bool initial) {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
bool cleared = true;
|
||||
|
||||
// Copy requested track list
|
||||
currentTracks = playbackState->remoteTracks;
|
||||
currentTracksIndex = playbackState->innerFrame.state.playing_track_index;
|
||||
|
||||
if (initial) {
|
||||
// Clear preloaded tracks
|
||||
preloadedTracks.clear();
|
||||
|
||||
if (currentTracksIndex < currentTracks.size()) {
|
||||
// Push a song on the preloaded queue
|
||||
queueNextTrack(0, requestedPosition);
|
||||
}
|
||||
|
||||
// We already updated track meta, mark it
|
||||
notifyPending = true;
|
||||
|
||||
playableSemaphore->give();
|
||||
} else if (preloadedTracks[0]->loading) {
|
||||
// try to not re-load track if we are still loading it
|
||||
|
||||
// remove everything except first track
|
||||
preloadedTracks.erase(preloadedTracks.begin() + 1, preloadedTracks.end());
|
||||
|
||||
// Push a song on the preloaded queue
|
||||
CSPOT_LOG(info, "Keeping current track %d", currentTracksIndex);
|
||||
queueNextTrack(1);
|
||||
|
||||
cleared = false;
|
||||
} else {
|
||||
// Clear preloaded tracks
|
||||
preloadedTracks.clear();
|
||||
|
||||
// Push a song on the preloaded queue
|
||||
CSPOT_LOG(info, "Re-loading current track");
|
||||
queueNextTrack(0, requestedPosition);
|
||||
}
|
||||
|
||||
return cleared;
|
||||
}
|
||||
152
lib/spotify/cspot/src/TrackReference.cpp
Normal file
152
lib/spotify/cspot/src/TrackReference.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#include "TrackReference.h"
|
||||
|
||||
#include "NanoPBExtensions.h"
|
||||
#include "Utils.h"
|
||||
#include "protobuf/spirc.pb.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
static constexpr auto base62Alphabet =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
TrackReference::TrackReference() : type(Type::TRACK) {}
|
||||
|
||||
void TrackReference::decodeURI() {
|
||||
if (gid.size() == 0) {
|
||||
// Episode GID is being fetched via base62 encoded URI
|
||||
auto idString = uri.substr(uri.find_last_of(":") + 1, uri.size());
|
||||
gid = {0};
|
||||
|
||||
std::string_view alphabet(base62Alphabet);
|
||||
for (int x = 0; x < idString.size(); x++) {
|
||||
size_t d = alphabet.find(idString[x]);
|
||||
gid = bigNumMultiply(gid, 62);
|
||||
gid = bigNumAdd(gid, d);
|
||||
}
|
||||
|
||||
if (uri.find("episode:") != std::string::npos) {
|
||||
type = Type::EPISODE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TrackReference::operator==(const TrackReference& other) const {
|
||||
return other.gid == gid && other.uri == uri;
|
||||
}
|
||||
|
||||
bool TrackReference::pbEncodeTrackList(pb_ostream_t* stream,
|
||||
const pb_field_t* field,
|
||||
void* const* arg) {
|
||||
auto trackQueue = *static_cast<std::vector<TrackReference>*>(*arg);
|
||||
static TrackRef msg = TrackRef_init_zero;
|
||||
|
||||
// Prepare nanopb callbacks
|
||||
msg.context.funcs.encode = &bell::nanopb::encodeString;
|
||||
msg.uri.funcs.encode = &bell::nanopb::encodeString;
|
||||
msg.gid.funcs.encode = &bell::nanopb::encodeVector;
|
||||
msg.queued.funcs.encode = &bell::nanopb::encodeBoolean;
|
||||
|
||||
for (auto trackRef : trackQueue) {
|
||||
if (!pb_encode_tag_for_field(stream, field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
msg.gid.arg = &trackRef.gid;
|
||||
msg.uri.arg = &trackRef.uri;
|
||||
msg.context.arg = &trackRef.context;
|
||||
msg.queued.arg = &trackRef.queued;
|
||||
|
||||
if (!pb_encode_submessage(stream, TrackRef_fields, &msg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TrackReference::pbDecodeTrackList(pb_istream_t* stream,
|
||||
const pb_field_t* field, void** arg) {
|
||||
auto trackQueue = static_cast<std::vector<TrackReference>*>(*arg);
|
||||
|
||||
// Push a new reference
|
||||
trackQueue->push_back(TrackReference());
|
||||
|
||||
auto& track = trackQueue->back();
|
||||
|
||||
bool eof = false;
|
||||
pb_wire_type_t wire_type;
|
||||
pb_istream_t substream;
|
||||
uint32_t tag;
|
||||
|
||||
while (!eof) {
|
||||
if (!pb_decode_tag(stream, &wire_type, &tag, &eof)) {
|
||||
// Decoding failed and not eof
|
||||
if (!eof) {
|
||||
return false;
|
||||
}
|
||||
// EOF
|
||||
} else {
|
||||
switch (tag) {
|
||||
case TrackRef_uri_tag:
|
||||
case TrackRef_context_tag:
|
||||
case TrackRef_gid_tag: {
|
||||
// Make substream
|
||||
if (!pb_make_string_substream(stream, &substream)) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* destBuffer = nullptr;
|
||||
|
||||
// Handle GID
|
||||
if (tag == TrackRef_gid_tag) {
|
||||
track.gid.resize(substream.bytes_left);
|
||||
destBuffer = &track.gid[0];
|
||||
} else if (tag == TrackRef_context_tag) {
|
||||
track.context.resize(substream.bytes_left);
|
||||
|
||||
destBuffer = reinterpret_cast<uint8_t*>(&track.context[0]);
|
||||
} else if (tag == TrackRef_uri_tag) {
|
||||
track.uri.resize(substream.bytes_left);
|
||||
|
||||
destBuffer = reinterpret_cast<uint8_t*>(&track.uri[0]);
|
||||
}
|
||||
|
||||
if (!pb_read(&substream, destBuffer, substream.bytes_left)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Close substream
|
||||
if (!pb_close_string_substream(stream, &substream)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case TrackRef_queued_tag: {
|
||||
uint32_t queuedValue;
|
||||
|
||||
// Decode boolean
|
||||
if (!pb_decode_varint32(stream, &queuedValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cast down to bool
|
||||
track.queued = (bool)queuedValue;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Field not known, skip
|
||||
pb_skip_field(stream, wire_type);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in GID when only URI is provided
|
||||
track.decodeURI();
|
||||
|
||||
return true;
|
||||
}
|
||||
154
lib/spotify/cspot/src/Utils.cpp
Normal file
154
lib/spotify/cspot/src/Utils.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#include "Utils.h"
|
||||
|
||||
#include <stdlib.h> // for strtol
|
||||
#include <chrono>
|
||||
#include <iomanip> // for operator<<, setfill, setw
|
||||
#include <iostream> // for basic_ostream, hex
|
||||
#include <sstream> // for stringstream
|
||||
#include <string> // for string
|
||||
#include <type_traits> // for enable_if<>::type
|
||||
#ifndef _WIN32
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
unsigned long long getCurrentTimestamp() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
uint64_t hton64(uint64_t value) {
|
||||
int num = 42;
|
||||
if (*(char*)&num == 42) {
|
||||
uint32_t high_part = htonl((uint32_t)(value >> 32));
|
||||
uint32_t low_part = htonl((uint32_t)(value & 0xFFFFFFFFLL));
|
||||
return (((uint64_t)low_part) << 32) | high_part;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> stringHexToBytes(const std::string& s) {
|
||||
std::vector<uint8_t> v;
|
||||
v.reserve(s.length() / 2);
|
||||
|
||||
for (std::string::size_type i = 0; i < s.length(); i += 2) {
|
||||
std::string byteString = s.substr(i, 2);
|
||||
uint8_t byte = (uint8_t)strtol(byteString.c_str(), NULL, 16);
|
||||
v.push_back(byte);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
std::string bytesToHexString(const std::vector<uint8_t>& v) {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0');
|
||||
std::vector<uint8_t>::const_iterator it;
|
||||
|
||||
for (it = v.begin(); it != v.end(); it++) {
|
||||
ss << std::setw(2) << static_cast<unsigned>(*it);
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bigNumAdd(std::vector<uint8_t> num, int n) {
|
||||
auto carry = n;
|
||||
for (int x = num.size() - 1; x >= 0; x--) {
|
||||
int res = num[x] + carry;
|
||||
if (res < 256) {
|
||||
carry = 0;
|
||||
num[x] = res;
|
||||
} else {
|
||||
// Carry the rest of the division
|
||||
carry = res / 256;
|
||||
num[x] = res % 256;
|
||||
|
||||
// extend the vector at the last index
|
||||
if (x == 0) {
|
||||
num.insert(num.begin(), carry);
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bigNumDivide(std::vector<uint8_t> num, int n) {
|
||||
auto carry = 0;
|
||||
for (int x = 0; x < num.size(); x++) {
|
||||
int res = num[x] + carry * 256;
|
||||
if (res < n) {
|
||||
carry = res;
|
||||
num[x] = 0;
|
||||
} else {
|
||||
// Carry the rest of the division
|
||||
carry = res % n;
|
||||
num[x] = res / n;
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bigNumMultiply(std::vector<uint8_t> num, int n) {
|
||||
auto carry = 0;
|
||||
for (int x = num.size() - 1; x >= 0; x--) {
|
||||
int res = num[x] * n + carry;
|
||||
if (res < 256) {
|
||||
carry = 0;
|
||||
num[x] = res;
|
||||
} else {
|
||||
// Carry the rest of the division
|
||||
carry = res / 256;
|
||||
num[x] = res % 256;
|
||||
|
||||
// extend the vector at the last index
|
||||
if (x == 0) {
|
||||
num.insert(num.begin(), carry);
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
unsigned char h2int(char c) {
|
||||
if (c >= '0' && c <= '9') {
|
||||
return ((unsigned char)c - '0');
|
||||
}
|
||||
if (c >= 'a' && c <= 'f') {
|
||||
return ((unsigned char)c - 'a' + 10);
|
||||
}
|
||||
if (c >= 'A' && c <= 'F') {
|
||||
return ((unsigned char)c - 'A' + 10);
|
||||
}
|
||||
return (0);
|
||||
}
|
||||
|
||||
std::string urlDecode(std::string str) {
|
||||
std::string encodedString = "";
|
||||
char c;
|
||||
char code0;
|
||||
char code1;
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
c = str[i];
|
||||
if (c == '+') {
|
||||
encodedString += ' ';
|
||||
} else if (c == '%') {
|
||||
i++;
|
||||
code0 = str[i];
|
||||
i++;
|
||||
code1 = str[i];
|
||||
c = (h2int(code0) << 4) | h2int(code1);
|
||||
encodedString += c;
|
||||
} else {
|
||||
|
||||
encodedString += c;
|
||||
}
|
||||
}
|
||||
|
||||
return encodedString;
|
||||
}
|
||||
Reference in New Issue
Block a user