TrackQueue.cpp 18 KB

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