瀏覽代碼

Merge tag 'v3.8.0-rc2' into staging

Owen Diffey 2 年之前
父節點
當前提交
5654c21bac

+ 24 - 0
CHANGELOG.md

@@ -1,5 +1,29 @@
 # Changelog
 
+## [v3.8.0-rc2] - 2022-10-31
+
+This release includes all changes from v3.8.0-rc1, in addition to the following.
+Upgrade instructions can be found at [.wiki/Upgrading](.wiki/Upgrading.md).
+
+### Added
+
+- feat: Add/remove media to/from admin playlist from admin/songs/import
+
+### Changed
+
+- refactor: Do not send ActivtyWatch watch event when video is buffering
+- refactor: Include playback rate in ActivtyWatch watch event
+
+### Fixed
+
+- fix: Toggling night mode does not update other tabs if logged out
+- fix: User not removed as DJ from station on deletion
+- fix: Clicking view YouTube video in song item does not close actions tippy
+- fix: ActivityWatch integration event started at was broken
+- fix: AddToPlaylistDropdown missing song added and removed event handling
+- fix: User logged out after removing another user
+- fix: Paused station elapsed duration incorrectly set
+
 ## [v3.8.0-rc1] - 2022-10-16
 
 Upgrade instructions can be found at [.wiki/Upgrading](.wiki/Upgrading.md).

+ 11 - 2
backend/logic/actions/users.js

@@ -380,7 +380,12 @@ export default {
 					});
 				},
 
+				// remove user as station DJ
 				next => {
+					stationModel.updateMany({ djs: session.userId }, { $pull: { djs: session.userId } }, next);
+				},
+
+				(res, next) => {
 					playlistModel.findOne({ createdBy: session.userId, type: "user-liked" }, next);
 				},
 
@@ -606,7 +611,12 @@ export default {
 					});
 				},
 
+				// remove user as station DJ
 				next => {
+					stationModel.updateMany({ djs: userId }, { $pull: { djs: userId } }, next);
+				},
+
+				(res, next) => {
 					playlistModel.findOne({ createdBy: userId, type: "user-liked" }, next);
 				},
 
@@ -662,7 +672,7 @@ export default {
 				(res, next) => {
 					CacheModule.runJob("PUB", {
 						channel: "user.removeSessions",
-						value: session.userId
+						value: userId
 					});
 
 					async.waterfall(
@@ -685,7 +695,6 @@ export default {
 
 							(keys, sessions, next) => {
 								// temp fix, need to wait properly for the SUB/PUB refactor (on wekan)
-								const { userId } = session;
 								setTimeout(
 									() =>
 										async.each(

+ 20 - 12
frontend/src/App.vue

@@ -34,7 +34,10 @@ const modalsStore = useModalsStore();
 const apiDomain = ref("");
 const socketConnected = ref(true);
 const keyIsDown = ref("");
-const broadcastChannel = ref();
+const broadcastChannel = ref({
+	user_login: null,
+	nightmode: null
+});
 const christmas = ref(false);
 const disconnectedMessage = ref();
 
@@ -59,9 +62,9 @@ const toggleNightMode = () => {
 				if (res.status !== "success") new Toast(res.message);
 			}
 		);
+	} else {
+		broadcastChannel.value.nightmode.postMessage(!nightmode.value);
 	}
-
-	changeNightmode(!nightmode.value);
 };
 
 const enableNightmode = () => {
@@ -96,18 +99,27 @@ onMounted(async () => {
 	window
 		.matchMedia("(prefers-color-scheme: dark)")
 		.addEventListener("change", e => {
-			if (e.matches === !nightmode.value) toggleNightMode();
+			if (e.matches === !nightmode.value) changeNightmode(true);
 		});
 
 	if (!loggedIn.value) {
 		lofig.get("cookie.SIDname").then(sid => {
-			broadcastChannel.value = new BroadcastChannel(`${sid}.user_login`);
-			broadcastChannel.value.onmessage = (data: boolean) => {
-				if (data) {
-					broadcastChannel.value.close();
+			broadcastChannel.value.user_login = new BroadcastChannel(
+				`${sid}.user_login`
+			);
+			broadcastChannel.value.user_login.onmessage = res => {
+				if (res.data) {
+					broadcastChannel.value.user_login.close();
 					window.location.reload();
 				}
 			};
+
+			broadcastChannel.value.nightmode = new BroadcastChannel(
+				`${sid}.nightmode`
+			);
+			broadcastChannel.value.nightmode.onmessage = res => {
+				changeNightmode(!!res.data);
+			};
 		});
 	}
 
@@ -172,9 +184,6 @@ onMounted(async () => {
 						preferences.anonymousSongRequests
 					);
 					changeActivityWatch(preferences.activityWatch);
-
-					if (nightmode.value) enableNightmode();
-					else disableNightmode();
 				}
 			}
 		);
@@ -250,7 +259,6 @@ onMounted(async () => {
 
 	if (localStorage.getItem("nightmode") === "true") {
 		changeNightmode(true);
-		enableNightmode();
 	} else changeNightmode(false);
 
 	lofig.get("siteSettings.christmas").then((enabled: boolean) => {

+ 33 - 0
frontend/src/components/AddToPlaylistDropdown.vue

@@ -98,6 +98,39 @@ onMounted(() => {
 		},
 		{ replaceable: true }
 	);
+
+	socket.on(
+		"event:playlist.song.added",
+		res => {
+			playlists.value.forEach((playlist, index) => {
+				if (playlist._id === res.data.playlistId) {
+					playlists.value[index].songs.push(res.data.song);
+				}
+			});
+		},
+		{ replaceable: true }
+	);
+
+	socket.on(
+		"event:playlist.song.removed",
+		res => {
+			playlists.value.forEach((playlist, playlistIndex) => {
+				if (playlist._id === res.data.playlistId) {
+					playlists.value[playlistIndex].songs.forEach(
+						(song, songIndex) => {
+							if (song.youtubeId === res.data.youtubeId) {
+								playlists.value[playlistIndex].songs.splice(
+									songIndex,
+									1
+								);
+							}
+						}
+					);
+				}
+			});
+		},
+		{ replaceable: true }
+	);
 });
 </script>
 

+ 14 - 10
frontend/src/components/MainHeader.vue

@@ -29,19 +29,21 @@ const siteSettings = ref({
 	registrationDisabled: false
 });
 const windowWidth = ref(0);
+const sidName = ref();
+const broadcastChannel = ref();
 
 const { socket } = useWebsocketsStore();
 
 const { loggedIn, username } = storeToRefs(userAuthStore);
 const { logout, hasPermission } = userAuthStore;
-const { changeNightmode } = useUserPreferencesStore();
+const userPreferencesStore = useUserPreferencesStore();
+const { nightmode } = storeToRefs(userPreferencesStore);
 
 const { openModal } = useModalsStore();
 
 const toggleNightmode = toggle => {
-	localNightmode.value = toggle || !localNightmode.value;
-
-	localStorage.setItem("nightmode", `${localNightmode.value}`);
+	localNightmode.value =
+		toggle === undefined ? !localNightmode.value : toggle;
 
 	if (loggedIn.value) {
 		socket.dispatch(
@@ -51,28 +53,30 @@ const toggleNightmode = toggle => {
 				if (res.status !== "success") new Toast(res.message);
 			}
 		);
+	} else {
+		broadcastChannel.value.postMessage(localNightmode.value);
 	}
-
-	changeNightmode(localNightmode.value);
 };
 
 const onResize = () => {
 	windowWidth.value = window.innerWidth;
 };
 
-watch(localNightmode, nightmode => {
-	if (localNightmode.value !== nightmode) toggleNightmode(nightmode);
+watch(nightmode, value => {
+	localNightmode.value = value;
 });
 
 onMounted(async () => {
-	localNightmode.value = localStorage.getItem("nightmode") === "true";
-
+	localNightmode.value = nightmode.value;
 	frontendDomain.value = await lofig.get("frontendDomain");
 	siteSettings.value = await lofig.get("siteSettings");
+	sidName.value = await lofig.get("cookie.SIDname");
 
 	await nextTick();
 	onResize();
 	window.addEventListener("resize", onResize);
+
+	broadcastChannel.value = new BroadcastChannel(`${sidName.value}.nightmode`);
 });
 </script>
 

+ 11 - 8
frontend/src/components/SongItem.vue

@@ -91,6 +91,16 @@ const hoverTippy = () => {
 	hoveredTippy.value = true;
 };
 
+const viewYoutubeVideo = youtubeId => {
+	hideTippyElements();
+	openModal({
+		modal: "viewYoutubeVideo",
+		props: {
+			youtubeId
+		}
+	});
+};
+
 const report = song => {
 	hideTippyElements();
 	openModal({ modal: "report", props: { song } });
@@ -201,14 +211,7 @@ onUnmounted(() => {
 						<div class="icons-group">
 							<i
 								v-if="disabledActions.indexOf('youtube') === -1"
-								@click="
-									openModal({
-										modal: 'viewYoutubeVideo',
-										props: {
-											youtubeId: song.youtubeId
-										}
-									})
-								"
+								@click="viewYoutubeVideo(song.youtubeId)"
 								content="View YouTube Video"
 								v-tippy
 							>

+ 11 - 2
frontend/src/components/modals/EditSong/index.vue

@@ -856,7 +856,10 @@ const resetGenreHelper = () => {
 };
 
 const sendActivityWatchVideoData = () => {
-	if (!video.value.paused) {
+	if (
+		!video.value.paused &&
+		video.value.player.getPlayerState() === window.YT.PlayerState.PLAYING
+	) {
 		if (activityWatchVideoLastStatus.value !== "playing") {
 			activityWatchVideoLastStatus.value = "playing";
 			if (
@@ -887,7 +890,13 @@ const sendActivityWatchVideoData = () => {
 					? 0
 					: activityWatchVideoLastStartDuration.value,
 			source: `editSong#${inputs.value.youtubeId.value}`,
-			hostname: window.location.hostname
+			hostname: window.location.hostname,
+			playerState: Object.keys(window.YT.PlayerState).find(
+				key =>
+					window.YT.PlayerState[key] ===
+					video.value.player.getPlayerState()
+			),
+			playbackRate: video.value.playbackRate
 		};
 
 		aw.sendVideoData(videoData);

+ 11 - 2
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -173,7 +173,10 @@ const setTrackPosition = event => {
 	);
 };
 const sendActivityWatchVideoData = () => {
-	if (!player.value.paused) {
+	if (
+		!player.value.paused &&
+		player.value.player.getPlayerState() === window.YT.PlayerState.PLAYING
+	) {
 		if (activityWatchVideoLastStatus.value !== "playing") {
 			activityWatchVideoLastStatus.value = "playing";
 			activityWatchVideoLastStartDuration.value = Math.floor(
@@ -192,7 +195,13 @@ const sendActivityWatchVideoData = () => {
 					? 0
 					: activityWatchVideoLastStartDuration.value,
 			source: `viewYoutubeVideo#${video.value.youtubeId}`,
-			hostname: window.location.hostname
+			hostname: window.location.hostname,
+			playerState: Object.keys(window.YT.PlayerState).find(
+				key =>
+					window.YT.PlayerState[key] ===
+					player.value.player.getPlayerState()
+			),
+			playbackRate: player.value.playbackRate
 		};
 
 		aw.sendVideoData(videoData);

+ 29 - 2
frontend/src/pages/Admin/Songs/Import.vue

@@ -46,8 +46,8 @@ const columns = ref<TableColumn[]>([
 		sortable: false,
 		hidable: false,
 		resizable: false,
-		minWidth: 160,
-		defaultWidth: 160
+		minWidth: 200,
+		defaultWidth: 200
 	},
 	{
 		name: "type",
@@ -358,6 +358,15 @@ const importAlbum = youtubeIds => {
 	});
 };
 
+const bulkEditPlaylist = youtubeIds => {
+	openModal({
+		modal: "bulkEditPlaylist",
+		props: {
+			youtubeIds
+		}
+	});
+};
+
 const removeImportJob = jobId => {
 	socket.dispatch("media.removeImportJobs", jobId, res => {
 		new Toast(res.message);
@@ -540,6 +549,24 @@ const removeImportJob = jobId => {
 								>
 									album
 								</button>
+								<button
+									v-if="hasPermission('playlists.songs.add')"
+									class="button is-primary icon-with-button material-icons"
+									@click="
+										bulkEditPlaylist(
+											slotProps.item.response
+												.successfulVideoIds
+										)
+									"
+									:disabled="
+										slotProps.item.removed ||
+										slotProps.item.status !== 'success'
+									"
+									content="Add/remove media to/from playlist"
+									v-tippy
+								>
+									playlist_add
+								</button>
 								<button
 									v-if="
 										hasPermission('media.removeImportJobs')

+ 1 - 1
frontend/src/pages/Admin/Songs/index.vue

@@ -778,7 +778,7 @@ onMounted(() => {
 						v-if="hasPermission('playlists.songs.add')"
 						class="material-icons playlist-bulk-edit-icon"
 						@click.prevent="bulkEditPlaylist(slotProps.item)"
-						content="Add To Playlist"
+						content="Add/remove to/from playlist"
 						v-tippy
 						tabindex="0"
 					>

+ 1 - 1
frontend/src/pages/Home.vue

@@ -916,7 +916,7 @@ onBeforeUnmount(() => {
 					</div>
 				</router-link>
 				<h4 v-if="stations.length === 0">
-					{{ t("NoStations", 0) }}
+					{{ t("NoStationsToDisplay", 0) }}
 				</h4>
 			</div>
 			<main-footer />

+ 31 - 15
frontend/src/pages/Station/index.vue

@@ -79,7 +79,7 @@ const frontendDevMode = ref("production");
 const activityWatchVideoDataInterval = ref(null);
 const activityWatchVideoLastStatus = ref("");
 const activityWatchVideoLastYouTubeId = ref("");
-// const activityWatchVideoLastStartDuration = ref("");
+const activityWatchVideoLastStartDuration = ref(0);
 const nextCurrentSong = ref(null);
 const mediaModalWatcher = ref(null);
 const beforeMediaModalLocalPausedLock = ref(false);
@@ -234,9 +234,10 @@ const autoRequestSong = () => {
 const dateCurrently = () => new Date().getTime() + systemDifference.value;
 const getTimeElapsed = () => {
 	if (currentSong.value) {
+		let localTimePaused = timePaused.value;
 		if (stationPaused.value)
-			timePaused.value += dateCurrently() - pausedAt.value;
-		return dateCurrently() - startedAt.value - timePaused.value;
+			localTimePaused += dateCurrently() - pausedAt.value;
+		return dateCurrently() - startedAt.value - localTimePaused;
 	}
 	return 0;
 };
@@ -361,11 +362,12 @@ const calculateTimeElapsed = () => {
 		}
 	}
 
+	let localTimePaused = timePaused.value;
 	if (stationPaused.value)
-		timePaused.value += dateCurrently() - pausedAt.value;
+		localTimePaused += dateCurrently() - pausedAt.value;
 
 	const duration =
-		(dateCurrently() - startedAt.value - timePaused.value) / 1000;
+		(dateCurrently() - startedAt.value - localTimePaused) / 1000;
 
 	const songDuration = currentSong.value.duration;
 	if (playerReady.value && songDuration <= duration)
@@ -805,12 +807,16 @@ const resetKeyboardShortcutsHelper = () => {
 	keyboardShortcutsHelper.value.resetBox();
 };
 const sendActivityWatchVideoData = () => {
-	if (!stationPaused.value && !localPaused.value && !noSong.value) {
+	if (
+		!stationPaused.value &&
+		!localPaused.value &&
+		!noSong.value &&
+		player.value.getPlayerState() === window.YT.PlayerState.PLAYING
+	) {
 		if (activityWatchVideoLastStatus.value !== "playing") {
 			activityWatchVideoLastStatus.value = "playing";
-			activityWatchVideoLastStatus.value = `${
-				currentSong.value.skipDuration + getTimeElapsed()
-			}`;
+			activityWatchVideoLastStartDuration.value =
+				currentSong.value.skipDuration + getTimeElapsed();
 		}
 
 		if (
@@ -818,9 +824,8 @@ const sendActivityWatchVideoData = () => {
 			currentSong.value.youtubeId
 		) {
 			activityWatchVideoLastYouTubeId.value = currentSong.value.youtubeId;
-			activityWatchVideoLastStatus.value = `${
-				currentSong.value.skipDuration + getTimeElapsed()
-			}`;
+			activityWatchVideoLastStartDuration.value =
+				currentSong.value.skipDuration + getTimeElapsed();
 		}
 
 		const videoData = {
@@ -833,13 +838,18 @@ const sendActivityWatchVideoData = () => {
 			muted: muted.value,
 			volume: volumeSliderValue.value,
 			startedDuration:
-				Number(activityWatchVideoLastStatus.value) <= 0
+				activityWatchVideoLastStartDuration.value <= 0
 					? 0
 					: Math.floor(
-							Number(activityWatchVideoLastStatus.value) / 1000
+							activityWatchVideoLastStartDuration.value / 1000
 					  ),
 			source: `station#${station.value.name}`,
-			hostname: window.location.hostname
+			hostname: window.location.hostname,
+			playerState: Object.keys(window.YT.PlayerState).find(
+				key =>
+					window.YT.PlayerState[key] === player.value.getPlayerState()
+			),
+			playbackRate: playbackRate.value
 		};
 
 		aw.sendVideoData(videoData);
@@ -2073,6 +2083,12 @@ onBeforeUnmount(() => {
 				<span><b>Volume slider value</b>: {{ volumeSliderValue }}</span>
 				<span><b>Local paused</b>: {{ localPaused }}</span>
 				<span><b>Station paused</b>: {{ stationPaused }}</span>
+				<span :title="new Date(pausedAt).toString()"
+					><b>Paused at</b>: {{ pausedAt }}</span
+				>
+				<span :title="new Date(startedAt).toString()"
+					><b>Started at</b>: {{ startedAt }}</span
+				>
 				<span
 					><b>Requests enabled</b>:
 					{{ station.requests.enabled }}</span