spotify.js 44 KB

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