Browse Source

refactor: Improved SocketHandler onConnect usage and consistency

Owen Diffey 1 year ago
parent
commit
6a0de9af85
30 changed files with 2135 additions and 2077 deletions
  1. 4 4
      frontend/src/App.vue
  2. 34 21
      frontend/src/classes/SocketHandler.class.ts
  3. 22 17
      frontend/src/classes/__mocks__/SocketHandler.class.ts
  4. 34 33
      frontend/src/components/AddToPlaylistDropdown.vue
  5. 74 77
      frontend/src/components/AdvancedTable.vue
  6. 34 30
      frontend/src/components/LongJobs.vue
  7. 32 34
      frontend/src/components/PlaylistTabBase.vue
  8. 11 13
      frontend/src/components/modals/BulkActions.vue
  9. 16 18
      frontend/src/components/modals/EditNews.vue
  10. 54 56
      frontend/src/components/modals/EditPlaylist/index.vue
  11. 29 27
      frontend/src/components/modals/EditSong/Tabs/Reports.vue
  12. 308 306
      frontend/src/components/modals/EditSong/index.vue
  13. 5 7
      frontend/src/components/modals/ImportAlbum.vue
  14. 261 238
      frontend/src/components/modals/ManageStation/index.vue
  15. 23 22
      frontend/src/components/modals/Report.vue
  16. 27 29
      frontend/src/components/modals/ViewApiRequest.vue
  17. 23 25
      frontend/src/components/modals/ViewPunishment.vue
  18. 61 52
      frontend/src/components/modals/ViewReport.vue
  19. 235 217
      frontend/src/components/modals/ViewYoutubeVideo.vue
  20. 88 89
      frontend/src/composables/useSortablePlaylists.ts
  21. 14 16
      frontend/src/pages/Admin/Statistics.vue
  22. 31 31
      frontend/src/pages/Admin/YouTube/index.vue
  23. 115 117
      frontend/src/pages/Home.vue
  24. 30 28
      frontend/src/pages/News.vue
  25. 33 35
      frontend/src/pages/Profile/Tabs/RecentActivity.vue
  26. 16 18
      frontend/src/pages/Profile/index.vue
  27. 16 16
      frontend/src/pages/Settings/Tabs/Preferences.vue
  28. 34 36
      frontend/src/pages/Settings/index.vue
  29. 470 462
      frontend/src/pages/Station/index.vue
  30. 1 3
      frontend/src/stores/userPlaylists.ts

+ 4 - 4
frontend/src/App.vue

@@ -183,7 +183,7 @@ onMounted(async () => {
 
 	disconnectedMessage.value.hide();
 
-	socket.onConnect(true, () => {
+	socket.onConnect(() => {
 		socketConnected.value = true;
 
 		socket.dispatch(
@@ -254,11 +254,11 @@ onMounted(async () => {
 			if (!localStorage.getItem("firstVisited"))
 				localStorage.setItem("firstVisited", Date.now().toString());
 		});
-	});
+	}, true);
 
-	socket.onDisconnect(true, () => {
+	socket.onDisconnect(() => {
 		socketConnected.value = false;
-	});
+	}, true);
 
 	apiDomain.value = await lofig.get("backend.apiDomain");
 

+ 34 - 21
frontend/src/classes/SocketHandler.class.ts

@@ -3,31 +3,35 @@ import { useUserAuthStore } from "@/stores/userAuth";
 import utils from "@/utils";
 
 export default class SocketHandler {
-	socket: WebSocket;
+	socket?: WebSocket;
 
 	url: string;
 
 	dispatcher: ListenerHandler;
 
 	onConnectCbs: {
-		temp: any[];
-		persist: any[];
+		temp: ((...args: any[]) => any)[];
+		persist: ((...args: any[]) => any)[];
 	};
 
 	ready: boolean;
 
 	firstInit: boolean;
 
-	pendingDispatches: any[];
+	pendingDispatches: ((...args: any[]) => any)[];
 
 	onDisconnectCbs: {
-		temp: any[];
-		persist: any[];
+		temp: ((...args: any[]) => any)[];
+		persist: ((...args: any[]) => any)[];
 	};
 
-	CB_REFS: object;
+	CB_REFS: {
+		[key: string]: (...args: any[]) => any;
+	};
 
-	PROGRESS_CB_REFS: object;
+	PROGRESS_CB_REFS: {
+		[key: string]: (...args: any[]) => any;
+	};
 
 	data: {
 		dispatch?: {
@@ -70,6 +74,11 @@ export default class SocketHandler {
 		this.PROGRESS_CB_REFS = {};
 
 		this.init();
+
+		// Mock only
+		this.data = {};
+		this.executeDispatch = true;
+		this.trigger = () => {};
 	}
 
 	init() {
@@ -142,17 +151,23 @@ export default class SocketHandler {
 		}
 	}
 
-	on(target, cb, options?) {
+	on(
+		target: string,
+		cb: (...args: any[]) => any,
+		options?: EventListenerOptions
+	) {
 		this.dispatcher.addEventListener(
 			target,
-			event => cb(...event.detail),
+			(event: CustomEvent) => cb(...event.detail),
 			options
 		);
 	}
 
-	dispatch(...args) {
-		if (this.socket.readyState !== 1)
-			return this.pendingDispatches.push(() => this.dispatch(...args));
+	dispatch(...args: [string, ...any[]]) {
+		if (!this.socket || this.socket.readyState !== 1) {
+			this.pendingDispatches.push(() => this.dispatch(...args));
+			return undefined;
+		}
 
 		const lastArg = args[args.length - 1];
 		const CB_REF = utils.guid();
@@ -179,17 +194,15 @@ export default class SocketHandler {
 		return this.socket.send(JSON.stringify([...args]));
 	}
 
-	onConnect(...args) {
-		const cb = args[1] || args[0];
-		if (this.socket.readyState === 1 && this.ready) cb();
+	onConnect(cb: (...args: any[]) => any, persist = false) {
+		if (this.socket && this.socket.readyState === 1 && this.ready) cb();
 
-		if (args[0] === true) this.onConnectCbs.persist.push(cb);
+		if (persist) this.onConnectCbs.persist.push(cb);
 		else this.onConnectCbs.temp.push(cb);
 	}
 
-	onDisconnect(...args) {
-		const cb = args[1] || args[0];
-		if (args[0] === true) this.onDisconnectCbs.persist.push(cb);
+	onDisconnect(cb: (...args: any[]) => any, persist = false) {
+		if (persist) this.onDisconnectCbs.persist.push(cb);
 		else this.onDisconnectCbs.temp.push(cb);
 	}
 
@@ -222,7 +235,7 @@ export default class SocketHandler {
 		});
 	}
 
-	destroyModalListeners(modalUuid) {
+	destroyModalListeners(modalUuid: string) {
 		// destroy all listeners for a specific modal
 		Object.keys(this.dispatcher.listeners).forEach(type =>
 			this.dispatcher.listeners[type].forEach((element, index) => {

+ 22 - 17
frontend/src/classes/__mocks__/SocketHandler.class.ts

@@ -24,7 +24,7 @@ export default class SocketHandlerMock {
 
 	executeDispatch: boolean;
 
-	constructor(url) {
+	constructor(url: string) {
 		this.dispatcher = new ListenerHandler();
 		this.url = url;
 		this.data = {
@@ -39,16 +39,20 @@ export default class SocketHandlerMock {
 		this.executeDispatch = true;
 	}
 
-	on(target, cb, options?) {
+	on(
+		target: string,
+		cb: (...args: any[]) => any,
+		options?: EventListenerOptions
+	) {
 		const onData = this.data.on && this.data.on[target];
 		this.dispatcher.addEventListener(
 			`on.${target}`,
-			event => cb(event.detail() || onData),
+			(event: CustomEvent) => cb(event.detail() || onData),
 			options
 		);
 	}
 
-	dispatch(target, ...args) {
+	dispatch(target: string, ...args: any[]) {
 		const lastArg = args[args.length - 1];
 		const _args = args.slice(0, -1);
 		const dispatchData = () =>
@@ -67,24 +71,25 @@ export default class SocketHandlerMock {
 			else if (!this.executeDispatch)
 				this.dispatcher.addEventListener(
 					`dispatch.${target}`,
-					event => lastArg(event.detail(..._args) || dispatchData()),
+					(event: CustomEvent) =>
+						lastArg(event.detail(..._args) || dispatchData()),
 					false
 				);
 		} else if (typeof lastArg === "object") {
 			if (this.executeDispatch) {
 				if (progressData())
-					progressData().forEach(data => {
+					progressData().forEach((data: any) => {
 						lastArg.onProgress(data);
 					});
 				if (dispatchData()) lastArg.cb(dispatchData());
 			} else {
 				this.dispatcher.addEventListener(
 					`progress.${target}`,
-					event => {
+					(event: CustomEvent) => {
 						if (event.detail(..._args))
 							lastArg.onProgress(event.detail(..._args));
 						else if (progressData())
-							progressData().forEach(data => {
+							progressData().forEach((data: any) => {
 								lastArg.onProgress(data);
 							});
 					},
@@ -92,7 +97,7 @@ export default class SocketHandlerMock {
 				);
 				this.dispatcher.addEventListener(
 					`dispatch.${target}`,
-					event =>
+					(event: CustomEvent) =>
 						lastArg.cb(event.detail(..._args) || dispatchData()),
 					false
 				);
@@ -101,19 +106,19 @@ export default class SocketHandlerMock {
 	}
 
 	// eslint-disable-next-line class-methods-use-this
-	onConnect(cb) {
+	onConnect(cb: (...args: any[]) => any) {
 		cb();
 	}
 
-	onDisconnect(...args) {
-		if (args[0] === true) this.onDisconnectCbs.persist.push(args[1]);
-		else this.onDisconnectCbs.temp.push(args[0]);
+	onDisconnect(cb: (...args: any[]) => any, persist = false) {
+		if (persist) this.onDisconnectCbs.persist.push(cb);
+		else this.onDisconnectCbs.temp.push(cb);
 
 		this.dispatcher.addEventListener(
 			"socket.disconnect",
 			() => {
-				this.onDisconnectCbs.temp.forEach(cb => cb());
-				this.onDisconnectCbs.persist.forEach(cb => cb());
+				this.onDisconnectCbs.temp.forEach(callback => callback());
+				this.onDisconnectCbs.persist.forEach(callback => callback());
 			},
 			false
 		);
@@ -126,10 +131,10 @@ export default class SocketHandlerMock {
 	// eslint-disable-next-line class-methods-use-this
 	destroyModalListeners() {}
 
-	trigger(type, target, data?) {
+	trigger(type: string, target: string, data?: any) {
 		this.dispatcher.dispatchEvent(
 			new CustomEvent(`${type}.${target}`, {
-				detail: (...args) => {
+				detail: (...args: any[]) => {
 					if (typeof data === "function") return data(...args);
 					if (typeof data === "undefined") return undefined;
 					return JSON.parse(JSON.stringify(data));

+ 34 - 33
frontend/src/components/AddToPlaylistDropdown.vue

@@ -29,22 +29,11 @@ const dropdown = ref(null);
 const { socket } = useWebsocketsStore();
 const userPlaylistsStore = useUserPlaylistsStore();
 
-const { playlists, fetchedPlaylists } = storeToRefs(userPlaylistsStore);
+const { playlists } = storeToRefs(userPlaylistsStore);
 const { setPlaylists, addPlaylist, removePlaylist } = userPlaylistsStore;
 
 const { openModal } = useModalsStore();
 
-const init = () => {
-	if (!fetchedPlaylists.value)
-		socket.dispatch(
-			"playlists.indexMyPlaylists",
-			(res: IndexMyPlaylistsResponse) => {
-				if (res.status === "success")
-					if (!fetchedPlaylists.value)
-						setPlaylists(res.data.playlists);
-			}
-		);
-};
 const hasSong = playlist =>
 	playlist.songs.map(song => song.youtubeId).indexOf(props.song.youtubeId) !==
 	-1;
@@ -79,29 +68,41 @@ const createPlaylist = () => {
 };
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch(
+			"playlists.indexMyPlaylists",
+			(res: IndexMyPlaylistsResponse) => {
+				if (res.status === "success") setPlaylists(res.data.playlists);
+			}
+		);
 
-	socket.on("event:playlist.created", res => addPlaylist(res.data.playlist), {
-		replaceable: true
-	});
+		socket.on(
+			"event:playlist.created",
+			res => addPlaylist(res.data.playlist),
+			{
+				replaceable: true
+			}
+		);
 
-	socket.on(
-		"event:playlist.deleted",
-		res => removePlaylist(res.data.playlistId),
-		{ replaceable: true }
-	);
-
-	socket.on(
-		"event:playlist.displayName.updated",
-		res => {
-			playlists.value.forEach((playlist, index) => {
-				if (playlist._id === res.data.playlistId) {
-					playlists.value[index].displayName = res.data.displayName;
-				}
-			});
-		},
-		{ replaceable: true }
-	);
+		socket.on(
+			"event:playlist.deleted",
+			res => removePlaylist(res.data.playlistId),
+			{ replaceable: true }
+		);
+
+		socket.on(
+			"event:playlist.displayName.updated",
+			res => {
+				playlists.value.forEach((playlist, index) => {
+					if (playlist._id === res.data.playlistId) {
+						playlists.value[index].displayName =
+							res.data.displayName;
+					}
+				});
+			},
+			{ replaceable: true }
+		);
+	});
 });
 </script>
 

+ 74 - 77
frontend/src/components/AdvancedTable.vue

@@ -762,33 +762,6 @@ const removeData = index => {
 	};
 };
 
-const init = () => {
-	getData();
-	if (props.query) setQuery();
-	if (props.events) {
-		// if (props.events.room)
-		// 	socket.dispatch("apis.joinRoom", props.events.room, () => {});
-		if (props.events.adminRoom)
-			socket.dispatch(
-				"apis.joinAdminRoom",
-				props.events.adminRoom,
-				() => {}
-			);
-	}
-	props.filters.forEach(filter => {
-		if (filter.autosuggest && filter.autosuggestDataAction) {
-			socket.dispatch(filter.autosuggestDataAction, res => {
-				if (res.status === "success") {
-					const { items } = res.data;
-					autosuggest.value.allItems[filter.name] = items;
-				} else {
-					new Toast(res.message);
-				}
-			});
-		}
-	});
-};
-
 onMounted(async () => {
 	const tableSettings = getTableSettings();
 
@@ -933,58 +906,82 @@ onMounted(async () => {
 		}
 	}
 
-	socket.onConnect(init);
-
-	// TODO, this doesn't address special properties
-	if (props.events && props.events.updated)
-		socket.on(`event:${props.events.updated.event}`, res => {
-			const index = rows.value
-				.map(row => row._id)
-				.indexOf(
-					props.events.updated.id
-						.split(".")
-						.reduce(
-							(previous, current) =>
-								previous &&
-								previous[current] !== null &&
-								previous[current] !== undefined
-									? previous[current]
-									: null,
-							res.data
-						)
-				);
-			const row = props.events.updated.item
-				.split(".")
-				.reduce(
-					(previous, current) =>
-						previous &&
-						previous[current] !== null &&
-						previous[current] !== undefined
-							? previous[current]
-							: null,
-					res.data
-				);
-			updateData(index, row);
-		});
-	if (props.events && props.events.removed)
-		socket.on(`event:${props.events.removed.event}`, res => {
-			const index = rows.value
-				.map(row => row._id)
-				.indexOf(
-					props.events.removed.id
-						.split(".")
-						.reduce(
-							(previous, current) =>
-								previous &&
-								previous[current] !== null &&
-								previous[current] !== undefined
-									? previous[current]
-									: null,
-							res.data
-						)
+	socket.onConnect(() => {
+		getData();
+		if (props.query) setQuery();
+		if (props.events) {
+			// if (props.events.room)
+			// 	socket.dispatch("apis.joinRoom", props.events.room, () => {});
+			if (props.events.adminRoom)
+				socket.dispatch(
+					"apis.joinAdminRoom",
+					props.events.adminRoom,
+					() => {}
 				);
-			removeData(index);
+		}
+		props.filters.forEach(filter => {
+			if (filter.autosuggest && filter.autosuggestDataAction) {
+				socket.dispatch(filter.autosuggestDataAction, res => {
+					if (res.status === "success") {
+						const { items } = res.data;
+						autosuggest.value.allItems[filter.name] = items;
+					} else {
+						new Toast(res.message);
+					}
+				});
+			}
 		});
+		// TODO, this doesn't address special properties
+		if (props.events && props.events.updated)
+			socket.on(`event:${props.events.updated.event}`, res => {
+				const index = rows.value
+					.map(row => row._id)
+					.indexOf(
+						props.events.updated.id
+							.split(".")
+							.reduce(
+								(previous, current) =>
+									previous &&
+									previous[current] !== null &&
+									previous[current] !== undefined
+										? previous[current]
+										: null,
+								res.data
+							)
+					);
+				const row = props.events.updated.item
+					.split(".")
+					.reduce(
+						(previous, current) =>
+							previous &&
+							previous[current] !== null &&
+							previous[current] !== undefined
+								? previous[current]
+								: null,
+						res.data
+					);
+				updateData(index, row);
+			});
+		if (props.events && props.events.removed)
+			socket.on(`event:${props.events.removed.event}`, res => {
+				const index = rows.value
+					.map(row => row._id)
+					.indexOf(
+						props.events.removed.id
+							.split(".")
+							.reduce(
+								(previous, current) =>
+									previous &&
+									previous[current] !== null &&
+									previous[current] !== undefined
+										? previous[current]
+										: null,
+								res.data
+							)
+					);
+				removeData(index);
+			});
+	});
 
 	if (props.keyboardShortcuts) {
 		// Navigation section

+ 34 - 30
frontend/src/components/LongJobs.vue

@@ -31,38 +31,42 @@ const remove = job => {
 };
 
 onMounted(() => {
-	if (loggedIn.value) {
-		socket.dispatch("users.getLongJobs", {
-			cb: res => {
-				if (res.status === "success") {
-					setJobs(res.data.longJobs);
-				} else console.log(res.message);
-			},
-			onProgress: res => {
-				setJob(res);
-			}
-		});
+	socket.onConnect(() => {
+		if (loggedIn.value) {
+			socket.dispatch("users.getLongJobs", {
+				cb: res => {
+					if (res.status === "success") {
+						setJobs(res.data.longJobs);
+					} else console.log(res.message);
+				},
+				onProgress: res => {
+					setJob(res);
+				}
+			});
 
-		socket.on("keep.event:longJob.removed", ({ data }) => {
-			removeJob(data.jobId);
-		});
+			socket.on("keep.event:longJob.removed", ({ data }) => {
+				removeJob(data.jobId);
+			});
 
-		socket.on("keep.event:longJob.added", ({ data }) => {
-			if (
-				!activeJobs.value.find(activeJob => activeJob.id === data.jobId)
-			)
-				socket.dispatch("users.getLongJob", data.jobId, {
-					cb: res => {
-						if (res.status === "success") {
-							setJob(res.data.longJob);
-						} else console.log(res.message);
-					},
-					onProgress: res => {
-						setJob(res);
-					}
-				});
-		});
-	}
+			socket.on("keep.event:longJob.added", ({ data }) => {
+				if (
+					!activeJobs.value.find(
+						activeJob => activeJob.id === data.jobId
+					)
+				)
+					socket.dispatch("users.getLongJob", data.jobId, {
+						cb: res => {
+							if (res.status === "success") {
+								setJob(res.data.longJob);
+							} else console.log(res.message);
+						},
+						onProgress: res => {
+							setJob(res);
+						}
+					});
+			});
+		}
+	});
 });
 </script>
 

+ 32 - 34
frontend/src/components/PlaylistTabBase.vue

@@ -106,39 +106,6 @@ const { setPlaylists } = useUserPlaylistsStore();
 const { addPlaylistToAutoRequest, removePlaylistFromAutoRequest } =
 	stationStore;
 
-const init = () => {
-	socket.dispatch("playlists.indexMyPlaylists", res => {
-		if (res.status === "success") setPlaylists(res.data.playlists);
-		orderOfPlaylists.value = calculatePlaylistOrder(); // order in regards to the database
-	});
-
-	socket.dispatch("playlists.indexFeaturedPlaylists", res => {
-		if (res.status === "success")
-			featuredPlaylists.value = res.data.playlists;
-	});
-
-	if (props.type === "autofill")
-		socket.dispatch(
-			`stations.getStationAutofillPlaylistsById`,
-			station.value._id,
-			res => {
-				if (res.status === "success") {
-					station.value.autofill.playlists = res.data.playlists;
-				}
-			}
-		);
-
-	socket.dispatch(
-		`stations.getStationBlacklistById`,
-		station.value._id,
-		res => {
-			if (res.status === "success") {
-				station.value.blacklist = res.data.playlists;
-			}
-		}
-	);
-};
-
 const showTab = _tab => {
 	tabs.value[`${_tab}-tab`].scrollIntoView({ block: "nearest" });
 	tab.value = _tab;
@@ -295,7 +262,38 @@ const searchForPlaylists = page => {
 onMounted(() => {
 	showTab("search");
 
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch("playlists.indexMyPlaylists", res => {
+			if (res.status === "success") setPlaylists(res.data.playlists);
+			orderOfPlaylists.value = calculatePlaylistOrder(); // order in regards to the database
+		});
+
+		socket.dispatch("playlists.indexFeaturedPlaylists", res => {
+			if (res.status === "success")
+				featuredPlaylists.value = res.data.playlists;
+		});
+
+		if (props.type === "autofill")
+			socket.dispatch(
+				`stations.getStationAutofillPlaylistsById`,
+				station.value._id,
+				res => {
+					if (res.status === "success") {
+						station.value.autofill.playlists = res.data.playlists;
+					}
+				}
+			);
+
+		socket.dispatch(
+			`stations.getStationBlacklistById`,
+			station.value._id,
+			res => {
+				if (res.status === "success") {
+					station.value.blacklist = res.data.playlists;
+				}
+			}
+		);
+	});
 });
 </script>
 

+ 11 - 13
frontend/src/components/modals/BulkActions.vue

@@ -30,18 +30,6 @@ const items = ref([]);
 const itemInput = ref();
 const allItems = ref([]);
 
-const init = () => {
-	if (type.value.autosuggest && type.value.autosuggestDataAction)
-		socket.dispatch(type.value.autosuggestDataAction, res => {
-			if (res.status === "success") {
-				const { items } = res.data;
-				allItems.value = items;
-			} else {
-				new Toast(res.message);
-			}
-		});
-};
-
 const addItem = () => {
 	if (!itemInput.value) return;
 	if (type.value.regex && !type.value.regex.test(itemInput.value)) {
@@ -95,7 +83,17 @@ onBeforeUnmount(() => {
 });
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		if (type.value.autosuggest && type.value.autosuggestDataAction)
+			socket.dispatch(type.value.autosuggestDataAction, res => {
+				if (res.status === "success") {
+					const { items } = res.data;
+					allItems.value = items;
+				} else {
+					new Toast(res.message);
+				}
+			});
+	});
 });
 </script>
 

+ 16 - 18
frontend/src/components/modals/EditNews.vue

@@ -36,23 +36,6 @@ const showToNewUsers = ref(false);
 const createdBy = ref();
 const createdAt = ref(0);
 
-const init = () => {
-	if (newsId && !createNews.value) {
-		socket.dispatch(`news.getNewsFromId`, newsId.value, res => {
-			if (res.status === "success") {
-				markdown.value = res.data.news.markdown;
-				status.value = res.data.news.status;
-				showToNewUsers.value = res.data.news.showToNewUsers;
-				createdBy.value = res.data.news.createdBy;
-				createdAt.value = res.data.news.createdAt;
-			} else {
-				new Toast("News with that ID not found.");
-				closeCurrentModal();
-			}
-		});
-	}
-};
-
 const getTitle = () => {
 	let title = "";
 	const preview = document.getElementById("preview");
@@ -141,7 +124,22 @@ onMounted(() => {
 		}
 	});
 
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		if (newsId.value && !createNews.value) {
+			socket.dispatch(`news.getNewsFromId`, newsId.value, res => {
+				if (res.status === "success") {
+					markdown.value = res.data.news.markdown;
+					status.value = res.data.news.status;
+					showToNewUsers.value = res.data.news.showToNewUsers;
+					createdBy.value = res.data.news.createdBy;
+					createdAt.value = res.data.news.createdAt;
+				} else {
+					new Toast("News with that ID not found.");
+					closeCurrentModal();
+				}
+			});
+		}
+	});
 });
 </script>
 

+ 54 - 56
frontend/src/components/modals/EditPlaylist/index.vue

@@ -79,16 +79,6 @@ const isEditable = permission =>
 		permission === "playlists.update.privacy" &&
 		hasPermission(permission));
 
-const init = () => {
-	gettingSongs.value = true;
-	socket.dispatch("playlists.getPlaylist", playlistId.value, res => {
-		if (res.status === "success") {
-			setPlaylist(res.data.playlist);
-		} else new Toast(res.message);
-		gettingSongs.value = false;
-	});
-};
-
 const repositionSong = ({ moved }) => {
 	const { oldIndex, newIndex } = moved;
 	if (oldIndex === newIndex) return; // we only need to update when song is moved
@@ -256,54 +246,62 @@ const clearAndRefillGenrePlaylist = () => {
 };
 
 onMounted(() => {
-	socket.onConnect(init);
-
-	socket.on(
-		"event:playlist.song.added",
-		res => {
-			if (playlist.value._id === res.data.playlistId)
-				addSong(res.data.song);
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:playlist.song.removed",
-		res => {
-			if (playlist.value._id === res.data.playlistId) {
-				// remove song from array of playlists
-				removeSong(res.data.youtubeId);
-			}
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:playlist.displayName.updated",
-		res => {
-			if (playlist.value._id === res.data.playlistId) {
-				setPlaylist({
-					displayName: res.data.displayName,
-					...playlist.value
-				});
-			}
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:playlist.song.repositioned",
-		res => {
-			if (playlist.value._id === res.data.playlistId) {
-				const { song, playlistId } = res.data;
+	socket.onConnect(() => {
+		gettingSongs.value = true;
+		socket.dispatch("playlists.getPlaylist", playlistId.value, res => {
+			if (res.status === "success") {
+				setPlaylist(res.data.playlist);
+			} else new Toast(res.message);
+			gettingSongs.value = false;
+		});
 
-				if (playlist.value._id === playlistId) {
-					repositionedSong(song);
+		socket.on(
+			"event:playlist.song.added",
+			res => {
+				if (playlist.value._id === res.data.playlistId)
+					addSong(res.data.song);
+			},
+			{ modalUuid: props.modalUuid }
+		);
+
+		socket.on(
+			"event:playlist.song.removed",
+			res => {
+				if (playlist.value._id === res.data.playlistId) {
+					// remove song from array of playlists
+					removeSong(res.data.youtubeId);
 				}
-			}
-		},
-		{ modalUuid: props.modalUuid }
-	);
+			},
+			{ modalUuid: props.modalUuid }
+		);
+
+		socket.on(
+			"event:playlist.displayName.updated",
+			res => {
+				if (playlist.value._id === res.data.playlistId) {
+					setPlaylist({
+						displayName: res.data.displayName,
+						...playlist.value
+					});
+				}
+			},
+			{ modalUuid: props.modalUuid }
+		);
+
+		socket.on(
+			"event:playlist.song.repositioned",
+			res => {
+				if (playlist.value._id === res.data.playlistId) {
+					const { song, playlistId } = res.data;
+
+					if (playlist.value._id === playlistId) {
+						repositionedSong(song);
+					}
+				}
+			},
+			{ modalUuid: props.modalUuid }
+		);
+	});
 });
 
 onBeforeUnmount(() => {

+ 29 - 27
frontend/src/components/modals/EditSong/Tabs/Reports.vue

@@ -71,33 +71,35 @@ const toggleIssue = (reportId, issueId) => {
 };
 
 onMounted(() => {
-	socket.on(
-		"event:admin.report.created",
-		res => reports.value.unshift(res.data.report),
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:admin.report.resolved",
-		res => resolveReport(res.data.reportId),
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:admin.report.issue.toggled",
-		res => {
-			reports.value.forEach((report, index) => {
-				if (report._id === res.data.reportId) {
-					const issue = reports.value[index].issues.find(
-						issue => issue._id.toString() === res.data.issueId
-					);
-
-					issue.resolved = res.data.resolved;
-				}
-			});
-		},
-		{ modalUuid: props.modalUuid }
-	);
+	socket.onConnect(() => {
+		socket.on(
+			"event:admin.report.created",
+			res => reports.value.unshift(res.data.report),
+			{ modalUuid: props.modalUuid }
+		);
+
+		socket.on(
+			"event:admin.report.resolved",
+			res => resolveReport(res.data.reportId),
+			{ modalUuid: props.modalUuid }
+		);
+
+		socket.on(
+			"event:admin.report.issue.toggled",
+			res => {
+				reports.value.forEach((report, index) => {
+					if (report._id === res.data.reportId) {
+						const issue = reports.value[index].issues.find(
+							issue => issue._id.toString() === res.data.issueId
+						);
+
+						issue.resolved = res.data.resolved;
+					}
+				});
+			},
+			{ modalUuid: props.modalUuid }
+		);
+	});
 });
 </script>
 

+ 308 - 306
frontend/src/components/modals/EditSong/index.vue

@@ -430,236 +430,6 @@ const seekTo = position => {
 	video.value.player.seekTo(position);
 };
 
-const init = () => {
-	if (newSong.value && !youtubeId.value && !bulk.value) {
-		setSong({
-			youtubeId: "",
-			title: "",
-			artists: [],
-			genres: [],
-			tags: [],
-			duration: 0,
-			skipDuration: 0,
-			thumbnail: "",
-			verified: false
-		});
-		songDataLoaded.value = true;
-		showTab("youtube");
-	} else if (youtubeId.value) loadSong(youtubeId.value);
-	else if (!bulk.value) {
-		new Toast("You can't open EditSong without editing a song");
-		return modalsStore.closeCurrentModal();
-	}
-
-	interval.value = setInterval(() => {
-		if (
-			song.value.duration !== -1 &&
-			video.value.paused === false &&
-			playerReady.value &&
-			(video.value.player.getCurrentTime() - song.value.skipDuration >
-				song.value.duration ||
-				(video.value.player.getCurrentTime() > 0 &&
-					video.value.player.getCurrentTime() >=
-						video.value.player.getDuration()))
-		) {
-			stopVideo();
-			pauseVideo(true);
-
-			drawCanvas();
-		}
-		if (
-			playerReady.value &&
-			video.value.player.getVideoData &&
-			video.value.player.getVideoData() &&
-			video.value.player.getVideoData().video_id === song.value.youtubeId
-		) {
-			const currentTime = video.value.player.getCurrentTime();
-
-			if (currentTime !== undefined)
-				youtubeVideoCurrentTime.value = currentTime.toFixed(3);
-
-			if (youtubeVideoDuration.value.indexOf(".000") !== -1) {
-				const duration = video.value.player.getDuration();
-
-				if (duration !== undefined) {
-					if (
-						`${youtubeVideoDuration.value}` ===
-						`${Number(song.value.duration).toFixed(3)}`
-					)
-						song.value.duration = duration.toFixed(3);
-
-					youtubeVideoDuration.value = duration.toFixed(3);
-					if (youtubeVideoDuration.value.indexOf(".000") !== -1)
-						youtubeVideoNote.value = "(~)";
-					else youtubeVideoNote.value = "";
-
-					drawCanvas();
-				}
-			}
-		}
-
-		if (video.value.paused === false) drawCanvas();
-	}, 200);
-
-	if (window.YT && window.YT.Player) {
-		video.value.player = new window.YT.Player(
-			`editSongPlayer-${props.modalUuid}`,
-			{
-				height: 298,
-				width: 530,
-				videoId: null,
-				host: "https://www.youtube-nocookie.com",
-				playerVars: {
-					controls: 0,
-					iv_load_policy: 3,
-					rel: 0,
-					showinfo: 0,
-					autoplay: 0
-				},
-				startSeconds: song.value.skipDuration,
-				events: {
-					onReady: () => {
-						let volume = parseFloat(localStorage.getItem("volume"));
-						volume = typeof volume === "number" ? volume : 20;
-						video.value.player.setVolume(volume);
-						if (volume > 0) video.value.player.unMute();
-
-						playerReady.value = true;
-
-						if (song.value && song.value.youtubeId)
-							video.value.player.cueVideoById(
-								song.value.youtubeId,
-								song.value.skipDuration
-							);
-
-						setPlaybackRate(null);
-
-						drawCanvas();
-					},
-					onStateChange: event => {
-						drawCanvas();
-
-						if (event.data === 1) {
-							video.value.paused = false;
-							updateMediaModalPlayingAudio(true);
-							let youtubeDuration =
-								video.value.player.getDuration();
-							const newYoutubeVideoDuration =
-								youtubeDuration.toFixed(3);
-
-							if (
-								youtubeVideoDuration.value.indexOf(".000") !==
-									-1 &&
-								`${youtubeVideoDuration.value}` !==
-									`${newYoutubeVideoDuration}`
-							) {
-								const songDurationNumber = Number(
-									song.value.duration
-								);
-								const songDurationNumber2 =
-									Number(song.value.duration) + 1;
-								const songDurationNumber3 =
-									Number(song.value.duration) - 1;
-								const fixedSongDuration =
-									songDurationNumber.toFixed(3);
-								const fixedSongDuration2 =
-									songDurationNumber2.toFixed(3);
-								const fixedSongDuration3 =
-									songDurationNumber3.toFixed(3);
-
-								if (
-									`${youtubeVideoDuration.value}` ===
-										`${Number(song.value.duration).toFixed(
-											3
-										)}` &&
-									(fixedSongDuration ===
-										youtubeVideoDuration.value ||
-										fixedSongDuration2 ===
-											youtubeVideoDuration.value ||
-										fixedSongDuration3 ===
-											youtubeVideoDuration.value)
-								)
-									song.value.duration =
-										newYoutubeVideoDuration;
-
-								youtubeVideoDuration.value =
-									newYoutubeVideoDuration;
-								if (
-									youtubeVideoDuration.value.indexOf(
-										".000"
-									) !== -1
-								)
-									youtubeVideoNote.value = "(~)";
-								else youtubeVideoNote.value = "";
-							}
-
-							if (song.value.duration === -1)
-								song.value.duration = Number.parseInt(
-									youtubeVideoDuration.value
-								);
-
-							youtubeDuration -= song.value.skipDuration;
-							if (song.value.duration > youtubeDuration + 1) {
-								stopVideo();
-								pauseVideo(true);
-
-								return new Toast(
-									"Video can't play. Specified duration is bigger than the YouTube song duration."
-								);
-							}
-							if (song.value.duration <= 0) {
-								stopVideo();
-								pauseVideo(true);
-
-								return new Toast(
-									"Video can't play. Specified duration has to be more than 0 seconds."
-								);
-							}
-
-							if (
-								video.value.player.getCurrentTime() <
-								song.value.skipDuration
-							) {
-								return seekTo(song.value.skipDuration);
-							}
-
-							setPlaybackRate(null);
-						} else if (event.data === 2) {
-							video.value.paused = true;
-							updateMediaModalPlayingAudio(false);
-						}
-
-						return false;
-					}
-				}
-			}
-		);
-	} else {
-		youtubeError.value = true;
-		youtubeErrorMessage.value = "Player could not be loaded.";
-	}
-
-	["artists", "genres", "tags"].forEach(type => {
-		socket.dispatch(
-			`songs.get${type.charAt(0).toUpperCase()}${type.slice(1)}`,
-			res => {
-				if (res.status === "success") {
-					const { items } = res.data;
-					if (type === "genres")
-						autosuggest.value.allItems[type] = Array.from(
-							new Set([...recommendedGenres.value, ...items])
-						);
-					else autosuggest.value.allItems[type] = items;
-				} else {
-					new Toast(res.message);
-				}
-			}
-		);
-	});
-
-	return null;
-};
-
 const save = (songToCopy, closeOrNext, saveButtonRefName, _newSong = false) => {
 	const _song = JSON.parse(JSON.stringify(songToCopy));
 
@@ -1219,98 +989,330 @@ onMounted(async () => {
 
 	useHTTPS.value = await lofig.get("cookie.secure");
 
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		if (newSong.value && !youtubeId.value && !bulk.value) {
+			setSong({
+				youtubeId: "",
+				title: "",
+				artists: [],
+				genres: [],
+				tags: [],
+				duration: 0,
+				skipDuration: 0,
+				thumbnail: "",
+				verified: false
+			});
+			songDataLoaded.value = true;
+			showTab("youtube");
+		} else if (youtubeId.value) loadSong(youtubeId.value);
+		else if (!bulk.value) {
+			new Toast("You can't open EditSong without editing a song");
+			return modalsStore.closeCurrentModal();
+		}
 
-	let volume = parseFloat(localStorage.getItem("volume"));
-	volume = typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
-	localStorage.setItem("volume", `${volume}`);
-	volumeSliderValue.value = volume;
+		interval.value = setInterval(() => {
+			if (
+				song.value.duration !== -1 &&
+				video.value.paused === false &&
+				playerReady.value &&
+				(video.value.player.getCurrentTime() - song.value.skipDuration >
+					song.value.duration ||
+					(video.value.player.getCurrentTime() > 0 &&
+						video.value.player.getCurrentTime() >=
+							video.value.player.getDuration()))
+			) {
+				stopVideo();
+				pauseVideo(true);
 
-	socket.on(
-		"event:admin.song.removed",
-		res => {
-			if (res.data.songId === song.value._id) {
-				songDeleted.value = true;
+				drawCanvas();
 			}
-		},
-		{ modalUuid: props.modalUuid }
-	);
+			if (
+				playerReady.value &&
+				video.value.player.getVideoData &&
+				video.value.player.getVideoData() &&
+				video.value.player.getVideoData().video_id ===
+					song.value.youtubeId
+			) {
+				const currentTime = video.value.player.getCurrentTime();
 
-	if (bulk.value) {
-		socket.dispatch("apis.joinRoom", "edit-songs");
+				if (currentTime !== undefined)
+					youtubeVideoCurrentTime.value = currentTime.toFixed(3);
 
-		socket.dispatch(
-			"songs.getSongsFromYoutubeIds",
-			youtubeIds.value,
-			res => {
-				if (res.data.songs.length === 0) {
-					modalsStore.closeCurrentModal();
-					new Toast("You can't edit 0 songs.");
-				} else {
-					items.value = res.data.songs.map(song => ({
-						status: "todo",
-						flagged: false,
-						song
-					}));
-					editNextSong();
+				if (youtubeVideoDuration.value.indexOf(".000") !== -1) {
+					const duration = video.value.player.getDuration();
+
+					if (duration !== undefined) {
+						if (
+							`${youtubeVideoDuration.value}` ===
+							`${Number(song.value.duration).toFixed(3)}`
+						)
+							song.value.duration = duration.toFixed(3);
+
+						youtubeVideoDuration.value = duration.toFixed(3);
+						if (youtubeVideoDuration.value.indexOf(".000") !== -1)
+							youtubeVideoNote.value = "(~)";
+						else youtubeVideoNote.value = "";
+
+						drawCanvas();
+					}
 				}
 			}
-		);
 
-		socket.on(
-			`event:admin.song.created`,
-			res => {
-				const index = items.value
-					.map(item => item.song.youtubeId)
-					.indexOf(res.data.song.youtubeId);
-				if (index >= 0)
-					items.value[index].song = {
-						...items.value[index].song,
-						...res.data.song,
-						created: true
-					};
-			},
-			{ modalUuid: props.modalUuid }
-		);
+			if (video.value.paused === false) drawCanvas();
+		}, 200);
+
+		if (window.YT && window.YT.Player) {
+			video.value.player = new window.YT.Player(
+				`editSongPlayer-${props.modalUuid}`,
+				{
+					height: 298,
+					width: 530,
+					videoId: null,
+					host: "https://www.youtube-nocookie.com",
+					playerVars: {
+						controls: 0,
+						iv_load_policy: 3,
+						rel: 0,
+						showinfo: 0,
+						autoplay: 0
+					},
+					startSeconds: song.value.skipDuration,
+					events: {
+						onReady: () => {
+							let volume = parseFloat(
+								localStorage.getItem("volume")
+							);
+							volume = typeof volume === "number" ? volume : 20;
+							video.value.player.setVolume(volume);
+							if (volume > 0) video.value.player.unMute();
 
-		socket.on(
-			`event:admin.song.updated`,
-			res => {
-				const index = items.value
-					.map(item => item.song.youtubeId)
-					.indexOf(res.data.song.youtubeId);
-				if (index >= 0)
-					items.value[index].song = {
-						...items.value[index].song,
-						...res.data.song,
-						updated: true
-					};
-			},
-			{ modalUuid: props.modalUuid }
-		);
+							playerReady.value = true;
 
-		socket.on(
-			`event:admin.song.removed`,
-			res => {
-				const index = items.value
-					.map(item => item.song._id)
-					.indexOf(res.data.songId);
-				if (index >= 0) items.value[index].song.removed = true;
-			},
-			{ modalUuid: props.modalUuid }
-		);
+							if (song.value && song.value.youtubeId)
+								video.value.player.cueVideoById(
+									song.value.youtubeId,
+									song.value.skipDuration
+								);
+
+							setPlaybackRate(null);
+
+							drawCanvas();
+						},
+						onStateChange: event => {
+							drawCanvas();
+
+							if (event.data === 1) {
+								video.value.paused = false;
+								updateMediaModalPlayingAudio(true);
+								let youtubeDuration =
+									video.value.player.getDuration();
+								const newYoutubeVideoDuration =
+									youtubeDuration.toFixed(3);
+
+								if (
+									youtubeVideoDuration.value.indexOf(
+										".000"
+									) !== -1 &&
+									`${youtubeVideoDuration.value}` !==
+										`${newYoutubeVideoDuration}`
+								) {
+									const songDurationNumber = Number(
+										song.value.duration
+									);
+									const songDurationNumber2 =
+										Number(song.value.duration) + 1;
+									const songDurationNumber3 =
+										Number(song.value.duration) - 1;
+									const fixedSongDuration =
+										songDurationNumber.toFixed(3);
+									const fixedSongDuration2 =
+										songDurationNumber2.toFixed(3);
+									const fixedSongDuration3 =
+										songDurationNumber3.toFixed(3);
+
+									if (
+										`${youtubeVideoDuration.value}` ===
+											`${Number(
+												song.value.duration
+											).toFixed(3)}` &&
+										(fixedSongDuration ===
+											youtubeVideoDuration.value ||
+											fixedSongDuration2 ===
+												youtubeVideoDuration.value ||
+											fixedSongDuration3 ===
+												youtubeVideoDuration.value)
+									)
+										song.value.duration =
+											newYoutubeVideoDuration;
+
+									youtubeVideoDuration.value =
+										newYoutubeVideoDuration;
+									if (
+										youtubeVideoDuration.value.indexOf(
+											".000"
+										) !== -1
+									)
+										youtubeVideoNote.value = "(~)";
+									else youtubeVideoNote.value = "";
+								}
+
+								if (song.value.duration === -1)
+									song.value.duration = Number.parseInt(
+										youtubeVideoDuration.value
+									);
+
+								youtubeDuration -= song.value.skipDuration;
+								if (song.value.duration > youtubeDuration + 1) {
+									stopVideo();
+									pauseVideo(true);
+
+									return new Toast(
+										"Video can't play. Specified duration is bigger than the YouTube song duration."
+									);
+								}
+								if (song.value.duration <= 0) {
+									stopVideo();
+									pauseVideo(true);
+
+									return new Toast(
+										"Video can't play. Specified duration has to be more than 0 seconds."
+									);
+								}
+
+								if (
+									video.value.player.getCurrentTime() <
+									song.value.skipDuration
+								) {
+									return seekTo(song.value.skipDuration);
+								}
+
+								setPlaybackRate(null);
+							} else if (event.data === 2) {
+								video.value.paused = true;
+								updateMediaModalPlayingAudio(false);
+							}
+
+							return false;
+						}
+					}
+				}
+			);
+		} else {
+			youtubeError.value = true;
+			youtubeErrorMessage.value = "Player could not be loaded.";
+		}
+
+		["artists", "genres", "tags"].forEach(type => {
+			socket.dispatch(
+				`songs.get${type.charAt(0).toUpperCase()}${type.slice(1)}`,
+				res => {
+					if (res.status === "success") {
+						const { items } = res.data;
+						if (type === "genres")
+							autosuggest.value.allItems[type] = Array.from(
+								new Set([...recommendedGenres.value, ...items])
+							);
+						else autosuggest.value.allItems[type] = items;
+					} else {
+						new Toast(res.message);
+					}
+				}
+			);
+		});
 
 		socket.on(
-			`event:admin.youtubeVideo.removed`,
+			"event:admin.song.removed",
 			res => {
-				const index = items.value
-					.map(item => item.song.youtubeVideoId)
-					.indexOf(res.videoId);
-				if (index >= 0) items.value[index].song.removed = true;
+				if (res.data.songId === song.value._id) {
+					songDeleted.value = true;
+				}
 			},
 			{ modalUuid: props.modalUuid }
 		);
-	}
+
+		if (bulk.value) {
+			socket.dispatch("apis.joinRoom", "edit-songs");
+
+			socket.dispatch(
+				"songs.getSongsFromYoutubeIds",
+				youtubeIds.value,
+				res => {
+					if (res.data.songs.length === 0) {
+						modalsStore.closeCurrentModal();
+						new Toast("You can't edit 0 songs.");
+					} else {
+						items.value = res.data.songs.map(song => ({
+							status: "todo",
+							flagged: false,
+							song
+						}));
+						editNextSong();
+					}
+				}
+			);
+
+			socket.on(
+				`event:admin.song.created`,
+				res => {
+					const index = items.value
+						.map(item => item.song.youtubeId)
+						.indexOf(res.data.song.youtubeId);
+					if (index >= 0)
+						items.value[index].song = {
+							...items.value[index].song,
+							...res.data.song,
+							created: true
+						};
+				},
+				{ modalUuid: props.modalUuid }
+			);
+
+			socket.on(
+				`event:admin.song.updated`,
+				res => {
+					const index = items.value
+						.map(item => item.song.youtubeId)
+						.indexOf(res.data.song.youtubeId);
+					if (index >= 0)
+						items.value[index].song = {
+							...items.value[index].song,
+							...res.data.song,
+							updated: true
+						};
+				},
+				{ modalUuid: props.modalUuid }
+			);
+
+			socket.on(
+				`event:admin.song.removed`,
+				res => {
+					const index = items.value
+						.map(item => item.song._id)
+						.indexOf(res.data.songId);
+					if (index >= 0) items.value[index].song.removed = true;
+				},
+				{ modalUuid: props.modalUuid }
+			);
+
+			socket.on(
+				`event:admin.youtubeVideo.removed`,
+				res => {
+					const index = items.value
+						.map(item => item.song.youtubeVideoId)
+						.indexOf(res.videoId);
+					if (index >= 0) items.value[index].song.removed = true;
+				},
+				{ modalUuid: props.modalUuid }
+			);
+		}
+
+		return null;
+	});
+
+	let volume = parseFloat(localStorage.getItem("volume"));
+	volume = typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
+	localStorage.setItem("volume", `${volume}`);
+	volumeSliderValue.value = volume;
 
 	keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
 		keyCode: 101,

+ 5 - 7
frontend/src/components/modals/ImportAlbum.vue

@@ -71,10 +71,6 @@ const showDiscogsTab = tab => {
 	return importAlbumStore.showDiscogsTab(tab);
 };
 
-const init = () => {
-	socket.dispatch("apis.joinRoom", "import-album");
-};
-
 const startEditingSongs = () => {
 	songsToEdit.value = [];
 	trackSongs.value.forEach((songs, index) => {
@@ -331,10 +327,12 @@ const updateTrackSong = updatedSong => {
 };
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch("apis.joinRoom", "import-album");
 
-	socket.on("event:admin.song.updated", res => {
-		updateTrackSong(res.data.song);
+		socket.on("event:admin.song.updated", res => {
+			updateTrackSong(res.data.song);
+		});
 	});
 });
 

+ 261 - 238
frontend/src/components/modals/ManageStation/index.vue

@@ -147,276 +147,299 @@ watch(
 );
 
 onMounted(() => {
-	socket.dispatch(`stations.getStationById`, stationId.value, async res => {
-		if (res.status === "success") {
-			editStation(res.data.station);
+	socket.onConnect(() => {
+		socket.dispatch(
+			`stations.getStationById`,
+			stationId.value,
+			async res => {
+				if (res.status === "success") {
+					editStation(res.data.station);
 
-			await updatePermissions();
+					await updatePermissions();
 
-			findTabOrClose();
+					findTabOrClose();
 
-			const currentSong = res.data.station.currentSong
-				? res.data.station.currentSong
-				: {};
+					const currentSong = res.data.station.currentSong
+						? res.data.station.currentSong
+						: {};
 
-			updateCurrentSong(currentSong);
+					updateCurrentSong(currentSong);
 
-			updateStationPaused(res.data.station.paused);
+					updateStationPaused(res.data.station.paused);
 
-			socket.dispatch(
-				"stations.getStationAutofillPlaylistsById",
-				stationId.value,
-				res => {
-					if (res.status === "success")
-						setAutofillPlaylists(res.data.playlists);
-				}
-			);
-
-			socket.dispatch(
-				"stations.getStationBlacklistById",
-				stationId.value,
-				res => {
-					if (res.status === "success")
-						setBlacklist(res.data.playlists);
-				}
-			);
-
-			if (hasPermission("stations.view")) {
-				socket.dispatch(
-					"playlists.getPlaylistForStation",
-					stationId.value,
-					true,
-					res => {
-						if (res.status === "success") {
-							updateStationPlaylist(res.data.playlist);
+					socket.dispatch(
+						"stations.getStationAutofillPlaylistsById",
+						stationId.value,
+						res => {
+							if (res.status === "success")
+								setAutofillPlaylists(res.data.playlists);
+						}
+					);
+
+					socket.dispatch(
+						"stations.getStationBlacklistById",
+						stationId.value,
+						res => {
+							if (res.status === "success")
+								setBlacklist(res.data.playlists);
 						}
+					);
+
+					if (hasPermission("stations.view")) {
+						socket.dispatch(
+							"playlists.getPlaylistForStation",
+							stationId.value,
+							true,
+							res => {
+								if (res.status === "success") {
+									updateStationPlaylist(res.data.playlist);
+								}
+							}
+						);
 					}
-				);
+
+					socket.dispatch(
+						"stations.getQueue",
+						stationId.value,
+						res => {
+							if (res.status === "success")
+								updateSongsList(res.data.queue);
+						}
+					);
+
+					socket.dispatch(
+						"apis.joinRoom",
+						`manage-station.${stationId.value}`
+					);
+
+					socket.on(
+						"event:station.updated",
+						res => {
+							updateStation(res.data.station);
+						},
+						{ modalUuid: props.modalUuid }
+					);
+
+					socket.on(
+						"event:station.autofillPlaylist",
+						res => {
+							const { playlist } = res.data;
+							const playlistIndex = autofill.value
+								.map(autofillPlaylist => autofillPlaylist._id)
+								.indexOf(playlist._id);
+							if (playlistIndex === -1)
+								autofill.value.push(playlist);
+						},
+						{ modalUuid: props.modalUuid }
+					);
+
+					socket.on(
+						"event:station.blacklistedPlaylist",
+						res => {
+							const { playlist } = res.data;
+							const playlistIndex = blacklist.value
+								.map(
+									blacklistedPlaylist =>
+										blacklistedPlaylist._id
+								)
+								.indexOf(playlist._id);
+							if (playlistIndex === -1)
+								blacklist.value.push(playlist);
+						},
+						{ modalUuid: props.modalUuid }
+					);
+
+					socket.on(
+						"event:station.removedAutofillPlaylist",
+						res => {
+							const { playlistId } = res.data;
+							const playlistIndex = autofill.value
+								.map(playlist => playlist._id)
+								.indexOf(playlistId);
+							if (playlistIndex >= 0)
+								autofill.value.splice(playlistIndex, 1);
+						},
+						{ modalUuid: props.modalUuid }
+					);
+
+					socket.on(
+						"event:station.removedBlacklistedPlaylist",
+						res => {
+							const { playlistId } = res.data;
+							const playlistIndex = blacklist.value
+								.map(playlist => playlist._id)
+								.indexOf(playlistId);
+							if (playlistIndex >= 0)
+								blacklist.value.splice(playlistIndex, 1);
+						},
+						{ modalUuid: props.modalUuid }
+					);
+
+					socket.on(
+						"event:station.deleted",
+						() => {
+							new Toast(
+								`The station you were editing was deleted.`
+							);
+							closeCurrentModal();
+						},
+						{ modalUuid: props.modalUuid }
+					);
+
+					socket.on(
+						"event:user.station.favorited",
+						res => {
+							if (res.data.stationId === stationId.value)
+								updateIsFavorited(true);
+						},
+						{ modalUuid: props.modalUuid }
+					);
+
+					socket.on(
+						"event:user.station.unfavorited",
+						res => {
+							if (res.data.stationId === stationId.value)
+								updateIsFavorited(false);
+						},
+						{ modalUuid: props.modalUuid }
+					);
+				} else {
+					new Toast(`Station with that ID not found`);
+					closeCurrentModal();
+				}
 			}
+		);
 
-			socket.dispatch("stations.getQueue", stationId.value, res => {
-				if (res.status === "success") updateSongsList(res.data.queue);
-			});
+		socket.on(
+			"event:manageStation.queue.updated",
+			res => {
+				if (res.data.stationId === stationId.value)
+					updateSongsList(res.data.queue);
+			},
+			{ modalUuid: props.modalUuid }
+		);
 
-			socket.dispatch(
-				"apis.joinRoom",
-				`manage-station.${stationId.value}`
-			);
+		socket.on(
+			"event:manageStation.queue.song.repositioned",
+			res => {
+				if (res.data.stationId === stationId.value)
+					repositionSongInList(res.data.song);
+			},
+			{ modalUuid: props.modalUuid }
+		);
 
-			socket.on(
-				"event:station.updated",
-				res => {
-					updateStation(res.data.station);
-				},
-				{ modalUuid: props.modalUuid }
-			);
+		socket.on(
+			"event:station.pause",
+			res => {
+				if (res.data.stationId === stationId.value)
+					updateStationPaused(true);
+			},
+			{ modalUuid: props.modalUuid }
+		);
 
-			socket.on(
-				"event:station.autofillPlaylist",
-				res => {
-					const { playlist } = res.data;
-					const playlistIndex = autofill.value
-						.map(autofillPlaylist => autofillPlaylist._id)
-						.indexOf(playlist._id);
-					if (playlistIndex === -1) autofill.value.push(playlist);
-				},
-				{ modalUuid: props.modalUuid }
-			);
+		socket.on(
+			"event:station.resume",
+			res => {
+				if (res.data.stationId === stationId.value)
+					updateStationPaused(false);
+			},
+			{ modalUuid: props.modalUuid }
+		);
 
-			socket.on(
-				"event:station.blacklistedPlaylist",
-				res => {
-					const { playlist } = res.data;
-					const playlistIndex = blacklist.value
-						.map(blacklistedPlaylist => blacklistedPlaylist._id)
-						.indexOf(playlist._id);
-					if (playlistIndex === -1) blacklist.value.push(playlist);
-				},
-				{ modalUuid: props.modalUuid }
-			);
+		socket.on(
+			"event:station.nextSong",
+			res => {
+				if (res.data.stationId === stationId.value)
+					updateCurrentSong(res.data.currentSong || {});
+			},
+			{ modalUuid: props.modalUuid }
+		);
 
-			socket.on(
-				"event:station.removedAutofillPlaylist",
-				res => {
-					const { playlistId } = res.data;
-					const playlistIndex = autofill.value
-						.map(playlist => playlist._id)
-						.indexOf(playlistId);
-					if (playlistIndex >= 0)
-						autofill.value.splice(playlistIndex, 1);
-				},
-				{ modalUuid: props.modalUuid }
-			);
+		socket.on("event:manageStation.djs.added", res => {
+			if (res.data.stationId === stationId.value) {
+				if (res.data.user._id === userId.value) updatePermissions();
+				addDj(res.data.user);
+			}
+		});
 
-			socket.on(
-				"event:station.removedBlacklistedPlaylist",
-				res => {
-					const { playlistId } = res.data;
-					const playlistIndex = blacklist.value
-						.map(playlist => playlist._id)
-						.indexOf(playlistId);
-					if (playlistIndex >= 0)
-						blacklist.value.splice(playlistIndex, 1);
-				},
-				{ modalUuid: props.modalUuid }
-			);
+		socket.on("event:manageStation.djs.removed", res => {
+			if (res.data.stationId === stationId.value) {
+				if (res.data.user._id === userId.value) updatePermissions();
+				removeDj(res.data.user);
+			}
+		});
+
+		socket.on("keep.event:user.role.updated", () => {
+			updatePermissions();
+		});
 
+		if (hasPermission("stations.view")) {
 			socket.on(
-				"event:station.deleted",
-				() => {
-					new Toast(`The station you were editing was deleted.`);
-					closeCurrentModal();
+				"event:playlist.song.added",
+				res => {
+					if (stationPlaylist.value._id === res.data.playlistId)
+						stationPlaylist.value.songs.push(res.data.song);
 				},
-				{ modalUuid: props.modalUuid }
+				{
+					modalUuid: props.modalUuid
+				}
 			);
 
 			socket.on(
-				"event:user.station.favorited",
+				"event:playlist.song.removed",
 				res => {
-					if (res.data.stationId === stationId.value)
-						updateIsFavorited(true);
+					if (stationPlaylist.value._id === res.data.playlistId) {
+						// remove song from array of playlists
+						stationPlaylist.value.songs.forEach((song, index) => {
+							if (song.youtubeId === res.data.youtubeId)
+								stationPlaylist.value.songs.splice(index, 1);
+						});
+					}
 				},
-				{ modalUuid: props.modalUuid }
+				{
+					modalUuid: props.modalUuid
+				}
 			);
 
 			socket.on(
-				"event:user.station.unfavorited",
+				"event:playlist.songs.repositioned",
 				res => {
-					if (res.data.stationId === stationId.value)
-						updateIsFavorited(false);
+					if (stationPlaylist.value._id === res.data.playlistId) {
+						// for each song that has a new position
+						res.data.songsBeingChanged.forEach(changedSong => {
+							stationPlaylist.value.songs.forEach(
+								(song, index) => {
+									// find song locally
+									if (
+										song.youtubeId === changedSong.youtubeId
+									) {
+										// change song position attribute
+										stationPlaylist.value.songs[
+											index
+										].position = changedSong.position;
+
+										// reposition in array if needed
+										if (index !== changedSong.position - 1)
+											stationPlaylist.value.songs.splice(
+												changedSong.position - 1,
+												0,
+												stationPlaylist.value.songs.splice(
+													index,
+													1
+												)[0]
+											);
+									}
+								}
+							);
+						});
+					}
 				},
-				{ modalUuid: props.modalUuid }
+				{
+					modalUuid: props.modalUuid
+				}
 			);
-		} else {
-			new Toast(`Station with that ID not found`);
-			closeCurrentModal();
-		}
-	});
-
-	socket.on(
-		"event:manageStation.queue.updated",
-		res => {
-			if (res.data.stationId === stationId.value)
-				updateSongsList(res.data.queue);
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:manageStation.queue.song.repositioned",
-		res => {
-			if (res.data.stationId === stationId.value)
-				repositionSongInList(res.data.song);
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:station.pause",
-		res => {
-			if (res.data.stationId === stationId.value)
-				updateStationPaused(true);
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:station.resume",
-		res => {
-			if (res.data.stationId === stationId.value)
-				updateStationPaused(false);
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on(
-		"event:station.nextSong",
-		res => {
-			if (res.data.stationId === stationId.value)
-				updateCurrentSong(res.data.currentSong || {});
-		},
-		{ modalUuid: props.modalUuid }
-	);
-
-	socket.on("event:manageStation.djs.added", res => {
-		if (res.data.stationId === stationId.value) {
-			if (res.data.user._id === userId.value) updatePermissions();
-			addDj(res.data.user);
-		}
-	});
-
-	socket.on("event:manageStation.djs.removed", res => {
-		if (res.data.stationId === stationId.value) {
-			if (res.data.user._id === userId.value) updatePermissions();
-			removeDj(res.data.user);
 		}
 	});
-
-	socket.on("keep.event:user.role.updated", () => {
-		updatePermissions();
-	});
-
-	if (hasPermission("stations.view")) {
-		socket.on(
-			"event:playlist.song.added",
-			res => {
-				if (stationPlaylist.value._id === res.data.playlistId)
-					stationPlaylist.value.songs.push(res.data.song);
-			},
-			{
-				modalUuid: props.modalUuid
-			}
-		);
-
-		socket.on(
-			"event:playlist.song.removed",
-			res => {
-				if (stationPlaylist.value._id === res.data.playlistId) {
-					// remove song from array of playlists
-					stationPlaylist.value.songs.forEach((song, index) => {
-						if (song.youtubeId === res.data.youtubeId)
-							stationPlaylist.value.songs.splice(index, 1);
-					});
-				}
-			},
-			{
-				modalUuid: props.modalUuid
-			}
-		);
-
-		socket.on(
-			"event:playlist.songs.repositioned",
-			res => {
-				if (stationPlaylist.value._id === res.data.playlistId) {
-					// for each song that has a new position
-					res.data.songsBeingChanged.forEach(changedSong => {
-						stationPlaylist.value.songs.forEach((song, index) => {
-							// find song locally
-							if (song.youtubeId === changedSong.youtubeId) {
-								// change song position attribute
-								stationPlaylist.value.songs[index].position =
-									changedSong.position;
-
-								// reposition in array if needed
-								if (index !== changedSong.position - 1)
-									stationPlaylist.value.songs.splice(
-										changedSong.position - 1,
-										0,
-										stationPlaylist.value.songs.splice(
-											index,
-											1
-										)[0]
-									);
-							}
-						});
-					});
-				}
-			},
-			{
-				modalUuid: props.modalUuid
-			}
-		);
-	}
 });
 
 onBeforeUnmount(() => {

+ 23 - 22
frontend/src/components/modals/Report.vue

@@ -145,27 +145,6 @@ const predefinedCategories = ref([
 	}
 ]);
 
-const init = () => {
-	socket.dispatch("reports.myReportsForSong", song.value._id, res => {
-		if (res.status === "success") {
-			existingReports.value = res.data.reports;
-			existingReports.value.forEach(report =>
-				socket.dispatch("apis.joinRoom", `view-report.${report._id}`)
-			);
-		}
-	});
-
-	socket.on(
-		"event:admin.report.resolved",
-		res => {
-			existingReports.value = existingReports.value.filter(
-				report => report._id !== res.data.reportId
-			);
-		},
-		{ modalUuid: props.modalUuid }
-	);
-};
-
 const create = () => {
 	const issues = [];
 
@@ -203,7 +182,29 @@ const create = () => {
 };
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch("reports.myReportsForSong", song.value._id, res => {
+			if (res.status === "success") {
+				existingReports.value = res.data.reports;
+				existingReports.value.forEach(report =>
+					socket.dispatch(
+						"apis.joinRoom",
+						`view-report.${report._id}`
+					)
+				);
+			}
+		});
+
+		socket.on(
+			"event:admin.report.resolved",
+			res => {
+				existingReports.value = existingReports.value.filter(
+					report => report._id !== res.data.reportId
+				);
+			},
+			{ modalUuid: props.modalUuid }
+		);
+	});
 });
 
 onBeforeUnmount(() => {

+ 27 - 29
frontend/src/components/modals/ViewApiRequest.vue

@@ -27,34 +27,6 @@ const { closeCurrentModal } = useModalsStore();
 
 const loaded = ref(false);
 
-const init = () => {
-	loaded.value = false;
-	socket.dispatch("youtube.getApiRequest", requestId.value, res => {
-		if (res.status === "success") {
-			const { apiRequest } = res.data;
-			viewApiRequest(apiRequest);
-			loaded.value = true;
-
-			socket.dispatch(
-				"apis.joinRoom",
-				`view-api-request.${requestId.value}`
-			);
-
-			socket.on(
-				"event:youtubeApiRequest.removed",
-				() => {
-					new Toast("This API request was removed.");
-					closeCurrentModal();
-				},
-				{ modalUuid: props.modalUuid }
-			);
-		} else {
-			new Toast("API request with that ID not found");
-			closeCurrentModal();
-		}
-	});
-};
-
 const remove = () => {
 	if (removeAction.value)
 		socket.dispatch(removeAction.value, requestId.value, res => {
@@ -68,7 +40,33 @@ const remove = () => {
 };
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		loaded.value = false;
+		socket.dispatch("youtube.getApiRequest", requestId.value, res => {
+			if (res.status === "success") {
+				const { apiRequest } = res.data;
+				viewApiRequest(apiRequest);
+				loaded.value = true;
+
+				socket.dispatch(
+					"apis.joinRoom",
+					`view-api-request.${requestId.value}`
+				);
+
+				socket.on(
+					"event:youtubeApiRequest.removed",
+					() => {
+						new Toast("This API request was removed.");
+						closeCurrentModal();
+					},
+					{ modalUuid: props.modalUuid }
+				);
+			} else {
+				new Toast("API request with that ID not found");
+				closeCurrentModal();
+			}
+		});
+	});
 });
 
 onBeforeUnmount(() => {

+ 23 - 25
frontend/src/components/modals/ViewPunishment.vue

@@ -23,30 +23,6 @@ const { viewPunishment } = viewPunishmentStore;
 
 const { closeCurrentModal } = useModalsStore();
 
-const init = () => {
-	socket.dispatch(`punishments.findOne`, punishmentId.value, res => {
-		if (res.status === "success") {
-			viewPunishment(res.data.punishment);
-
-			socket.dispatch(
-				"apis.joinRoom",
-				`view-punishment.${punishmentId.value}`
-			);
-
-			socket.on(
-				"event:admin.punishment.updated",
-				({ data }) => {
-					punishment.value = data.punishment;
-				},
-				{ modalUuid: props.modalUuid }
-			);
-		} else {
-			new Toast("Punishment with that ID not found");
-			closeCurrentModal();
-		}
-	});
-};
-
 const deactivatePunishment = event => {
 	event.preventDefault();
 	socket.dispatch(
@@ -63,7 +39,29 @@ const deactivatePunishment = event => {
 };
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch(`punishments.findOne`, punishmentId.value, res => {
+			if (res.status === "success") {
+				viewPunishment(res.data.punishment);
+
+				socket.dispatch(
+					"apis.joinRoom",
+					`view-punishment.${punishmentId.value}`
+				);
+
+				socket.on(
+					"event:admin.punishment.updated",
+					({ data }) => {
+						punishment.value = data.punishment;
+					},
+					{ modalUuid: props.modalUuid }
+				);
+			} else {
+				new Toast("Punishment with that ID not found");
+				closeCurrentModal();
+			}
+		});
+	});
 });
 
 onBeforeUnmount(() => {

+ 61 - 52
frontend/src/components/modals/ViewReport.vue

@@ -53,57 +53,6 @@ const icons = ref({
 const report = ref(<Report>{});
 const song = ref();
 
-const init = () => {
-	socket.dispatch("reports.findOne", reportId.value, res => {
-		if (res.status === "success") {
-			report.value = res.data.report;
-
-			socket.dispatch("apis.joinRoom", `view-report.${reportId.value}`);
-
-			socket.dispatch(
-				"songs.getSongFromSongId",
-				report.value.song._id,
-				res => {
-					if (res.status === "success") song.value = res.data.song;
-					else {
-						new Toast("Cannot find the report's associated song");
-						closeCurrentModal();
-					}
-				}
-			);
-
-			socket.on(
-				"event:admin.report.resolved",
-				res => {
-					report.value.resolved = res.data.resolved;
-				},
-				{ modalUuid: props.modalUuid }
-			);
-
-			socket.on("event:admin.report.removed", () => closeCurrentModal(), {
-				modalUuid: props.modalUuid
-			});
-
-			socket.on(
-				"event:admin.report.issue.toggled",
-				res => {
-					if (reportId.value === res.data.reportId) {
-						const issue = report.value.issues.find(
-							issue => issue._id.toString() === res.data.issueId
-						);
-
-						issue.resolved = res.data.resolved;
-					}
-				},
-				{ modalUuid: props.modalUuid }
-			);
-		} else {
-			new Toast("Report with that ID not found");
-			closeCurrentModal();
-		}
-	});
-};
-
 const resolve = value =>
 	resolveReport({ reportId: reportId.value, value })
 		.then((res: any) => {
@@ -139,7 +88,67 @@ watch(
 );
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch("reports.findOne", reportId.value, res => {
+			if (res.status === "success") {
+				report.value = res.data.report;
+
+				socket.dispatch(
+					"apis.joinRoom",
+					`view-report.${reportId.value}`
+				);
+
+				socket.dispatch(
+					"songs.getSongFromSongId",
+					report.value.song._id,
+					res => {
+						if (res.status === "success")
+							song.value = res.data.song;
+						else {
+							new Toast(
+								"Cannot find the report's associated song"
+							);
+							closeCurrentModal();
+						}
+					}
+				);
+
+				socket.on(
+					"event:admin.report.resolved",
+					res => {
+						report.value.resolved = res.data.resolved;
+					},
+					{ modalUuid: props.modalUuid }
+				);
+
+				socket.on(
+					"event:admin.report.removed",
+					() => closeCurrentModal(),
+					{
+						modalUuid: props.modalUuid
+					}
+				);
+
+				socket.on(
+					"event:admin.report.issue.toggled",
+					res => {
+						if (reportId.value === res.data.reportId) {
+							const issue = report.value.issues.find(
+								issue =>
+									issue._id.toString() === res.data.issueId
+							);
+
+							issue.resolved = res.data.resolved;
+						}
+					},
+					{ modalUuid: props.modalUuid }
+				);
+			} else {
+				new Toast("Report with that ID not found");
+				closeCurrentModal();
+			}
+		});
+	});
 });
 
 onBeforeUnmount(() => {

+ 235 - 217
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -217,246 +217,264 @@ const sendActivityWatchVideoData = () => {
 	}
 };
 
-const init = () => {
-	loaded.value = false;
-	socket.dispatch(
-		"youtube.getVideo",
-		videoId.value || youtubeId.value,
-		true,
-		res => {
-			if (res.status === "success") {
-				const youtubeVideo = res.data;
-				viewYoutubeVideo(youtubeVideo);
-				loaded.value = true;
-
-				interval.value = setInterval(() => {
-					if (
-						video.value.duration !== -1 &&
-						player.value.paused === false &&
-						player.value.playerReady &&
-						(player.value.player.getCurrentTime() >
-							video.value.duration ||
-							(player.value.player.getCurrentTime() > 0 &&
-								player.value.player.getCurrentTime() >=
-									player.value.player.getDuration()))
-					) {
-						stopVideo();
-						pauseVideo(true);
-						drawCanvas();
-					}
-					if (
-						player.value.playerReady &&
-						player.value.player.getVideoData &&
-						player.value.player.getVideoData() &&
-						player.value.player.getVideoData().video_id ===
-							video.value.youtubeId
-					) {
-						const currentTime =
-							player.value.player.getCurrentTime();
-
-						if (currentTime !== undefined)
-							player.value.currentTime = currentTime.toFixed(3);
-
-						if (player.value.duration.indexOf(".000") !== -1) {
-							const duration = player.value.player.getDuration();
-
-							if (duration !== undefined) {
-								if (
-									`${player.value.duration}` ===
-									`${Number(video.value.duration).toFixed(3)}`
-								)
-									video.value.duration = duration.toFixed(3);
-
-								player.value.duration = duration.toFixed(3);
-								if (
-									player.value.duration.indexOf(".000") !== -1
-								)
-									player.value.videoNote = "(~)";
-								else player.value.videoNote = "";
-
-								drawCanvas();
+onMounted(() => {
+	socket.onConnect(() => {
+		loaded.value = false;
+		socket.dispatch(
+			"youtube.getVideo",
+			videoId.value || youtubeId.value,
+			true,
+			res => {
+				if (res.status === "success") {
+					const youtubeVideo = res.data;
+					viewYoutubeVideo(youtubeVideo);
+					loaded.value = true;
+
+					interval.value = setInterval(() => {
+						if (
+							video.value.duration !== -1 &&
+							player.value.paused === false &&
+							player.value.playerReady &&
+							(player.value.player.getCurrentTime() >
+								video.value.duration ||
+								(player.value.player.getCurrentTime() > 0 &&
+									player.value.player.getCurrentTime() >=
+										player.value.player.getDuration()))
+						) {
+							stopVideo();
+							pauseVideo(true);
+							drawCanvas();
+						}
+						if (
+							player.value.playerReady &&
+							player.value.player.getVideoData &&
+							player.value.player.getVideoData() &&
+							player.value.player.getVideoData().video_id ===
+								video.value.youtubeId
+						) {
+							const currentTime =
+								player.value.player.getCurrentTime();
+
+							if (currentTime !== undefined)
+								player.value.currentTime =
+									currentTime.toFixed(3);
+
+							if (player.value.duration.indexOf(".000") !== -1) {
+								const duration =
+									player.value.player.getDuration();
+
+								if (duration !== undefined) {
+									if (
+										`${player.value.duration}` ===
+										`${Number(video.value.duration).toFixed(
+											3
+										)}`
+									)
+										video.value.duration =
+											duration.toFixed(3);
+
+									player.value.duration = duration.toFixed(3);
+									if (
+										player.value.duration.indexOf(
+											".000"
+										) !== -1
+									)
+										player.value.videoNote = "(~)";
+									else player.value.videoNote = "";
+
+									drawCanvas();
+								}
 							}
 						}
-					}
 
-					if (player.value.paused === false) drawCanvas();
-				}, 200);
-
-				activityWatchVideoDataInterval.value = setInterval(() => {
-					sendActivityWatchVideoData();
-				}, 1000);
-
-				if (window.YT && window.YT.Player) {
-					player.value.player = new window.YT.Player(
-						`viewYoutubeVideoPlayer-${props.modalUuid}`,
-						{
-							height: 298,
-							width: 530,
-							videoId: null,
-							host: "https://www.youtube-nocookie.com",
-							playerVars: {
-								controls: 0,
-								iv_load_policy: 3,
-								rel: 0,
-								showinfo: 0,
-								autoplay: 0
-							},
-							events: {
-								onReady: () => {
-									let volume = parseFloat(
-										localStorage.getItem("volume")
-									);
-									volume =
-										typeof volume === "number"
-											? volume
-											: 20;
-									player.value.player.setVolume(volume);
-									if (volume > 0)
-										player.value.player.unMute();
-
-									player.value.playerReady = true;
-
-									if (video.value && video.value._id)
-										player.value.player.cueVideoById(
-											video.value.youtubeId
+						if (player.value.paused === false) drawCanvas();
+					}, 200);
+
+					activityWatchVideoDataInterval.value = setInterval(() => {
+						sendActivityWatchVideoData();
+					}, 1000);
+
+					if (window.YT && window.YT.Player) {
+						player.value.player = new window.YT.Player(
+							`viewYoutubeVideoPlayer-${props.modalUuid}`,
+							{
+								height: 298,
+								width: 530,
+								videoId: null,
+								host: "https://www.youtube-nocookie.com",
+								playerVars: {
+									controls: 0,
+									iv_load_policy: 3,
+									rel: 0,
+									showinfo: 0,
+									autoplay: 0
+								},
+								events: {
+									onReady: () => {
+										let volume = parseFloat(
+											localStorage.getItem("volume")
 										);
+										volume =
+											typeof volume === "number"
+												? volume
+												: 20;
+										player.value.player.setVolume(volume);
+										if (volume > 0)
+											player.value.player.unMute();
+
+										player.value.playerReady = true;
+
+										if (video.value && video.value._id)
+											player.value.player.cueVideoById(
+												video.value.youtubeId
+											);
 
-									setPlaybackRate(null);
+										setPlaybackRate(null);
 
-									drawCanvas();
-								},
-								onStateChange: event => {
-									drawCanvas();
+										drawCanvas();
+									},
+									onStateChange: event => {
+										drawCanvas();
 
-									if (event.data === 1) {
-										player.value.paused = false;
-										updateMediaModalPlayingAudio(true);
-										const youtubeDuration =
-											player.value.player.getDuration();
-										const newYoutubeVideoDuration =
-											youtubeDuration.toFixed(3);
-
-										if (
-											player.value.duration.indexOf(
-												".000"
-											) !== -1 &&
-											`${player.value.duration}` !==
-												`${newYoutubeVideoDuration}`
-										) {
-											const songDurationNumber = Number(
-												video.value.duration
-											);
-											const songDurationNumber2 =
-												Number(video.value.duration) +
-												1;
-											const songDurationNumber3 =
-												Number(video.value.duration) -
-												1;
-											const fixedSongDuration =
-												songDurationNumber.toFixed(3);
-											const fixedSongDuration2 =
-												songDurationNumber2.toFixed(3);
-											const fixedSongDuration3 =
-												songDurationNumber3.toFixed(3);
+										if (event.data === 1) {
+											player.value.paused = false;
+											updateMediaModalPlayingAudio(true);
+											const youtubeDuration =
+												player.value.player.getDuration();
+											const newYoutubeVideoDuration =
+												youtubeDuration.toFixed(3);
 
 											if (
-												`${player.value.duration}` ===
-													`${Number(
+												player.value.duration.indexOf(
+													".000"
+												) !== -1 &&
+												`${player.value.duration}` !==
+													`${newYoutubeVideoDuration}`
+											) {
+												const songDurationNumber =
+													Number(
+														video.value.duration
+													);
+												const songDurationNumber2 =
+													Number(
+														video.value.duration
+													) + 1;
+												const songDurationNumber3 =
+													Number(
 														video.value.duration
-													).toFixed(3)}` &&
-												(fixedSongDuration ===
-													player.value.duration ||
-													fixedSongDuration2 ===
+													) - 1;
+												const fixedSongDuration =
+													songDurationNumber.toFixed(
+														3
+													);
+												const fixedSongDuration2 =
+													songDurationNumber2.toFixed(
+														3
+													);
+												const fixedSongDuration3 =
+													songDurationNumber3.toFixed(
+														3
+													);
+
+												if (
+													`${player.value.duration}` ===
+														`${Number(
+															video.value.duration
+														).toFixed(3)}` &&
+													(fixedSongDuration ===
 														player.value.duration ||
-													fixedSongDuration3 ===
-														player.value.duration)
-											)
-												video.value.duration =
+														fixedSongDuration2 ===
+															player.value
+																.duration ||
+														fixedSongDuration3 ===
+															player.value
+																.duration)
+												)
+													video.value.duration =
+														newYoutubeVideoDuration;
+
+												player.value.duration =
 													newYoutubeVideoDuration;
+												if (
+													player.value.duration.indexOf(
+														".000"
+													) !== -1
+												)
+													player.value.videoNote =
+														"(~)";
+												else
+													player.value.videoNote = "";
+											}
+
+											if (video.value.duration === -1)
+												video.value.duration = Number(
+													player.value.duration
+												);
 
-											player.value.duration =
-												newYoutubeVideoDuration;
 											if (
-												player.value.duration.indexOf(
-													".000"
-												) !== -1
-											)
-												player.value.videoNote = "(~)";
-											else player.value.videoNote = "";
+												video.value.duration >
+												youtubeDuration + 1
+											) {
+												stopVideo();
+												pauseVideo(true);
+												return new Toast(
+													"Video can't play. Specified duration is bigger than the YouTube song duration."
+												);
+											}
+											if (video.value.duration <= 0) {
+												stopVideo();
+												pauseVideo(true);
+												return new Toast(
+													"Video can't play. Specified duration has to be more than 0 seconds."
+												);
+											}
+
+											setPlaybackRate(null);
+										} else if (event.data === 2) {
+											player.value.paused = true;
+											updateMediaModalPlayingAudio(false);
 										}
 
-										if (video.value.duration === -1)
-											video.value.duration = Number(
-												player.value.duration
-											);
-
-										if (
-											video.value.duration >
-											youtubeDuration + 1
-										) {
-											stopVideo();
-											pauseVideo(true);
-											return new Toast(
-												"Video can't play. Specified duration is bigger than the YouTube song duration."
-											);
-										}
-										if (video.value.duration <= 0) {
-											stopVideo();
-											pauseVideo(true);
-											return new Toast(
-												"Video can't play. Specified duration has to be more than 0 seconds."
-											);
-										}
-
-										setPlaybackRate(null);
-									} else if (event.data === 2) {
-										player.value.paused = true;
-										updateMediaModalPlayingAudio(false);
+										return false;
 									}
-
-									return false;
 								}
 							}
-						}
+						);
+					} else {
+						updatePlayer({
+							error: true,
+							errorMessage: "Player could not be loaded."
+						});
+					}
+
+					let volume = parseFloat(localStorage.getItem("volume"));
+					volume =
+						typeof volume === "number" && !Number.isNaN(volume)
+							? volume
+							: 20;
+					localStorage.setItem("volume", volume.toString());
+					updatePlayer({ volume });
+
+					socket.dispatch(
+						"apis.joinRoom",
+						`view-youtube-video.${videoId.value}`
+					);
+
+					socket.on(
+						"event:youtubeVideo.removed",
+						() => {
+							new Toast("This YouTube video was removed.");
+							closeCurrentModal();
+						},
+						{ modalUuid: props.modalUuid }
 					);
 				} else {
-					updatePlayer({
-						error: true,
-						errorMessage: "Player could not be loaded."
-					});
+					new Toast("YouTube video with that ID not found");
+					closeCurrentModal();
 				}
-
-				let volume = parseFloat(localStorage.getItem("volume"));
-				volume =
-					typeof volume === "number" && !Number.isNaN(volume)
-						? volume
-						: 20;
-				localStorage.setItem("volume", volume.toString());
-				updatePlayer({ volume });
-
-				socket.dispatch(
-					"apis.joinRoom",
-					`view-youtube-video.${videoId.value}`
-				);
-
-				socket.on(
-					"event:youtubeVideo.removed",
-					() => {
-						new Toast("This YouTube video was removed.");
-						closeCurrentModal();
-					},
-					{ modalUuid: props.modalUuid }
-				);
-			} else {
-				new Toast("YouTube video with that ID not found");
-				closeCurrentModal();
 			}
-		}
-	);
-};
-
-onMounted(() => {
-	socket.onConnect(init);
+		);
+	});
 });
 
 onBeforeUnmount(() => {

+ 88 - 89
frontend/src/composables/useSortablePlaylists.ts

@@ -72,103 +72,102 @@ export const useSortablePlaylists = () => {
 				if (res.status === "success") setPlaylists(res.data.playlists);
 				orderOfPlaylists.value = calculatePlaylistOrder(); // order in regards to the database
 			});
-		});
 
-		socket.on(
-			"event:playlist.created",
-			res => addPlaylist(res.data.playlist),
-			{ replaceable: true }
-		);
+			socket.on(
+				"event:playlist.created",
+				res => addPlaylist(res.data.playlist),
+				{ replaceable: true }
+			);
 
-		socket.on(
-			"event:playlist.deleted",
-			res => removePlaylist(res.data.playlistId),
-			{ replaceable: true }
-		);
+			socket.on(
+				"event:playlist.deleted",
+				res => removePlaylist(res.data.playlistId),
+				{ replaceable: true }
+			);
 
-		socket.on(
-			"event:playlist.song.added",
-			res => {
-				playlists.value.forEach((playlist, index) => {
-					if (playlist._id === res.data.playlistId) {
-						playlists.value[index].songs.push(res.data.song);
-					}
-				});
-			},
-			{ replaceable: true }
-		);
+			socket.on(
+				"event:playlist.song.added",
+				res => {
+					playlists.value.forEach((playlist, index) => {
+						if (playlist._id === res.data.playlistId) {
+							playlists.value[index].songs.push(res.data.song);
+						}
+					});
+				},
+				{ replaceable: true }
+			);
 
-		socket.on(
-			"event:playlist.song.removed",
-			res => {
-				playlists.value.forEach((playlist, playlistIndex) => {
-					if (playlist._id === res.data.playlistId) {
-						playlists.value[playlistIndex].songs.forEach(
-							(song, songIndex) => {
-								if (song.youtubeId === res.data.youtubeId) {
-									playlists.value[playlistIndex].songs.splice(
-										songIndex,
-										1
-									);
+			socket.on(
+				"event:playlist.song.removed",
+				res => {
+					playlists.value.forEach((playlist, playlistIndex) => {
+						if (playlist._id === res.data.playlistId) {
+							playlists.value[playlistIndex].songs.forEach(
+								(song, songIndex) => {
+									if (song.youtubeId === res.data.youtubeId) {
+										playlists.value[
+											playlistIndex
+										].songs.splice(songIndex, 1);
+									}
 								}
-							}
-						);
-					}
-				});
-			},
-			{ replaceable: true }
-		);
+							);
+						}
+					});
+				},
+				{ replaceable: true }
+			);
 
-		socket.on(
-			"event:playlist.displayName.updated",
-			res => {
-				playlists.value.forEach((playlist, index) => {
-					if (playlist._id === res.data.playlistId) {
-						playlists.value[index].displayName =
-							res.data.displayName;
-					}
-				});
-			},
-			{ replaceable: true }
-		);
+			socket.on(
+				"event:playlist.displayName.updated",
+				res => {
+					playlists.value.forEach((playlist, index) => {
+						if (playlist._id === res.data.playlistId) {
+							playlists.value[index].displayName =
+								res.data.displayName;
+						}
+					});
+				},
+				{ replaceable: true }
+			);
 
-		socket.on(
-			"event:playlist.privacy.updated",
-			res => {
-				playlists.value.forEach((playlist, index) => {
-					if (playlist._id === res.data.playlist._id) {
-						playlists.value[index].privacy =
-							res.data.playlist.privacy;
-					}
-				});
-			},
-			{ replaceable: true }
-		);
+			socket.on(
+				"event:playlist.privacy.updated",
+				res => {
+					playlists.value.forEach((playlist, index) => {
+						if (playlist._id === res.data.playlist._id) {
+							playlists.value[index].privacy =
+								res.data.playlist.privacy;
+						}
+					});
+				},
+				{ replaceable: true }
+			);
 
-		socket.on(
-			"event:user.orderOfPlaylists.updated",
-			res => {
-				const order = res.data.order.filter(playlistId =>
-					playlists.value.find(
-						playlist =>
-							playlist._id === playlistId &&
-							(isCurrentUser.value ||
-								playlist.privacy === "public")
-					)
-				);
-				const sortedPlaylists = [];
-
-				playlists.value.forEach(playlist => {
-					const playlistOrder = order.indexOf(playlist._id);
-					if (playlistOrder >= 0)
-						sortedPlaylists[playlistOrder] = playlist;
-				});
-
-				playlists.value = sortedPlaylists;
-				orderOfPlaylists.value = calculatePlaylistOrder();
-			},
-			{ replaceable: true }
-		);
+			socket.on(
+				"event:user.orderOfPlaylists.updated",
+				res => {
+					const order = res.data.order.filter(playlistId =>
+						playlists.value.find(
+							playlist =>
+								playlist._id === playlistId &&
+								(isCurrentUser.value ||
+									playlist.privacy === "public")
+						)
+					);
+					const sortedPlaylists = [];
+
+					playlists.value.forEach(playlist => {
+						const playlistOrder = order.indexOf(playlist._id);
+						if (playlistOrder >= 0)
+							sortedPlaylists[playlistOrder] = playlist;
+					});
+
+					playlists.value = sortedPlaylists;
+					orderOfPlaylists.value = calculatePlaylistOrder();
+				},
+				{ replaceable: true }
+			);
+		});
 	});
 
 	onBeforeUnmount(() => {

+ 14 - 16
frontend/src/pages/Admin/Statistics.vue

@@ -10,24 +10,22 @@ const { socket } = useWebsocketsStore();
 const modules = ref([]);
 const activeModule = ref();
 
-const init = () => {
-	socket.dispatch("utils.getModules", res => {
-		if (res.status === "success") modules.value = res.data.modules;
-	});
-
-	if (route.query.moduleName) {
-		socket.dispatch("utils.getModule", route.query.moduleName, res => {
-			if (res.status === "success")
-				activeModule.value = {
-					runningJobs: res.data.runningJobs,
-					jobStatistics: res.data.jobStatistics
-				};
+onMounted(() => {
+	socket.onConnect(() => {
+		socket.dispatch("utils.getModules", res => {
+			if (res.status === "success") modules.value = res.data.modules;
 		});
-	}
-};
 
-onMounted(() => {
-	socket.onConnect(init);
+		if (route.query.moduleName) {
+			socket.dispatch("utils.getModule", route.query.moduleName, res => {
+				if (res.status === "success")
+					activeModule.value = {
+						runningJobs: res.data.runningJobs,
+						jobStatistics: res.data.jobStatistics
+					};
+			});
+		}
+	});
 });
 </script>
 

+ 31 - 31
frontend/src/pages/Admin/YouTube/index.vue

@@ -143,36 +143,6 @@ const jobs = ref([
 
 const { openModal } = useModalsStore();
 
-const init = () => {
-	if (route.query.fromDate) fromDate.value = route.query.fromDate;
-
-	socket.dispatch("youtube.getQuotaStatus", fromDate.value, res => {
-		if (res.status === "success") quotaStatus.value = res.data.status;
-	});
-
-	socket.dispatch(
-		"youtube.getQuotaChartData",
-		"days",
-		new Date().setDate(new Date().getDate() - 6),
-		new Date().setDate(new Date().getDate() + 1),
-		"usage",
-		res => {
-			if (res.status === "success") charts.value.quotaUsage = res.data;
-		}
-	);
-
-	socket.dispatch(
-		"youtube.getQuotaChartData",
-		"days",
-		new Date().setDate(new Date().getDate() - 6),
-		new Date().setDate(new Date().getDate() + 1),
-		"count",
-		res => {
-			if (res.status === "success") charts.value.apiRequests = res.data;
-		}
-	);
-};
-
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
@@ -192,7 +162,37 @@ const removeApiRequest = requestId => {
 };
 
 onMounted(() => {
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		if (route.query.fromDate) fromDate.value = route.query.fromDate;
+
+		socket.dispatch("youtube.getQuotaStatus", fromDate.value, res => {
+			if (res.status === "success") quotaStatus.value = res.data.status;
+		});
+
+		socket.dispatch(
+			"youtube.getQuotaChartData",
+			"days",
+			new Date().setDate(new Date().getDate() - 6),
+			new Date().setDate(new Date().getDate() + 1),
+			"usage",
+			res => {
+				if (res.status === "success")
+					charts.value.quotaUsage = res.data;
+			}
+		);
+
+		socket.dispatch(
+			"youtube.getQuotaChartData",
+			"days",
+			new Date().setDate(new Date().getDate() - 6),
+			new Date().setDate(new Date().getDate() + 1),
+			"count",
+			res => {
+				if (res.status === "success")
+					charts.value.apiRequests = res.data;
+			}
+		);
+	});
 });
 </script>
 

+ 115 - 117
frontend/src/pages/Home.vue

@@ -120,12 +120,6 @@ const fetchStations = () => {
 	);
 };
 
-const init = () => {
-	socket.dispatch("apis.joinRoom", "home");
-
-	fetchStations();
-};
-
 const canRequest = (station, requireLogin = true) =>
 	station &&
 	(!requireLogin || loggedIn.value) &&
@@ -191,145 +185,149 @@ onMounted(async () => {
 		openModal(route.redirectedFrom.name);
 	}
 
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch("apis.joinRoom", "home");
 
-	socket.on("event:station.created", res => {
-		const { station } = res.data;
+		fetchStations();
 
-		if (stations.value.find(_station => _station._id === station._id)) {
-			stations.value.forEach(s => {
-				const _station = s;
-				if (_station._id === station._id) {
-					_station.privacy = station.privacy;
-				}
-			});
-		} else {
-			if (!station.currentSong)
-				station.currentSong = {
-					thumbnail: "/assets/notes-transparent.png"
-				};
-			if (station.currentSong && !station.currentSong.thumbnail)
-				station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.youtubeId}/mqdefault.jpg`;
-			stations.value.push(station);
-		}
-	});
+		socket.on("event:station.created", res => {
+			const { station } = res.data;
 
-	socket.on("event:station.deleted", res => {
-		const { stationId } = res.data;
+			if (stations.value.find(_station => _station._id === station._id)) {
+				stations.value.forEach(s => {
+					const _station = s;
+					if (_station._id === station._id) {
+						_station.privacy = station.privacy;
+					}
+				});
+			} else {
+				if (!station.currentSong)
+					station.currentSong = {
+						thumbnail: "/assets/notes-transparent.png"
+					};
+				if (station.currentSong && !station.currentSong.thumbnail)
+					station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.youtubeId}/mqdefault.jpg`;
+				stations.value.push(station);
+			}
+		});
 
-		const station = stations.value.find(
-			station => station._id === stationId
-		);
+		socket.on("event:station.deleted", res => {
+			const { stationId } = res.data;
 
-		if (station) {
-			const stationIndex = stations.value.indexOf(station);
-			stations.value.splice(stationIndex, 1);
+			const station = stations.value.find(
+				station => station._id === stationId
+			);
 
-			if (station.isFavorited)
-				orderOfFavoriteStations.value =
-					orderOfFavoriteStations.value.filter(
-						favoritedId => favoritedId !== stationId
-					);
-		}
-	});
+			if (station) {
+				const stationIndex = stations.value.indexOf(station);
+				stations.value.splice(stationIndex, 1);
 
-	socket.on("event:station.userCount.updated", res => {
-		const station = stations.value.find(
-			station => station._id === res.data.stationId
-		);
+				if (station.isFavorited)
+					orderOfFavoriteStations.value =
+						orderOfFavoriteStations.value.filter(
+							favoritedId => favoritedId !== stationId
+						);
+			}
+		});
 
-		if (station) station.userCount = res.data.userCount;
-	});
+		socket.on("event:station.userCount.updated", res => {
+			const station = stations.value.find(
+				station => station._id === res.data.stationId
+			);
 
-	socket.on("event:station.updated", res => {
-		const stationIndex = stations.value
-			.map(station => station._id)
-			.indexOf(res.data.station._id);
+			if (station) station.userCount = res.data.userCount;
+		});
 
-		if (stationIndex !== -1) {
-			stations.value[stationIndex] = {
-				...stations.value[stationIndex],
-				...res.data.station
-			};
-		}
-	});
+		socket.on("event:station.updated", res => {
+			const stationIndex = stations.value
+				.map(station => station._id)
+				.indexOf(res.data.station._id);
 
-	socket.on("event:station.nextSong", res => {
-		const station = stations.value.find(
-			station => station._id === res.data.stationId
-		);
+			if (stationIndex !== -1) {
+				stations.value[stationIndex] = {
+					...stations.value[stationIndex],
+					...res.data.station
+				};
+			}
+		});
 
-		if (station) {
-			let newSong = res.data.currentSong;
+		socket.on("event:station.nextSong", res => {
+			const station = stations.value.find(
+				station => station._id === res.data.stationId
+			);
 
-			if (!newSong)
-				newSong = {
-					thumbnail: "/assets/notes-transparent.png"
-				};
+			if (station) {
+				let newSong = res.data.currentSong;
 
-			station.currentSong = newSong;
-		}
-	});
+				if (!newSong)
+					newSong = {
+						thumbnail: "/assets/notes-transparent.png"
+					};
 
-	socket.on("event:station.pause", res => {
-		const station = stations.value.find(
-			station => station._id === res.data.stationId
-		);
+				station.currentSong = newSong;
+			}
+		});
 
-		if (station) station.paused = true;
-	});
+		socket.on("event:station.pause", res => {
+			const station = stations.value.find(
+				station => station._id === res.data.stationId
+			);
 
-	socket.on("event:station.resume", res => {
-		const station = stations.value.find(
-			station => station._id === res.data.stationId
-		);
+			if (station) station.paused = true;
+		});
 
-		if (station) station.paused = false;
-	});
+		socket.on("event:station.resume", res => {
+			const station = stations.value.find(
+				station => station._id === res.data.stationId
+			);
 
-	socket.on("event:user.station.favorited", res => {
-		const { stationId } = res.data;
+			if (station) station.paused = false;
+		});
 
-		const station = stations.value.find(
-			station => station._id === stationId
-		);
+		socket.on("event:user.station.favorited", res => {
+			const { stationId } = res.data;
 
-		if (station) {
-			station.isFavorited = true;
-			orderOfFavoriteStations.value.push(stationId);
-		}
-	});
+			const station = stations.value.find(
+				station => station._id === stationId
+			);
+
+			if (station) {
+				station.isFavorited = true;
+				orderOfFavoriteStations.value.push(stationId);
+			}
+		});
 
-	socket.on("event:user.station.unfavorited", res => {
-		const { stationId } = res.data;
+		socket.on("event:user.station.unfavorited", res => {
+			const { stationId } = res.data;
 
-		const station = stations.value.find(
-			station => station._id === stationId
-		);
+			const station = stations.value.find(
+				station => station._id === stationId
+			);
 
-		if (station) {
-			station.isFavorited = false;
-			orderOfFavoriteStations.value =
-				orderOfFavoriteStations.value.filter(
-					favoritedId => favoritedId !== stationId
-				);
-		}
-	});
+			if (station) {
+				station.isFavorited = false;
+				orderOfFavoriteStations.value =
+					orderOfFavoriteStations.value.filter(
+						favoritedId => favoritedId !== stationId
+					);
+			}
+		});
 
-	socket.on("event:user.orderOfFavoriteStations.updated", res => {
-		orderOfFavoriteStations.value = res.data.order;
-	});
+		socket.on("event:user.orderOfFavoriteStations.updated", res => {
+			orderOfFavoriteStations.value = res.data.order;
+		});
 
-	socket.on("event:station.djs.added", res => {
-		if (res.data.user._id === userId.value) fetchStations();
-	});
+		socket.on("event:station.djs.added", res => {
+			if (res.data.user._id === userId.value) fetchStations();
+		});
 
-	socket.on("event:station.djs.removed", res => {
-		if (res.data.user._id === userId.value) fetchStations();
-	});
+		socket.on("event:station.djs.removed", res => {
+			if (res.data.user._id === userId.value) fetchStations();
+		});
 
-	socket.on("keep.event:user.role.updated", () => {
-		fetchStations();
+		socket.on("keep.event:user.role.updated", () => {
+			fetchStations();
+		});
 	});
 
 	// ctrl + alt + f

+ 30 - 28
frontend/src/pages/News.vue

@@ -20,14 +20,6 @@ const { socket } = useWebsocketsStore();
 
 const news = ref([]);
 
-const init = () => {
-	socket.dispatch("news.getPublished", res => {
-		if (res.status === "success") news.value = res.data.news;
-	});
-
-	socket.dispatch("apis.joinRoom", "news");
-};
-
 const { sanitize } = DOMPurify;
 
 onMounted(() => {
@@ -42,30 +34,40 @@ onMounted(() => {
 		}
 	});
 
-	socket.on("event:news.created", res => news.value.unshift(res.data.news));
+	socket.onConnect(() => {
+		socket.dispatch("news.getPublished", res => {
+			if (res.status === "success") news.value = res.data.news;
+		});
 
-	socket.on("event:news.updated", res => {
-		if (res.data.news.status === "draft") {
-			news.value = news.value.filter(
-				item => item._id !== res.data.news._id
-			);
-			return;
-		}
+		socket.dispatch("apis.joinRoom", "news");
 
-		for (let n = 0; n < news.value.length; n += 1) {
-			if (news.value[n]._id === res.data.news._id)
-				news.value[n] = {
-					...news.value[n],
-					...res.data.news
-				};
-		}
-	});
+		socket.on("event:news.created", res =>
+			news.value.unshift(res.data.news)
+		);
 
-	socket.on("event:news.deleted", res => {
-		news.value = news.value.filter(item => item._id !== res.data.newsId);
-	});
+		socket.on("event:news.updated", res => {
+			if (res.data.news.status === "draft") {
+				news.value = news.value.filter(
+					item => item._id !== res.data.news._id
+				);
+				return;
+			}
+
+			for (let n = 0; n < news.value.length; n += 1) {
+				if (news.value[n]._id === res.data.news._id)
+					news.value[n] = {
+						...news.value[n],
+						...res.data.news
+					};
+			}
+		});
 
-	socket.onConnect(init);
+		socket.on("event:news.deleted", res => {
+			news.value = news.value.filter(
+				item => item._id !== res.data.newsId
+			);
+		});
+	});
 });
 </script>
 

+ 33 - 35
frontend/src/pages/Profile/Tabs/RecentActivity.vue

@@ -60,20 +60,6 @@ const getSet = () => {
 	);
 };
 
-const init = () => {
-	if (myUserId.value !== props.userId)
-		getBasicUser(props.userId).then((user: any) => {
-			if (user && user.username) username.value = user.username;
-		});
-
-	socket.dispatch("activities.length", props.userId, res => {
-		if (res.status === "success") {
-			maxPosition.value = Math.ceil(res.data.length / 15) + 1;
-			getSet();
-		}
-	});
-};
-
 const handleScroll = () => {
 	const scrollPosition = document.body.clientHeight + window.scrollY;
 	const bottomPosition = document.body.scrollHeight;
@@ -86,32 +72,44 @@ const handleScroll = () => {
 onMounted(() => {
 	window.addEventListener("scroll", handleScroll);
 
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		if (myUserId.value !== props.userId)
+			getBasicUser(props.userId).then((user: any) => {
+				if (user && user.username) username.value = user.username;
+			});
 
-	socket.on("event:activity.updated", res => {
-		activities.value.find(
-			activity => activity._id === res.data.activityId
-		).payload.message = res.data.message;
-	});
+		socket.dispatch("activities.length", props.userId, res => {
+			if (res.status === "success") {
+				maxPosition.value = Math.ceil(res.data.length / 15) + 1;
+				getSet();
+			}
+		});
 
-	socket.on("event:activity.created", res => {
-		activities.value.unshift(res.data.activity);
-		offsettedFromNextSet.value += 1;
-	});
+		socket.on("event:activity.updated", res => {
+			activities.value.find(
+				activity => activity._id === res.data.activityId
+			).payload.message = res.data.message;
+		});
+
+		socket.on("event:activity.created", res => {
+			activities.value.unshift(res.data.activity);
+			offsettedFromNextSet.value += 1;
+		});
 
-	socket.on("event:activity.hidden", res => {
-		activities.value = activities.value.filter(
-			activity => activity._id !== res.data.activityId
-		);
+		socket.on("event:activity.hidden", res => {
+			activities.value = activities.value.filter(
+				activity => activity._id !== res.data.activityId
+			);
 
-		offsettedFromNextSet.value -= 1;
-	});
+			offsettedFromNextSet.value -= 1;
+		});
 
-	socket.on("event:activity.removeAllForUser", () => {
-		activities.value = [];
-		position.value = 1;
-		maxPosition.value = 1;
-		offsettedFromNextSet.value = 0;
+		socket.on("event:activity.removeAllForUser", () => {
+			activities.value = [];
+			position.value = 1;
+			maxPosition.value = 1;
+			offsettedFromNextSet.value = 0;
+		});
 	});
 });
 

+ 16 - 18
frontend/src/pages/Profile/index.vue

@@ -35,23 +35,6 @@ const userAuthStore = useUserAuthStore();
 const { userId: myUserId } = storeToRefs(userAuthStore);
 const { hasPermission } = userAuthStore;
 
-const init = () => {
-	socket.dispatch("users.getBasicUser", route.params.username, res => {
-		if (res.status === "error") router.push("/404");
-		else {
-			user.value = res.data;
-
-			user.value.createdAt = format(
-				parseISO(user.value.createdAt),
-				"MMMM do yyyy"
-			);
-
-			isUser.value = true;
-			userId.value = user.value._id;
-		}
-	});
-};
-
 onMounted(() => {
 	if (
 		route.query.tab === "recent-activity" ||
@@ -59,7 +42,22 @@ onMounted(() => {
 	)
 		tab.value = route.query.tab;
 
-	socket.onConnect(init);
+	socket.onConnect(() => {
+		socket.dispatch("users.getBasicUser", route.params.username, res => {
+			if (res.status === "error") router.push("/404");
+			else {
+				user.value = res.data;
+
+				user.value.createdAt = format(
+					parseISO(user.value.createdAt),
+					"MMMM do yyyy"
+				);
+
+				isUser.value = true;
+				userId.value = user.value._id;
+			}
+		});
+	});
 });
 </script>
 

+ 16 - 16
frontend/src/pages/Settings/Tabs/Preferences.vue

@@ -65,7 +65,7 @@ const saveChanges = () => {
 };
 
 onMounted(() => {
-	socket.onConnect(() =>
+	socket.onConnect(() => {
 		socket.dispatch("users.getPreferences", res => {
 			const { preferences } = res.data;
 
@@ -77,27 +77,27 @@ onMounted(() => {
 					preferences.anonymousSongRequests;
 				localActivityWatch.value = preferences.activityWatch;
 			}
-		})
-	);
+		});
 
-	socket.on("keep.event:user.preferences.updated", res => {
-		const { preferences } = res.data;
+		socket.on("keep.event:user.preferences.updated", res => {
+			const { preferences } = res.data;
 
-		if (preferences.nightmode !== undefined)
-			localNightmode.value = preferences.nightmode;
+			if (preferences.nightmode !== undefined)
+				localNightmode.value = preferences.nightmode;
 
-		if (preferences.autoSkipDisliked !== undefined)
-			localAutoSkipDisliked.value = preferences.autoSkipDisliked;
+			if (preferences.autoSkipDisliked !== undefined)
+				localAutoSkipDisliked.value = preferences.autoSkipDisliked;
 
-		if (preferences.activityLogPublic !== undefined)
-			localActivityLogPublic.value = preferences.activityLogPublic;
+			if (preferences.activityLogPublic !== undefined)
+				localActivityLogPublic.value = preferences.activityLogPublic;
 
-		if (preferences.anonymousSongRequests !== undefined)
-			localAnonymousSongRequests.value =
-				preferences.anonymousSongRequests;
+			if (preferences.anonymousSongRequests !== undefined)
+				localAnonymousSongRequests.value =
+					preferences.anonymousSongRequests;
 
-		if (preferences.activityWatch !== undefined)
-			localActivityWatch.value = preferences.activityWatch;
+			if (preferences.activityWatch !== undefined)
+				localActivityWatch.value = preferences.activityWatch;
+		});
 	});
 });
 </script>

+ 34 - 36
frontend/src/pages/Settings/index.vue

@@ -33,13 +33,6 @@ const { socket } = useWebsocketsStore();
 
 const { setUser, updateOriginalUser } = settingsStore;
 
-const init = () => {
-	socket.dispatch("users.findBySession", res => {
-		if (res.status === "success") setUser(res.data.user);
-		else new Toast("You're not currently signed in.");
-	});
-};
-
 onMounted(() => {
 	if (
 		route.query.tab === "profile" ||
@@ -52,35 +45,40 @@ onMounted(() => {
 
 	// this.localNightmode = this.nightmode;
 
-	socket.onConnect(init);
-
-	socket.on("event:user.password.linked", () =>
-		updateOriginalUser({
-			property: "password",
-			value: true
-		})
-	);
-
-	socket.on("event:user.password.unlinked", () =>
-		updateOriginalUser({
-			property: "password",
-			value: false
-		})
-	);
-
-	socket.on("event:user.github.linked", () =>
-		updateOriginalUser({
-			property: "github",
-			value: true
-		})
-	);
-
-	socket.on("event:user.github.unlinked", () =>
-		updateOriginalUser({
-			property: "github",
-			value: false
-		})
-	);
+	socket.onConnect(() => {
+		socket.dispatch("users.findBySession", res => {
+			if (res.status === "success") setUser(res.data.user);
+			else new Toast("You're not currently signed in.");
+		});
+
+		socket.on("event:user.password.linked", () =>
+			updateOriginalUser({
+				property: "password",
+				value: true
+			})
+		);
+
+		socket.on("event:user.password.unlinked", () =>
+			updateOriginalUser({
+				property: "password",
+				value: false
+			})
+		);
+
+		socket.on("event:user.github.linked", () =>
+			updateOriginalUser({
+				property: "github",
+				value: true
+			})
+		);
+
+		socket.on("event:user.github.unlinked", () =>
+			updateOriginalUser({
+				property: "github",
+				value: false
+			})
+		);
+	});
 });
 </script>
 

+ 470 - 462
frontend/src/pages/Station/index.vue

@@ -84,7 +84,6 @@ const nextCurrentSong = ref(null);
 const mediaModalWatcher = ref(null);
 const beforeMediaModalLocalPausedLock = ref(false);
 const beforeMediaModalLocalPaused = ref(null);
-const socketConnected = ref(null);
 const persistentToastCheckerInterval = ref(null);
 const persistentToasts = ref([]);
 const mediasession = ref(false);
@@ -587,7 +586,7 @@ const setCurrentSong = data => {
 		else playVideo();
 
 		// If the station is playing and the backend is not connected, set the next song to skip to after this song and set a timer to skip
-		if (!stationPaused.value && !socketConnected.value) {
+		if (!stationPaused.value && !socket.ready) {
 			if (nextSong)
 				setNextCurrentSong(
 					{
@@ -804,238 +803,6 @@ const toggleKeyboardShortcutsHelper = () => {
 const resetKeyboardShortcutsHelper = () => {
 	keyboardShortcutsHelper.value.resetBox();
 };
-const join = () => {
-	socket.dispatch("stations.join", stationIdentifier.value, async res => {
-		if (res.status === "success") {
-			setTimeout(() => {
-				loading.value = false;
-			}, 1000); // prevents popping in of youtube embed etc.
-
-			const {
-				_id,
-				displayName,
-				name,
-				description,
-				privacy,
-				owner,
-				autofill,
-				blacklist,
-				type,
-				isFavorited,
-				theme,
-				requests,
-				djs
-			} = res.data;
-
-			// change url to use station name instead of station id
-			if (name !== stationIdentifier.value) {
-				// eslint-disable-next-line no-restricted-globals
-				router.replace(name);
-			}
-
-			joinStation({
-				_id,
-				name,
-				displayName,
-				description,
-				privacy,
-				owner,
-				autofill,
-				blacklist,
-				type,
-				isFavorited,
-				theme,
-				requests,
-				djs
-			});
-
-			document.getElementsByTagName(
-				"html"
-			)[0].style.cssText = `--primary-color: var(--${res.data.theme})`;
-
-			setCurrentSong({
-				currentSong: res.data.currentSong,
-				startedAt: res.data.startedAt,
-				paused: res.data.paused,
-				timePaused: res.data.timePaused,
-				pausedAt: res.data.pausedAt
-			});
-
-			updateUserCount(res.data.userCount);
-			updateUsers(res.data.users);
-
-			await updatePermissions();
-
-			socket.dispatch(
-				"stations.getStationAutofillPlaylistsById",
-				station.value._id,
-				res => {
-					if (res.status === "success") {
-						setAutofillPlaylists(res.data.playlists);
-					}
-				}
-			);
-
-			socket.dispatch(
-				"stations.getStationBlacklistById",
-				station.value._id,
-				res => {
-					if (res.status === "success") {
-						setBlacklist(res.data.playlists);
-					}
-				}
-			);
-
-			socket.dispatch("stations.getQueue", _id, res => {
-				if (res.status === "success") {
-					const { queue } = res.data;
-					updateSongsList(queue);
-					const [nextSong] = queue;
-
-					updateNextSong(nextSong);
-				}
-			});
-
-			if (hasPermission("stations.playback.toggle"))
-				keyboardShortcuts.registerShortcut("station.pauseResume", {
-					keyCode: 32, // Spacebar
-					shift: false,
-					ctrl: true,
-					preventDefault: true,
-					handler: () => {
-						if (aModalIsOpen.value) return;
-						if (stationPaused.value) resumeStation();
-						else pauseStation();
-					}
-				});
-
-			if (hasPermission("stations.skip"))
-				keyboardShortcuts.registerShortcut("station.skipStation", {
-					keyCode: 39, // Right arrow key
-					shift: false,
-					ctrl: true,
-					preventDefault: true,
-					handler: () => {
-						if (aModalIsOpen.value) return;
-						skipStation();
-					}
-				});
-
-			keyboardShortcuts.registerShortcut("station.lowerVolumeLarge", {
-				keyCode: 40, // Down arrow key
-				shift: false,
-				ctrl: true,
-				preventDefault: true,
-				handler: () => {
-					if (aModalIsOpen.value) return;
-					volumeSliderValue.value -= 10;
-					changeVolume();
-				}
-			});
-
-			keyboardShortcuts.registerShortcut("station.lowerVolumeSmall", {
-				keyCode: 40, // Down arrow key
-				shift: true,
-				ctrl: true,
-				preventDefault: true,
-				handler: () => {
-					if (aModalIsOpen.value) return;
-					volumeSliderValue.value -= 1;
-					changeVolume();
-				}
-			});
-
-			keyboardShortcuts.registerShortcut("station.increaseVolumeLarge", {
-				keyCode: 38, // Up arrow key
-				shift: false,
-				ctrl: true,
-				preventDefault: true,
-				handler: () => {
-					if (aModalIsOpen.value) return;
-					volumeSliderValue.value += 10;
-					changeVolume();
-				}
-			});
-
-			keyboardShortcuts.registerShortcut("station.increaseVolumeSmall", {
-				keyCode: 38, // Up arrow key
-				shift: true,
-				ctrl: true,
-				preventDefault: true,
-				handler: () => {
-					if (aModalIsOpen.value) return;
-					volumeSliderValue.value += 1;
-					changeVolume();
-				}
-			});
-
-			keyboardShortcuts.registerShortcut("station.toggleDebug", {
-				keyCode: 68, // D key
-				shift: false,
-				ctrl: true,
-				preventDefault: true,
-				handler: () => {
-					if (aModalIsOpen.value) return;
-					togglePlayerDebugBox();
-				}
-			});
-
-			keyboardShortcuts.registerShortcut(
-				"station.toggleKeyboardShortcutsHelper",
-				{
-					keyCode: 191, // '/' key
-					ctrl: true,
-					preventDefault: true,
-					handler: () => {
-						if (aModalIsOpen.value) return;
-						toggleKeyboardShortcutsHelper();
-					}
-				}
-			);
-
-			keyboardShortcuts.registerShortcut(
-				"station.resetKeyboardShortcutsHelper",
-				{
-					keyCode: 191, // '/' key
-					ctrl: true,
-					shift: true,
-					preventDefault: true,
-					handler: () => {
-						if (aModalIsOpen.value) return;
-						resetKeyboardShortcutsHelper();
-					}
-				}
-			);
-
-			// UNIX client time before ping
-			const beforePing = Date.now();
-			socket.dispatch("apis.ping", res => {
-				if (res.status === "success") {
-					// UNIX client time after ping
-					const afterPing = Date.now();
-					// Average time in MS it took between the server responding and the client receiving
-					const connectionLatency = (afterPing - beforePing) / 2;
-					console.log(connectionLatency, beforePing - afterPing);
-					// UNIX server time
-					const serverDate = res.data.date;
-					// Difference between the server UNIX time and the client UNIX time after ping, with the connectionLatency added to the server UNIX time
-					const difference =
-						serverDate + connectionLatency - afterPing;
-					console.log("Difference: ", difference);
-					if (difference > 3000 || difference < -3000) {
-						console.log(
-							"System time difference is bigger than 3 seconds."
-						);
-					}
-					systemDifference.value = difference;
-				}
-			});
-		} else {
-			loading.value = false;
-			exists.value = false;
-		}
-	});
-};
 const sendActivityWatchVideoData = () => {
 	if (!stationPaused.value && !localPaused.value && !noSong.value) {
 		if (activityWatchVideoLastStatus.value !== "playing") {
@@ -1119,250 +886,474 @@ onMounted(async () => {
 		);
 	}, 1000);
 
-	if (socket.readyState === 1) join();
 	socket.onConnect(() => {
-		socketConnected.value = true;
 		clearTimeout(window.stationNextSongTimeout);
-		join();
-	});
 
-	socket.onDisconnect(true, () => {
-		socketConnected.value = false;
-		const _currentSong = currentSong.value;
-		if (nextSong.value)
-			setNextCurrentSong(
-				{
-					currentSong: nextSong.value,
-					startedAt: Date.now() + getTimeRemaining(),
-					paused: false,
-					timePaused: 0
-				},
-				true
-			);
-		else
-			setNextCurrentSong(
-				{
-					currentSong: null,
-					startedAt: 0,
-					paused: false,
-					timePaused: 0,
-					pausedAt: 0
-				},
-				true
-			);
-		window.stationNextSongTimeout = setTimeout(() => {
-			if (!noSong.value && currentSong.value._id === _currentSong._id)
-				skipSong();
-		}, getTimeRemaining());
-	});
+		socket.dispatch("stations.join", stationIdentifier.value, async res => {
+			if (res.status === "success") {
+				setTimeout(() => {
+					loading.value = false;
+				}, 1000); // prevents popping in of youtube embed etc.
+
+				const {
+					_id,
+					displayName,
+					name,
+					description,
+					privacy,
+					owner,
+					autofill,
+					blacklist,
+					type,
+					isFavorited,
+					theme,
+					requests,
+					djs
+				} = res.data;
+
+				// change url to use station name instead of station id
+				if (name !== stationIdentifier.value) {
+					// eslint-disable-next-line no-restricted-globals
+					router.replace(name);
+				}
 
-	frontendDevMode.value = await lofig.get("mode");
-	mediasession.value = await lofig.get("siteSettings.mediasession");
-	christmas.value = await lofig.get("siteSettings.christmas");
-	sitename.value = await lofig.get("siteSettings.sitename");
+				joinStation({
+					_id,
+					name,
+					displayName,
+					description,
+					privacy,
+					owner,
+					autofill,
+					blacklist,
+					type,
+					isFavorited,
+					theme,
+					requests,
+					djs
+				});
 
-	socket.dispatch("stations.existsByName", stationIdentifier.value, res => {
-		if (res.status === "error" || !res.data.exists) {
-			// station identifier may be using stationid instead
-			socket.dispatch(
-				"stations.existsById",
-				stationIdentifier.value,
-				res => {
-					if (res.status === "error" || !res.data.exists) {
-						loading.value = false;
-						exists.value = false;
+				document.getElementsByTagName(
+					"html"
+				)[0].style.cssText = `--primary-color: var(--${res.data.theme})`;
+
+				setCurrentSong({
+					currentSong: res.data.currentSong,
+					startedAt: res.data.startedAt,
+					paused: res.data.paused,
+					timePaused: res.data.timePaused,
+					pausedAt: res.data.pausedAt
+				});
+
+				updateUserCount(res.data.userCount);
+				updateUsers(res.data.users);
+
+				await updatePermissions();
+
+				socket.dispatch(
+					"stations.getStationAutofillPlaylistsById",
+					station.value._id,
+					res => {
+						if (res.status === "success") {
+							setAutofillPlaylists(res.data.playlists);
+						}
 					}
-				}
-			);
-		}
-	});
+				);
 
-	ms.setListeners(0, {
-		play: () => {
-			if (hasPermission("stations.playback.toggle")) resumeStation();
-			else resumeLocalStation();
-		},
-		pause: () => {
-			if (hasPermission("stations.playback.toggle")) pauseStation();
-			else pauseLocalStation();
-		},
-		nexttrack: () => {
-			if (hasPermission("stations.skip")) skipStation();
-			else if (!currentSong.value.voted) toggleSkipVote();
-		}
-	});
+				socket.dispatch(
+					"stations.getStationBlacklistById",
+					station.value._id,
+					res => {
+						if (res.status === "success") {
+							setBlacklist(res.data.playlists);
+						}
+					}
+				);
 
-	socket.on("event:station.nextSong", res => {
-		const { currentSong, startedAt, paused, timePaused } = res.data;
+				socket.dispatch("stations.getQueue", _id, res => {
+					if (res.status === "success") {
+						const { queue } = res.data;
+						updateSongsList(queue);
+						const [nextSong] = queue;
 
-		setCurrentSong({
-			currentSong,
-			startedAt,
-			paused,
-			timePaused,
-			pausedAt: 0
-		});
-	});
+						updateNextSong(nextSong);
+					}
+				});
 
-	socket.on("event:station.pause", res => {
-		pausedAt.value = res.data.pausedAt;
-		updateStationPaused(true);
-		pauseLocalPlayer();
+				if (hasPermission("stations.playback.toggle"))
+					keyboardShortcuts.registerShortcut("station.pauseResume", {
+						keyCode: 32, // Spacebar
+						shift: false,
+						ctrl: true,
+						preventDefault: true,
+						handler: () => {
+							if (aModalIsOpen.value) return;
+							if (stationPaused.value) resumeStation();
+							else pauseStation();
+						}
+					});
 
-		clearTimeout(window.stationNextSongTimeout);
-	});
+				if (hasPermission("stations.skip"))
+					keyboardShortcuts.registerShortcut("station.skipStation", {
+						keyCode: 39, // Right arrow key
+						shift: false,
+						ctrl: true,
+						preventDefault: true,
+						handler: () => {
+							if (aModalIsOpen.value) return;
+							skipStation();
+						}
+					});
 
-	socket.on("event:station.resume", res => {
-		timePaused.value = res.data.timePaused;
-		updateStationPaused(false);
-		if (!localPaused.value) resumeLocalPlayer();
+				keyboardShortcuts.registerShortcut("station.lowerVolumeLarge", {
+					keyCode: 40, // Down arrow key
+					shift: false,
+					ctrl: true,
+					preventDefault: true,
+					handler: () => {
+						if (aModalIsOpen.value) return;
+						volumeSliderValue.value -= 10;
+						changeVolume();
+					}
+				});
 
-		autoRequestSong();
-	});
+				keyboardShortcuts.registerShortcut("station.lowerVolumeSmall", {
+					keyCode: 40, // Down arrow key
+					shift: true,
+					ctrl: true,
+					preventDefault: true,
+					handler: () => {
+						if (aModalIsOpen.value) return;
+						volumeSliderValue.value -= 1;
+						changeVolume();
+					}
+				});
+
+				keyboardShortcuts.registerShortcut(
+					"station.increaseVolumeLarge",
+					{
+						keyCode: 38, // Up arrow key
+						shift: false,
+						ctrl: true,
+						preventDefault: true,
+						handler: () => {
+							if (aModalIsOpen.value) return;
+							volumeSliderValue.value += 10;
+							changeVolume();
+						}
+					}
+				);
+
+				keyboardShortcuts.registerShortcut(
+					"station.increaseVolumeSmall",
+					{
+						keyCode: 38, // Up arrow key
+						shift: true,
+						ctrl: true,
+						preventDefault: true,
+						handler: () => {
+							if (aModalIsOpen.value) return;
+							volumeSliderValue.value += 1;
+							changeVolume();
+						}
+					}
+				);
+
+				keyboardShortcuts.registerShortcut("station.toggleDebug", {
+					keyCode: 68, // D key
+					shift: false,
+					ctrl: true,
+					preventDefault: true,
+					handler: () => {
+						if (aModalIsOpen.value) return;
+						togglePlayerDebugBox();
+					}
+				});
+
+				keyboardShortcuts.registerShortcut(
+					"station.toggleKeyboardShortcutsHelper",
+					{
+						keyCode: 191, // '/' key
+						ctrl: true,
+						preventDefault: true,
+						handler: () => {
+							if (aModalIsOpen.value) return;
+							toggleKeyboardShortcutsHelper();
+						}
+					}
+				);
+
+				keyboardShortcuts.registerShortcut(
+					"station.resetKeyboardShortcutsHelper",
+					{
+						keyCode: 191, // '/' key
+						ctrl: true,
+						shift: true,
+						preventDefault: true,
+						handler: () => {
+							if (aModalIsOpen.value) return;
+							resetKeyboardShortcutsHelper();
+						}
+					}
+				);
 
-	socket.on("event:station.deleted", () => {
-		router.push({
-			path: "/",
-			query: {
-				toast: "The station you were in was deleted."
+				// UNIX client time before ping
+				const beforePing = Date.now();
+				socket.dispatch("apis.ping", res => {
+					if (res.status === "success") {
+						// UNIX client time after ping
+						const afterPing = Date.now();
+						// Average time in MS it took between the server responding and the client receiving
+						const connectionLatency = (afterPing - beforePing) / 2;
+						console.log(connectionLatency, beforePing - afterPing);
+						// UNIX server time
+						const serverDate = res.data.date;
+						// Difference between the server UNIX time and the client UNIX time after ping, with the connectionLatency added to the server UNIX time
+						const difference =
+							serverDate + connectionLatency - afterPing;
+						console.log("Difference: ", difference);
+						if (difference > 3000 || difference < -3000) {
+							console.log(
+								"System time difference is bigger than 3 seconds."
+							);
+						}
+						systemDifference.value = difference;
+					}
+				});
+			} else {
+				loading.value = false;
+				exists.value = false;
 			}
 		});
-	});
 
-	socket.on("event:ratings.liked", res => {
-		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
-				updateCurrentSongRatings(res.data);
+		socket.dispatch(
+			"stations.existsByName",
+			stationIdentifier.value,
+			res => {
+				if (res.status === "error" || !res.data.exists) {
+					// station identifier may be using stationid instead
+					socket.dispatch(
+						"stations.existsById",
+						stationIdentifier.value,
+						res => {
+							if (res.status === "error" || !res.data.exists) {
+								loading.value = false;
+								exists.value = false;
+							}
+						}
+					);
+				}
 			}
-		}
-	});
+		);
+
+		socket.on("event:station.nextSong", res => {
+			const { currentSong, startedAt, paused, timePaused } = res.data;
 
-	socket.on("event:ratings.disliked", res => {
-		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
-				updateCurrentSongRatings(res.data);
+			setCurrentSong({
+				currentSong,
+				startedAt,
+				paused,
+				timePaused,
+				pausedAt: 0
+			});
+		});
+
+		socket.on("event:station.pause", res => {
+			pausedAt.value = res.data.pausedAt;
+			updateStationPaused(true);
+			pauseLocalPlayer();
+
+			clearTimeout(window.stationNextSongTimeout);
+		});
+
+		socket.on("event:station.resume", res => {
+			timePaused.value = res.data.timePaused;
+			updateStationPaused(false);
+			if (!localPaused.value) resumeLocalPlayer();
+
+			autoRequestSong();
+		});
+
+		socket.on("event:station.deleted", () => {
+			router.push({
+				path: "/",
+				query: {
+					toast: "The station you were in was deleted."
+				}
+			});
+		});
+
+		socket.on("event:ratings.liked", res => {
+			if (!noSong.value) {
+				if (res.data.youtubeId === currentSong.value.youtubeId) {
+					updateCurrentSongRatings(res.data);
+				}
 			}
-		}
-	});
+		});
 
-	socket.on("event:ratings.unliked", res => {
-		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
-				updateCurrentSongRatings(res.data);
+		socket.on("event:ratings.disliked", res => {
+			if (!noSong.value) {
+				if (res.data.youtubeId === currentSong.value.youtubeId) {
+					updateCurrentSongRatings(res.data);
+				}
 			}
-		}
-	});
+		});
 
-	socket.on("event:ratings.undisliked", res => {
-		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
-				updateCurrentSongRatings(res.data);
+		socket.on("event:ratings.unliked", res => {
+			if (!noSong.value) {
+				if (res.data.youtubeId === currentSong.value.youtubeId) {
+					updateCurrentSongRatings(res.data);
+				}
 			}
-		}
-	});
+		});
 
-	socket.on("event:ratings.updated", res => {
-		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
-				updateOwnCurrentSongRatings(res.data);
+		socket.on("event:ratings.undisliked", res => {
+			if (!noSong.value) {
+				if (res.data.youtubeId === currentSong.value.youtubeId) {
+					updateCurrentSongRatings(res.data);
+				}
 			}
-		}
-	});
+		});
 
-	socket.on("event:station.queue.updated", res => {
-		updateSongsList(res.data.queue);
+		socket.on("event:ratings.updated", res => {
+			if (!noSong.value) {
+				if (res.data.youtubeId === currentSong.value.youtubeId) {
+					updateOwnCurrentSongRatings(res.data);
+				}
+			}
+		});
 
-		let nextSong = null;
-		if (songsList.value[0])
-			nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
+		socket.on("event:station.queue.updated", res => {
+			updateSongsList(res.data.queue);
 
-		updateNextSong(nextSong);
+			let nextSong = null;
+			if (songsList.value[0])
+				nextSong = songsList.value[0].youtubeId
+					? songsList.value[0]
+					: null;
 
-		autoRequestSong();
-	});
+			updateNextSong(nextSong);
 
-	socket.on("event:station.queue.song.repositioned", res => {
-		repositionSongInList(res.data.song);
+			autoRequestSong();
+		});
 
-		let nextSong = null;
-		if (songsList.value[0])
-			nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
+		socket.on("event:station.queue.song.repositioned", res => {
+			repositionSongInList(res.data.song);
 
-		updateNextSong(nextSong);
-	});
+			let nextSong = null;
+			if (songsList.value[0])
+				nextSong = songsList.value[0].youtubeId
+					? songsList.value[0]
+					: null;
 
-	socket.on("event:station.toggleSkipVote", res => {
-		if (currentSong.value)
-			updateCurrentSongSkipVotes({
-				skipVotes: res.data.voted
-					? currentSong.value.skipVotes + 1
-					: currentSong.value.skipVotes - 1,
-				skipVotesCurrent: null,
-				voted:
-					res.data.userId === userId.value
-						? res.data.voted
-						: currentSong.value.voted
-			});
-	});
+			updateNextSong(nextSong);
+		});
 
-	socket.on("event:station.updated", async res => {
-		const { name, theme, privacy } = res.data.station;
+		socket.on("event:station.toggleSkipVote", res => {
+			if (currentSong.value)
+				updateCurrentSongSkipVotes({
+					skipVotes: res.data.voted
+						? currentSong.value.skipVotes + 1
+						: currentSong.value.skipVotes - 1,
+					skipVotesCurrent: null,
+					voted:
+						res.data.userId === userId.value
+							? res.data.voted
+							: currentSong.value.voted
+				});
+		});
 
-		if (!hasPermission("stations.view") && privacy === "private") {
-			router.push({
-				path: "/",
-				query: {
-					toast: "The station you were in was made private."
+		socket.on("event:station.updated", async res => {
+			const { name, theme, privacy } = res.data.station;
+
+			if (!hasPermission("stations.view") && privacy === "private") {
+				router.push({
+					path: "/",
+					query: {
+						toast: "The station you were in was made private."
+					}
+				});
+			} else {
+				if (station.value.name !== name) {
+					await router.push(
+						`${name}?${Object.keys(route.query)
+							.map(
+								key =>
+									`${encodeURIComponent(
+										key
+									)}=${encodeURIComponent(
+										JSON.stringify(route.query[key])
+									)}`
+							)
+							.join("&")}`
+					);
+
+					// eslint-disable-next-line no-restricted-globals
+					history.replaceState({ ...history.state, ...{} }, null);
 				}
-			});
-		} else {
-			if (station.value.name !== name) {
-				await router.push(
-					`${name}?${Object.keys(route.query)
-						.map(
-							key =>
-								`${encodeURIComponent(
-									key
-								)}=${encodeURIComponent(
-									JSON.stringify(route.query[key])
-								)}`
-						)
-						.join("&")}`
-				);
 
-				// eslint-disable-next-line no-restricted-globals
-				history.replaceState({ ...history.state, ...{} }, null);
+				if (station.value.theme !== theme)
+					document.getElementsByTagName(
+						"html"
+					)[0].style.cssText = `--primary-color: var(--${theme})`;
+
+				updateStation(res.data.station);
 			}
+		});
 
-			if (station.value.theme !== theme)
-				document.getElementsByTagName(
-					"html"
-				)[0].style.cssText = `--primary-color: var(--${theme})`;
+		socket.on("event:station.users.updated", res =>
+			updateUsers(res.data.users)
+		);
 
-			updateStation(res.data.station);
-		}
-	});
+		socket.on("event:station.userCount.updated", res =>
+			updateUserCount(res.data.userCount)
+		);
 
-	socket.on("event:station.users.updated", res =>
-		updateUsers(res.data.users)
-	);
+		socket.on("event:user.station.favorited", res => {
+			if (res.data.stationId === station.value._id)
+				updateIfStationIsFavorited({ isFavorited: true });
+		});
 
-	socket.on("event:station.userCount.updated", res =>
-		updateUserCount(res.data.userCount)
-	);
+		socket.on("event:user.station.unfavorited", res => {
+			if (res.data.stationId === station.value._id)
+				updateIfStationIsFavorited({ isFavorited: false });
+		});
 
-	socket.on("event:user.station.favorited", res => {
-		if (res.data.stationId === station.value._id)
-			updateIfStationIsFavorited({ isFavorited: true });
-	});
+		socket.on("event:station.djs.added", res => {
+			if (res.data.user._id === userId.value)
+				updatePermissions().then(() => {
+					if (
+						!hasPermission("stations.view") &&
+						station.value.privacy === "private"
+					)
+						router.push({
+							path: "/",
+							query: {
+								toast: "You no longer have access to the station you were in."
+							}
+						});
+				});
+			addDj(res.data.user);
+		});
 
-	socket.on("event:user.station.unfavorited", res => {
-		if (res.data.stationId === station.value._id)
-			updateIfStationIsFavorited({ isFavorited: false });
-	});
+		socket.on("event:station.djs.removed", res => {
+			if (res.data.user._id === userId.value)
+				updatePermissions().then(() => {
+					if (
+						!hasPermission("stations.view") &&
+						station.value.privacy === "private"
+					)
+						router.push({
+							path: "/",
+							query: {
+								toast: "You no longer have access to the station you were in."
+							}
+						});
+				});
+			removeDj(res.data.user);
+		});
 
-	socket.on("event:station.djs.added", res => {
-		if (res.data.user._id === userId.value)
+		socket.on("keep.event:user.role.updated", () => {
 			updatePermissions().then(() => {
 				if (
 					!hasPermission("stations.view") &&
@@ -1375,39 +1366,56 @@ onMounted(async () => {
 						}
 					});
 			});
-		addDj(res.data.user);
+		});
 	});
 
-	socket.on("event:station.djs.removed", res => {
-		if (res.data.user._id === userId.value)
-			updatePermissions().then(() => {
-				if (
-					!hasPermission("stations.view") &&
-					station.value.privacy === "private"
-				)
-					router.push({
-						path: "/",
-						query: {
-							toast: "You no longer have access to the station you were in."
-						}
-					});
-			});
-		removeDj(res.data.user);
-	});
+	socket.onDisconnect(() => {
+		const _currentSong = currentSong.value;
+		if (nextSong.value)
+			setNextCurrentSong(
+				{
+					currentSong: nextSong.value,
+					startedAt: Date.now() + getTimeRemaining(),
+					paused: false,
+					timePaused: 0
+				},
+				true
+			);
+		else
+			setNextCurrentSong(
+				{
+					currentSong: null,
+					startedAt: 0,
+					paused: false,
+					timePaused: 0,
+					pausedAt: 0
+				},
+				true
+			);
+		window.stationNextSongTimeout = setTimeout(() => {
+			if (!noSong.value && currentSong.value._id === _currentSong._id)
+				skipSong();
+		}, getTimeRemaining());
+	}, true);
 
-	socket.on("keep.event:user.role.updated", () => {
-		updatePermissions().then(() => {
-			if (
-				!hasPermission("stations.view") &&
-				station.value.privacy === "private"
-			)
-				router.push({
-					path: "/",
-					query: {
-						toast: "You no longer have access to the station you were in."
-					}
-				});
-		});
+	frontendDevMode.value = await lofig.get("mode");
+	mediasession.value = await lofig.get("siteSettings.mediasession");
+	christmas.value = await lofig.get("siteSettings.christmas");
+	sitename.value = await lofig.get("siteSettings.sitename");
+
+	ms.setListeners(0, {
+		play: () => {
+			if (hasPermission("stations.playback.toggle")) resumeStation();
+			else resumeLocalStation();
+		},
+		pause: () => {
+			if (hasPermission("stations.playback.toggle")) pauseStation();
+			else pauseLocalStation();
+		},
+		nexttrack: () => {
+			if (hasPermission("stations.skip")) skipStation();
+			else if (!currentSong.value.voted) toggleSkipVote();
+		}
 	});
 
 	if (JSON.parse(localStorage.getItem("muted"))) {

+ 1 - 3
frontend/src/stores/userPlaylists.ts

@@ -3,12 +3,10 @@ import { Playlist } from "@/types/playlist";
 
 export const useUserPlaylistsStore = defineStore("userPlaylists", {
 	state: () => ({
-		playlists: <Playlist[]>[],
-		fetchedPlaylists: false
+		playlists: <Playlist[]>[]
 	}),
 	actions: {
 		setPlaylists(playlists) {
-			this.fetchedPlaylists = true;
 			this.playlists = playlists;
 		},
 		updatePlaylists(playlists) {