Browse Source

Merge branch 'polishing' of github.com:Musare/MusareNode into polishing

Jonathan 3 years ago
parent
commit
23d6821177

+ 10 - 3
backend/core.js

@@ -244,6 +244,13 @@ class Job {
 		this.responseType = responseType;
 	}
 
+	/**
+	 * Removes child jobs to prevent memory leak
+	 */
+	cleanup() {
+		this.childJobs = this.childJobs.map(() => null);
+	}
+
 	/**
 	 * Logs to the module of the job
 	 *
@@ -306,7 +313,6 @@ export default class CoreClass {
 		this.concurrency = options && options.concurrency ? options.concurrency : 10;
 		this.jobQueue = new Queue((job, options) => this._runJob(job, options), this.concurrency);
 		this.jobQueue.pause();
-		this.runningJobs = [];
 		this.priorities = options && options.priorities ? options.priorities : {};
 		this.stage = 0;
 		this.jobStatistics = {};
@@ -554,7 +560,7 @@ export default class CoreClass {
 
 			const previousStatus = job.status;
 			job.setStatus("RUNNING");
-			this.runningJobs.push(job);
+			this.moduleManager.jobManager.addJob(job);
 
 			if (previousStatus === "QUEUED") {
 				if (!options.isQuiet) this.log("INFO", `Job ${job.name} (${job.toString()}) is queued, so calling it`);
@@ -606,7 +612,8 @@ export default class CoreClass {
 						const executionTime = endTime - startTime;
 						this.jobStatistics[job.name].total += 1;
 						this.jobStatistics[job.name].averageTiming.update(executionTime);
-						this.runningJobs.splice(this.runningJobs.indexOf(job), 1);
+						this.moduleManager.jobManager.removeJob(job);
+						job.cleanup();
 
 						if (!job.parentJob) {
 							if (job.responseType === "RESOLVE") {

+ 35 - 19
backend/index.js

@@ -224,11 +224,36 @@ if (config.debug && config.debug.traceUnhandledPromises === true) {
 // 	}
 // }
 
+class JobManager {
+	constructor() {
+		this.runningJobs = {};
+	}
+
+	addJob(job) {
+		if (!this.runningJobs[job.module.name]) this.runningJobs[job.module.name] = {};
+		this.runningJobs[job.module.name][job.toString()] = job;
+	}
+
+	removeJob(job) {
+		if (!this.runningJobs[job.module.name]) this.runningJobs[job.module.name] = {};
+		delete this.runningJobs[job.module.name][job.toString()];
+	}
+
+	getJob(uuid) {
+		let job = null;
+		Object.keys(this.runningJobs).forEach(moduleName => {
+			if (this.runningJobs[moduleName][uuid]) job = this.runningJobs[moduleName][uuid];
+		});
+		return job;
+	}
+}
+
 class ModuleManager {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		this.modules = {};
 		this.modulesNotInitialized = [];
+		this.jobManager = new JobManager();
 		this.i = 0;
 		this.lockdown = false;
 		this.fancyConsole = fancyConsole;
@@ -318,8 +343,7 @@ class ModuleManager {
 
 			this.log(
 				"INFO",
-				`Initialized: ${Object.keys(this.modules).length - this.modulesNotInitialized.length}/${
-					Object.keys(this.modules).length
+				`Initialized: ${Object.keys(this.modules).length - this.modulesNotInitialized.length}/${Object.keys(this.modules).length
 				}.`
 			);
 
@@ -404,10 +428,12 @@ moduleManager.initialize();
  */
 function printJob(job, layer) {
 	const tabs = Array(layer).join("\t");
-	console.log(`${tabs}${job.name} (${job.toString()}) ${job.status}`);
-	job.childJobs.forEach(childJob => {
-		printJob(childJob, layer + 1);
-	});
+	if (job) {
+		console.log(`${tabs}${job.name} (${job.toString()}) ${job.status}`);
+		job.childJobs.forEach(childJob => {
+			printJob(childJob, layer + 1);
+		});
+	} else console.log(`${tabs}JOB WAS REMOVED`);
 }
 
 /**
@@ -439,8 +465,7 @@ process.stdin.on("data", data => {
 			console.log(
 				`${moduleName.toUpperCase()}${Array(tabsNeeded).join(
 					"\t"
-				)}${module.getStatus()}. Jobs in queue: ${module.jobQueue.lengthQueue()}. Jobs in progress: ${module.jobQueue.lengthRunning()}. Jobs paused: ${module.jobQueue.lengthPaused()} Concurrency: ${
-					module.jobQueue.concurrency
+				)}${module.getStatus()}. Jobs in queue: ${module.jobQueue.lengthQueue()}. Jobs in progress: ${module.jobQueue.lengthRunning()}. Jobs paused: ${module.jobQueue.lengthPaused()} Concurrency: ${module.jobQueue.concurrency
 				}. Stage: ${module.getStage()}`
 			);
 		});
@@ -476,17 +501,8 @@ process.stdin.on("data", data => {
 		const parts = command.split(" ");
 
 		const uuid = parts[1];
-		let jobFound = null;
+		let jobFound = moduleManager.jobManager.getJob(uuid);
 
-		Object.keys(moduleManager.modules).forEach(moduleName => {
-			const module = moduleManager.modules[moduleName];
-			const task1 = module.jobQueue.runningTasks.find(task => task.job.uniqueId === uuid);
-			const task2 = module.jobQueue.queue.find(task => task.job.uniqueId === uuid);
-			const task3 = module.jobQueue.pausedTasks.find(task => task.job.uniqueId === uuid);
-			if (task1) jobFound = task1.job;
-			if (task2) jobFound = task2.job;
-			if (task3) jobFound = task3.job;
-		});
 
 		if (jobFound) {
 			let topParent = jobFound;
@@ -500,7 +516,7 @@ process.stdin.on("data", data => {
 			);
 			console.log(jobFound);
 			printJob(topParent, 1);
-		} else console.log("Could not find job in any running, queued or paused lists in any module.");
+		} else console.log("Could not find job in job manager.");
 	}
 	if (command.startsWith("runjob")) {
 		const parts = command.split(" ");

+ 74 - 74
backend/logic/actions/songs.js

@@ -332,7 +332,7 @@ export default {
 		async.waterfall(
 			[
 				next => {
-					SongsModule.runJob("UPDATE_ALL_SONGS", {})
+					SongsModule.runJob("UPDATE_ALL_SONGS", {}, this)
 						.then(() => {
 							next();
 						})
@@ -499,8 +499,8 @@ export default {
 								.filter((value, index, self) => self.indexOf(value) === index)
 								.forEach(genre => {
 									PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
-										.then(() => {})
-										.catch(() => {});
+										.then(() => { })
+										.catch(() => { });
 								});
 
 							next(null, song);
@@ -545,80 +545,80 @@ export default {
 		);
 	}),
 
-	/**
-	 * 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);
-				},
+	// /**
+	//  * 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);
-				},
+	// 			(_song, next) => {
+	// 				song = _song;
+	// 				songModel.deleteOne({ _id: songId }, next);
+	// 			},
 
-				(res, next) => {
-					// TODO Check if res gets returned from above
-					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);
+	// 			(res, next) => {
+	// 				// TODO Check if res gets returned from above
+	// 				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}"`);
+	// 				this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
 
-					return cb({ status: "error", message: err });
-				}
+	// 				return cb({ status: "error", message: err });
+	// 			}
 
-				this.log("SUCCESS", "SONGS_REMOVE", `Successfully removed song "${songId}".`);
+	// 			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
-					});
-				}
+	// 			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"
-				});
-			}
-		);
-	}),
+	// 			return cb({
+	// 				status: "success",
+	// 				message: "Song has been successfully removed"
+	// 			});
+	// 		}
+	// 	);
+	// }),
 
 	/**
 	 * Searches through official songs
@@ -776,8 +776,8 @@ export default {
 				(song, next) => {
 					song.genres.forEach(genre => {
 						PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
-							.then(() => {})
-							.catch(() => {});
+							.then(() => { })
+							.catch(() => { });
 					});
 
 					SongsModule.runJob("UPDATE_SONG", { songId: song._id });
@@ -844,8 +844,8 @@ export default {
 				(song, next) => {
 					song.genres.forEach(genre => {
 						PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
-							.then(() => {})
-							.catch(() => {});
+							.then(() => { })
+							.catch(() => { });
 					});
 
 					SongsModule.runJob("UPDATE_SONG", { songId });

+ 36 - 5
backend/logic/playlists.js

@@ -152,6 +152,29 @@ class _PlaylistsModule extends CoreClass {
 		});
 	}
 
+	// /**
+	//  * Returns a list of playlists that include a specific song
+	//  *
+	//  * @param {object} payload - object that contains the payload
+	//  * @param {string} payload.songId - the song id
+	//  * @param {string} payload.includeSongs - include the songs
+	//  * @returns {Promise} - returns promise (reject, resolve)
+	//  */
+	// GET_PLAYLISTS_WITH_SONG(payload) {
+	// 	return new Promise((resolve, reject) => {
+	// 		async.waterfall([
+	// 			next => {
+	// 				const includeObject = payload.includeSongs ? null : { songs: false };
+	// 				PlaylistsModule.playlistModel.find({ "songs._id": payload.songId }, includeObject, next);
+	// 			},
+
+	// 			(playlists, next) => {
+	// 				console.log(playlists);
+	// 			}
+	// 		]);
+	// 	});
+	// }
+
 	/**
 	 * Creates a playlist that contains all songs of a specific genre
 	 *
@@ -531,14 +554,22 @@ class _PlaylistsModule extends CoreClass {
 					// },
 
 					(playlistId, next) => {
-						StationsModule.runJob("GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST", { playlistId })
+						StationsModule.runJob("GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST", { playlistId }, this)
 							.then(response => {
-								response.stationIds.forEach(stationId => {
-									PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }).then().catch();
+								async.eachLimit(response.stationIds, 1, (stationId, next) => {
+									PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this).then(() => {
+										next();
+									}).catch(err => {
+										next(err);
+									});
+								}, err => {
+									if (err) next(err);
+									else next();
 								});
 							})
-							.catch();
-						next();
+							.catch(err => {
+								next(err);
+							});
 					}
 				],
 				err => {

+ 155 - 51
backend/logic/songs.js

@@ -203,7 +203,7 @@ class _SongsModule extends CoreClass {
 						} else {
 							const status =
 								(!payload.userId && config.get("hideAnonymousSongs")) ||
-								(payload.automaticallyRequested && config.get("hideAutomaticallyRequestedSongs"))
+									(payload.automaticallyRequested && config.get("hideAutomaticallyRequestedSongs"))
 									? "hidden"
 									: "unverified";
 
@@ -291,7 +291,6 @@ class _SongsModule extends CoreClass {
 					},
 
 					(song, next) => {
-						next(null, song);
 						const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
 						const trimmedSong = {
 							_id,
@@ -302,25 +301,75 @@ class _SongsModule extends CoreClass {
 							duration,
 							status
 						};
-						this.log("INFO", `Going to update playlists and stations now for song ${_id}`);
-						DBModule.runJob("GET_MODEL", { modelName: "playlist" }).then(playlistModel => {
+						this.log("INFO", `Going to update playlists now for song ${_id}`);
+						DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this).then(playlistModel => {
 							playlistModel.updateMany(
 								{ "songs._id": song._id },
 								{ $set: { "songs.$": trimmedSong } },
 								err => {
-									if (err) this.log("ERROR", err);
+									if (err) next(err);
 									else
 										playlistModel.find({ "songs._id": song._id }, (err, playlists) => {
-											playlists.forEach(playlist => {
-												PlaylistsModule.runJob("UPDATE_PLAYLIST", {
-													playlistId: playlist._id
+											if (err) next(err);
+											else {
+												async.eachLimit(playlists, 1, (playlist, next) => {
+													PlaylistsModule.runJob("UPDATE_PLAYLIST", {
+														playlistId: playlist._id
+													}, this).then(() => {
+														next();
+													}).catch(err => {
+														next(err);
+													});
+												}, err => {
+													if (err) next(err);
+													else next(null, song)
 												});
-											});
+											}
+											// playlists.forEach(playlist => {
+											// 	PlaylistsModule.runJob("UPDATE_PLAYLIST", {
+											// 		playlistId: playlist._id
+											// 	});
+											// });
 										});
 								}
 							);
+						}).catch(err => {
+							next(err);
 						});
-						DBModule.runJob("GET_MODEL", { modelName: "station" }).then(stationModel => {
+					},
+
+					(song, next) => {
+						// next(null, song);
+						const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
+						// const trimmedSong = {
+						// 	_id,
+						// 	youtubeId,
+						// 	title,
+						// 	artists,
+						// 	thumbnail,
+						// 	duration,
+						// 	status
+						// };
+						// this.log("INFO", `Going to update playlists and stations now for song ${_id}`);
+						// DBModule.runJob("GET_MODEL", { modelName: "playlist" }).then(playlistModel => {
+						// 	playlistModel.updateMany(
+						// 		{ "songs._id": song._id },
+						// 		{ $set: { "songs.$": trimmedSong } },
+						// 		err => {
+						// 			if (err) this.log("ERROR", err);
+						// 			else
+						// 				playlistModel.find({ "songs._id": song._id }, (err, playlists) => {
+						// 					playlists.forEach(playlist => {
+						// 						PlaylistsModule.runJob("UPDATE_PLAYLIST", {
+						// 							playlistId: playlist._id
+						// 						});
+						// 					});
+						// 				});
+						// 		}
+						// 	);
+						// });
+						this.log("INFO", `Going to update stations now for song ${_id}`);
+						DBModule.runJob("GET_MODEL", { modelName: "station" }, this).then(stationModel => {
 							stationModel.updateMany(
 								{ "queue._id": song._id },
 								{
@@ -337,12 +386,24 @@ class _SongsModule extends CoreClass {
 									if (err) this.log("ERROR", err);
 									else
 										stationModel.find({ "queue._id": song._id }, (err, stations) => {
-											stations.forEach(station => {
-												StationsModule.runJob("UPDATE_STATION", { stationId: station._id });
-											});
+											if (err) next(err);
+											else {
+												async.eachLimit(stations, 1, (station, next) => {
+													StationsModule.runJob("UPDATE_STATION", { stationId: station._id }, this).then(() => {
+														next();
+													}).catch(err => {
+														next(err);
+													});
+												}, err => {
+													if (err) next(err);
+													else next(null, song);
+												});
+											}
 										});
 								}
 							);
+						}).catch(err => {
+							next(err);
 						});
 					},
 
@@ -381,7 +442,6 @@ class _SongsModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						return next("Currently disabled since it's broken due to the backend memory leak issue.");
 						SongsModule.SongModel.find({}, next);
 					},
 
@@ -390,11 +450,11 @@ class _SongsModule extends CoreClass {
 						const { length } = songs;
 						async.eachLimit(
 							songs,
-							10,
+							2,
 							(song, next) => {
 								index += 1;
 								console.log(`Updating song #${index} out of ${length}: ${song._id}`);
-								SongsModule.runJob("UPDATE_SONG", { songId: song._id }, this, 9)
+								SongsModule.runJob("UPDATE_SONG", { songId: song._id }, this)
 									.then(() => {
 										next();
 									})
@@ -416,41 +476,85 @@ class _SongsModule extends CoreClass {
 		);
 	}
 
-	/**
-	 * Deletes song from id from Mongo and cache
-	 *
-	 * @param {object} payload - returns an object containing the payload
-	 * @param {string} payload.youtubeId - the youtube id of the song we are trying to delete
-	 * @returns {Promise} - returns a promise (resolve, reject)
-	 */
-	DELETE_SONG(payload) {
-		return new Promise((resolve, reject) =>
-			async.waterfall(
-				[
-					next => {
-						SongsModule.SongModel.deleteOne({ youtubeId: payload.youtubeId }, next);
-					},
-
-					next => {
-						CacheModule.runJob(
-							"HDEL",
-							{
-								table: "songs",
-								key: payload.youtubeId
-							},
-							this
-						)
-							.then(() => next())
-							.catch(next);
-					}
-				],
-				err => {
-					if (err && err !== true) return reject(new Error(err));
-					return resolve();
-				}
-			)
-		);
-	}
+	// /**
+	//  * Deletes song from id from Mongo and cache
+	//  *
+	//  * @param {object} payload - returns an object containing the payload
+	//  * @param {string} payload.songId - the song id of the song we are trying to delete
+	//  * @returns {Promise} - returns a promise (resolve, reject)
+	//  */
+	// DELETE_SONG(payload) {
+	// 	return new Promise((resolve, reject) =>
+	// 		async.waterfall(
+	// 			[
+	// 				next => {
+	// 					SongsModule.SongModel.deleteOne({ _id: payload.songId }, next);
+	// 				},
+
+	// 				next => {
+	// 					CacheModule.runJob(
+	// 						"HDEL",
+	// 						{
+	// 							table: "songs",
+	// 							key: payload.songId
+	// 						},
+	// 						this
+	// 					)
+	// 						.then(() => next())
+	// 						.catch(next);
+	// 				},
+
+	// 				next => {
+	// 					this.log("INFO", `Going to update playlists and stations now for deleted song ${payload.songId}`);
+	// 					DBModule.runJob("GET_MODEL", { modelName: "playlist" }).then(playlistModel => {
+	// 						playlistModel.find({ "songs._id": song._id }, (err, playlists) => {
+	// 							if (err) this.log("ERROR", err);
+	// 							else {
+	// 								playlistModel.updateMany(
+	// 									{ "songs._id": payload.songId },
+	// 									{ $pull: { "songs.$._id": payload.songId} },
+	// 									err => {
+	// 										if (err) this.log("ERROR", err);
+	// 										else {
+	// 											playlists.forEach(playlist => {
+	// 												PlaylistsModule.runJob("UPDATE_PLAYLIST", {
+	// 													playlistId: playlist._id
+	// 												});
+	// 											});
+	// 										}
+	// 									}
+	// 								);
+
+	// 							}
+	// 						});
+	// 					});
+	// 					DBModule.runJob("GET_MODEL", { modelName: "station" }).then(stationModel => {
+	// 						stationModel.find({ "queue._id": payload.songId }, (err, stations) => {
+	// 							stationModel.updateMany(
+	// 								{ "queue._id": payload.songId },
+	// 								{
+	// 									$pull: { "queue._id":  }
+	// 								},
+	// 								err => {
+	// 									if (err) this.log("ERROR", err);
+	// 									else {
+	// 										stations.forEach(station => {
+	// 											StationsModule.runJob("UPDATE_STATION", { stationId: station._id });
+	// 										});
+	// 									}
+	// 								}
+	// 							);
+	// 						});
+	// 					});
+	// 				}
+	// 			],
+	// 			err => {
+	// 				if (err && err !== true) return reject(new Error(err));
+	// 				return resolve();
+	// 			}
+	// 		)
+	// 	);
+	// }
 
 	/**
 	 * Searches through songs

+ 2 - 1
frontend/dist/config/template.json

@@ -7,6 +7,7 @@
 	"websocketsDomain": "ws://localhost/backend/ws",
 	"frontendDomain": "http://localhost",
 	"frontendPort": "81",
+	"mode": "development",
 	"cookie": {
 		"domain": "localhost",
 		"secure": false,
@@ -22,5 +23,5 @@
 		"accountRemoval": "Your account will be deactivated instantly and your data will shortly be deleted by an admin."
 	},
 	"skipConfigVersionCheck": false,
-	"configVersion": 3
+	"configVersion": 4
 }

+ 26 - 12
frontend/src/components/modals/EditSong.vue

@@ -15,8 +15,8 @@
 								<div class="player-footer-left">
 									<i
 										class="material-icons player-play-pause"
-										@click="settings('play')"
-										@keyup.enter="settings('play')"
+										@click="play()"
+										@keyup.enter="play()"
 										tabindex="0"
 										v-if="video.paused"
 										>play_arrow</i
@@ -523,7 +523,7 @@
 					>
 						<i class="material-icons">visibility</i>
 					</button>
-					<confirm placement="left" @confirm="remove(song._id)">
+					<!-- <confirm placement="left" @confirm="remove(song._id)">
 						<button
 							class="button is-danger"
 							content="Remove Song"
@@ -531,7 +531,7 @@
 						>
 							<i class="material-icons">delete</i>
 						</button>
-					</confirm>
+					</confirm> -->
 				</div>
 			</div>
 		</modal>
@@ -650,10 +650,10 @@ export default {
 	},
 	watch: {
 		/* eslint-disable */
-		"song.duration": function () {
+		"song.duration": function() {
 			this.drawCanvas();
 		},
-		"song.skipDuration": function () {
+		"song.skipDuration": function() {
 			this.drawCanvas();
 		}
 		/* eslint-enable */
@@ -839,7 +839,7 @@ export default {
 			keyCode: 101,
 			preventDefault: true,
 			handler: () => {
-				if (this.video.paused) this.settings("play");
+				if (this.video.paused) this.play();
 				else this.settings("pause");
 			}
 		});
@@ -1033,6 +1033,11 @@ export default {
 			let saveButtonRef = this.$refs.saveButton;
 			if (close) saveButtonRef = this.$refs.saveAndCloseButton;
 
+			if (this.youtubeVideoDuration === "0.000") {
+				saveButtonRef.handleFailedSave();
+				return new Toast("The video appears to not be working.");
+			}
+
 			if (!song.title) {
 				saveButtonRef.handleFailedSave();
 				return new Toast("Please fill in all fields");
@@ -1370,6 +1375,15 @@ export default {
 					break;
 			}
 		},
+		play() {
+			if (
+				this.video.player.getVideoData().video_id !==
+				this.song.youtubeId
+			) {
+				this.loadVideoById(this.song.youtubeId, this.song.skipDuration);
+			}
+			this.settings("play");
+		},
 		changeVolume() {
 			const volume = this.volumeSliderValue;
 			localStorage.setItem("volume", volume / 100);
@@ -1519,11 +1533,11 @@ export default {
 				new Toast(res.message);
 			});
 		},
-		remove(id) {
-			this.socket.dispatch("songs.remove", id, res => {
-				new Toast(res.message);
-			});
-		},
+		// remove(id) {
+		// 	this.socket.dispatch("songs.remove", id, res => {
+		// 		new Toast(res.message);
+		// 	});
+		// },
 		...mapActions("modals/editSong", [
 			"stopVideo",
 			"loadVideoById",

+ 1 - 1
frontend/src/main.js

@@ -8,7 +8,7 @@ import store from "./store";
 
 import App from "./App.vue";
 
-const REQUIRED_CONFIG_VERSION = 3;
+const REQUIRED_CONFIG_VERSION = 4;
 
 const handleMetadata = attrs => {
 	document.title = `Musare | ${attrs.title}`;

+ 3 - 2
frontend/src/pages/Admin/tabs/VerifiedSongs.vue

@@ -27,7 +27,7 @@
 			>
 				Keyboard shortcuts helper
 			</button>
-			<!-- <confirm placement="bottom" @confirm="updateAllSongs()">
+			<confirm placement="bottom" @confirm="updateAllSongs()">
 				<button
 					class="button is-danger"
 					content="Update all songs"
@@ -35,7 +35,7 @@
 				>
 					Update all songs
 				</button>
-			</confirm> -->
+			</confirm>
 			<br />
 			<div>
 				<input
@@ -363,6 +363,7 @@ export default {
 			});
 		},
 		updateAllSongs() {
+			new Toast("Updating all songs, this could take a very long time.");
 			this.socket.dispatch("songs.updateAll", res => {
 				if (res.status === "success") new Toast(res.message);
 				else new Toast(res.message);

+ 41 - 28
frontend/src/pages/Station/index.vue

@@ -577,12 +577,16 @@
 
 		<floating-box id="player-debug-box" ref="playerDebugBox">
 			<template #body>
+				<span><b>No song</b>: {{ noSong }}</span>
+				<span><b>Song id</b>: {{ currentSong._id }}</span>
 				<span><b>YouTube id</b>: {{ currentSong.youtubeId }}</span>
 				<span><b>Duration</b>: {{ currentSong.duration }}</span>
 				<span
 					><b>Skip duration</b>: {{ currentSong.skipDuration }}</span
 				>
+				<span><b>Loading</b>: {{ loading }}</span>
 				<span><b>Can autoplay</b>: {{ canAutoplay }}</span>
+				<span><b>Player ready</b>: {{ playerReady }}</span>
 				<span
 					><b>Attempts to play video</b>:
 					{{ attemptsToPlayVideo }}</span
@@ -591,29 +595,17 @@
 					><b>Last time requested if can autoplay</b>:
 					{{ lastTimeRequestedIfCanAutoplay }}</span
 				>
-				<span><b>Loading</b>: {{ loading }}</span>
-				<span><b>Playback rate</b>: {{ playbackRate }}</span>
-				<span><b>Player ready</b>: {{ playerReady }}</span>
-				<span><b>Ready</b>: {{ ready }}</span>
 				<span><b>Seeking</b>: {{ seeking }}</span>
+				<span><b>Playback rate</b>: {{ playbackRate }}</span>
 				<span><b>System difference</b>: {{ systemDifference }}</span>
 				<span><b>Time before paused</b>: {{ timeBeforePause }}</span>
-				<span><b>Time elapsed</b>: {{ timeElapsed }}</span>
 				<span><b>Time paused</b>: {{ timePaused }}</span>
+				<span><b>Time elapsed</b>: {{ timeElapsed }}</span>
 				<span><b>Volume slider value</b>: {{ volumeSliderValue }}</span>
 				<span><b>Local paused</b>: {{ localPaused }}</span>
-				<span><b>No song</b>: {{ noSong }}</span>
-				<span
-					><b>Party playlists selected</b>: {{ partyPlaylists }}</span
-				>
 				<span><b>Station paused</b>: {{ stationPaused }}</span>
 				<span
-					><b>Station Included Playlists</b>:
-					{{ station.includedPlaylists.join(", ") }}</span
-				>
-				<span
-					><b>Station Excluded Playlists</b>:
-					{{ station.excludedPlaylists.join(", ") }}</span
+					><b>Party playlists selected</b>: {{ partyPlaylists }}</span
 				>
 			</template>
 		</floating-box>
@@ -666,7 +658,6 @@ export default {
 			utils,
 			title: "Station",
 			loading: true,
-			ready: false,
 			exists: true,
 			playerReady: false,
 			player: undefined,
@@ -692,7 +683,9 @@ export default {
 			activityWatchVideoLastStatus: "",
 			activityWatchVideoLastYouTubeId: "",
 			activityWatchVideoLastStartDuration: "",
-			nextCurrentSong: null
+			nextCurrentSong: null,
+			editSongModalWatcher: null,
+			beforeEditSongModalLocalPaused: null
 		};
 	},
 	computed: {
@@ -723,6 +716,18 @@ export default {
 		})
 	},
 	async mounted() {
+		this.editSongModalWatcher = this.$store.watch(
+			state => state.modalVisibility.modals.editSong,
+			newValue => {
+				if (newValue === true) {
+					this.beforeEditSongModalLocalPaused = this.localPaused;
+					this.pauseLocalStation();
+				} else if (!this.beforeEditSongModalLocalPaused) {
+					this.resumeLocalStation();
+				}
+			}
+		);
+
 		window.scrollTo(0, 0);
 
 		Date.currently = () => {
@@ -993,6 +998,8 @@ export default {
 			keyboardShortcuts.unregisterShortcut(shortcutName);
 		});
 
+		this.editSongModalWatcher(); // removes the watcher
+
 		clearInterval(this.activityWatchVideoDataInterval);
 		clearTimeout(window.stationNextSongTimeout);
 
@@ -1829,7 +1836,7 @@ export default {
 			);
 		},
 		sendActivityWatchVideoData() {
-			if (!this.stationPaused && !this.localPaused && this.currentSong) {
+			if (!this.stationPaused && !this.localPaused && !this.noSong) {
 				if (this.activityWatchVideoLastStatus !== "playing") {
 					this.activityWatchVideoLastStatus = "playing";
 					this.activityWatchVideoLastStartDuration =
@@ -1914,6 +1921,22 @@ export default {
 		}
 	}
 }
+
+#player-debug-box {
+	.box-body {
+		// flex-direction: column;
+		flex-flow: column;
+
+		span {
+			flex: 1;
+			display: block;
+		}
+
+		b {
+			color: var(--black);
+		}
+	}
+}
 </style>
 
 <style lang="scss" scoped>
@@ -1966,16 +1989,6 @@ export default {
 	filter: brightness(90%);
 }
 
-#player-debug-box {
-	.box-body {
-		flex-direction: column;
-
-		b {
-			color: var(--black);
-		}
-	}
-}
-
 .night-mode {
 	#currently-playing-container,
 	#next-up-container,