Browse Source

feat: worked more on converting Spotify songs functionality

Kristian Vos 1 year ago
parent
commit
5ad2c511fa

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

@@ -479,7 +479,7 @@ export default {
 	 */
 	getAlternativeMediaSourcesForTracks: useHasPermission(
 		"admin.view.spotify",
-		function getAlternativeMediaSourcesForTracks(session, mediaSources, cb) {
+		function getAlternativeMediaSourcesForTracks(session, mediaSources, collectAlternativeMediaSourcesOrigins, cb) {
 			async.waterfall(
 				[
 					next => {
@@ -506,7 +506,11 @@ export default {
 						// 	this
 						// );
 
-						SpotifyModule.runJob("GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACKS", { mediaSources }, this);
+						SpotifyModule.runJob(
+							"GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACKS",
+							{ mediaSources, collectAlternativeMediaSourcesOrigins },
+							this
+						);
 					}
 				],
 				async err => {

+ 0 - 2
backend/logic/playlists.js

@@ -519,8 +519,6 @@ class _PlaylistsModule extends CoreClass {
 		return new Promise((resolve, reject) => {
 			const { playlistId, newMediaSource, oldMediaSource } = payload;
 
-			console.log("KRISISISIS", payload, newMediaSource, oldMediaSource);
-
 			async.waterfall(
 				[
 					next => {

+ 210 - 74
backend/logic/spotify.js

@@ -543,7 +543,7 @@ class _SpotifyModule extends CoreClass {
 	 * @returns
 	 */
 	async GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACKS(payload) {
-		const { mediaSources } = payload;
+		const { mediaSources, collectAlternativeMediaSourcesOrigins } = payload;
 
 		// console.log("KR*S94955", mediaSources);
 
@@ -553,7 +553,7 @@ class _SpotifyModule extends CoreClass {
 			try {
 				const result = await SpotifyModule.runJob(
 					"GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACK",
-					{ mediaSource },
+					{ mediaSource, collectAlternativeMediaSourcesOrigins },
 					this
 				);
 				this.publishProgress({
@@ -591,7 +591,7 @@ class _SpotifyModule extends CoreClass {
 	 * @returns
 	 */
 	async GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACK(payload) {
-		const { mediaSource } = payload;
+		const { mediaSource, collectAlternativeMediaSourcesOrigins } = payload;
 
 		if (!mediaSource || !mediaSource.startsWith("spotify:"))
 			throw new Error("Media source provided is not a valid Spotify media source.");
@@ -610,6 +610,11 @@ class _SpotifyModule extends CoreClass {
 		const ISRC = spotifyTrack.externalIds.isrc;
 		if (!ISRC) throw new Error(`ISRC not found for Spotify track ${mediaSource}.`);
 
+		const mediaSources = new Set();
+		const mediaSourcesOrigins = {};
+
+		const jobsToRun = [];
+
 		const ISRCApiResponse = await MusicBrainzModule.runJob(
 			"API_CALL",
 			{
@@ -622,12 +627,8 @@ class _SpotifyModule extends CoreClass {
 			this
 		);
 
-		console.dir(ISRCApiResponse, { depth: 5 });
-
-		const mediaSources = new Set();
-		const mediaSourcesOrigins = {};
-
-		const jobsToRun = [];
+		// console.log("ISRCApiResponse");
+		// console.dir(ISRCApiResponse, { depth: 5 });
 
 		ISRCApiResponse.recordings.forEach(recording => {
 			recording.relations.forEach(relation => {
@@ -648,21 +649,24 @@ class _SpotifyModule extends CoreClass {
 									const { trackId } = response.track;
 									const mediaSource = `soundcloud:${trackId}`;
 
-									const mediaSourceOrigins = [
-										`Spotify track ${spotifyTrackId}`,
-										`ISRC ${ISRC}`,
-										`MusicBrainz recordings`,
-										`MusicBrainz recording ${recording.id}`,
-										`MusicBrainz relations`,
-										`MusicBrainz relation target-type url`,
-										`MusicBrainz relation resource ${resource}`,
-										`SoundCloud ID ${trackId}`
-									];
-
 									mediaSources.add(mediaSource);
-									if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
 
-									mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+									if (collectAlternativeMediaSourcesOrigins) {
+										const mediaSourceOrigins = [
+											`Spotify track ${spotifyTrackId}`,
+											`ISRC ${ISRC}`,
+											`MusicBrainz recordings`,
+											`MusicBrainz recording ${recording.id}`,
+											`MusicBrainz relations`,
+											`MusicBrainz relation target-type url`,
+											`MusicBrainz relation resource ${resource}`,
+											`SoundCloud ID ${trackId}`
+										];
+
+										if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
+
+										mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+									}
 
 									resolve();
 								})
@@ -684,21 +688,25 @@ class _SpotifyModule extends CoreClass {
 						if (!youtubeId) throw new Error(`Unable to parse YouTube resource ${resource}.`);
 
 						const mediaSource = `youtube:${youtubeId}`;
-						const mediaSourceOrigins = [
-							`Spotify track ${spotifyTrackId}`,
-							`ISRC ${ISRC}`,
-							`MusicBrainz recordings`,
-							`MusicBrainz recording ${recording.id}`,
-							`MusicBrainz relations`,
-							`MusicBrainz relation target-type url`,
-							`MusicBrainz relation resource ${resource}`,
-							`YouTube ID ${youtubeId}`
-						];
 
 						mediaSources.add(mediaSource);
-						if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
 
-						mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+						if (collectAlternativeMediaSourcesOrigins) {
+							const mediaSourceOrigins = [
+								`Spotify track ${spotifyTrackId}`,
+								`ISRC ${ISRC}`,
+								`MusicBrainz recordings`,
+								`MusicBrainz recording ${recording.id}`,
+								`MusicBrainz relations`,
+								`MusicBrainz relation target-type url`,
+								`MusicBrainz relation resource ${resource}`,
+								`YouTube ID ${youtubeId}`
+							];
+
+							if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
+
+							mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+						}
 
 						return;
 					}
@@ -712,8 +720,6 @@ class _SpotifyModule extends CoreClass {
 					const promise = new Promise(resolve => {
 						WikiDataModule.runJob("API_GET_DATA_FROM_MUSICBRAINZ_WORK", { workId: relation.work.id }, this)
 							.then(resultBody => {
-								console.log("KRISWORKSUCCESS", resultBody);
-
 								const youtubeIds = Array.from(
 									new Set(
 										resultBody.results.bindings
@@ -721,55 +727,61 @@ class _SpotifyModule extends CoreClass {
 											.map(binding => binding.YouTube_video_ID.value)
 									)
 								);
-								const soundcloudIds = Array.from(
-									new Set(
-										resultBody.results.bindings
-											.filter(binding => !!binding["SoundCloud_track_ID"])
-											.map(binding => binding["SoundCloud_track_ID"].value)
-									)
-								);
+								// const soundcloudIds = Array.from(
+								// 	new Set(
+								// 		resultBody.results.bindings
+								// 			.filter(binding => !!binding["SoundCloud_track_ID"])
+								// 			.map(binding => binding["SoundCloud_track_ID"].value)
+								// 	)
+								// );
 
 								youtubeIds.forEach(youtubeId => {
 									const mediaSource = `youtube:${youtubeId}`;
-									const mediaSourceOrigins = [
-										`Spotify track ${spotifyTrackId}`,
-										`ISRC ${ISRC}`,
-										`MusicBrainz recordings`,
-										`MusicBrainz recording ${recording.id}`,
-										`MusicBrainz relations`,
-										`MusicBrainz relation target-type work`,
-										`MusicBrainz relation work id ${relation.work.id}`,
-										`WikiData select from MusicBrainz work id ${relation.work.id}`,
-										`YouTube ID ${youtubeId}`
-									];
 
 									mediaSources.add(mediaSource);
-									if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
 
-									mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+									if (collectAlternativeMediaSourcesOrigins) {
+										const mediaSourceOrigins = [
+											`Spotify track ${spotifyTrackId}`,
+											`ISRC ${ISRC}`,
+											`MusicBrainz recordings`,
+											`MusicBrainz recording ${recording.id}`,
+											`MusicBrainz relations`,
+											`MusicBrainz relation target-type work`,
+											`MusicBrainz relation work id ${relation.work.id}`,
+											`WikiData select from MusicBrainz work id ${relation.work.id}`,
+											`YouTube ID ${youtubeId}`
+										];
+
+										if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
+
+										mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+									}
 								});
 
-								soundcloudIds.forEach(soundcloudId => {
-									const mediaSource = `soundcloud:${soundcloudId}`;
-									const mediaSourceOrigins = [
-										`Spotify track ${spotifyTrackId}`,
-										`ISRC ${ISRC}`,
-										`MusicBrainz recordings`,
-										`MusicBrainz recording ${recording.id}`,
-										`MusicBrainz relations`,
-										`MusicBrainz relation target-type work`,
-										`MusicBrainz relation work id ${relation.work.id}`,
-										`WikiData select from MusicBrainz work id ${relation.work.id}`,
-										`SoundCloud ID ${soundcloudId}`
-									];
+								// soundcloudIds.forEach(soundcloudId => {
+								// 	const mediaSource = `soundcloud:${soundcloudId}`;
 
-									mediaSources.add(mediaSource);
-									if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
+								// 	mediaSources.add(mediaSource);
 
-									mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
-								});
+								// 	if (collectAlternativeMediaSourcesOrigins) {
+								// 		const mediaSourceOrigins = [
+								// 			`Spotify track ${spotifyTrackId}`,
+								// 			`ISRC ${ISRC}`,
+								// 			`MusicBrainz recordings`,
+								// 			`MusicBrainz recording ${recording.id}`,
+								// 			`MusicBrainz relations`,
+								// 			`MusicBrainz relation target-type work`,
+								// 			`MusicBrainz relation work id ${relation.work.id}`,
+								// 			`WikiData select from MusicBrainz work id ${relation.work.id}`,
+								// 			`SoundCloud ID ${soundcloudId}`
+								// 		];
+
+								// 		if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
 
-								console.log("KRISWORKWOW", youtubeIds, soundcloudIds);
+								// 		mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+								// 	}
+								// });
 
 								resolve();
 							})
@@ -788,6 +800,130 @@ class _SpotifyModule extends CoreClass {
 			});
 		});
 
+		const RecordingApiResponse = await MusicBrainzModule.runJob(
+			"API_CALL",
+			{
+				url: `https://musicbrainz.org/ws/2/recording/`,
+				params: {
+					fmt: "json",
+					query: `isrc:${ISRC}`
+				}
+			},
+			this
+		);
+
+		const releaseIds = new Set();
+		const releaseGroupIds = new Set();
+
+		RecordingApiResponse.recordings.forEach(recording => {
+			const recordingId = recording.id;
+			// console.log("Recording:", recording.id);
+
+			recording.releases.forEach(release => {
+				const releaseId = release.id;
+				// console.log("Release:", releaseId);
+
+				const releaseGroupId = release["release-group"].id;
+				// console.log("Release group:", release["release-group"]);
+				// console.log("Release group id:", release["release-group"].id);
+				// console.log("Release group type id:", release["release-group"]["type-id"]);
+				// console.log("Release group primary type id:", release["release-group"]["primary-type-id"]);
+				// console.log("Release group primary type:", release["release-group"]["primary-type"]);
+
+				// d6038452-8ee0-3f68-affc-2de9a1ede0b9 = single
+				// 6d0c5bf6-7a33-3420-a519-44fc63eedebf = EP
+				if (
+					release["release-group"]["type-id"] === "d6038452-8ee0-3f68-affc-2de9a1ede0b9" ||
+					release["release-group"]["type-id"] === "6d0c5bf6-7a33-3420-a519-44fc63eedebf"
+				) {
+					releaseIds.add(releaseId);
+					releaseGroupIds.add(releaseGroupId);
+				}
+			});
+		});
+
+		Array.from(releaseGroupIds).forEach(releaseGroupId => {
+			const promise = new Promise(resolve => {
+				WikiDataModule.runJob("API_GET_DATA_FROM_MUSICBRAINZ_RELEASE_GROUP", { releaseGroupId }, this)
+					.then(resultBody => {
+						const youtubeIds = Array.from(
+							new Set(
+								resultBody.results.bindings
+									.filter(binding => !!binding.YouTube_video_ID)
+									.map(binding => binding.YouTube_video_ID.value)
+							)
+						);
+						// const soundcloudIds = Array.from(
+						// 	new Set(
+						// 		resultBody.results.bindings
+						// 			.filter(binding => !!binding["SoundCloud_track_ID"])
+						// 			.map(binding => binding["SoundCloud_track_ID"].value)
+						// 	)
+						// );
+
+						youtubeIds.forEach(youtubeId => {
+							const mediaSource = `youtube:${youtubeId}`;
+
+							mediaSources.add(mediaSource);
+
+							// if (collectAlternativeMediaSourcesOrigins) {
+							// 	const mediaSourceOrigins = [
+							// 		`Spotify track ${spotifyTrackId}`,
+							// 		`ISRC ${ISRC}`,
+							// 		`MusicBrainz recordings`,
+							// 		`MusicBrainz recording ${recording.id}`,
+							// 		`MusicBrainz relations`,
+							// 		`MusicBrainz relation target-type work`,
+							// 		`MusicBrainz relation work id ${relation.work.id}`,
+							// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
+							// 		`YouTube ID ${youtubeId}`
+							// 	];
+
+							// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
+
+							// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+							// }
+						});
+
+						// soundcloudIds.forEach(soundcloudId => {
+						// 	const mediaSource = `soundcloud:${soundcloudId}`;
+
+						// 	mediaSources.add(mediaSource);
+
+						// 	// if (collectAlternativeMediaSourcesOrigins) {
+						// 	// 	const mediaSourceOrigins = [
+						// 	// 		`Spotify track ${spotifyTrackId}`,
+						// 	// 		`ISRC ${ISRC}`,
+						// 	// 		`MusicBrainz recordings`,
+						// 	// 		`MusicBrainz recording ${recording.id}`,
+						// 	// 		`MusicBrainz relations`,
+						// 	// 		`MusicBrainz relation target-type work`,
+						// 	// 		`MusicBrainz relation work id ${relation.work.id}`,
+						// 	// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
+						// 	// 		`SoundCloud ID ${soundcloudId}`
+						// 	// 	];
+
+						// 	// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
+
+						// 	// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+						// 	// }
+						// });
+
+						resolve();
+					})
+					.catch(err => {
+						console.log("KRISWORKERR", err);
+						resolve();
+					});
+			});
+
+			jobsToRun.push(promise);
+		});
+
+		// console.log("RecordingApiResponse");
+		// console.dir(RecordingApiResponse, { depth: 10 });
+		// console.dir(RecordingApiResponse.recordings[0].releases[0], { depth: 10 });
+
 		await Promise.allSettled(jobsToRun);
 
 		return {

+ 3 - 1
backend/logic/stations.js

@@ -629,7 +629,8 @@ class _StationsModule extends CoreClass {
 							if (
 								songsToAdd.length < songsStillNeeded &&
 								currentYoutubeIds.indexOf(song.mediaSource) === -1 &&
-								!songsToAdd.find(songToAdd => songToAdd.mediaSource === song.mediaSource)
+								!songsToAdd.find(songToAdd => songToAdd.mediaSource === song.mediaSource) &&
+								!song.mediaSource.startsWith("spotify:")
 							) {
 								lastSongAdded = song;
 								songsToAdd.push(song);
@@ -1924,6 +1925,7 @@ class _StationsModule extends CoreClass {
 					(station, next) => {
 						if (!station) return next("Station not found.");
 						if (!station.requests.enabled) return next("Requests are disabled in this station.");
+						if (mediaSource.startsWith("spotify:")) return next("Spotify playback is not supported.");
 						if (station.currentSong && station.currentSong.mediaSource === mediaSource)
 							return next("That song is currently playing.");
 						if (station.queue.find(song => song.mediaSource === mediaSource))

+ 44 - 4
backend/logic/wikidata.js

@@ -158,17 +158,57 @@ class _WikiDataModule extends CoreClass {
 	async API_GET_DATA_FROM_MUSICBRAINZ_WORK(payload) {
 		const { workId } = payload;
 
-		const sparqlQuery = `SELECT DISTINCT ?item ?itemLabel ?YouTube_video_ID WHERE {
+		const sparqlQuery = `SELECT DISTINCT ?item ?itemLabel ?YouTube_video_ID ?SoundCloud_track_ID WHERE {
 								SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }
 								{
 									SELECT DISTINCT ?item WHERE {
-									?item p:P435 ?statement0.
-									?statement0 ps:P435 "${workId}".
+										?item p:P435 ?statement0.
+										?statement0 ps:P435 "${workId}".
 									}
 									LIMIT 100
 								}
 								OPTIONAL { ?item wdt:P1651 ?YouTube_video_ID. }
-							}`;
+								OPTIONAL { ?item wdt:P3040 ?SoundCloud_track_ID. }
+							}`
+			.replaceAll("\n", "")
+			.replaceAll("\t", "");
+
+		return WikiDataModule.runJob(
+			"API_CALL",
+			{
+				url: "https://query.wikidata.org/sparql",
+				params: {
+					query: sparqlQuery
+				}
+			},
+			this
+		);
+	}
+
+	/**
+	 * Get WikiData data from release group id
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {object} payload.releaseGroupId - release group id
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async API_GET_DATA_FROM_MUSICBRAINZ_RELEASE_GROUP(payload) {
+		const { releaseGroupId } = payload;
+
+		const sparqlQuery = `SELECT DISTINCT ?item ?itemLabel ?YouTube_video_ID ?SoundCloud_track_ID WHERE {
+								SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }
+								{
+									SELECT DISTINCT ?item WHERE {
+										?item p:P436 ?statement0.
+										?statement0 ps:P436 "${releaseGroupId}".
+									}
+									LIMIT 100
+								}
+								OPTIONAL { ?item wdt:P1651 ?YouTube_video_ID. }
+								OPTIONAL { ?item wdt:P3040 ?SoundCloud_track_ID. }
+							}`
+			.replaceAll("\n", "")
+			.replaceAll("\t", "");
 
 		return WikiDataModule.runJob(
 			"API_CALL",

+ 4 - 2
frontend/src/components/PlaylistTabBase.vue

@@ -143,7 +143,9 @@ const totalUniqueAutorequestableYoutubeIds = computed(() => {
 const actuallyAutorequestingYoutubeIds = computed(() => {
 	const excluded = excludedYoutubeIds.value;
 	const remaining = totalUniqueAutorequestableYoutubeIds.value.filter(
-		mediaSource => excluded.indexOf(mediaSource) === -1
+		mediaSource =>
+			excluded.indexOf(mediaSource) === -1 &&
+			!mediaSource.startsWith("spotify:")
 	);
 	return remaining;
 });
@@ -839,7 +841,7 @@ onMounted(() => {
 						>were played recently or</span
 					>
 					are currently in the queue or playing will not be
-					autorequested.
+					autorequested. Spotify songs will also not be autorequested.
 
 					<br />
 					<br />

+ 64 - 22
frontend/src/components/modals/ConvertSpotifySongs.vue

@@ -66,6 +66,21 @@ const preferredAlternativeSongMode = ref<
 // const singleMode = ref(false);
 const showExtra = ref(false);
 
+const collectAlternativeMediaSourcesOrigins = ref(false);
+
+const filteredSpotifySongs = computed(() =>
+	hideSpotifySongsWithNoAlternativesFound.value
+		? spotifySongs.value.filter(
+				spotifySong =>
+					(!gettingAllAlternativeMediaPerTrack.value &&
+						!gotAllAlternativeMediaPerTrack.value) ||
+					(alternativeMediaPerTrack[spotifySong.mediaSource] &&
+						alternativeMediaPerTrack[spotifySong.mediaSource]
+							.mediaSources.length > 0)
+		  )
+		: spotifySongs.value
+);
+
 const missingMediaSources = computed(() => {
 	const missingMediaSources = [];
 
@@ -280,33 +295,38 @@ const getAlternativeMedia = () => {
 
 	const mediaSources = spotifySongs.value.map(song => song.mediaSource);
 
-	socket.dispatch("apis.getAlternativeMediaSourcesForTracks", mediaSources, {
-		cb: res => {
-			console.log(
-				"apis.getAlternativeMediaSourcesForTracks response",
-				res
-			);
-		},
-		onProgress: data => {
-			console.log(
-				"apis.getAlternativeMediaSourcesForTracks onProgress",
-				data
-			);
+	socket.dispatch(
+		"apis.getAlternativeMediaSourcesForTracks",
+		mediaSources,
+		collectAlternativeMediaSourcesOrigins.value,
+		{
+			cb: res => {
+				console.log(
+					"apis.getAlternativeMediaSourcesForTracks response",
+					res
+				);
+			},
+			onProgress: data => {
+				console.log(
+					"apis.getAlternativeMediaSourcesForTracks onProgress",
+					data
+				);
 
-			if (data.status === "working") {
-				if (data.data.status === "success") {
-					const { mediaSource, result } = data.data;
+				if (data.status === "working") {
+					if (data.data.status === "success") {
+						const { mediaSource, result } = data.data;
 
-					if (!spotifyTracks[mediaSource]) return;
+						if (!spotifyTracks[mediaSource]) return;
 
-					alternativeMediaPerTrack[mediaSource] = result;
+						alternativeMediaPerTrack[mediaSource] = result;
+					}
+				} else if (data.status === "finished") {
+					gotAllAlternativeMediaPerTrack.value = true;
+					gettingAllAlternativeMediaPerTrack.value = false;
 				}
-			} else if (data.status === "finished") {
-				gotAllAlternativeMediaPerTrack.value = true;
-				gettingAllAlternativeMediaPerTrack.value = false;
 			}
 		}
-	});
+	);
 };
 
 const loadSpotifyTracks = () =>
@@ -555,6 +575,28 @@ onMounted(() => {
 								</label>
 							</p>
 
+							<p class="is-expanded checkbox-control">
+								<label class="switch">
+									<input
+										type="checkbox"
+										id="collect-alternative-media-sources-origins"
+										v-model="
+											collectAlternativeMediaSourcesOrigins
+										"
+									/>
+									<span class="slider round"></span>
+								</label>
+
+								<label
+									for="collect-alternative-media-sources-origins"
+								>
+									<p>
+										Collect alternative media sources
+										origins
+									</p>
+								</label>
+							</p>
+
 							<p class="is-expanded checkbox-control">
 								<label class="switch">
 									<input
@@ -728,7 +770,7 @@ onMounted(() => {
 						<h4>Alternative songs</h4>
 
 						<template
-							v-for="spotifySong in spotifySongs"
+							v-for="spotifySong in filteredSpotifySongs"
 							:key="spotifySong.mediaSource"
 						>
 							<div

+ 4 - 1
frontend/src/pages/Station/index.vue

@@ -326,7 +326,10 @@ const autoRequestSong = () => {
 
 	autoRequest.value.forEach(playlist => {
 		playlist.songs.forEach(song => {
-			if (excludedYoutubeIds.indexOf(song.mediaSource) === -1)
+			if (
+				excludedYoutubeIds.indexOf(song.mediaSource) === -1 &&
+				!song.mediaSource.startsWith("spotify:")
+			)
 				uniqueYoutubeIds.add(song.mediaSource);
 		});
 	});