spotify.js 44 KB

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