import async from "async"; import isLoginRequired from "../hooks/loginRequired"; import { useHasPermission } from "../hooks/hasPermission"; // eslint-disable-next-line 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 MediaModule = moduleManager.modules.media; CacheModule.runJob("SUB", { channel: "ratings.like", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.mediaSource}`, args: [ "event:ratings.liked", { data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:ratings.updated", { data: { mediaSource: data.mediaSource, liked: true, disliked: false } }); }); }); } }); CacheModule.runJob("SUB", { channel: "ratings.dislike", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.mediaSource}`, args: [ "event:ratings.disliked", { data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:ratings.updated", { data: { mediaSource: data.mediaSource, liked: false, disliked: true } }); }); }); } }); CacheModule.runJob("SUB", { channel: "ratings.unlike", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.mediaSource}`, args: [ "event:ratings.unliked", { data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:ratings.updated", { data: { mediaSource: data.mediaSource, liked: false, disliked: false } }); }); }); } }); CacheModule.runJob("SUB", { channel: "ratings.undislike", cb: data => { WSModule.runJob("EMIT_TO_ROOM", { room: `song.${data.mediaSource}`, args: [ "event:ratings.undisliked", { data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes } } ] }); WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => { sockets.forEach(socket => { socket.dispatch("event:ratings.updated", { data: { mediaSource: data.mediaSource, liked: false, disliked: false } }); }); }); } }); export default { /** * Recalculates all ratings * * @param {object} session - the session object automatically added by the websocket * @param cb */ recalculateAllRatings: useHasPermission( "media.recalculateAllRatings", async function recalculateAllRatings(session, cb) { this.keepLongJob(); this.publishProgress({ status: "started", title: "Recalculate all ratings", message: "Recalculating all ratings.", id: this.toString() }); await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this); await CacheModule.runJob( "PUB", { channel: "longJob.added", value: { jobId: this.toString(), userId: session.userId } }, this ); async.waterfall( [ next => { MediaModule.runJob("RECALCULATE_ALL_RATINGS", {}, this) .then(() => { next(); }) .catch(err => { next(err); }); } ], async err => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_RECALCULATE_ALL_RATINGS", `Failed to recalculate all ratings. "${err}"` ); this.publishProgress({ status: "error", message: err }); return cb({ status: "error", message: err }); } this.log("SUCCESS", "MEDIA_RECALCULATE_ALL_RATINGS", `Recalculated all ratings successfully.`); this.publishProgress({ status: "success", message: "Successfully recalculated all ratings." }); return cb({ status: "success", message: "Successfully recalculated all ratings." }); } ); } ), /** * Like * * @param session * @param mediaSource - the media source * @param cb */ like: isLoginRequired(async function like(session, mediaSource, cb) { const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this); async.waterfall( [ next => { MediaModule.runJob( "GET_MEDIA", { mediaSource }, this ) .then(response => { const { song } = response; const { _id, title, artists, thumbnail, duration, verified } = song; next(null, { _id, mediaSource, title, artists, thumbnail, duration, verified }); }) .catch(next); }, (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: [mediaSource, user.dislikedSongsPlaylist] }, this ) .then(() => next(null, song, user.likedSongsPlaylist)) .catch(res => { if (!(res.message && res.message === "That song is not currently in the playlist.")) return next("Unable to remove song from the 'Disliked Songs' playlist."); return next(null, song, user.likedSongsPlaylist); }); }, (song, likedSongsPlaylist, next) => this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "addSongToPlaylist", args: [false, mediaSource, likedSongsPlaylist] }, this ) .then(() => next(null, song)) .catch(res => { if (res.message && 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."); }), (song, next) => { MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, ratings) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_RATINGS_LIKE", `User "${session.userId}" failed to like song ${mediaSource}. "${err}"` ); return cb({ status: "error", message: err }); } const { likes, dislikes } = ratings; if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "ratings.like", value: JSON.stringify({ mediaSource, 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(", ")}`, mediaSource, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully liked this song." }); } ); }), /** * Dislike * * @param session * @param mediaSource - the media source * @param cb */ dislike: isLoginRequired(async function dislike(session, mediaSource, cb) { const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this); async.waterfall( [ next => { MediaModule.runJob( "GET_MEDIA", { mediaSource }, this ) .then(response => { const { song } = response; const { _id, title, artists, thumbnail, duration, verified } = song; next(null, { _id, mediaSource, title, artists, thumbnail, duration, verified }); }) .catch(next); }, (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: [mediaSource, user.likedSongsPlaylist] }, this ) .then(() => next(null, song, user.dislikedSongsPlaylist)) .catch(res => { if (!(res.message && res.message === "That song is not currently in the playlist.")) return next("Unable to remove song from the 'Liked Songs' playlist."); return next(null, song, user.dislikedSongsPlaylist); }); }, (song, dislikedSongsPlaylist, next) => this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "addSongToPlaylist", args: [false, mediaSource, dislikedSongsPlaylist] }, this ) .then(() => next(null, song)) .catch(res => { if (res.message && 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."); }), (song, next) => { MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, ratings) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_RATINGS_DISLIKE", `User "${session.userId}" failed to dislike song ${mediaSource}. "${err}"` ); return cb({ status: "error", message: err }); } const { likes, dislikes } = ratings; if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "ratings.dislike", value: JSON.stringify({ mediaSource, 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(", ")}`, mediaSource, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully disliked this song." }); } ); }), /** * Undislike * * @param session * @param mediaSource - the media source * @param cb */ undislike: isLoginRequired(async function undislike(session, mediaSource, cb) { const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this); async.waterfall( [ next => { MediaModule.runJob( "GET_MEDIA", { mediaSource }, this ) .then(response => { const { song } = response; const { _id, title, artists, thumbnail, duration, verified } = song; next(null, { _id, mediaSource, title, artists, thumbnail, duration, verified }); }) .catch(next); }, (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: [mediaSource, 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: [mediaSource, likedSongsPlaylist] }, this ) .then(() => next(null, song)) .catch(res => { if (!(res.message && res.message === "That song is not currently in the playlist.")) return next("Unable to remove song from the 'Liked Songs' playlist."); return next(null, song); }); }, (song, next) => { MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, ratings) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_RATINGS_UNDISLIKE", `User "${session.userId}" failed to undislike song ${mediaSource}. "${err}"` ); return cb({ status: "error", message: err }); } const { likes, dislikes } = ratings; if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "ratings.undislike", value: JSON.stringify({ mediaSource, 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`, mediaSource, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully undisliked this song." }); } ); }), /** * Unlike * * @param session * @param mediaSource - the media source * @param cb */ unlike: isLoginRequired(async function unlike(session, mediaSource, cb) { const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this); async.waterfall( [ next => { MediaModule.runJob( "GET_MEDIA", { mediaSource }, this ) .then(response => { const { song } = response; const { _id, title, artists, thumbnail, duration, verified } = song; next(null, { _id, mediaSource, title, artists, thumbnail, duration, verified }); }) .catch(next); }, (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: [mediaSource, user.dislikedSongsPlaylist] }, this ) .then(() => next(null, song, user.likedSongsPlaylist)) .catch(res => { if (!(res.message && res.message === "That song is not currently in the playlist.")) return next("Unable to remove song from the 'Disliked Songs' playlist."); return next(null, song, user.likedSongsPlaylist); }); }, (song, likedSongsPlaylist, next) => { this.module .runJob( "RUN_ACTION2", { session, namespace: "playlists", action: "removeSongFromPlaylist", args: [mediaSource, 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) => { MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }) .then(ratings => next(null, song, ratings)) .catch(err => next(err)); } ], async (err, song, ratings) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_RATINGS_UNLIKE", `User "${session.userId}" failed to unlike song ${mediaSource}. "${err}"` ); return cb({ status: "error", message: err }); } const { likes, dislikes } = ratings; if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id }); CacheModule.runJob("PUB", { channel: "ratings.unlike", value: JSON.stringify({ mediaSource, 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`, mediaSource, thumbnail: song.thumbnail } }); return cb({ status: "success", message: "You have successfully unliked this song." }); } ); }), /** * Get ratings * * @param session * @param mediaSource - the media source * @param cb */ async getRatings(session, mediaSource, cb) { async.waterfall( [ next => { MediaModule.runJob("GET_RATINGS", { mediaSource, createMissing: true }, this) .then(res => next(null, res.ratings)) .catch(next); }, (ratings, next) => { next(null, { likes: ratings.likes, dislikes: ratings.dislikes }); } ], async (err, ratings) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_GET_RATINGS", `User "${session.userId}" failed to get ratings for ${mediaSource}. "${err}"` ); return cb({ status: "error", message: err }); } const { likes, dislikes } = ratings; return cb({ status: "success", data: { likes, dislikes } }); } ); }, /** * Gets user's own ratings * * @param session * @param mediaSource - the media source * @param cb */ getOwnRatings: isLoginRequired(async function getOwnRatings(session, mediaSource, cb) { const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this); async.waterfall( [ next => { MediaModule.runJob( "GET_MEDIA", { mediaSource }, this ) .then(() => next()) .catch(next); }, 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.mediaSource === mediaSource) 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.mediaSource === mediaSource) ratings.isDisliked = true; }); return next(null, ratings); } ) ], async (err, ratings) => { if (err) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_GET_OWN_RATINGS", `User "${session.userId}" failed to get ratings for ${mediaSource}. "${err}"` ); return cb({ status: "error", message: err }); } const { isLiked, isDisliked } = ratings; return cb({ status: "success", data: { mediaSource, liked: isLiked, disliked: isDisliked } }); } ); }), /** * Gets importJobs, used in the admin import page by the AdvancedTable component * * @param {object} session - the session object automatically added by the websocket * @param page - the page * @param pageSize - the size per page * @param properties - the properties to return for each news item * @param sort - the sort object * @param queries - the queries array * @param operator - the operator for queries * @param cb */ getImportJobs: useHasPermission( "admin.view.import", async function getImportJobs(session, page, pageSize, properties, sort, queries, operator, cb) { async.waterfall( [ next => { DBModule.runJob( "GET_DATA", { page, pageSize, properties, sort, queries, operator, modelName: "importJob", blacklistedProperties: [], specialProperties: {}, specialQueries: {} }, this ) .then(response => { next(null, response); }) .catch(err => { next(err); }); } ], async (err, response) => { if (err && err !== true) { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "MEDIA_GET_IMPORT_JOBS", `Failed to get import jobs. "${err}"`); return cb({ status: "error", message: err }); } this.log("SUCCESS", "MEDIA_GET_IMPORT_JOBS", `Fetched import jobs successfully.`); return cb({ status: "success", message: "Successfully fetched import jobs.", data: response }); } ); } ), /** * Remove import jobs * * @returns {{status: string, data: object}} */ removeImportJobs: useHasPermission("media.removeImportJobs", function removeImportJobs(session, jobIds, cb) { MediaModule.runJob("REMOVE_IMPORT_JOBS", { jobIds }, this) .then(() => { this.log("SUCCESS", "MEDIA_REMOVE_IMPORT_JOBS", `Removing import jobs was successful.`); return cb({ status: "success", message: "Successfully removed import jobs" }); }) .catch(async err => { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log("ERROR", "MEDIA_REMOVE_IMPORT_JOBS", `Removing import jobs failed. "${err}"`); return cb({ status: "error", message: err }); }); }), /** * Gets an array of media from media sources * * @returns {{status: string, data: object}} */ getMediaFromMediaSources: isLoginRequired(function getMediaFromMediaSources(session, mediaSources, cb) { MediaModule.runJob("GET_MEDIA_FROM_MEDIA_SOURCES", { mediaSources }, this) .then(songMap => { this.log("SUCCESS", "MEDIA_GET_MEDIA_FROM_MEDIA_SOURCES", `GET_MEDIA_FROM_MEDIA_SOURCES successful.`); return cb({ status: "success", message: "GET_MEDIA_FROM_MEDIA_SOURCES success", data: { songMap } }); }) .catch(async err => { err = await UtilsModule.runJob("GET_ERROR", { error: err }, this); this.log( "ERROR", "MEDIA_GET_MEDIA_FROM_MEDIA_SOURCES", `GET_MEDIA_FROM_MEDIA_SOURCES failed. "${err}"` ); return cb({ status: "error", message: err }); }); }) };