Переглянути джерело

refactor: Edit Song(s) create from youtube video support

Owen Diffey 2 роки тому
батько
коміт
ffb519327a

+ 3 - 3
backend/logic/actions/songs.js

@@ -267,17 +267,17 @@ export default {
 	 * At this time only used in EditSongs
 	 * At this time only used in EditSongs
 	 *
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {Array} songIds - the song ids
+	 * @param {Array} youtubeIds - the song ids
 	 * @param {Function} cb
 	 * @param {Function} cb
 	 */
 	 */
-	getSongsFromSongIds: isAdminRequired(function getSongFromSongId(session, songIds, cb) {
+	getSongsFromYoutubeIds: isAdminRequired(function getSongsFromYoutubeIds(session, youtubeIds, cb) {
 		async.waterfall(
 		async.waterfall(
 			[
 			[
 				next => {
 				next => {
 					SongsModule.runJob(
 					SongsModule.runJob(
 						"GET_SONGS",
 						"GET_SONGS",
 						{
 						{
-							songIds,
+							youtubeIds,
 							properties: ["youtubeId", "title", "artists", "thumbnail", "duration", "verified", "_id"]
 							properties: ["youtubeId", "title", "artists", "thumbnail", "duration", "verified", "_id"]
 						},
 						},
 						this
 						this

+ 51 - 18
backend/logic/songs.js

@@ -10,6 +10,7 @@ let YouTubeModule;
 let StationsModule;
 let StationsModule;
 let PlaylistsModule;
 let PlaylistsModule;
 let RatingsModule;
 let RatingsModule;
+let WSModule;
 
 
 class _SongsModule extends CoreClass {
 class _SongsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	// eslint-disable-next-line require-jsdoc
@@ -34,6 +35,7 @@ class _SongsModule extends CoreClass {
 		StationsModule = this.moduleManager.modules.stations;
 		StationsModule = this.moduleManager.modules.stations;
 		PlaylistsModule = this.moduleManager.modules.playlists;
 		PlaylistsModule = this.moduleManager.modules.playlists;
 		RatingsModule = this.moduleManager.modules.ratings;
 		RatingsModule = this.moduleManager.modules.ratings;
+		WSModule = this.moduleManager.modules.ws;
 
 
 		this.SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
 		this.SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
 		this.SongSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "song" });
 		this.SongSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "song" });
@@ -41,6 +43,15 @@ class _SongsModule extends CoreClass {
 		this.setStage(2);
 		this.setStage(2);
 
 
 		return new Promise((resolve, reject) => {
 		return new Promise((resolve, reject) => {
+			CacheModule.runJob("SUB", {
+				channel: "song.created",
+				cb: async data =>
+					WSModule.runJob("EMIT_TO_ROOMS", {
+						rooms: ["import-album", `edit-song.${data.song._id}`, "edit-songs"],
+						args: ["event:admin.song.created", { data }]
+					})
+			});
+
 			async.waterfall(
 			async.waterfall(
 				[
 				[
 					next => {
 					next => {
@@ -160,31 +171,44 @@ class _SongsModule extends CoreClass {
 	 * Gets songs by id from Mongo
 	 * Gets songs by id from Mongo
 	 *
 	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {object} payload - object containing the payload
-	 * @param {string} payload.songIds - the ids of the songs we are trying to get
-	 * @param {string} payload.properties - the properties to return
+	 * @param {string} payload.youtubeIds - the youtube ids of the songs we are trying to get
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	 */
 	GET_SONGS(payload) {
 	GET_SONGS(payload) {
 		return new Promise((resolve, reject) => {
 		return new Promise((resolve, reject) => {
 			async.waterfall(
 			async.waterfall(
 				[
 				[
-					next => {
-						if (!payload.songIds.every(songId => mongoose.Types.ObjectId.isValid(songId)))
-							next("One or more songIds are not a valid ObjectId.");
-						else next();
-					},
+					next => SongsModule.SongModel.find({ youtubeId: { $in: payload.youtubeIds } }, next),
 
 
-					next => {
-						const includeProperties = {};
-						payload.properties.forEach(property => {
-							includeProperties[property] = true;
-						});
-						return SongsModule.SongModel.find(
-							{
-								_id: { $in: payload.songIds }
-							},
-							includeProperties,
-							next
+					(songs, next) => {
+						const youtubeIds = payload.youtubeIds.filter(
+							youtubeId => !songs.find(song => song.youtubeId === youtubeId)
+						);
+						return YouTubeModule.youtubeVideoModel.find(
+							{ youtubeId: { $in: youtubeIds } },
+							(err, videos) => {
+								if (err) next(err);
+								else {
+									const youtubeVideos = videos.map(video => {
+										const { youtubeId, title, author, duration, thumbnail } = video;
+										return {
+											youtubeId,
+											title,
+											artists: [author],
+											genres: [],
+											tags: [],
+											duration,
+											skipDuration: 0,
+											thumbnail:
+												thumbnail || `https://img.youtube.com/vi/${youtubeId}/mqdefault.jpg`,
+											requestedBy: null,
+											requestedAt: Date.now(),
+											verified: false
+										};
+									});
+									next(null, [...songs, ...youtubeVideos]);
+								}
+							}
 						);
 						);
 					}
 					}
 				],
 				],
@@ -304,6 +328,15 @@ class _SongsModule extends CoreClass {
 						RatingsModule.runJob("RECALCULATE_RATINGS", { youtubeId: song.youtubeId }, this)
 						RatingsModule.runJob("RECALCULATE_RATINGS", { youtubeId: song.youtubeId }, this)
 							.then(() => next(null, song))
 							.then(() => next(null, song))
 							.catch(next);
 							.catch(next);
+					},
+
+					(song, next) => {
+						CacheModule.runJob("PUB", {
+							channel: "song.created",
+							value: { song }
+						})
+							.then(() => next(null, song))
+							.catch(next);
 					}
 					}
 				],
 				],
 				(err, song) => {
 				(err, song) => {

+ 1 - 1
frontend/src/components/SongItem.vue

@@ -270,7 +270,7 @@ export default {
 			this.hideTippyElements();
 			this.hideTippyElements();
 			this.openModal({
 			this.openModal({
 				modal: "editSong",
 				modal: "editSong",
-				data: { song: { songId: song._id } }
+				data: { song }
 			});
 			});
 		},
 		},
 		...mapActions("modalVisibility", ["openModal"]),
 		...mapActions("modalVisibility", ["openModal"]),

+ 124 - 81
frontend/src/components/modals/EditSong/index.vue

@@ -15,7 +15,7 @@
 				<slot name="sidebar" />
 				<slot name="sidebar" />
 			</template>
 			</template>
 			<template #body>
 			<template #body>
-				<div v-if="!songId && !newSong" class="notice-container">
+				<div v-if="!youtubeId && !newSong" class="notice-container">
 					<h4>No song has been selected</h4>
 					<h4>No song has been selected</h4>
 				</div>
 				</div>
 				<div v-if="songDeleted" class="notice-container">
 				<div v-if="songDeleted" class="notice-container">
@@ -23,14 +23,17 @@
 				</div>
 				</div>
 				<div
 				<div
 					v-if="
 					v-if="
-						songId && !songDataLoaded && !songNotFound && !newSong
+						youtubeId &&
+						!songDataLoaded &&
+						!songNotFound &&
+						!newSong
 					"
 					"
 					class="notice-container"
 					class="notice-container"
 				>
 				>
 					<h4>Song hasn't loaded yet</h4>
 					<h4>Song hasn't loaded yet</h4>
 				</div>
 				</div>
 				<div
 				<div
-					v-if="songId && songNotFound && !newSong"
+					v-if="youtubeId && songNotFound && !newSong"
 					class="notice-container"
 					class="notice-container"
 				>
 				>
 					<h4>Song was not found</h4>
 					<h4>Song was not found</h4>
@@ -634,7 +637,7 @@
 					<button
 					<button
 						class="button is-primary"
 						class="button is-primary"
 						@click="toggleFlag()"
 						@click="toggleFlag()"
-						v-if="songId && !songDeleted"
+						v-if="youtubeId && !songDeleted"
 					>
 					>
 						{{ flagged ? "Unflag" : "Flag" }}
 						{{ flagged ? "Unflag" : "Flag" }}
 					</button>
 					</button>
@@ -676,6 +679,15 @@
 						default-message="Create Song"
 						default-message="Create Song"
 						@clicked="save(song, false, 'createButton', true)"
 						@clicked="save(song, false, 'createButton', true)"
 					/>
 					/>
+					<save-button
+						ref="createAndCloseButton"
+						:default-message="
+							bulk ? `Create and next` : `Create and close`
+						"
+						@clicked="
+							save(song, true, 'createAndCloseButton', true)
+						"
+					/>
 				</div>
 				</div>
 			</template>
 			</template>
 		</modal>
 		</modal>
@@ -826,7 +838,7 @@ export default {
 			tab: state => state.tab,
 			tab: state => state.tab,
 			video: state => state.video,
 			video: state => state.video,
 			song: state => state.song,
 			song: state => state.song,
-			songId: state => state.songId,
+			youtubeId: state => state.youtubeId,
 			prefillData: state => state.prefillData,
 			prefillData: state => state.prefillData,
 			originalSong: state => state.originalSong,
 			originalSong: state => state.originalSong,
 			reports: state => state.reports,
 			reports: state => state.reports,
@@ -848,10 +860,10 @@ export default {
 			this.drawCanvas();
 			this.drawCanvas();
 		},
 		},
 		/* eslint-enable */
 		/* eslint-enable */
-		songId(songId, oldSongId) {
-			console.log("NEW SONG ID", songId);
-			this.unloadSong(oldSongId);
-			this.loadSong(songId);
+		youtubeId(youtubeId, oldYoutubeId) {
+			console.log("NEW YOUTUBE ID", youtubeId);
+			this.unloadSong(oldYoutubeId);
+			this.loadSong(youtubeId);
 		}
 		}
 	},
 	},
 	async mounted() {
 	async mounted() {
@@ -870,17 +882,15 @@ export default {
 		localStorage.setItem("volume", volume);
 		localStorage.setItem("volume", volume);
 		this.volumeSliderValue = volume;
 		this.volumeSliderValue = volume;
 
 
-		if (!this.newSong) {
-			this.socket.on(
-				"event:admin.song.removed",
-				res => {
-					if (res.data.songId === this.song._id) {
-						this.songDeleted = true;
-					}
-				},
-				{ modalUuid: this.modalUuid }
-			);
-		}
+		this.socket.on(
+			"event:admin.song.removed",
+			res => {
+				if (res.data.songId === this.song._id) {
+					this.songDeleted = true;
+				}
+			},
+			{ modalUuid: this.modalUuid }
+		);
 
 
 		keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
 		keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
 			keyCode: 101,
 			keyCode: 101,
@@ -1052,7 +1062,7 @@ export default {
 	},
 	},
 	beforeUnmount() {
 	beforeUnmount() {
 		console.log("UNMOUNT");
 		console.log("UNMOUNT");
-		if (!this.newSong) this.unloadSong(this.songId);
+		this.unloadSong(this.youtubeId, this.song._id);
 
 
 		this.playerReady = false;
 		this.playerReady = false;
 		clearInterval(this.interval);
 		clearInterval(this.interval);
@@ -1108,7 +1118,7 @@ export default {
 			this.thumbnailLoadError = error !== 0;
 			this.thumbnailLoadError = error !== 0;
 		},
 		},
 		init() {
 		init() {
-			if (this.newSong) {
+			if (this.newSong && !this.youtubeId) {
 				this.setSong({
 				this.setSong({
 					youtubeId: "",
 					youtubeId: "",
 					title: "",
 					title: "",
@@ -1122,7 +1132,7 @@ export default {
 				});
 				});
 				this.songDataLoaded = true;
 				this.songDataLoaded = true;
 				this.showTab("youtube");
 				this.showTab("youtube");
-			} else if (this.songId) this.loadSong(this.songId);
+			} else if (this.youtubeId) this.loadSong(this.youtubeId);
 			else if (!this.bulk) {
 			else if (!this.bulk) {
 				new Toast("You can't open EditSong without editing a song");
 				new Toast("You can't open EditSong without editing a song");
 				return this.closeModal("editSong");
 				return this.closeModal("editSong");
@@ -1346,12 +1356,12 @@ export default {
 
 
 			return null;
 			return null;
 		},
 		},
-		unloadSong(songId) {
+		unloadSong(youtubeId, songId) {
 			this.songDataLoaded = false;
 			this.songDataLoaded = false;
 			this.songDeleted = false;
 			this.songDeleted = false;
 			this.stopVideo();
 			this.stopVideo();
 			this.pauseVideo(true);
 			this.pauseVideo(true);
-			this.resetSong(songId);
+			this.resetSong(youtubeId);
 			this.thumbnailNotSquare = false;
 			this.thumbnailNotSquare = false;
 			this.thumbnailWidth = null;
 			this.thumbnailWidth = null;
 			this.thumbnailHeight = null;
 			this.thumbnailHeight = null;
@@ -1361,40 +1371,52 @@ export default {
 			this.socket.dispatch("apis.leaveRoom", `edit-song.${songId}`);
 			this.socket.dispatch("apis.leaveRoom", `edit-song.${songId}`);
 			if (this.$refs.saveButton) this.$refs.saveButton.status = "default";
 			if (this.$refs.saveButton) this.$refs.saveButton.status = "default";
 		},
 		},
-		loadSong(songId) {
-			console.log(`LOAD SONG ${songId}`);
+		loadSong(youtubeId) {
+			console.log(`LOAD SONG ${youtubeId}`);
 			this.songNotFound = false;
 			this.songNotFound = false;
-			this.socket.dispatch(`songs.getSongFromSongId`, songId, res => {
-				if (res.status === "success") {
-					let { song } = res.data;
-
-					song = Object.assign(song, this.prefillData);
-
-					this.setSong(song);
+			this.socket.dispatch(
+				`songs.getSongsFromYoutubeIds`,
+				[youtubeId],
+				res => {
+					const { songs } = res.data;
+					if (res.status === "success" && songs.length > 0) {
+						let song = songs[0];
+						song = Object.assign(song, this.prefillData);
 
 
-					this.songDataLoaded = true;
+						this.setSong(song);
 
 
-					this.socket.dispatch(
-						"apis.joinRoom",
-						`edit-song.${this.song._id}`
-					);
+						this.songDataLoaded = true;
 
 
-					if (this.video.player && this.video.player.cueVideoById) {
-						this.video.player.cueVideoById(
-							this.song.youtubeId,
-							this.song.skipDuration
+						this.socket.dispatch(
+							"apis.joinRoom",
+							`edit-song.${this.song._id}`
 						);
 						);
+
+						if (
+							this.video.player &&
+							this.video.player.cueVideoById
+						) {
+							this.video.player.cueVideoById(
+								this.youtubeId,
+								song.skipDuration
+							);
+						}
+					} else {
+						new Toast("Song with that ID not found");
+						if (this.bulk) this.songNotFound = true;
+						if (!this.bulk) this.closeModal("editSong");
 					}
 					}
-				} else {
-					new Toast("Song with that ID not found");
-					if (this.bulk) this.songNotFound = true;
-					if (!this.bulk) this.closeModal("editSong");
 				}
 				}
-			});
+			);
 
 
-			this.socket.dispatch("reports.getReportsForSong", songId, res => {
-				this.updateReports(res.data.reports);
-			});
+			if (!this.newSong)
+				this.socket.dispatch(
+					"reports.getReportsForSong",
+					this.song._id,
+					res => {
+						this.updateReports(res.data.reports);
+					}
+				);
 		},
 		},
 		importAlbum(result) {
 		importAlbum(result) {
 			this.selectDiscogsAlbum(result);
 			this.selectDiscogsAlbum(result);
@@ -1404,25 +1426,27 @@ export default {
 		save(songToCopy, closeOrNext, saveButtonRefName, newSong = false) {
 		save(songToCopy, closeOrNext, saveButtonRefName, newSong = false) {
 			const song = JSON.parse(JSON.stringify(songToCopy));
 			const song = JSON.parse(JSON.stringify(songToCopy));
 
 
-			if (!newSong) this.$emit("saving", song._id);
+			if (!newSong || this.bulk) this.$emit("saving", song.youtubeId);
 
 
 			const saveButtonRef = this.$refs[saveButtonRefName];
 			const saveButtonRef = this.$refs[saveButtonRefName];
 
 
 			if (!this.youtubeError && this.youtubeVideoDuration === "0.000") {
 			if (!this.youtubeError && this.youtubeVideoDuration === "0.000") {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song.youtubeId);
 				return new Toast("The video appears to not be working.");
 				return new Toast("The video appears to not be working.");
 			}
 			}
 
 
 			if (!song.title) {
 			if (!song.title) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast("Please fill in all fields");
 				return new Toast("Please fill in all fields");
 			}
 			}
 
 
 			if (!song.thumbnail) {
 			if (!song.thumbnail) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast("Please fill in all fields");
 				return new Toast("Please fill in all fields");
 			}
 			}
 
 
@@ -1455,7 +1479,8 @@ export default {
 				this.originalSong.youtubeId !== song.youtubeId
 				this.originalSong.youtubeId !== song.youtubeId
 			) {
 			) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 				return new Toast(
 					"You're not allowed to change the YouTube id while the player is not working"
 					"You're not allowed to change the YouTube id while the player is not working"
 				);
 				);
@@ -1465,11 +1490,12 @@ export default {
 			if (
 			if (
 				Number(song.skipDuration) + Number(song.duration) >
 				Number(song.skipDuration) + Number(song.duration) >
 					this.youtubeVideoDuration &&
 					this.youtubeVideoDuration &&
-				((!newSong && !this.youtubeError) ||
+				(((!newSong || this.bulk) && !this.youtubeError) ||
 					this.originalSong.duration !== song.duration)
 					this.originalSong.duration !== song.duration)
 			) {
 			) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 				return new Toast(
 					"Duration can't be higher than the length of the video"
 					"Duration can't be higher than the length of the video"
 				);
 				);
@@ -1478,7 +1504,8 @@ export default {
 			// Title
 			// Title
 			if (!validation.isLength(song.title, 1, 100)) {
 			if (!validation.isLength(song.title, 1, 100)) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 				return new Toast(
 					"Title must have between 1 and 100 characters."
 					"Title must have between 1 and 100 characters."
 				);
 				);
@@ -1490,7 +1517,8 @@ export default {
 				song.artists.length > 10
 				song.artists.length > 10
 			) {
 			) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 				return new Toast(
 					"Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
 					"Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
 				);
 				);
@@ -1513,25 +1541,27 @@ export default {
 
 
 			if (error) {
 			if (error) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(error);
 				return new Toast(error);
 			}
 			}
 
 
 			// Genres
 			// Genres
 			error = undefined;
 			error = undefined;
-			song.genres.forEach(genre => {
-				if (!validation.isLength(genre, 1, 32)) {
-					error = "Genre must have between 1 and 32 characters.";
-					return error;
-				}
-				if (!validation.regex.ascii.test(genre)) {
-					error =
-						"Invalid genre format. Only ascii characters are allowed.";
-					return error;
-				}
+			if (song.verified && song.genres.length < 1)
+				song.genres.forEach(genre => {
+					if (!validation.isLength(genre, 1, 32)) {
+						error = "Genre must have between 1 and 32 characters.";
+						return error;
+					}
+					if (!validation.regex.ascii.test(genre)) {
+						error =
+							"Invalid genre format. Only ascii characters are allowed.";
+						return error;
+					}
 
 
-				return false;
-			});
+					return false;
+				});
 
 
 			if (
 			if (
 				(song.verified && song.genres.length < 1) ||
 				(song.verified && song.genres.length < 1) ||
@@ -1541,7 +1571,8 @@ export default {
 
 
 			if (error) {
 			if (error) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(error);
 				return new Toast(error);
 			}
 			}
 
 
@@ -1561,21 +1592,24 @@ export default {
 
 
 			if (error) {
 			if (error) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(error);
 				return new Toast(error);
 			}
 			}
 
 
 			// Thumbnail
 			// Thumbnail
 			if (!validation.isLength(song.thumbnail, 1, 256)) {
 			if (!validation.isLength(song.thumbnail, 1, 256)) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 				return new Toast(
 					"Thumbnail must have between 8 and 256 characters."
 					"Thumbnail must have between 8 and 256 characters."
 				);
 				);
 			}
 			}
 			if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
 			if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast('Thumbnail must start with "https://".');
 				return new Toast('Thumbnail must start with "https://".');
 			}
 			}
 
 
@@ -1585,7 +1619,8 @@ export default {
 				song.thumbnail.indexOf("https://") !== 0
 				song.thumbnail.indexOf("https://") !== 0
 			) {
 			) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast('Thumbnail must start with "http://".');
 				return new Toast('Thumbnail must start with "http://".');
 			}
 			}
 
 
@@ -1597,26 +1632,34 @@ export default {
 
 
 					if (res.status === "error") {
 					if (res.status === "error") {
 						saveButtonRef.handleFailedSave();
 						saveButtonRef.handleFailedSave();
+						this.$emit("savedError", song.youtubeId);
 						return;
 						return;
 					}
 					}
 
 
 					saveButtonRef.handleSuccessfulSave();
 					saveButtonRef.handleSuccessfulSave();
+					this.$emit("savedSuccess", song.youtubeId);
+
+					if (!closeOrNext) {
+						this.loadSong(song.youtubeId);
+						return;
+					}
 
 
-					this.closeModal("editSong");
+					if (this.bulk) this.$emit("nextSong");
+					else this.closeModal("editSong");
 				});
 				});
 			return this.socket.dispatch(`songs.update`, song._id, song, res => {
 			return this.socket.dispatch(`songs.update`, song._id, song, res => {
 				new Toast(res.message);
 				new Toast(res.message);
 
 
 				if (res.status === "error") {
 				if (res.status === "error") {
 					saveButtonRef.handleFailedSave();
 					saveButtonRef.handleFailedSave();
-					this.$emit("savedError", song._id);
+					this.$emit("savedError", song.youtubeId);
 					return;
 					return;
 				}
 				}
 
 
 				this.updateOriginalSong(song);
 				this.updateOriginalSong(song);
 
 
 				saveButtonRef.handleSuccessfulSave();
 				saveButtonRef.handleSuccessfulSave();
-				this.$emit("savedSuccess", song._id);
+				this.$emit("savedSuccess", song.youtubeId);
 
 
 				if (!closeOrNext) return;
 				if (!closeOrNext) return;
 
 

+ 59 - 34
frontend/src/components/modals/EditSongs.vue

@@ -44,8 +44,8 @@
 							v-for="(
 							v-for="(
 								{ status, flagged, song }, index
 								{ status, flagged, song }, index
 							) in filteredItems"
 							) in filteredItems"
-							:key="song._id"
-							:ref="`edit-songs-item-${song._id}`"
+							:key="song.youtubeId"
+							:ref="`edit-songs-item-${song.youtubeId}`"
 						>
 						>
 							<song-item
 							<song-item
 								:song="song"
 								:song="song"
@@ -61,7 +61,10 @@
 							>
 							>
 								<template #leftIcon>
 								<template #leftIcon>
 									<i
 									<i
-										v-if="currentSong._id === song._id"
+										v-if="
+											currentSong.youtubeId ===
+												song.youtubeId && !song.removed
+										"
 										class="material-icons item-icon editing-icon"
 										class="material-icons item-icon editing-icon"
 										content="Currently editing song"
 										content="Currently editing song"
 										v-tippy="{ theme: 'info' }"
 										v-tippy="{ theme: 'info' }"
@@ -201,12 +204,12 @@ export default {
 	computed: {
 	computed: {
 		editingItemIndex() {
 		editingItemIndex() {
 			return this.items.findIndex(
 			return this.items.findIndex(
-				item => item.song._id === this.currentSong._id
+				item => item.song.youtubeId === this.currentSong.youtubeId
 			);
 			);
 		},
 		},
 		filteredEditingItemIndex() {
 		filteredEditingItemIndex() {
 			return this.filteredItems.findIndex(
 			return this.filteredItems.findIndex(
-				item => item.song._id === this.currentSong._id
+				item => item.song.youtubeId === this.currentSong.youtubeId
 			);
 			);
 		},
 		},
 		filteredItems: {
 		filteredItems: {
@@ -217,18 +220,18 @@ export default {
 			},
 			},
 			set(newItem) {
 			set(newItem) {
 				const index = this.items.findIndex(
 				const index = this.items.findIndex(
-					item => item.song._id === newItem._id
+					item => item.song.youtubeId === newItem.youtubeId
 				);
 				);
 				this.item[index] = newItem;
 				this.item[index] = newItem;
 			}
 			}
 		},
 		},
 		currentSongFlagged() {
 		currentSongFlagged() {
 			return this.items.find(
 			return this.items.find(
-				item => item.song._id === this.currentSong._id
+				item => item.song.youtubeId === this.currentSong.youtubeId
 			)?.flagged;
 			)?.flagged;
 		},
 		},
 		...mapModalState("modals/editSongs/MODAL_UUID", {
 		...mapModalState("modals/editSongs/MODAL_UUID", {
-			songIds: state => state.songIds,
+			youtubeIds: state => state.youtubeIds,
 			songPrefillData: state => state.songPrefillData
 			songPrefillData: state => state.songPrefillData
 		}),
 		}),
 		...mapGetters({
 		...mapGetters({
@@ -243,27 +246,46 @@ export default {
 			editSong
 			editSong
 		);
 		);
 
 
-		this.socket.dispatch("songs.getSongsFromSongIds", this.songIds, res => {
-			res.data.songs.forEach(song => {
-				this.items.push({
-					status: "todo",
-					flagged: false,
-					song
+		this.socket.dispatch(
+			"songs.getSongsFromYoutubeIds",
+			this.youtubeIds,
+			res => {
+				res.data.songs.forEach(song => {
+					this.items.push({
+						status: "todo",
+						flagged: false,
+						song
+					});
 				});
 				});
-			});
 
 
-			if (this.items.length === 0) {
-				this.closeThisModal();
-				new Toast("You can't edit 0 songs.");
-			} else this.editNextSong();
-		});
+				if (this.items.length === 0) {
+					this.closeThisModal();
+					new Toast("You can't edit 0 songs.");
+				} else this.editNextSong();
+			}
+		);
+
+		this.socket.on(
+			`event:admin.song.created`,
+			res => {
+				const index = this.items
+					.map(item => item.song.youtubeId)
+					.indexOf(res.data.song.youtubeId);
+				this.items[index].song = {
+					...this.items[index].song,
+					...res.data.song,
+					created: true
+				};
+			},
+			{ modalUuid: this.modalUuid }
+		);
 
 
 		this.socket.on(
 		this.socket.on(
 			`event:admin.song.updated`,
 			`event:admin.song.updated`,
 			res => {
 			res => {
 				const index = this.items
 				const index = this.items
-					.map(item => item.song._id)
-					.indexOf(res.data.song._id);
+					.map(item => item.song.youtubeId)
+					.indexOf(res.data.song.youtubeId);
 				this.items[index].song = {
 				this.items[index].song = {
 					...this.items[index].song,
 					...this.items[index].song,
 					...res.data.song,
 					...res.data.song,
@@ -294,15 +316,17 @@ export default {
 	methods: {
 	methods: {
 		pickSong(song) {
 		pickSong(song) {
 			this.editSong({
 			this.editSong({
-				songId: song._id,
-				prefill: this.songPrefillData[song._id]
+				youtubeId: song.youtubeId,
+				prefill: this.songPrefillData[song.youtubeId]
 			});
 			});
 			this.currentSong = song;
 			this.currentSong = song;
 			if (
 			if (
-				this.$refs[`edit-songs-item-${song._id}`] &&
-				this.$refs[`edit-songs-item-${song._id}`][0]
+				this.$refs[`edit-songs-item-${song.youtubeId}`] &&
+				this.$refs[`edit-songs-item-${song.youtubeId}`][0]
 			)
 			)
-				this.$refs[`edit-songs-item-${song._id}`][0].scrollIntoView();
+				this.$refs[
+					`edit-songs-item-${song.youtubeId}`
+				][0].scrollIntoView();
 		},
 		},
 		editNextSong() {
 		editNextSong() {
 			const currentlyEditingSongIndex = this.filteredEditingItemIndex;
 			const currentlyEditingSongIndex = this.filteredEditingItemIndex;
@@ -320,7 +344,8 @@ export default {
 
 
 			if (newEditingSongIndex > -1) {
 			if (newEditingSongIndex > -1) {
 				const nextSong = this.filteredItems[newEditingSongIndex].song;
 				const nextSong = this.filteredItems[newEditingSongIndex].song;
-				this.pickSong(nextSong);
+				if (nextSong.removed) this.editNextSong();
+				else this.pickSong(nextSong);
 			}
 			}
 		},
 		},
 		toggleFlag(songIndex = null) {
 		toggleFlag(songIndex = null) {
@@ -346,24 +371,24 @@ export default {
 				);
 				);
 			}
 			}
 		},
 		},
-		onSavedSuccess(songId) {
+		onSavedSuccess(youtubeId) {
 			const itemIndex = this.items.findIndex(
 			const itemIndex = this.items.findIndex(
-				item => item.song._id === songId
+				item => item.song.youtubeId === youtubeId
 			);
 			);
 			if (itemIndex > -1) {
 			if (itemIndex > -1) {
 				this.items[itemIndex].status = "done";
 				this.items[itemIndex].status = "done";
 				this.items[itemIndex].flagged = false;
 				this.items[itemIndex].flagged = false;
 			}
 			}
 		},
 		},
-		onSavedError(songId) {
+		onSavedError(youtubeId) {
 			const itemIndex = this.items.findIndex(
 			const itemIndex = this.items.findIndex(
-				item => item.song._id === songId
+				item => item.song.youtubeId === youtubeId
 			);
 			);
 			if (itemIndex > -1) this.items[itemIndex].status = "error";
 			if (itemIndex > -1) this.items[itemIndex].status = "error";
 		},
 		},
-		onSaving(songId) {
+		onSaving(youtubeId) {
 			const itemIndex = this.items.findIndex(
 			const itemIndex = this.items.findIndex(
-				item => item.song._id === songId
+				item => item.song.youtubeId === youtubeId
 			);
 			);
 			if (itemIndex > -1) this.items[itemIndex].status = "saving";
 			if (itemIndex > -1) this.items[itemIndex].status = "saving";
 		},
 		},

+ 1 - 1
frontend/src/components/modals/ImportAlbum.vue

@@ -425,7 +425,7 @@ export default {
 					delete discogsAlbum.gotMoreInfo;
 					delete discogsAlbum.gotMoreInfo;
 
 
 					const songToEdit = {
 					const songToEdit = {
-						songId: song._id,
+						youtubeId: song.youtubeId,
 						prefill: {
 						prefill: {
 							discogs: discogsAlbum
 							discogs: discogsAlbum
 						}
 						}

+ 1 - 1
frontend/src/components/modals/ViewReport.vue

@@ -246,7 +246,7 @@ export default {
 		openSong() {
 		openSong() {
 			this.openModal({
 			this.openModal({
 				modal: "editSong",
 				modal: "editSong",
-				data: { song: { songId: this.report.song._id } }
+				data: { song: this.report.song }
 			});
 			});
 		},
 		},
 		...mapActions("admin/reports", [
 		...mapActions("admin/reports", [

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

@@ -558,14 +558,14 @@ export default {
 		editOne(song) {
 		editOne(song) {
 			this.openModal({
 			this.openModal({
 				modal: "editSong",
 				modal: "editSong",
-				data: { song: { songId: song._id } }
+				data: { song }
 			});
 			});
 		},
 		},
 		editMany(selectedRows) {
 		editMany(selectedRows) {
 			if (selectedRows.length === 1) this.editOne(selectedRows[0]);
 			if (selectedRows.length === 1) this.editOne(selectedRows[0]);
 			else {
 			else {
 				const songs = selectedRows.map(row => ({
 				const songs = selectedRows.map(row => ({
-					songId: row._id
+					youtubeId: row.youtubeId
 				}));
 				}));
 				this.openModal({ modal: "editSongs", data: { songs } });
 				this.openModal({ modal: "editSongs", data: { songs } });
 			}
 			}

+ 14 - 5
frontend/src/pages/Admin/YouTube/Videos.vue

@@ -37,6 +37,15 @@
 					>
 					>
 						open_in_full
 						open_in_full
 					</button>
 					</button>
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="editOne(slotProps.item)"
+						:disabled="slotProps.item.removed"
+						content="Create/edit song from video"
+						v-tippy
+					>
+						music_note
+					</button>
 					<button
 					<button
 						class="button is-danger icon-with-button material-icons"
 						class="button is-danger icon-with-button material-icons"
 						@click.prevent="
 						@click.prevent="
@@ -99,7 +108,7 @@
 					<i
 					<i
 						class="material-icons create-songs-icon"
 						class="material-icons create-songs-icon"
 						@click.prevent="editMany(slotProps.item)"
 						@click.prevent="editMany(slotProps.item)"
-						content="Create songs from videos"
+						content="Create/edit songs from videos"
 						v-tippy
 						v-tippy
 						tabindex="0"
 						tabindex="0"
 					>
 					>
@@ -159,8 +168,8 @@ export default {
 					sortable: false,
 					sortable: false,
 					hidable: false,
 					hidable: false,
 					resizable: false,
 					resizable: false,
-					minWidth: 85,
-					defaultWidth: 85
+					minWidth: 129,
+					defaultWidth: 129
 				},
 				},
 				{
 				{
 					name: "thumbnailImage",
 					name: "thumbnailImage",
@@ -295,14 +304,14 @@ export default {
 		editOne(song) {
 		editOne(song) {
 			this.openModal({
 			this.openModal({
 				modal: "editSong",
 				modal: "editSong",
-				data: { song: { songId: song._id } }
+				data: { song }
 			});
 			});
 		},
 		},
 		editMany(selectedRows) {
 		editMany(selectedRows) {
 			if (selectedRows.length === 1) this.editOne(selectedRows[0]);
 			if (selectedRows.length === 1) this.editOne(selectedRows[0]);
 			else {
 			else {
 				const songs = selectedRows.map(row => ({
 				const songs = selectedRows.map(row => ({
-					songId: row._id
+					youtubeId: row.youtubeId
 				}));
 				}));
 				this.openModal({ modal: "editSongs", data: { songs } });
 				this.openModal({ modal: "editSongs", data: { songs } });
 			}
 			}

+ 14 - 8
frontend/src/store/modules/modals/editSong.js

@@ -11,7 +11,7 @@ export default {
 			currentTime: 0,
 			currentTime: 0,
 			playbackRate: 1
 			playbackRate: 1
 		},
 		},
-		songId: null,
+		youtubeId: null,
 		song: {},
 		song: {},
 		originalSong: {},
 		originalSong: {},
 		reports: [],
 		reports: [],
@@ -26,7 +26,7 @@ export default {
 		setSong: ({ commit }, song) => commit("setSong", song),
 		setSong: ({ commit }, song) => commit("setSong", song),
 		updateOriginalSong: ({ commit }, song) =>
 		updateOriginalSong: ({ commit }, song) =>
 			commit("updateOriginalSong", song),
 			commit("updateOriginalSong", song),
-		resetSong: ({ commit }, songId) => commit("resetSong", songId),
+		resetSong: ({ commit }, youtubeId) => commit("resetSong", youtubeId),
 		stopVideo: ({ commit }) => commit("stopVideo"),
 		stopVideo: ({ commit }) => commit("stopVideo"),
 		hardStopVideo: ({ commit }) => commit("hardStopVideo"),
 		hardStopVideo: ({ commit }) => commit("hardStopVideo"),
 		loadVideoById: ({ commit }, id, skipDuration) =>
 		loadVideoById: ({ commit }, id, skipDuration) =>
@@ -58,22 +58,28 @@ export default {
 			state.tab = tab;
 			state.tab = tab;
 		},
 		},
 		editSong(state, song) {
 		editSong(state, song) {
-			state.newSong = !!song.newSong;
-			state.songId = song.newSong ? null : song.songId;
+			state.newSong = !!song.newSong || !song._id;
+			state.youtubeId = song.newSong ? null : song.youtubeId;
 			state.prefillData = song.prefill ? song.prefill : {};
 			state.prefillData = song.prefill ? song.prefill : {};
 		},
 		},
 		setSong(state, song) {
 		setSong(state, song) {
 			if (song.discogs === undefined) song.discogs = null;
 			if (song.discogs === undefined) song.discogs = null;
 			state.originalSong = JSON.parse(JSON.stringify(song));
 			state.originalSong = JSON.parse(JSON.stringify(song));
 			state.song = { ...song };
 			state.song = { ...song };
+			state.newSong = !song._id;
+			state.youtubeId = song.youtubeId;
 		},
 		},
 		updateOriginalSong(state, song) {
 		updateOriginalSong(state, song) {
 			state.originalSong = JSON.parse(JSON.stringify(song));
 			state.originalSong = JSON.parse(JSON.stringify(song));
 		},
 		},
-		resetSong(state, songId) {
-			if (state.songId === songId) state.songId = "";
-			if (state.song && state.song._id === songId) state.song = {};
-			if (state.originalSong && state.originalSong._id === songId)
+		resetSong(state, youtubeId) {
+			if (state.youtubeId === youtubeId) state.youtubeId = "";
+			if (state.song && state.song.youtubeId === youtubeId)
+				state.song = {};
+			if (
+				state.originalSong &&
+				state.originalSong.youtubeId === youtubeId
+			)
 				state.originalSong = {};
 				state.originalSong = {};
 		},
 		},
 		stopVideo(state) {
 		stopVideo(state) {

+ 4 - 4
frontend/src/store/modules/modals/editSongs.js

@@ -3,7 +3,7 @@
 export default {
 export default {
 	namespaced: true,
 	namespaced: true,
 	state: {
 	state: {
-		songIds: [],
+		youtubeIds: [],
 		songPrefillData: {}
 		songPrefillData: {}
 	},
 	},
 	getters: {},
 	getters: {},
@@ -13,16 +13,16 @@ export default {
 	},
 	},
 	mutations: {
 	mutations: {
 		init(state, { songs }) {
 		init(state, { songs }) {
-			state.songIds = songs.map(song => song.songId);
+			state.youtubeIds = songs.map(song => song.youtubeId);
 			state.songPrefillData = Object.fromEntries(
 			state.songPrefillData = Object.fromEntries(
 				songs.map(song => [
 				songs.map(song => [
-					song.songId,
+					song.youtubeId,
 					song.prefill ? song.prefill : {}
 					song.prefill ? song.prefill : {}
 				])
 				])
 			);
 			);
 		}
 		}
 		// resetSongs(state) {
 		// resetSongs(state) {
-		// 	state.songIds = [];
+		// 	state.youtubeIds = [];
 		// 	state.songPrefillData = {};
 		// 	state.songPrefillData = {};
 		// }
 		// }
 	}
 	}