spotify.js 45 KB

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