CDNTrackStream.cpp 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. #include "CDNTrackStream.h"
  2. using namespace cspot;
  3. CDNTrackStream::CDNTrackStream(
  4. std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher) {
  5. this->accessKeyFetcher = accessKeyFetcher;
  6. this->status = Status::INITIALIZING;
  7. this->trackReady = std::make_unique<bell::WrappedSemaphore>(5);
  8. this->crypto = std::make_unique<Crypto>();
  9. }
  10. CDNTrackStream::~CDNTrackStream() {
  11. }
  12. void CDNTrackStream::fail() {
  13. this->status = Status::FAILED;
  14. this->trackReady->give();
  15. }
  16. void CDNTrackStream::fetchFile(const std::vector<uint8_t>& trackId,
  17. const std::vector<uint8_t>& audioKey) {
  18. this->status = Status::HAS_DATA;
  19. this->trackId = trackId;
  20. this->audioKey = std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
  21. accessKeyFetcher->getAccessKey([this, trackId, audioKey](std::string key) {
  22. CSPOT_LOG(info, "Received access key, fetching CDN URL...");
  23. std::string requestUrl = string_format(
  24. "https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
  25. "%s?alt=json&product=9",
  26. bytesToHexString(trackId).c_str());
  27. auto req = bell::HTTPClient::get(
  28. requestUrl,
  29. {bell::HTTPClient::ValueHeader({"Authorization", "Bearer " + key})});
  30. std::string_view result = req->body();
  31. #ifdef BELL_ONLY_CJSON
  32. cJSON* jsonResult = cJSON_Parse(result.data());
  33. std::string cdnUrl = cJSON_GetArrayItem(cJSON_GetObjectItem(jsonResult, "cdnurl"), 0)->valuestring;
  34. cJSON_Delete(jsonResult);
  35. #else
  36. auto jsonResult = nlohmann::json::parse(result);
  37. std::string cdnUrl = jsonResult["cdnurl"][0];
  38. #endif
  39. if (this->status != Status::FAILED) {
  40. this->cdnUrl = cdnUrl;
  41. this->status = Status::HAS_URL;
  42. CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
  43. this->openStream();
  44. this->trackReady->give();
  45. }
  46. });
  47. }
  48. size_t CDNTrackStream::getPosition() {
  49. return this->position;
  50. }
  51. void CDNTrackStream::seek(size_t newPos) {
  52. this->enableRequestMargin = true;
  53. this->position = newPos;
  54. }
  55. void CDNTrackStream::openStream() {
  56. CSPOT_LOG(info, "Opening HTTP stream to %s", this->cdnUrl.c_str());
  57. // Open connection, read first 128 bytes
  58. this->httpConnection = bell::HTTPClient::get(
  59. this->cdnUrl,
  60. {bell::HTTPClient::RangeHeader::range(0, OPUS_HEADER_SIZE - 1)});
  61. this->httpConnection->stream().read((char*)header.data(), OPUS_HEADER_SIZE);
  62. this->totalFileSize =
  63. this->httpConnection->totalLength() - SPOTIFY_OPUS_HEADER;
  64. this->decrypt(header.data(), OPUS_HEADER_SIZE, 0);
  65. // Location must be dividable by 16
  66. size_t footerStartLocation =
  67. (this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) -
  68. (this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) % 16;
  69. this->footer = std::vector<uint8_t>(
  70. this->totalFileSize - footerStartLocation + SPOTIFY_OPUS_HEADER);
  71. this->httpConnection->get(
  72. cdnUrl, {bell::HTTPClient::RangeHeader::last(footer.size())});
  73. this->httpConnection->stream().read((char*)footer.data(),
  74. this->footer.size());
  75. this->decrypt(footer.data(), footer.size(), footerStartLocation);
  76. CSPOT_LOG(info, "Header and footer bytes received");
  77. this->position = 0;
  78. this->lastRequestPosition = 0;
  79. this->lastRequestCapacity = 0;
  80. this->isConnected = true;
  81. }
  82. size_t CDNTrackStream::readBytes(uint8_t* dst, size_t bytes) {
  83. size_t offsetPosition = position + SPOTIFY_OPUS_HEADER;
  84. size_t actualFileSize = this->totalFileSize + SPOTIFY_OPUS_HEADER;
  85. if (position + bytes >= this->totalFileSize) {
  86. return 0;
  87. }
  88. // // Opus tries to read header, use prefetched data
  89. if (offsetPosition < OPUS_HEADER_SIZE &&
  90. bytes + offsetPosition <= OPUS_HEADER_SIZE) {
  91. memcpy(dst, this->header.data() + offsetPosition, bytes);
  92. position += bytes;
  93. return bytes;
  94. }
  95. // // Opus tries to read footer, use prefetched data
  96. if (offsetPosition >= (actualFileSize - this->footer.size())) {
  97. size_t toReadBytes = bytes;
  98. if ((position + bytes) > this->totalFileSize) {
  99. // Tries to read outside of bounds, truncate
  100. toReadBytes = this->totalFileSize - position;
  101. }
  102. size_t footerOffset =
  103. offsetPosition - (actualFileSize - this->footer.size());
  104. memcpy(dst, this->footer.data() + footerOffset, toReadBytes);
  105. position += toReadBytes;
  106. return toReadBytes;
  107. }
  108. // Data not in the headers. Make sense of whats going on.
  109. // Position in bounds :)
  110. if (offsetPosition >= this->lastRequestPosition &&
  111. offsetPosition < this->lastRequestPosition + this->lastRequestCapacity) {
  112. size_t toRead = bytes;
  113. if ((toRead + offsetPosition) >
  114. this->lastRequestPosition + lastRequestCapacity) {
  115. toRead = this->lastRequestPosition + lastRequestCapacity - offsetPosition;
  116. }
  117. memcpy(dst, this->httpBuffer.data() + offsetPosition - lastRequestPosition,
  118. toRead);
  119. position += toRead;
  120. return toRead;
  121. } else {
  122. size_t requestPosition = (offsetPosition) - ((offsetPosition) % 16);
  123. if (this->enableRequestMargin && requestPosition > SEEK_MARGIN_SIZE) {
  124. requestPosition = (offsetPosition - SEEK_MARGIN_SIZE) -
  125. ((offsetPosition - SEEK_MARGIN_SIZE) % 16);
  126. this->enableRequestMargin = false;
  127. }
  128. this->httpConnection->get(
  129. cdnUrl, {bell::HTTPClient::RangeHeader::range(
  130. requestPosition, requestPosition + HTTP_BUFFER_SIZE - 1)});
  131. this->lastRequestPosition = requestPosition;
  132. this->lastRequestCapacity = this->httpConnection->contentLength();
  133. this->httpConnection->stream().read((char*)this->httpBuffer.data(),
  134. lastRequestCapacity);
  135. this->decrypt(this->httpBuffer.data(), lastRequestCapacity,
  136. this->lastRequestPosition);
  137. return readBytes(dst, bytes);
  138. }
  139. return bytes;
  140. }
  141. size_t CDNTrackStream::getSize() {
  142. return this->totalFileSize;
  143. }
  144. void CDNTrackStream::decrypt(uint8_t* dst, size_t nbytes, size_t pos) {
  145. auto calculatedIV = bigNumAdd(audioAESIV, pos / 16);
  146. this->crypto->aesCTRXcrypt(this->audioKey, calculatedIV, dst, nbytes);
  147. }