applied platformio structure

This commit is contained in:
2026-03-13 17:03:22 +00:00
parent c5233cf15c
commit db7d90e736
3510 changed files with 691878 additions and 0 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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(), &currentSection);
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;
}

View 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 = &currentTracks;
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;
}

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

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