|
@@ -0,0 +1,357 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import Toast from "toasters";
|
|
|
+import { defineAsyncComponent, ref, computed } from "vue";
|
|
|
+import { useRouter } from "vue-router";
|
|
|
+import { useWebsocketsStore } from "@/stores/websockets";
|
|
|
+import { useModalsStore } from "@/stores/modals";
|
|
|
+import { MusicBrainzArtistTemp } from "@/types/artist";
|
|
|
+
|
|
|
+const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
|
|
|
+// const SaveButton = defineAsyncComponent(
|
|
|
+// () => import("@/components/SaveButton.vue")
|
|
|
+// );
|
|
|
+
|
|
|
+defineProps({
|
|
|
+ modalUuid: { type: String, required: true }
|
|
|
+});
|
|
|
+
|
|
|
+const { socket } = useWebsocketsStore();
|
|
|
+const { closeCurrentModal } = useModalsStore();
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+const musicbrainzArtistSearchQuery = ref("");
|
|
|
+const musicbrainzArtists = ref<MusicBrainzArtistTemp[]>([]);
|
|
|
+const selectedMusicbrainzArtistId = ref("");
|
|
|
+
|
|
|
+type GetMusicbrainzArtistResponse = {
|
|
|
+ data: MusicBrainzArtistTemp;
|
|
|
+};
|
|
|
+
|
|
|
+type CreateArtistResponse = {
|
|
|
+ data: {
|
|
|
+ artistId: string;
|
|
|
+ };
|
|
|
+ message: string;
|
|
|
+ status: "success" | "error";
|
|
|
+};
|
|
|
+
|
|
|
+const createArtist = async () => {
|
|
|
+ const musicbrainzIdentifier = selectedMusicbrainzArtistId.value;
|
|
|
+
|
|
|
+ const musicbrainzArtistRes = (await socket.dispatchAsync(
|
|
|
+ "artists.getMusicbrainzArtist",
|
|
|
+ musicbrainzIdentifier
|
|
|
+ )) as { res: GetMusicbrainzArtistResponse };
|
|
|
+ const { data: musicbrainzData } = musicbrainzArtistRes.res;
|
|
|
+ if (!musicbrainzData) {
|
|
|
+ new Toast("Failed to get MusicBrainz artist data.");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { aliases, id } = musicbrainzData;
|
|
|
+ let { name } = musicbrainzData;
|
|
|
+ aliases.forEach(alias => {
|
|
|
+ if (alias.locale === "en" && alias.primary === true) name = alias.name;
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = {
|
|
|
+ name,
|
|
|
+ musicbrainzIdentifier: id,
|
|
|
+ musicbrainzData
|
|
|
+ };
|
|
|
+
|
|
|
+ const createArtistRes = (await socket.dispatchAsync(
|
|
|
+ "artists.create",
|
|
|
+ data
|
|
|
+ )) as { res: CreateArtistResponse };
|
|
|
+ if (createArtistRes.res.status !== "success") {
|
|
|
+ new Toast("Failed to create artist.");
|
|
|
+ new Toast(createArtistRes.res.message);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { artistId } = createArtistRes.res.data;
|
|
|
+
|
|
|
+ return artistId;
|
|
|
+};
|
|
|
+
|
|
|
+const alternativeNameMap = computed(() => {
|
|
|
+ const map = {};
|
|
|
+ musicbrainzArtists.value.forEach(musicbrainzArtist => {
|
|
|
+ const { aliases, id } = musicbrainzArtist;
|
|
|
+ let name;
|
|
|
+ if (!aliases) return;
|
|
|
+ aliases.forEach(alias => {
|
|
|
+ if (alias.locale === "en" && alias.primary === true)
|
|
|
+ name = alias.name;
|
|
|
+ });
|
|
|
+ if (musicbrainzArtist.name === name) return;
|
|
|
+ if (name) map[id] = name;
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+});
|
|
|
+
|
|
|
+const createArtistAndEdit = async () => {
|
|
|
+ const artistId = await createArtist();
|
|
|
+ if (!artistId) return;
|
|
|
+ closeCurrentModal();
|
|
|
+ router.push({
|
|
|
+ path: "/admin/artists",
|
|
|
+ query: {
|
|
|
+ artistId
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const createArtistAndClose = async () => {
|
|
|
+ const artistId = await createArtist();
|
|
|
+ if (!artistId) return;
|
|
|
+ closeCurrentModal();
|
|
|
+};
|
|
|
+
|
|
|
+const searchMusicbrainzArtists = () => {
|
|
|
+ socket.dispatch(
|
|
|
+ "artists.searchMusicbrainzArtists",
|
|
|
+ musicbrainzArtistSearchQuery.value,
|
|
|
+ res => {
|
|
|
+ // TODO handle error
|
|
|
+ const { data } = res;
|
|
|
+ const { musicbrainzArtists: _musicbrainzArtists } = data;
|
|
|
+ musicbrainzArtists.value = _musicbrainzArtists;
|
|
|
+ }
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const selectMusicbrainzArtist = musicbrainzArtistId => {
|
|
|
+ selectedMusicbrainzArtistId.value = musicbrainzArtistId;
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <modal
|
|
|
+ class="create-artist-modal"
|
|
|
+ title="Create Artist"
|
|
|
+ size="slim"
|
|
|
+ :split="false"
|
|
|
+ >
|
|
|
+ <template #body>
|
|
|
+ <div class="flex flex-column w-full gap-4">
|
|
|
+ <div class="flex flex-column gap-4 w-full">
|
|
|
+ <div class="control is-grouped w-full">
|
|
|
+ <div
|
|
|
+ class="musicbrainz-artist-search-query-container w-full"
|
|
|
+ >
|
|
|
+ <label class="label"
|
|
|
+ >MusicBrainz artist search query</label
|
|
|
+ >
|
|
|
+ <p class="control has-addons">
|
|
|
+ <input
|
|
|
+ class="input"
|
|
|
+ type="text"
|
|
|
+ v-model="musicbrainzArtistSearchQuery"
|
|
|
+ placeholder="Enter artist name..."
|
|
|
+ @keyup.enter="searchMusicbrainzArtists()"
|
|
|
+ />
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ class="button is-primary"
|
|
|
+ @click="searchMusicbrainzArtists()"
|
|
|
+ >
|
|
|
+ Search MusicBrainz artists
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="musicbrainz-artists">
|
|
|
+ <div
|
|
|
+ class="musicbrainz-artist"
|
|
|
+ :class="
|
|
|
+ musicbrainzArtist.id === selectedMusicbrainzArtistId
|
|
|
+ ? 'active-musicbrainz-artist'
|
|
|
+ : ''
|
|
|
+ "
|
|
|
+ v-for="musicbrainzArtist in musicbrainzArtists"
|
|
|
+ :key="musicbrainzArtist.id"
|
|
|
+ @click="selectMusicbrainzArtist(musicbrainzArtist.id)"
|
|
|
+ >
|
|
|
+ <p class="musicbrainz-artist-title">
|
|
|
+ <a
|
|
|
+ :href="`https://musicbrainz.org/artist/${musicbrainzArtist.id}`"
|
|
|
+ target="_blank"
|
|
|
+ >
|
|
|
+ {{ musicbrainzArtist.name }}
|
|
|
+ </a>
|
|
|
+ <span class="musicbrainz-artist-disambiguation">
|
|
|
+ {{ musicbrainzArtist.disambiguation }}
|
|
|
+ </span>
|
|
|
+ <span>
|
|
|
+ {{ musicbrainzArtist.type }}
|
|
|
+ </span>
|
|
|
+ </p>
|
|
|
+ <p
|
|
|
+ v-if="alternativeNameMap[musicbrainzArtist.id]"
|
|
|
+ class="musicbrainz-artist-alternative-title"
|
|
|
+ >
|
|
|
+ <span class="material-icons">list_alt</span>
|
|
|
+ <span>
|
|
|
+ {{ alternativeNameMap[musicbrainzArtist.id] }}
|
|
|
+ </span>
|
|
|
+ </p>
|
|
|
+ <p
|
|
|
+ v-if="musicbrainzArtist.area"
|
|
|
+ class="musicbrainz-artist-area"
|
|
|
+ >
|
|
|
+ <span class="material-icons">public</span>
|
|
|
+ <span>
|
|
|
+ {{ musicbrainzArtist.area.name }}
|
|
|
+ </span>
|
|
|
+ <span v-if="musicbrainzArtist.area.disambigutation">
|
|
|
+ ({{ musicbrainzArtist.area.disambigutation }})
|
|
|
+ </span>
|
|
|
+ <span v-if="musicbrainzArtist.country">
|
|
|
+ ({{ musicbrainzArtist.country }})
|
|
|
+ </span>
|
|
|
+ </p>
|
|
|
+ <p v-else-if="musicbrainzArtist.country">
|
|
|
+ <span class="material-icons">public</span>
|
|
|
+ <span>
|
|
|
+ {{ musicbrainzArtist.country }}
|
|
|
+ </span>
|
|
|
+ </p>
|
|
|
+ <p
|
|
|
+ v-if="
|
|
|
+ musicbrainzArtist['life-span'].begin ||
|
|
|
+ musicbrainzArtist['life-span'].ended
|
|
|
+ "
|
|
|
+ class="musicbrainz-artist-life-span"
|
|
|
+ >
|
|
|
+ <span class="material-icons">schedule</span>
|
|
|
+ <span>{{
|
|
|
+ musicbrainzArtist["life-span"].begin ??
|
|
|
+ "unknown"
|
|
|
+ }}</span
|
|
|
+ >-<span>{{
|
|
|
+ musicbrainzArtist["life-span"].ended ?? "now"
|
|
|
+ }}</span>
|
|
|
+ </p>
|
|
|
+ <p class="musicbrainz-artist-score">
|
|
|
+ <span class="material-icons">percent</span>
|
|
|
+ <span> Score: {{ musicbrainzArtist.score }} </span>
|
|
|
+ </p>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class="musicbrainz-artist-tags"
|
|
|
+ v-if="
|
|
|
+ musicbrainzArtist.tags &&
|
|
|
+ musicbrainzArtist.tags.length
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <span class="material-icons">label</span>
|
|
|
+ <span
|
|
|
+ v-for="tag in musicbrainzArtist.tags"
|
|
|
+ :key="tag.name"
|
|
|
+ class="musicbrainz-artist-tag"
|
|
|
+ >
|
|
|
+ {{ tag.name }} ({{ tag.count }})
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template #footer>
|
|
|
+ <div>
|
|
|
+ <button
|
|
|
+ v-if="selectedMusicbrainzArtistId"
|
|
|
+ class="button is-primary"
|
|
|
+ @click="createArtistAndEdit()"
|
|
|
+ >
|
|
|
+ <span>Create and edit</span>
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ v-if="selectedMusicbrainzArtistId"
|
|
|
+ class="button is-primary"
|
|
|
+ @click="createArtistAndClose()"
|
|
|
+ >
|
|
|
+ <span>Create and close</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </modal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+.flex {
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.flex-column {
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.gap-4 {
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.w-full {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.musicbrainz-artists {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .musicbrainz-artist {
|
|
|
+ padding: 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: var(--dark-grey);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+
|
|
|
+ &.active-musicbrainz-artist {
|
|
|
+ outline: 1px solid var(--white);
|
|
|
+ }
|
|
|
+
|
|
|
+ .musicbrainz-artist-title {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ line-height: 20px;
|
|
|
+
|
|
|
+ a {
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .musicbrainz-artist-disambiguation {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .musicbrainz-artist-alternative-title,
|
|
|
+ .musicbrainz-artist-area,
|
|
|
+ .musicbrainz-artist-life-span,
|
|
|
+ .musicbrainz-artist-score {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ gap: 4px;
|
|
|
+ line-height: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .musicbrainz-artist-tags {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 4px;
|
|
|
+ line-height: 24px;
|
|
|
+
|
|
|
+ .musicbrainz-artist-tag {
|
|
|
+ background-color: var(--dark-grey-2);
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 16px;
|
|
|
+ line-height: 16px;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|