Browse Source

Merge branch 'master' into staging

Owen Diffey 1 year ago
parent
commit
f5811b345b

+ 35 - 0
CHANGELOG.md

@@ -1,5 +1,40 @@
 # Changelog
 
+## [v3.10.0] - 2023-05-21
+
+This release includes all changes from v3.10.0-rc1, v3.10.0-rc2 and v3.10.0-rc3,
+in addition to the following.
+Upgrade instructions can be found at [.wiki/Upgrading](.wiki/Upgrading.md).
+
+### Fixed
+
+- fix: Stations created with empty song object
+
+## [v3.10.0-rc3] - 2023-05-14
+
+This release includes all changes from v3.10.0-rc1 and v3.10.0-rc2,
+in addition to the following.
+Upgrade instructions can be found at [.wiki/Upgrading](.wiki/Upgrading.md).
+
+### Added
+
+- feat: Finished basic implementation of showing jobs on statistics admin page
+- feat: Exclude disliked songs from being autorequested,
+if "Automatically vote to skip disliked songs" preference is enabled
+
+### Changed
+
+- refactor: Increased playlist displayname max length to 64
+- refactor: Improved song thumbnail fallback logic
+
+### Fixed
+
+- fix: SoundCloud player not destroyed properly
+- fix: getPlayerState is not a function thrown in browser console
+- fix: Activity items `<youtubeId>` payload message not migrated
+- fix: Import playlist from file never finishes
+- fix: Tippy can be null and throw an error in console
+
 ## [v3.10.0-rc2] - 2023-04-30
 
 This release includes all changes from v3.10.0-rc1, in addition to the following.

+ 15 - 0
backend/logic/actions/stations.js

@@ -1843,6 +1843,21 @@ export default {
 							next
 						);
 					}
+				},
+
+				// This extra step is needed because Mongoose decides to create an object with empty arrays for currentSong for some reason
+				(station, next) => {
+					stationModel.updateOne(
+						{ _id: station._id },
+						{
+							$set: {
+								currentSong: null
+							}
+						},
+						err => {
+							next(err, station);
+						}
+					);
 				}
 			],
 			async (err, station) => {

+ 15 - 12
backend/logic/db/schemas/station.js

@@ -7,18 +7,21 @@ export default {
 	description: { type: String, minlength: 2, maxlength: 128, trim: true, required: true },
 	paused: { type: Boolean, default: false, required: true },
 	currentSong: {
-		_id: { type: mongoose.Schema.Types.ObjectId },
-		mediaSource: { type: String },
-		title: { type: String },
-		artists: [{ type: String }],
-		duration: { type: Number },
-		skipDuration: { type: Number },
-		thumbnail: { type: String },
-		skipVotes: [{ type: String }],
-		requestedBy: { type: String },
-		requestedAt: { type: Date },
-		requestedType: { type: String, enum: ["manual", "autorequest", "autofill"] },
-		verified: { type: Boolean }
+		type: {
+			_id: { type: mongoose.Schema.Types.ObjectId },
+			mediaSource: { type: String },
+			title: { type: String },
+			artists: [{ type: String }],
+			duration: { type: Number },
+			skipDuration: { type: Number },
+			thumbnail: { type: String },
+			skipVotes: [{ type: String }],
+			requestedBy: { type: String },
+			requestedAt: { type: Date },
+			requestedType: { type: String, enum: ["manual", "autorequest", "autofill"] },
+			verified: { type: Boolean }
+		},
+		default: null
 	},
 	currentSongIndex: { type: Number, default: 0, required: true },
 	timePaused: { type: Number, default: 0, required: true },

+ 1 - 1
backend/logic/stations.js

@@ -307,7 +307,7 @@ class _StationsModule extends CoreClass {
 					(station, next) => {
 						// A current song is invalid if it isn't allowed to be played. Spotify songs can never be played, and SoundCloud songs can't be played if SoundCloud isn't enabled
 						let currentSongIsInvalid = false;
-						if (station.currentSong) {
+						if (station.currentSong && station.currentSong.mediaSource) {
 							if (station.currentSong.mediaSource.startsWith("spotify:")) currentSongIsInvalid = true;
 							if (
 								station.currentSong.mediaSource.startsWith("soundcloud:") &&

+ 33 - 59
frontend/src/components/PlaylistTabBase.vue

@@ -39,8 +39,6 @@ const { socket } = useWebsocketsStore();
 const stationStore = useStationStore();
 const userPreferencesStore = useUserPreferencesStore();
 
-const { autoSkipDisliked } = storeToRefs(userPreferencesStore);
-
 const tab = ref("current");
 const search = reactive({
 	query: "",
@@ -65,8 +63,10 @@ const {
 } = useSortablePlaylists();
 
 const { experimental } = storeToRefs(configStore);
+const { autoSkipDisliked } = storeToRefs(userPreferencesStore);
 
-const { autoRequest, history, songsList } = storeToRefs(stationStore);
+const { autoRequest, autorequestExcludedMediaSources } =
+	storeToRefs(stationStore);
 
 const manageStationStore = useManageStationStore({
 	modalUuid: props.modalUuid
@@ -98,60 +98,12 @@ const blacklist = computed({
 	}
 });
 
-const dislikedPlaylist = computed(() =>
-	playlists.value.find(playlist => playlist.type === "user-disliked")
-);
-
 const resultsLeftCount = computed(() => search.count - search.results.length);
 
 const nextPageResultsCount = computed(() =>
 	Math.min(search.pageSize, resultsLeftCount.value)
 );
 
-// List of media sources that will not be allowed to be autorequested
-const excludedMediaSources = computed(() => {
-	const mediaSources = new Set();
-
-	// Exclude the current song
-	if (station.value.currentSong)
-		mediaSources.add(station.value.currentSong.mediaSource);
-
-	// Exclude songs in the queue
-	if (songsList.value) {
-		songsList.value.forEach(song => {
-			mediaSources.add(song.mediaSource);
-		});
-	}
-
-	// If auto skip disliked preference is enabled, exclude all songs in the disliked playlist
-	if (autoSkipDisliked.value && dislikedPlaylist.value) {
-		dislikedPlaylist.value.songs.forEach(song => {
-			mediaSources.add(song.mediaSource);
-		});
-	}
-
-	// If no history exists, just stop here
-	if (!history.value) Array.from(mediaSources);
-
-	const {
-		autorequestDisallowRecentlyPlayedEnabled,
-		autorequestDisallowRecentlyPlayedNumber
-	} = station.value.requests;
-
-	// If the station is set to disallow recently played songs, and station history is enabled, exclude the last X history songs
-	if (
-		autorequestDisallowRecentlyPlayedEnabled &&
-		experimental.value.station_history
-	) {
-		history.value.forEach((historyItem, index) => {
-			if (index < autorequestDisallowRecentlyPlayedNumber)
-				mediaSources.add(historyItem.payload.song.mediaSource);
-		});
-	}
-
-	return Array.from(mediaSources);
-});
-
 const totalUniqueAutorequestableMediaSources = computed<string[]>(() => {
 	if (!autoRequest.value) return [];
 
@@ -167,7 +119,7 @@ const totalUniqueAutorequestableMediaSources = computed<string[]>(() => {
 });
 
 const actuallyAutorequestingMediaSources = computed(() => {
-	const excluded = excludedMediaSources.value;
+	const excluded = autorequestExcludedMediaSources.value;
 	const remaining = totalUniqueAutorequestableMediaSources.value.filter(
 		mediaSource => {
 			if (excluded.indexOf(mediaSource) !== -1) return false;
@@ -880,6 +832,10 @@ onMounted(() => {
 					autorequested. Spotify
 					<span v-if="!experimental.soundcloud">and SoundCloud</span>
 					songs will also not be autorequested.
+					<span v-if="autoSkipDisliked"
+						>Disliked songs will also not be autorequested due to
+						your preferences.</span
+					>
 
 					<br />
 					<br />
@@ -1060,13 +1016,31 @@ onMounted(() => {
 												'blacklist'
 											)
 										"
-										@click="selectPlaylist(element)"
-										class="material-icons play-icon"
-										:content="`${label(
-											'future',
-											null,
-											true
-										)} songs from this playlist`"
+										:class="{
+											'play-icon':
+												type !== 'autorequest' ||
+												!autoSkipDisliked ||
+												element.type !== 'user-disliked'
+										}"
+										@click="
+											type !== 'autorequest' ||
+											!autoSkipDisliked ||
+											element.type !== 'user-disliked'
+												? selectPlaylist(element)
+												: null
+										"
+										class="material-icons"
+										:content="
+											type !== 'autorequest' ||
+											!autoSkipDisliked ||
+											element.type !== 'user-disliked'
+												? `${label(
+														'future',
+														null,
+														true
+												  )} songs from this playlist`
+												: 'Your preferences are set to skip disliked songs'
+										"
 										v-tippy
 										>play_arrow</i
 									>

+ 9 - 39
frontend/src/pages/Station/index.vue

@@ -142,7 +142,7 @@ const {
 	noSong,
 	autoRequest,
 	autoRequestLock,
-	history
+	autorequestExcludedMediaSources
 } = storeToRefs(stationStore);
 
 const youtubePlayerState = ref<
@@ -261,16 +261,6 @@ const {
 // const stopVideo = payload =>
 // 	store.dispatch("modals/editSong/stopVideo", payload);
 
-const recentlyPlayedYoutubeIds = (max: number) => {
-	const mediaSources = new Set();
-
-	history.value.forEach((historyItem, index) => {
-		if (index < max) mediaSources.add(historyItem.payload.song.mediaSource);
-	});
-
-	return Array.from(mediaSources);
-};
-
 const updateMediaSessionData = song => {
 	if (song) {
 		ms.setMediaSessionData(
@@ -285,13 +275,8 @@ const updateMediaSessionData = song => {
 	} else ms.removeMediaSessionData(0);
 };
 const autoRequestSong = () => {
-	const {
-		limit,
-		allowAutorequest,
-		autorequestLimit,
-		autorequestDisallowRecentlyPlayedEnabled,
-		autorequestDisallowRecentlyPlayedNumber
-	} = station.value.requests;
+	const { limit, allowAutorequest, autorequestLimit } =
+		station.value.requests;
 
 	if (autoRequestLock.value) return;
 	if (!allowAutorequest) return;
@@ -303,31 +288,16 @@ const autoRequestSong = () => {
 	if (currentUserQueueSongs.value >= autorequestLimit) return;
 	if (songsList.value.length >= 50) return;
 
-	let excludedYoutubeIds = [];
-	if (
-		autorequestDisallowRecentlyPlayedEnabled &&
-		experimental.value.station_history
-	) {
-		excludedYoutubeIds = recentlyPlayedYoutubeIds(
-			autorequestDisallowRecentlyPlayedNumber
-		);
-	}
-
-	if (songsList.value) {
-		songsList.value.forEach(song => {
-			excludedYoutubeIds.push(song.mediaSource);
-		});
-	}
-
-	if (!noSong.value) {
-		excludedYoutubeIds.push(currentSong.value.mediaSource);
-	}
-
 	const uniqueMediaSources = new Set();
 
 	autoRequest.value.forEach(playlist => {
 		playlist.songs.forEach(song => {
-			if (excludedYoutubeIds.indexOf(song.mediaSource) !== -1) return;
+			if (
+				autorequestExcludedMediaSources.value.indexOf(
+					song.mediaSource
+				) !== -1
+			)
+				return;
 			if (song.mediaSource.startsWith("spotify:")) return;
 			if (
 				!experimental.value.soundcloud &&

+ 62 - 1
frontend/src/stores/station.ts

@@ -1,10 +1,21 @@
-import { defineStore } from "pinia";
+import { defineStore, storeToRefs } from "pinia";
 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";
+import { useUserPreferencesStore } from "@/stores/userPreferences";
+import { useConfigStore } from "@/stores/config";
+import { useSortablePlaylists } from "@/composables/useSortablePlaylists";
+
+const userPreferencesStore = useUserPreferencesStore();
+const configStore = useConfigStore();
+
+const { autoSkipDisliked } = storeToRefs(userPreferencesStore);
+const { experimental } = storeToRefs(configStore);
+
+const { playlists } = useSortablePlaylists();
 
 export const useStationStore = defineStore("station", {
 	state: (): {
@@ -48,6 +59,56 @@ export const useStationStore = defineStore("station", {
 		permissions: {},
 		history: []
 	}),
+	getters: {
+		dislikedPlaylist() {
+			return playlists.value.find(
+				playlist => playlist.type === "user-disliked"
+			);
+		},
+		// List of media sources that will not be allowed to be autorequested
+		autorequestExcludedMediaSources() {
+			const mediaSources = new Set();
+
+			// Exclude the current song
+			if (this.currentSong && this.currentSong.mediaSource)
+				mediaSources.add(this.currentSong.mediaSource);
+
+			// Exclude songs in the queue
+			if (this.songsList) {
+				this.songsList.forEach(song => {
+					mediaSources.add(song.mediaSource);
+				});
+			}
+
+			// If auto skip disliked preference is enabled, exclude all songs in the disliked playlist
+			if (autoSkipDisliked.value && this.dislikedPlaylist) {
+				this.dislikedPlaylist.songs.forEach(song => {
+					mediaSources.add(song.mediaSource);
+				});
+			}
+
+			// If no history exists, just stop here
+			if (!this.history) Array.from(mediaSources);
+
+			const {
+				autorequestDisallowRecentlyPlayedEnabled,
+				autorequestDisallowRecentlyPlayedNumber
+			} = this.station.requests;
+
+			// If the station is set to disallow recently played songs, and station history is enabled, exclude the last X history songs
+			if (
+				autorequestDisallowRecentlyPlayedEnabled &&
+				experimental.value.station_history
+			) {
+				this.history.forEach((historyItem, index) => {
+					if (index < autorequestDisallowRecentlyPlayedNumber)
+						mediaSources.add(historyItem.payload.song.mediaSource);
+				});
+			}
+
+			return Array.from(mediaSources);
+		}
+	},
 	actions: {
 		joinStation(station) {
 			this.station = { ...station };