Prechádzať zdrojové kódy

refactor(liked/disliked songs): now using a playlist structure for like/disliked songs

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 rokov pred
rodič
commit
b50f2c1ad1

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

@@ -210,6 +210,7 @@ export default {
 			},
 			this
 		);
+
 		async.waterfall(
 			[
 				next => (data ? next() : cb({ status: "failure", message: "Invalid data" })),
@@ -731,7 +732,7 @@ export default {
 			[
 				next => {
 					if (!songId || typeof songId !== "string") return next("Invalid song id.");
-					if (!playlistId || typeof playlistId !== "string") return next("Invalid playlist id.");
+					if (!playlistId) return next("Invalid playlist id.");
 					return next();
 				},
 

+ 350 - 332
backend/logic/actions/songs.js

@@ -464,218 +464,211 @@ export default {
 	 * Likes a song
 	 *
 	 * @param session
-	 * @param songId - the song id
+	 * @param musareSongId - the song id
 	 * @param cb
 	 */
-	like: isLoginRequired(async function like(session, songId, cb) {
+	like: isLoginRequired(async function like(session, musareSongId, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+
 		async.waterfall(
 			[
 				next => {
-					songModel.findOne({ songId }, next);
+					songModel.findOne({ songId: musareSongId }, next);
 				},
 
 				(song, next) => {
 					if (!song) return next("No song found with that id.");
-					return next(null, song);
+					return next(null, song._id);
+				},
+
+				(songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
+
+				(songId, user, next) => {
+					if (!user) return next("User does not exist.");
+
+					return this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "addSongToPlaylist",
+								args: [false, musareSongId, user.likedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to add song to the 'Liked Songs' playlist.");
+							return next(null, songId, user.dislikedSongsPlaylist);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, dislikedSongsPlaylist, next) => {
+					this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "removeSongFromPlaylist",
+								args: [musareSongId, dislikedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to remove song from the 'Disliked Songs' playlist.");
+							return next(null, songId);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, next) => {
+					SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
+						.then(ratings => next(null, songId, ratings))
+						.catch(err => next(err));
 				}
 			],
-			async (err, song) => {
+			async (err, songId, { likes, dislikes }) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "SONGS_LIKE", `User "${session.userId}" failed to like song ${songId}. "${err}"`);
+					this.log(
+						"ERROR",
+						"SONGS_LIKE",
+						`User "${session.userId}" failed to like song ${musareSongId}. "${err}"`
+					);
 					return cb({ status: "failure", message: err });
 				}
 
-				const oldSongId = songId;
-				songId = song._id;
+				SongsModule.runJob("UPDATE_SONG", { songId });
 
-				return userModel.findOne({ _id: session.userId }, (err, user) => {
-					if (user.liked.indexOf(songId) !== -1)
-						return cb({
-							status: "failure",
-							message: "You have already liked this song."
-						});
+				CacheModule.runJob("PUB", {
+					channel: "song.like",
+					value: JSON.stringify({
+						songId: musareSongId,
+						userId: session.userId,
+						likes,
+						dislikes
+					})
+				});
 
-					return userModel.updateOne(
-						{ _id: session.userId },
-						{
-							$push: { liked: songId },
-							$pull: { disliked: songId }
-						},
-						err => {
-							if (!err) {
-								return userModel.countDocuments({ liked: songId }, (err, likes) => {
-									if (err)
-										return cb({
-											status: "failure",
-											message: "Something went wrong while liking this song."
-										});
-
-									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
-										if (err)
-											return cb({
-												status: "failure",
-												message: "Something went wrong while liking this song."
-											});
-
-										return songModel.update(
-											{ _id: songId },
-											{
-												$set: {
-													likes,
-													dislikes
-												}
-											},
-											err => {
-												if (err)
-													return cb({
-														status: "failure",
-														message: "Something went wrong while liking this song."
-													});
-
-												SongsModule.runJob("UPDATE_SONG", { songId });
-
-												CacheModule.runJob("PUB", {
-													channel: "song.like",
-													value: JSON.stringify({
-														songId: oldSongId,
-														userId: session.userId,
-														likes,
-														dislikes
-													})
-												});
-
-												ActivitiesModule.runJob("ADD_ACTIVITY", {
-													userId: session.userId,
-													activityType: "liked_song",
-													payload: [songId]
-												});
-
-												return cb({
-													status: "success",
-													message: "You have successfully liked this song."
-												});
-											}
-										);
-									});
-								});
-							}
-							return cb({
-								status: "failure",
-								message: "Something went wrong while liking this song."
-							});
-						}
-					);
+				ActivitiesModule.runJob("ADD_ACTIVITY", {
+					userId: session.userId,
+					activityType: "liked_song",
+					payload: [songId]
+				});
+
+				return cb({
+					status: "success",
+					message: "You have successfully liked this song."
 				});
 			}
 		);
 	}),
 
+	// TODO: ALready liked/disliked etc.
+
 	/**
 	 * Dislikes a song
 	 *
 	 * @param session
-	 * @param songId - the song id
+	 * @param musareSongId - the song id
 	 * @param cb
 	 */
-	dislike: isLoginRequired(async function dislike(session, songId, cb) {
+	dislike: isLoginRequired(async function dislike(session, musareSongId, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+
 		async.waterfall(
 			[
 				next => {
-					songModel.findOne({ songId }, next);
+					songModel.findOne({ songId: musareSongId }, next);
 				},
 
 				(song, next) => {
 					if (!song) return next("No song found with that id.");
-					return next(null, song);
+					return next(null, song._id);
+				},
+
+				(songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
+
+				(songId, user, next) => {
+					if (!user) return next("User does not exist.");
+
+					return this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "addSongToPlaylist",
+								args: [false, musareSongId, user.dislikedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to add song to the 'Disliked Songs' playlist.");
+							return next(null, songId, user.likedSongsPlaylist);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, likedSongsPlaylist, next) => {
+					this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "removeSongFromPlaylist",
+								args: [musareSongId, likedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to remove song from the 'Liked Songs' playlist.");
+							return next(null, songId);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, next) => {
+					SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
+						.then(ratings => next(null, songId, ratings))
+						.catch(err => next(err));
 				}
 			],
-			async (err, song) => {
+			async (err, songId, { likes, dislikes }) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
 						"ERROR",
 						"SONGS_DISLIKE",
-						`User "${session.userId}" failed to like song ${songId}. "${err}"`
+						`User "${session.userId}" failed to dislike song ${musareSongId}. "${err}"`
 					);
 					return cb({ status: "failure", message: err });
 				}
-				const oldSongId = songId;
-				songId = song._id;
-				return userModel.findOne({ _id: session.userId }, (err, user) => {
-					if (user.disliked.indexOf(songId) !== -1)
-						return cb({
-							status: "failure",
-							message: "You have already disliked this song."
-						});
 
-					return userModel.updateOne(
-						{ _id: session.userId },
-						{
-							$push: { disliked: songId },
-							$pull: { liked: songId }
-						},
-						err => {
-							if (!err) {
-								return userModel.countDocuments({ liked: songId }, (err, likes) => {
-									if (err)
-										return cb({
-											status: "failure",
-											message: "Something went wrong while disliking this song."
-										});
-
-									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
-										if (err)
-											return cb({
-												status: "failure",
-												message: "Something went wrong while disliking this song."
-											});
-
-										return songModel.update(
-											{ _id: songId },
-											{
-												$set: {
-													likes,
-													dislikes
-												}
-											},
-											err => {
-												if (err)
-													return cb({
-														status: "failure",
-														message: "Something went wrong while disliking this song."
-													});
-
-												SongsModule.runJob("UPDATE_SONG", { songId });
-												CacheModule.runJob("PUB", {
-													channel: "song.dislike",
-													value: JSON.stringify({
-														songId: oldSongId,
-														userId: session.userId,
-														likes,
-														dislikes
-													})
-												});
-
-												return cb({
-													status: "success",
-													message: "You have successfully disliked this song."
-												});
-											}
-										);
-									});
-								});
-							}
-							return cb({
-								status: "failure",
-								message: "Something went wrong while disliking this song."
-							});
-						}
-					);
+				SongsModule.runJob("UPDATE_SONG", { songId });
+
+				CacheModule.runJob("PUB", {
+					channel: "song.dislike",
+					value: JSON.stringify({
+						songId: musareSongId,
+						userId: session.userId,
+						likes,
+						dislikes
+					})
+				});
+
+				return cb({
+					status: "success",
+					message: "You have successfully disliked this song."
 				});
 			}
 		);
@@ -685,100 +678,100 @@ export default {
 	 * Undislikes a song
 	 *
 	 * @param session
-	 * @param songId - the song id
+	 * @param musareSongId - the song id
 	 * @param cb
 	 */
-	undislike: isLoginRequired(async function undislike(session, songId, cb) {
+	undislike: isLoginRequired(async function undislike(session, musareSongId, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+
 		async.waterfall(
 			[
 				next => {
-					songModel.findOne({ songId }, next);
+					songModel.findOne({ songId: musareSongId }, next);
 				},
 
 				(song, next) => {
 					if (!song) return next("No song found with that id.");
-					return next(null, song);
+					return next(null, song._id);
+				},
+
+				(songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
+
+				(songId, user, next) => {
+					if (!user) return next("User does not exist.");
+
+					return this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "removeSongFromPlaylist",
+								args: [musareSongId, user.dislikedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to remove song from the 'Disliked Songs' playlist.");
+							return next(null, songId, user.likedSongsPlaylist);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, likedSongsPlaylist, next) => {
+					this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "removeSongFromPlaylist",
+								args: [musareSongId, likedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to remove song from the 'Liked Songs' playlist.");
+							return next(null, songId);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, next) => {
+					SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
+						.then(ratings => next(null, songId, ratings))
+						.catch(err => next(err));
 				}
 			],
-			async (err, song) => {
+			async (err, songId, { likes, dislikes }) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
 						"ERROR",
 						"SONGS_UNDISLIKE",
-						`User "${session.userId}" failed to like song ${songId}. "${err}"`
+						`User "${session.userId}" failed to undislike song ${musareSongId}. "${err}"`
 					);
 					return cb({ status: "failure", message: err });
 				}
-				const oldSongId = songId;
-				songId = song._id;
-				return userModel.findOne({ _id: session.userId }, (err, user) => {
-					if (user.disliked.indexOf(songId) === -1)
-						return cb({
-							status: "failure",
-							message: "You have not disliked this song."
-						});
 
-					return userModel.updateOne(
-						{ _id: session.userId },
-						{ $pull: { liked: songId, disliked: songId } },
-						err => {
-							if (!err) {
-								return userModel.countDocuments({ liked: songId }, (err, likes) => {
-									if (err)
-										return cb({
-											status: "failure",
-											message: "Something went wrong while undisliking this song."
-										});
-
-									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
-										if (err)
-											return cb({
-												status: "failure",
-												message: "Something went wrong while undisliking this song."
-											});
-
-										return songModel.update(
-											{ _id: songId },
-											{
-												$set: {
-													likes,
-													dislikes
-												}
-											},
-											err => {
-												if (err)
-													return cb({
-														status: "failure",
-														message: "Something went wrong while undisliking this song."
-													});
-												SongsModule.runJob("UPDATE_SONG", { songId });
-												CacheModule.runJob("PUB", {
-													channel: "song.undislike",
-													value: JSON.stringify({
-														songId: oldSongId,
-														userId: session.userId,
-														likes,
-														dislikes
-													})
-												});
-												return cb({
-													status: "success",
-													message: "You have successfully undisliked this song."
-												});
-											}
-										);
-									});
-								});
-							}
-							return cb({
-								status: "failure",
-								message: "Something went wrong while undisliking this song."
-							});
-						}
-					);
+				SongsModule.runJob("UPDATE_SONG", { songId });
+
+				CacheModule.runJob("PUB", {
+					channel: "song.undislike",
+					value: JSON.stringify({
+						songId: musareSongId,
+						userId: session.userId,
+						likes,
+						dislikes
+					})
+				});
+
+				return cb({
+					status: "success",
+					message: "You have successfully undisliked this song."
 				});
 			}
 		);
@@ -788,99 +781,100 @@ export default {
 	 * Unlikes a song
 	 *
 	 * @param session
-	 * @param songId - the song id
+	 * @param musareSongId - the song id
 	 * @param cb
 	 */
-	unlike: isLoginRequired(async function unlike(session, songId, cb) {
+	unlike: isLoginRequired(async function unlike(session, musareSongId, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+
 		async.waterfall(
 			[
 				next => {
-					songModel.findOne({ songId }, next);
+					songModel.findOne({ songId: musareSongId }, next);
 				},
 
 				(song, next) => {
 					if (!song) return next("No song found with that id.");
-					return next(null, song);
+					return next(null, song._id);
+				},
+
+				(songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
+
+				(songId, user, next) => {
+					if (!user) return next("User does not exist.");
+
+					return this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "removeSongFromPlaylist",
+								args: [musareSongId, user.dislikedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to remove song from the 'Disliked Songs' playlist.");
+							return next(null, songId, user.likedSongsPlaylist);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, likedSongsPlaylist, next) => {
+					this.module
+						.runJob(
+							"RUN_ACTION2",
+							{
+								session,
+								namespace: "playlists",
+								action: "removeSongFromPlaylist",
+								args: [musareSongId, likedSongsPlaylist]
+							},
+							this
+						)
+						.then(res => {
+							if (res.status === "failure")
+								return next("Unable to remove song from the 'Liked Songs' playlist.");
+							return next(null, songId);
+						})
+						.catch(err => next(err));
+				},
+
+				(songId, next) => {
+					SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
+						.then(ratings => next(null, songId, ratings))
+						.catch(err => next(err));
 				}
 			],
-			async (err, song) => {
+			async (err, songId, { likes, dislikes }) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
 						"ERROR",
 						"SONGS_UNLIKE",
-						`User "${session.userId}" failed to like song ${songId}. "${err}"`
+						`User "${session.userId}" failed to unlike song ${musareSongId}. "${err}"`
 					);
 					return cb({ status: "failure", message: err });
 				}
-				const oldSongId = songId;
-				songId = song._id;
-				return userModel.findOne({ _id: session.userId }, (err, user) => {
-					if (user.liked.indexOf(songId) === -1)
-						return cb({
-							status: "failure",
-							message: "You have not liked this song."
-						});
 
-					return userModel.updateOne(
-						{ _id: session.userId },
-						{ $pull: { liked: songId, disliked: songId } },
-						err => {
-							if (!err) {
-								return userModel.countDocuments({ liked: songId }, (err, likes) => {
-									if (err)
-										return cb({
-											status: "failure",
-											message: "Something went wrong while unliking this song."
-										});
-
-									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
-										if (err)
-											return cb({
-												status: "failure",
-												message: "Something went wrong while undiking this song."
-											});
-										return songModel.updateOne(
-											{ _id: songId },
-											{
-												$set: {
-													likes,
-													dislikes
-												}
-											},
-											err => {
-												if (err)
-													return cb({
-														status: "failure",
-														message: "Something went wrong while unliking this song."
-													});
-												SongsModule.runJob("UPDATE_SONG", { songId });
-												CacheModule.runJob("PUB", {
-													channel: "song.unlike",
-													value: JSON.stringify({
-														songId: oldSongId,
-														userId: session.userId,
-														likes,
-														dislikes
-													})
-												});
-												return cb({
-													status: "success",
-													message: "You have successfully unliked this song."
-												});
-											}
-										);
-									});
-								});
-							}
-							return cb({
-								status: "failure",
-								message: "Something went wrong while unliking this song."
-							});
-						}
-					);
+				SongsModule.runJob("UPDATE_SONG", { songId });
+
+				CacheModule.runJob("PUB", {
+					channel: "song.unlike",
+					value: JSON.stringify({
+						songId: musareSongId,
+						userId: session.userId,
+						likes,
+						dislikes
+					})
+				});
+
+				return cb({
+					status: "success",
+					message: "You have successfully unliked this song."
 				});
 			}
 		);
@@ -890,53 +884,77 @@ export default {
 	 * Gets user's own song ratings
 	 *
 	 * @param session
-	 * @param songId - the song id
+	 * @param musareSongId - the song id
 	 * @param cb
 	 */
-	getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, songId, cb) {
-		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+	getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, musareSongId, cb) {
+		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+
 		async.waterfall(
 			[
 				next => {
-					songModel.findOne({ songId }, next);
+					songModel.findOne({ songId: musareSongId }, next);
 				},
 
 				(song, next) => {
 					if (!song) return next("No song found with that id.");
-					return next(null, song);
-				}
+					return next(null);
+				},
+
+				next =>
+					playlistModel.findOne(
+						{ createdBy: session.userId, displayName: "Liked Songs" },
+						(err, playlist) => {
+							if (err) return next(err);
+							if (!playlist) return next("'Liked Songs' playlist does not exist.");
+
+							let isLiked = false;
+
+							Object.values(playlist.songs).forEach(song => {
+								// song is found in 'liked songs' playlist
+								if (song.songId === musareSongId) isLiked = true;
+							});
+
+							return next(null, isLiked);
+						}
+					),
+
+				(isLiked, next) =>
+					playlistModel.findOne(
+						{ createdBy: session.userId, displayName: "Disliked Songs" },
+						(err, playlist) => {
+							if (err) return next(err);
+							if (!playlist) return next("'Disliked Songs' playlist does not exist.");
+
+							const ratings = { isLiked, isDisliked: false };
+
+							Object.values(playlist.songs).forEach(song => {
+								// song is found in 'disliked songs' playlist
+								if (song.songId === musareSongId) ratings.isDisliked = true;
+							});
+
+							return next(null, ratings);
+						}
+					)
 			],
-			async (err, song) => {
+			async (err, { isLiked, isDisliked }) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
 						"ERROR",
 						"SONGS_GET_OWN_RATINGS",
-						`User "${session.userId}" failed to get ratings for ${songId}. "${err}"`
+						`User "${session.userId}" failed to get ratings for ${musareSongId}. "${err}"`
 					);
 					return cb({ status: "failure", message: err });
 				}
-				const newSongId = song._id;
-				return userModel.findOne({ _id: session.userId }, async (err, user) => {
-					if (!err && user) {
-						return cb({
-							status: "success",
-							songId,
-							liked: user.liked.indexOf(newSongId) !== -1,
-							disliked: user.disliked.indexOf(newSongId) !== -1
-						});
-					}
-					return cb({
-						status: "failure",
-						message: await UtilsModule.runJob(
-							"GET_ERROR",
-							{
-								error: err
-							},
-							this
-						)
-					});
+
+				return cb({
+					status: "success",
+					songId: musareSongId,
+					liked: isLiked,
+					disliked: isDisliked
 				});
 			}
 		);

+ 40 - 2
backend/logic/actions/users.js

@@ -16,6 +16,7 @@ const CacheModule = moduleManager.modules.cache;
 const MailModule = moduleManager.modules.mail;
 const PunishmentsModule = moduleManager.modules.punishments;
 const ActivitiesModule = moduleManager.modules.activities;
+const PlaylistsModule = moduleManager.modules.playlists;
 
 CacheModule.runJob("SUB", {
 	channel: "user.updateUsername",
@@ -387,10 +388,47 @@ export default {
 				},
 
 				// respond with the new user
-				(newUser, next) => {
+				(user, next) => {
 					verifyEmailSchema(email, username, verificationToken, err => {
-						next(err, newUser);
+						next(err, user._id);
 					});
+				},
+
+				// create a liked songs playlist for the new user
+				(userId, next) => {
+					PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
+						userId,
+						displayName: "Liked Songs"
+					})
+						.then(likedSongsPlaylist => {
+							next(null, likedSongsPlaylist, userId);
+						})
+						.catch(err => next(err));
+				},
+
+				// create a disliked songs playlist for the new user
+				(likedSongsPlaylist, userId, next) => {
+					PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
+						userId,
+						displayName: "Disliked Songs"
+					})
+						.then(dislikedSongsPlaylist => {
+							next(null, { likedSongsPlaylist, dislikedSongsPlaylist }, userId);
+						})
+						.catch(err => next(err));
+				},
+
+				// associate liked + disliked songs playlist to the user object
+				({ likedSongsPlaylist, dislikedSongsPlaylist }, userId, next) => {
+					userModel.updateOne(
+						{ _id: userId },
+						{ $set: { likedSongsPlaylist, dislikedSongsPlaylist } },
+						{ runValidators: true },
+						err => {
+							if (err) return next(err);
+							return next(null, userId);
+						}
+					);
 				}
 			],
 			async (err, user) => {

+ 49 - 9
backend/logic/app.js

@@ -17,6 +17,7 @@ let MailModule;
 let CacheModule;
 let DBModule;
 let ActivitiesModule;
+let PlaylistsModule;
 let UtilsModule;
 
 class _AppModule extends CoreClass {
@@ -38,6 +39,7 @@ class _AppModule extends CoreClass {
 			CacheModule = this.moduleManager.modules.cache;
 			DBModule = this.moduleManager.modules.db;
 			ActivitiesModule = this.moduleManager.modules.activities;
+			PlaylistsModule = this.moduleManager.modules.playlists;
 			UtilsModule = this.moduleManager.modules.utils;
 
 			const app = (this.app = express());
@@ -312,15 +314,6 @@ class _AppModule extends CoreClass {
 							userModel.create(user, next);
 						},
 
-						// add the activity of account creation
-						(user, next) => {
-							ActivitiesModule.runJob("ADD_ACTIVITY", {
-								userId: user._id,
-								activityType: "created_account"
-							});
-							next(null, user);
-						},
-
 						(user, next) => {
 							MailModule.runJob("GET_SCHEMA", {
 								schemaName: "verifyEmail"
@@ -329,6 +322,53 @@ class _AppModule extends CoreClass {
 									next(err, user._id);
 								});
 							});
+						},
+
+						// create a liked songs playlist for the new user
+						(userId, next) => {
+							PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
+								userId,
+								displayName: "Liked Songs"
+							})
+								.then(likedSongsPlaylist => {
+									next(null, likedSongsPlaylist, userId);
+								})
+								.catch(err => next(err));
+						},
+
+						// create a disliked songs playlist for the new user
+						(likedSongsPlaylist, userId, next) => {
+							PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
+								userId,
+								displayName: "Disliked Songs"
+							})
+								.then(dislikedSongsPlaylist => {
+									next(null, { likedSongsPlaylist, dislikedSongsPlaylist }, userId);
+								})
+								.catch(err => next(err));
+						},
+
+						// associate liked + disliked songs playlist to the user object
+						({ likedSongsPlaylist, dislikedSongsPlaylist }, userId, next) => {
+							userModel.updateOne(
+								{ _id: userId },
+								{ $set: { likedSongsPlaylist, dislikedSongsPlaylist } },
+								{ runValidators: true },
+								err => {
+									if (err) return next(err);
+									return next(null, userId);
+								}
+							);
+						},
+
+						// add the activity of account creation
+						(userId, next) => {
+							ActivitiesModule.runJob("ADD_ACTIVITY", {
+								userId,
+								activityType: "created_account"
+							});
+
+							next(null, userId);
 						}
 					],
 					async (err, userId) => {

+ 1 - 0
backend/logic/db/schemas/playlist.js

@@ -1,5 +1,6 @@
 export default {
 	displayName: { type: String, min: 2, max: 32, required: true },
+	isUserModifiable: { type: Boolean, default: true, required: true },
 	songs: { type: Array },
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now, required: true }

+ 4 - 2
backend/logic/db/schemas/user.js

@@ -1,3 +1,5 @@
+import mongoose from "mongoose";
+
 export default {
 	username: { type: String, required: true },
 	role: { type: String, default: "default", required: true },
@@ -30,8 +32,8 @@ export default {
 	statistics: {
 		songsRequested: { type: Number, default: 0, required: true }
 	},
-	liked: [{ type: String }],
-	disliked: [{ type: String }],
+	likedSongsPlaylist: { type: mongoose.Schema.Types.ObjectId },
+	dislikedSongsPlaylist: { type: mongoose.Schema.Types.ObjectId },
 	favoriteStations: [{ type: String }],
 	name: { type: String, default: "" },
 	location: { type: String, default: "" },

+ 26 - 0
backend/logic/playlists.js

@@ -104,6 +104,32 @@ class _PlaylistsModule extends CoreClass {
 		);
 	}
 
+	/**
+	 * Creates a playlist that is not generated or editable by a user e.g. liked songs playlist
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.userId - the id of the user to create the playlist for
+	 * @param {string} payload.displayName - the display name of the playlist
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	CREATE_READ_ONLY_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			PlaylistsModule.playlistModel.create(
+				{
+					isUserModifiable: false,
+					displayName: payload.displayName,
+					songs: [],
+					createdBy: payload.userId,
+					createdAt: Date.now()
+				},
+				(err, playlist) => {
+					if (err) return reject(new Error(err));
+					return resolve(playlist._id);
+				}
+			);
+		});
+	}
+
 	/**
 	 * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
 	 *

+ 55 - 1
backend/logic/songs.js

@@ -228,7 +228,6 @@ class _SongsModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	DELETE_SONG(payload) {
-		// songId, cb
 		return new Promise((resolve, reject) =>
 			async.waterfall(
 				[
@@ -256,6 +255,61 @@ class _SongsModule extends CoreClass {
 			)
 		);
 	}
+
+	/**
+	 * Recalculates dislikes and likes for a song
+	 *
+	 * @param {object} payload - returns an object containing the payload
+	 * @param {string} payload.musareSongId - the (musare) id of the song
+	 * @param {string} payload.songId - the (mongodb) id of the song
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	async RECALCULATE_SONG_RATINGS(payload) {
+		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
+
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						playlistModel.countDocuments(
+							{ songs: { $elemMatch: { songId: payload.musareSongId } }, displayName: "Liked Songs" },
+							(err, likes) => {
+								if (err) return next(err);
+								return next(null, likes);
+							}
+						);
+					},
+
+					(likes, next) => {
+						playlistModel.countDocuments(
+							{ songs: { $elemMatch: { songId: payload.musareSongId } }, displayName: "Disliked Songs" },
+							(err, dislikes) => {
+								if (err) return next(err);
+								return next(err, { likes, dislikes });
+							}
+						);
+					},
+
+					({ likes, dislikes }, next) => {
+						SongsModule.songModel.updateOne(
+							{ _id: payload.songId },
+							{
+								$set: {
+									likes,
+									dislikes
+								}
+							},
+							err => next(err, { likes, dislikes })
+						);
+					}
+				],
+				(err, { likes, dislikes }) => {
+					if (err) return reject(new Error(err));
+					return resolve({ likes, dislikes });
+				}
+			);
+		});
+	}
 }
 
 export default new _SongsModule();