import async from "async"; import { isAdminRequired, isLoginRequired } from "./hooks"; import moduleManager from "../../index"; const DBModule = moduleManager.modules.db; const UtilsModule = moduleManager.modules.utils; const WSModule = moduleManager.modules.ws; const CacheModule = moduleManager.modules.cache; const SongsModule = moduleManager.modules.songs; const ActivitiesModule = moduleManager.modules.activities; const YouTubeModule = moduleManager.modules.youtube; const PlaylistsModule = moduleManager.modules.playlists; CacheModule.runJob("SUB", { channel: "song.newUnverifiedSong", cb: async songId => { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }); songModel.findOne({ _id: songId }, (err, song) => WSModule.runJob("EMIT_TO_ROOMS", { rooms: ["admin.unverifiedSongs", `edit-song.${songId}`], args: ["event:admin.unverifiedSong.created", { data: { song } }] }) ); } }); CacheModule.runJob("SUB", { channel: "song.removedUnverifiedSong", cb: songId => { WSModule.runJob("EMIT_TO_ROOM", { room: "admin.unverifiedSongs", args: ["event:admin.unverifiedSong.deleted", { data: { songId } }] }); } }); CacheModule.runJob("SUB", { channel: "song.updatedUnverifiedSong", cb: async songId => { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }); songModel.findOne({ _id: songId }, (err, song) => { WSModule.runJob("EMIT_TO_ROOM", { room: "admin.unverifiedSongs", args: ["event:admin.unverifiedSong.updated", { data: { song } }] }); }); } }); CacheModule.runJob("SUB", { channel: "song.newVerifiedSong", cb: async songId => { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }); songModel.findOne({ _id: songId }, (err, song) => WSModule.runJob("EMIT_TO_ROOMS", { rooms: ["admin.songs", `edit-song.${songId}`], args: ["event:admin.verifiedSong.created", { data: { song } }] }) ); } }); CacheModule.runJob("SUB", { channel: "song.removedVerifiedSong", cb: songId => { WSModule.runJob("EMIT_TO_ROOM", { room: "admin.songs", args: ["event:admin.verifiedSong.deleted", { data: { songId } }] }); } }); CacheModule.runJob("SUB", { channel: "song.updatedVerifiedSong", cb: async songId => { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }); songModel.findOne({ _id: songId }, (err, song) => { WSModule.runJob("EMIT_TO_ROOM", { room: "admin.songs", args: ["event:admin.verifiedSong.updated", { data: { song } }] }); }); } }); CacheModule.runJob("SUB", { channel: "song.newHiddenSong", cb: async songId => { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }); songModel.findOne({ _id: songId }, (err, song) => WSModule.runJob("EMIT_TO_ROOMS", { rooms: ["admin.hiddenSongs", `edit-song.${songId}`], args: ["event:admin.hiddenSong.created", { data: { song } }] }) ); } }); CacheModule.runJob("SUB", { channel: "song.removedHiddenSong", cb: songId => { WSModule.runJob("EMIT_TO_ROOM", { room: "admin.hiddenSongs", args: ["event:admin.hiddenSong.deleted", { data: { songId } }] }); } }); CacheModule.runJob("SUB", { channel: "song.updatedHiddenSong", cb: async songId => { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }); songModel.findOne({ _id: songId }, (err, song) => { WSModule.runJob("EMIT_TO_ROOM", { room: "admin.hiddenSongs", args: ["event:admin.hiddenSong.updated", { data: { song } }] }); }); } }); CacheModule.runJob("SUB", { channel: "song.like", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.youtubeId}`, args: [ "event:song.liked", { data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:song.ratings.updated", { data: { youtubeId: data.youtubeId, liked: true, disliked: false } }); }); }); } }); CacheModule.runJob("SUB", { channel: "song.dislike", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.youtubeId}`, args: [ "event:song.disliked", { data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:song.ratings.updated", { data: { youtubeId: data.youtubeId, liked: false, disliked: true } }); }); }); } }); CacheModule.runJob("SUB", { channel: "song.unlike", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.youtubeId}`, args: [ "event:song.unliked", { data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:song.ratings.updated", { data: { youtubeId: data.youtubeId, liked: false, disliked: false } }); }); }); } }); CacheModule.runJob("SUB", { channel: "song.undislike", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.youtubeId}`, args: [ "event:song.undisliked", { data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:song.ratings.updated", { data: { youtubeId: data.youtubeId, liked: false, disliked: false } }); }); }); } }); export default { /** * Returns the length of the songs list * * @param {object} session - the session object automatically added by the websocket * @param cb */ length: isAdminRequired(async function length(session, status, cb) { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this); async.waterfall( [ next => { songModel.countDocuments({ status }, next); } ], async (err, count) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "SONGS_LENGTH", `Failed to get length from songs that have the status ${status}. "${err}"` ); return cb({ status: "error", message: err }); } this.log( "SUCCESS", "SONGS_LENGTH", `Got length from songs that have the status ${status} successfully.` ); return cb({ status: "success", message: "Successfully got length of songs.", data: { length: count } }); } ); }), /** * Gets a set of songs * * @param {object} session - the session object automatically added by the websocket * @param set - the set number to return * @param cb */ getSet: isAdminRequired(async function getSet(session, set, status, cb) { const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this); async.waterfall( [ next => { songModel .find({ status }) .skip(15 * (set - 1)) .limit(15) .exec(next); } ], async (err, songs) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "SONGS_GET_SET", `Failed to get set from songs that have the status ${status}. "${err}"` ); return cb({ status: "error", message: err }); } this.log("SUCCESS", "SONGS_GET_SET", `Got set from songs that have the status ${status} successfully.`); return cb({ status: "success", message: "Successfully got set of songs.", data: { songs } }); } ); }), /** * Updates all songs * * @param {object} session - the session object automatically added by the websocket * @param cb */ updateAll: isAdminRequired(async function length(session, cb) { async.waterfall( [ next => { SongsModule.runJob("UPDATE_ALL_SONGS", {}, this) .then(() => { next(); }) .catch(err => { next(err); }); } ], async err => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "SONGS_UPDATE_ALL", `Failed to update all songs. "${err}"`); return cb({ status: "error", message: err }); } this.log("SUCCESS", "SONGS_UPDATE_ALL", `Updated all songs successfully.`); return cb({ status: "success", message: "Successfully updated all songs." }); } ); }), /** * Gets a song from the Musare song id * * @param {object} session - the session object automatically added by the websocket * @param {string} songId - the song id * @param {Function} cb */ getSongFromSongId: isAdminRequired(function getSongFromSongId(session, songId, cb) { async.waterfall( [ next => { SongsModule.runJob("GET_SONG", { songId }, this) .then(response => next(null, response.song)) .catch(err => next(err)); } ], async (err, song) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`); return cb({ status: "error", message: err }); } this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`); return cb({ status: "success", data: { song } }); } ); }), /** * Updates a song * * @param {object} session - the session object automatically added by the websocket * @param {string} songId - the song id * @param {object} song - the updated song object * @param {Function} cb */ 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); } ], async (err, song) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`); return cb({ status: "error", message: err }); } this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`); if (song.status === "verified") { CacheModule.runJob("PUB", { channel: "song.updatedVerifiedSong", value: song._id }); } else if (song.status === "unverified") { CacheModule.runJob("PUB", { channel: "song.updatedUnverifiedSong", value: song._id }); } else if (song.status === "hidden") { CacheModule.runJob("PUB", { channel: "song.updatedHiddenSong", value: song._id }); } return cb({ status: "success", message: "Song has been successfully updated", data: { song } }); } ); }), // /** // * Removes a song // * // * @param session // * @param songId - the song id // * @param cb // */ // 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); // }, // (res, next) => { // CacheModule.runJob("HDEL", { table: "songs", key: songId }, this) // .then(() => { // next(); // }) // .catch(next) // .finally(() => { // song.genres.forEach(genre => { // PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre }) // .then(() => {}) // .catch(() => {}); // }); // }); // } // ], // async err => { // if (err) { // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); // this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`); // return cb({ status: "error", message: err }); // } // this.log("SUCCESS", "SONGS_REMOVE", `Successfully removed song "${songId}".`); // if (song.status === "verified") { // CacheModule.runJob("PUB", { // channel: "song.removedVerifiedSong", // value: songId // }); // } // if (song.status === "unverified") { // CacheModule.runJob("PUB", { // channel: "song.removedUnverifiedSong", // value: songId // }); // } // if (song.status === "hidden") { // CacheModule.runJob("PUB", { // channel: "song.removedHiddenSong", // value: songId // }); // } // return cb({ // status: "success", // message: "Song has been successfully removed" // }); // } // ); // }), /** * Searches through official songs * * @param {object} session - the session object automatically added by the websocket * @param {string} query - the query * @param {string} page - the page * @param {Function} cb - gets called with the result */ searchOfficial: isLoginRequired(async function searchOfficial(session, query, page, cb) { async.waterfall( [ next => { if ((!query && query !== "") || typeof query !== "string") next("Invalid query."); else next(); }, next => { SongsModule.runJob("SEARCH", { query, includeVerified: true, trimmed: true, page }) .then(response => { next(null, response); }) .catch(err => { next(err); }); } ], async (err, data) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "SONGS_SEARCH_OFFICIAL", `Searching songs failed. "${err}"`); return cb({ status: "error", message: err }); } this.log("SUCCESS", "SONGS_SEARCH_OFFICIAL", "Searching songs successful."); return cb({ status: "success", data }); } ); }), /** * Requests a song * * @param {object} session - the session object automatically added by the websocket * @param {string} youtubeId - the youtube id of the song that gets requested * @param {string} returnSong - returns the simple song * @param {Function} cb - gets called with the result */ request: isLoginRequired(async function add(session, youtubeId, returnSong, cb) { SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this) .then(response => { this.log( "SUCCESS", "SONGS_REQUEST", `User "${session.userId}" successfully requested song "${youtubeId}".` ); return cb({ status: "success", message: "Successfully requested that song", song: returnSong ? response.song : null }); }) .catch(async _err => { const err = await UtilsModule.runJob("GET_ERROR", { error: _err }, this); this.log( "ERROR", "SONGS_REQUEST", `Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"` ); return cb({ status: "error", message: err, song: returnSong && _err.data ? _err.data.song : null }); }); }), /** * Hides a song * * @param {object} session - the session object automatically added by the websocket * @param {string} songId - the song id of the song that gets hidden * @param {Function} cb - gets called with the result */ hide: isLoginRequired(async function add(session, songId, cb) { SongsModule.runJob("HIDE_SONG", { songId }, this) .then(() => { this.log("SUCCESS", "SONGS_HIDE", `User "${session.userId}" successfully hid song "${songId}".`); return cb({ status: "success", message: "Successfully hid that song" }); }) .catch(async err => { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "SONGS_HIDE", `Hiding song "${songId}" failed for user ${session.userId}. "${err}"`); return cb({ status: "error", message: err }); }); }), /** * Unhides a song * * @param {object} session - the session object automatically added by the websocket * @param {string} songId - the song id of the song that gets hidden * @param {Function} cb - gets called with the result */ unhide: isLoginRequired(async function add(session, songId, cb) { SongsModule.runJob("UNHIDE_SONG", { songId }, this) .then(() => { this.log("SUCCESS", "SONGS_UNHIDE", `User "${session.userId}" successfully unhid song "${songId}".`); return cb({ status: "success", message: "Successfully unhid that song" }); }) .catch(async err => { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "SONGS_UNHIDE", `Unhiding song "${songId}" failed for user ${session.userId}. "${err}"` ); return cb({ status: "error", message: err }); }); }), /** * Verifies a song * * @param session * @param songId - the song id * @param cb */ verify: isAdminRequired(async function add(session, songId, cb) { const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this); async.waterfall( [ next => { SongModel.findOne({ _id: songId }, next); }, (song, next) => { if (!song) return next("This song is not in the database."); return next(null, song); }, (song, next) => { const oldStatus = song.status; song.verifiedBy = session.userId; song.verifiedAt = Date.now(); song.status = "verified"; song.save(err => next(err, song, oldStatus)); }, (song, oldStatus, next) => { song.genres.forEach(genre => { PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre }) .then(() => {}) .catch(() => {}); }); SongsModule.runJob("UPDATE_SONG", { songId: song._id }); next(null, song, oldStatus); } ], async (err, song, oldStatus) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`); return cb({ status: "error", message: err }); } this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`); if (oldStatus === "hidden") CacheModule.runJob("PUB", { channel: "song.removedHiddenSong", value: song._id }); CacheModule.runJob("PUB", { channel: "song.newVerifiedSong", value: song._id }); CacheModule.runJob("PUB", { channel: "song.removedUnverifiedSong", value: song._id }); return cb({ status: "success", message: "Song has been verified successfully." }); } ); // TODO Check if video is in queue and Add the song to the appropriate stations }), /** * Un-verifies a song * * @param session * @param songId - the song id * @param cb */ unverify: isAdminRequired(async function add(session, songId, cb) { const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this); async.waterfall( [ next => { SongModel.findOne({ _id: songId }, next); }, (song, next) => { if (!song) return next("This song is not in the database."); return next(null, song); }, (song, next) => { song.status = "unverified"; song.save(err => { next(err, song); }); }, (song, next) => { song.genres.forEach(genre => { PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre }) .then(() => {}) .catch(() => {}); }); SongsModule.runJob("UPDATE_SONG", { songId }); next(null); } ], async err => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "SONGS_UNVERIFY", `User "${session.userId}" failed to verify song. "${err}"`); return cb({ status: "error", message: err }); } this.log( "SUCCESS", "SONGS_UNVERIFY", `User "${session.userId}" successfully unverified song "${songId}".` ); CacheModule.runJob("PUB", { channel: "song.newUnverifiedSong", value: songId }); CacheModule.runJob("PUB", { channel: "song.removedVerifiedSong", value: songId }); return cb({ status: "success", message: "Song has been unverified successfully." }); } ); // TODO Check if video is in queue and Add the song to the appropriate stations }), /** * Requests a set of songs * * @param {object} session - the session object automatically added by the websocket * @param {string} url - the url of the the YouTube playlist * @param {boolean} musicOnly - whether to only get music from the playlist * @param {Function} cb - gets called with the result */ requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnSongs, cb) { async.waterfall( [ next => { YouTubeModule.runJob( "GET_PLAYLIST", { url, musicOnly }, this ) .then(res => { next(null, res.songs); }) .catch(next); }, (youtubeIds, next) => { let successful = 0; let songs = {}; let failed = 0; let alreadyInDatabase = 0; if (youtubeIds.length === 0) next(); async.eachOfLimit( youtubeIds, 1, (youtubeId, index, next) => { WSModule.runJob( "RUN_ACTION2", { session, namespace: "songs", action: "request", args: [youtubeId, returnSongs] }, this ) .then(res => { if (res.status === "success") successful += 1; else failed += 1; if (res.message === "This song is already in the database.") alreadyInDatabase += 1; if (res.song) songs[index] = res.song; else songs[index] = null; }) .catch(() => { failed += 1; }) .finally(() => { next(); }); }, () => { if (returnSongs) songs = Object.keys(songs) .sort() .map(key => songs[key]); next(null, { successful, failed, alreadyInDatabase, songs }); } ); } ], async (err, response) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "REQUEST_SET", `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"` ); return cb({ status: "error", message: err }); } this.log( "SUCCESS", "REQUEST_SET", `Successfully imported a YouTube playlist to be requested for user "${session.userId}".` ); return cb({ status: "success", message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`, songs: returnSongs ? response.songs : null }); } ); }), /** * Likes a song * * @param session * @param youtubeId - the youtube id * @param cb */ like: isLoginRequired(async function like(session, youtubeId, 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({ youtubeId }, next), (song, next) => { if (!song) return next("No song found with that id."); return next(null, song); }, (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)), (song, user, next) => { if (!user) return next("User does not exist."); return this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "addSongToPlaylist", args: [false, youtubeId, user.likedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") { if (res.message === "That song is already in the playlist") return next("You have already liked this song."); return next("Unable to add song to the 'Liked Songs' playlist."); } return next(null, song, user.dislikedSongsPlaylist); }) .catch(err => next(err)); }, (song, dislikedSongsPlaylist, next) => { this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "removeSongFromPlaylist", args: [youtubeId, dislikedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") return next("Unable to remove song from the 'Disliked Songs' playlist."); return next(null, song); }) .catch(err => next(err)); }, (song, next) => { SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, { 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 ${youtubeId}. "${err}"` ); return cb({ status: "error", message: err }); } SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "song.like", value: JSON.stringify({ youtubeId, userId: session.userId, likes, dislikes }) }); ActivitiesModule.runJob("ADD_ACTIVITY", { userId: session.userId, type: "song__like", payload: { message: `Liked song ${song.title} by ${song.artists.join(", ")}`, youtubeId, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully liked this song." }); } ); }), /** * Dislikes a song * * @param session * @param youtubeId - the youtube id * @param cb */ dislike: isLoginRequired(async function dislike(session, youtubeId, 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({ youtubeId }, next); }, (song, next) => { if (!song) return next("No song found with that id."); return next(null, song); }, (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)), (song, user, next) => { if (!user) return next("User does not exist."); return this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "addSongToPlaylist", args: [false, youtubeId, user.dislikedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") { if (res.message === "That song is already in the playlist") return next("You have already disliked this song."); return next("Unable to add song to the 'Disliked Songs' playlist."); } return next(null, song, user.likedSongsPlaylist); }) .catch(err => next(err)); }, (song, likedSongsPlaylist, next) => { this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "removeSongFromPlaylist", args: [youtubeId, likedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") return next("Unable to remove song from the 'Liked Songs' playlist."); return next(null, song); }) .catch(err => next(err)); }, (song, next) => { SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, { likes, dislikes }) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "SONGS_DISLIKE", `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"` ); return cb({ status: "error", message: err }); } SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "song.dislike", value: JSON.stringify({ youtubeId, userId: session.userId, likes, dislikes }) }); ActivitiesModule.runJob("ADD_ACTIVITY", { userId: session.userId, type: "song__dislike", payload: { message: `Disliked song ${song.title} by ${song.artists.join(", ")}`, youtubeId, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully disliked this song." }); } ); }), /** * Undislikes a song * * @param session * @param youtubeId - the youtube id * @param cb */ undislike: isLoginRequired(async function undislike(session, youtubeId, 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({ youtubeId }, next); }, (song, next) => { if (!song) return next("No song found with that id."); return next(null, song); }, (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)), (song, user, next) => { if (!user) return next("User does not exist."); return this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "removeSongFromPlaylist", args: [youtubeId, user.dislikedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") return next("Unable to remove song from the 'Disliked Songs' playlist."); return next(null, song, user.likedSongsPlaylist); }) .catch(err => next(err)); }, (song, likedSongsPlaylist, next) => { this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "removeSongFromPlaylist", args: [youtubeId, likedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") return next("Unable to remove song from the 'Liked Songs' playlist."); return next(null, song); }) .catch(err => next(err)); }, (song, next) => { SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, { likes, dislikes }) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "SONGS_UNDISLIKE", `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"` ); return cb({ status: "error", message: err }); } SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "song.undislike", value: JSON.stringify({ youtubeId, userId: session.userId, likes, dislikes }) }); ActivitiesModule.runJob("ADD_ACTIVITY", { userId: session.userId, type: "song__undislike", payload: { message: `Removed ${song.title} by ${song.artists.join( ", " )} from your Disliked Songs`, youtubeId, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully undisliked this song." }); } ); }), /** * Unlikes a song * * @param session * @param youtubeId - the youtube id * @param cb */ unlike: isLoginRequired(async function unlike(session, youtubeId, 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({ youtubeId }, next); }, (song, next) => { if (!song) return next("No song found with that id."); return next(null, song); }, (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)), (song, user, next) => { if (!user) return next("User does not exist."); return this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "removeSongFromPlaylist", args: [youtubeId, user.dislikedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") return next("Unable to remove song from the 'Disliked Songs' playlist."); return next(null, song, user.likedSongsPlaylist); }) .catch(err => next(err)); }, (song, likedSongsPlaylist, next) => { this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "removeSongFromPlaylist", args: [youtubeId, likedSongsPlaylist] }, this ) .then(res => { if (res.status === "error") return next("Unable to remove song from the 'Liked Songs' playlist."); return next(null, song); }) .catch(err => next(err)); }, (song, next) => { SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, { likes, dislikes }) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "SONGS_UNLIKE", `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"` ); return cb({ status: "error", message: err }); } SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "song.unlike", value: JSON.stringify({ youtubeId, userId: session.userId, likes, dislikes }) }); ActivitiesModule.runJob("ADD_ACTIVITY", { userId: session.userId, type: "song__unlike", payload: { message: `Removed ${song.title} by ${song.artists.join( ", " )} from your Liked Songs`, youtubeId, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully unliked this song." }); } ); }), /** * Gets song ratings * * @param session * @param songId - the Musare song id * @param cb */ getSongRatings: isLoginRequired(async function getSongRatings(session, songId, cb) { async.waterfall( [ next => { SongsModule.runJob("GET_SONG", { songId }, this) .then(res => next(null, res.song)) .catch(next); }, (song, next) => { next(null, { likes: song.likes, dislikes: song.dislikes }); } ], async (err, ratings) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "SONGS_GET_RATINGS", `User "${session.userId}" failed to get ratings for ${songId}. "${err}"` ); return cb({ status: "error", message: err }); } const { likes, dislikes } = ratings; return cb({ status: "success", data: { likes, dislikes } }); } ); }), /** * Gets user's own song ratings * * @param session * @param youtubeId - the youtube id * @param cb */ getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, youtubeId, 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({ youtubeId }, next), (song, next) => { if (!song) return next("No song found with that id."); 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.youtubeId === youtubeId) 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.youtubeId === youtubeId) ratings.isDisliked = true; }); return next(null, ratings); } ) ], async (err, ratings) => { 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 ${youtubeId}. "${err}"` ); return cb({ status: "error", message: err }); } const { isLiked, isDisliked } = ratings; return cb({ status: "success", data: { youtubeId, liked: isLiked, disliked: isDisliked } }); } ); }) };