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,40 @@
#pragma once
#include <atomic> // or std::atomic
#include <functional> // for function
#include <memory> // for shared_ptr
#include <string> // for string
namespace cspot {
struct Context;
class AccessKeyFetcher {
public:
AccessKeyFetcher(std::shared_ptr<cspot::Context> ctx);
/**
* @brief Checks if key is expired
* @returns true when currently held access key is not valid
*/
bool isExpired();
/**
* @brief Fetches a new access key
* @remark In case the key is expired, this function blocks until a refresh is done.
* @returns access key
*/
std::string getAccessKey();
/**
* @brief Forces a refresh of the access key
*/
void updateAccessKey();
private:
std::shared_ptr<cspot::Context> ctx;
std::atomic<bool> keyPending = false;
std::string accessKey;
long long int expiresAt;
};
} // namespace cspot

View File

@@ -0,0 +1,23 @@
#pragma once
#include <string> // for string
#ifdef BELL_ONLY_CJSON
#include "cJSON.h"
#else
#endif
namespace cspot {
class ApResolve {
public:
ApResolve(std::string apOverride);
/**
* @brief Connects to spotify's servers and returns first valid ap address
* @returns std::string Address in form of url:port
*/
std::string fetchFirstApAddress();
private:
std::string apOverride;
};
} // namespace cspot

View File

@@ -0,0 +1,64 @@
#pragma once
#include <cstdint> // for uint8_t
#include <memory> // for unique_ptr
#include <string> // for string
#include <vector> // for vector
#include "Crypto.h" // for Crypto
#include "protobuf/authentication.pb.h" // for ClientResponseEncrypted
#include "protobuf/keyexchange.pb.h" // for APResponseMessage, ClientHello
namespace cspot {
class AuthChallenges {
public:
AuthChallenges();
~AuthChallenges();
/**
* @brief Prepares a spotify authentication packet
* @param authBlob authentication blob bytes
* @param authType value representing spotify's authentication type
* @param deviceId device id to use during auth.
* @param username spotify's username
*
* @returns vector containing bytes of the authentication packet
*/
std::vector<uint8_t> prepareAuthPacket(std::vector<uint8_t>& authBlob,
int authType,
const std::string& deviceId,
const std::string& username);
/**
* @brief Solves the ApHello packet, and returns a packet with response
*
* @param helloPacket hello packet bytes received from the server
* @param data authentication data received from the server
*
* @returns vector containing response packet
*/
std::vector<uint8_t> solveApHello(std::vector<uint8_t>& helloPacket,
std::vector<uint8_t>& data);
/**
* @brief Prepares an client hello packet, used for initial auth with spotify
*
* @returns vector containing the packet's data
*/
std::vector<uint8_t> prepareClientHello();
std::vector<uint8_t> shanSendKey = {};
std::vector<uint8_t> shanRecvKey = {};
private:
const long long SPOTIFY_VERSION = 0x10800000000;
// Protobuf structures
ClientResponseEncrypted authRequest;
ClientResponsePlaintext clientResPlaintext;
ClientHello clientHello;
APResponseMessage apResponse;
std::unique_ptr<Crypto> crypto;
};
} // namespace cspot

View File

@@ -0,0 +1,90 @@
#pragma once
#include <cstddef> // for size_t
#include <cstdint> // for uint8_t
#include <memory> // for shared_ptr, unique_ptr
#include <string> // for string
#include <vector> // for vector
#include "Crypto.h" // for Crypto
#include "HTTPClient.h" // for HTTPClient
namespace bell {
class WrappedSemaphore;
} // namespace bell
namespace cspot {
class AccessKeyFetcher;
class CDNAudioFile {
public:
CDNAudioFile(const std::string& cdnUrl, const std::vector<uint8_t>& audioKey);
/**
* @brief Opens connection to the provided cdn url, and fetches track metadata.
*/
void openStream();
/**
* @brief Read and decrypt part of the cdn stream
*
* @param dst buffer where to read received data to
* @param amount of bytes to read
*
* @returns amount of bytes read
*/
size_t readBytes(uint8_t* dst, size_t bytes);
/**
* @brief Returns current position in CDN stream
*/
size_t getPosition();
/**
* @brief returns total size of the audio file in bytes
*/
size_t getSize();
/**
* @brief Seeks the track to provided position
* @param position position where to seek the track
*/
void seek(size_t position);
private:
const int OPUS_HEADER_SIZE = 8 * 1024;
const int OPUS_FOOTER_PREFFERED = 1024 * 12; // 12K should be safe
const int SEEK_MARGIN_SIZE = 1024 * 4;
const int HTTP_BUFFER_SIZE = 1024 * 14;
const int SPOTIFY_OPUS_HEADER = 167;
// Used to store opus metadata, speeds up read
std::vector<uint8_t> header = std::vector<uint8_t>(OPUS_HEADER_SIZE);
std::vector<uint8_t> footer;
// General purpose buffer to read data
std::vector<uint8_t> httpBuffer = std::vector<uint8_t>(HTTP_BUFFER_SIZE);
// AES IV for decrypting the audio stream
const std::vector<uint8_t> audioAESIV = {0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb,
0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64,
0x3f, 0x63, 0x0d, 0x93};
std::unique_ptr<Crypto> crypto;
std::unique_ptr<bell::HTTPClient::Response> httpConnection;
size_t position = 0;
size_t totalFileSize = 0;
size_t lastRequestPosition = 0;
size_t lastRequestCapacity = 0;
bool enableRequestMargin = false;
std::string cdnUrl;
std::vector<uint8_t> audioKey;
void decrypt(uint8_t* dst, size_t nbytes, size_t pos);
};
} // namespace cspot

View File

@@ -0,0 +1,82 @@
#pragma once
#include <stdint.h>
#include <memory>
#include "Crypto.h"
#include "LoginBlob.h"
#include "MercurySession.h"
#include "TimeProvider.h"
#include "protobuf/authentication.pb.h" // for AuthenticationType_AUTHE...
#include "protobuf/metadata.pb.h"
#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
namespace cspot {
struct Context {
struct ConfigState {
// Setup default bitrate to 160
AudioFormat audioFormat = AudioFormat::AudioFormat_OGG_VORBIS_160;
std::string deviceId;
std::string deviceName;
std::string clientId;
std::string clientSecret;
std::vector<uint8_t> authData;
int volume;
std::string username;
std::string countryCode;
};
ConfigState config;
std::shared_ptr<TimeProvider> timeProvider;
std::shared_ptr<cspot::MercurySession> session;
std::string getCredentialsJson() {
#ifdef BELL_ONLY_CJSON
cJSON* json_obj = cJSON_CreateObject();
cJSON_AddStringToObject(json_obj, "authData",
Crypto::base64Encode(config.authData).c_str());
cJSON_AddNumberToObject(
json_obj, "authType",
AuthenticationType_AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS);
cJSON_AddStringToObject(json_obj, "username", config.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(config.authData);
obj["authType"] =
AuthenticationType_AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS;
obj["username"] = config.username;
return obj.dump();
#endif
}
static std::shared_ptr<Context> createFromBlob(
std::shared_ptr<LoginBlob> blob) {
auto ctx = std::make_shared<Context>();
ctx->timeProvider = std::make_shared<TimeProvider>();
ctx->session = std::make_shared<MercurySession>(ctx->timeProvider);
ctx->config.deviceId = blob->getDeviceId();
ctx->config.deviceName = blob->getDeviceName();
ctx->config.authData = blob->authData;
ctx->config.volume = 0;
ctx->config.username = blob->getUserName();
return ctx;
}
};
} // namespace cspot

View File

@@ -0,0 +1,17 @@
#pragma once
#define MAX_VOLUME 65536
// variable weakly set in ZeroconfAuthentificator.cpp
extern char deviceId[];
namespace cspot {
// Hardcoded information sent to spotify servers
const char* const informationString = "cspot-player";
const char* const brandName = "cspot";
const char* const versionString = "cspot-1.1";
const char* const protocolVersion = "2.7.1";
const char* const defaultDeviceName = "CSpot";
const char* const swVersion = "1.0.0";
} // namespace cspot

View File

@@ -0,0 +1,15 @@
#ifndef CSPOT_ASSERT_H
#define CSPOT_ASSERT_H
#include <stdio.h>
#include <cassert>
#define CSPOT_ASSERT(CONDITION, MESSAGE) \
do { \
if (!(CONDITION)) { \
printf("At %s in %s:%d\n Assertion %s failed: %s", __func__, __FILE__, \
__LINE__, #CONDITION, MESSAGE); \
abort(); \
} \
} while (0)
#endif

View File

@@ -0,0 +1,8 @@
#pragma once
#include <BellLogger.h>
#define CSPOT_LOG(type, ...) \
do { \
bell::bellGlobalLogger->type(__FILE__, __LINE__, "cspot", __VA_ARGS__); \
} while (0)

View File

@@ -0,0 +1,46 @@
#pragma once
#include <cstdint> // for uint8_t, uint32_t
#include <map> // for map
#include <memory> // for unique_ptr
#include <string> // for string
#include <vector> // for vector
#include "Crypto.h" // for CryptoMbedTLS, Crypto
namespace cspot {
class LoginBlob {
private:
int blobSkipPosition = 0;
std::unique_ptr<Crypto> crypto;
std::string name, deviceId;
uint32_t readBlobInt(const std::vector<uint8_t>& loginData);
std::vector<uint8_t> decodeBlob(const std::vector<uint8_t>& blob,
const std::vector<uint8_t>& sharedKey);
std::vector<uint8_t> decodeBlobSecondary(const std::vector<uint8_t>& blob,
const std::string& username,
const std::string& deviceId);
public:
LoginBlob(std::string name);
std::vector<uint8_t> authData;
std::string username = "";
int authType;
// Loading
void loadZeroconfQuery(std::map<std::string, std::string>& queryParams);
void loadZeroconf(const std::vector<uint8_t>& blob,
const std::vector<uint8_t>& sharedKey,
const std::string& deviceId, const std::string& username);
void loadUserPass(const std::string& username, const std::string& password);
void loadJson(const std::string& json);
std::string buildZeroconfInfo();
std::string getDeviceId();
std::string getDeviceName();
std::string getUserName();
std::string toJson();
};
} // namespace cspot

View File

@@ -0,0 +1,134 @@
#pragma once
#include <atomic> // for atomic
#include <cstdint> // for uint8_t, uint64_t, uint32_t
#include <functional> // for function
#include <memory> // for shared_ptr
#include <mutex> // for mutex
#include <string> // for string
#include <unordered_map> // for unordered_map
#include <vector> // for vector
#include "BellTask.h" // for Task
#include "Packet.h" // for Packet
#include "Queue.h" // for Queue
#include "Session.h" // for Session
#include "protobuf/mercury.pb.h" // for Header
namespace cspot {
class TimeProvider;
class MercurySession : public bell::Task, public cspot::Session {
public:
MercurySession(std::shared_ptr<cspot::TimeProvider> timeProvider);
~MercurySession();
typedef std::vector<std::vector<uint8_t>> DataParts;
struct Response {
Header mercuryHeader;
uint8_t flags;
DataParts parts;
uint64_t sequenceId;
bool fail;
};
typedef std::function<void(Response&)> ResponseCallback;
typedef std::function<void(bool, const std::vector<uint8_t>&)>
AudioKeyCallback;
typedef std::function<void()> ConnectionEstabilishedCallback;
enum class RequestType : uint8_t {
SUB = 0xb3,
UNSUB = 0xb4,
SUBRES = 0xb5,
SEND = 0xb2,
GET = 0xFF, // Shitty workaround, it's value is actually same as SEND
PING = 0x04,
PONG_ACK = 0x4a,
AUDIO_CHUNK_REQUEST_COMMAND = 0x08,
AUDIO_CHUNK_SUCCESS_RESPONSE = 0x09,
AUDIO_CHUNK_FAILURE_RESPONSE = 0x0A,
AUDIO_KEY_REQUEST_COMMAND = 0x0C,
AUDIO_KEY_SUCCESS_RESPONSE = 0x0D,
AUDIO_KEY_FAILURE_RESPONSE = 0x0E,
COUNTRY_CODE_RESPONSE = 0x1B,
};
std::unordered_map<RequestType, std::string> RequestTypeMap = {
{RequestType::GET, "GET"},
{RequestType::SEND, "SEND"},
{RequestType::SUB, "SUB"},
{RequestType::UNSUB, "UNSUB"},
};
void handlePacket();
uint64_t executeSubscription(RequestType type, const std::string& uri,
ResponseCallback callback,
ResponseCallback subscription, DataParts& parts);
uint64_t executeSubscription(RequestType type, const std::string& uri,
ResponseCallback callback,
ResponseCallback subscription) {
DataParts parts = {};
return this->executeSubscription(type, uri, callback, subscription, parts);
}
uint64_t execute(RequestType type, const std::string& uri,
ResponseCallback callback) {
return this->executeSubscription(type, uri, callback, nullptr);
}
uint64_t execute(RequestType type, const std::string& uri,
ResponseCallback callback, DataParts& parts) {
return this->executeSubscription(type, uri, callback, nullptr, parts);
}
void unregister(uint64_t sequenceId);
void unregisterAudioKey(uint32_t sequenceId);
uint32_t requestAudioKey(const std::vector<uint8_t>& trackId,
const std::vector<uint8_t>& fileId,
AudioKeyCallback audioCallback);
std::string getCountryCode();
void disconnect();
void setConnectedHandler(ConnectionEstabilishedCallback callback);
bool triggerTimeout() override;
private:
const int PING_TIMEOUT_MS = 2 * 60 * 1000 + 5000;
std::shared_ptr<cspot::TimeProvider> timeProvider;
Header tempMercuryHeader = {};
ConnectionEstabilishedCallback connectionReadyCallback = nullptr;
bell::Queue<cspot::Packet> packetQueue;
void runTask() override;
void reconnect();
std::unordered_map<uint64_t, ResponseCallback> callbacks;
std::unordered_map<std::string, ResponseCallback> subscriptions;
std::unordered_map<uint32_t, AudioKeyCallback> audioKeyCallbacks;
uint64_t sequenceId = 1;
uint32_t audioKeySequence = 1;
unsigned long long timestampDiff;
unsigned long long lastPingTimestamp = -1;
std::string countryCode = "";
std::mutex isRunningMutex;
std::atomic<bool> isRunning = false;
std::atomic<bool> isReconnecting = false;
std::atomic<bool> executeEstabilishedCallback = false;
void failAllPending();
Response decodeResponse(const std::vector<uint8_t>& data);
};
} // namespace cspot

View File

@@ -0,0 +1,11 @@
#pragma once
#include <cstdint>
#include <vector>
namespace cspot {
struct Packet {
uint8_t command;
std::vector<uint8_t> data;
};
} // namespace cspot

View File

@@ -0,0 +1,45 @@
#ifndef PLAINCONNECTION_H
#define PLAINCONNECTION_H
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include "win32shim.h"
#else
#include <unistd.h> // for size_t
#endif
#include <cstdint> // for uint8_t
#include <functional> // for function
#include <string> // for string
#include <vector> // for vector
typedef std::function<bool()> timeoutCallback;
namespace cspot {
class PlainConnection {
public:
PlainConnection();
~PlainConnection();
/**
* @brief Connect to the given AP address
*
* @param apAddress The AP url to connect to
*/
void connect(const std::string& apAddress);
void close();
timeoutCallback timeoutHandler;
std::vector<uint8_t> sendPrefixPacket(const std::vector<uint8_t>& prefix,
const std::vector<uint8_t>& data);
std::vector<uint8_t> recvPacket();
void readBlock(const uint8_t* dst, size_t size);
size_t writeBlock(const std::vector<uint8_t>& data);
private:
int apSock;
};
} // namespace cspot
#endif

View File

@@ -0,0 +1,97 @@
#pragma once
#include <stdint.h> // for uint8_t, uint32_t
#include <memory> // for shared_ptr
#include <string> // for string
#include <vector> // for vector
#include "TrackReference.h"
#include "protobuf/spirc.pb.h" // for Frame, TrackRef, CapabilityType, Mess...
namespace cspot {
struct Context;
class PlaybackState {
private:
std::shared_ptr<cspot::Context> ctx;
uint32_t seqNum = 0;
uint8_t capabilityIndex = 0;
std::vector<uint8_t> frameData;
void addCapability(
CapabilityType typ, int intValue = -1,
std::vector<std::string> stringsValue = std::vector<std::string>());
public:
Frame innerFrame;
Frame remoteFrame;
std::vector<TrackReference> remoteTracks;
enum class State { Playing, Stopped, Loading, Paused };
/**
* @brief Player state represents the current state of player.
*
* Responsible for keeping track of player's state. Doesn't control the playback itself.
*
* @param timeProvider synced time provider
*/
PlaybackState(std::shared_ptr<cspot::Context> ctx);
~PlaybackState();
/**
* @brief Updates state according to current playback state.
*
* @param state playback state
*/
void setPlaybackState(const PlaybackState::State state);
/**
* @brief Sets player activity
*
* @param isActive activity status
*/
void setActive(bool isActive);
/**
* @brief Simple getter
*
* @return true player is active
* @return false player is inactive
*/
bool isActive();
/**
* @brief Updates local track position.
*
* @param position position in milliseconds
*/
void updatePositionMs(uint32_t position);
/**
* @brief Sets local volume on internal state.
*
* @param volume volume between 0 and UINT16 max
*/
void setVolume(uint32_t volume);
/**
* @brief Updates local track queue from remote data.
*/
void syncWithRemote();
/**
* @brief Encodes current frame into binary data via protobuf.
*
* @param typ message type to include in frame type
* @return std::vector<uint8_t> binary frame data
*/
std::vector<uint8_t> encodeCurrentFrame(MessageType typ);
bool decodeRemoteFrame(std::vector<uint8_t>& data);
};
} // namespace cspot

View File

@@ -0,0 +1,40 @@
#pragma once
#include <stdint.h> // for uint8_t
#include <memory> // for shared_ptr, unique_ptr
#include <string> // for string
#include <vector> // for vector
namespace cspot {
class AuthChallenges;
class LoginBlob;
class PlainConnection;
class ShannonConnection;
} // namespace cspot
#define LOGIN_REQUEST_COMMAND 0xAB
#define AUTH_SUCCESSFUL_COMMAND 0xAC
#define AUTH_DECLINED_COMMAND 0xAD
namespace cspot {
class Session {
protected:
std::unique_ptr<cspot::AuthChallenges> challenges;
std::shared_ptr<cspot::PlainConnection> conn;
std::shared_ptr<LoginBlob> authBlob;
std::string deviceId = "142137fd329622137a14901634264e6f332e2411";
public:
Session();
~Session();
std::shared_ptr<cspot::ShannonConnection> shanConn;
void connect(std::unique_ptr<cspot::PlainConnection> connection);
void connectWithRandomAp();
void close();
virtual bool triggerTimeout() = 0;
std::vector<uint8_t> authenticate(std::shared_ptr<LoginBlob> blob);
};
} // namespace cspot

View File

@@ -0,0 +1,43 @@
#ifndef SHANNON_H
#define SHANNON_H
#include <cstdint> // for uint32_t, uint8_t
#include <vector> // for vector
class Shannon {
public:
static constexpr unsigned int N = 16;
void key(const std::vector<uint8_t>& key); /* set key */
void nonce(const std::vector<uint8_t>& nonce); /* set Init Vector */
void stream(std::vector<uint8_t>& buf); /* stream cipher */
void maconly(std::vector<uint8_t>& buf); /* accumulate MAC */
void encrypt(std::vector<uint8_t>& buf); /* encrypt + MAC */
void decrypt(std::vector<uint8_t>& buf); /* finalize + MAC */
void finish(std::vector<uint8_t>& buf); /* finalise MAC */
private:
static constexpr unsigned int FOLD = Shannon::N;
static constexpr unsigned int INITKONST = 0x6996c53a;
static constexpr unsigned int KEYP = 13;
uint32_t R[Shannon::N];
uint32_t CRC[Shannon::N];
uint32_t initR[Shannon::N];
uint32_t konst;
uint32_t sbuf;
uint32_t mbuf;
int nbuf;
static uint32_t sbox1(uint32_t w);
static uint32_t sbox2(uint32_t w);
void cycle();
void crcfunc(uint32_t i);
void macfunc(uint32_t i);
void initState();
void saveState();
void reloadState();
void genkonst();
void diffuse();
void loadKey(const std::vector<uint8_t>& key);
};
#endif

View File

@@ -0,0 +1,41 @@
#ifndef SHANNONCONNECTION_H
#define SHANNONCONNECTION_H
#include <cstdint> // for uint8_t, uint32_t
#include <memory> // for shared_ptr, unique_ptr
#include <mutex> // for mutex
#include <vector> // for vector
#include "Packet.h" // for Packet
class Shannon;
namespace cspot {
class PlainConnection;
} // namespace cspot
#define MAC_SIZE 4
namespace cspot {
class ShannonConnection {
private:
std::unique_ptr<Shannon> sendCipher;
std::unique_ptr<Shannon> recvCipher;
uint32_t sendNonce = 0;
uint32_t recvNonce = 0;
std::vector<uint8_t> cipherPacket(uint8_t cmd, std::vector<uint8_t>& data);
std::mutex writeMutex;
std::mutex readMutex;
public:
ShannonConnection();
~ShannonConnection();
void wrapConnection(std::shared_ptr<PlainConnection> conn,
std::vector<uint8_t>& sendKey,
std::vector<uint8_t>& recvKey);
void sendPacket(uint8_t cmd, std::vector<uint8_t>& data);
std::shared_ptr<PlainConnection> conn;
Packet recvPacket();
};
} // namespace cspot
#endif

View File

@@ -0,0 +1,82 @@
#pragma once
#include <stdint.h> // for uint32_t, uint8_t
#include <functional> // for function
#include <memory> // for shared_ptr, unique_ptr
#include <string> // for string
#include <variant> // for variant
#include <vector> // for vector
#include "CDNAudioFile.h" // for CDNTrackStream, CDNTrackStream::Track...
#include "TrackQueue.h"
#include "protobuf/spirc.pb.h" // for MessageType
namespace cspot {
class TrackPlayer;
struct Context;
class SpircHandler {
public:
SpircHandler(std::shared_ptr<cspot::Context> ctx);
enum class EventType {
PLAY_PAUSE,
VOLUME,
TRACK_INFO,
DISC,
NEXT,
PREV,
SEEK,
DEPLETED,
FLUSH,
PLAYBACK_START
};
typedef std::variant<TrackInfo, int, bool> EventData;
struct Event {
EventType eventType;
EventData data;
};
typedef std::function<void(std::unique_ptr<Event>)> EventHandler;
void subscribeToMercury();
std::shared_ptr<TrackPlayer> getTrackPlayer();
void setEventHandler(EventHandler handler);
void setPause(bool pause);
bool previousSong();
bool nextSong();
void notifyAudioReachedPlayback();
void notifyAudioEnded();
void updatePositionMs(uint32_t position);
void setRemoteVolume(int volume);
void loadTrackFromURI(const std::string& uri);
std::shared_ptr<cspot::TrackQueue> getTrackQueue() { return trackQueue; }
void disconnect();
private:
std::shared_ptr<cspot::Context> ctx;
std::shared_ptr<cspot::TrackPlayer> trackPlayer;
std::shared_ptr<cspot::TrackQueue> trackQueue;
EventHandler eventHandler = nullptr;
std::shared_ptr<cspot::PlaybackState> playbackState;
void sendCmd(MessageType typ);
void sendEvent(EventType type);
void sendEvent(EventType type, EventData data);
bool skipSong(TrackQueue::SkipDirection dir);
void handleFrame(std::vector<uint8_t>& data);
void notify();
};
} // namespace cspot

View File

@@ -0,0 +1,32 @@
#pragma once
#include <stdint.h> // for uint8_t
#include <vector> // for vector
namespace cspot {
class TimeProvider {
private:
unsigned long long timestampDiff;
public:
/**
* @brief Bypasses the need for NTP server sync by syncing with spotify's servers
*
*/
TimeProvider();
/**
* @brief Syncs the TimeProvider with spotify server's timestamp
*
* @param pongPacket pong packet containing timestamp
*/
void syncWithPingPacket(const std::vector<uint8_t>& pongPacket);
/**
* @brief Get current timestamp synced with spotify servers
*
* @return unsigned long long timestamp
*/
unsigned long long getSyncedTimestamp();
};
} // namespace cspot

View File

@@ -0,0 +1,98 @@
#pragma once
#include <atomic> // for atomic
#include <cstdint> // for uint8_t, int64_t
#include <ctime> // for size_t, time
#include <functional> // for function
#include <memory> // for shared_ptr, unique_ptr
#include <mutex> // for mutex
#include <string_view> // for string_view
#include <vector> // for vector
#include "BellTask.h" // for Task
#include "CDNAudioFile.h"
#include "TrackQueue.h"
namespace bell {
class WrappedSemaphore;
} // namespace bell
#ifdef BELL_VORBIS_FLOAT
#include "vorbis/vorbisfile.h"
#else
#include "ivorbisfile.h" // for OggVorbis_File, ov_callbacks
#endif
namespace cspot {
class TrackProvider;
class TrackQueue;
struct Context;
struct TrackReference;
class TrackPlayer : bell::Task {
public:
// Callback types
typedef std::function<void(std::shared_ptr<QueuedTrack>, bool)>
TrackLoadedCallback;
typedef std::function<size_t(uint8_t*, size_t, std::string_view)>
DataCallback;
typedef std::function<void()> EOFCallback;
TrackPlayer(std::shared_ptr<cspot::Context> ctx,
std::shared_ptr<cspot::TrackQueue> trackQueue,
EOFCallback eofCallback, TrackLoadedCallback loadedCallback);
~TrackPlayer();
void loadTrackFromRef(TrackReference& ref, size_t playbackMs,
bool startAutomatically);
void setDataCallback(DataCallback callback);
// CDNTrackStream::TrackInfo getCurrentTrackInfo();
void seekMs(size_t ms);
void resetState(bool paused = false);
// Vorbis codec callbacks
size_t _vorbisRead(void* ptr, size_t size, size_t nmemb);
size_t _vorbisClose();
int _vorbisSeek(int64_t offset, int whence);
long _vorbisTell();
void stop();
void start();
private:
std::shared_ptr<cspot::Context> ctx;
std::shared_ptr<cspot::TrackQueue> trackQueue;
std::shared_ptr<cspot::CDNAudioFile> currentTrackStream;
std::unique_ptr<bell::WrappedSemaphore> playbackSemaphore;
TrackLoadedCallback trackLoaded;
DataCallback dataCallback = nullptr;
EOFCallback eofCallback;
// Playback control
std::atomic<bool> currentSongPlaying;
std::mutex playbackMutex;
std::mutex dataOutMutex;
// Vorbis related
OggVorbis_File vorbisFile;
ov_callbacks vorbisCallbacks;
int currentSection;
std::vector<uint8_t> pcmBuffer = std::vector<uint8_t>(1024);
bool autoStart = false;
std::atomic<bool> isRunning = false;
std::atomic<bool> pendingReset = false;
std::atomic<bool> inFuture = false;
std::atomic<size_t> pendingSeekPositionMs = 0;
std::atomic<bool> startPaused = false;
std::mutex runningMutex;
void runTask() override;
};
} // namespace cspot

View File

@@ -0,0 +1,134 @@
#pragma once
#include <stddef.h> // for size_t
#include <atomic>
#include <deque>
#include <functional>
#include <mutex>
#include "BellTask.h"
#include "PlaybackState.h"
#include "TrackReference.h"
#include "protobuf/metadata.pb.h" // for Track, _Track, AudioFile, Episode
namespace bell {
class WrappedSemaphore;
};
namespace cspot {
struct Context;
class AccessKeyFetcher;
class CDNAudioFile;
// Used in got track info event
struct TrackInfo {
std::string name, album, artist, imageUrl, trackId;
uint32_t duration, number, discNumber;
void loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid);
void loadPbEpisode(Episode* pbEpisode, const std::vector<uint8_t>& gid);
};
class QueuedTrack {
public:
QueuedTrack(TrackReference& ref, std::shared_ptr<cspot::Context> ctx,
uint32_t requestedPosition = 0);
~QueuedTrack();
enum class State {
QUEUED,
PENDING_META,
KEY_REQUIRED,
PENDING_KEY,
CDN_REQUIRED,
READY,
FAILED
};
std::shared_ptr<bell::WrappedSemaphore> loadedSemaphore;
State state = State::QUEUED; // Current state of the track
TrackReference ref; // Holds GID, URI and Context
TrackInfo trackInfo; // Full track information fetched from spotify, name etc
uint32_t requestedPosition;
std::string identifier;
bool loading = false;
// Will return nullptr if the track is not ready
std::shared_ptr<cspot::CDNAudioFile> getAudioFile();
// --- Steps ---
void stepLoadMetadata(
Track* pbTrack, Episode* pbEpisode, std::mutex& trackListMutex,
std::shared_ptr<bell::WrappedSemaphore> updateSemaphore);
void stepParseMetadata(Track* pbTrack, Episode* pbEpisode);
void stepLoadAudioFile(
std::mutex& trackListMutex,
std::shared_ptr<bell::WrappedSemaphore> updateSemaphore);
void stepLoadCDNUrl(const std::string& accessKey);
void expire();
private:
std::shared_ptr<cspot::Context> ctx;
uint64_t pendingMercuryRequest = 0;
uint32_t pendingAudioKeyRequest = 0;
std::vector<uint8_t> trackId, fileId, audioKey;
std::string cdnUrl;
};
class TrackQueue : public bell::Task {
public:
TrackQueue(std::shared_ptr<cspot::Context> ctx,
std::shared_ptr<cspot::PlaybackState> playbackState);
~TrackQueue();
enum class SkipDirection { NEXT, PREV };
std::shared_ptr<bell::WrappedSemaphore> playableSemaphore;
std::atomic<bool> notifyPending = false;
void runTask() override;
void stopTask();
bool hasTracks();
bool isFinished();
bool skipTrack(SkipDirection dir, bool expectNotify = true);
bool updateTracks(uint32_t requestedPosition = 0, bool initial = false);
TrackInfo getTrackInfo(std::string_view identifier);
std::shared_ptr<QueuedTrack> consumeTrack(
std::shared_ptr<QueuedTrack> prevSong, int& offset);
private:
static const int MAX_TRACKS_PRELOAD = 3;
std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher;
std::shared_ptr<PlaybackState> playbackState;
std::shared_ptr<cspot::Context> ctx;
std::shared_ptr<bell::WrappedSemaphore> processSemaphore;
std::deque<std::shared_ptr<QueuedTrack>> preloadedTracks;
std::vector<TrackReference> currentTracks;
std::mutex tracksMutex, runningMutex;
// PB data
Track pbTrack;
Episode pbEpisode;
std::string accessKey;
int16_t currentTracksIndex = -1;
bool isRunning = false;
void processTrack(std::shared_ptr<QueuedTrack> track);
bool queueNextTrack(int offset = 0, uint32_t positionMs = 0);
};
} // namespace cspot

View File

@@ -0,0 +1,36 @@
#pragma once
#include <pb_encode.h>
#include <optional>
#include <string_view>
#include <vector>
#include "NanoPBHelper.h"
#include "pb_decode.h"
#include "protobuf/spirc.pb.h"
namespace cspot {
struct TrackReference {
TrackReference();
// Resolved track GID
std::vector<uint8_t> gid;
std::string uri, context;
std::optional<bool> queued;
// Type identifier
enum class Type { TRACK, EPISODE };
Type type;
void decodeURI();
bool operator==(const TrackReference& other) const;
// Encodes list of track references into a pb structure, used by nanopb
static bool pbEncodeTrackList(pb_ostream_t* stream, const pb_field_t* field,
void* const* arg);
static bool pbDecodeTrackList(pb_istream_t* stream, const pb_field_t* field,
void** arg);
};
} // namespace cspot

View File

@@ -0,0 +1,120 @@
#ifndef UTILS_H
#define UTILS_H
#include <cstdio> // for snprintf, size_t
#include <vector> // for vector
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include "win32shim.h"
#else
#endif
#include <cstdint> // for uint8_t, uint64_t
#include <cstring> // for memcpy
#include <memory> // for unique_ptr
#include <stdexcept> // for runtime_error
#include <string> // for string
#define HMAC_SHA1_BLOCKSIZE 64
/**
* @brief Returns current timestamp
*
* @return unsigned long long resulting timestamp in milliseconds from unix time zero
*/
unsigned long long getCurrentTimestamp();
/**
* @brief portable 64bit equivalent of htons / htonl. aka endianess swap
*
* @param value input value to swap
* @return uint64_t swapped result
*/
uint64_t hton64(uint64_t value);
std::vector<uint8_t> bigNumDivide(std::vector<uint8_t> num, int n);
/**
* @brief Performs big number multiplication on two numbers
*
* @param num big num in vector format
* @param n secondary number
* @return std::vector<uint8_t> resulting number
*/
std::vector<uint8_t> bigNumMultiply(std::vector<uint8_t> num, int n);
/**
* @brief Performs big number addition on two numbers
*
* @param num big num in vector format
* @param n secondary number
* @return std::vector<uint8_t> resulting number
*/
std::vector<uint8_t> bigNumAdd(std::vector<uint8_t> num, int n);
unsigned char h2int(char c);
std::string urlDecode(std::string str);
/**
* @brief Converts provided hex string into binary data
*
* @param s string containing hex data
* @return std::vector<uint8_t> vector containing binary data
*/
std::vector<uint8_t> stringHexToBytes(const std::string& s);
/**
* @brief Converts provided bytes into a human readable hex string
*
* @param bytes vector containing binary data
* @return std::string string containing hex representation of inputted data
*/
std::string bytesToHexString(const std::vector<uint8_t>& bytes);
/**
* @brief Extracts given type from binary data
*
* @tparam T type to extract
* @param v vector containing binary data to extract from
* @param pos position offset
* @return T extracted type
*/
template <typename T>
T extract(const std::vector<unsigned char>& v, int pos) {
T value;
memcpy(&value, &v[pos], sizeof(T));
return value;
}
/**
* @brief Packs given type into binary data
*
* @tparam T type of data to pack
* @param data data to pack
* @return std::vector<uint8_t> resulting vector containing binary data
*/
template <typename T>
std::vector<uint8_t> pack(T data) {
std::vector<std::uint8_t> rawData((std::uint8_t*)&data,
(std::uint8_t*)&(data) + sizeof(T));
return rawData;
}
template <typename... Args>
std::string string_format(const std::string& format, Args... args) {
int size_s = std::snprintf(nullptr, 0, format.c_str(), args...) +
1; // Extra space for '\0'
if (size_s <= 0) {
throw std::runtime_error("Error during formatting.");
}
auto size = static_cast<size_t>(size_s);
std::unique_ptr<char[]> buf(new char[size]);
std::snprintf(buf.get(), size, format.c_str(), args...);
return std::string(buf.get(),
buf.get() + size - 1); // We don't want the '\0' inside
}
#endif