| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366 | import async from "async";import config from "config";import mongoose from "mongoose";import CoreClass from "../core";let SongsModule;let CacheModule;let DBModule;let UtilsModule;let YouTubeModule;let SoundCloudModule;let StationsModule;let PlaylistsModule;let MediaModule;let WSModule;class _SongsModule extends CoreClass {	// eslint-disable-next-line require-jsdoc	constructor() {		super("songs");		SongsModule = this;	}	/**	 * Initialises the songs module	 * @returns {Promise} - returns promise (reject, resolve)	 */	async initialize() {		this.setStage(1);		CacheModule = this.moduleManager.modules.cache;		DBModule = this.moduleManager.modules.db;		UtilsModule = this.moduleManager.modules.utils;		YouTubeModule = this.moduleManager.modules.youtube;		SoundCloudModule = this.moduleManager.modules.soundcloud;		StationsModule = this.moduleManager.modules.stations;		PlaylistsModule = this.moduleManager.modules.playlists;		MediaModule = this.moduleManager.modules.media;		WSModule = this.moduleManager.modules.ws;		this.SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });		this.SongSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "song" });		this.setStage(2);		return new Promise((resolve, reject) => {			CacheModule.runJob("SUB", {				channel: "song.created",				cb: async data =>					WSModule.runJob("EMIT_TO_ROOMS", {						rooms: ["import-album", `edit-song.${data.song._id}`, "edit-songs"],						args: ["event:admin.song.created", { data }]					})			});			async.waterfall(				[					next => {						this.setStage(2);						CacheModule.runJob("HGETALL", { table: "songs" })							.then(songs => {								next(null, songs);							})							.catch(next);					},					(songs, next) => {						this.setStage(3);						if (!songs) return next();						const mediaSources = Object.keys(songs);						return async.each(							mediaSources,							(mediaSource, next) => {								SongsModule.SongModel.findOne({ mediaSource }, (err, song) => {									if (err) next(err);									else if (!song)										CacheModule.runJob("HDEL", {											table: "songs",											key: mediaSource										})											.then(() => next())											.catch(next);									else next();								});							},							next						);					},					next => {						this.setStage(4);						SongsModule.SongModel.find({}, next);					},					(songs, next) => {						this.setStage(5);						async.each(							songs,							(song, next) => {								CacheModule.runJob("HSET", {									table: "songs",									key: song.mediaSource,									value: SongsModule.SongSchemaCache(song)								})									.then(() => next())									.catch(next);							},							next						);					}				],				async err => {					if (err) {						err = await UtilsModule.runJob("GET_ERROR", { error: err });						reject(new Error(err));					} else resolve();				}			);		});	}	/**	 * Gets a song by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache	 * @param {object} payload - object containing the payload	 * @param {string} payload.songId - the id of the song we are trying to get	 * @returns {Promise} - returns a promise (resolve, reject)	 */	GET_SONG(payload) {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						if (!mongoose.Types.ObjectId.isValid(payload.songId))							return next("songId is not a valid ObjectId.");						return CacheModule.runJob("HGET", { table: "songs", key: payload.songId }, this)							.then(song => next(null, song))							.catch(next);					},					(song, next) => {						if (song) return next(true, song);						return SongsModule.SongModel.findOne({ _id: payload.songId }, next);					},					(song, next) => {						if (song) {							CacheModule.runJob(								"HSET",								{									table: "songs",									key: payload.songId,									value: song								},								this							).then(song => next(null, song));						} else next("Song not found.");					}				],				(err, song) => {					if (err && err !== true) return reject(new Error(err));					return resolve({ song });				}			);		});	}	/**	 * Gets songs by id from Mongo	 * @param {object} payload - object containing the payload	 * @param {string} payload.mediaSources - the media sources of the songs we are trying to get	 * @returns {Promise} - returns a promise (resolve, reject)	 */	GET_SONGS(payload) {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => SongsModule.SongModel.find({ mediaSource: { $in: payload.mediaSources } }, next),					(songs, next) => {						const mediaSources = payload.mediaSources.filter(							mediaSource => !songs.find(song => song.mediaSource === mediaSource)						);						if (mediaSources.length === 0) return next(null, songs);						const mediaSourceTypes = [];						mediaSources.forEach(mediaSource => {							const mediaSourceType = mediaSource.split(":")[0];							if (mediaSourceTypes.indexOf(mediaSourceType) === -1)								mediaSourceTypes.push(mediaSourceType);						});						if (mediaSourceTypes.length !== 1)							return next(`Expected 1 media source types, got ${mediaSourceTypes.length}.`);						const [mediaSourceType] = mediaSourceTypes;						if (mediaSourceType === "youtube")							return YouTubeModule.youtubeVideoModel.find(								{									youtubeId: {										$in: mediaSources											.filter(mediaSource => mediaSource.startsWith("youtube:"))											.map(mediaSource => mediaSource.split(":")[1])									}								},								(err, videos) => {									if (err) next(err);									else {										const youtubeVideos = videos.map(video => {											const { youtubeId, title, author, duration, thumbnail } = video;											return {												mediaSource: `youtube:${youtubeId}`,												title,												artists: [author],												genres: [],												tags: [],												duration,												skipDuration: 0,												thumbnail:													thumbnail ||													`https://img.youtube.com/vi/${youtubeId}/mqdefault.jpg`,												requestedBy: null,												requestedAt: Date.now(),												verified: false,												youtubeVideoId: video._id											};										});										next(											null,											payload.mediaSources												.map(													mediaSource =>														songs.find(song => song.mediaSource === mediaSource) ||														youtubeVideos.find(video => video.mediaSource === mediaSource)												)												.filter(song => !!song)										);									}								}							);						if (config.get("experimental.soundcloud") && mediaSourceType === "soundcloud")							return SoundCloudModule.soundcloudTrackModel.find(								{									trackId: {										$in: mediaSources											.filter(mediaSource => mediaSource.startsWith("soundcloud:"))											.map(mediaSource => mediaSource.split(":")[1])									}								},								(err, tracks) => {									if (err) next(err);									else {										const soundcloudSongs = tracks.map(track => {											const { trackId, title, username, duration, artworkUrl } = track;											return {												mediaSource: `soundcloud:${trackId}`,												title,												artists: [username],												genres: [],												tags: [],												duration,												skipDuration: 0,												thumbnail: artworkUrl,												requestedBy: null,												requestedAt: Date.now(),												verified: false,												soundcloudTrackId: track._id											};										});										next(											null,											payload.mediaSources												.map(													mediaSource =>														songs.find(song => song.mediaSource === mediaSource) ||														soundcloudSongs.find(															soundcloudSong => soundcloudSong.mediaSource === mediaSource														)												)												.filter(song => !!song)										);									}								}							);						return next(`Unknown media source specified: ${mediaSourceType}.`);					}				],				(err, songs) => {					if (err && err !== true) return reject(new Error(err));					return resolve({ songs });				}			);		});	}	/**	 * Create song	 * @param {object} payload - an object containing the payload	 * @param {string} payload.song - the song object	 * @param {string} payload.userId - the user id of the person requesting the song	 * @returns {Promise} - returns a promise (resolve, reject)	 */	CREATE_SONG(payload) {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						DBModule.runJob("GET_MODEL", { modelName: "user" }, this)							.then(UserModel => {								UserModel.findOne(									{ _id: payload.userId },									{ "preferences.anonymousSongRequests": 1 },									next								);							})							.catch(next);					},					(user, next) => {						const song = new SongsModule.SongModel({							...payload.song,							requestedBy: user.preferences.anonymousSongRequests ? null : payload.userId,							requestedAt: Date.now()						});						if (song.verified) {							song.verifiedBy = payload.userId;							song.verifiedAt = Date.now();						}						song.save({ validateBeforeSave: true }, err => {							if (err) return next(err, song);							return next(null, song);						});					},					(song, next) => {						SongsModule.runJob("UPDATE_SONG", { songId: song._id });						return next(null, song);					},					(song, next) => {						MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource: song.mediaSource }, this)							.then(() => next(null, song))							.catch(next);					},					(song, next) => {						CacheModule.runJob("PUB", {							channel: "song.created",							value: { song }						})							.then(() => next(null, song))							.catch(next);					}				],				(err, song) => {					if (err && err !== true) return reject(new Error(err));					return resolve({ song });				}			);		});	}	/**	 * Gets a song from id from Mongo and updates the cache with it	 * @param {object} payload - an object containing the payload	 * @param {string} payload.songId - the id of the song we are trying to update	 * @param {string} payload.oldStatus - old status of song being updated (optional)	 * @returns {Promise} - returns a promise (resolve, reject)	 */	async UPDATE_SONG(payload) {		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);		const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.findOne({ _id: payload.songId }, next);					},					(song, next) => {						if (!song) {							CacheModule.runJob("HDEL", {								table: "songs",								key: payload.songId							});							return next("Song not found.");						}						return CacheModule.runJob(							"HSET",							{								table: "songs",								key: payload.songId,								value: song							},							this						)							.then(() => {								const {									_id,									mediaSource,									title,									artists,									thumbnail,									duration,									skipDuration,									verified								} = song;								next(null, {									_id,									mediaSource,									title,									artists,									thumbnail,									duration,									skipDuration,									verified								});							})							.catch(next);					},					(song, next) => {						playlistModel.updateMany({ "songs._id": song._id }, { $set: { "songs.$": song } }, err => {							if (err) next(err);							else next(null, song);						});					},					(song, next) => {						playlistModel.updateMany(							{ "songs.mediaSource": song.mediaSource },							{ $set: { "songs.$": song } },							err => {								if (err) next(err);								else next(null, song);							}						);					},					(song, next) => {						playlistModel.find({ "songs._id": song._id }, (err, playlists) => {							if (err) next(err);							else {								async.eachLimit(									playlists,									1,									(playlist, next) => {										PlaylistsModule.runJob(											"UPDATE_PLAYLIST",											{												playlistId: playlist._id											},											this										)											.then(() => {												next();											})											.catch(err => {												next(err);											});									},									err => {										if (err) next(err);										else next(null, song);									}								);							}						});					},					(song, next) => {						stationModel.updateMany({ "queue._id": song._id }, { $set: { "queue.$": song } }, err => {							if (err) next(err);							else next(null, song);						});					},					(song, next) => {						stationModel.updateMany(							{ "queue.mediaSource": song.mediaSource },							{ $set: { "queue.$": song } },							err => {								if (err) next(err);								else next(null, song);							}						);					},					(song, next) => {						stationModel.find({ "queue._id": song._id }, (err, stations) => {							if (err) next(err);							else {								async.eachLimit(									stations,									1,									(station, next) => {										StationsModule.runJob("UPDATE_STATION", { stationId: station._id }, this)											.then(() => {												next();											})											.catch(err => {												next(err);											});									},									err => {										if (err) next(err);										else next(null, song);									}								);							}						});					},					(song, next) => {						async.eachLimit(							song.genres,							1,							(genre, next) => {								PlaylistsModule.runJob(									"AUTOFILL_GENRE_PLAYLIST",									{ genre, createPlaylist: song.verified },									this								)									.then(() => {										next();									})									.catch(err => next(err));							},							err => {								next(err, song);							}						);					}				],				(err, song) => {					if (err && err !== true) return reject(new Error(err));					if (!payload.oldStatus) payload.oldStatus = null;					CacheModule.runJob("PUB", {						channel: "song.updated",						value: { songId: song._id, oldStatus: payload.oldStatus }					});					return resolve(song);				}			);		});	}	/**	 * Gets multiple songs from id from Mongo and updates the cache with it	 * @param {object} payload - an object containing the payload	 * @param {Array} payload.songIds - the ids of the songs we are trying to update	 * @param {string} payload.oldStatus - old status of song being updated (optional)	 * @returns {Promise} - returns a promise (resolve, reject)	 */	async UPDATE_SONGS(payload) {		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);		const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);		return new Promise((resolve, reject) => {			async.waterfall(				[					// Get songs from Mongo					next => {						const { songIds } = payload;						this.publishProgress({ status: "update", message: `Updating songs (stage 1)` });						SongsModule.SongModel.find({ _id: songIds }, next);					},					// Any songs that were not in Mongo, remove from cache, if they're in the cache					(songs, next) => {						const { songIds } = payload;						this.publishProgress({ status: "update", message: `Updating songs (stage 2)` });						async.eachLimit(							songIds,							1,							(songId, next) => {								if (songs.findIndex(song => song._id.toString() === songId) === -1) {									// NOTE: could be made lower priority									CacheModule.runJob("HDEL", {										table: "songs",										key: songId									});									next();								} else next();							},							() => {								next(null, songs);							}						);					},					// Adds/updates all songs in the cache					(songs, next) => {						this.publishProgress({ status: "update", message: `Updating songs (stage 3)` });						async.eachLimit(							songs,							1,							(song, next) => {								CacheModule.runJob(									"HSET",									{										table: "songs",										key: song._id,										value: song									},									this								)									.then(() => {										next();									})									.catch(next);							},							() => {								next(null, songs);							}						);					},					// Updates all playlists that the songs are in by setting the new trimmed song					(songs, next) => {						this.publishProgress({ status: "update", message: `Updating songs (stage 4)` });						const trimmedSongs = songs.map(song => {							const { _id, mediaSource, title, artists, thumbnail, duration, verified } = song;							return {								_id,								mediaSource,								title,								artists,								thumbnail,								duration,								verified							};						});						const playlistsToUpdate = new Set();						async.eachLimit(							trimmedSongs,							1,							(trimmedSong, next) => {								async.waterfall(									[										next => {											playlistModel.updateMany(												{ "songs._id": trimmedSong._id },												{ $set: { "songs.$": trimmedSong } },												next											);										},										(res, next) => {											playlistModel.find({ "songs._id": trimmedSong._id }, next);										},										(playlists, next) => {											playlists.forEach(playlist => {												playlistsToUpdate.add(playlist._id.toString());											});											next();										}									],									next								);							},							err => {								next(err, songs, playlistsToUpdate);							}						);					},					// Updates all playlists that the songs are in					(songs, playlistsToUpdate, next) => {						this.publishProgress({ status: "update", message: `Updating songs (stage 5)` });						async.eachLimit(							playlistsToUpdate,							1,							(playlistId, next) => {								PlaylistsModule.runJob(									"UPDATE_PLAYLIST",									{										playlistId									},									this								)									.then(() => {										next();									})									.catch(err => {										next(err);									});							},							err => {								next(err, songs);							}						);					},					// Updates all station queues that the songs are in by setting the new trimmed song					(songs, next) => {						this.publishProgress({ status: "update", message: `Updating songs (stage 6)` });						const stationsToUpdate = new Set();						async.eachLimit(							songs,							1,							(song, next) => {								async.waterfall(									[										next => {											const { mediaSource, title, artists, thumbnail, duration, verified } = song;											stationModel.updateMany(												{ "queue._id": song._id },												{													$set: {														"queue.$.mediaSource": mediaSource,														"queue.$.title": title,														"queue.$.artists": artists,														"queue.$.thumbnail": thumbnail,														"queue.$.duration": duration,														"queue.$.verified": verified													}												},												next											);										},										(res, next) => {											stationModel.find({ "queue._id": song._id }, next);										},										(stations, next) => {											stations.forEach(station => {												stationsToUpdate.add(station._id.toString());											});											next();										}									],									next								);							},							err => {								next(err, songs, stationsToUpdate);							}						);					},					// Updates all playlists that the songs are in					(songs, stationsToUpdate, next) => {						this.publishProgress({ status: "update", message: `Updating songs (stage 7)` });						async.eachLimit(							stationsToUpdate,							1,							(stationId, next) => {								StationsModule.runJob(									"UPDATE_STATION",									{										stationId									},									this								)									.then(() => {										next();									})									.catch(err => {										next(err);									});							},							err => {								next(err, songs);							}						);					},					// Autofill the genre playlists of all genres of all songs					(songs, next) => {						this.publishProgress({ status: "update", message: `Updating songs (stage 8)` });						const genresToAutofill = new Set();						songs.forEach(song => {							if (song.verified)								song.genres.forEach(genre => {									genresToAutofill.add(genre);								});						});						async.eachLimit(							genresToAutofill,							1,							(genre, next) => {								PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre, createPlaylist: true }, this)									.then(() => {										next();									})									.catch(err => next(err));							},							err => {								next(err, songs);							}						);					},					// Send event that the song was updated					(songs, next) => {						this.publishProgress({ status: "update", message: `Updating songs (stage 9)` });						async.eachLimit(							songs,							1,							(song, next) => {								CacheModule.runJob("PUB", {									channel: "song.updated",									value: { songId: song._id, oldStatus: null }								});								next();							},							() => {								next();							}						);					}				],				err => {					if (err && err !== true) return reject(new Error(err));					return resolve();				}			);		});	}	/**	 * Updates all songs	 * @returns {Promise} - returns a promise (resolve, reject)	 */	UPDATE_ALL_SONGS() {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.find({}, next);					},					(songs, next) => {						let index = 0;						const { length } = songs;						async.eachLimit(							songs,							2,							(song, next) => {								index += 1;								console.log(`Updating song #${index} out of ${length}: ${song._id}`);								this.publishProgress({ status: "update", message: `Updating song "${song._id}"` });								SongsModule.runJob("UPDATE_SONG", { songId: song._id }, this)									.then(() => {										next();									})									.catch(err => {										next(err);									});							},							err => {								next(err);							}						);					}				],				err => {					if (err && err !== true) return reject(new Error(err));					return resolve();				}			);		});	}	// /**	//  * Deletes song from id from Mongo and cache	//  *	//  * @param {object} payload - returns an object containing the payload	//  * @param {string} payload.songId - the song id of the song we are trying to delete	//  * @returns {Promise} - returns a promise (resolve, reject)	//  */	// DELETE_SONG(payload) {	// 	return new Promise((resolve, reject) =>	// 		async.waterfall(	// 			[	// 				next => {	// 					SongsModule.SongModel.deleteOne({ _id: payload.songId }, next);	// 				},	// 				next => {	// 					CacheModule.runJob(	// 						"HDEL",	// 						{	// 							table: "songs",	// 							key: payload.songId	// 						},	// 						this	// 					)	// 						.then(() => next())	// 						.catch(next);	// 				},	// 				next => {	// 					this.log("INFO", `Going to update playlists and stations now for deleted song ${payload.songId}`);	// 					DBModule.runJob("GET_MODEL", { modelName: "playlist" }).then(playlistModel => {	// 						playlistModel.find({ "songs._id": song._id }, (err, playlists) => {	// 							if (err) this.log("ERROR", err);	// 							else {	// 								playlistModel.updateMany(	// 									{ "songs._id": payload.songId },	// 									{ $pull: { "songs.$._id": payload.songId} },	// 									err => {	// 										if (err) this.log("ERROR", err);	// 										else {	// 											playlists.forEach(playlist => {	// 												PlaylistsModule.runJob("UPDATE_PLAYLIST", {	// 													playlistId: playlist._id	// 												});	// 											});	// 										}	// 									}	// 								);	// 							}	// 						});	// 					});	// 					DBModule.runJob("GET_MODEL", { modelName: "station" }).then(stationModel => {	// 						stationModel.find({ "queue._id": payload.songId }, (err, stations) => {	// 							stationModel.updateMany(	// 								{ "queue._id": payload.songId },	// 								{	// 									$pull: { "queue._id":  }	// 								},	// 								err => {	// 									if (err) this.log("ERROR", err);	// 									else {	// 										stations.forEach(station => {	// 											StationsModule.runJob("UPDATE_STATION", { stationId: station._id });	// 										});	// 									}	// 								}	// 							);	// 						});	// 					});	// 				}	// 			],	// 			err => {	// 				if (err && err !== true) return reject(new Error(err));	// 				return resolve();	// 			}	// 		)	// 	);	// }	/**	 * Searches through songs	 * @param {object} payload - object that contains the payload	 * @param {string} payload.query - the query	 * @param {string} payload.includeUnverified - include unverified songs	 * @param {string} payload.includeVerified - include verified songs	 * @param {string} payload.trimmed - include trimmed songs	 * @param {string} payload.page - page (default 1)	 * @returns {Promise} - returns promise (reject, resolve)	 */	SEARCH(payload) {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						const isVerified = [];						if (payload.includeUnverified) isVerified.push(false);						if (payload.includeVerified) isVerified.push(true);						if (isVerified.length === 0) return next("No verified status has been included.");						let { query } = payload;						const isRegex =							query.length > 2 && query.indexOf("/") === 0 && query.lastIndexOf("/") === query.length - 1;						if (isRegex) query = query.slice(1, query.length - 1);						else query = query.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");						const filterArray = [							{								title: new RegExp(`${query}`, "i"),								verified: { $in: isVerified }							},							{								artists: new RegExp(`${query}`, "i"),								verified: { $in: isVerified }							}						];						return next(null, filterArray);					},					(filterArray, next) => {						const page = payload.page ? payload.page : 1;						const pageSize = 15;						const skipAmount = pageSize * (page - 1);						const query = { $or: filterArray };						const mediaSources = [];						if (!config.get("experimental.soundcloud")) {							mediaSources.push(/^soundcloud:/);						}						if (!config.get("experimental.spotify")) {							mediaSources.push(/^spotify:/);						}						if (mediaSources.length > 0) query.mediaSource = { $nin: mediaSources };						SongsModule.SongModel.find(query).count((err, count) => {							if (err) next(err);							else {								SongsModule.SongModel.find(query)									.skip(skipAmount)									.limit(pageSize)									.exec((err, songs) => {										if (err) next(err);										else {											next(null, {												songs,												page,												pageSize,												skipAmount,												count											});										}									});							}						});					},					(data, next) => {						if (data.songs.length === 0) next("No songs found");						else if (payload.trimmed) {							next(null, {								songs: data.songs.map(song => {									const { _id, mediaSource, title, artists, thumbnail, duration, verified } = song;									return {										_id,										mediaSource,										title,										artists,										thumbnail,										duration,										verified									};								}),								...data							});						} else next(null, data);					}				],				(err, data) => {					if (err && err !== true) return reject(new Error(err));					return resolve(data);				}			);		});	}	/**	 * Gets an array of all genres	 * @returns {Promise} - returns a promise (resolve, reject)	 */	GET_ALL_GENRES() {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.find({ verified: true }, { genres: 1, _id: false }, next);					},					(songs, next) => {						let allGenres = [];						songs.forEach(song => {							allGenres = allGenres.concat(song.genres);						});						const lowerCaseGenres = allGenres.map(genre => genre.toLowerCase());						const uniqueGenres = lowerCaseGenres.filter(							(value, index, self) => self.indexOf(value) === index						);						next(null, uniqueGenres);					}				],				(err, genres) => {					if (err && err !== true) return reject(new Error(err));					return resolve({ genres });				}			);		});	}	/**	 * Gets an array of all songs with a specific genre	 * @param {object} payload - returns an object containing the payload	 * @param {string} payload.genre - the genre	 * @returns {Promise} - returns a promise (resolve, reject)	 */	GET_ALL_SONGS_WITH_GENRE(payload) {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.find(							{								verified: true,								genres: { $regex: new RegExp(`^${payload.genre.toLowerCase()}$`, "i") }							},							next						);					}				],				(err, songs) => {					if (err && err !== true) return reject(new Error(err));					return resolve({ songs });				}			);		});	}	// runjob songs GET_ORPHANED_PLAYLIST_SONGS {}	/**	 * Gets a orphaned playlist songs	 * @returns {Promise} - returns promise (reject, resolve)	 */	GET_ORPHANED_PLAYLIST_SONGS() {		return new Promise((resolve, reject) => {			DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this).then(playlistModel => {				playlistModel.find({}, (err, playlists) => {					if (err) reject(new Error(err));					else {						SongsModule.SongModel.find({}, { _id: true, mediaSource: true }, (err, songs) => {							if (err) reject(new Error(err));							else {								const songIds = songs.map(song => song._id.toString());								const orphanedYoutubeIds = new Set();								async.eachLimit(									playlists,									1,									(playlist, next) => {										playlist.songs.forEach(song => {											if (												(!song._id || songIds.indexOf(song._id.toString() === -1)) &&												!orphanedYoutubeIds.has(song.mediaSource)											) {												orphanedYoutubeIds.add(song.mediaSource);											}										});										next();									},									() => {										resolve({ youtubeIds: Array.from(orphanedYoutubeIds) });									}								);							}						});					}				});			});		});	}	/**	 * Requests all orphaned playlist songs, adding them to the database	 * @returns {Promise} - returns promise (reject, resolve)	 */	REQUEST_ORPHANED_PLAYLIST_SONGS() {		return new Promise((resolve, reject) => {			DBModule.runJob("GET_MODEL", { modelName: "playlist" })				.then(playlistModel => {					SongsModule.runJob("GET_ORPHANED_PLAYLIST_SONGS", {}, this).then(response => {						const { mediaSources } = response;						const playlistsToUpdate = new Set();						async.eachLimit(							mediaSources,							1,							(mediaSource, next) => {								async.waterfall(									[										next => {											this.publishProgress({												status: "update",												message: `Requesting "${mediaSource}"`											});											console.log(												mediaSource,												`this is song ${mediaSources.indexOf(mediaSource) + 1}/${													mediaSources.length												}`											);											setTimeout(next, 150);										},										next => {											MediaModule.runJob("GET_MEDIA", { mediaSource }, this)												.then(res => next(null, res.song))												.catch(next);										},										(song, next) => {											const { _id, title, artists, thumbnail, duration, verified } = song;											const trimmedSong = {												_id,												mediaSource,												title,												artists,												thumbnail,												duration,												verified											};											playlistModel.updateMany(												{ "songs.mediaSource": song.mediaSource },												{ $set: { "songs.$": trimmedSong } },												err => {													next(err, song);												}											);										},										(song, next) => {											playlistModel.find({ "songs._id": song._id }, next);										},										(playlists, next) => {											playlists.forEach(playlist => {												playlistsToUpdate.add(playlist._id.toString());											});											next();										}									],									next								);							},							err => {								if (err) reject(err);								else {									async.eachLimit(										Array.from(playlistsToUpdate),										1,										(playlistId, next) => {											PlaylistsModule.runJob(												"UPDATE_PLAYLIST",												{													playlistId												},												this											)												.then(() => {													next();												})												.catch(next);										},										err => {											if (err) reject(err);											else resolve();										}									);								}							}						);					});				})				.catch(reject);		});	}	/**	 * Gets a list of all genres	 * @returns {Promise} - returns promise (reject, resolve)	 */	GET_GENRES() {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.distinct("genres", next);					}				],				(err, genres) => {					if (err) reject(err);					resolve({ genres });				}			);		});	}	/**	 * Gets a list of all artists	 * @returns {Promise} - returns promise (reject, resolve)	 */	GET_ARTISTS() {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.distinct("artists", next);					}				],				(err, artists) => {					if (err) reject(err);					resolve({ artists });				}			);		});	}	/**	 * Gets a list of all tags	 * @returns {Promise} - returns promise (reject, resolve)	 */	GET_TAGS() {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.distinct("tags", next);					}				],				(err, tags) => {					if (err) reject(err);					resolve({ tags });				}			);		});	}	/**	 * Gets a list of all media sources	 * @returns {Promise} - returns promise (reject, resolve)	 */	async GET_ALL_MEDIA_SOURCES() {		return SongsModule.SongModel.distinct("mediaSource");	}}export default new _SongsModule();
 |