Files
squeezelite-esp32/components/spotify/cspot/src/TrackQueue.cpp
2023-05-07 00:12:02 +02:00

604 lines
16 KiB
C++

#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 = &currentTracks;
pbTrack = Track_init_zero;
pbEpisode = Episode_init_zero;
// Start the task
startTask();
};
TrackQueue::~TrackQueue() {
stopTask();
std::scoped_lock lock(tracksMutex);
pb_release(Track_fields, &pbTrack);
pb_release(Episode_fields, &pbEpisode);
}
TrackInfo TrackQueue::getTrackInfo(std::string_view identifier) {
for (auto& track : preloadedTracks) {
if (track->identifier == identifier)
return track->trackInfo;
}
return TrackInfo{};
}
void TrackQueue::runTask() {
isRunning = true;
std::scoped_lock lock(runningMutex);
std::deque<std::shared_ptr<QueuedTrack>> trackQueue;
while (isRunning) {
processSemaphore->twait(100);
// Make sure we have the newest access key
accessKey = accessKeyFetcher->getAccessKey();
int loadedIndex = currentTracksIndex;
// No tracks loaded yet
if (loadedIndex < 0) {
continue;
} else {
std::scoped_lock lock(tracksMutex);
trackQueue = preloadedTracks;
}
for (auto& track : trackQueue) {
if (track) {
this->processTrack(track);
}
}
}
}
void TrackQueue::stopTask() {
if (isRunning) {
isRunning = false;
processSemaphore->give();
std::scoped_lock lock(runningMutex);
}
}
std::shared_ptr<QueuedTrack> TrackQueue::consumeTrack(
std::shared_ptr<QueuedTrack> prevTrack, int& offset) {
std::scoped_lock lock(tracksMutex);
if (currentTracksIndex == -1 || currentTracksIndex >= currentTracks.size()) {
return nullptr;
}
// No previous track, return head
if (prevTrack == nullptr) {
offset = 0;
return preloadedTracks[0];
}
// if (currentTracksIndex + preloadedTracks.size() >= currentTracks.size()) {
// offset = -1;
// // Last track in queue
// return nullptr;
// }
auto prevTrackIter =
std::find(preloadedTracks.begin(), preloadedTracks.end(), prevTrack);
if (prevTrackIter != preloadedTracks.end()) {
// Get offset of next track
offset = prevTrackIter - preloadedTracks.begin() + 1;
} else {
offset = 0;
}
if (offset >= preloadedTracks.size()) {
// Last track in preloaded queue
return nullptr;
}
// Return the current track
return preloadedTracks[offset];
}
void TrackQueue::processTrack(std::shared_ptr<QueuedTrack> track) {
switch (track->state) {
case QueuedTrack::State::QUEUED:
track->stepLoadMetadata(&pbTrack, &pbEpisode, tracksMutex,
processSemaphore);
break;
case QueuedTrack::State::KEY_REQUIRED:
track->stepLoadAudioFile(tracksMutex, processSemaphore);
break;
case QueuedTrack::State::CDN_REQUIRED:
track->stepLoadCDNUrl(accessKey);
if (track->state == QueuedTrack::State::READY) {
if (preloadedTracks.size() < MAX_TRACKS_PRELOAD) {
// Queue a new track to preload
queueNextTrack(preloadedTracks.size());
}
}
break;
default:
// Do not perform any action
break;
}
}
bool TrackQueue::queueNextTrack(int offset, uint32_t positionMs) {
const int requestedRefIndex = offset + currentTracksIndex;
if (requestedRefIndex < 0 || requestedRefIndex >= currentTracks.size()) {
return false;
}
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);
}
}