Browse Source

feat: Create songs from scratch

Owen Diffey 3 years ago
parent
commit
63240a2848

+ 40 - 0
backend/logic/actions/songs.js

@@ -441,6 +441,46 @@ export default {
 		);
 	}),
 
+	/**
+	 * Creates a song
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param {object} newSong - the song object
+	 * @param {Function} cb
+	 */
+	create: isAdminRequired(async function create(session, newSong, cb) {
+		async.waterfall(
+			[
+				next => {
+					const song = new SongsModule.SongModel(newSong);
+					song.save({ validateBeforeSave: true }, err => {
+						if (err) return next(err, song);
+						return next(null, song);
+					});
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+					this.log("ERROR", "SONGS_CREATE", `Failed to create song "${JSON.stringify(song)}". "${err}"`);
+
+					return cb({ status: "error", message: err });
+				}
+
+				SongsModule.runJob("UPDATE_SONG", { songId: song._id });
+
+				this.log("SUCCESS", "SONGS_CREATE", `Successfully created song "${song._id}".`);
+
+				return cb({
+					status: "success",
+					message: "Song has been successfully created",
+					data: { song }
+				});
+			}
+		);
+	}),
+
 	/**
 	 * Updates a song
 	 *

+ 145 - 70
frontend/src/components/modals/EditSong/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<div>
 		<modal
-			title="Edit Song"
+			:title="`${newSong ? 'Create' : 'Edit'} Song`"
 			class="song-modal"
 			:size="'wide'"
 			:split="true"
@@ -15,16 +15,21 @@
 				<slot name="sidebar" />
 			</template>
 			<template #body>
-				<div v-if="!songId" class="notice-container">
+				<div v-if="!songId && !newSong" class="notice-container">
 					<h4>No song has been selected</h4>
 				</div>
 				<div
-					v-if="songId && !songDataLoaded && !songNotFound"
+					v-if="
+						songId && !songDataLoaded && !songNotFound && !newSong
+					"
 					class="notice-container"
 				>
 					<h4>Song hasn't loaded yet</h4>
 				</div>
-				<div v-if="songId && songNotFound" class="notice-container">
+				<div
+					v-if="songId && songNotFound && !newSong"
+					class="notice-container"
+				>
 					<h4>Song was not found</h4>
 				</div>
 				<div class="left-section" v-show="songDataLoaded">
@@ -412,6 +417,7 @@
 								Discogs
 							</button>
 							<button
+								v-if="!newSong"
 								class="button is-default"
 								:class="{ selected: tab === 'reports' }"
 								ref="reports-tab"
@@ -441,7 +447,11 @@
 							v-show="tab === 'discogs'"
 							:bulk="bulk"
 						/>
-						<reports class="tab" v-show="tab === 'reports'" />
+						<reports
+							v-if="!newSong"
+							class="tab"
+							v-show="tab === 'reports'"
+						/>
 						<youtube class="tab" v-show="tab === 'youtube'" />
 						<musare-songs
 							class="tab"
@@ -463,7 +473,7 @@
 						{{ flagged ? "Unflag" : "Flag" }}
 					</button>
 				</div>
-				<div>
+				<div v-if="!newSong">
 					<save-button
 						ref="saveButton"
 						@clicked="save(song, false, false, 'saveButton')"
@@ -498,7 +508,7 @@
 							<i class="material-icons">check_circle</i>
 						</button>
 						<quick-confirm
-							v-if="song.verified"
+							v-else
 							placement="left"
 							@confirm="unverify(song._id)"
 						>
@@ -527,6 +537,35 @@
 						</button>
 					</div>
 				</div>
+				<div v-else>
+					<save-button
+						ref="createButton"
+						default-message="Create Song"
+						@clicked="
+							save(song, false, false, 'createButton', true)
+						"
+					/>
+					<div class="right">
+						<button
+							v-if="!song.verified"
+							class="button is-success"
+							@click="verify()"
+							content="Verify Song"
+							v-tippy
+						>
+							<i class="material-icons">check_circle</i>
+						</button>
+						<button
+							v-else
+							class="button is-danger"
+							@click="unverify()"
+							content="Unverify Song"
+							v-tippy
+						>
+							<i class="material-icons">cancel</i>
+						</button>
+					</div>
+				</div>
 			</template>
 		</modal>
 		<floating-box id="genreHelper" ref="genreHelper" :column="false">
@@ -668,7 +707,8 @@ export default {
 			songId: state => state.songId,
 			prefillData: state => state.prefillData,
 			originalSong: state => state.originalSong,
-			reports: state => state.reports
+			reports: state => state.reports,
+			newSong: state => state.newSong
 		}),
 		...mapState("modalVisibility", {
 			modals: state => state.modals,
@@ -709,27 +749,29 @@ export default {
 		localStorage.setItem("volume", volume);
 		this.volumeSliderValue = volume * 100;
 
-		this.socket.on(
-			"event:admin.song.updated",
-			res => {
-				if (res.data.song._id === this.song._id)
-					this.song.verified = res.data.song.verified;
-			},
-			{ modal: "editSong" }
-		);
+		if (!this.newSong) {
+			this.socket.on(
+				"event:admin.song.updated",
+				res => {
+					if (res.data.song._id === this.song._id)
+						this.song.verified = res.data.song.verified;
+				},
+				{ modal: "editSong" }
+			);
 
-		this.socket.on(
-			"event:admin.song.removed",
-			res => {
-				if (res.data.songId === this.song._id) {
-					this.closeModal("editSong");
-					setTimeout(() => {
-						window.focusedElementBefore.focus();
-					}, 500);
-				}
-			},
-			{ modal: "editSong" }
-		);
+			this.socket.on(
+				"event:admin.song.removed",
+				res => {
+					if (res.data.songId === this.song._id) {
+						this.closeModal("editSong");
+						setTimeout(() => {
+							window.focusedElementBefore.focus();
+						}, 500);
+					}
+				},
+				{ modal: "editSong" }
+			);
+		}
 
 		keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
 			keyCode: 101,
@@ -898,7 +940,7 @@ export default {
 	},
 	beforeUnmount() {
 		console.log("UNMOUNT");
-		this.unloadSong(this.songId);
+		if (!this.newSong) this.unloadSong(this.songId);
 
 		this.playerReady = false;
 		clearInterval(this.interval);
@@ -927,7 +969,20 @@ export default {
 	},
 	methods: {
 		init() {
-			if (this.songId) this.loadSong(this.songId);
+			if (this.newSong) {
+				this.setSong({
+					youtubeId: "",
+					title: "",
+					artists: [],
+					genres: [],
+					tags: [],
+					duration: 0,
+					skipDuration: 0,
+					thumbnail: "",
+					verified: false
+				});
+				this.songDataLoaded = true;
+			} else if (this.songId) this.loadSong(this.songId);
 			else if (!this.bulk) {
 				new Toast("You can't open EditSong without editing a song");
 				return this.closeModal("editSong");
@@ -1175,28 +1230,34 @@ export default {
 			this.openModal("importAlbum");
 			this.closeModal("editSong");
 		},
-		save(songToCopy, verify, closeOrNext, saveButtonRefName) {
+		save(
+			songToCopy,
+			verify,
+			closeOrNext,
+			saveButtonRefName,
+			newSong = false
+		) {
 			const song = JSON.parse(JSON.stringify(songToCopy));
 
-			this.$emit("saving", song._id);
+			if (!newSong) this.$emit("saving", song._id);
 
 			const saveButtonRef = this.$refs[saveButtonRefName];
 
 			if (!this.youtubeError && this.youtubeVideoDuration === "0.000") {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast("The video appears to not be working.");
 			}
 
 			if (!song.title) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast("Please fill in all fields");
 			}
 
 			if (!song.thumbnail) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast("Please fill in all fields");
 			}
 
@@ -1224,11 +1285,12 @@ export default {
 
 			// Youtube Id
 			if (
+				!newSong &&
 				this.youtubeError &&
 				this.originalSong.youtubeId !== song.youtubeId
 			) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(
 					"You're not allowed to change the YouTube id while the player is not working"
 				);
@@ -1238,11 +1300,11 @@ export default {
 			if (
 				Number(song.skipDuration) + Number(song.duration) >
 					this.youtubeVideoDuration &&
-				(!this.youtubeError ||
+				((!newSong && !this.youtubeError) ||
 					this.originalSong.duration !== song.duration)
 			) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(
 					"Duration can't be higher than the length of the video"
 				);
@@ -1251,7 +1313,7 @@ export default {
 			// Title
 			if (!validation.isLength(song.title, 1, 100)) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(
 					"Title must have between 1 and 100 characters."
 				);
@@ -1260,7 +1322,7 @@ export default {
 			// Artists
 			if (song.artists.length < 1 || song.artists.length > 10) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(
 					"Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
 				);
@@ -1283,7 +1345,7 @@ export default {
 
 			if (error) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(error);
 			}
 
@@ -1308,7 +1370,7 @@ export default {
 
 			if (error) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(error);
 			}
 
@@ -1328,21 +1390,21 @@ export default {
 
 			if (error) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(error);
 			}
 
 			// Thumbnail
 			if (!validation.isLength(song.thumbnail, 1, 256)) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(
 					"Thumbnail must have between 8 and 256 characters."
 				);
 			}
 			if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast('Thumbnail must start with "https://".');
 			}
 
@@ -1352,12 +1414,25 @@ export default {
 				song.thumbnail.indexOf("https://") !== 0
 			) {
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast('Thumbnail must start with "http://".');
 			}
 
 			saveButtonRef.status = "saving";
 
+			if (newSong)
+				return this.socket.dispatch(`songs.create`, song, res => {
+					new Toast(res.message);
+
+					if (res.status === "error") {
+						saveButtonRef.handleFailedSave();
+						return;
+					}
+
+					saveButtonRef.handleSuccessfulSave();
+
+					this.closeModal("editSong");
+				});
 			return this.socket.dispatch(`songs.update`, song._id, song, res => {
 				new Toast(res.message);
 
@@ -1635,25 +1710,19 @@ export default {
 			}
 		},
 		verify(id, cb) {
-			this.socket.dispatch("songs.verify", id, res => {
-				new Toast(res.message);
-				if (cb) cb(res.status === "success");
-			});
+			if (this.newSong) this.song.verified = true;
+			else
+				this.socket.dispatch("songs.verify", id, res => {
+					new Toast(res.message);
+					if (cb) cb(res.status === "success");
+				});
 		},
 		unverify(id) {
-			this.socket.dispatch("songs.unverify", id, res => {
-				new Toast(res.message);
-			});
-		},
-		hide(id) {
-			this.socket.dispatch("songs.hide", id, res => {
-				new Toast(res.message);
-			});
-		},
-		unhide(id) {
-			this.socket.dispatch("songs.unhide", id, res => {
-				new Toast(res.message);
-			});
+			if (this.newSong) this.song.verified = false;
+			else
+				this.socket.dispatch("songs.unverify", id, res => {
+					new Toast(res.message);
+				});
 		},
 		remove(id) {
 			this.socket.dispatch("songs.remove", id, res => {
@@ -1732,7 +1801,7 @@ export default {
 <style lang="less" scoped>
 .night-mode {
 	.edit-section,
-	.player-footer,
+	.player-section,
 	#tabs-container {
 		background-color: var(--dark-grey-3) !important;
 		border: 0 !important;
@@ -1758,6 +1827,10 @@ export default {
 			}
 		}
 	}
+
+	#durationCanvas {
+		background-color: var(--dark-grey-2) !important;
+	}
 }
 
 .modal-card-body {
@@ -1788,14 +1861,18 @@ export default {
 			width: 530px;
 			display: flex;
 			flex-direction: column;
+			border: 1px solid var(--light-grey-3);
+			border-radius: @border-radius;
+			overflow: hidden;
+
+			#durationCanvas {
+				background-color: var(--light-grey-2);
+			}
 
 			.player-error {
+				display: flex;
 				height: 318px;
 				width: 530px;
-				display: block;
-				border: 1px rgba(163, 224, 255, 0.75) solid;
-				border-radius: @border-radius @border-radius 0px 0px;
-				display: flex;
 				align-items: center;
 
 				* {
@@ -1807,8 +1884,6 @@ export default {
 			}
 
 			.player-footer {
-				border: 1px solid var(--light-grey-3);
-				border-radius: 0px 0px @border-radius @border-radius;
 				display: flex;
 				justify-content: space-between;
 				height: 54px;

+ 7 - 0
frontend/src/pages/Admin/tabs/Songs.vue

@@ -3,6 +3,9 @@
 		<page-metadata title="Admin | Songs" />
 		<div class="admin-tab">
 			<div class="button-row">
+				<button class="button is-primary" @click="create()">
+					Create song
+				</button>
 				<button
 					class="button is-primary"
 					@click="openModal('requestSong')"
@@ -655,6 +658,10 @@ export default {
 		}
 	},
 	methods: {
+		create() {
+			this.editSong({ newSong: true });
+			this.openModal("editSong");
+		},
 		editOne(song) {
 			this.editSong({ songId: song._id });
 			this.openModal("editSong");

+ 5 - 3
frontend/src/store/modules/modals/editSong.js

@@ -10,11 +10,12 @@ export default {
 			autoPlayed: false,
 			currentTime: 0
 		},
-		songId: "",
+		songId: null,
 		song: {},
 		originalSong: {},
 		reports: [],
-		tab: "discogs"
+		tab: "discogs",
+		newSong: false
 	},
 	getters: {},
 	actions: {
@@ -50,7 +51,8 @@ export default {
 			state.tab = tab;
 		},
 		editSong(state, song) {
-			state.songId = song.songId;
+			state.newSong = !!song.newSong;
+			state.songId = song.newSong ? null : song.songId;
 			state.prefillData = song.prefill ? song.prefill : {};
 		},
 		setSong(state, song) {