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
 	 * Updates a song
 	 *
 	 *

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

@@ -1,7 +1,7 @@
 <template>
 <template>
 	<div>
 	<div>
 		<modal
 		<modal
-			title="Edit Song"
+			:title="`${newSong ? 'Create' : 'Edit'} Song`"
 			class="song-modal"
 			class="song-modal"
 			:size="'wide'"
 			:size="'wide'"
 			:split="true"
 			:split="true"
@@ -15,16 +15,21 @@
 				<slot name="sidebar" />
 				<slot name="sidebar" />
 			</template>
 			</template>
 			<template #body>
 			<template #body>
-				<div v-if="!songId" class="notice-container">
+				<div v-if="!songId && !newSong" class="notice-container">
 					<h4>No song has been selected</h4>
 					<h4>No song has been selected</h4>
 				</div>
 				</div>
 				<div
 				<div
-					v-if="songId && !songDataLoaded && !songNotFound"
+					v-if="
+						songId && !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 v-if="songId && songNotFound" class="notice-container">
+				<div
+					v-if="songId && songNotFound && !newSong"
+					class="notice-container"
+				>
 					<h4>Song was not found</h4>
 					<h4>Song was not found</h4>
 				</div>
 				</div>
 				<div class="left-section" v-show="songDataLoaded">
 				<div class="left-section" v-show="songDataLoaded">
@@ -412,6 +417,7 @@
 								Discogs
 								Discogs
 							</button>
 							</button>
 							<button
 							<button
+								v-if="!newSong"
 								class="button is-default"
 								class="button is-default"
 								:class="{ selected: tab === 'reports' }"
 								:class="{ selected: tab === 'reports' }"
 								ref="reports-tab"
 								ref="reports-tab"
@@ -441,7 +447,11 @@
 							v-show="tab === 'discogs'"
 							v-show="tab === 'discogs'"
 							:bulk="bulk"
 							: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'" />
 						<youtube class="tab" v-show="tab === 'youtube'" />
 						<musare-songs
 						<musare-songs
 							class="tab"
 							class="tab"
@@ -463,7 +473,7 @@
 						{{ flagged ? "Unflag" : "Flag" }}
 						{{ flagged ? "Unflag" : "Flag" }}
 					</button>
 					</button>
 				</div>
 				</div>
-				<div>
+				<div v-if="!newSong">
 					<save-button
 					<save-button
 						ref="saveButton"
 						ref="saveButton"
 						@clicked="save(song, false, false, 'saveButton')"
 						@clicked="save(song, false, false, 'saveButton')"
@@ -498,7 +508,7 @@
 							<i class="material-icons">check_circle</i>
 							<i class="material-icons">check_circle</i>
 						</button>
 						</button>
 						<quick-confirm
 						<quick-confirm
-							v-if="song.verified"
+							v-else
 							placement="left"
 							placement="left"
 							@confirm="unverify(song._id)"
 							@confirm="unverify(song._id)"
 						>
 						>
@@ -527,6 +537,35 @@
 						</button>
 						</button>
 					</div>
 					</div>
 				</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>
 			</template>
 		</modal>
 		</modal>
 		<floating-box id="genreHelper" ref="genreHelper" :column="false">
 		<floating-box id="genreHelper" ref="genreHelper" :column="false">
@@ -668,7 +707,8 @@ export default {
 			songId: state => state.songId,
 			songId: state => state.songId,
 			prefillData: state => state.prefillData,
 			prefillData: state => state.prefillData,
 			originalSong: state => state.originalSong,
 			originalSong: state => state.originalSong,
-			reports: state => state.reports
+			reports: state => state.reports,
+			newSong: state => state.newSong
 		}),
 		}),
 		...mapState("modalVisibility", {
 		...mapState("modalVisibility", {
 			modals: state => state.modals,
 			modals: state => state.modals,
@@ -709,27 +749,29 @@ export default {
 		localStorage.setItem("volume", volume);
 		localStorage.setItem("volume", volume);
 		this.volumeSliderValue = volume * 100;
 		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", {
 		keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
 			keyCode: 101,
 			keyCode: 101,
@@ -898,7 +940,7 @@ export default {
 	},
 	},
 	beforeUnmount() {
 	beforeUnmount() {
 		console.log("UNMOUNT");
 		console.log("UNMOUNT");
-		this.unloadSong(this.songId);
+		if (!this.newSong) this.unloadSong(this.songId);
 
 
 		this.playerReady = false;
 		this.playerReady = false;
 		clearInterval(this.interval);
 		clearInterval(this.interval);
@@ -927,7 +969,20 @@ export default {
 	},
 	},
 	methods: {
 	methods: {
 		init() {
 		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) {
 			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");
@@ -1175,28 +1230,34 @@ export default {
 			this.openModal("importAlbum");
 			this.openModal("importAlbum");
 			this.closeModal("editSong");
 			this.closeModal("editSong");
 		},
 		},
-		save(songToCopy, verify, closeOrNext, saveButtonRefName) {
+		save(
+			songToCopy,
+			verify,
+			closeOrNext,
+			saveButtonRefName,
+			newSong = false
+		) {
 			const song = JSON.parse(JSON.stringify(songToCopy));
 			const song = JSON.parse(JSON.stringify(songToCopy));
 
 
-			this.$emit("saving", song._id);
+			if (!newSong) this.$emit("saving", song._id);
 
 
 			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();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				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();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				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();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast("Please fill in all fields");
 				return new Toast("Please fill in all fields");
 			}
 			}
 
 
@@ -1224,11 +1285,12 @@ export default {
 
 
 			// Youtube Id
 			// Youtube Id
 			if (
 			if (
+				!newSong &&
 				this.youtubeError &&
 				this.youtubeError &&
 				this.originalSong.youtubeId !== song.youtubeId
 				this.originalSong.youtubeId !== song.youtubeId
 			) {
 			) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				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"
 				);
 				);
@@ -1238,11 +1300,11 @@ export default {
 			if (
 			if (
 				Number(song.skipDuration) + Number(song.duration) >
 				Number(song.skipDuration) + Number(song.duration) >
 					this.youtubeVideoDuration &&
 					this.youtubeVideoDuration &&
-				(!this.youtubeError ||
+				((!newSong && !this.youtubeError) ||
 					this.originalSong.duration !== song.duration)
 					this.originalSong.duration !== song.duration)
 			) {
 			) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				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"
 				);
 				);
@@ -1251,7 +1313,7 @@ export default {
 			// Title
 			// Title
 			if (!validation.isLength(song.title, 1, 100)) {
 			if (!validation.isLength(song.title, 1, 100)) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(
 				return new Toast(
 					"Title must have between 1 and 100 characters."
 					"Title must have between 1 and 100 characters."
 				);
 				);
@@ -1260,7 +1322,7 @@ export default {
 			// Artists
 			// Artists
 			if (song.artists.length < 1 || song.artists.length > 10) {
 			if (song.artists.length < 1 || song.artists.length > 10) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				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."
 				);
 				);
@@ -1283,7 +1345,7 @@ export default {
 
 
 			if (error) {
 			if (error) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(error);
 				return new Toast(error);
 			}
 			}
 
 
@@ -1308,7 +1370,7 @@ export default {
 
 
 			if (error) {
 			if (error) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast(error);
 				return new Toast(error);
 			}
 			}
 
 
@@ -1328,21 +1390,21 @@ export default {
 
 
 			if (error) {
 			if (error) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				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();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				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();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast('Thumbnail must start with "https://".');
 				return new Toast('Thumbnail must start with "https://".');
 			}
 			}
 
 
@@ -1352,12 +1414,25 @@ export default {
 				song.thumbnail.indexOf("https://") !== 0
 				song.thumbnail.indexOf("https://") !== 0
 			) {
 			) {
 				saveButtonRef.handleFailedSave();
 				saveButtonRef.handleFailedSave();
-				this.$emit("savedError", song._id);
+				if (!newSong) this.$emit("savedError", song._id);
 				return new Toast('Thumbnail must start with "http://".');
 				return new Toast('Thumbnail must start with "http://".');
 			}
 			}
 
 
 			saveButtonRef.status = "saving";
 			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 => {
 			return this.socket.dispatch(`songs.update`, song._id, song, res => {
 				new Toast(res.message);
 				new Toast(res.message);
 
 
@@ -1635,25 +1710,19 @@ export default {
 			}
 			}
 		},
 		},
 		verify(id, cb) {
 		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) {
 		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) {
 		remove(id) {
 			this.socket.dispatch("songs.remove", id, res => {
 			this.socket.dispatch("songs.remove", id, res => {
@@ -1732,7 +1801,7 @@ export default {
 <style lang="less" scoped>
 <style lang="less" scoped>
 .night-mode {
 .night-mode {
 	.edit-section,
 	.edit-section,
-	.player-footer,
+	.player-section,
 	#tabs-container {
 	#tabs-container {
 		background-color: var(--dark-grey-3) !important;
 		background-color: var(--dark-grey-3) !important;
 		border: 0 !important;
 		border: 0 !important;
@@ -1758,6 +1827,10 @@ export default {
 			}
 			}
 		}
 		}
 	}
 	}
+
+	#durationCanvas {
+		background-color: var(--dark-grey-2) !important;
+	}
 }
 }
 
 
 .modal-card-body {
 .modal-card-body {
@@ -1788,14 +1861,18 @@ export default {
 			width: 530px;
 			width: 530px;
 			display: flex;
 			display: flex;
 			flex-direction: column;
 			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 {
 			.player-error {
+				display: flex;
 				height: 318px;
 				height: 318px;
 				width: 530px;
 				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;
 				align-items: center;
 
 
 				* {
 				* {
@@ -1807,8 +1884,6 @@ export default {
 			}
 			}
 
 
 			.player-footer {
 			.player-footer {
-				border: 1px solid var(--light-grey-3);
-				border-radius: 0px 0px @border-radius @border-radius;
 				display: flex;
 				display: flex;
 				justify-content: space-between;
 				justify-content: space-between;
 				height: 54px;
 				height: 54px;

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

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

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

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