1
0

3 Revīzijas d2b1916851 ... 913535c1d3

Autors SHA1 Ziņojums Datums
  Kristian Vos 913535c1d3 fix: updating email wouldn't work 1 mēnesi atpakaļ
  Kristian Vos 7cc9209ebc refactor: WIP changes to import artist page from a while ago 1 mēnesi atpakaļ
  Kristian Vos e96c5e845c refactor: WIP changes to EditAlbum modal from a while ago 1 mēnesi atpakaļ

+ 2 - 2
backend/logic/db/schemas/album.js

@@ -18,8 +18,8 @@ export default {
 	// 		comment: { type: String },
 	// 	}
 	// ],
-	// musicBrainzIdentifier: { type: String, required: true },
-	// musicBrainzData: { type: Object, required: true },
+	musicBrainzIdentifier: { type: String, required: true },
+	musicBrainzData: { type: Object, required: true },
 	comment: { type: String },
 	createdBy: { type: String, required: true },
 	createdAt: { type: Number, default: Date.now, required: true },

+ 1 - 1
backend/logic/users.js

@@ -520,7 +520,7 @@ class _UsersModule extends CoreClass {
 		if (!user) throw new Error("User not found.");
 		if (user.email.address === email) throw new Error("New email can't be the same as your the old email.");
 
-		const existingUser = UsersModule.userModel.findOne({ "email.address": email });
+		const existingUser = await UsersModule.userModel.findOne({ "email.address": email });
 		if (existingUser) throw new Error("That email is already in use.");
 
 		const gravatarUrl = await UtilsModule.runJob("CREATE_GRAVATAR", { email }, this);

+ 1 - 0
frontend/src/components/ModalManager.vue

@@ -15,6 +15,7 @@ const modalComponents = shallowRef(
 		createStation: "CreateStation.vue",
 		editNews: "EditNews.vue",
 		editArtist: "EditArtist.vue",
+		editAlbum: "EditAlbum.vue",
 		manageStation: "ManageStation/index.vue",
 		importArtistMB: "ImportArtistMB.vue",
 		editPlaylist: "EditPlaylist/index.vue",

+ 116 - 51
frontend/src/components/modals/EditAlbum.vue

@@ -2,6 +2,7 @@
 import Toast from "toasters";
 import { defineAsyncComponent, ref, onMounted } from "vue";
 import { GenericResponse } from "@musare_types/actions/GenericActions";
+import VueJsonPretty from "vue-json-pretty";
 import { useForm } from "@/composables/useForm";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
@@ -13,8 +14,8 @@ const SaveButton = defineAsyncComponent(
 
 const props = defineProps({
 	modalUuid: { type: String, required: true },
-	createArtist: { type: Boolean, default: false },
-	artistId: { type: String, default: null },
+	createAlbum: { type: Boolean, default: false },
+	albumId: { type: String, default: null },
 	sector: { type: String, default: "admin" }
 });
 
@@ -24,6 +25,7 @@ const { closeCurrentModal } = useModalsStore();
 
 const createdBy = ref();
 const createdAt = ref(0);
+const hideMusicbrainzData = ref(true);
 
 const { inputs, save, setOriginalValue } = useForm(
 	{
@@ -32,21 +34,25 @@ const { inputs, save, setOriginalValue } = useForm(
 		},
 		musicbrainzIdentifier: {
 			value: ""
+		},
+		musicbrainzData: {
+			value: {}
 		}
 	},
 	({ status, messages, values }, resolve, reject) => {
 		if (status === "success") {
 			const data = {
 				name: values.name,
-				musicbrainzIdentifier: values.musicbrainzIdentifier
+				musicbrainzIdentifier: values.musicbrainzIdentifier,
+				musicbrainzData: values.musicbrainzData
 			};
 			const cb = (res: GenericResponse) => {
 				new Toast(res.message);
 				if (res.status === "success") resolve();
 				else reject(new Error(res.message));
 			};
-			if (props.createArtist) socket.dispatch("artists.create", data, cb);
-			else socket.dispatch("artists.update", props.artistId, data, cb);
+			if (props.createAlbum) socket.dispatch("albums.create", data, cb);
+			else socket.dispatch("albums.update", props.albumId, data, cb);
 		} else {
 			if (status === "unchanged") new Toast(messages.unchanged);
 			else if (status === "error")
@@ -62,20 +68,23 @@ const { inputs, save, setOriginalValue } = useForm(
 );
 
 onMounted(() => {
+	console.log(111);
 	socket.onConnect(() => {
-		if (props.artistId && !props.createArtist) {
-			socket.dispatch(`artists.getArtistFromId`, props.artistId, res => {
-				// res: GetArtistResponse
+		console.log(222);
+		if (props.albumId && !props.createAlbum) {
+			socket.dispatch(`albums.getAlbumFromId`, props.albumId, res => {
+				// res: GetAlbumResponse
 				if (res.status === "success") {
 					setOriginalValue({
-						name: res.data.artist.name,
+						name: res.data.album.name,
 						musicbrainzIdentifier:
-							res.data.artist.musicbrainzIdentifier
+							res.data.album.musicbrainzIdentifier,
+						musicbrainzData: res.data.album.musicbrainzData ?? {}
 					});
-					createdBy.value = res.data.artist.createdBy;
-					createdAt.value = res.data.artist.createdAt;
+					createdBy.value = res.data.album.createdBy;
+					createdAt.value = res.data.album.createdAt;
 				} else {
-					new Toast("Artist with that ID not found.");
+					new Toast("Album with that ID not found.");
 					closeCurrentModal();
 				}
 			});
@@ -83,66 +92,122 @@ onMounted(() => {
 	});
 });
 
-const saveArtist = (close?: boolean) => {
+const saveAlbum = (close?: boolean) => {
 	save(() => {
 		if (close) {
 			closeCurrentModal();
 		}
 	});
 };
+
+const getMusicbrainzAlbumData = musicbrainzIdentifier => {
+	socket.dispatch(
+		"albums.getMusicbrainzAlbum",
+		musicbrainzIdentifier,
+		res => {
+			new Toast("Successfully got data");
+			inputs.value.musicbrainzData.value = res.data;
+		}
+	);
+};
 </script>
 
 <template>
 	<modal
-		class="edit-artist-modal"
-		:title="createArtist ? 'Create Artist' : 'Edit Artist'"
+		class="edit-album-modal"
+		:title="createAlbum ? 'Create Album' : 'Edit Album'"
 		:size="'wide'"
 		:split="true"
 	>
 		<template #body>
-			<div class="control is-grouped">
-				<div class="name-container">
-					<label class="label">Name</label>
-					<p class="control has-addons">
-						<input
-							class="input"
-							type="text"
-							:ref="el => (inputs['name'].ref = el)"
-							v-model="inputs['name'].value"
-							placeholder="Enter artist name..."
-						/>
-					</p>
+			<div class="flex flex-row w-full">
+				<div class="flex flex-column gap-4 w-2/3">
+					<div>
+						<div class="control is-grouped">
+							<div class="name-container">
+								<label class="label">Name</label>
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										:ref="el => (inputs['name'].ref = el)"
+										v-model="inputs['name'].value"
+										placeholder="Enter album name..."
+									/>
+								</p>
+							</div>
+						</div>
+					</div>
+					<div>
+						<div class="control is-grouped gap-4">
+							<div class="musicbrainz-identifier-container">
+								<label class="label"
+									>MusicBrainz identifier</label
+								>
+								<input
+									class="input"
+									type="text"
+									:ref="
+										el =>
+											(inputs[
+												'musicbrainzIdentifier'
+											].ref = el)
+									"
+									v-model="
+										inputs['musicbrainzIdentifier'].value
+									"
+									placeholder="Enter MusicBrainz identifier..."
+								/>
+							</div>
+							<button
+								class="button is-primary button-bottom"
+								@click="
+									getMusicbrainzAlbumData(
+										inputs['musicbrainzIdentifier'].value
+									)
+								"
+							>
+								Get MusicBrainz album data
+							</button>
+						</div>
+						<div>
+							<div class="flex flex-row gap-4">
+								<p class="text-vcenter">MusicBrainz data</p>
+								<button
+									class="button is-primary"
+									@click="
+										hideMusicbrainzData =
+											!hideMusicbrainzData
+									"
+								>
+									<span v-show="hideMusicbrainzData"
+										>Show MusicBrainz data</span
+									>
+									<span v-show="!hideMusicbrainzData"
+										>Hide MusicBrainz data</span
+									>
+								</button>
+							</div>
+							<vue-json-pretty
+								:data="inputs['musicbrainzData'].value"
+								:show-length="true"
+								v-if="!hideMusicbrainzData"
+							></vue-json-pretty>
+						</div>
+					</div>
 				</div>
-			</div>
-			<div class="control is-grouped">
-				<div class="musicbrainz-identifier-container">
-					<label class="label">MusicBrainz identifier</label>
-					<p class="control has-addons">
-						<input
-							class="input"
-							type="text"
-							:ref="
-								el => (inputs['musicbrainzIdentifier'].ref = el)
-							"
-							v-model="inputs['musicbrainzIdentifier'].value"
-							placeholder="Enter MusicBrainz identifier..."
-						/>
-					</p>
-				</div>
-			</div>
-			<div>
-				<p>MusicBrainz data</p>
+				<div class="flex flex-column w-1/3"></div>
 			</div>
 		</template>
 		<template #footer>
 			<div>
 				<save-button
-					:default-message="`${createArtist ? 'Create' : 'Update'} Artist`"
-					@clicked="saveArtist()"
+					:default-message="`${createAlbum ? 'Create' : 'Update'} Album`"
+					@clicked="saveAlbum()"
 				/>
 				<save-button
-					:default-message="`${createArtist ? 'Create' : 'Update'} and close`"
-					@clicked="saveArtist(true)"
+					:default-message="`${createAlbum ? 'Create' : 'Update'} and close`"
+					@clicked="saveAlbum(true)"
 				/>
 			</div>
 		</template>

+ 6 - 0
frontend/src/main.ts

@@ -155,6 +155,12 @@ const router = createRouter({
 					component: () => import("@/pages/Admin/Songs/Import.vue"),
 					meta: { permissionRequired: "admin.view.import" }
 				},
+				{
+					path: "songs/import-artist",
+					component: () =>
+						import("@/pages/Admin/Songs/ImportArtist.vue"),
+					meta: { permissionRequired: "admin.view.import" }
+				},
 				{
 					path: "artists",
 					component: () => import("@/pages/Admin/Artists.vue"),

+ 237 - 0
frontend/src/pages/Admin/Songs/ImportArtist.vue

@@ -0,0 +1,237 @@
+<script setup lang="ts">
+import { defineAsyncComponent, ref } from "vue";
+import { useRouter } from "vue-router";
+import Toast from "toasters";
+import { useWebsocketsStore } from "@/stores/websockets";
+import { useLongJobsStore } from "@/stores/longJobs";
+import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
+import utils from "@/utils";
+
+const InfoIcon = defineAsyncComponent(
+	() => import("@/components/InfoIcon.vue")
+);
+
+const router = useRouter();
+
+const { socket } = useWebsocketsStore();
+
+const { openModal } = useModalsStore();
+
+const { setJob } = useLongJobsStore();
+
+const { hasPermission } = useUserAuthStore();
+</script>
+
+<template>
+	<div>
+		<page-metadata title="Admin | Songs | Import" />
+		<div class="admin-tab import-tab">
+			<div class="card">
+				<h1>Import Artist</h1>
+				<p>Import artist</p>
+			</div>
+
+			<div class="section-row">
+				<div class="card left-section">
+					<h4>Start New Import</h4>
+					<hr class="section-horizontal-rule" />
+
+					<!-- <div v-if="false && createImport.stage === 1" class="stage">
+						<label class="label">Import Method</label>
+						<div class="control is-expanded select">
+							<select v-model="createImport.importMethod">
+								<option value="youtube">YouTube</option>
+							</select>
+						</div>
+
+						<div class="control is-expanded">
+							<button
+								class="button is-primary"
+								@click.prevent="submitCreateImport(1)"
+							>
+								<i class="material-icons">navigate_next</i>
+								Next
+							</button>
+						</div>
+					</div>
+
+					<div
+						v-else-if="
+							createImport.stage === 2 &&
+							createImport.importMethod === 'youtube'
+						"
+						class="stage"
+					>
+						<label class="label"
+							>YouTube URL
+							<info-icon
+								tooltip="YouTube playlist or channel URLs may be provided"
+							/>
+						</label>
+						<div class="control is-expanded">
+							<input
+								class="input"
+								type="text"
+								placeholder="YouTube Playlist or Channel URL"
+								v-model="createImport.youtubeUrl"
+							/>
+						</div>
+						<div class="control is-expanded">
+							<input
+								class="input"
+								type="number"
+								placeholder="Max"
+								v-model="createImport.max"
+							/>
+						</div>
+						<div class="control is-expanded checkbox-control">
+							<label class="switch">
+								<input
+									type="checkbox"
+									id="import-music-only"
+									v-model="createImport.isImportingOnlyMusic"
+								/>
+								<span class="slider round"></span>
+							</label>
+
+							<label class="label" for="import-music-only">
+								Import Music Only
+								<info-icon
+									tooltip="Only import videos from YouTube identified as music"
+									@click.prevent
+								/>
+							</label>
+						</div>
+						<!!-- <div class="control is-expanded">
+							<input
+								class="input"
+								type="text"
+								placeholder="Comment"
+								v-model="createImport.comment"
+							/>
+						</div> --!>
+
+						<div class="control is-expanded">
+							<button
+								class="control is-expanded button is-primary"
+								@click.prevent="submitCreateImport(2)"
+							>
+								<i class="material-icons icon-with-button"
+									>publish</i
+								>
+								Import
+							</button>
+						</div>
+					</div>
+
+					<div v-if="createImport.stage === 3" class="stage">
+						<p class="has-text-centered import-started">
+							Import Started
+						</p>
+
+						<div class="control is-expanded">
+							<button
+								class="button is-info"
+								@click.prevent="submitCreateImport(3)"
+							>
+								<i class="material-icons icon-with-button"
+									>restart_alt</i
+								>
+								Start Again
+							</button>
+						</div>
+					</div> -->
+				</div>
+				<div class="card right-section">
+					<h4>Manage Imports</h4>
+					<hr class="section-horizontal-rule" />
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style lang="less" scoped>
+.admin-tab.import-tab {
+	.section-row {
+		display: flex;
+		flex-wrap: wrap;
+		height: 100%;
+
+		.card {
+			max-height: 100%;
+			overflow-y: auto;
+			flex-grow: 1;
+
+			.control.is-expanded {
+				.button {
+					width: 100%;
+				}
+
+				&:not(:last-of-type) {
+					margin-bottom: 10px !important;
+				}
+
+				&:last-of-type {
+					margin-bottom: 0 !important;
+				}
+			}
+
+			.control.is-grouped > .button {
+				&:not(:last-child) {
+					border-radius: @border-radius 0 0 @border-radius;
+				}
+
+				&:last-child {
+					border-radius: 0 @border-radius @border-radius 0;
+				}
+			}
+		}
+
+		.left-section {
+			height: 100%;
+			max-width: 400px;
+			margin-right: 20px !important;
+
+			.checkbox-control label.label {
+				margin-left: 10px;
+			}
+
+			.import-started {
+				font-size: 18px;
+				font-weight: 600;
+				margin-bottom: 10px;
+			}
+		}
+
+		.right-section {
+			max-width: calc(100% - 400px);
+
+			.row-options .material-icons.import-album-icon {
+				background-color: var(--purple);
+				color: var(--white);
+				border-color: var(--purple);
+				font-size: 20px;
+			}
+		}
+
+		@media screen and (max-width: 1200px) {
+			.card {
+				flex-basis: 100%;
+				max-height: unset;
+
+				&.left-section {
+					max-width: unset;
+					margin-right: 0 !important;
+					margin-bottom: 10px !important;
+				}
+
+				&.right-section {
+					max-width: unset;
+				}
+			}
+		}
+	}
+}
+</style>

+ 9 - 0
frontend/src/pages/Admin/index.vue

@@ -225,6 +225,15 @@ onBeforeUnmount(() => {
 									>
 										Import
 									</router-link>
+									<router-link
+										v-if="
+											hasPermission('admin.view.import')
+										"
+										class="sidebar-item-child"
+										to="/admin/songs/import-artist"
+									>
+										Import Artist
+									</router-link>
 								</div>
 							</div>
 							<router-link