10 Commits b56a835c1c ... f5811b345b

Author SHA1 Message Date
  Owen Diffey f5811b345b Merge branch 'master' into staging 11 months ago
  Owen Diffey 5c629dac13 Merge branch 'v3.10.0' 11 months ago
  Owen Diffey 991993e1dc chore: Update package.json version to v3.10.0 11 months ago
  Owen Diffey 69a58ca0eb chore: Added v3.10.0 changelog 11 months ago
  Owen Diffey 753959c685 refactor: Set station currentSong default to null in schema 11 months ago
  Kristian Vos ff6b95abd1 fix: stations are created with currentSong as an empty object instead of null 11 months ago
  Owen Diffey f13fa2575f chore: Update package.json version 1 year ago
  Owen Diffey de75988993 chore: Added v3.10.0-rc3 changelog 1 year ago
  Kristian Vos 6edc4e5a77 refactor: don't allow autorequesting disliked playlist if automatically skip disliked songs preference is enabled 1 year ago
  Kristian Vos 673b9026be fix: excluded media sources wasn't up-to-date in station 1 year ago

+ 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 };