mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2025-12-07 20:17:04 +03:00
add missing files, removing un-nedded ones
This commit is contained in:
@@ -16,41 +16,40 @@ class WrappedSemaphore;
|
||||
namespace cspot {
|
||||
class AccessKeyFetcher;
|
||||
|
||||
class CDNTrackStream {
|
||||
class CDNAudioFile {
|
||||
|
||||
public:
|
||||
CDNTrackStream(std::shared_ptr<cspot::AccessKeyFetcher>);
|
||||
~CDNTrackStream();
|
||||
|
||||
enum class Status { INITIALIZING, HAS_DATA, HAS_URL, FAILED };
|
||||
|
||||
struct TrackInfo {
|
||||
std::string trackId;
|
||||
std::string name;
|
||||
std::string album;
|
||||
std::string artist;
|
||||
std::string imageUrl;
|
||||
int duration;
|
||||
};
|
||||
|
||||
TrackInfo trackInfo;
|
||||
|
||||
Status status;
|
||||
std::unique_ptr<bell::WrappedSemaphore> trackReady;
|
||||
|
||||
void fetchFile(const std::vector<uint8_t>& trackId,
|
||||
const std::vector<uint8_t>& audioKey);
|
||||
|
||||
void fail();
|
||||
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:
|
||||
@@ -74,12 +73,9 @@ class CDNTrackStream {
|
||||
0x3f, 0x63, 0x0d, 0x93};
|
||||
std::unique_ptr<Crypto> crypto;
|
||||
|
||||
std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher;
|
||||
|
||||
std::unique_ptr<bell::HTTPClient::Response> httpConnection;
|
||||
bool isConnected = false;
|
||||
|
||||
size_t position = 0; // Spotify header size
|
||||
size_t position = 0;
|
||||
size_t totalFileSize = 0;
|
||||
size_t lastRequestPosition = 0;
|
||||
size_t lastRequestCapacity = 0;
|
||||
@@ -87,7 +83,6 @@ class CDNTrackStream {
|
||||
bool enableRequestMargin = false;
|
||||
|
||||
std::string cdnUrl;
|
||||
std::vector<uint8_t> trackId;
|
||||
std::vector<uint8_t> audioKey;
|
||||
|
||||
void decrypt(uint8_t* dst, size_t nbytes, size_t pos);
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h> // for uint8_t
|
||||
#include <memory> // for shared_ptr, unique_ptr, weak_ptr
|
||||
#include <vector> // for vector
|
||||
|
||||
#include "MercurySession.h" // for MercurySession
|
||||
#include "TrackReference.h" // for TrackReference
|
||||
#include "protobuf/metadata.pb.h" // for Episode, Restriction, Track
|
||||
|
||||
namespace cspot {
|
||||
class AccessKeyFetcher;
|
||||
class CDNTrackStream;
|
||||
struct Context;
|
||||
|
||||
class TrackProvider {
|
||||
public:
|
||||
TrackProvider(std::shared_ptr<cspot::Context> ctx);
|
||||
~TrackProvider();
|
||||
|
||||
std::shared_ptr<CDNTrackStream> loadFromTrackRef(TrackReference& trackRef);
|
||||
|
||||
private:
|
||||
std::shared_ptr<AccessKeyFetcher> accessKeyFetcher;
|
||||
std::shared_ptr<cspot::Context> ctx;
|
||||
std::unique_ptr<cspot::CDNTrackStream> cdnStream;
|
||||
|
||||
Track trackInfo;
|
||||
Episode episodeInfo;
|
||||
std::weak_ptr<CDNTrackStream> currentTrackReference;
|
||||
TrackReference trackIdInfo;
|
||||
|
||||
void queryMetadata();
|
||||
void onMetadataResponse(MercurySession::Response& res);
|
||||
bool doRestrictionsApply(Restriction* restrictions, int count);
|
||||
void fetchFile(const std::vector<uint8_t>& fileId,
|
||||
const std::vector<uint8_t>& trackId);
|
||||
bool canPlayTrack(int index);
|
||||
};
|
||||
} // namespace cspot
|
||||
134
components/spotify/cspot/include/TrackQueue.h
Normal file
134
components/spotify/cspot/include/TrackQueue.h
Normal file
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h> // for size_t
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
|
||||
#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;
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
void 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
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "CDNTrackStream.h"
|
||||
#include "CDNAudioFile.h"
|
||||
|
||||
#include <string.h> // for memcpy
|
||||
#include <functional> // for __base
|
||||
@@ -9,13 +9,14 @@
|
||||
|
||||
#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"
|
||||
#include "cJSON.h "
|
||||
#else
|
||||
#include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
|
||||
#include "nlohmann/json_fwd.hpp" // for json
|
||||
@@ -23,73 +24,22 @@
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
CDNTrackStream::CDNTrackStream(
|
||||
std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher) {
|
||||
this->accessKeyFetcher = accessKeyFetcher;
|
||||
this->status = Status::INITIALIZING;
|
||||
this->trackReady = std::make_unique<bell::WrappedSemaphore>(5);
|
||||
CDNAudioFile::CDNAudioFile(const std::string& cdnUrl,
|
||||
const std::vector<uint8_t>& audioKey)
|
||||
: cdnUrl(cdnUrl), audioKey(audioKey) {
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
}
|
||||
|
||||
CDNTrackStream::~CDNTrackStream() {}
|
||||
|
||||
void CDNTrackStream::fail() {
|
||||
this->status = Status::FAILED;
|
||||
this->trackReady->give();
|
||||
}
|
||||
|
||||
void CDNTrackStream::fetchFile(const std::vector<uint8_t>& trackId,
|
||||
const std::vector<uint8_t>& audioKey) {
|
||||
this->status = Status::HAS_DATA;
|
||||
this->trackId = trackId;
|
||||
this->audioKey = std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
|
||||
|
||||
accessKeyFetcher->getAccessKey([this, trackId, audioKey](std::string key) {
|
||||
CSPOT_LOG(info, "Received access key, fetching CDN URL...");
|
||||
|
||||
std::string requestUrl = string_format(
|
||||
"https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
|
||||
"%s?alt=json&product=9",
|
||||
bytesToHexString(trackId).c_str());
|
||||
|
||||
auto req = bell::HTTPClient::get(
|
||||
requestUrl,
|
||||
{bell::HTTPClient::ValueHeader({"Authorization", "Bearer " + key})});
|
||||
|
||||
std::string_view result = req->body();
|
||||
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* jsonResult = cJSON_Parse(result.data());
|
||||
std::string cdnUrl =
|
||||
cJSON_GetArrayItem(cJSON_GetObjectItem(jsonResult, "cdnurl"), 0)
|
||||
->valuestring;
|
||||
cJSON_Delete(jsonResult);
|
||||
#else
|
||||
auto jsonResult = nlohmann::json::parse(result);
|
||||
std::string cdnUrl = jsonResult["cdnurl"][0];
|
||||
#endif
|
||||
if (this->status != Status::FAILED) {
|
||||
|
||||
this->cdnUrl = cdnUrl;
|
||||
this->status = Status::HAS_URL;
|
||||
CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
|
||||
|
||||
this->openStream();
|
||||
this->trackReady->give();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
size_t CDNTrackStream::getPosition() {
|
||||
size_t CDNAudioFile::getPosition() {
|
||||
return this->position;
|
||||
}
|
||||
|
||||
void CDNTrackStream::seek(size_t newPos) {
|
||||
void CDNAudioFile::seek(size_t newPos) {
|
||||
this->enableRequestMargin = true;
|
||||
this->position = newPos;
|
||||
}
|
||||
|
||||
void CDNTrackStream::openStream() {
|
||||
void CDNAudioFile::openStream() {
|
||||
CSPOT_LOG(info, "Opening HTTP stream to %s", this->cdnUrl.c_str());
|
||||
|
||||
// Open connection, read first 128 bytes
|
||||
@@ -121,10 +71,9 @@ void CDNTrackStream::openStream() {
|
||||
this->position = 0;
|
||||
this->lastRequestPosition = 0;
|
||||
this->lastRequestCapacity = 0;
|
||||
this->isConnected = true;
|
||||
}
|
||||
|
||||
size_t CDNTrackStream::readBytes(uint8_t* dst, size_t bytes) {
|
||||
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;
|
||||
|
||||
@@ -199,11 +148,11 @@ size_t CDNTrackStream::readBytes(uint8_t* dst, size_t bytes) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
size_t CDNTrackStream::getSize() {
|
||||
size_t CDNAudioFile::getSize() {
|
||||
return this->totalFileSize;
|
||||
}
|
||||
|
||||
void CDNTrackStream::decrypt(uint8_t* dst, size_t nbytes, size_t pos) {
|
||||
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);
|
||||
@@ -1,245 +0,0 @@
|
||||
#include "TrackProvider.h"
|
||||
|
||||
#include <assert.h> // for assert
|
||||
#include <string.h> // for strlen
|
||||
#include <cstdint> // for uint8_t
|
||||
#include <functional> // for __base
|
||||
#include <memory> // for shared_ptr, weak_ptr, make_shared
|
||||
#include <string> // for string, operator+
|
||||
#include <type_traits> // for remove_extent_t
|
||||
|
||||
#include "AccessKeyFetcher.h" // for AccessKeyFetcher
|
||||
#include "BellLogger.h" // for AbstractLogger
|
||||
#include "CDNTrackStream.h" // for CDNTrackStream, CDNTrackStream::Tr...
|
||||
#include "CSpotContext.h" // for Context::ConfigState, Context (ptr...
|
||||
#include "Logger.h" // for CSPOT_LOG
|
||||
#include "MercurySession.h" // for MercurySession, MercurySession::Da...
|
||||
#include "NanoPBHelper.h" // for pbArrayToVector, pbDecode
|
||||
#include "Packet.h" // for cspot
|
||||
#include "TrackReference.h" // for TrackReference, TrackReference::Type
|
||||
#include "Utils.h" // for bytesToHexString, string_format
|
||||
#include "WrappedSemaphore.h" // for WrappedSemaphore
|
||||
#include "pb_decode.h" // for pb_release
|
||||
#include "protobuf/metadata.pb.h" // for Track, _Track, AudioFile, Episode
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
TrackProvider::TrackProvider(std::shared_ptr<cspot::Context> ctx) {
|
||||
this->accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
|
||||
this->ctx = ctx;
|
||||
this->cdnStream =
|
||||
std::make_unique<cspot::CDNTrackStream>(this->accessKeyFetcher);
|
||||
|
||||
this->trackInfo = {};
|
||||
}
|
||||
|
||||
TrackProvider::~TrackProvider() {
|
||||
pb_release(Track_fields, &trackInfo);
|
||||
pb_release(Episode_fields, &trackInfo);
|
||||
}
|
||||
|
||||
std::shared_ptr<cspot::CDNTrackStream> TrackProvider::loadFromTrackRef(
|
||||
TrackReference& trackRef) {
|
||||
auto track = std::make_shared<cspot::CDNTrackStream>(this->accessKeyFetcher);
|
||||
this->currentTrackReference = track;
|
||||
this->trackIdInfo = trackRef;
|
||||
|
||||
queryMetadata();
|
||||
return track;
|
||||
}
|
||||
|
||||
void TrackProvider::queryMetadata() {
|
||||
std::string requestUrl = string_format(
|
||||
"hm://metadata/3/%s/%s",
|
||||
trackIdInfo.type == TrackReference::Type::TRACK ? "track" : "episode",
|
||||
bytesToHexString(trackIdInfo.gid).c_str());
|
||||
CSPOT_LOG(debug, "Requesting track metadata from %s", requestUrl.c_str());
|
||||
|
||||
auto responseHandler = [this](MercurySession::Response& res) {
|
||||
this->onMetadataResponse(res);
|
||||
};
|
||||
|
||||
// Execute the request
|
||||
ctx->session->execute(MercurySession::RequestType::GET, requestUrl,
|
||||
responseHandler);
|
||||
}
|
||||
|
||||
void TrackProvider::onMetadataResponse(MercurySession::Response& res) {
|
||||
CSPOT_LOG(debug, "Got track metadata response");
|
||||
|
||||
int alternativeCount, filesCount = 0;
|
||||
bool canPlay = false;
|
||||
AudioFile* selectedFiles;
|
||||
std::vector<uint8_t> trackId, fileId;
|
||||
|
||||
if (trackIdInfo.type == TrackReference::Type::TRACK) {
|
||||
pb_release(Track_fields, &trackInfo);
|
||||
assert(res.parts.size() > 0);
|
||||
pbDecode(trackInfo, Track_fields, res.parts[0]);
|
||||
CSPOT_LOG(info, "Track name: %s", trackInfo.name);
|
||||
CSPOT_LOG(info, "Track duration: %d", trackInfo.duration);
|
||||
|
||||
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
|
||||
trackInfo.restriction_count);
|
||||
|
||||
if (doRestrictionsApply(trackInfo.restriction,
|
||||
trackInfo.restriction_count)) {
|
||||
// Go through alternatives
|
||||
for (int x = 0; x < trackInfo.alternative_count; x++) {
|
||||
if (!doRestrictionsApply(trackInfo.alternative[x].restriction,
|
||||
trackInfo.alternative[x].restriction_count)) {
|
||||
selectedFiles = trackInfo.alternative[x].file;
|
||||
filesCount = trackInfo.alternative[x].file_count;
|
||||
trackId = pbArrayToVector(trackInfo.alternative[x].gid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedFiles = trackInfo.file;
|
||||
filesCount = trackInfo.file_count;
|
||||
trackId = pbArrayToVector(trackInfo.gid);
|
||||
}
|
||||
|
||||
// Set track's metadata
|
||||
auto trackRef = this->currentTrackReference.lock();
|
||||
|
||||
auto imageId =
|
||||
pbArrayToVector(trackInfo.album.cover_group.image[0].file_id);
|
||||
|
||||
trackRef->trackInfo.trackId = bytesToHexString(trackIdInfo.gid);
|
||||
trackRef->trackInfo.name = std::string(trackInfo.name);
|
||||
trackRef->trackInfo.album = std::string(trackInfo.album.name);
|
||||
trackRef->trackInfo.artist = std::string(trackInfo.artist[0].name);
|
||||
trackRef->trackInfo.imageUrl =
|
||||
"https://i.scdn.co/image/" + bytesToHexString(imageId);
|
||||
trackRef->trackInfo.duration = trackInfo.duration;
|
||||
} else {
|
||||
pb_release(Episode_fields, &episodeInfo);
|
||||
assert(res.parts.size() > 0);
|
||||
pbDecode(episodeInfo, Episode_fields, res.parts[0]);
|
||||
|
||||
CSPOT_LOG(info, "Episode name: %s", episodeInfo.name);
|
||||
CSPOT_LOG(info, "Episode duration: %d", episodeInfo.duration);
|
||||
|
||||
CSPOT_LOG(debug, "episodeInfo.restriction.size() = %d",
|
||||
episodeInfo.restriction_count);
|
||||
if (!doRestrictionsApply(episodeInfo.restriction,
|
||||
episodeInfo.restriction_count)) {
|
||||
selectedFiles = episodeInfo.file;
|
||||
filesCount = episodeInfo.file_count;
|
||||
trackId = pbArrayToVector(episodeInfo.gid);
|
||||
}
|
||||
|
||||
auto trackRef = this->currentTrackReference.lock();
|
||||
|
||||
auto imageId = pbArrayToVector(episodeInfo.covers->image[0].file_id);
|
||||
|
||||
trackRef->trackInfo.trackId = bytesToHexString(trackIdInfo.gid);
|
||||
trackRef->trackInfo.name = std::string(episodeInfo.name);
|
||||
trackRef->trackInfo.album = "";
|
||||
trackRef->trackInfo.artist = "",
|
||||
trackRef->trackInfo.imageUrl =
|
||||
"https://i.scdn.co/image/" + bytesToHexString(imageId);
|
||||
trackRef->trackInfo.duration = episodeInfo.duration;
|
||||
}
|
||||
|
||||
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
|
||||
if (!this->currentTrackReference.expired()) {
|
||||
auto trackRef = this->currentTrackReference.lock();
|
||||
trackRef->status = CDNTrackStream::Status::FAILED;
|
||||
trackRef->trackReady->give();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->fetchFile(fileId, trackId);
|
||||
}
|
||||
|
||||
void TrackProvider::fetchFile(const std::vector<uint8_t>& fileId,
|
||||
const std::vector<uint8_t>& trackId) {
|
||||
ctx->session->requestAudioKey(
|
||||
trackId, fileId,
|
||||
[this, fileId](bool success, const std::vector<uint8_t>& audioKey) {
|
||||
if (success) {
|
||||
CSPOT_LOG(info, "Got audio key");
|
||||
if (!this->currentTrackReference.expired()) {
|
||||
auto ref = this->currentTrackReference.lock();
|
||||
ref->fetchFile(fileId, audioKey);
|
||||
}
|
||||
|
||||
} else {
|
||||
CSPOT_LOG(error, "Failed to get audio key");
|
||||
if (!this->currentTrackReference.expired()) {
|
||||
auto ref = this->currentTrackReference.lock();
|
||||
ref->fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool countryListContains(char* countryList, 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 TrackProvider::doRestrictionsApply(Restriction* restrictions, int count) {
|
||||
for (int x = 0; x < count; x++) {
|
||||
if (restrictions[x].countries_allowed != nullptr) {
|
||||
return !countryListContains(restrictions[x].countries_allowed,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
|
||||
if (restrictions[x].countries_forbidden != nullptr) {
|
||||
return countryListContains(restrictions[x].countries_forbidden,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TrackProvider::canPlayTrack(int altIndex) {
|
||||
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,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
|
||||
if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden !=
|
||||
nullptr) {
|
||||
return !countryListContains(
|
||||
trackInfo.alternative[altIndex].restriction[x].countries_forbidden,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
603
components/spotify/cspot/src/TrackQueue.cpp
Normal file
603
components/spotify/cspot/src/TrackQueue.cpp
Normal file
@@ -0,0 +1,603 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
duration = pbEpisode->duration;
|
||||
}
|
||||
|
||||
QueuedTrack::QueuedTrack(TrackReference& ref,
|
||||
std::shared_ptr<cspot::Context> ctx,
|
||||
uint32_t requestedPosition)
|
||||
: requestedPosition(requestedPosition), ctx(ctx) {
|
||||
this->ref = ref;
|
||||
|
||||
loadedSemaphore = std::make_shared<bell::WrappedSemaphore>();
|
||||
state = State::QUEUED;
|
||||
}
|
||||
|
||||
QueuedTrack::~QueuedTrack() {
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
|
||||
if (pendingMercuryRequest != 0) {
|
||||
ctx->session->unregister(pendingMercuryRequest);
|
||||
}
|
||||
|
||||
if (pendingAudioKeyRequest != 0) {
|
||||
ctx->session->unregisterAudioKey(pendingAudioKeyRequest);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<cspot::CDNAudioFile> QueuedTrack::getAudioFile() {
|
||||
if (state != State::READY) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_shared<cspot::CDNAudioFile>(cdnUrl, audioKey);
|
||||
}
|
||||
|
||||
void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) {
|
||||
int alternativeCount, filesCount = 0;
|
||||
bool canPlay = false;
|
||||
AudioFile* selectedFiles = nullptr;
|
||||
|
||||
const char* countryCode = ctx->config.countryCode.c_str();
|
||||
|
||||
if (ref.type == TrackReference::Type::TRACK) {
|
||||
CSPOT_LOG(info, "Track name: %s", pbTrack->name);
|
||||
CSPOT_LOG(info, "Track duration: %d", pbTrack->duration);
|
||||
|
||||
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
|
||||
pbTrack->restriction_count);
|
||||
|
||||
// Check if we can play the track, if not, try alternatives
|
||||
if (TrackDataUtils::doRestrictionsApply(
|
||||
pbTrack->restriction, pbTrack->restriction_count, countryCode)) {
|
||||
// Go through alternatives
|
||||
for (int x = 0; x < pbTrack->alternative_count; x++) {
|
||||
if (!TrackDataUtils::doRestrictionsApply(
|
||||
pbTrack->alternative[x].restriction,
|
||||
pbTrack->alternative[x].restriction_count, countryCode)) {
|
||||
selectedFiles = pbTrack->alternative[x].file;
|
||||
filesCount = pbTrack->alternative[x].file_count;
|
||||
trackId = pbArrayToVector(pbTrack->alternative[x].gid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We can play the track
|
||||
selectedFiles = pbTrack->file;
|
||||
filesCount = pbTrack->file_count;
|
||||
trackId = pbArrayToVector(pbTrack->gid);
|
||||
}
|
||||
|
||||
if (trackId.size() > 0) {
|
||||
// Load track information
|
||||
trackInfo.loadPbTrack(pbTrack, trackId);
|
||||
}
|
||||
} else {
|
||||
// Handle episodes
|
||||
CSPOT_LOG(info, "Episode name: %s", pbEpisode->name);
|
||||
CSPOT_LOG(info, "Episode duration: %d", pbEpisode->duration);
|
||||
|
||||
CSPOT_LOG(debug, "episodeInfo.restriction.size() = %d",
|
||||
pbEpisode->restriction_count);
|
||||
|
||||
// Check if we can play the episode
|
||||
if (!TrackDataUtils::doRestrictionsApply(pbEpisode->restriction,
|
||||
pbEpisode->restriction_count,
|
||||
countryCode)) {
|
||||
selectedFiles = pbEpisode->file;
|
||||
filesCount = pbEpisode->file_count;
|
||||
trackId = pbArrayToVector(pbEpisode->gid);
|
||||
|
||||
// Load track information
|
||||
trackInfo.loadPbEpisode(pbEpisode, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
// Find playable file
|
||||
for (int x = 0; x < filesCount; x++) {
|
||||
CSPOT_LOG(debug, "File format: %d", selectedFiles[x].format);
|
||||
if (selectedFiles[x].format == ctx->config.audioFormat) {
|
||||
fileId = pbArrayToVector(selectedFiles[x].file_id);
|
||||
break; // If file found stop searching
|
||||
}
|
||||
|
||||
// Fallback to OGG Vorbis 96kbps
|
||||
if (fileId.size() == 0 &&
|
||||
selectedFiles[x].format == AudioFormat_OGG_VORBIS_96) {
|
||||
fileId = pbArrayToVector(selectedFiles[x].file_id);
|
||||
}
|
||||
}
|
||||
|
||||
// No viable files found for playback
|
||||
if (fileId.size() == 0) {
|
||||
CSPOT_LOG(info, "File not available for playback");
|
||||
|
||||
// no alternatives for song
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign track identifier
|
||||
identifier = bytesToHexString(fileId);
|
||||
|
||||
state = State::KEY_REQUIRED;
|
||||
}
|
||||
|
||||
void QueuedTrack::stepLoadAudioFile(
|
||||
std::mutex& trackListMutex,
|
||||
std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
|
||||
// Request audio key
|
||||
this->pendingAudioKeyRequest = ctx->session->requestAudioKey(
|
||||
trackId, fileId,
|
||||
[this, &trackListMutex, updateSemaphore](
|
||||
bool success, const std::vector<uint8_t>& audioKey) {
|
||||
std::scoped_lock lock(trackListMutex);
|
||||
|
||||
if (success) {
|
||||
CSPOT_LOG(info, "Got audio key");
|
||||
this->audioKey =
|
||||
std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
|
||||
|
||||
state = State::CDN_REQUIRED;
|
||||
} else {
|
||||
CSPOT_LOG(error, "Failed to get audio key");
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
}
|
||||
updateSemaphore->give();
|
||||
});
|
||||
|
||||
state = State::PENDING_KEY;
|
||||
}
|
||||
|
||||
void QueuedTrack::stepLoadCDNUrl(const std::string& accessKey) {
|
||||
if (accessKey.size() == 0) {
|
||||
// Wait for access key
|
||||
return;
|
||||
}
|
||||
|
||||
// Request CDN URL
|
||||
CSPOT_LOG(info, "Received access key, fetching CDN URL...");
|
||||
|
||||
try {
|
||||
|
||||
std::string requestUrl = string_format(
|
||||
"https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
|
||||
"%s?alt=json&product=9",
|
||||
bytesToHexString(fileId).c_str());
|
||||
|
||||
auto req = bell::HTTPClient::get(
|
||||
requestUrl, {bell::HTTPClient::ValueHeader(
|
||||
{"Authorization", "Bearer " + accessKey})});
|
||||
|
||||
// Wait for response
|
||||
std::string_view result = req->body();
|
||||
|
||||
#ifdef BELL_ONLY_CJSON
|
||||
cJSON* jsonResult = cJSON_Parse(result.data());
|
||||
cdnUrl = cJSON_GetArrayItem(cJSON_GetObjectItem(jsonResult, "cdnurl"), 0)
|
||||
->valuestring;
|
||||
cJSON_Delete(jsonResult);
|
||||
#else
|
||||
auto jsonResult = nlohmann::json::parse(result);
|
||||
cdnUrl = jsonResult["cdnurl"][0];
|
||||
#endif
|
||||
|
||||
CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
|
||||
state = State::READY;
|
||||
loadedSemaphore->give();
|
||||
} catch (...) {
|
||||
CSPOT_LOG(error, "Cannot fetch CDN URL");
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
}
|
||||
}
|
||||
|
||||
void QueuedTrack::expire() {
|
||||
if (state != State::QUEUED) {
|
||||
state = State::FAILED;
|
||||
loadedSemaphore->give();
|
||||
}
|
||||
}
|
||||
|
||||
void QueuedTrack::stepLoadMetadata(
|
||||
Track* pbTrack, Episode* pbEpisode, std::mutex& trackListMutex,
|
||||
std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
|
||||
|
||||
// Prepare request ID
|
||||
std::string requestUrl = string_format(
|
||||
"hm://metadata/3/%s/%s",
|
||||
ref.type == TrackReference::Type::TRACK ? "track" : "episode",
|
||||
bytesToHexString(ref.gid).c_str());
|
||||
|
||||
auto responseHandler = [this, pbTrack, pbEpisode, &trackListMutex,
|
||||
updateSemaphore](MercurySession::Response& res) {
|
||||
std::scoped_lock lock(trackListMutex);
|
||||
|
||||
if (res.parts.size() == 0) {
|
||||
// Invalid metadata, cannot proceed
|
||||
state = State::FAILED;
|
||||
updateSemaphore->give();
|
||||
loadedSemaphore->give();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the metadata
|
||||
if (ref.type == TrackReference::Type::TRACK) {
|
||||
pb_release(Track_fields, pbTrack);
|
||||
pbDecode(*pbTrack, Track_fields, res.parts[0]);
|
||||
} else {
|
||||
pb_release(Episode_fields, pbEpisode);
|
||||
pbDecode(*pbEpisode, Episode_fields, res.parts[0]);
|
||||
}
|
||||
|
||||
// Parse received metadata
|
||||
stepParseMetadata(pbTrack, pbEpisode);
|
||||
|
||||
updateSemaphore->give();
|
||||
};
|
||||
// Execute the request
|
||||
pendingMercuryRequest = ctx->session->execute(
|
||||
MercurySession::RequestType::GET, requestUrl, responseHandler);
|
||||
|
||||
// Set the state to pending
|
||||
state = State::PENDING_META;
|
||||
}
|
||||
|
||||
TrackQueue::TrackQueue(std::shared_ptr<cspot::Context> ctx,
|
||||
std::shared_ptr<cspot::PlaybackState> state)
|
||||
: bell::Task("CSpotTrackQueue", 1024 * 32, 2, 1),
|
||||
playbackState(state),
|
||||
ctx(ctx) {
|
||||
accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
|
||||
processSemaphore = std::make_shared<bell::WrappedSemaphore>();
|
||||
playableSemaphore = std::make_shared<bell::WrappedSemaphore>();
|
||||
|
||||
// Assign encode callback to track list
|
||||
playbackState->innerFrame.state.track.funcs.encode =
|
||||
&TrackReference::pbEncodeTrackList;
|
||||
playbackState->innerFrame.state.track.arg = ¤tTracks;
|
||||
pbTrack = Track_init_zero;
|
||||
pbEpisode = Episode_init_zero;
|
||||
|
||||
// Start the task
|
||||
startTask();
|
||||
};
|
||||
|
||||
TrackQueue::~TrackQueue() {
|
||||
stopTask();
|
||||
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
pb_release(Track_fields, &pbTrack);
|
||||
pb_release(Episode_fields, &pbEpisode);
|
||||
}
|
||||
|
||||
TrackInfo TrackQueue::getTrackInfo(std::string_view identifier) {
|
||||
for (auto& track : preloadedTracks) {
|
||||
if (track->identifier == identifier)
|
||||
return track->trackInfo;
|
||||
}
|
||||
return TrackInfo{};
|
||||
}
|
||||
|
||||
void TrackQueue::runTask() {
|
||||
isRunning = true;
|
||||
|
||||
std::scoped_lock lock(runningMutex);
|
||||
|
||||
std::deque<std::shared_ptr<QueuedTrack>> trackQueue;
|
||||
|
||||
while (isRunning) {
|
||||
processSemaphore->twait(100);
|
||||
|
||||
// Make sure we have the newest access key
|
||||
accessKey = accessKeyFetcher->getAccessKey();
|
||||
|
||||
int loadedIndex = currentTracksIndex;
|
||||
|
||||
// No tracks loaded yet
|
||||
if (loadedIndex < 0) {
|
||||
continue;
|
||||
} else {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
trackQueue = preloadedTracks;
|
||||
}
|
||||
|
||||
for (auto& track : trackQueue) {
|
||||
if (track) {
|
||||
this->processTrack(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TrackQueue::stopTask() {
|
||||
if (isRunning) {
|
||||
isRunning = false;
|
||||
processSemaphore->give();
|
||||
std::scoped_lock lock(runningMutex);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QueuedTrack> TrackQueue::consumeTrack(
|
||||
std::shared_ptr<QueuedTrack> prevTrack, int& offset) {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
if (currentTracksIndex == -1 || currentTracksIndex >= currentTracks.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// No previous track, return head
|
||||
if (prevTrack == nullptr) {
|
||||
offset = 0;
|
||||
|
||||
return preloadedTracks[0];
|
||||
}
|
||||
|
||||
// if (currentTracksIndex + preloadedTracks.size() >= currentTracks.size()) {
|
||||
// offset = -1;
|
||||
|
||||
// // Last track in queue
|
||||
// return nullptr;
|
||||
// }
|
||||
|
||||
auto prevTrackIter =
|
||||
std::find(preloadedTracks.begin(), preloadedTracks.end(), prevTrack);
|
||||
|
||||
if (prevTrackIter != preloadedTracks.end()) {
|
||||
// Get offset of next track
|
||||
offset = prevTrackIter - preloadedTracks.begin() + 1;
|
||||
} else {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
if (offset >= preloadedTracks.size()) {
|
||||
// Last track in preloaded queue
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Return the current track
|
||||
return preloadedTracks[offset];
|
||||
}
|
||||
|
||||
void TrackQueue::processTrack(std::shared_ptr<QueuedTrack> track) {
|
||||
switch (track->state) {
|
||||
case QueuedTrack::State::QUEUED:
|
||||
track->stepLoadMetadata(&pbTrack, &pbEpisode, tracksMutex,
|
||||
processSemaphore);
|
||||
break;
|
||||
case QueuedTrack::State::KEY_REQUIRED:
|
||||
track->stepLoadAudioFile(tracksMutex, processSemaphore);
|
||||
break;
|
||||
case QueuedTrack::State::CDN_REQUIRED:
|
||||
track->stepLoadCDNUrl(accessKey);
|
||||
|
||||
if (track->state == QueuedTrack::State::READY) {
|
||||
if (preloadedTracks.size() < MAX_TRACKS_PRELOAD) {
|
||||
// Queue a new track to preload
|
||||
queueNextTrack(preloadedTracks.size());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Do not perform any action
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool TrackQueue::queueNextTrack(int offset, uint32_t positionMs) {
|
||||
const int requestedRefIndex = offset + currentTracksIndex;
|
||||
if (requestedRefIndex < 0 || requestedRefIndex >= currentTracks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 canSkipNext = currentTracks.size() > currentTracksIndex + 1;
|
||||
bool canSkipPrev = currentTracksIndex > 0;
|
||||
|
||||
if ((dir == SkipDirection::NEXT && canSkipNext) ||
|
||||
(dir == SkipDirection::PREV && canSkipPrev)) {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
if (dir == SkipDirection::NEXT) {
|
||||
preloadedTracks.pop_front();
|
||||
|
||||
if (!queueNextTrack(preloadedTracks.size() + 1)) {
|
||||
CSPOT_LOG(info, "Failed to queue next track");
|
||||
}
|
||||
|
||||
currentTracksIndex++;
|
||||
} else {
|
||||
queueNextTrack(-1);
|
||||
|
||||
if (preloadedTracks.size() > MAX_TRACKS_PRELOAD) {
|
||||
preloadedTracks.pop_back();
|
||||
}
|
||||
|
||||
currentTracksIndex--;
|
||||
}
|
||||
|
||||
// Update frame data
|
||||
playbackState->innerFrame.state.playing_track_index = currentTracksIndex;
|
||||
|
||||
if (expectNotify) {
|
||||
// Reset position to zero
|
||||
notifyPending = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void TrackQueue::updateTracks(uint32_t requestedPosition, bool initial) {
|
||||
std::scoped_lock lock(tracksMutex);
|
||||
|
||||
if (initial) {
|
||||
// Clear preloaded tracks
|
||||
preloadedTracks.clear();
|
||||
|
||||
// Copy requested track list
|
||||
currentTracks = playbackState->remoteTracks;
|
||||
|
||||
currentTracksIndex = playbackState->innerFrame.state.playing_track_index;
|
||||
|
||||
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 {
|
||||
// Clear preloaded tracks
|
||||
preloadedTracks.clear();
|
||||
|
||||
// Copy requested track list
|
||||
currentTracks = playbackState->remoteTracks;
|
||||
|
||||
// Push a song on the preloaded queue
|
||||
queueNextTrack(0, requestedPosition);
|
||||
}
|
||||
}
|
||||
156
components/spotify/cspot/src/TrackReference.cpp
Normal file
156
components/spotify/cspot/src/TrackReference.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
#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 __cplusplus >= 202002L
|
||||
if (uri.starts_with("episode")) {
|
||||
#else
|
||||
if (uri.find("episode") == 0) {
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user