Browse Source

Added genre playlists

Kristian Vos 4 years ago
parent
commit
cd9aed3d91

+ 15 - 3
backend/index.js

@@ -486,9 +486,21 @@ process.stdin.on("data", data => {
 			printJob(topParent, 1);
 		} else console.log("Could not find job in any running, queued or paused lists in any module.");
 	}
-	// if (command.startsWith("debug")) {
-	// }
-
+	if (command.startsWith("runjob")) {
+		const parts = command.split(" ");
+		const module = parts[1];
+		const jobName = parts[2];
+		const payload = JSON.parse(parts[3]);
+
+		moduleManager.modules[module]
+			.runJob(jobName, payload)
+			.then(response => {
+				console.log("runjob success", response);
+			})
+			.catch(err => {
+				console.log("runjob error", err);
+			});
+	}
 	if (command.startsWith("eval")) {
 		const evalCommand = command.replace("eval ", "");
 		console.log(`Running eval command: ${evalCommand}`);

+ 1 - 0
backend/logic/actions/playlists.js

@@ -404,6 +404,7 @@ export default {
 							songs,
 							createdBy: session.userId,
 							createdAt: Date.now(),
+							createdFor: null,
 							type: "user"
 						},
 						next

+ 36 - 1
backend/logic/actions/songs.js

@@ -10,6 +10,7 @@ const IOModule = moduleManager.modules.io;
 const CacheModule = moduleManager.modules.cache;
 const SongsModule = moduleManager.modules.songs;
 const ActivitiesModule = moduleManager.modules.activities;
+const PlaylistsModule = moduleManager.modules.playlists;
 
 CacheModule.runJob("SUB", {
 	channel: "song.removed",
@@ -339,15 +340,30 @@ export default {
 	 */
 	update: isAdminRequired(async function update(session, songId, song, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+		let existingSong = null;
 		async.waterfall(
 			[
 				next => {
+					songModel.findOne({ _id: songId }, next);
+				},
+
+				(_existingSong, next) => {
+					existingSong = _existingSong;
 					songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
 				},
 
 				(res, next) => {
 					SongsModule.runJob("UPDATE_SONG", { songId }, this)
 						.then(song => {
+							existingSong.genres
+								.concat(song.genres)
+								.filter((value, index, self) => self.indexOf(value) === index)
+								.forEach(genre => {
+									PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
+										.then(() => {})
+										.catch(() => {});
+								});
+
 							next(null, song);
 						})
 						.catch(next);
@@ -387,9 +403,15 @@ export default {
 	 */
 	remove: isAdminRequired(async function remove(session, songId, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+		let song = null;
 		async.waterfall(
 			[
 				next => {
+					songModel.findOne({ _id: songId }, next);
+				},
+
+				(_song, next) => {
+					song = _song;
 					songModel.deleteOne({ _id: songId }, next);
 				},
 
@@ -399,7 +421,14 @@ export default {
 						.then(() => {
 							next();
 						})
-						.catch(next);
+						.catch(next)
+						.finally(() => {
+							song.genres.forEach(genre => {
+								PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
+									.then(() => {})
+									.catch(() => {});
+							});
+						});
 				}
 			],
 			async err => {
@@ -463,6 +492,12 @@ export default {
 							this
 						)
 						.finally(() => {
+							song.genres.forEach(genre => {
+								PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
+									.then(() => {})
+									.catch(() => {});
+							});
+
 							next();
 						});
 				}

+ 2 - 0
backend/logic/db/index.js

@@ -248,6 +248,8 @@ class _DBModule extends CoreClass {
 						return songs[0].duration <= 10800;
 					}, "Max 3 hours per song.");
 
+					this.schemas.playlist.index({ createdFor: 1, type: 1 }, { unique: true });
+
 					// Report
 					this.schemas.report
 						.path("description")

+ 243 - 0
backend/logic/playlists.js

@@ -3,6 +3,7 @@ import async from "async";
 import CoreClass from "../core";
 
 let PlaylistsModule;
+let SongsModule;
 let CacheModule;
 let DBModule;
 let UtilsModule;
@@ -26,6 +27,7 @@ class _PlaylistsModule extends CoreClass {
 		CacheModule = this.moduleManager.modules.cache;
 		DBModule = this.moduleManager.modules.db;
 		UtilsModule = this.moduleManager.modules.utils;
+		SongsModule = this.moduleManager.modules.songs;
 
 		this.playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" });
 		this.playlistSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "playlist" });
@@ -121,6 +123,7 @@ class _PlaylistsModule extends CoreClass {
 					songs: [],
 					createdBy: payload.userId,
 					createdAt: Date.now(),
+					createdFor: null,
 					type: payload.type
 				},
 				(err, playlist) => {
@@ -131,6 +134,246 @@ class _PlaylistsModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Creates a playlist that contains all songs of a specific genre
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.genre - the genre
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	CREATE_GENRE_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			PlaylistsModule.runJob("GET_GENRE_PLAYLIST", { genre: payload.genre.toLowerCase() }, this)
+				.then(() => {
+					reject(new Error("Playlist already exists"));
+				})
+				.catch(err => {
+					if (err.message === "Playlist not found") {
+						PlaylistsModule.playlistModel.create(
+							{
+								isUserModifiable: false,
+								displayName: `Genre - ${payload.genre}`,
+								songs: [],
+								createdBy: "Musare",
+								createdFor: `${payload.genre.toLowerCase()}`,
+								createdAt: Date.now(),
+								type: "genre"
+							},
+							(err, playlist) => {
+								if (err) return reject(new Error(err));
+								return resolve(playlist._id);
+							}
+						);
+					} else reject(new Error(err));
+				});
+		});
+	}
+
+	/**
+	 * Gets all genre playlists
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.includeSongs - include the songs
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_ALL_GENRE_PLAYLISTS(payload) {
+		return new Promise((resolve, reject) => {
+			const includeObject = payload.includeSongs ? null : { songs: false };
+			PlaylistsModule.playlistModel.find({ type: "genre" }, includeObject, (err, playlists) => {
+				if (err) reject(new Error(err));
+				else resolve({ playlists });
+			});
+		});
+	}
+
+	/**
+	 * Gets a genre playlist
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.genre - the genre
+	 * @param {string} payload.includeSongs - include the songs
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_GENRE_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			const includeObject = payload.includeSongs ? null : { songs: false };
+			PlaylistsModule.playlistModel.findOne(
+				{ type: "genre", createdFor: payload.genre },
+				includeObject,
+				(err, playlist) => {
+					if (err) reject(new Error(err));
+					else if (!playlist) reject(new Error("Playlist not found"));
+					else resolve({ playlist });
+				}
+			);
+		});
+	}
+
+	/**
+	 * Adds a song to a playlist
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.playlistId - the playlist id
+	 * @param {string} payload.song - the song
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	ADD_SONG_TO_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			const song = {
+				_id: payload.song._id,
+				songId: payload.song.songId,
+				title: payload.song.title,
+				duration: payload.song.duration
+			};
+
+			PlaylistsModule.playlistModel.updateOne(
+				{ _id: payload.playlistId },
+				{ $push: { songs: song } },
+				{ runValidators: true },
+				err => {
+					if (err) reject(new Error(err));
+					else {
+						PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: payload.playlistId }, this)
+							.then(() => resolve())
+							.catch(err => {
+								reject(new Error(err));
+							});
+					}
+				}
+			);
+		});
+	}
+
+	/**
+	 * Deletes a song from a playlist based on the songId
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.playlistId - the playlist id
+	 * @param {string} payload.songId - the songId
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	DELETE_SONG_FROM_PLAYLIST_BY_SONGID(payload) {
+		return new Promise((resolve, reject) => {
+			PlaylistsModule.playlistModel.updateOne(
+				{ _id: payload.playlistId },
+				{ $pull: { songs: { songId: payload.songId } } },
+				err => {
+					if (err) reject(new Error(err));
+					else {
+						PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: payload.playlistId }, this)
+							.then(() => resolve())
+							.catch(err => {
+								reject(new Error(err));
+							});
+					}
+				}
+			);
+		});
+	}
+
+	/**
+	 * Fills a genre playlist with songs
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.genre - the genre
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	AUTOFILL_GENRE_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						PlaylistsModule.runJob(
+							"GET_GENRE_PLAYLIST",
+							{ genre: payload.genre.toLowerCase(), includeSongs: true },
+							this
+						)
+							.then(response => {
+								next(null, { playlist: response.playlist });
+							})
+							.catch(err => {
+								if (err.message === "Playlist not found") {
+									PlaylistsModule.runJob("CREATE_GENRE_PLAYLIST", { genre: payload.genre }, this)
+										.then(playlistId => {
+											next(null, { playlist: { _id: playlistId, songs: [] } });
+										})
+										.catch(err => {
+											next(err);
+										});
+								} else next(err);
+							});
+					},
+
+					(data, next) => {
+						SongsModule.runJob("GET_ALL_SONGS_WITH_GENRE", { genre: payload.genre }, this)
+							.then(response => {
+								data.songs = response.songs;
+								next(null, data);
+							})
+							.catch(err => {
+								console.log(err);
+								next(err);
+							});
+					},
+
+					(data, next) => {
+						data.songsToDelete = [];
+						data.songsToAdd = [];
+
+						data.playlist.songs.forEach(playlistSong => {
+							const found = data.songs.find(song => playlistSong.songId === song.songId);
+							if (!found) data.songsToDelete.push(playlistSong);
+						});
+
+						data.songs.forEach(song => {
+							const found = data.playlist.songs.find(playlistSong => song.songId === playlistSong.songId);
+							if (!found) data.songsToAdd.push(song);
+						});
+
+						next(null, data);
+					},
+
+					(data, next) => {
+						const promises = [];
+						data.songsToAdd.forEach(song => {
+							promises.push(
+								PlaylistsModule.runJob(
+									"ADD_SONG_TO_PLAYLIST",
+									{ playlistId: data.playlist._id, song },
+									this
+								)
+							);
+						});
+						data.songsToDelete.forEach(song => {
+							promises.push(
+								PlaylistsModule.runJob(
+									"DELETE_SONG_FROM_PLAYLIST_BY_SONGID",
+									{
+										playlistId: data.playlist._id,
+										songId: song.songId
+									},
+									this
+								)
+							);
+						});
+
+						Promise.allSettled(promises)
+							.then(() => {
+								next();
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				err => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({});
+				}
+			);
+		});
+	}
+
 	/**
 	 * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
 	 *

+ 61 - 0
backend/logic/songs.js

@@ -310,6 +310,67 @@ class _SongsModule extends CoreClass {
 			);
 		});
 	}
+
+	/**
+	 * 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({}, { 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(
+							{ genres: { $regex: new RegExp(`^${payload.genre.toLowerCase()}$`, "i") } },
+							next
+						);
+					}
+				],
+				(err, songs) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({ songs });
+				}
+			)
+		);
+	}
 }
 
 export default new _SongsModule();