spotify.js 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535
  1. import mongoose from "mongoose";
  2. import async from "async";
  3. import config from "config";
  4. import * as rax from "retry-axios";
  5. import axios from "axios";
  6. import url from "url";
  7. import CoreClass from "../core";
  8. let SpotifyModule;
  9. let SoundcloudModule;
  10. let DBModule;
  11. let CacheModule;
  12. let MediaModule;
  13. let MusicBrainzModule;
  14. let WikiDataModule;
  15. const youtubeVideoUrlRegex =
  16. /^(https?:\/\/)?(www\.)?(m\.)?(music\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?(?<youtubeId>[\w-]{11})((&([A-Za-z0-9]+)?)*)?$/;
  17. const spotifyTrackObjectToMusareTrackObject = spotifyTrackObject => ({
  18. trackId: spotifyTrackObject.id,
  19. name: spotifyTrackObject.name,
  20. albumId: spotifyTrackObject.album.id,
  21. albumTitle: spotifyTrackObject.album.title,
  22. albumImageUrl: spotifyTrackObject.album.images[0].url,
  23. artists: spotifyTrackObject.artists.map(artist => artist.name),
  24. artistIds: spotifyTrackObject.artists.map(artist => artist.id),
  25. duration: spotifyTrackObject.duration_ms / 1000,
  26. explicit: spotifyTrackObject.explicit,
  27. externalIds: spotifyTrackObject.external_ids,
  28. popularity: spotifyTrackObject.popularity,
  29. isLocal: spotifyTrackObject.is_local
  30. });
  31. class RateLimitter {
  32. /**
  33. * Constructor
  34. *
  35. * @param {number} timeBetween - The time between each allowed YouTube request
  36. */
  37. constructor(timeBetween) {
  38. this.dateStarted = Date.now();
  39. this.timeBetween = timeBetween;
  40. }
  41. /**
  42. * Returns a promise that resolves whenever the ratelimit of a YouTube request is done
  43. *
  44. * @returns {Promise} - promise that gets resolved when the rate limit allows it
  45. */
  46. continue() {
  47. return new Promise(resolve => {
  48. if (Date.now() - this.dateStarted >= this.timeBetween) resolve();
  49. else setTimeout(resolve, this.dateStarted + this.timeBetween - Date.now());
  50. });
  51. }
  52. /**
  53. * Restart the rate limit timer
  54. */
  55. restart() {
  56. this.dateStarted = Date.now();
  57. }
  58. }
  59. class _SpotifyModule extends CoreClass {
  60. // eslint-disable-next-line require-jsdoc
  61. constructor() {
  62. super("spotify");
  63. SpotifyModule = this;
  64. }
  65. /**
  66. * Initialises the spotify module
  67. *
  68. * @returns {Promise} - returns promise (reject, resolve)
  69. */
  70. async initialize() {
  71. DBModule = this.moduleManager.modules.db;
  72. CacheModule = this.moduleManager.modules.cache;
  73. MediaModule = this.moduleManager.modules.media;
  74. MusicBrainzModule = this.moduleManager.modules.musicbrainz;
  75. SoundcloudModule = this.moduleManager.modules.soundcloud;
  76. WikiDataModule = this.moduleManager.modules.wikidata;
  77. // this.youtubeApiRequestModel = this.YoutubeApiRequestModel = await DBModule.runJob("GET_MODEL", {
  78. // modelName: "youtubeApiRequest"
  79. // });
  80. this.spotifyTrackModel = this.SpotifyTrackModel = await DBModule.runJob("GET_MODEL", {
  81. modelName: "spotifyTrack"
  82. });
  83. this.spotifyAlbumModel = this.SpotifyAlbumModel = await DBModule.runJob("GET_MODEL", {
  84. modelName: "spotifyAlbum"
  85. });
  86. this.spotifyArtistModel = this.SpotifyArtistModel = await DBModule.runJob("GET_MODEL", {
  87. modelName: "spotifyArtist"
  88. });
  89. return new Promise((resolve, reject) => {
  90. if (!config.get("apis.spotify.enabled")) {
  91. reject(new Error("Spotify is not enabled."));
  92. return;
  93. }
  94. this.rateLimiter = new RateLimitter(config.get("apis.spotify.rateLimit"));
  95. this.requestTimeout = config.get("apis.spotify.requestTimeout");
  96. this.axios = axios.create();
  97. this.axios.defaults.raxConfig = {
  98. instance: this.axios,
  99. retry: config.get("apis.spotify.retryAmount"),
  100. noResponseRetries: config.get("apis.spotify.retryAmount")
  101. };
  102. rax.attach(this.axios);
  103. resolve();
  104. });
  105. }
  106. /**
  107. * Fetches a Spotify API token from either the cache, or Spotify using the client id and secret from the config
  108. *
  109. * @returns {Promise} - returns promise (reject, resolve)
  110. */
  111. GET_API_TOKEN() {
  112. return new Promise((resolve, reject) => {
  113. CacheModule.runJob("GET", { key: "spotifyApiKey" }, this).then(spotifyApiKey => {
  114. if (spotifyApiKey) {
  115. resolve(spotifyApiKey);
  116. return;
  117. }
  118. this.log("INFO", `No Spotify API token stored in cache, requesting new token.`);
  119. const clientId = config.get("apis.spotify.clientId");
  120. const clientSecret = config.get("apis.spotify.clientSecret");
  121. const unencoded = `${clientId}:${clientSecret}`;
  122. const encoded = Buffer.from(unencoded).toString("base64");
  123. const params = new url.URLSearchParams({ grant_type: "client_credentials" });
  124. SpotifyModule.axios
  125. .post("https://accounts.spotify.com/api/token", params.toString(), {
  126. headers: {
  127. Authorization: `Basic ${encoded}`,
  128. "Content-Type": "application/x-www-form-urlencoded"
  129. }
  130. })
  131. .then(res => {
  132. const { access_token: accessToken, expires_in: expiresIn } = res.data;
  133. // TODO TTL can be later if stuck in queue
  134. CacheModule.runJob(
  135. "SET",
  136. { key: "spotifyApiKey", value: accessToken, ttl: expiresIn - 30 },
  137. this
  138. )
  139. .then(spotifyApiKey => {
  140. this.log(
  141. "SUCCESS",
  142. `Stored new Spotify API token in cache. Expires in ${expiresIn - 30}`
  143. );
  144. resolve(spotifyApiKey);
  145. })
  146. .catch(err => {
  147. this.log(
  148. "ERROR",
  149. `Failed to store new Spotify API token in cache.`,
  150. typeof err === "string" ? err : err.message
  151. );
  152. reject(err);
  153. });
  154. })
  155. .catch(err => {
  156. this.log(
  157. "ERROR",
  158. `Failed to get new Spotify API token.`,
  159. typeof err === "string" ? err : err.message
  160. );
  161. reject(err);
  162. });
  163. });
  164. });
  165. }
  166. /**
  167. * Perform Spotify API get albums request
  168. *
  169. * @param {object} payload - object that contains the payload
  170. * @param {object} payload.albumIds - the album ids to get
  171. * @returns {Promise} - returns promise (reject, resolve)
  172. */
  173. API_GET_ALBUMS(payload) {
  174. return new Promise((resolve, reject) => {
  175. const { albumIds } = payload;
  176. SpotifyModule.runJob(
  177. "API_CALL",
  178. {
  179. url: `https://api.spotify.com/v1/albums`,
  180. params: {
  181. ids: albumIds.join(",")
  182. }
  183. },
  184. this
  185. )
  186. .then(response => {
  187. resolve(response);
  188. })
  189. .catch(err => {
  190. reject(err);
  191. });
  192. });
  193. }
  194. /**
  195. * Perform Spotify API get artists request
  196. *
  197. * @param {object} payload - object that contains the payload
  198. * @param {object} payload.artistIds - the artist ids to get
  199. * @returns {Promise} - returns promise (reject, resolve)
  200. */
  201. API_GET_ARTISTS(payload) {
  202. return new Promise((resolve, reject) => {
  203. const { artistIds } = payload;
  204. SpotifyModule.runJob(
  205. "API_CALL",
  206. {
  207. url: `https://api.spotify.com/v1/artists`,
  208. params: {
  209. ids: artistIds.join(",")
  210. }
  211. },
  212. this
  213. )
  214. .then(response => {
  215. resolve(response);
  216. })
  217. .catch(err => {
  218. reject(err);
  219. });
  220. });
  221. }
  222. /**
  223. * Perform Spotify API get track request
  224. *
  225. * @param {object} payload - object that contains the payload
  226. * @param {object} payload.params - request parameters
  227. * @returns {Promise} - returns promise (reject, resolve)
  228. */
  229. API_GET_TRACK(payload) {
  230. return new Promise((resolve, reject) => {
  231. const { trackId } = payload;
  232. SpotifyModule.runJob(
  233. "API_CALL",
  234. {
  235. url: `https://api.spotify.com/v1/tracks/${trackId}`
  236. },
  237. this
  238. )
  239. .then(response => {
  240. resolve(response);
  241. })
  242. .catch(err => {
  243. reject(err);
  244. });
  245. });
  246. }
  247. /**
  248. * Perform Spotify API get playlist request
  249. *
  250. * @param {object} payload - object that contains the payload
  251. * @param {object} payload.params - request parameters
  252. * @returns {Promise} - returns promise (reject, resolve)
  253. */
  254. API_GET_PLAYLIST(payload) {
  255. return new Promise((resolve, reject) => {
  256. const { playlistId, nextUrl } = payload;
  257. SpotifyModule.runJob(
  258. "API_CALL",
  259. {
  260. url: nextUrl || `https://api.spotify.com/v1/playlists/${playlistId}/tracks`
  261. },
  262. this
  263. )
  264. .then(response => {
  265. resolve(response);
  266. })
  267. .catch(err => {
  268. reject(err);
  269. });
  270. });
  271. }
  272. /**
  273. * Perform Spotify API call
  274. *
  275. * @param {object} payload - object that contains the payload
  276. * @param {object} payload.url - request url
  277. * @param {object} payload.params - request parameters
  278. * @param {object} payload.quotaCost - request quotaCost
  279. * @returns {Promise} - returns promise (reject, resolve)
  280. */
  281. API_CALL(payload) {
  282. return new Promise((resolve, reject) => {
  283. // const { url, params, quotaCost } = payload;
  284. const { url, params } = payload;
  285. SpotifyModule.runJob("GET_API_TOKEN", {}, this)
  286. .then(spotifyApiToken => {
  287. SpotifyModule.axios
  288. .get(url, {
  289. headers: {
  290. Authorization: `Bearer ${spotifyApiToken}`
  291. },
  292. timeout: SpotifyModule.requestTimeout,
  293. params
  294. })
  295. .then(response => {
  296. if (response.data.error) {
  297. reject(new Error(response.data.error));
  298. } else {
  299. resolve({ response });
  300. }
  301. })
  302. .catch(err => {
  303. reject(err);
  304. });
  305. })
  306. .catch(err => {
  307. this.log(
  308. "ERROR",
  309. `Spotify API call failed as an error occured whilst getting the API token`,
  310. typeof err === "string" ? err : err.message
  311. );
  312. resolve(err);
  313. });
  314. });
  315. }
  316. /**
  317. * Create Spotify track
  318. *
  319. * @param {object} payload - an object containing the payload
  320. * @param {string} payload.spotifyTracks - the spotifyTracks
  321. * @returns {Promise} - returns a promise (resolve, reject)
  322. */
  323. CREATE_TRACKS(payload) {
  324. return new Promise((resolve, reject) => {
  325. async.waterfall(
  326. [
  327. next => {
  328. const { spotifyTracks } = payload;
  329. if (!Array.isArray(spotifyTracks)) next("Invalid spotifyTracks type");
  330. else {
  331. const trackIds = spotifyTracks.map(spotifyTrack => spotifyTrack.trackId);
  332. SpotifyModule.spotifyTrackModel.find({ trackId: trackIds }, (err, existingTracks) => {
  333. if (err) {
  334. next(err);
  335. return;
  336. }
  337. const existingTrackIds = existingTracks.map(existingTrack => existingTrack.trackId);
  338. const newSpotifyTracks = spotifyTracks.filter(
  339. spotifyTrack => existingTrackIds.indexOf(spotifyTrack.trackId) === -1
  340. );
  341. SpotifyModule.spotifyTrackModel.insertMany(newSpotifyTracks, next);
  342. });
  343. }
  344. },
  345. (spotifyTracks, next) => {
  346. const mediaSources = spotifyTracks.map(spotifyTrack => `spotify:${spotifyTrack.trackId}`);
  347. async.eachLimit(
  348. mediaSources,
  349. 2,
  350. (mediaSource, next) => {
  351. MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }, this)
  352. .then(() => next())
  353. .catch(next);
  354. },
  355. err => {
  356. if (err) next(err);
  357. else next(null, spotifyTracks);
  358. }
  359. );
  360. }
  361. ],
  362. (err, spotifyTracks) => {
  363. if (err) reject(new Error(err));
  364. else resolve({ spotifyTracks });
  365. }
  366. );
  367. });
  368. }
  369. /**
  370. * Create Spotify albums
  371. *
  372. * @param {object} payload - an object containing the payload
  373. * @param {string} payload.spotifyAlbums - the Spotify albums
  374. * @returns {Promise} - returns a promise (resolve, reject)
  375. */
  376. async CREATE_ALBUMS(payload) {
  377. const { spotifyAlbums } = payload;
  378. if (!Array.isArray(spotifyAlbums)) throw new Error("Invalid spotifyAlbums type");
  379. const albumIds = spotifyAlbums.map(spotifyAlbum => spotifyAlbum.albumId);
  380. const existingAlbums = (await SpotifyModule.spotifyAlbumModel.find({ albumId: albumIds })).map(
  381. album => album._doc
  382. );
  383. const existingAlbumIds = existingAlbums.map(existingAlbum => existingAlbum.albumId);
  384. const newSpotifyAlbums = spotifyAlbums.filter(
  385. spotifyAlbum => existingAlbumIds.indexOf(spotifyAlbum.albumId) === -1
  386. );
  387. if (newSpotifyAlbums.length === 0) return existingAlbums;
  388. await SpotifyModule.spotifyAlbumModel.insertMany(newSpotifyAlbums);
  389. return existingAlbums.concat(newSpotifyAlbums);
  390. }
  391. /**
  392. * Create Spotify artists
  393. *
  394. * @param {object} payload - an object containing the payload
  395. * @param {string} payload.spotifyArtists - the Spotify artists
  396. * @returns {Promise} - returns a promise (resolve, reject)
  397. */
  398. async CREATE_ARTISTS(payload) {
  399. const { spotifyArtists } = payload;
  400. if (!Array.isArray(spotifyArtists)) throw new Error("Invalid spotifyArtists type");
  401. const artistIds = spotifyArtists.map(spotifyArtist => spotifyArtist.artistId);
  402. const existingArtists = (await SpotifyModule.spotifyArtistModel.find({ artistId: artistIds })).map(
  403. artist => artist._doc
  404. );
  405. const existingArtistIds = existingArtists.map(existingArtist => existingArtist.artistId);
  406. const newSpotifyArtists = spotifyArtists.filter(
  407. spotifyArtist => existingArtistIds.indexOf(spotifyArtist.artistId) === -1
  408. );
  409. if (newSpotifyArtists.length === 0) return existingArtists;
  410. await SpotifyModule.spotifyArtistModel.insertMany(newSpotifyArtists);
  411. return existingArtists.concat(newSpotifyArtists);
  412. }
  413. /**
  414. * Gets tracks from media sources
  415. *
  416. * @param {object} payload - object that contains the payload
  417. * @param {string} payload.mediaSources - the media sources to get tracks from
  418. * @returns {Promise} - returns promise (reject, resolve)
  419. */
  420. async GET_TRACKS_FROM_MEDIA_SOURCES(payload) {
  421. return new Promise((resolve, reject) => {
  422. const { mediaSources } = payload;
  423. const responses = {};
  424. const promises = [];
  425. mediaSources.forEach(mediaSource => {
  426. promises.push(
  427. new Promise(resolve => {
  428. const trackId = mediaSource.split(":")[1];
  429. SpotifyModule.runJob("GET_TRACK", { identifier: trackId, createMissing: true }, this)
  430. .then(({ track }) => {
  431. responses[mediaSource] = track;
  432. })
  433. .catch(err => {
  434. SpotifyModule.log(
  435. "ERROR",
  436. `Getting tracked with media source ${mediaSource} failed.`,
  437. typeof err === "string" ? err : err.message
  438. );
  439. responses[mediaSource] = typeof err === "string" ? err : err.message;
  440. })
  441. .finally(() => {
  442. resolve();
  443. });
  444. })
  445. );
  446. });
  447. Promise.all(promises)
  448. .then(() => {
  449. SpotifyModule.log("SUCCESS", `Got all tracks.`);
  450. resolve({ tracks: responses });
  451. })
  452. .catch(reject);
  453. });
  454. }
  455. /**
  456. * Gets albums from Spotify album ids
  457. *
  458. * @param {object} payload - object that contains the payload
  459. * @param {string} payload.albumIds - the Spotify album ids
  460. * @returns {Promise} - returns promise (reject, resolve)
  461. */
  462. async GET_ALBUMS_FROM_IDS(payload) {
  463. const { albumIds } = payload;
  464. const existingAlbums = (await SpotifyModule.spotifyAlbumModel.find({ albumId: albumIds })).map(
  465. album => album._doc
  466. );
  467. const existingAlbumIds = existingAlbums.map(existingAlbum => existingAlbum.albumId);
  468. const missingAlbumIds = albumIds.filter(albumId => existingAlbumIds.indexOf(albumId) === -1);
  469. if (missingAlbumIds.length === 0) return existingAlbums;
  470. const jobsToRun = [];
  471. const chunkSize = 2;
  472. while (missingAlbumIds.length > 0) {
  473. const chunkedMissingAlbumIds = missingAlbumIds.splice(0, chunkSize);
  474. jobsToRun.push(SpotifyModule.runJob("API_GET_ALBUMS", { albumIds: chunkedMissingAlbumIds }, this));
  475. }
  476. const jobResponses = await Promise.all(jobsToRun);
  477. const newAlbums = jobResponses
  478. .map(jobResponse => jobResponse.response.data.albums)
  479. .flat()
  480. .map(album => ({
  481. albumId: album.id,
  482. rawData: album
  483. }));
  484. await SpotifyModule.runJob("CREATE_ALBUMS", { spotifyAlbums: newAlbums }, this);
  485. return existingAlbums.concat(newAlbums);
  486. }
  487. /**
  488. * Gets Spotify artists from Spotify artist ids
  489. *
  490. * @param {object} payload - object that contains the payload
  491. * @param {string} payload.artistIds - the Spotify artist ids
  492. * @returns {Promise} - returns promise (reject, resolve)
  493. */
  494. async GET_ARTISTS_FROM_IDS(payload) {
  495. const { artistIds } = payload;
  496. const existingArtists = (await SpotifyModule.spotifyArtistModel.find({ artistId: artistIds })).map(
  497. artist => artist._doc
  498. );
  499. const existingArtistIds = existingArtists.map(existingArtist => existingArtist.artistId);
  500. const missingArtistIds = artistIds.filter(artistId => existingArtistIds.indexOf(artistId) === -1);
  501. if (missingArtistIds.length === 0) return existingArtists;
  502. const jobsToRun = [];
  503. const chunkSize = 50;
  504. while (missingArtistIds.length > 0) {
  505. const chunkedMissingArtistIds = missingArtistIds.splice(0, chunkSize);
  506. jobsToRun.push(SpotifyModule.runJob("API_GET_ARTISTS", { artistIds: chunkedMissingArtistIds }, this));
  507. }
  508. const jobResponses = await Promise.all(jobsToRun);
  509. const newArtists = jobResponses
  510. .map(jobResponse => jobResponse.response.data.artists)
  511. .flat()
  512. .map(artist => ({
  513. artistId: artist.id,
  514. rawData: artist
  515. }));
  516. await SpotifyModule.runJob("CREATE_ARTISTS", { spotifyArtists: newArtists }, this);
  517. return existingArtists.concat(newArtists);
  518. }
  519. /**
  520. * Get Spotify track
  521. *
  522. * @param {object} payload - an object containing the payload
  523. * @param {string} payload.identifier - the spotify track ObjectId or track id
  524. * @param {string} payload.createMissing - attempt to fetch and create track if not in db
  525. * @returns {Promise} - returns a promise (resolve, reject)
  526. */
  527. GET_TRACK(payload) {
  528. return new Promise((resolve, reject) => {
  529. async.waterfall(
  530. [
  531. next => {
  532. const query = mongoose.isObjectIdOrHexString(payload.identifier)
  533. ? { _id: payload.identifier }
  534. : { trackId: payload.identifier };
  535. return SpotifyModule.spotifyTrackModel.findOne(query, next);
  536. },
  537. (track, next) => {
  538. if (track) return next(null, track, false);
  539. if (mongoose.isObjectIdOrHexString(payload.identifier) || !payload.createMissing)
  540. return next("Spotify track not found.");
  541. return SpotifyModule.runJob("API_GET_TRACK", { trackId: payload.identifier }, this)
  542. .then(({ response }) => {
  543. const { data } = response;
  544. if (!data || !data.id)
  545. return next("The specified track does not exist or cannot be publicly accessed.");
  546. const spotifyTrack = spotifyTrackObjectToMusareTrackObject(data);
  547. return next(null, false, spotifyTrack);
  548. })
  549. .catch(next);
  550. },
  551. (track, spotifyTrack, next) => {
  552. if (track) return next(null, track, true);
  553. return SpotifyModule.runJob("CREATE_TRACKS", { spotifyTracks: [spotifyTrack] }, this)
  554. .then(res => {
  555. if (res.spotifyTracks.length === 1) next(null, res.spotifyTracks[0], false);
  556. else next("Spotify track not found.");
  557. })
  558. .catch(next);
  559. }
  560. ],
  561. (err, track, existing) => {
  562. if (err) reject(new Error(err));
  563. else if (track.isLocal) reject(new Error("Track is local."));
  564. else resolve({ track, existing });
  565. }
  566. );
  567. });
  568. }
  569. /**
  570. * Get Spotify album
  571. *
  572. * @param {object} payload - an object containing the payload
  573. * @param {string} payload.identifier - the spotify album ObjectId or track id
  574. * @returns {Promise} - returns a promise (resolve, reject)
  575. */
  576. async GET_ALBUM(payload) {
  577. const query = mongoose.isObjectIdOrHexString(payload.identifier)
  578. ? { _id: payload.identifier }
  579. : { albumId: payload.identifier };
  580. const album = await SpotifyModule.spotifyAlbumModel.findOne(query);
  581. if (album) return album._doc;
  582. return null;
  583. }
  584. /**
  585. * Returns an array of songs taken from a Spotify playlist
  586. *
  587. * @param {object} payload - object that contains the payload
  588. * @param {string} payload.url - the id of the Spotify playlist
  589. * @returns {Promise} - returns promise (reject, resolve)
  590. */
  591. GET_PLAYLIST(payload) {
  592. return new Promise((resolve, reject) => {
  593. const spotifyPlaylistUrlRegex = /.+open\.spotify\.com\/playlist\/(?<playlistId>[A-Za-z0-9]+)/;
  594. const match = spotifyPlaylistUrlRegex.exec(payload.url);
  595. if (!match || !match.groups) {
  596. SpotifyModule.log("ERROR", "GET_PLAYLIST", "Invalid Spotify playlist URL query.");
  597. reject(new Error("Invalid playlist URL."));
  598. return;
  599. }
  600. const { playlistId } = match.groups;
  601. async.waterfall(
  602. [
  603. next => {
  604. let spotifyTracks = [];
  605. let total = -1;
  606. let nextUrl = "";
  607. async.whilst(
  608. next => {
  609. SpotifyModule.log(
  610. "INFO",
  611. `Getting playlist progress for job (${this.toString()}): ${
  612. spotifyTracks.length
  613. } tracks gotten so far. Total tracks: ${total}.`
  614. );
  615. next(null, nextUrl !== null);
  616. },
  617. next => {
  618. // Add 250ms delay between each job request
  619. setTimeout(() => {
  620. SpotifyModule.runJob("API_GET_PLAYLIST", { playlistId, nextUrl }, this)
  621. .then(({ response }) => {
  622. const { data } = response;
  623. if (!data) {
  624. next("The provided URL does not exist or cannot be accessed.");
  625. return;
  626. }
  627. total = data.total;
  628. nextUrl = data.next;
  629. const { items } = data;
  630. const trackObjects = items.map(item => item.track);
  631. const newSpotifyTracks = trackObjects.map(trackObject =>
  632. spotifyTrackObjectToMusareTrackObject(trackObject)
  633. );
  634. spotifyTracks = spotifyTracks.concat(newSpotifyTracks);
  635. next();
  636. })
  637. .catch(err => next(err));
  638. }, 1000);
  639. },
  640. err => {
  641. if (err) next(err);
  642. else
  643. SpotifyModule.runJob("CREATE_TRACKS", { spotifyTracks }, this)
  644. .then(() => {
  645. next(
  646. null,
  647. spotifyTracks.map(spotifyTrack => spotifyTrack.trackId)
  648. );
  649. })
  650. .catch(next);
  651. }
  652. );
  653. }
  654. ],
  655. (err, soundcloudTrackIds) => {
  656. if (err && err !== true) {
  657. SpotifyModule.log(
  658. "ERROR",
  659. "GET_PLAYLIST",
  660. "Some error has occurred.",
  661. typeof err === "string" ? err : err.message
  662. );
  663. reject(new Error(typeof err === "string" ? err : err.message));
  664. } else {
  665. resolve({ songs: soundcloudTrackIds });
  666. }
  667. }
  668. );
  669. // kind;
  670. });
  671. }
  672. /**
  673. * Tries to get alternative artists sources for a list of Spotify artist ids
  674. *
  675. * @param {object} payload - object that contains the payload
  676. * @param {string} payload.artistIds - the Spotify artist ids to try and get alternative artist sources for
  677. * @param {string} payload.collectAlternativeArtistSourcesOrigins - whether to collect the origin of any alternative artist sources found
  678. * @returns {Promise} - returns promise (reject, resolve)
  679. */
  680. async GET_ALTERNATIVE_ARTIST_SOURCES_FOR_ARTISTS(payload) {
  681. const { artistIds, collectAlternativeArtistSourcesOrigins } = payload;
  682. await async.eachLimit(artistIds, 1, async artistId => {
  683. try {
  684. const result = await SpotifyModule.runJob(
  685. "GET_ALTERNATIVE_ARTIST_SOURCES_FOR_ARTIST",
  686. { artistId, collectAlternativeArtistSourcesOrigins },
  687. this
  688. );
  689. this.publishProgress({
  690. status: "working",
  691. message: `Got alternative artist source for ${artistId}`,
  692. data: {
  693. artistId,
  694. status: "success",
  695. result
  696. }
  697. });
  698. } catch (err) {
  699. this.publishProgress({
  700. status: "working",
  701. message: `Failed to get alternative artist source for ${artistId}`,
  702. data: {
  703. artistId,
  704. status: "error"
  705. }
  706. });
  707. }
  708. });
  709. this.publishProgress({
  710. status: "finished",
  711. message: `Finished getting alternative artist sources`
  712. });
  713. }
  714. /**
  715. * Tries to get alternative artist sources for a Spotify artist id
  716. *
  717. * @param {object} payload - object that contains the payload
  718. * @param {string} payload.artistId - the Spotify artist id to try and get alternative artist sources for
  719. * @param {string} payload.collectAlternativeArtistSourcesOrigins - whether to collect the origin of any alternative artist sources found
  720. * @returns {Promise} - returns promise (reject, resolve)
  721. */
  722. async GET_ALTERNATIVE_ARTIST_SOURCES_FOR_ARTIST(payload) {
  723. const { artistId /* , collectAlternativeArtistSourcesOrigins */ } = payload;
  724. if (!artistId) throw new Error("Artist id provided is not valid.");
  725. // const artist = await SpotifyModule.runJob(
  726. // "GET_ARTIST",
  727. // {
  728. // identifier: artistId
  729. // },
  730. // this
  731. // );
  732. const wikiDataResponse = await WikiDataModule.runJob(
  733. "API_GET_DATA_FROM_SPOTIFY_ARTIST",
  734. { spotifyArtistId: artistId },
  735. this
  736. );
  737. const youtubeChannelIds = Array.from(
  738. new Set(
  739. wikiDataResponse.results.bindings
  740. .filter(binding => !!binding.YouTube_channel_ID)
  741. .map(binding => binding.YouTube_channel_ID.value)
  742. )
  743. );
  744. // const soundcloudIds = Array.from(
  745. // new Set(
  746. // wikiDataResponse.results.bindings
  747. // .filter(binding => !!binding.SoundCloud_ID)
  748. // .map(binding => binding.SoundCloud_ID.value)
  749. // )
  750. // );
  751. // const musicbrainzArtistIds = Array.from(
  752. // new Set(
  753. // wikiDataResponse.results.bindings
  754. // .filter(binding => !!binding.MusicBrainz_artist_ID)
  755. // .map(binding => binding.MusicBrainz_artist_ID.value)
  756. // )
  757. // );
  758. return youtubeChannelIds;
  759. }
  760. /**
  761. * Tries to get alternative album sources for a list of Spotify album ids
  762. *
  763. * @param {object} payload - object that contains the payload
  764. * @param {string} payload.albumIds - the Spotify album ids to try and get alternative album sources for
  765. * @param {string} payload.collectAlternativeAlbumSourcesOrigins - whether to collect the origin of any alternative album sources found
  766. * @returns {Promise} - returns promise (reject, resolve)
  767. */
  768. async GET_ALTERNATIVE_ALBUM_SOURCES_FOR_ALBUMS(payload) {
  769. const { albumIds, collectAlternativeAlbumSourcesOrigins } = payload;
  770. await async.eachLimit(albumIds, 1, async albumId => {
  771. try {
  772. const result = await SpotifyModule.runJob(
  773. "GET_ALTERNATIVE_ALBUM_SOURCES_FOR_ALBUM",
  774. { albumId, collectAlternativeAlbumSourcesOrigins },
  775. this
  776. );
  777. this.publishProgress({
  778. status: "working",
  779. message: `Got alternative album source for ${albumId}`,
  780. data: {
  781. albumId,
  782. status: "success",
  783. result
  784. }
  785. });
  786. } catch (err) {
  787. this.publishProgress({
  788. status: "working",
  789. message: `Failed to get alternative album source for ${albumId}`,
  790. data: {
  791. albumId,
  792. status: "error"
  793. }
  794. });
  795. }
  796. });
  797. this.publishProgress({
  798. status: "finished",
  799. message: `Finished getting alternative album sources`
  800. });
  801. }
  802. /**
  803. * Tries to get alternative album sources for a Spotify album id
  804. *
  805. * @param {object} payload - object that contains the payload
  806. * @param {string} payload.albumId - the Spotify album id to try and get alternative album sources for
  807. * @param {string} payload.collectAlternativeAlbumSourcesOrigins - whether to collect the origin of any alternative album sources found
  808. * @returns {Promise} - returns promise (reject, resolve)
  809. */
  810. async GET_ALTERNATIVE_ALBUM_SOURCES_FOR_ALBUM(payload) {
  811. const { albumId /* , collectAlternativeAlbumSourcesOrigins */ } = payload;
  812. if (!albumId) throw new Error("Album id provided is not valid.");
  813. // const album = await SpotifyModule.runJob(
  814. // "GET_ALBUM",
  815. // {
  816. // identifier: albumId
  817. // },
  818. // this
  819. // );
  820. const wikiDataResponse = await WikiDataModule.runJob(
  821. "API_GET_DATA_FROM_SPOTIFY_ALBUM",
  822. { spotifyAlbumId: albumId },
  823. this
  824. );
  825. const youtubePlaylistIds = Array.from(
  826. new Set(
  827. wikiDataResponse.results.bindings
  828. .filter(binding => !!binding.YouTube_playlist_ID)
  829. .map(binding => binding.YouTube_playlist_ID.value)
  830. )
  831. );
  832. return youtubePlaylistIds;
  833. }
  834. /**
  835. * Tries to get alternative track sources for a list of Spotify track media sources
  836. *
  837. * @param {object} payload - object that contains the payload
  838. * @param {string} payload.mediaSources - the Spotify media sources to try and get alternative track sources for
  839. * @param {string} payload.collectAlternativeMediaSourcesOrigins - whether to collect the origin of any alternative track sources found
  840. * @returns {Promise} - returns promise (reject, resolve)
  841. */
  842. async GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACKS(payload) {
  843. const { mediaSources, collectAlternativeMediaSourcesOrigins } = payload;
  844. await async.eachLimit(mediaSources, 1, async mediaSource => {
  845. try {
  846. const result = await SpotifyModule.runJob(
  847. "GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACK",
  848. { mediaSource, collectAlternativeMediaSourcesOrigins },
  849. this
  850. );
  851. this.publishProgress({
  852. status: "working",
  853. message: `Got alternative media for ${mediaSource}`,
  854. data: {
  855. mediaSource,
  856. status: "success",
  857. result
  858. }
  859. });
  860. } catch (err) {
  861. this.publishProgress({
  862. status: "working",
  863. message: `Failed to get alternative media for ${mediaSource}`,
  864. data: {
  865. mediaSource,
  866. status: "error"
  867. }
  868. });
  869. }
  870. });
  871. this.publishProgress({
  872. status: "finished",
  873. message: `Finished getting alternative media`
  874. });
  875. }
  876. /**
  877. * Tries to get alternative track sources for a Spotify track media source
  878. *
  879. * @param {object} payload - object that contains the payload
  880. * @param {string} payload.mediaSource - the Spotify media source to try and get alternative track sources for
  881. * @param {string} payload.collectAlternativeMediaSourcesOrigins - whether to collect the origin of any alternative track sources found
  882. * @returns {Promise} - returns promise (reject, resolve)
  883. */
  884. async GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACK(payload) {
  885. const { mediaSource, collectAlternativeMediaSourcesOrigins } = payload;
  886. if (!mediaSource || !mediaSource.startsWith("spotify:"))
  887. throw new Error("Media source provided is not a valid Spotify media source.");
  888. const spotifyTrackId = mediaSource.split(":")[1];
  889. const { track: spotifyTrack } = await SpotifyModule.runJob(
  890. "GET_TRACK",
  891. {
  892. identifier: spotifyTrackId,
  893. createMissing: true
  894. },
  895. this
  896. );
  897. const ISRC = spotifyTrack.externalIds.isrc;
  898. if (!ISRC) throw new Error(`ISRC not found for Spotify track ${mediaSource}.`);
  899. const mediaSources = new Set();
  900. const mediaSourcesOrigins = {};
  901. const jobsToRun = [];
  902. try {
  903. const ISRCApiResponse = await MusicBrainzModule.runJob(
  904. "API_CALL",
  905. {
  906. url: `https://musicbrainz.org/ws/2/isrc/${ISRC}`,
  907. params: {
  908. fmt: "json",
  909. inc: "url-rels+work-rels"
  910. }
  911. },
  912. this
  913. );
  914. // console.log("ISRCApiResponse");
  915. // console.dir(ISRCApiResponse, { depth: 5 });
  916. ISRCApiResponse.recordings.forEach(recording => {
  917. recording.relations.forEach(relation => {
  918. if (relation["target-type"] === "url" && relation.url) {
  919. // relation["type-id"] === "7e41ef12-a124-4324-afdb-fdbae687a89c"
  920. const { resource } = relation.url;
  921. if (config.get("experimental.soundcloud") && resource.indexOf("soundcloud.com") !== -1) {
  922. // throw new Error(`Unable to parse SoundCloud resource ${resource}.`);
  923. const promise = new Promise(resolve => {
  924. SoundcloudModule.runJob(
  925. "GET_TRACK_FROM_URL",
  926. { identifier: resource, createMissing: true },
  927. this
  928. )
  929. .then(response => {
  930. const { trackId } = response.track;
  931. const mediaSource = `soundcloud:${trackId}`;
  932. mediaSources.add(mediaSource);
  933. if (collectAlternativeMediaSourcesOrigins) {
  934. const mediaSourceOrigins = [
  935. `Spotify track ${spotifyTrackId}`,
  936. `ISRC ${ISRC}`,
  937. `MusicBrainz recordings`,
  938. `MusicBrainz recording ${recording.id}`,
  939. `MusicBrainz relations`,
  940. `MusicBrainz relation target-type url`,
  941. `MusicBrainz relation resource ${resource}`,
  942. `SoundCloud ID ${trackId}`
  943. ];
  944. if (!mediaSourcesOrigins[mediaSource])
  945. mediaSourcesOrigins[mediaSource] = [];
  946. mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  947. }
  948. resolve();
  949. })
  950. .catch(() => {
  951. resolve();
  952. });
  953. });
  954. jobsToRun.push(promise);
  955. return;
  956. }
  957. if (resource.indexOf("youtube.com") !== -1 || resource.indexOf("youtu.be") !== -1) {
  958. const match = youtubeVideoUrlRegex.exec(resource);
  959. if (!match) throw new Error(`Unable to parse YouTube resource ${resource}.`);
  960. const { youtubeId } = match.groups;
  961. if (!youtubeId) throw new Error(`Unable to parse YouTube resource ${resource}.`);
  962. const mediaSource = `youtube:${youtubeId}`;
  963. mediaSources.add(mediaSource);
  964. if (collectAlternativeMediaSourcesOrigins) {
  965. const mediaSourceOrigins = [
  966. `Spotify track ${spotifyTrackId}`,
  967. `ISRC ${ISRC}`,
  968. `MusicBrainz recordings`,
  969. `MusicBrainz recording ${recording.id}`,
  970. `MusicBrainz relations`,
  971. `MusicBrainz relation target-type url`,
  972. `MusicBrainz relation resource ${resource}`,
  973. `YouTube ID ${youtubeId}`
  974. ];
  975. if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  976. mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  977. }
  978. return;
  979. }
  980. return;
  981. }
  982. if (relation["target-type"] === "work") {
  983. const promise = new Promise(resolve => {
  984. WikiDataModule.runJob(
  985. "API_GET_DATA_FROM_MUSICBRAINZ_WORK",
  986. { workId: relation.work.id },
  987. this
  988. )
  989. .then(resultBody => {
  990. const youtubeIds = Array.from(
  991. new Set(
  992. resultBody.results.bindings
  993. .filter(binding => !!binding.YouTube_video_ID)
  994. .map(binding => binding.YouTube_video_ID.value)
  995. )
  996. );
  997. // const soundcloudIds = Array.from(
  998. // new Set(
  999. // resultBody.results.bindings
  1000. // .filter(binding => !!binding["SoundCloud_track_ID"])
  1001. // .map(binding => binding["SoundCloud_track_ID"].value)
  1002. // )
  1003. // );
  1004. const musicVideoEntityUrls = Array.from(
  1005. new Set(
  1006. resultBody.results.bindings
  1007. .filter(binding => !!binding.Music_video_entity_URL)
  1008. .map(binding => binding.Music_video_entity_URL.value)
  1009. )
  1010. );
  1011. youtubeIds.forEach(youtubeId => {
  1012. const mediaSource = `youtube:${youtubeId}`;
  1013. mediaSources.add(mediaSource);
  1014. if (collectAlternativeMediaSourcesOrigins) {
  1015. const mediaSourceOrigins = [
  1016. `Spotify track ${spotifyTrackId}`,
  1017. `ISRC ${ISRC}`,
  1018. `MusicBrainz recordings`,
  1019. `MusicBrainz recording ${recording.id}`,
  1020. `MusicBrainz relations`,
  1021. `MusicBrainz relation target-type work`,
  1022. `MusicBrainz relation work id ${relation.work.id}`,
  1023. `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1024. `YouTube ID ${youtubeId}`
  1025. ];
  1026. if (!mediaSourcesOrigins[mediaSource])
  1027. mediaSourcesOrigins[mediaSource] = [];
  1028. mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1029. }
  1030. });
  1031. // soundcloudIds.forEach(soundcloudId => {
  1032. // const mediaSource = `soundcloud:${soundcloudId}`;
  1033. // mediaSources.add(mediaSource);
  1034. // if (collectAlternativeMediaSourcesOrigins) {
  1035. // const mediaSourceOrigins = [
  1036. // `Spotify track ${spotifyTrackId}`,
  1037. // `ISRC ${ISRC}`,
  1038. // `MusicBrainz recordings`,
  1039. // `MusicBrainz recording ${recording.id}`,
  1040. // `MusicBrainz relations`,
  1041. // `MusicBrainz relation target-type work`,
  1042. // `MusicBrainz relation work id ${relation.work.id}`,
  1043. // `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1044. // `SoundCloud ID ${soundcloudId}`
  1045. // ];
  1046. // if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  1047. // mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1048. // }
  1049. // });
  1050. const promisesToRun2 = [];
  1051. musicVideoEntityUrls.forEach(musicVideoEntityUrl => {
  1052. promisesToRun2.push(
  1053. new Promise(resolve => {
  1054. WikiDataModule.runJob(
  1055. "API_GET_DATA_FROM_ENTITY_URL",
  1056. { entityUrl: musicVideoEntityUrl },
  1057. this
  1058. ).then(resultBody => {
  1059. const youtubeIds = Array.from(
  1060. new Set(
  1061. resultBody.results.bindings
  1062. .filter(binding => !!binding.YouTube_video_ID)
  1063. .map(binding => binding.YouTube_video_ID.value)
  1064. )
  1065. );
  1066. // const soundcloudIds = Array.from(
  1067. // new Set(
  1068. // resultBody.results.bindings
  1069. // .filter(binding => !!binding["SoundCloud_track_ID"])
  1070. // .map(binding => binding["SoundCloud_track_ID"].value)
  1071. // )
  1072. // );
  1073. youtubeIds.forEach(youtubeId => {
  1074. const mediaSource = `youtube:${youtubeId}`;
  1075. mediaSources.add(mediaSource);
  1076. // if (collectAlternativeMediaSourcesOrigins) {
  1077. // const mediaSourceOrigins = [
  1078. // `Spotify track ${spotifyTrackId}`,
  1079. // `ISRC ${ISRC}`,
  1080. // `MusicBrainz recordings`,
  1081. // `MusicBrainz recording ${recording.id}`,
  1082. // `MusicBrainz relations`,
  1083. // `MusicBrainz relation target-type work`,
  1084. // `MusicBrainz relation work id ${relation.work.id}`,
  1085. // `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1086. // `YouTube ID ${youtubeId}`
  1087. // ];
  1088. // if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  1089. // mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1090. // }
  1091. });
  1092. // soundcloudIds.forEach(soundcloudId => {
  1093. // const mediaSource = `soundcloud:${soundcloudId}`;
  1094. // mediaSources.add(mediaSource);
  1095. // // if (collectAlternativeMediaSourcesOrigins) {
  1096. // // const mediaSourceOrigins = [
  1097. // // `Spotify track ${spotifyTrackId}`,
  1098. // // `ISRC ${ISRC}`,
  1099. // // `MusicBrainz recordings`,
  1100. // // `MusicBrainz recording ${recording.id}`,
  1101. // // `MusicBrainz relations`,
  1102. // // `MusicBrainz relation target-type work`,
  1103. // // `MusicBrainz relation work id ${relation.work.id}`,
  1104. // // `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1105. // // `SoundCloud ID ${soundcloudId}`
  1106. // // ];
  1107. // // if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  1108. // // mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1109. // // }
  1110. // });
  1111. resolve();
  1112. });
  1113. })
  1114. );
  1115. });
  1116. Promise.allSettled(promisesToRun2).then(resolve);
  1117. })
  1118. .catch(err => {
  1119. console.log(err);
  1120. resolve();
  1121. });
  1122. });
  1123. jobsToRun.push(promise);
  1124. // WikiDataModule.runJob("API_GET_DATA_FROM_MUSICBRAINZ_WORK", { workId: relation.work.id }, this));
  1125. }
  1126. });
  1127. });
  1128. } catch (err) {
  1129. console.log("Error during initial ISRC getting/parsing", err);
  1130. }
  1131. try {
  1132. const RecordingApiResponse = await MusicBrainzModule.runJob(
  1133. "API_CALL",
  1134. {
  1135. url: `https://musicbrainz.org/ws/2/recording/`,
  1136. params: {
  1137. fmt: "json",
  1138. query: `isrc:${ISRC}`
  1139. }
  1140. },
  1141. this
  1142. );
  1143. const releaseIds = new Set();
  1144. const releaseGroupIds = new Set();
  1145. RecordingApiResponse.recordings.forEach(recording => {
  1146. // const recordingId = recording.id;
  1147. // console.log("Recording:", recording.id);
  1148. recording.releases.forEach(release => {
  1149. const releaseId = release.id;
  1150. // console.log("Release:", releaseId);
  1151. const releaseGroupId = release["release-group"].id;
  1152. // console.log("Release group:", release["release-group"]);
  1153. // console.log("Release group id:", release["release-group"].id);
  1154. // console.log("Release group type id:", release["release-group"]["type-id"]);
  1155. // console.log("Release group primary type id:", release["release-group"]["primary-type-id"]);
  1156. // console.log("Release group primary type:", release["release-group"]["primary-type"]);
  1157. // d6038452-8ee0-3f68-affc-2de9a1ede0b9 = single
  1158. // 6d0c5bf6-7a33-3420-a519-44fc63eedebf = EP
  1159. if (
  1160. release["release-group"]["type-id"] === "d6038452-8ee0-3f68-affc-2de9a1ede0b9" ||
  1161. release["release-group"]["type-id"] === "6d0c5bf6-7a33-3420-a519-44fc63eedebf"
  1162. ) {
  1163. releaseIds.add(releaseId);
  1164. releaseGroupIds.add(releaseGroupId);
  1165. }
  1166. });
  1167. });
  1168. Array.from(releaseGroupIds).forEach(releaseGroupId => {
  1169. const promise = new Promise(resolve => {
  1170. WikiDataModule.runJob("API_GET_DATA_FROM_MUSICBRAINZ_RELEASE_GROUP", { releaseGroupId }, this)
  1171. .then(resultBody => {
  1172. const youtubeIds = Array.from(
  1173. new Set(
  1174. resultBody.results.bindings
  1175. .filter(binding => !!binding.YouTube_video_ID)
  1176. .map(binding => binding.YouTube_video_ID.value)
  1177. )
  1178. );
  1179. // const soundcloudIds = Array.from(
  1180. // new Set(
  1181. // resultBody.results.bindings
  1182. // .filter(binding => !!binding["SoundCloud_track_ID"])
  1183. // .map(binding => binding["SoundCloud_track_ID"].value)
  1184. // )
  1185. // );
  1186. const musicVideoEntityUrls = Array.from(
  1187. new Set(
  1188. resultBody.results.bindings
  1189. .filter(binding => !!binding.Music_video_entity_URL)
  1190. .map(binding => binding.Music_video_entity_URL.value)
  1191. )
  1192. );
  1193. youtubeIds.forEach(youtubeId => {
  1194. const mediaSource = `youtube:${youtubeId}`;
  1195. mediaSources.add(mediaSource);
  1196. // if (collectAlternativeMediaSourcesOrigins) {
  1197. // const mediaSourceOrigins = [
  1198. // `Spotify track ${spotifyTrackId}`,
  1199. // `ISRC ${ISRC}`,
  1200. // `MusicBrainz recordings`,
  1201. // `MusicBrainz recording ${recording.id}`,
  1202. // `MusicBrainz relations`,
  1203. // `MusicBrainz relation target-type work`,
  1204. // `MusicBrainz relation work id ${relation.work.id}`,
  1205. // `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1206. // `YouTube ID ${youtubeId}`
  1207. // ];
  1208. // if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  1209. // mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1210. // }
  1211. });
  1212. // soundcloudIds.forEach(soundcloudId => {
  1213. // const mediaSource = `soundcloud:${soundcloudId}`;
  1214. // mediaSources.add(mediaSource);
  1215. // // if (collectAlternativeMediaSourcesOrigins) {
  1216. // // const mediaSourceOrigins = [
  1217. // // `Spotify track ${spotifyTrackId}`,
  1218. // // `ISRC ${ISRC}`,
  1219. // // `MusicBrainz recordings`,
  1220. // // `MusicBrainz recording ${recording.id}`,
  1221. // // `MusicBrainz relations`,
  1222. // // `MusicBrainz relation target-type work`,
  1223. // // `MusicBrainz relation work id ${relation.work.id}`,
  1224. // // `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1225. // // `SoundCloud ID ${soundcloudId}`
  1226. // // ];
  1227. // // if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  1228. // // mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1229. // // }
  1230. // });
  1231. const promisesToRun2 = [];
  1232. musicVideoEntityUrls.forEach(musicVideoEntityUrl => {
  1233. promisesToRun2.push(
  1234. new Promise(resolve => {
  1235. WikiDataModule.runJob(
  1236. "API_GET_DATA_FROM_ENTITY_URL",
  1237. { entityUrl: musicVideoEntityUrl },
  1238. this
  1239. ).then(resultBody => {
  1240. const youtubeIds = Array.from(
  1241. new Set(
  1242. resultBody.results.bindings
  1243. .filter(binding => !!binding.YouTube_video_ID)
  1244. .map(binding => binding.YouTube_video_ID.value)
  1245. )
  1246. );
  1247. // const soundcloudIds = Array.from(
  1248. // new Set(
  1249. // resultBody.results.bindings
  1250. // .filter(binding => !!binding["SoundCloud_track_ID"])
  1251. // .map(binding => binding["SoundCloud_track_ID"].value)
  1252. // )
  1253. // );
  1254. youtubeIds.forEach(youtubeId => {
  1255. const mediaSource = `youtube:${youtubeId}`;
  1256. mediaSources.add(mediaSource);
  1257. // if (collectAlternativeMediaSourcesOrigins) {
  1258. // const mediaSourceOrigins = [
  1259. // `Spotify track ${spotifyTrackId}`,
  1260. // `ISRC ${ISRC}`,
  1261. // `MusicBrainz recordings`,
  1262. // `MusicBrainz recording ${recording.id}`,
  1263. // `MusicBrainz relations`,
  1264. // `MusicBrainz relation target-type work`,
  1265. // `MusicBrainz relation work id ${relation.work.id}`,
  1266. // `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1267. // `YouTube ID ${youtubeId}`
  1268. // ];
  1269. // if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  1270. // mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1271. // }
  1272. });
  1273. // soundcloudIds.forEach(soundcloudId => {
  1274. // const mediaSource = `soundcloud:${soundcloudId}`;
  1275. // mediaSources.add(mediaSource);
  1276. // // if (collectAlternativeMediaSourcesOrigins) {
  1277. // // const mediaSourceOrigins = [
  1278. // // `Spotify track ${spotifyTrackId}`,
  1279. // // `ISRC ${ISRC}`,
  1280. // // `MusicBrainz recordings`,
  1281. // // `MusicBrainz recording ${recording.id}`,
  1282. // // `MusicBrainz relations`,
  1283. // // `MusicBrainz relation target-type work`,
  1284. // // `MusicBrainz relation work id ${relation.work.id}`,
  1285. // // `WikiData select from MusicBrainz work id ${relation.work.id}`,
  1286. // // `SoundCloud ID ${soundcloudId}`
  1287. // // ];
  1288. // // if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
  1289. // // mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
  1290. // // }
  1291. // });
  1292. resolve();
  1293. });
  1294. })
  1295. );
  1296. });
  1297. Promise.allSettled(promisesToRun2).then(resolve);
  1298. })
  1299. .catch(err => {
  1300. console.log(err);
  1301. resolve();
  1302. });
  1303. });
  1304. jobsToRun.push(promise);
  1305. });
  1306. } catch (err) {
  1307. console.log("Error during getting releases from ISRC", err);
  1308. }
  1309. // console.log("RecordingApiResponse");
  1310. // console.dir(RecordingApiResponse, { depth: 10 });
  1311. // console.dir(RecordingApiResponse.recordings[0].releases[0], { depth: 10 });
  1312. await Promise.allSettled(jobsToRun);
  1313. return {
  1314. mediaSources: Array.from(mediaSources),
  1315. mediaSourcesOrigins
  1316. };
  1317. }
  1318. }
  1319. export default new _SpotifyModule();