TrackQueue.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. #include "TrackQueue.h"
  2. #include <pb_decode.h>
  3. #include <algorithm>
  4. #include <functional>
  5. #include <memory>
  6. #include <mutex>
  7. #include "AccessKeyFetcher.h"
  8. #include "BellTask.h"
  9. #include "CDNAudioFile.h"
  10. #include "CSpotContext.h"
  11. #include "HTTPClient.h"
  12. #include "Logger.h"
  13. #include "Utils.h"
  14. #include "WrappedSemaphore.h"
  15. #ifdef BELL_ONLY_CJSON
  16. #include "cJSON.h"
  17. #else
  18. #include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
  19. #include "nlohmann/json_fwd.hpp" // for json
  20. #endif
  21. #include "protobuf/metadata.pb.h"
  22. using namespace cspot;
  23. namespace TrackDataUtils {
  24. bool countryListContains(char* countryList, const char* country) {
  25. uint16_t countryList_length = strlen(countryList);
  26. for (int x = 0; x < countryList_length; x += 2) {
  27. if (countryList[x] == country[0] && countryList[x + 1] == country[1]) {
  28. return true;
  29. }
  30. }
  31. return false;
  32. }
  33. bool doRestrictionsApply(Restriction* restrictions, int count,
  34. const char* country) {
  35. for (int x = 0; x < count; x++) {
  36. if (restrictions[x].countries_allowed != nullptr) {
  37. return !countryListContains(restrictions[x].countries_allowed, country);
  38. }
  39. if (restrictions[x].countries_forbidden != nullptr) {
  40. return countryListContains(restrictions[x].countries_forbidden, country);
  41. }
  42. }
  43. return false;
  44. }
  45. bool canPlayTrack(Track& trackInfo, int altIndex, const char* country) {
  46. if (altIndex < 0) {
  47. } else {
  48. for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count;
  49. x++) {
  50. if (trackInfo.alternative[altIndex].restriction[x].countries_allowed !=
  51. nullptr) {
  52. return countryListContains(
  53. trackInfo.alternative[altIndex].restriction[x].countries_allowed,
  54. country);
  55. }
  56. if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden !=
  57. nullptr) {
  58. return !countryListContains(
  59. trackInfo.alternative[altIndex].restriction[x].countries_forbidden,
  60. country);
  61. }
  62. }
  63. }
  64. return true;
  65. }
  66. } // namespace TrackDataUtils
  67. void TrackInfo::loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid) {
  68. // Generate ID based on GID
  69. trackId = bytesToHexString(gid);
  70. name = std::string(pbTrack->name);
  71. if (pbTrack->artist_count > 0) {
  72. // Handle artist data
  73. artist = std::string(pbTrack->artist[0].name);
  74. }
  75. if (pbTrack->has_album) {
  76. // Handle album data
  77. album = std::string(pbTrack->album.name);
  78. if (pbTrack->album.has_cover_group &&
  79. pbTrack->album.cover_group.image_count > 0) {
  80. auto imageId =
  81. pbArrayToVector(pbTrack->album.cover_group.image[0].file_id);
  82. imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId);
  83. }
  84. }
  85. duration = pbTrack->duration;
  86. }
  87. void TrackInfo::loadPbEpisode(Episode* pbEpisode,
  88. const std::vector<uint8_t>& gid) {
  89. // Generate ID based on GID
  90. trackId = bytesToHexString(gid);
  91. name = std::string(pbEpisode->name);
  92. if (pbEpisode->covers->image_count > 0) {
  93. // Handle episode info
  94. auto imageId = pbArrayToVector(pbEpisode->covers->image[0].file_id);
  95. imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId);
  96. }
  97. duration = pbEpisode->duration;
  98. }
  99. QueuedTrack::QueuedTrack(TrackReference& ref,
  100. std::shared_ptr<cspot::Context> ctx,
  101. uint32_t requestedPosition)
  102. : requestedPosition(requestedPosition), ctx(ctx) {
  103. this->ref = ref;
  104. loadedSemaphore = std::make_shared<bell::WrappedSemaphore>();
  105. state = State::QUEUED;
  106. }
  107. QueuedTrack::~QueuedTrack() {
  108. state = State::FAILED;
  109. loadedSemaphore->give();
  110. if (pendingMercuryRequest != 0) {
  111. ctx->session->unregister(pendingMercuryRequest);
  112. }
  113. if (pendingAudioKeyRequest != 0) {
  114. ctx->session->unregisterAudioKey(pendingAudioKeyRequest);
  115. }
  116. }
  117. std::shared_ptr<cspot::CDNAudioFile> QueuedTrack::getAudioFile() {
  118. if (state != State::READY) {
  119. return nullptr;
  120. }
  121. return std::make_shared<cspot::CDNAudioFile>(cdnUrl, audioKey);
  122. }
  123. void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) {
  124. int alternativeCount, filesCount = 0;
  125. bool canPlay = false;
  126. AudioFile* selectedFiles = nullptr;
  127. const char* countryCode = ctx->config.countryCode.c_str();
  128. if (ref.type == TrackReference::Type::TRACK) {
  129. CSPOT_LOG(info, "Track name: %s", pbTrack->name);
  130. CSPOT_LOG(info, "Track duration: %d", pbTrack->duration);
  131. CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
  132. pbTrack->restriction_count);
  133. // Check if we can play the track, if not, try alternatives
  134. if (TrackDataUtils::doRestrictionsApply(
  135. pbTrack->restriction, pbTrack->restriction_count, countryCode)) {
  136. // Go through alternatives
  137. for (int x = 0; x < pbTrack->alternative_count; x++) {
  138. if (!TrackDataUtils::doRestrictionsApply(
  139. pbTrack->alternative[x].restriction,
  140. pbTrack->alternative[x].restriction_count, countryCode)) {
  141. selectedFiles = pbTrack->alternative[x].file;
  142. filesCount = pbTrack->alternative[x].file_count;
  143. trackId = pbArrayToVector(pbTrack->alternative[x].gid);
  144. break;
  145. }
  146. }
  147. } else {
  148. // We can play the track
  149. selectedFiles = pbTrack->file;
  150. filesCount = pbTrack->file_count;
  151. trackId = pbArrayToVector(pbTrack->gid);
  152. }
  153. if (trackId.size() > 0) {
  154. // Load track information
  155. trackInfo.loadPbTrack(pbTrack, trackId);
  156. }
  157. } else {
  158. // Handle episodes
  159. CSPOT_LOG(info, "Episode name: %s", pbEpisode->name);
  160. CSPOT_LOG(info, "Episode duration: %d", pbEpisode->duration);
  161. CSPOT_LOG(debug, "episodeInfo.restriction.size() = %d",
  162. pbEpisode->restriction_count);
  163. // Check if we can play the episode
  164. if (!TrackDataUtils::doRestrictionsApply(pbEpisode->restriction,
  165. pbEpisode->restriction_count,
  166. countryCode)) {
  167. selectedFiles = pbEpisode->file;
  168. filesCount = pbEpisode->file_count;
  169. trackId = pbArrayToVector(pbEpisode->gid);
  170. // Load track information
  171. trackInfo.loadPbEpisode(pbEpisode, trackId);
  172. }
  173. }
  174. // Find playable file
  175. for (int x = 0; x < filesCount; x++) {
  176. CSPOT_LOG(debug, "File format: %d", selectedFiles[x].format);
  177. if (selectedFiles[x].format == ctx->config.audioFormat) {
  178. fileId = pbArrayToVector(selectedFiles[x].file_id);
  179. break; // If file found stop searching
  180. }
  181. // Fallback to OGG Vorbis 96kbps
  182. if (fileId.size() == 0 &&
  183. selectedFiles[x].format == AudioFormat_OGG_VORBIS_96) {
  184. fileId = pbArrayToVector(selectedFiles[x].file_id);
  185. }
  186. }
  187. // No viable files found for playback
  188. if (fileId.size() == 0) {
  189. CSPOT_LOG(info, "File not available for playback");
  190. // no alternatives for song
  191. state = State::FAILED;
  192. loadedSemaphore->give();
  193. return;
  194. }
  195. // Assign track identifier
  196. identifier = bytesToHexString(fileId);
  197. state = State::KEY_REQUIRED;
  198. }
  199. void QueuedTrack::stepLoadAudioFile(
  200. std::mutex& trackListMutex,
  201. std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
  202. // Request audio key
  203. this->pendingAudioKeyRequest = ctx->session->requestAudioKey(
  204. trackId, fileId,
  205. [this, &trackListMutex, updateSemaphore](
  206. bool success, const std::vector<uint8_t>& audioKey) {
  207. std::scoped_lock lock(trackListMutex);
  208. if (success) {
  209. CSPOT_LOG(info, "Got audio key");
  210. this->audioKey =
  211. std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
  212. state = State::CDN_REQUIRED;
  213. } else {
  214. CSPOT_LOG(error, "Failed to get audio key");
  215. state = State::FAILED;
  216. loadedSemaphore->give();
  217. }
  218. updateSemaphore->give();
  219. });
  220. state = State::PENDING_KEY;
  221. }
  222. void QueuedTrack::stepLoadCDNUrl(const std::string& accessKey) {
  223. if (accessKey.size() == 0) {
  224. // Wait for access key
  225. return;
  226. }
  227. // Request CDN URL
  228. CSPOT_LOG(info, "Received access key, fetching CDN URL...");
  229. try {
  230. std::string requestUrl = string_format(
  231. "https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
  232. "%s?alt=json&product=9",
  233. bytesToHexString(fileId).c_str());
  234. auto req = bell::HTTPClient::get(
  235. requestUrl, {bell::HTTPClient::ValueHeader(
  236. {"Authorization", "Bearer " + accessKey})});
  237. // Wait for response
  238. std::string_view result = req->body();
  239. #ifdef BELL_ONLY_CJSON
  240. cJSON* jsonResult = cJSON_Parse(result.data());
  241. cdnUrl = cJSON_GetArrayItem(cJSON_GetObjectItem(jsonResult, "cdnurl"), 0)
  242. ->valuestring;
  243. cJSON_Delete(jsonResult);
  244. #else
  245. auto jsonResult = nlohmann::json::parse(result);
  246. cdnUrl = jsonResult["cdnurl"][0];
  247. #endif
  248. CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
  249. state = State::READY;
  250. loadedSemaphore->give();
  251. } catch (...) {
  252. CSPOT_LOG(error, "Cannot fetch CDN URL");
  253. state = State::FAILED;
  254. loadedSemaphore->give();
  255. }
  256. }
  257. void QueuedTrack::expire() {
  258. if (state != State::QUEUED) {
  259. state = State::FAILED;
  260. loadedSemaphore->give();
  261. }
  262. }
  263. void QueuedTrack::stepLoadMetadata(
  264. Track* pbTrack, Episode* pbEpisode, std::mutex& trackListMutex,
  265. std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
  266. // Prepare request ID
  267. std::string requestUrl = string_format(
  268. "hm://metadata/3/%s/%s",
  269. ref.type == TrackReference::Type::TRACK ? "track" : "episode",
  270. bytesToHexString(ref.gid).c_str());
  271. auto responseHandler = [this, pbTrack, pbEpisode, &trackListMutex,
  272. updateSemaphore](MercurySession::Response& res) {
  273. std::scoped_lock lock(trackListMutex);
  274. if (res.parts.size() == 0) {
  275. // Invalid metadata, cannot proceed
  276. state = State::FAILED;
  277. updateSemaphore->give();
  278. loadedSemaphore->give();
  279. return;
  280. }
  281. // Parse the metadata
  282. if (ref.type == TrackReference::Type::TRACK) {
  283. pb_release(Track_fields, pbTrack);
  284. pbDecode(*pbTrack, Track_fields, res.parts[0]);
  285. } else {
  286. pb_release(Episode_fields, pbEpisode);
  287. pbDecode(*pbEpisode, Episode_fields, res.parts[0]);
  288. }
  289. // Parse received metadata
  290. stepParseMetadata(pbTrack, pbEpisode);
  291. updateSemaphore->give();
  292. };
  293. // Execute the request
  294. pendingMercuryRequest = ctx->session->execute(
  295. MercurySession::RequestType::GET, requestUrl, responseHandler);
  296. // Set the state to pending
  297. state = State::PENDING_META;
  298. }
  299. TrackQueue::TrackQueue(std::shared_ptr<cspot::Context> ctx,
  300. std::shared_ptr<cspot::PlaybackState> state)
  301. : bell::Task("CSpotTrackQueue", 1024 * 32, 2, 1),
  302. playbackState(state),
  303. ctx(ctx) {
  304. accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
  305. processSemaphore = std::make_shared<bell::WrappedSemaphore>();
  306. playableSemaphore = std::make_shared<bell::WrappedSemaphore>();
  307. // Assign encode callback to track list
  308. playbackState->innerFrame.state.track.funcs.encode =
  309. &TrackReference::pbEncodeTrackList;
  310. playbackState->innerFrame.state.track.arg = &currentTracks;
  311. pbTrack = Track_init_zero;
  312. pbEpisode = Episode_init_zero;
  313. // Start the task
  314. startTask();
  315. };
  316. TrackQueue::~TrackQueue() {
  317. stopTask();
  318. std::scoped_lock lock(tracksMutex);
  319. pb_release(Track_fields, &pbTrack);
  320. pb_release(Episode_fields, &pbEpisode);
  321. }
  322. TrackInfo TrackQueue::getTrackInfo(std::string_view identifier) {
  323. for (auto& track : preloadedTracks) {
  324. if (track->identifier == identifier)
  325. return track->trackInfo;
  326. }
  327. return TrackInfo{};
  328. }
  329. void TrackQueue::runTask() {
  330. isRunning = true;
  331. std::scoped_lock lock(runningMutex);
  332. std::deque<std::shared_ptr<QueuedTrack>> trackQueue;
  333. while (isRunning) {
  334. processSemaphore->twait(100);
  335. // Make sure we have the newest access key
  336. accessKey = accessKeyFetcher->getAccessKey();
  337. int loadedIndex = currentTracksIndex;
  338. // No tracks loaded yet
  339. if (loadedIndex < 0) {
  340. continue;
  341. } else {
  342. std::scoped_lock lock(tracksMutex);
  343. trackQueue = preloadedTracks;
  344. }
  345. for (auto& track : trackQueue) {
  346. if (track) {
  347. this->processTrack(track);
  348. }
  349. }
  350. }
  351. }
  352. void TrackQueue::stopTask() {
  353. if (isRunning) {
  354. isRunning = false;
  355. processSemaphore->give();
  356. std::scoped_lock lock(runningMutex);
  357. }
  358. }
  359. std::shared_ptr<QueuedTrack> TrackQueue::consumeTrack(
  360. std::shared_ptr<QueuedTrack> prevTrack, int& offset) {
  361. std::scoped_lock lock(tracksMutex);
  362. if (currentTracksIndex == -1 || currentTracksIndex >= currentTracks.size()) {
  363. return nullptr;
  364. }
  365. // No previous track, return head
  366. if (prevTrack == nullptr) {
  367. offset = 0;
  368. return preloadedTracks[0];
  369. }
  370. // if (currentTracksIndex + preloadedTracks.size() >= currentTracks.size()) {
  371. // offset = -1;
  372. // // Last track in queue
  373. // return nullptr;
  374. // }
  375. auto prevTrackIter =
  376. std::find(preloadedTracks.begin(), preloadedTracks.end(), prevTrack);
  377. if (prevTrackIter != preloadedTracks.end()) {
  378. // Get offset of next track
  379. offset = prevTrackIter - preloadedTracks.begin() + 1;
  380. } else {
  381. offset = 0;
  382. }
  383. if (offset >= preloadedTracks.size()) {
  384. // Last track in preloaded queue
  385. return nullptr;
  386. }
  387. // Return the current track
  388. return preloadedTracks[offset];
  389. }
  390. void TrackQueue::processTrack(std::shared_ptr<QueuedTrack> track) {
  391. switch (track->state) {
  392. case QueuedTrack::State::QUEUED:
  393. track->stepLoadMetadata(&pbTrack, &pbEpisode, tracksMutex,
  394. processSemaphore);
  395. break;
  396. case QueuedTrack::State::KEY_REQUIRED:
  397. track->stepLoadAudioFile(tracksMutex, processSemaphore);
  398. break;
  399. case QueuedTrack::State::CDN_REQUIRED:
  400. track->stepLoadCDNUrl(accessKey);
  401. if (track->state == QueuedTrack::State::READY) {
  402. if (preloadedTracks.size() < MAX_TRACKS_PRELOAD) {
  403. // Queue a new track to preload
  404. queueNextTrack(preloadedTracks.size());
  405. }
  406. }
  407. break;
  408. default:
  409. // Do not perform any action
  410. break;
  411. }
  412. }
  413. bool TrackQueue::queueNextTrack(int offset, uint32_t positionMs) {
  414. const int requestedRefIndex = offset + currentTracksIndex;
  415. if (requestedRefIndex < 0 || requestedRefIndex >= currentTracks.size()) {
  416. return false;
  417. }
  418. if (offset < 0) {
  419. preloadedTracks.push_front(std::make_shared<QueuedTrack>(
  420. currentTracks[requestedRefIndex], ctx, positionMs));
  421. } else {
  422. preloadedTracks.push_back(std::make_shared<QueuedTrack>(
  423. currentTracks[requestedRefIndex], ctx, positionMs));
  424. }
  425. return true;
  426. }
  427. bool TrackQueue::skipTrack(SkipDirection dir, bool expectNotify) {
  428. bool canSkipNext = currentTracks.size() > currentTracksIndex + 1;
  429. bool canSkipPrev = currentTracksIndex > 0;
  430. if ((dir == SkipDirection::NEXT && canSkipNext) ||
  431. (dir == SkipDirection::PREV && canSkipPrev)) {
  432. std::scoped_lock lock(tracksMutex);
  433. if (dir == SkipDirection::NEXT) {
  434. preloadedTracks.pop_front();
  435. if (!queueNextTrack(preloadedTracks.size() + 1)) {
  436. CSPOT_LOG(info, "Failed to queue next track");
  437. }
  438. currentTracksIndex++;
  439. } else {
  440. queueNextTrack(-1);
  441. if (preloadedTracks.size() > MAX_TRACKS_PRELOAD) {
  442. preloadedTracks.pop_back();
  443. }
  444. currentTracksIndex--;
  445. }
  446. // Update frame data
  447. playbackState->innerFrame.state.playing_track_index = currentTracksIndex;
  448. if (expectNotify) {
  449. // Reset position to zero
  450. notifyPending = true;
  451. }
  452. return true;
  453. }
  454. return false;
  455. }
  456. bool TrackQueue::hasTracks() {
  457. std::scoped_lock lock(tracksMutex);
  458. return currentTracks.size() > 0;
  459. }
  460. bool TrackQueue::isFinished() {
  461. std::scoped_lock lock(tracksMutex);
  462. return currentTracksIndex >= currentTracks.size() - 1;
  463. }
  464. void TrackQueue::updateTracks(uint32_t requestedPosition, bool initial) {
  465. std::scoped_lock lock(tracksMutex);
  466. if (initial) {
  467. // Clear preloaded tracks
  468. preloadedTracks.clear();
  469. // Copy requested track list
  470. currentTracks = playbackState->remoteTracks;
  471. currentTracksIndex = playbackState->innerFrame.state.playing_track_index;
  472. if (currentTracksIndex < currentTracks.size()) {
  473. // Push a song on the preloaded queue
  474. queueNextTrack(0, requestedPosition);
  475. }
  476. // We already updated track meta, mark it
  477. notifyPending = true;
  478. playableSemaphore->give();
  479. } else {
  480. // Clear preloaded tracks
  481. preloadedTracks.clear();
  482. // Copy requested track list
  483. currentTracks = playbackState->remoteTracks;
  484. // Push a song on the preloaded queue
  485. queueNextTrack(0, requestedPosition);
  486. }
  487. }