瀏覽代碼

Revert "refactor(Songs): verified property as boolean and removed hidden type"

This reverts commit 2e749eff6c7c42a474fc869d70bc1bdd50ff92ce.
Kristian Vos 3 年之前
父節點
當前提交
1e40c95369

+ 2 - 0
.wiki/Configuration.md

@@ -12,6 +12,8 @@ Location: `backend/config/default.json`
 | `serverDomain` | Should be the url where the backend will be accessible from, usually `http://localhost/backend` for docker or `http://localhost:8080` for non-Docker. |
 | `serverPort` | Should be the port where the backend will listen on, should always be `8080` for Docker, and is recommended for non-Docker. |
 | `registrationDisabled` | If set to true, users can't register accounts. |
+| `hideAutomaticallyRequestedSongs` | If `true` any automatically requested songs will be hidden. |
+| `hideAnonymousSongs` | If `true` any anonymously requested songs will be hidden. |
 | `sendDataRequestEmails` | If `true` all admin users will be sent an email if a data request is received. |
 | `apis.youtube.key` | YouTube Data API v3 key, obtained from [here](https://developers.google.com/youtube/v3/getting-started). |
 | `apis.youtube.rateLimit` | Minimum interval between YouTube API requests in milliseconds. |

+ 3 - 1
backend/config/template.json

@@ -7,6 +7,8 @@
 	"serverDomain": "http://localhost/backend",
 	"serverPort": 8080,
 	"registrationDisabled": true,
+	"hideAutomaticallyRequestedSongs": false,
+	"hideAnonymousSongs": false,
 	"sendDataRequestEmails": true,
 	"apis": {
 		"youtube": {
@@ -93,5 +95,5 @@
 			]
 		}
 	},
-	"configVersion": 9
+	"configVersion": 8
 }

+ 1 - 1
backend/index.js

@@ -6,7 +6,7 @@ import fs from "fs";
 
 import package_json from "./package.json";
 
-const REQUIRED_CONFIG_VERSION = 9;
+const REQUIRED_CONFIG_VERSION = 8;
 
 // eslint-disable-next-line
 Array.prototype.remove = function (item) {

+ 2 - 0
backend/logic/actions/apis.js

@@ -184,7 +184,9 @@ export default {
 	 */
 	joinAdminRoom: isAdminRequired((session, page, cb) => {
 		if (
+			page === "unverifiedSongs" ||
 			page === "songs" ||
+			page === "hiddenSongs" ||
 			page === "stations" ||
 			page === "reports" ||
 			page === "news" ||

+ 61 - 5
backend/logic/actions/songs.js

@@ -22,7 +22,13 @@ CacheModule.runJob("SUB", {
 
 		songModel.findOne({ _id: data.songId }, (err, song) => {
 			WSModule.runJob("EMIT_TO_ROOMS", {
-				rooms: ["import-album", "admin.songs", `edit-song.${data.songId}`],
+				rooms: [
+					"import-album",
+					"admin.songs",
+					"admin.unverifiedSongs",
+					"admin.hiddenSongs",
+					`edit-song.${data.songId}`
+				],
 				args: ["event:admin.song.updated", { data: { song, oldStatus: data.oldStatus } }]
 			});
 		});
@@ -546,6 +552,56 @@ export default {
 			});
 	}),
 
+	/**
+	 * 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
 	 *
@@ -567,11 +623,11 @@ export default {
 				},
 
 				(song, next) => {
-					const oldStatus = false;
+					const oldStatus = song.status;
 
 					song.verifiedBy = session.userId;
 					song.verifiedAt = Date.now();
-					song.verified = true;
+					song.status = "verified";
 
 					song.save(err => next(err, song, oldStatus));
 				},
@@ -626,7 +682,7 @@ export default {
 				},
 
 				(song, next) => {
-					song.verified = false;
+					song.status = "unverified";
 					song.save(err => {
 						next(err, song);
 					});
@@ -639,7 +695,7 @@ export default {
 							.catch(() => {});
 					});
 
-					SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: true });
+					SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: "verified" });
 
 					next(null);
 				}

+ 3 - 3
backend/logic/db/index.js

@@ -8,12 +8,12 @@ import CoreClass from "../../core";
 const REQUIRED_DOCUMENT_VERSIONS = {
 	activity: 2,
 	news: 2,
-	playlist: 6,
+	playlist: 5,
 	punishment: 1,
 	queueSong: 1,
 	report: 5,
-	song: 6,
-	station: 7,
+	song: 5,
+	station: 6,
 	user: 3
 };
 

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

@@ -18,5 +18,5 @@ export default {
 	createdFor: { type: String },
 	privacy: { type: String, enum: ["public", "private"], default: "private" },
 	type: { type: String, enum: ["user", "user-liked", "user-disliked", "genre", "station"], required: true },
-	documentVersion: { type: Number, default: 6, required: true }
+	documentVersion: { type: Number, default: 5, required: true }
 };

+ 2 - 2
backend/logic/db/schemas/song.js

@@ -11,9 +11,9 @@ export default {
 	explicit: { type: Boolean },
 	requestedBy: { type: String },
 	requestedAt: { type: Date },
-	verified: { type: Boolean, default: false },
 	verifiedBy: { type: String },
 	verifiedAt: { type: Date },
 	discogs: { type: Object },
-	documentVersion: { type: Number, default: 6, required: true }
+	status: { type: String, required: true, default: "hidden", enum: ["hidden", "unverified", "verified"] },
+	documentVersion: { type: Number, default: 5, required: true }
 };

+ 3 - 3
backend/logic/db/schemas/station.js

@@ -17,7 +17,7 @@ export default {
 		skipVotes: [{ type: String }],
 		requestedBy: { type: String },
 		requestedAt: { type: Date },
-		verified: { type: Boolean }
+		status: { type: String }
 	},
 	currentSongIndex: { type: Number, default: 0, required: true },
 	timePaused: { type: Number, default: 0, required: true },
@@ -36,7 +36,7 @@ export default {
 			thumbnail: { type: String },
 			requestedBy: { type: String },
 			requestedAt: { type: Date },
-			verified: { type: Boolean }
+			status: { type: String }
 		}
 	],
 	owner: { type: String },
@@ -45,5 +45,5 @@ export default {
 	theme: { type: String, enum: ["blue", "purple", "teal", "orange", "red"], default: "blue" },
 	includedPlaylists: [{ type: String }],
 	excludedPlaylists: [{ type: String }],
-	documentVersion: { type: Number, default: 7, required: true }
+	documentVersion: { type: Number, default: 6, required: true }
 };

+ 0 - 162
backend/logic/migration/migrations/migration17.js

@@ -1,162 +0,0 @@
-import async from "async";
-
-/**
- * Migration 17
- *
- * Migration for song status property.
- *
- * @param {object} MigrationModule - the MigrationModule
- * @returns {Promise} - returns promise
- */
-export default async function migrate(MigrationModule) {
-	const songModel = await MigrationModule.runJob("GET_MODEL", { modelName: "song" }, this);
-	const playlistModel = await MigrationModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
-	const stationModel = await MigrationModule.runJob("GET_MODEL", { modelName: "station" }, this);
-
-	return new Promise((resolve, reject) => {
-		async.waterfall(
-			[
-				next => {
-					this.log("INFO", `Migration 7. Finding unverified and hidden songs with document version 5.`);
-					songModel.updateMany(
-						{ documentVersion: 5, status: { $in: ["unverified", "hidden"] } },
-						{ $set: { documentVersion: 6, verified: false }, $unset: { status: "" } },
-						(err, res) => {
-							if (err) next(err);
-							else {
-								this.log(
-									"INFO",
-									`Migration 7 (unverified songs). Matched: ${res.n}, modified: ${res.nModified}, ok: ${res.ok}.`
-								);
-
-								next();
-							}
-						}
-					);
-				},
-
-				next => {
-					this.log("INFO", `Migration 7. Finding verified songs with document version 5.`);
-					songModel.updateMany(
-						{ documentVersion: 5, status: "verified" },
-						{ $set: { documentVersion: 6, verified: true }, $unset: { status: "" } },
-						(err, res) => {
-							if (err) next(err);
-							else {
-								this.log(
-									"INFO",
-									`Migration 7 (verified songs). Matched: ${res.n}, modified: ${res.nModified}, ok: ${res.ok}.`
-								);
-
-								next();
-							}
-						}
-					);
-				},
-
-				next => {
-					this.log("INFO", `Migration 7. Updating playlist songs and queue songs.`);
-					songModel.find({ documentVersion: 6 }, (err, songs) => {
-						if (err) next(err);
-						else {
-							async.eachLimit(
-								songs.map(song => song._doc),
-								1,
-								(song, next) => {
-									const {
-										_id,
-										youtubeId,
-										title,
-										artists,
-										thumbnail,
-										duration,
-										skipDuration,
-										verified
-									} = song;
-									const trimmedSong = {
-										_id,
-										youtubeId,
-										title,
-										artists,
-										thumbnail,
-										duration,
-										skipDuration,
-										verified
-									};
-									async.waterfall(
-										[
-											next => {
-												playlistModel.updateMany(
-													{ "songs._id": song._id, documentVersion: 5 },
-													{ $set: { "songs.$": trimmedSong } },
-													next
-												);
-											},
-
-											(res, next) => {
-												stationModel.updateMany(
-													{ "queue._id": song._id, documentVersion: 6 },
-													{ $set: { "queue.$": trimmedSong } },
-													next
-												);
-											},
-
-											(res, next) => {
-												stationModel.updateMany(
-													{ "currentSong._id": song._id, documentVersion: 6 },
-													{ $set: { currentSong: null } },
-													next
-												);
-											}
-										],
-										err => {
-											next(err);
-										}
-									);
-								},
-								err => {
-									next(err);
-								}
-							);
-						}
-					});
-				},
-
-				next => {
-					playlistModel.updateMany({ documentVersion: 5 }, { $set: { documentVersion: 6 } }, (err, res) => {
-						if (err) next(err);
-						else {
-							this.log(
-								"INFO",
-								`Migration 7 (playlist). Matched: ${res.n}, modified: ${res.nModified}, ok: ${res.ok}.`
-							);
-
-							next();
-						}
-					});
-				},
-
-				next => {
-					stationModel.updateMany({ documentVersion: 6 }, { $set: { documentVersion: 7 } }, (err, res) => {
-						if (err) next(err);
-						else {
-							this.log(
-								"INFO",
-								`Migration 7 (station). Matched: ${res.n}, modified: ${res.nModified}, ok: ${res.ok}.`
-							);
-
-							next();
-						}
-					});
-				}
-			],
-			err => {
-				if (err) {
-					reject(new Error(err));
-				} else {
-					resolve();
-				}
-			}
-		);
-	});
-}

+ 4 - 4
backend/logic/playlists.js

@@ -361,7 +361,7 @@ class _PlaylistsModule extends CoreClass {
 	 */
 	ADD_SONG_TO_PLAYLIST(payload) {
 		return new Promise((resolve, reject) => {
-			const { _id, youtubeId, title, artists, thumbnail, duration, verified } = payload.song;
+			const { _id, youtubeId, title, artists, thumbnail, duration, status } = payload.song;
 			const trimmedSong = {
 				_id,
 				youtubeId,
@@ -369,7 +369,7 @@ class _PlaylistsModule extends CoreClass {
 				artists,
 				thumbnail,
 				duration,
-				verified
+				status
 			};
 
 			PlaylistsModule.playlistModel.updateOne(
@@ -463,7 +463,7 @@ class _PlaylistsModule extends CoreClass {
 
 					(playlistId, _songs, next) => {
 						const songs = _songs.map(song => {
-							const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+							const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
 							return {
 								_id,
 								youtubeId,
@@ -471,7 +471,7 @@ class _PlaylistsModule extends CoreClass {
 								artists,
 								thumbnail,
 								duration,
-								verified
+								status
 							};
 						});
 

+ 118 - 19
backend/logic/songs.js

@@ -1,4 +1,5 @@
 import async from "async";
+import config from "config";
 import mongoose from "mongoose";
 import CoreClass from "../core";
 
@@ -306,8 +307,15 @@ class _SongsModule extends CoreClass {
 								return next(null, song);
 							});
 						} else {
+							const status =
+								(!payload.userId && config.get("hideAnonymousSongs")) ||
+								(payload.automaticallyRequested && config.get("hideAutomaticallyRequestedSongs"))
+									? "hidden"
+									: "unverified";
+
 							const song = new SongsModule.SongModel({
 								...youtubeSong,
+								status,
 								requestedBy: payload.userId,
 								requestedAt: Date.now()
 							});
@@ -390,7 +398,7 @@ class _SongsModule extends CoreClass {
 					},
 
 					(song, next) => {
-						const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+						const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
 						const trimmedSong = {
 							_id,
 							youtubeId,
@@ -398,7 +406,7 @@ class _SongsModule extends CoreClass {
 							artists,
 							thumbnail,
 							duration,
-							verified
+							status
 						};
 						this.log("INFO", `Going to update playlists now for song ${_id}`);
 						DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this)
@@ -446,7 +454,7 @@ class _SongsModule extends CoreClass {
 					},
 
 					(song, next) => {
-						const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+						const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
 						this.log("INFO", `Going to update stations now for song ${_id}`);
 						DBModule.runJob("GET_MODEL", { modelName: "station" }, this)
 							.then(stationModel => {
@@ -459,7 +467,7 @@ class _SongsModule extends CoreClass {
 											"queue.$.artists": artists,
 											"queue.$.thumbnail": thumbnail,
 											"queue.$.duration": duration,
-											"queue.$.verified": verified
+											"queue.$.status": status
 										}
 									},
 									err => {
@@ -661,6 +669,7 @@ class _SongsModule extends CoreClass {
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.query - the query
+	 * @param {string} payload.includeHidden - include hidden songs
 	 * @param {string} payload.includeUnverified - include unverified songs
 	 * @param {string} payload.includeVerified - include verified songs
 	 * @param {string} payload.trimmed - include trimmed songs
@@ -672,10 +681,11 @@ class _SongsModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						const isVerified = [];
-						if (payload.includeUnverified) isVerified.push(false);
-						if (payload.includeVerified) isVerified.push(true);
-						if (isVerified.length === 0) return next("No verified status has been included.");
+						const statuses = [];
+						if (payload.includeHidden) statuses.push("hidden");
+						if (payload.includeUnverified) statuses.push("unverified");
+						if (payload.includeVerified) statuses.push("verified");
+						if (statuses.length === 0) return next("No statuses have been included.");
 
 						let { query } = payload;
 
@@ -687,11 +697,11 @@ class _SongsModule extends CoreClass {
 						const filterArray = [
 							{
 								title: new RegExp(`${query}`, "i"),
-								verified: { $in: isVerified }
+								status: { $in: statuses }
 							},
 							{
 								artists: new RegExp(`${query}`, "i"),
-								verified: { $in: isVerified }
+								status: { $in: statuses }
 							}
 						];
 
@@ -730,7 +740,7 @@ class _SongsModule extends CoreClass {
 						else if (payload.trimmed) {
 							next(null, {
 								songs: data.songs.map(song => {
-									const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+									const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
 									return {
 										_id,
 										youtubeId,
@@ -738,7 +748,7 @@ class _SongsModule extends CoreClass {
 										artists,
 										thumbnail,
 										duration,
-										verified
+										status
 									};
 								}),
 								...data
@@ -859,7 +869,7 @@ class _SongsModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						SongsModule.SongModel.find({ verified: true }, { genres: 1, _id: false }, next);
+						SongsModule.SongModel.find({ status: "verified" }, { genres: 1, _id: false }, next);
 					},
 
 					(songs, next) => {
@@ -898,7 +908,7 @@ class _SongsModule extends CoreClass {
 					next => {
 						SongsModule.SongModel.find(
 							{
-								verified: true,
+								status: "verified",
 								genres: { $regex: new RegExp(`^${payload.genre.toLowerCase()}$`, "i") }
 							},
 							next
@@ -989,6 +999,9 @@ class _SongsModule extends CoreClass {
 						if (song) return next("This song is already in the database.", song);
 						// TODO Add err object as first param of callback
 
+						const requestedBy = user.preferences.anonymousSongRequests ? null : userId;
+						const status = !requestedBy && config.get("hideAnonymousSongs") ? "hidden" : "unverified";
+
 						return YouTubeModule.runJob("GET_SONG", { youtubeId }, this)
 							.then(response => {
 								const { song } = response;
@@ -998,7 +1011,7 @@ class _SongsModule extends CoreClass {
 								song.explicit = false;
 								song.requestedBy = user.preferences.anonymousSongRequests ? null : userId;
 								song.requestedAt = requestedAt;
-								song.verified = false;
+								song.status = status;
 								next(null, song);
 							})
 							.catch(next);
@@ -1031,7 +1044,7 @@ class _SongsModule extends CoreClass {
 				async (err, song) => {
 					if (err && err !== "This song is already in the database.") return reject(err);
 
-					const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+					const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
 					const trimmedSong = {
 						_id,
 						youtubeId,
@@ -1039,7 +1052,7 @@ class _SongsModule extends CoreClass {
 						artists,
 						thumbnail,
 						duration,
-						verified
+						status
 					};
 
 					if (err && err === "This song is already in the database.")
@@ -1053,6 +1066,92 @@ class _SongsModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Hides a song
+	 *
+	 * @param {object} payload - The payload
+	 * @param {string} payload.songId - The song id of the song
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	HIDE_SONG(payload) {
+		return new Promise((resolve, reject) => {
+			const { songId } = payload;
+
+			async.waterfall(
+				[
+					next => {
+						SongsModule.SongModel.findOne({ _id: songId }, next);
+					},
+
+					// Get YouTube data from id
+					(song, next) => {
+						if (!song) return next("This song does not exist.");
+						if (song.status === "hidden") return next("This song is already hidden.");
+						// TODO Add err object as first param of callback
+						return next(null, song.status);
+					},
+
+					(oldStatus, next) => {
+						SongsModule.SongModel.updateOne({ _id: songId }, { status: "hidden" }, res =>
+							next(null, res, oldStatus)
+						);
+					},
+
+					(res, oldStatus, next) => {
+						SongsModule.runJob("UPDATE_SONG", { songId, oldStatus });
+						next();
+					}
+				],
+				async err => {
+					if (err) reject(err);
+					resolve();
+				}
+			);
+		});
+	}
+
+	/**
+	 * Unhides a song
+	 *
+	 * @param {object} payload - The payload
+	 * @param {string} payload.songId - The song id of the song
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	UNHIDE_SONG(payload) {
+		return new Promise((resolve, reject) => {
+			const { songId } = payload;
+
+			async.waterfall(
+				[
+					next => {
+						SongsModule.SongModel.findOne({ _id: songId }, next);
+					},
+
+					// Get YouTube data from id
+					(song, next) => {
+						if (!song) return next("This song does not exist.");
+						if (song.status !== "hidden") return next("This song is not hidden.");
+						// TODO Add err object as first param of callback
+						return next();
+					},
+
+					next => {
+						SongsModule.SongModel.updateOne({ _id: songId }, { status: "unverified" }, next);
+					},
+
+					(res, next) => {
+						SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: "hidden" });
+						next();
+					}
+				],
+				async err => {
+					if (err) reject(err);
+					resolve();
+				}
+			);
+		});
+	}
+
 	// runjob songs REQUEST_ORPHANED_PLAYLIST_SONGS {}
 
 	/**
@@ -1099,7 +1198,7 @@ class _SongsModule extends CoreClass {
 										},
 
 										(song, next) => {
-											const { _id, title, artists, thumbnail, duration, verified } = song;
+											const { _id, title, artists, thumbnail, duration, status } = song;
 											const trimmedSong = {
 												_id,
 												youtubeId,
@@ -1107,7 +1206,7 @@ class _SongsModule extends CoreClass {
 												artists,
 												thumbnail,
 												duration,
-												verified
+												status
 											};
 											playlistModel.updateMany(
 												{ "songs.youtubeId": song.youtubeId },

+ 3 - 3
backend/logic/stations.js

@@ -538,7 +538,7 @@ class _StationsModule extends CoreClass {
 								"skipDuration",
 								"artists",
 								"thumbnail",
-								"verified"
+								"status"
 							]
 						})
 							.then(response => {
@@ -637,7 +637,7 @@ class _StationsModule extends CoreClass {
 											requestedBy: queueSong.requestedBy,
 											likes: song.likes,
 											dislikes: song.dislikes,
-											verified: song.verified
+											status: song.status
 										};
 
 										return next(null, newSong);
@@ -837,7 +837,7 @@ class _StationsModule extends CoreClass {
 								thumbnail: song.thumbnail,
 								requestedAt: song.requestedAt,
 								requestedBy: song.requestedBy,
-								verified: song.verified
+								status: song.status
 							};
 						}
 

+ 1 - 1
frontend/src/components/SongItem.vue

@@ -21,7 +21,7 @@
 						{{ song.title }}
 					</h4>
 					<i
-						v-if="song.verified"
+						v-if="song.status === 'verified'"
 						class="material-icons verified-song"
 						content="Verified Song"
 						v-tippy="{ theme: 'info' }"

+ 35 - 3
frontend/src/components/modals/EditSong/index.vue

@@ -458,7 +458,7 @@
 
 					<div class="right">
 						<button
-							v-if="!song.verified"
+							v-if="song.status !== 'verified'"
 							class="button is-success"
 							@click="verify(song._id)"
 							content="Verify Song"
@@ -467,7 +467,7 @@
 							<i class="material-icons">check_circle</i>
 						</button>
 						<confirm
-							v-if="song.verified"
+							v-if="song.status === 'verified'"
 							placement="left"
 							@confirm="unverify(song._id)"
 						>
@@ -479,6 +479,28 @@
 								<i class="material-icons">cancel</i>
 							</button>
 						</confirm>
+						<confirm
+							v-if="song.status !== 'hidden'"
+							placement="left"
+							@confirm="hide(song._id)"
+						>
+							<button
+								class="button is-danger"
+								content="Hide Song"
+								v-tippy
+							>
+								<i class="material-icons">visibility_off</i>
+							</button>
+						</confirm>
+						<button
+							v-if="song.status === 'hidden'"
+							class="button is-success"
+							@click="unhide(song._id)"
+							content="Unhide Song"
+							v-tippy
+						>
+							<i class="material-icons">visibility</i>
+						</button>
 						<!-- <confirm placement="left" @confirm="remove(song._id)">
 						<button
 							class="button is-danger"
@@ -654,7 +676,7 @@ export default {
 			"event:admin.song.updated",
 			res => {
 				if (res.data.song._id === this.song._id)
-					this.song.verified = res.data.song.verified;
+					this.song.status = res.data.song.status;
 			},
 			{ modal: "editSong" }
 		);
@@ -1511,6 +1533,16 @@ export default {
 				new Toast(res.message);
 			});
 		},
+		hide(id) {
+			this.socket.dispatch("songs.hide", id, res => {
+				new Toast(res.message);
+			});
+		},
+		unhide(id) {
+			this.socket.dispatch("songs.unhide", id, res => {
+				new Toast(res.message);
+			});
+		},
 		// remove(id) {
 		// 	this.socket.dispatch("songs.remove", id, res => {
 		// 		new Toast(res.message);

+ 3 - 1
frontend/src/components/modals/ImportAlbum.vue

@@ -508,7 +508,9 @@ export default {
 				true,
 				res => {
 					this.isImportingPlaylist = false;
-					const songs = res.songs.filter(song => !song.verified);
+					const songs = res.songs.filter(
+						song => song.status !== "verified"
+					);
 					const songsAlreadyVerified =
 						res.songs.length - songs.length;
 					this.setPlaylistSongs(songs);

+ 36 - 2
frontend/src/pages/Admin/tabs/Songs.vue

@@ -194,7 +194,7 @@
 									<i class="material-icons">edit</i>
 								</button>
 								<button
-									v-if="!song.verified"
+									v-if="song.status !== 'verified'"
 									class="button is-success"
 									@click="verify(song._id)"
 									content="Verify Song"
@@ -203,7 +203,7 @@
 									<i class="material-icons">check_circle</i>
 								</button>
 								<confirm
-									v-if="song.verified"
+									v-if="song.status === 'verified'"
 									placement="left"
 									@confirm="unverify(song._id)"
 								>
@@ -215,6 +215,30 @@
 										<i class="material-icons">cancel</i>
 									</button>
 								</confirm>
+								<confirm
+									v-if="song.status !== 'hidden'"
+									placement="left"
+									@confirm="hide(song._id)"
+								>
+									<button
+										class="button is-danger"
+										content="Hide Song"
+										v-tippy
+									>
+										<i class="material-icons"
+											>visibility_off</i
+										>
+									</button>
+								</confirm>
+								<button
+									v-if="song.status === 'hidden'"
+									class="button is-success"
+									@click="unhide(song._id)"
+									content="Unhide Song"
+									v-tippy
+								>
+									<i class="material-icons">visibility</i>
+								</button>
 							</div>
 						</td>
 					</tr>
@@ -513,6 +537,16 @@ export default {
 				new Toast(res.message);
 			});
 		},
+		hide(id) {
+			this.socket.dispatch("songs.hide", id, res => {
+				new Toast(res.message);
+			});
+		},
+		unhide(id) {
+			this.socket.dispatch("songs.unhide", id, res => {
+				new Toast(res.message);
+			});
+		},
 		updateAllSongs() {
 			new Toast("Updating all songs, this could take a very long time.");
 			this.socket.dispatch("songs.updateAll", res => {