|
@@ -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);
|
|
|
+ }
|
|
|
+}
|