Browse Source

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

Owen Diffey 2 years ago
parent
commit
ffb519327a

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

@@ -267,17 +267,17 @@ export default {
 	 * At this time only used in EditSongs
 	 *
 	 * @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
 	 */
-	getSongsFromSongIds: isAdminRequired(function getSongFromSongId(session, songIds, cb) {
+	getSongsFromYoutubeIds: isAdminRequired(function getSongsFromYoutubeIds(session, youtubeIds, cb) {
 		async.waterfall(
 			[
 				next => {
 					SongsModule.runJob(
 						"GET_SONGS",
 						{
-							songIds,
+							youtubeIds,
 							properties: ["youtubeId", "title", "artists", "thumbnail", "duration", "verified", "_id"]
 						},
 						this

+ 51 - 18
backend/logic/songs.js

@@ -10,6 +10,7 @@ let YouTubeModule;
 let StationsModule;
 let PlaylistsModule;
 let RatingsModule;
+let WSModule;
 
 class _SongsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
@@ -34,6 +35,7 @@ class _SongsModule extends CoreClass {
 		StationsModule = this.moduleManager.modules.stations;
 		PlaylistsModule = this.moduleManager.modules.playlists;
 		RatingsModule = this.moduleManager.modules.ratings;
+		WSModule = this.moduleManager.modules.ws;
 
 		this.SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
 		this.SongSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "song" });
@@ -41,6 +43,15 @@ class _SongsModule extends CoreClass {
 		this.setStage(2);
 
 		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(
 				[
 					next => {
@@ -160,31 +171,44 @@ class _SongsModule extends CoreClass {
 	 * Gets songs by id from Mongo
 	 *
 	 * @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)
 	 */
 	GET_SONGS(payload) {
 		return new Promise((resolve, reject) => {
 			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)
 							.then(() => next(null, song))
 							.catch(next);
+					},
+
+					(song, next) => {
+						CacheModule.runJob("PUB", {
+							channel: "song.created",
+							value: { song }
+						})
+							.then(() => next(null, song))
+							.catch(next);
 					}
 				],
 				(err, song) => {

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

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

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

@@ -15,7 +15,7 @@
 				<slot name="sidebar" />
 			</template>
 			<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>
 				</div>
 				<div v-if="songDeleted" class="notice-container">
@@ -23,14 +23,17 @@
 				</div>
 				<div
 					v-if="
-						songId && !songDataLoaded && !songNotFound && !newSong
+						youtubeId &&
+						!songDataLoaded &&
+						!songNotFound &&
+						!newSong
 					"
 					class="notice-container"
 				>
 					<h4>Song hasn't loaded yet</h4>
 				</div>
 				<div
-					v-if="songId && songNotFound && !newSong"
+					v-if="youtubeId && songNotFound && !newSong"
 					class="notice-container"
 				>
 					<h4>Song was not found</h4>
@@ -634,7 +637,7 @@
 					<button
 						class="button is-primary"
 						@click="toggleFlag()"
-						v-if="songId && !songDeleted"
+						v-if="youtubeId && !songDeleted"
 					>
 						{{ flagged ? "Unflag" : "Flag" }}
 					</button>
@@ -676,6 +679,15 @@
 						default-message="Create Song"
 						@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>
 			</template>
 		</modal>
@@ -826,7 +838,7 @@ export default {
 			tab: state => state.tab,
 			video: state => state.video,
 			song: state => state.song,
-			songId: state => state.songId,
+			youtubeId: state => state.youtubeId,
 			prefillData: state => state.prefillData,
 			originalSong: state => state.originalSong,
 			reports: state => state.reports,
@@ -848,10 +860,10 @@ export default {
 			this.drawCanvas();
 		},
 		/* 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() {
@@ -870,17 +882,15 @@ export default {
 		localStorage.setItem("volume", 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", {
 			keyCode: 101,
@@ -1052,7 +1062,7 @@ export default {
 	},
 	beforeUnmount() {
 		console.log("UNMOUNT");
-		if (!this.newSong) this.unloadSong(this.songId);
+		this.unloadSong(this.youtubeId, this.song._id);
 
 		this.playerReady = false;
 		clearInterval(this.interval);
@@ -1108,7 +1118,7 @@ export default {
 			this.thumbnailLoadError = error !== 0;
 		},
 		init() {
-			if (this.newSong) {
+			if (this.newSong && !this.youtubeId) {
 				this.setSong({
 					youtubeId: "",
 					title: "",
@@ -1122,7 +1132,7 @@ export default {
 				});
 				this.songDataLoaded = true;
 				this.showTab("youtube");
-			} else if (this.songId) this.loadSong(this.songId);
+			} else if (this.youtubeId) this.loadSong(this.youtubeId);
 			else if (!this.bulk) {
 				new Toast("You can't open EditSong without editing a song");
 				return this.closeModal("editSong");
@@ -1346,12 +1356,12 @@ export default {
 
 			return null;
 		},
-		unloadSong(songId) {
+		unloadSong(youtubeId, songId) {
 			this.songDataLoaded = false;
 			this.songDeleted = false;
 			this.stopVideo();
 			this.pauseVideo(true);
-			this.resetSong(songId);
+			this.resetSong(youtubeId);
 			this.thumbnailNotSquare = false;
 			this.thumbnailWidth = null;
 			this.thumbnailHeight = null;
@@ -1361,40 +1371,52 @@ export default {
 			this.socket.dispatch("apis.leaveRoom", `edit-song.${songId}`);
 			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.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) {
 			this.selectDiscogsAlbum(result);
@@ -1404,25 +1426,27 @@ export default {
 		save(songToCopy, closeOrNext, saveButtonRefName, newSong = false) {
 			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];
 
 			if (!this.youtubeError && this.youtubeVideoDuration === "0.000") {
 				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.");
 			}
 
 			if (!song.title) {
 				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");
 			}
 
 			if (!song.thumbnail) {
 				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");
 			}
 
@@ -1455,7 +1479,8 @@ export default {
 				this.originalSong.youtubeId !== song.youtubeId
 			) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 					"You're not allowed to change the YouTube id while the player is not working"
 				);
@@ -1465,11 +1490,12 @@ export default {
 			if (
 				Number(song.skipDuration) + Number(song.duration) >
 					this.youtubeVideoDuration &&
-				((!newSong && !this.youtubeError) ||
+				(((!newSong || this.bulk) && !this.youtubeError) ||
 					this.originalSong.duration !== song.duration)
 			) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 					"Duration can't be higher than the length of the video"
 				);
@@ -1478,7 +1504,8 @@ export default {
 			// Title
 			if (!validation.isLength(song.title, 1, 100)) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 					"Title must have between 1 and 100 characters."
 				);
@@ -1490,7 +1517,8 @@ export default {
 				song.artists.length > 10
 			) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 					"Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
 				);
@@ -1513,25 +1541,27 @@ export default {
 
 			if (error) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(error);
 			}
 
 			// Genres
 			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 (
 				(song.verified && song.genres.length < 1) ||
@@ -1541,7 +1571,8 @@ export default {
 
 			if (error) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(error);
 			}
 
@@ -1561,21 +1592,24 @@ export default {
 
 			if (error) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(error);
 			}
 
 			// Thumbnail
 			if (!validation.isLength(song.thumbnail, 1, 256)) {
 				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song._id);
+				if (!newSong || this.bulk)
+					this.$emit("savedError", song.youtubeId);
 				return new Toast(
 					"Thumbnail must have between 8 and 256 characters."
 				);
 			}
 			if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
 				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://".');
 			}
 
@@ -1585,7 +1619,8 @@ export default {
 				song.thumbnail.indexOf("https://") !== 0
 			) {
 				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://".');
 			}
 
@@ -1597,26 +1632,34 @@ export default {
 
 					if (res.status === "error") {
 						saveButtonRef.handleFailedSave();
+						this.$emit("savedError", song.youtubeId);
 						return;
 					}
 
 					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 => {
 				new Toast(res.message);
 
 				if (res.status === "error") {
 					saveButtonRef.handleFailedSave();
-					this.$emit("savedError", song._id);
+					this.$emit("savedError", song.youtubeId);
 					return;
 				}
 
 				this.updateOriginalSong(song);
 
 				saveButtonRef.handleSuccessfulSave();
-				this.$emit("savedSuccess", song._id);
+				this.$emit("savedSuccess", song.youtubeId);
 
 				if (!closeOrNext) return;
 

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

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

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

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

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

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

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

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

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

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

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

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