Browse Source

feat: add option to use a YouTube video URL directly instead of searching for a video

Kristian Vos 1 year ago
parent
commit
18d0cb4c62

+ 26 - 0
frontend/src/components/Request.vue

@@ -6,6 +6,7 @@ import { useStationStore } from "@/stores/station";
 import { useManageStationStore } from "@/stores/manageStation";
 import { useSearchYoutube } from "@/composables/useSearchYoutube";
 import { useSearchMusare } from "@/composables/useSearchMusare";
+import { useYoutubeDirect } from "@/composables/useYoutubeDirect";
 
 const SongItem = defineAsyncComponent(
 	() => import("@/components/SongItem.vue")
@@ -25,6 +26,7 @@ const props = defineProps({
 
 const { youtubeSearch, searchForSongs, loadMoreSongs } = useSearchYoutube();
 const { musareSearch, searchForMusareSongs } = useSearchMusare();
+const { youtubeDirect, addToQueue } = useYoutubeDirect();
 
 const { socket } = useWebsocketsStore();
 const stationStore = useStationStore();
@@ -221,6 +223,30 @@ onMounted(async () => {
 					</div>
 				</div>
 
+				<div class="youtube-direct">
+					<label class="label"> Add a YouTube song from a URL </label>
+					<div class="control is-grouped input-with-button">
+						<p class="control is-expanded">
+							<input
+								class="input"
+								type="text"
+								placeholder="Enter your YouTube song URL here..."
+								v-model="youtubeDirect"
+								@keyup.enter="addToQueue(station._id)"
+							/>
+						</p>
+						<p class="control">
+							<a
+								class="button is-info"
+								@click="addToQueue(station._id)"
+								><i class="material-icons icon-with-button"
+									>add</i
+								>Add</a
+							>
+						</p>
+					</div>
+				</div>
+
 				<div class="youtube-search">
 					<label class="label"> Search for a song on YouTube </label>
 					<div class="control is-grouped input-with-button">

+ 21 - 0
frontend/src/components/modals/EditPlaylist/Tabs/AddSongs.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref, watch, onMounted } from "vue";
 import { storeToRefs } from "pinia";
 import { useSearchYoutube } from "@/composables/useSearchYoutube";
 import { useSearchMusare } from "@/composables/useSearchMusare";
+import { useYoutubeDirect } from "@/composables/useYoutubeDirect";
 import { useEditPlaylistStore } from "@/stores/editPlaylist";
 
 const SongItem = defineAsyncComponent(
@@ -36,6 +37,8 @@ const {
 	addMusareSongToPlaylist
 } = useSearchMusare();
 
+const { youtubeDirect, addToPlaylist } = useYoutubeDirect();
+
 watch(
 	() => youtubeSearch.value.songs.results,
 	songs => {
@@ -164,6 +167,24 @@ onMounted(async () => {
 
 		<br v-if="musareSearch.results.length > 0" />
 
+		<label class="label"> Add a YouTube song from a URL </label>
+		<div class="control is-grouped input-with-button">
+			<p class="control is-expanded">
+				<input
+					class="input"
+					type="text"
+					placeholder="Enter your YouTube song URL here..."
+					v-model="youtubeDirect"
+					@keyup.enter="addToPlaylist(playlist._id)"
+				/>
+			</p>
+			<p class="control">
+				<a class="button is-info" @click="addToPlaylist(playlist._id)"
+					><i class="material-icons icon-with-button">add</i>Add</a
+				>
+			</p>
+		</div>
+
 		<div>
 			<label class="label"> Search for a song from YouTube </label>
 			<div class="control is-grouped input-with-button">

+ 26 - 4
frontend/src/components/modals/EditSong/Tabs/Youtube.vue

@@ -4,6 +4,7 @@ import { storeToRefs } from "pinia";
 import { useEditSongStore } from "@/stores/editSong";
 
 import { useSearchYoutube } from "@/composables/useSearchYoutube";
+import { useYoutubeDirect } from "@/composables/useYoutubeDirect";
 
 import SearchQueryItem from "../../../SearchQueryItem.vue";
 
@@ -19,11 +20,12 @@ const { form, newSong } = storeToRefs(editSongStore);
 const { updateYoutubeId } = editSongStore;
 
 const { youtubeSearch, searchForSongs, loadMoreSongs } = useSearchYoutube();
+const { youtubeDirect, getYoutubeVideoId } = useYoutubeDirect();
 
-const selectSong = result => {
-	updateYoutubeId(result.id);
+const selectSong = (youtubeId, result = null) => {
+	updateYoutubeId(youtubeId);
 
-	if (newSong)
+	if (newSong && result)
 		form.value.setValue({
 			title: result.title,
 			thumbnail: result.thumbnail
@@ -33,6 +35,26 @@ const selectSong = result => {
 
 <template>
 	<div class="youtube-tab">
+		<label class="label"> Add a YouTube song from a URL </label>
+		<div class="control is-grouped input-with-button">
+			<p class="control is-expanded">
+				<input
+					class="input"
+					type="text"
+					placeholder="Enter your YouTube song URL here..."
+					v-model="youtubeDirect"
+					@keyup.enter="selectSong(getYoutubeVideoId())"
+				/>
+			</p>
+			<p class="control">
+				<a
+					class="button is-info"
+					@click="selectSong(getYoutubeVideoId())"
+					><i class="material-icons icon-with-button">add</i>Add</a
+				>
+			</p>
+		</div>
+
 		<label class="label"> Search for a song from YouTube </label>
 		<div class="control is-grouped input-with-button">
 			<p class="control is-expanded">
@@ -74,7 +96,7 @@ const selectSong = result => {
 					<i
 						class="material-icons icon-not-selected"
 						v-else
-						@click.prevent="selectSong(result)"
+						@click.prevent="selectSong(result.id, result)"
 						key="not-selected"
 						>radio_button_unchecked
 					</i>

+ 90 - 0
frontend/src/composables/useYoutubeDirect.ts

@@ -0,0 +1,90 @@
+import { ref } from "vue";
+import Toast from "toasters";
+import { AddSongToPlaylistResponse } from "@musare_types/actions/PlaylistsActions";
+import { useWebsocketsStore } from "@/stores/websockets";
+
+const youtubeVideoUrlRegex =
+	/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?([\w-]{11})$/;
+const youtubeVideoIdRegex = /^([\w-]{11})$/;
+
+export const useYoutubeDirect = () => {
+	const youtubeDirect = ref("");
+
+	const { socket } = useWebsocketsStore();
+
+	const getYoutubeVideoId = () => {
+		const youtubeVideoUrlParts = youtubeVideoUrlRegex.exec(
+			youtubeDirect.value.trim()
+		);
+		if (youtubeVideoUrlParts) {
+			// eslint-disable-next-line prefer-destructuring
+			return youtubeVideoUrlParts[5];
+		}
+
+		const youtubeVideoIdParts = youtubeVideoIdRegex.exec(
+			youtubeDirect.value.trim()
+		);
+		if (youtubeVideoIdParts) {
+			// eslint-disable-next-line prefer-destructuring
+			return youtubeVideoIdParts[1];
+		}
+
+		return null;
+	};
+
+	const addToPlaylist = (playlistId: string) => {
+		const youtubeVideoId = getYoutubeVideoId();
+
+		if (!youtubeVideoId)
+			new Toast(
+				`Could not determine the YouTube video id from the provided URL.`
+			);
+		else {
+			socket.dispatch(
+				"playlists.addSongToPlaylist",
+				false,
+				youtubeVideoId,
+				playlistId,
+				(res: AddSongToPlaylistResponse) => {
+					if (res.status !== "success")
+						new Toast(`Error: ${res.message}`);
+					else {
+						new Toast(res.message);
+						youtubeDirect.value = "";
+					}
+				}
+			);
+		}
+	};
+
+	const addToQueue = (stationId: string) => {
+		const youtubeVideoId = getYoutubeVideoId();
+
+		if (!youtubeVideoId)
+			new Toast(
+				`Could not determine the YouTube video id from the provided URL.`
+			);
+		else {
+			socket.dispatch(
+				"stations.addToQueue",
+				stationId,
+				youtubeVideoId,
+				res => {
+					if (res.status !== "success")
+						new Toast(`Error: ${res.message}`);
+					else {
+						new Toast(res.message);
+						youtubeDirect.value = "";
+					}
+				}
+			);
+		}
+	};
+
+	return {
+		youtubeDirect,
+		addToPlaylist,
+		addToQueue,
+		getYoutubeVideoId
+	};
+};