Ver Fonte

fix: many frontend eslint errors and a few TS errors

Kristian Vos há 1 ano atrás
pai
commit
927a0221f1

+ 19 - 7
frontend/src/components/modals/ConvertSpotifySongs.vue

@@ -103,7 +103,6 @@ const youtubeVideoUrlRegex =
 const youtubeVideoIdRegex = /^([\w-]{11})$/;
 
 const youtubePlaylistUrlRegex = /[\\?&]list=([^&#]*)/;
-const youtubeChannelUrlRegex = /channel\/([A-Za-z0-9]+)\/?/;
 
 const filteredSpotifySongs = computed(() =>
 	hideSpotifySongsWithNoAlternativesFound.value
@@ -140,6 +139,7 @@ const filteredSpotifyArtists = computed(() => {
 			if (nameA === nameB) return 0;
 			if (nameA < nameB) return -1;
 			if (nameA > nameB) return 1;
+			return 0;
 		};
 	else if (loadedSpotifyArtists.value && sortArtistMode.value === "NAME_DESC")
 		sortFn = (artistA, artistB) => {
@@ -149,6 +149,7 @@ const filteredSpotifyArtists = computed(() => {
 			if (nameA === nameB) return 0;
 			if (nameA > nameB) return -1;
 			if (nameA < nameB) return 1;
+			return 0;
 		};
 
 	if (sortFn) artists = artists.sort(sortFn);
@@ -176,6 +177,7 @@ const filteredSpotifyAlbums = computed(() => {
 			if (nameA === nameB) return 0;
 			if (nameA < nameB) return -1;
 			if (nameA > nameB) return 1;
+			return 0;
 		};
 	else if (loadedSpotifyAlbums.value && sortAlbumMode.value === "NAME_DESC")
 		sortFn = (albumA, albumB) => {
@@ -185,6 +187,7 @@ const filteredSpotifyAlbums = computed(() => {
 			if (nameA === nameB) return 0;
 			if (nameA > nameB) return -1;
 			if (nameA < nameB) return 1;
+			return 0;
 		};
 
 	if (sortFn) albums = albums.sort(sortFn);
@@ -271,6 +274,8 @@ const preferredAlternativeSongPerTrack = computed(() => {
 					if (!aHasLyrics && bHasLyrics) return 1;
 					return 0;
 				}
+
+				return 0;
 			};
 
 			if (
@@ -372,8 +377,10 @@ const openReplaceAlbumModal = (spotifyAlbumId, youtubePlaylistId) => {
 	if (
 		!spotifyAlbums[spotifyAlbumId] ||
 		!spotifyAlbums[spotifyAlbumId].rawData
-	)
-		return new Toast("Album hasn't loaded yet.");
+	) {
+		new Toast("Album hasn't loaded yet.");
+		return;
+	}
 
 	openModal({
 		modal: "replaceSpotifySongs",
@@ -397,7 +404,7 @@ const openReplaceAlbumModalFromUrl = spotifyAlbumId => {
 	const youtubePlaylistUrlRegexMatches =
 		youtubePlaylistUrlRegex.exec(replacementUrl);
 	if (youtubePlaylistUrlRegexMatches)
-		youtubePlaylistId = youtubePlaylistUrlRegexMatches[0];
+		[youtubePlaylistId] = youtubePlaylistUrlRegexMatches;
 
 	console.log("Open modal for ", youtubePlaylistId);
 
@@ -410,8 +417,10 @@ const openReplaceArtistModal = (spotifyArtistId, youtubeChannelUrl) => {
 	if (
 		!spotifyArtists[spotifyArtistId] ||
 		!spotifyArtists[spotifyArtistId].rawData
-	)
-		return new Toast("Artist hasn't loaded yet.");
+	) {
+		new Toast("Artist hasn't loaded yet.");
+		return;
+	}
 
 	openModal({
 		modal: "replaceSpotifySongs",
@@ -461,7 +470,10 @@ const replaceSongFromUrl = spotifyMediaSource => {
 	if (youtubeVideoIdRegexMatches)
 		newMediaSource = `youtube:${youtubeVideoIdRegexMatches[0]}`;
 
-	if (!newMediaSource) return new Toast("Invalid URL/identifier specified.");
+	if (!newMediaSource) {
+		new Toast("Invalid URL/identifier specified.");
+		return;
+	}
 
 	replaceSpotifySong(spotifyMediaSource, newMediaSource);
 };

+ 55 - 52
frontend/src/components/modals/EditSong/index.vue

@@ -76,7 +76,6 @@ const {
 	soundcloudGetCurrentSound,
 	soundcloudGetTrackId,
 	soundcloudBindListener,
-	soundcloudOnTrackStateChange,
 	soundcloudUnload
 } = useSoundcloudPlayer();
 
@@ -211,6 +210,16 @@ const {
 
 const { updateMediaModalPlayingAudio } = stationStore;
 
+const playerHardStop = () => {
+	if (
+		youtubePlayerReady.value &&
+		video.value.player &&
+		video.value.player.stopVideo
+	)
+		video.value.player.stopVideo();
+	soundcloudUnload();
+};
+
 const unloadSong = (_mediaSource, songId?) => {
 	songDataLoaded.value = false;
 	songDeleted.value = false;
@@ -285,6 +294,15 @@ const onSaving = mediaSource => {
 	if (itemIndex > -1) items.value[itemIndex].status = "saving";
 };
 
+const getCurrentSongMediaType = mediaSource => {
+	if (!mediaSource || mediaSource.indexOf(":") === -1) return "none";
+	return mediaSource.split(":")[0];
+};
+const getCurrentSongMediaValue = mediaSource => {
+	if (!mediaSource || mediaSource.indexOf(":") === -1) return null;
+	return mediaSource.split(":")[1];
+};
+
 const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
 	{
 		title: {
@@ -339,13 +357,17 @@ const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
 		mediaSource: {
 			value: "",
 			validate: value => {
-				if (currentSongMediaType.value === "none")
+				if (
+					getCurrentSongMediaType(inputs.value.mediaSource.value) ===
+					"none"
+				)
 					return "Media source type is not valid.";
-				if (!currentSongMediaValue.value)
+				if (!getCurrentSongMediaValue(inputs.value.mediaSource.value))
 					return "Media source value is not valid.";
 
 				if (
-					currentSongMediaType.value === "youtube" &&
+					getCurrentSongMediaType(inputs.value.mediaSource.value) ===
+						"youtube" &&
 					!newSong.value &&
 					youtubeError.value &&
 					inputs.value.mediaSource.originalValue !== value
@@ -473,24 +495,15 @@ const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
 	{ modalUuid: props.modalUuid, preventCloseUnsaved: false }
 );
 
-const currentSongMediaType = computed(() => {
-	if (
-		!inputs.value.mediaSource.value ||
-		inputs.value.mediaSource.value.indexOf(":") === -1
-	)
-		return "none";
-	return inputs.value.mediaSource.value.split(":")[0];
-});
-const currentSongMediaValue = computed(() => {
-	if (
-		!inputs.value.mediaSource.value ||
-		inputs.value.mediaSource.value.indexOf(":") === -1
-	)
-		return null;
-	return inputs.value.mediaSource.value.split(":")[1];
-});
+const currentSongMediaType = computed(() =>
+	getCurrentSongMediaType(inputs.value.mediaSource.value)
+);
+const currentSongMediaValue = computed(() =>
+	getCurrentSongMediaValue(inputs.value.mediaSource.value)
+);
+
 const getCurrentPlayerTime = () =>
-	new Promise<number>((resolve, reject) => {
+	new Promise<number>(resolve => {
 		if (
 			currentSongMediaType.value === "youtube" &&
 			youtubePlayerReady.value
@@ -514,7 +527,7 @@ const getCurrentPlayerTime = () =>
 	});
 
 const getPlayerDuration = () =>
-	new Promise<number>((resolve, reject) => {
+	new Promise<number>(resolve => {
 		if (
 			currentSongMediaType.value === "youtube" &&
 			youtubePlayerReady.value
@@ -733,6 +746,26 @@ const drawCanvas = async () => {
 	ctx.fillRect(widthCurrentTime, 0, 1, 20);
 };
 
+const playerPlay = () => {
+	if (currentSongMediaType.value === "youtube") {
+		soundcloudPause();
+		if (
+			youtubePlayerReady.value &&
+			video.value.player &&
+			video.value.player.playVideo
+		)
+			video.value.player.playVideo();
+	} else if (currentSongMediaType.value === "soundcloud") {
+		if (
+			youtubePlayerReady.value &&
+			video.value.player &&
+			video.value.player.pauseVideo
+		)
+			video.value.player.pauseVideo();
+		soundcloudPlay();
+	}
+};
+
 const seekTo = (position, play = true) => {
 	if (currentSongMediaType.value === "youtube") {
 		if (play) {
@@ -868,16 +901,6 @@ const fillDuration = () => {
 	}
 };
 
-const playerHardStop = () => {
-	if (
-		youtubePlayerReady.value &&
-		video.value.player &&
-		video.value.player.stopVideo
-	)
-		video.value.player.stopVideo();
-	soundcloudUnload();
-};
-
 const playerPause = () => {
 	if (
 		youtubePlayerReady.value &&
@@ -888,26 +911,6 @@ const playerPause = () => {
 	soundcloudPause();
 };
 
-const playerPlay = () => {
-	if (currentSongMediaType.value === "youtube") {
-		soundcloudPause();
-		if (
-			youtubePlayerReady.value &&
-			video.value.player &&
-			video.value.player.playVideo
-		)
-			video.value.player.playVideo();
-	} else if (currentSongMediaType.value === "soundcloud") {
-		if (
-			youtubePlayerReady.value &&
-			video.value.player &&
-			video.value.player.pauseVideo
-		)
-			video.value.player.pauseVideo();
-		soundcloudPlay();
-	}
-};
-
 const settings = type => {
 	switch (type) {
 		case "stop":

+ 1 - 12
frontend/src/components/modals/ImportArtist.vue

@@ -1,35 +1,24 @@
 <script setup lang="ts">
 import {
 	defineAsyncComponent,
-	ref,
 	reactive,
 	onMounted,
 	onBeforeUnmount
 } from "vue";
-import Toast from "toasters";
-import { DraggableList } from "vue-draggable-list";
-import { useWebsocketsStore } from "@/stores/websockets";
-import { useModalsStore } from "@/stores/modals";
 
 import { useYoutubeChannel } from "@/composables/useYoutubeChannel";
 import { useSoundcloudArtist } from "@/composables/useSoundcloudArtist";
 import { useSpotifyArtist } from "@/composables/useSpotifyArtist";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
-const SongItem = defineAsyncComponent(
-	() => import("@/components/SongItem.vue")
-);
 const ArtistItem = defineAsyncComponent(
 	() => import("@/components/ArtistItem.vue")
 );
 
-const props = defineProps({
+defineProps({
 	modalUuid: { type: String, required: true }
 });
 
-const { socket } = useWebsocketsStore();
-const { closeCurrentModal } = useModalsStore();
-
 const { youtubeChannelURLOrID, getYoutubeChannel, getYoutubeChannelVideos } =
 	useYoutubeChannel();
 const {

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

@@ -160,7 +160,9 @@ onMounted(async () => {
 
 		recaptchaScript.setAttribute(
 			"src",
-			`https://www.google.com/recaptcha/api.js?render=${recaptcha.value.key}`
+			`https://www.google.com/recaptcha/api.js?render=${configStore.get(
+				"recaptcha.key"
+			)}`
 		);
 		document.head.appendChild(recaptchaScript);
 	}

+ 3 - 3
frontend/src/components/modals/ReplaceSpotifySongs.vue

@@ -14,9 +14,9 @@ const props = defineProps({
 	modalUuid: { type: String, required: true },
 	spotifyAlbum: { type: Object, default: () => ({}) },
 	spotifyTracks: { type: Array, default: () => [] },
-	playlistId: { type: String },
-	youtubePlaylistId: { type: String },
-	youtubeChannelUrl: { type: String }
+	playlistId: { type: String, required: true },
+	youtubePlaylistId: { type: String, default: null },
+	youtubeChannelUrl: { type: String, default: null }
 });
 
 const { socket } = useWebsocketsStore();

+ 55 - 55
frontend/src/composables/useSearchSpotify.ts

@@ -16,62 +16,62 @@ export const useSearchSpotify = () => {
 
 	const { socket } = useWebsocketsStore();
 
-	const searchForSongs = () => {
-		// let { query } = spotifySearch.value.songs;
-		// if (query.indexOf("&index=") !== -1) {
-		// 	const splitQuery = query.split("&index=");
-		// 	splitQuery.pop();
-		// 	query = splitQuery.join("");
-		// }
-		// if (query.indexOf("&list=") !== -1) {
-		// 	const splitQuery = query.split("&list=");
-		// 	splitQuery.pop();
-		// 	query = splitQuery.join("");
-		// }
-		// socket.dispatch("apis.searchSpotify", query, res => {
-		// 	if (res.status === "success") {
-		// 		spotifySearch.value.songs.nextPageToken =
-		// 			res.data.nextPageToken;
-		// 		spotifySearch.value.songs.results = [];
-		// 		res.data.items.forEach(result => {
-		// 			spotifySearch.value.songs.results.push({
-		// 				id: result.id.videoId,
-		// 				url: `https://www.spotify.com/watch?v=${result.id.videoId}`,
-		// 				title: result.snippet.title,
-		// 				thumbnail: result.snippet.thumbnails.default.url,
-		// 				channelId: result.snippet.channelId,
-		// 				channelTitle: result.snippet.channelTitle,
-		// 				isAddedToQueue: false
-		// 			});
-		// 		});
-		// 	} else if (res.status === "error") new Toast(res.message);
-		// });
-	};
+	// const searchForSongs = () => {
+	// let { query } = spotifySearch.value.songs;
+	// if (query.indexOf("&index=") !== -1) {
+	// 	const splitQuery = query.split("&index=");
+	// 	splitQuery.pop();
+	// 	query = splitQuery.join("");
+	// }
+	// if (query.indexOf("&list=") !== -1) {
+	// 	const splitQuery = query.split("&list=");
+	// 	splitQuery.pop();
+	// 	query = splitQuery.join("");
+	// }
+	// socket.dispatch("apis.searchSpotify", query, res => {
+	// 	if (res.status === "success") {
+	// 		spotifySearch.value.songs.nextPageToken =
+	// 			res.data.nextPageToken;
+	// 		spotifySearch.value.songs.results = [];
+	// 		res.data.items.forEach(result => {
+	// 			spotifySearch.value.songs.results.push({
+	// 				id: result.id.videoId,
+	// 				url: `https://www.spotify.com/watch?v=${result.id.videoId}`,
+	// 				title: result.snippet.title,
+	// 				thumbnail: result.snippet.thumbnails.default.url,
+	// 				channelId: result.snippet.channelId,
+	// 				channelTitle: result.snippet.channelTitle,
+	// 				isAddedToQueue: false
+	// 			});
+	// 		});
+	// 	} else if (res.status === "error") new Toast(res.message);
+	// });
+	// };
 
-	const loadMoreSongs = () => {
-		// socket.dispatch(
-		// 	"apis.searchSpotifyForPage",
-		// 	spotifySearch.value.songs.query,
-		// 	spotifySearch.value.songs.nextPageToken,
-		// 	res => {
-		// 		if (res.status === "success") {
-		// 			spotifySearch.value.songs.nextPageToken =
-		// 				res.data.nextPageToken;
-		// 			res.data.items.forEach(result => {
-		// 				spotifySearch.value.songs.results.push({
-		// 					id: result.id.videoId,
-		// 					url: `https://www.spotify.com/watch?v=${result.id.videoId}`,
-		// 					title: result.snippet.title,
-		// 					thumbnail: result.snippet.thumbnails.default.url,
-		// 					channelId: result.snippet.channelId,
-		// 					channelTitle: result.snippet.channelTitle,
-		// 					isAddedToQueue: false
-		// 				});
-		// 			});
-		// 		} else if (res.status === "error") new Toast(res.message);
-		// 	}
-		// );
-	};
+	// const loadMoreSongs = () => {
+	// socket.dispatch(
+	// 	"apis.searchSpotifyForPage",
+	// 	spotifySearch.value.songs.query,
+	// 	spotifySearch.value.songs.nextPageToken,
+	// 	res => {
+	// 		if (res.status === "success") {
+	// 			spotifySearch.value.songs.nextPageToken =
+	// 				res.data.nextPageToken;
+	// 			res.data.items.forEach(result => {
+	// 				spotifySearch.value.songs.results.push({
+	// 					id: result.id.videoId,
+	// 					url: `https://www.spotify.com/watch?v=${result.id.videoId}`,
+	// 					title: result.snippet.title,
+	// 					thumbnail: result.snippet.thumbnails.default.url,
+	// 					channelId: result.snippet.channelId,
+	// 					channelTitle: result.snippet.channelTitle,
+	// 					isAddedToQueue: false
+	// 				});
+	// 			});
+	// 		} else if (res.status === "error") new Toast(res.message);
+	// 	}
+	// );
+	// };
 
 	const addSpotifySongToPlaylist = (playlistId, id, index) => {
 		socket.dispatch(

+ 14 - 14
frontend/src/composables/useSoundcloudPlayer.ts

@@ -132,6 +132,20 @@ export const useSoundcloudPlayer = () => {
 		}
 	};
 
+	const soundcloudGetIsPaused = callback => {
+		let called = false;
+
+		const _callback = value => {
+			if (called) return;
+			called = true;
+
+			callback(value);
+		};
+		addMethodCallback("isPaused", _callback);
+
+		dispatchMessage("isPaused");
+	};
+
 	const attemptToPlay = () => {
 		if (trackState.value === "playing") return;
 
@@ -248,20 +262,6 @@ export const useSoundcloudPlayer = () => {
 		dispatchMessage("getDuration");
 	};
 
-	const soundcloudGetIsPaused = callback => {
-		let called = false;
-
-		const _callback = value => {
-			if (called) return;
-			called = true;
-
-			callback(value);
-		};
-		addMethodCallback("isPaused", _callback);
-
-		dispatchMessage("isPaused");
-	};
-
 	const soundcloudGetState = () => trackState.value;
 
 	const soundcloudGetCurrentSound = callback => {

+ 1 - 1
frontend/src/composables/useSpotifyArtist.ts

@@ -1,4 +1,4 @@
-import { ref, reactive } from "vue";
+import { ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 

+ 0 - 2
frontend/src/composables/useYoutubeVideo.ts

@@ -86,8 +86,6 @@ type YoutubeVideo = {
 	uploadedAt: Date;
 };
 
-const youtubeVideoURLRegex =
-	/^(https?:\/\/)?(www\.)?(m\.)?(music\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?(?<youtubeId>[\w-]{11})((&([A-Za-z0-9]+)?)*)?$/;
 const youtubeVideoIDRegex = /^([\w-]{11})$/;
 
 export const useYoutubeVideo = () => {

+ 1 - 257
frontend/src/pages/Admin/YouTube/Channels.vue

@@ -1,10 +1,5 @@
 <script setup lang="ts">
 import { defineAsyncComponent, ref } from "vue";
-import Toast from "toasters";
-import { useWebsocketsStore } from "@/stores/websockets";
-import { useLongJobsStore } from "@/stores/longJobs";
-import { useModalsStore } from "@/stores/modals";
-import { useUserAuthStore } from "@/stores/userAuth";
 import {
 	TableColumn,
 	TableFilter,
@@ -19,16 +14,6 @@ const AdvancedTable = defineAsyncComponent(
 const RunJobDropdown = defineAsyncComponent(
 	() => import("@/components/RunJobDropdown.vue")
 );
-const SongThumbnail = defineAsyncComponent(
-	() => import("@/components/SongThumbnail.vue")
-);
-
-const { setJob } = useLongJobsStore();
-
-const { socket } = useWebsocketsStore();
-
-const userAuthStore = useUserAuthStore();
-const { hasPermission } = userAuthStore;
 
 const columnDefault = ref<TableColumn>({
 	sortable: true,
@@ -49,26 +34,7 @@ const columns = ref<TableColumn[]>([
 		resizable: false,
 		minWidth: 85,
 		defaultWidth: 85
-		// 	(hasPermission("songs.create") || hasPermission("songs.update")) &&
-		// 	hasPermission("youtube.removeVideos")
-		// 		? 129
-		// 		: 85,
-		// defaultWidth:
-		// 	(hasPermission("songs.create") || hasPermission("songs.update")) &&
-		// 	hasPermission("youtube.removeVideos")
-		// 		? 129
-		// 		: 85
 	},
-	// {
-	// 	name: "thumbnailImage",
-	// 	displayName: "Thumb",
-	// 	properties: ["youtubeId"],
-	// 	sortable: false,
-	// 	minWidth: 75,
-	// 	defaultWidth: 75,
-	// 	maxWidth: 75,
-	// 	resizable: false
-	// },
 	{
 		name: "channelId",
 		displayName: "Channel ID",
@@ -142,27 +108,6 @@ const filters = ref<TableFilter[]>([
 		filterTypes: ["datetimeBefore", "datetimeAfter"],
 		defaultFilterType: "datetimeBefore"
 	}
-	// {
-	// 	name: "importJob",
-	// 	displayName: "Import Job",
-	// 	property: "importJob",
-	// 	filterTypes: ["special"],
-	// 	defaultFilterType: "special"
-	// },
-	// {
-	// 	name: "songId",
-	// 	displayName: "Song ID",
-	// 	property: "songId",
-	// 	filterTypes: ["contains", "exact", "regex"],
-	// 	defaultFilterType: "contains"
-	// },
-	// {
-	// 	name: "uploadedAt",
-	// 	displayName: "Uploaded At",
-	// 	property: "uploadedAt",
-	// 	filterTypes: ["datetimeBefore", "datetimeAfter"],
-	// 	defaultFilterType: "datetimeBefore"
-	// }
 ]);
 const events = ref<TableEvents>({
 	adminRoom: "youtubeChannels",
@@ -178,78 +123,11 @@ const events = ref<TableEvents>({
 });
 const bulkActions = ref<TableBulkActions>({ width: 200 });
 const jobs = ref([]);
-// if (hasPermission("media.recalculateAllRatings"))
+
 jobs.value.push({
 	name: "Get missing YouTube channels from YouTube video's",
 	socket: "youtube.getMissingChannels"
 });
-
-const { openModal } = useModalsStore();
-
-// const rowToSong = row => ({
-// 	mediaSource: `youtube:${row.channelId}`
-// });
-
-// const editOne = row => {
-// 	openModal({
-// 		modal: "editSong",
-// 		props: { song: rowToSong(row) }
-// 	});
-// };
-
-// const editMany = selectedRows => {
-// 	if (selectedRows.length === 1) editOne(rowToSong(selectedRows[0]));
-// 	else {
-// 		const songs = selectedRows.map(rowToSong);
-// 		openModal({ modal: "editSong", props: { songs } });
-// 	}
-// };
-
-// const importAlbum = selectedRows => {
-// 	const mediaSources = selectedRows.map(
-// 		({ youtubeId }) => `youtube:${youtubeId}`
-// 	);
-// 	console.log(77988, mediaSources);
-// 	socket.dispatch("songs.getSongsFromMediaSources", mediaSources, res => {
-// 		if (res.status === "success") {
-// 			openModal({
-// 				modal: "importAlbum",
-// 				props: { songs: res.data.songs }
-// 			});
-// 		} else new Toast("Could not get songs.");
-// 	});
-// };
-
-// const bulkEditPlaylist = selectedRows => {
-// 	openModal({
-// 		modal: "bulkEditPlaylist",
-// 		props: {
-// 			mediaSources: selectedRows.map(row => `youtube:${row.youtubeId}`)
-// 		}
-// 	});
-// };
-
-// const removeVideos = videoIds => {
-// 	let id;
-// 	let title;
-
-// 	socket.dispatch("youtube.removeVideos", videoIds, {
-// 		cb: () => {},
-// 		onProgress: res => {
-// 			if (res.status === "started") {
-// 				id = res.id;
-// 				title = res.title;
-// 			}
-
-// 			if (id)
-// 				setJob({
-// 					id,
-// 					name: title,
-// 					...res
-// 				});
-// 		}
-// 	});
-// };
 </script>
 
 <template>
@@ -274,66 +152,6 @@ const { openModal } = useModalsStore();
 			:max-width="1140"
 			:bulk-actions="bulkActions"
 		>
-			<template #column-options="slotProps">
-				<div class="row-options">
-					<!-- <button
-						class="button is-primary icon-with-button material-icons"
-						@click="
-							openModal({
-								modal: 'viewYoutubeChannel',
-								props: {
-									channelId: slotProps.item.channelId
-								}
-							})
-						"
-						:disabled="slotProps.item.removed"
-						content="View Video"
-						v-tippy
-					>
-						open_in_full
-					</button>
-					<button
-						v-if="
-							hasPermission('songs.create') ||
-							hasPermission('songs.update')
-						"
-						class="button is-primary icon-with-button material-icons"
-						@click="editOne(slotProps.item)"
-						:disabled="slotProps.item.removed"
-						:content="
-							!!slotProps.item.songId
-								? 'Edit Song'
-								: 'Create song from video'
-						"
-						v-tippy
-					>
-						music_note
-					</button>
-					<button
-						v-if="hasPermission('youtube.removeVideos')"
-						class="button is-danger icon-with-button material-icons"
-						@click.prevent="
-							openModal({
-								modal: 'confirm',
-								props: {
-									message:
-										'Removing this video will remove it from all playlists and cause a ratings recalculation.',
-									onCompleted: () =>
-										removeVideos(slotProps.item._id)
-								}
-							})
-						"
-						:disabled="slotProps.item.removed"
-						content="Delete Video"
-						v-tippy
-					>
-						delete_forever
-					</button> -->
-				</div>
-			</template>
-			<!-- <template #column-thumbnailImage="slotProps">
-				<song-thumbnail class="song-thumbnail" :song="slotProps.item" />
-			</template> -->
 			<template #column-channelId="slotProps">
 				<a
 					:href="`https://www.youtube.com/channels/${slotProps.item.channelId}`"
@@ -357,80 +175,6 @@ const { openModal } = useModalsStore();
 					utils.getDateFormatted(slotProps.item.createdAt)
 				}}</span>
 			</template>
-			<template #bulk-actions="slotProps">
-				<div class="bulk-actions">
-					<!-- <i
-						v-if="
-							hasPermission('songs.create') ||
-							hasPermission('songs.update')
-						"
-						class="material-icons create-songs-icon"
-						@click.prevent="editMany(slotProps.item)"
-						content="Create/edit songs from videos"
-						v-tippy
-						tabindex="0"
-					>
-						music_note
-					</i>
-					<i
-						v-if="
-							hasPermission('songs.create') ||
-							hasPermission('songs.update')
-						"
-						class="material-icons import-album-icon"
-						@click.prevent="importAlbum(slotProps.item)"
-						content="Import album from videos"
-						v-tippy
-						tabindex="0"
-					>
-						album
-					</i>
-					<i
-						v-if="hasPermission('playlists.songs.add')"
-						class="material-icons playlist-bulk-edit-icon"
-						@click.prevent="bulkEditPlaylist(slotProps.item)"
-						content="Add To Playlist"
-						v-tippy
-						tabindex="0"
-					>
-						playlist_add
-					</i>
-					<i
-						v-if="hasPermission('youtube.removeVideos')"
-						class="material-icons delete-icon"
-						@click.prevent="
-							openModal({
-								modal: 'confirm',
-								props: {
-									message:
-										'Removing these videos will remove them from all playlists and cause a ratings recalculation.',
-									onCompleted: () =>
-										removeVideos(
-											slotProps.item.map(
-												video => video._id
-											)
-										)
-								}
-							})
-						"
-						content="Delete Videos"
-						v-tippy
-						tabindex="0"
-					>
-						delete_forever
-					</i> -->
-				</div>
-			</template>
 		</advanced-table>
 	</div>
 </template>
-
-<style lang="less" scoped>
-// :deep(.song-thumbnail) {
-// 	width: 50px;
-// 	height: 50px;
-// 	min-width: 50px;
-// 	min-height: 50px;
-// 	margin: 0 auto;
-// }
-</style>

+ 1 - 1
frontend/src/pages/Station/Sidebar/History.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { defineAsyncComponent, computed, onMounted } from "vue";
+import { computed, onMounted } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";

+ 55 - 57
frontend/src/pages/Station/index.vue

@@ -220,15 +220,6 @@ const currentYoutubeId = computed(() => {
 	return currentSong.value.mediaSource.split(":")[1];
 });
 
-const currentSongIsYoutube = computed(() => {
-	if (
-		!currentSong.value ||
-		!currentSong.value.mediaSource.startsWith("youtube:")
-	)
-		return false;
-	return true;
-});
-
 const {
 	joinStation,
 	leaveStation,
@@ -376,7 +367,7 @@ const getTimeRemaining = () => {
 	return 0;
 };
 const getCurrentPlayerTime = () =>
-	new Promise<number>((resolve, reject) => {
+	new Promise<number>(resolve => {
 		if (
 			currentSongMediaType.value === "youtube" &&
 			youtubePlayerReady.value
@@ -442,6 +433,25 @@ const resizeSeekerbar = () => {
 	seekerbarPercentage.value =
 		(getTimeElapsed() / 1000 / currentSong.value.duration) * 100;
 };
+const playerSeekTo = position => {
+	console.debug("PLAYER SEEK TO", position);
+
+	// Position is in seconds
+	if (currentSongMediaType.value === "youtube" && youtubePlayerReady.value) {
+		youtubePlayer.value.seekTo(position);
+	}
+
+	if (currentSongMediaType.value === "soundcloud") {
+		soundcloudSeekTo(position * 1000);
+	}
+};
+const playerPause = () => {
+	if (youtubePlayerReady.value) {
+		youtubePlayer.value.pauseVideo();
+	}
+
+	soundcloudPause();
+};
 const calculateTimeElapsed = async () => {
 	if (experimentalChangableListenMode.value === "participate") return;
 	if (
@@ -510,7 +520,7 @@ const calculateTimeElapsed = async () => {
 			_playbackRate = 1.05;
 		} else if (
 			currentSongMediaType.value === "youtube" &&
-			youtubeReady.value &&
+			youtubePlayerReady.value &&
 			youtubePlayer.value.getPlaybackRate !== 1.0
 		) {
 			youtubePlayer.value.setPlaybackRate(1.0);
@@ -518,7 +528,7 @@ const calculateTimeElapsed = async () => {
 
 		if (
 			currentSongMediaType.value === "youtube" &&
-			youtubeReady.value &&
+			youtubePlayerReady.value &&
 			playbackRate.value !== _playbackRate
 		) {
 			youtubePlayer.value.setPlaybackRate(_playbackRate);
@@ -611,25 +621,6 @@ const playerStop = () => {
 
 	soundcloudDestroy();
 };
-const playerPause = () => {
-	if (youtubePlayerReady.value) {
-		youtubePlayer.value.pauseVideo();
-	}
-
-	soundcloudPause();
-};
-const playerSeekTo = position => {
-	console.debug("PLAYER SEEK TO", position);
-
-	// Position is in seconds
-	if (currentSongMediaType.value === "youtube" && youtubePlayerReady.value) {
-		youtubePlayer.value.seekTo(position);
-	}
-
-	if (currentSongMediaType.value === "soundcloud") {
-		soundcloudSeekTo(position * 1000);
-	}
-};
 const toggleSkipVote = (message?) => {
 	socket.dispatch("stations.toggleSkipVote", station.value._id, data => {
 		if (data.status !== "success") new Toast(`Error: ${data.message}`);
@@ -639,6 +630,31 @@ const toggleSkipVote = (message?) => {
 			);
 	});
 };
+const resumeLocalPlayer = () => {
+	if (configStore.get("experimental.media_session"))
+		updateMediaSessionData(currentSong.value);
+	if (!noSong.value) {
+		playerSeekTo(getTimeElapsed() / 1000 + currentSong.value.skipDuration);
+		playerPlay();
+	}
+};
+const resumeLocalStation = () => {
+	updateLocalPaused(false);
+	autoPaused.value = false;
+	if (!stationPaused.value) resumeLocalPlayer();
+};
+const pauseLocalPlayer = () => {
+	if (configStore.get("experimental.media_session"))
+		updateMediaSessionData(currentSong.value);
+	if (!noSong.value) {
+		timeBeforePause.value = getTimeElapsed();
+		playerPause();
+	}
+};
+const pauseLocalStation = () => {
+	updateLocalPaused(true);
+	pauseLocalPlayer();
+};
 const youtubeReady = () => {
 	if (experimentalChangableListenMode.value === "participate") return;
 	if (!youtubePlayer.value) {
@@ -983,31 +999,6 @@ const changeVolume = () => {
 
 	changePlayerVolume();
 };
-const resumeLocalPlayer = () => {
-	if (configStore.get("experimental.media_session"))
-		updateMediaSessionData(currentSong.value);
-	if (!noSong.value) {
-		playerSeekTo(getTimeElapsed() / 1000 + currentSong.value.skipDuration);
-		playerPlay();
-	}
-};
-const pauseLocalPlayer = () => {
-	if (configStore.get("experimental.media_session"))
-		updateMediaSessionData(currentSong.value);
-	if (!noSong.value) {
-		timeBeforePause.value = getTimeElapsed();
-		playerPause();
-	}
-};
-const resumeLocalStation = () => {
-	updateLocalPaused(false);
-	autoPaused.value = false;
-	if (!stationPaused.value) resumeLocalPlayer();
-};
-const pauseLocalStation = () => {
-	updateLocalPaused(true);
-	pauseLocalPlayer();
-};
 const skipStation = () => {
 	socket.dispatch("stations.forceSkip", station.value._id, data => {
 		if (data.status !== "success") new Toast(`Error: ${data.message}`);
@@ -1704,7 +1695,10 @@ onMounted(async () => {
 				);
 
 				// eslint-disable-next-line no-restricted-globals
-				history.replaceState({ ...history.state, ...{} }, null);
+				window.history.replaceState(
+					{ ...window.history.state, ...{} },
+					null
+				);
 			}
 
 			if (station.value.theme !== theme)
@@ -3180,6 +3174,7 @@ onBeforeUnmount(() => {
 
 					input[type="range"] {
 						-webkit-appearance: none;
+						appearance: none;
 						margin: 7.3px 0;
 					}
 
@@ -3206,6 +3201,7 @@ onBeforeUnmount(() => {
 						background: var(--primary-color);
 						cursor: pointer;
 						-webkit-appearance: none;
+						appearance: none;
 						margin-top: -6.5px;
 					}
 
@@ -3228,6 +3224,7 @@ onBeforeUnmount(() => {
 						background: var(--primary-color);
 						cursor: pointer;
 						-webkit-appearance: none;
+						appearance: none;
 						margin-top: -6.5px;
 					}
 					input[type="range"]::-ms-track {
@@ -3262,6 +3259,7 @@ onBeforeUnmount(() => {
 						background: var(--primary-color);
 						cursor: pointer;
 						-webkit-appearance: none;
+						appearance: none;
 						margin-top: 1.5px;
 					}
 				}

+ 4 - 3
frontend/src/stores/station.ts

@@ -3,6 +3,7 @@ import { Playlist } from "@/types/playlist";
 import { Song, CurrentSong } from "@/types/song";
 import { Station } from "@/types/station";
 import { User } from "@/types/user";
+import { StationHistory } from "@/types/stationHistory";
 import { useWebsocketsStore } from "@/stores/websockets";
 
 export const useStationStore = defineStore("station", {
@@ -25,7 +26,7 @@ export const useStationStore = defineStore("station", {
 		blacklist: Playlist[];
 		mediaModalPlayingAudio: boolean;
 		permissions: Record<string, boolean>;
-		history: any[];
+		history: StationHistory[];
 	} => ({
 		station: {},
 		autoRequest: [],
@@ -201,10 +202,10 @@ export const useStationStore = defineStore("station", {
 				}
 			});
 		},
-		setHistory(history) {
+		setHistory(history: StationHistory[]) {
 			this.history = history;
 		},
-		addHistoryItem(historyItem) {
+		addHistoryItem(historyItem: StationHistory) {
 			this.history.unshift(historyItem);
 		}
 	}

+ 21 - 0
frontend/src/types/stationHistory.ts

@@ -0,0 +1,21 @@
+export interface StationHistory {
+	_id: string;
+	stationId: string;
+	type: "song_played";
+	payload: {
+		song: {
+			_id: string;
+			mediaSource: string;
+			title: string;
+			artists: string[];
+			duration: number;
+			thumbnail: string;
+			requestedBy: string;
+			requestedAt: Date;
+			verified: boolean;
+		};
+		skippedAt: Date;
+		skipReason: "natural" | "force_skip" | "vote_skip" | "other";
+	};
+	documentVersion: number;
+}