| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334 | import async from "async";import mongoose from "mongoose";import CoreClass from "../core";let SongsModule;let CacheModule;let DBModule;let UtilsModule;let YouTubeModule;let StationsModule;let PlaylistsModule;let RatingsModule;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;		StationsModule = this.moduleManager.modules.stations;		PlaylistsModule = this.moduleManager.modules.playlists;		RatingsModule = this.moduleManager.modules.ratings;		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 youtubeIds = Object.keys(songs);						return async.each(							youtubeIds,							(youtubeId, next) => {								SongsModule.SongModel.findOne({ youtubeId }, (err, song) => {									if (err) next(err);									else if (!song)										CacheModule.runJob("HDEL", {											table: "songs",											key: youtubeId										})											.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.youtubeId,									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.youtubeIds - the youtube ids 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({ youtubeId: { $in: payload.youtubeIds } }, next),					(songs, next) => {						const youtubeIds = payload.youtubeIds.filter(							youtubeId => !songs.find(song => song.youtubeId === youtubeId)						);						return YouTubeModule.youtubeVideoModel.find(							{ youtubeId: { $in: youtubeIds } },							(err, videos) => {								if (err) next(err);								else {									const youtubeVideos = videos.map(video => {										const { youtubeId, title, author, duration, thumbnail } = video;										return {											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										};									});									next(null, [...songs, ...youtubeVideos]);								}							}						);					}				],				(err, songs) => {					if (err && err !== true) return reject(new Error(err));					return resolve({ songs });				}			);		});	}	/**	 * Makes sure that if a song is not currently in the songs db, to add it	 *	 * @param {object} payload - an object containing the payload	 * @param {string} payload.youtubeId - the youtube song id of the song we are trying to ensure is in the songs db	 * @param {string} payload.userId - the youtube song id of the song we are trying to ensure is in the songs db	 * @param {string} payload.automaticallyRequested - whether the song was automatically requested or not	 * @returns {Promise} - returns a promise (resolve, reject)	 */	ENSURE_SONG_EXISTS_BY_YOUTUBE_ID(payload) {		return new Promise((resolve, reject) => {			async.waterfall(				[					next => {						SongsModule.SongModel.findOne({ youtubeId: payload.youtubeId }, next);					},					(song, next) => {						if (song && song.duration > 0) next(true, song);						else {							YouTubeModule.runJob(								"GET_VIDEO",								{ identifier: payload.youtubeId, createMissing: true },								this							)								.then(response => {									const { youtubeId, title, author, duration } = response.video;									next(null, song, { youtubeId, title, artists: [author], duration });								})								.catch(next);						}					},					(song, youtubeVideo, next) => {						if (song && song.duration <= 0) {							song.duration = youtubeVideo.duration;							song.save({ validateBeforeSave: true }, err => {								if (err) next(err, song);								next(null, song);							});						} else {							next(null, {								...youtubeVideo,								skipDuration: 0,								requestedBy: payload.userId,								requestedAt: Date.now(),								verified: false							});						}					}				],				(err, song) => {					if (err && err !== true) return reject(new Error(err));					return resolve({ song });				}			);		});	}	/**	 * 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) => {						RatingsModule.runJob("RECALCULATE_RATINGS", { youtubeId: song.youtubeId }, 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, youtubeId, title, artists, thumbnail, duration, skipDuration, verified } =									song;								next(null, {									_id,									youtubeId,									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.youtubeId": song.youtubeId },							{ $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.youtubeId": song.youtubeId },							{ $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, youtubeId, title, artists, thumbnail, duration, verified } = song;							return {								_id,								youtubeId,								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 { youtubeId, title, artists, thumbnail, duration, verified } = song;											stationModel.updateMany(												{ "queue._id": song._id },												{													$set: {														"queue.$.youtubeId": youtubeId,														"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}`);								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);						SongsModule.SongModel.find({ $or: filterArray }).count((err, count) => {							if (err) next(err);							else {								SongsModule.SongModel.find({ $or: filterArray })									.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, youtubeId, title, artists, thumbnail, duration, verified } = song;									return {										_id,										youtubeId,										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, youtubeId: 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.youtubeId)											) {												orphanedYoutubeIds.add(song.youtubeId);											}										});										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 { youtubeIds } = response;						const playlistsToUpdate = new Set();						async.eachLimit(							youtubeIds,							1,							(youtubeId, next) => {								async.waterfall(									[										next => {											console.log(												youtubeId,												`this is song ${youtubeIds.indexOf(youtubeId) + 1}/${youtubeIds.length}`											);											setTimeout(next, 150);										},										next => {											SongsModule.runJob(												"ENSURE_SONG_EXISTS_BY_SONG_ID",												{ youtubeId, automaticallyRequested: true },												this											)												.then(() => next())												.catch(next);										},										next => {											console.log(444, youtubeId);											SongsModule.SongModel.findOne({ youtubeId }, next);										},										(song, next) => {											const { _id, title, artists, thumbnail, duration, verified } = song;											const trimmedSong = {												_id,												youtubeId,												title,												artists,												thumbnail,												duration,												verified											};											playlistModel.updateMany(												{ "songs.youtubeId": song.youtubeId },												{ $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 });				}			);		});	}}export default new _SongsModule();
 |