Browse Source

Merge tag 'v3.4.0' into v3.5.0

Owen Diffey 2 years ago
parent
commit
dc49d6a7af

+ 105 - 0
CHANGELOG.md

@@ -2,6 +2,111 @@
 
 ## [v3.5.0-dev] - Unreleased
 
+## [v3.4.0] - 2022-03-27
+
+### **Breaking Changes**
+This release makes the MongoDB version configurable in the .env file. Prior to this release, the MongoDB version was 4.0. We recommend upgrading to 5.0 or 4.4. Upgrade instructions can be found in [.wiki/Upgrading](.wiki/Upgrading.md#Upgrade/downgradeMongoDB).
+
+Please run the Update All Songs job after upgrading to ensure playlist and station song data accuracy.
+
+### Added
+- feat: Scroll to next song item in Edit Songs queue
+- feat: Reset Advanced Table bulk actions popup position on screen resize if in initial position
+- feat: Global LESS variables
+- refactor: Configurable Main Footer links
+- feat: Configurable Docker container restart policy
+- feat: Backend job to create a song
+- feat: Create song from scratch with Edit Song
+- chore: Added CodeQL analysis GitHub action
+- feat: Ability to select track position in Edit Song player
+- feat: Ability to select playback rate in Edit Song player
+- feat: Login with username or email
+- chore: Added CHANGELOG.md
+- feat: Added view profile button to admin/users table
+- feat: Ability to delete reports
+- feat: Added resolved attribute to reports Advanced Table
+- feat: Option to edit songs after import in Import Playlist
+- feat: Configurable MongoDB and Redis Docker container data directories
+- feat: Ability to toggle report resolution status
+- feat: Ability to show news items to new users on first visit
+- feat: Added warning label to thumbnails in Edit Song if not square
+- chore: Added Upgrading wiki page
+- feat: Configurable MongoDB container image version
+
+### Changed
+- refactor: Replaced night mode toggle slider in Main Header with day/night icons
+- refactor: Replaced SASS/SCCS with LESS
+- refactor: Hide registration buttons and prevent opening register modal if registration is disabled
+- refactor: Trim certain user modifiable strings in playlists, songs, reports and stations
+- refactor: Allow title to wrap to a 2nd line if no there are no artists in Song Item
+- refactor: Consistent border-radius
+- refactor: Consistent box-shadow
+- refactor: Replace deprecated /deep/ selector with :deep()
+- chore: Update frontend and backend packages, and docker images
+- refactor: Move Edit Song verify toggle button to in-form toggle switch
+- refactor: Volume slider styling improvements
+- refactor: Replaced admin secondary nav with sidebar
+- refactor: Moved Request Song import youtube playlist to Import Playlist modal
+- refactor: Select input styling consistency
+- refactor: Show notice that song has been deleted in Edit Song
+- refactor: Reduce dropdown toggle button width
+- refactor: Set title and thumbnail on YouTube video selection in Create Song
+- refactor: Show YouTube tab by default in Create Song
+- refactor: Move admin tab routing to vue router
+- refactor: Pull images in musare.sh build command
+- refactor: Delete user sessions when account is deleted
+
+### Fixed
+- fix: Relative homepage header height causing overlay of content on non-standard resolutions
+- fix: Unable to toggle nightmode on mobile logged out on homepage
+- fix: Station card top row should not wrap
+- fix: Advanced Table CTRL/SHIFT select rows does not work
+- fix: Station not automatically removed from favorite stations on homepage on deletion
+- fix: Playlist songs do not contain verified attribute
+- fix: Newest news should only fetch published items
+- fix: Deleting a song as an admin adds activity item that you deleted a song from genre playlists
+- fix: News item divider has no top/bottom margin
+- fix: Edit Song failing to fetch song reports
+- fix: Station refill can include current song
+- fix: Lofig can not be loaded from deep path
+- fix: CTRL/SHIFT+select Advanced Table rows no longer working
+- fix: Entering station with volume previously set to 0 is handled as muted
+- fix: Genre playlists are created even if the song is unverified
+- fix: Importing YouTube playlist throws URL invalid
+- fix: Song validation should not require genres or artists for unverified songs
+- fix: Station player not unloaded if queue runs empty
+- fix: Edit Song player state not reset on close or next song
+- fix: Playlists could sometimes not be created due to restrictive MongoDB index
+- fix: Add tags to songs doesn't give any feedback to the user
+- fix: AdvancedTable checkboxes overlay mobile navbar dropdown
+- fix: Nightmode -> EditSong -> Discogs API Result release on hover style is messed up
+- fix: Station creation validation always failing
+- fix: Station info display name and description overflow horizontally
+- fix: Volume slider incorrect sensitivity
+- fix: Song thumbnail loading causes jumpiness on admin/songs
+- fix: Manage Station go to station throws an error
+- fix: Edit Song seekTo does not apply if video is stopped
+- fix: Changing password in Settings does not create success toast
+- fix: Invalid user sessions could sometimes break actions
+- fix: Add To Playlist Dropdown create playlist button not full width
+
+### Removed
+- refactor: Removed skip to last 10s button from Edit Song player
+- refactor: Removed Request Song modal
+
+## [v3.4.0-rc2] - 2022-03-19
+
+### Added
+- feat: Re-added ability to hard stop player in Edit Song
+
+### Changed
+- refactor: Delete user sessions when account is deleted
+
+### Fixed
+- fix: Changing password in Settings does not create success toast
+- fix: Invalid user sessions could sometimes break actions
+- fix: Add To Playlist Dropdown create playlist button not full width
+
 ## [v3.4.0-rc1] - 2022-03-06
 
 ### **Breaking Changes**

+ 43 - 35
backend/logic/actions/playlists.js

@@ -525,25 +525,29 @@ export default {
 					userModel.findById(userId).select({ "preferences.orderOfPlaylists": -1 }).exec(next);
 				},
 
-				({ preferences }, next) => {
-					const { orderOfPlaylists } = preferences;
-
-					const match = {
-						createdBy: userId,
-						type: { $in: ["user", "user-liked", "user-disliked"] }
-					};
-
-					// if a playlist order exists
-					if (orderOfPlaylists > 0) match._id = { $in: orderOfPlaylists };
-
-					playlistModel
-						.aggregate()
-						.match(match)
-						.addFields({
-							weight: { $indexOfArray: [orderOfPlaylists, "$_id"] }
-						})
-						.sort({ weight: 1 })
-						.exec(next);
+				(user, next) => {
+					if (!user) next("User not found");
+					else {
+						const { preferences } = user;
+						const { orderOfPlaylists } = preferences;
+
+						const match = {
+							createdBy: userId,
+							type: { $in: ["user", "user-liked", "user-disliked"] }
+						};
+
+						// if a playlist order exists
+						if (orderOfPlaylists > 0) match._id = { $in: orderOfPlaylists };
+
+						playlistModel
+							.aggregate()
+							.match(match)
+							.addFields({
+								weight: { $indexOfArray: [orderOfPlaylists, "$_id"] }
+							})
+							.sort({ weight: 1 })
+							.exec(next);
+					}
 				},
 
 				(playlists, next) => {
@@ -598,25 +602,29 @@ export default {
 					userModel.findById(session.userId).select({ "preferences.orderOfPlaylists": -1 }).exec(next);
 				},
 
-				({ preferences }, next) => {
-					const { orderOfPlaylists } = preferences;
+				(user, next) => {
+					if (!user) next("User not found");
+					else {
+						const { preferences } = user;
+						const { orderOfPlaylists } = preferences;
 
-					const match = {
-						createdBy: session.userId,
-						type: { $in: ["user", "user-liked", "user-disliked"] }
-					};
+						const match = {
+							createdBy: session.userId,
+							type: { $in: ["user", "user-liked", "user-disliked"] }
+						};
 
-					// if a playlist order exists
-					if (orderOfPlaylists > 0) match._id = { $in: orderOfPlaylists };
+						// if a playlist order exists
+						if (orderOfPlaylists > 0) match._id = { $in: orderOfPlaylists };
 
-					playlistModel
-						.aggregate()
-						.match(match)
-						.addFields({
-							weight: { $indexOfArray: [orderOfPlaylists, "$_id"] }
-						})
-						.sort({ weight: 1 })
-						.exec(next);
+						playlistModel
+							.aggregate()
+							.match(match)
+							.addFields({
+								weight: { $indexOfArray: [orderOfPlaylists, "$_id"] }
+							})
+							.sort({ weight: 1 })
+							.exec(next);
+					}
 				}
 			],
 			async (err, playlists) => {

+ 10 - 5
backend/logic/actions/stations.js

@@ -66,11 +66,15 @@ CacheModule.runJob("SUB", {
 									this
 								).then(userModel =>
 									userModel.findOne({ _id: session.userId }, (err, user) => {
-										if (user.role === "admin")
+										if (user && user.role === "admin")
 											socket.dispatch("event:station.userCount.updated", {
 												data: { stationId, count }
 											});
-										else if (station.type === "community" && station.owner === session.userId)
+										else if (
+											user &&
+											station.type === "community" &&
+											station.owner === session.userId
+										)
 											socket.dispatch("event:station.userCount.updated", {
 												data: { stationId, count }
 											});
@@ -302,9 +306,9 @@ CacheModule.runJob("SUB", {
 						}).then(session => {
 							if (session) {
 								userModel.findOne({ _id: session.userId }, (err, user) => {
-									if (user.role === "admin")
+									if (user && user.role === "admin")
 										socket.dispatch("event:station.created", { data: { station } });
-									else if (station.type === "community" && station.owner === session.userId)
+									else if (user && station.type === "community" && station.owner === session.userId)
 										socket.dispatch("event:station.created", { data: { station } });
 								});
 							}
@@ -390,7 +394,8 @@ export default {
 					return next(null, { favoriteStations: [] });
 				},
 
-				({ favoriteStations }, next) => {
+				(user, next) => {
+					const favoriteStations = user ? user.favoriteStations : [];
 					CacheModule.runJob("HGETALL", { table: "stations" }, this).then(stations =>
 						next(null, stations, favoriteStations)
 					);

+ 131 - 6
backend/logic/actions/users.js

@@ -387,8 +387,68 @@ export default {
 					userModel.deleteMany({ _id: session.userId }, next);
 				},
 
-				// request data removal for user
+				// session
 				(res, next) => {
+					CacheModule.runJob("PUB", {
+						channel: "user.removeSessions",
+						value: session.userId
+					});
+
+					async.waterfall(
+						[
+							next => {
+								CacheModule.runJob("HGETALL", { table: "sessions" }, this)
+									.then(sessions => {
+										next(null, sessions);
+									})
+									.catch(next);
+							},
+
+							(sessions, next) => {
+								if (!sessions) return next(null, [], {});
+
+								const keys = Object.keys(sessions);
+
+								return next(null, keys, sessions);
+							},
+
+							(keys, sessions, next) => {
+								// temp fix, need to wait properly for the SUB/PUB refactor (on wekan)
+								const { userId } = session;
+								setTimeout(
+									() =>
+										async.each(
+											keys,
+											(sessionId, callback) => {
+												const session = sessions[sessionId];
+
+												if (session && session.userId === userId) {
+													CacheModule.runJob(
+														"HDEL",
+														{
+															table: "sessions",
+															key: sessionId
+														},
+														this
+													)
+														.then(() => callback(null))
+														.catch(callback);
+												} else callback();
+											},
+											err => {
+												next(err);
+											}
+										),
+									50
+								);
+							}
+						],
+						next
+					);
+				},
+
+				// request data removal for user
+				next => {
 					dataRequestModel.create({ userId: session.userId, type: "remove" }, next);
 				},
 
@@ -555,8 +615,68 @@ export default {
 					userModel.deleteMany({ _id: userId }, next);
 				},
 
-				// request data removal for user
+				// session
 				(res, next) => {
+					CacheModule.runJob("PUB", {
+						channel: "user.removeSessions",
+						value: session.userId
+					});
+
+					async.waterfall(
+						[
+							next => {
+								CacheModule.runJob("HGETALL", { table: "sessions" }, this)
+									.then(sessions => {
+										next(null, sessions);
+									})
+									.catch(next);
+							},
+
+							(sessions, next) => {
+								if (!sessions) return next(null, [], {});
+
+								const keys = Object.keys(sessions);
+
+								return next(null, keys, sessions);
+							},
+
+							(keys, sessions, next) => {
+								// temp fix, need to wait properly for the SUB/PUB refactor (on wekan)
+								const { userId } = session;
+								setTimeout(
+									() =>
+										async.each(
+											keys,
+											(sessionId, callback) => {
+												const session = sessions[sessionId];
+
+												if (session && session.userId === userId) {
+													CacheModule.runJob(
+														"HDEL",
+														{
+															table: "sessions",
+															key: sessionId
+														},
+														this
+													)
+														.then(() => callback(null))
+														.catch(callback);
+												} else callback();
+											},
+											err => {
+												next(err);
+											}
+										),
+									50
+								);
+							}
+						],
+						next
+					);
+				},
+
+				// request data removal for user
+				next => {
 					dataRequestModel.create({ userId, type: "remove" }, next);
 				},
 
@@ -1132,7 +1252,7 @@ export default {
 								(sessionId, callback) => {
 									const session = sessions[sessionId];
 
-									if (session.userId === userId) {
+									if (session && session.userId === userId) {
 										// TODO Also maybe add this to this runJob
 										CacheModule.runJob("HDEL", {
 											table: "sessions",
@@ -1140,7 +1260,7 @@ export default {
 										})
 											.then(() => callback(null))
 											.catch(callback);
-									}
+									} else callback();
 								},
 								err => {
 									next(err);
@@ -1403,9 +1523,14 @@ export default {
 			[
 				next => {
 					userModel.findById(session.userId).select({ preferences: -1 }).exec(next);
+				},
+
+				(user, next) => {
+					if (!user) next("User not found");
+					else next(null, user);
 				}
 			],
-			async (err, { preferences }) => {
+			async (err, user) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 
@@ -1427,7 +1552,7 @@ export default {
 				return cb({
 					status: "success",
 					message: "Preferences successfully retrieved",
-					data: { preferences }
+					data: { preferences: user.preferences }
 				});
 			}
 		);

+ 1 - 1
frontend/src/App.vue

@@ -1183,7 +1183,7 @@ img {
 
 	#create-playlist {
 		margin: 10px 10px 10px 10px;
-		width: unset;
+		width: calc(100% - 20px);
 	}
 }
 

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

@@ -79,8 +79,12 @@
 									</button>
 									<button
 										class="button is-danger"
-										@click="settings('stop')"
-										@keyup.enter="settings('stop')"
+										@click.exact="settings('stop')"
+										@click.shift="settings('hardStop')"
+										@keyup.enter.exact="settings('stop')"
+										@keyup.shift.enter="
+											settings('hardStop')
+										"
 										content="Stop Playback"
 										v-tippy
 									>
@@ -837,6 +841,16 @@ export default {
 			}
 		});
 
+		keyboardShortcuts.registerShortcut("editSong.hardStopVideo", {
+			keyCode: 101,
+			ctrl: true,
+			shift: true,
+			preventDefault: true,
+			handler: () => {
+				this.settings("hardStop");
+			}
+		});
+
 		keyboardShortcuts.registerShortcut("editSong.skipToLast10Secs", {
 			keyCode: 102,
 			preventDefault: true,
@@ -951,6 +965,7 @@ export default {
 
 		editSong.pauseResume - Num 5 - Pause/resume song
 		editSong.stopVideo - Ctrl - Num 5 - Stop
+		editSong.hardStopVideo - Shift - Ctrl - Num 5 - Stop
 		editSong.skipToLast10Secs - Num 6 - Skip to last 10 seconds
 
 		editSong.lowerVolumeLarge - Num 2 - Volume down by 10
@@ -983,6 +998,7 @@ export default {
 		const shortcutNames = [
 			"editSong.pauseResume",
 			"editSong.stopVideo",
+			"editSong.hardStopVideo",
 			"editSong.skipToLast10Secs",
 			"editSong.lowerVolumeLarge",
 			"editSong.lowerVolumeSmall",
@@ -1539,6 +1555,10 @@ export default {
 					this.stopVideo();
 					this.pauseVideo(true);
 					break;
+				case "hardStop":
+					this.hardStopVideo();
+					this.pauseVideo(true);
+					break;
 				case "pause":
 					this.pauseVideo(true);
 					break;
@@ -1804,6 +1824,7 @@ export default {
 		}),
 		...mapActions("modals/editSong", [
 			"stopVideo",
+			"hardStopVideo",
 			"loadVideoById",
 			"pauseVideo",
 			"getCurrentTime",

+ 1 - 1
frontend/src/pages/Settings/Tabs/Security.vue

@@ -254,7 +254,7 @@ export default {
 				res => {
 					if (res.status !== "success") new Toast(res.message);
 					else {
-						this.validation.prevPassword.value = "";
+						this.validation.oldPassword.value = "";
 						this.validation.newPassword.value = "";
 
 						new Toast("Successfully changed password.");

+ 6 - 0
frontend/src/store/modules/modals/editSong.js

@@ -27,6 +27,7 @@ export default {
 			commit("updateOriginalSong", song),
 		resetSong: ({ commit }, songId) => commit("resetSong", songId),
 		stopVideo: ({ commit }) => commit("stopVideo"),
+		hardStopVideo: ({ commit }) => commit("hardStopVideo"),
 		loadVideoById: ({ commit }, id, skipDuration) =>
 			commit("loadVideoById", id, skipDuration),
 		pauseVideo: ({ commit }, status) => commit("pauseVideo", status),
@@ -80,6 +81,11 @@ export default {
 				state.video.player.seekTo(0);
 			}
 		},
+		hardStopVideo(state) {
+			if (state.video.player && state.video.player.stopVideo) {
+				state.video.player.stopVideo();
+			}
+		},
 		loadVideoById(state, id, skipDuration) {
 			state.song.duration = -1;
 			state.video.player.loadVideoById(id, skipDuration);