spotify.js 47 KB

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