5 Commits 6cf362a901 ... 97c463e02b

Tác giả SHA1 Thông báo Ngày
  Kristian Vos 97c463e02b feat: basic modal for displaying musicbrainz data for testing 1 tháng trước cách đây
  Kristian Vos cfa934eaeb feat: testing/example implementation for the entity filter group view 1 tháng trước cách đây
  Kristian Vos 712721dc36 feat: new WIP component/store for a generic filter/group component to be used for videos, recordings and more 1 tháng trước cách đây
  Kristian Vos 6cf362a901 feat: testing/example implementation for the entity filter group view 1 tháng trước cách đây
  Kristian Vos cf50a603f6 refactor: WIP changes to ImportArtist2 videos/recording views, showing the items and some grouping logic based on normalized title 1 tháng trước cách đây

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

@@ -35,7 +35,8 @@ const modalComponents = shallowRef(
 		viewMedia: "ViewMedia.vue",
 		bulkEditPlaylist: "BulkEditPlaylist.vue",
 		convertSpotifySongs: "ConvertSpotifySongs.vue",
-		replaceSpotifySongs: "ReplaceSpotifySongs.vue"
+		replaceSpotifySongs: "ReplaceSpotifySongs.vue",
+		viewMBArtist: "ViewMBArtist.vue",
 	})
 );
 </script>

+ 736 - 0
frontend/src/components/modals/ViewMBArtist.vue

@@ -0,0 +1,736 @@
+<script setup lang="ts">
+import { defineAsyncComponent, ref, reactive, onMounted } from "vue";
+import { useWebsocketsStore } from "@/stores/websockets";
+import { ArtistTemp, ReleaseTemp } from "@/types/artist";
+import { useViewMBArtistStore } from "@/stores/viewMBArtist";
+import { useConfigStore } from "@/stores/config";
+import { storeToRefs } from "pinia";
+import VueJsonPretty from "vue-json-pretty";
+import { useModalsStore } from "@/stores/modals";
+import utils from "@/utils";
+
+const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
+
+const props = defineProps({
+	modalUuid: { type: String, required: true },
+	artistId: { type: String, required: true }
+});
+
+const configStore = useConfigStore();
+const { socket } = useWebsocketsStore();
+
+const viewMBArtistStore = useViewMBArtistStore({
+	modalUuid: props.modalUuid
+})();
+const {
+	artist,
+	//
+	releaseIds,
+	releaseGroupIds,
+	recordingIds,
+	workIds,
+	urlIds,
+	//
+	releaseMap,
+	releaseGroupMap,
+	recordingMap,
+	workMap,
+	urlMap,
+	//
+	releaseIdsForRecordingIdMap,
+	releaseGroupIdsForRecordingIdMap,
+	workIdsForRecordingIdMap,
+	// urlIdsForRecordingIdMap,
+	//
+	recordingsLimit,
+	filteredRecordingIds,
+	//
+	releaseGroupTypeMapForRecordingMap,
+	releaseIdsPerReleaseGroupMap,
+	//
+	urlSourceMap
+} = storeToRefs(viewMBArtistStore);
+
+const { setArtist, setMusicBrainzRecordingsReleasesReleaseGroups } =
+	viewMBArtistStore;
+
+const { openModal, closeCurrentModal } = useModalsStore();
+
+const showURLsForRecordingId = ref({});
+
+const showUrlsForRecording = recordingId => {
+	showURLsForRecordingId.value[recordingId] = true;
+};
+
+onMounted(() => {
+	socket.onConnect(() => {
+		socket.dispatch(`artists.getArtistFromId`, props.artistId, res => {
+			// res: GetArtistResponse
+			if (res.status === "success") {
+				setArtist(res.data.artist);
+
+				socket.dispatch(
+					`albums.getMusicBrainzRecordingsReleasesReleaseGroups`,
+					artist.value.musicbrainzIdentifier,
+					res => {
+						const { data } = res;
+
+						setMusicBrainzRecordingsReleasesReleaseGroups(
+							data.artistReleases,
+							data.trackArtistReleases,
+							data.recordings
+						);
+					}
+				);
+			}
+		});
+	});
+});
+
+const youtubeVideoUrlRegex =
+	/^(?:https?:\/\/)?(?:www\.)?(m\.)?(?:music\.)?(?:youtube\.com|youtu\.be)\/(?:watch\/?\?v=)?(?:.*&v=)?(?<youtubeId>[\w-]{11}).*$/;
+const youtubeVideoIdRegex = /^(?<youtubeId>[\w-]{11})$/;
+
+const openMediaModal = url => {
+	const getYoutubeVideoId = () => {
+		// Check if the user simply used a YouTube ID in the input directly
+		const youtubeVideoIdMatch = youtubeVideoIdRegex.exec(url);
+		if (youtubeVideoIdMatch && youtubeVideoIdMatch.groups.youtubeId) {
+			// eslint-disable-next-line prefer-destructuring
+			return youtubeVideoIdMatch.groups.youtubeId;
+		}
+
+		// Check if we can get the video ID from passing in the input value into the URL regex
+		const youtubeVideoUrlMatch = youtubeVideoUrlRegex.exec(url);
+		if (youtubeVideoUrlMatch && youtubeVideoUrlMatch.groups.youtubeId) {
+			// eslint-disable-next-line prefer-destructuring
+			return youtubeVideoUrlMatch.groups.youtubeId;
+		}
+
+		// Check if the user provided a URL of some kind that has the query parameter v, which also passes the YouTube video ID regex
+		try {
+			const { searchParams } = new URL(url);
+			if (searchParams.has("v")) {
+				const vValue = searchParams.get("v");
+				const vValueMatch = youtubeVideoIdRegex.exec(vValue);
+				if (vValueMatch && vValueMatch.groups.youtubeId) {
+					// eslint-disable-next-line prefer-destructuring
+					return youtubeVideoIdMatch.groups.youtubeId;
+				}
+			}
+		} catch {
+			return null;
+		}
+
+		return null;
+	};
+
+	const videoId = getYoutubeVideoId();
+	if (!videoId) return;
+
+	openModal({
+		modal: "viewMedia",
+		props: { mediaSource: `youtube:${videoId}` }
+	});
+};
+</script>
+
+<template>
+	<modal
+		class="view-mb-artist-modal"
+		title="View MB Artist"
+		:size="'wide'"
+		:split="true"
+	>
+		<template #body v-if="artist && releaseIds && releaseIds.length">
+			<div class="columns">
+				<div class="left-column">
+					<div class="box">
+						<p class="box-title">Artist info</p>
+					</div>
+					<div class="box">
+						<p class="box-title">Stats</p>
+						<ul>
+							<li>Releases: {{ releaseIds.length }}</li>
+							<li>
+								Release groups: {{ releaseGroupIds.length }}
+							</li>
+							<li>Recordings: {{ recordingIds.length }}</li>
+							<li>Works: {{ workIds.length }}</li>
+							<li>URLs: {{ urlIds.length }}</li>
+						</ul>
+					</div>
+					<div class="box">
+						<p class="box-title">Options</p>
+						<span>
+							Recording limit:
+							<input
+								type="number"
+								name="recordingLimit"
+								v-model="recordingsLimit"
+							/>
+						</span>
+					</div>
+				</div>
+				<div class="right-column">
+					<div class="box">
+						<p class="box-title">Recordings</p>
+						<div class="recordings">
+							<div
+								class="recording"
+								v-for="recordingId in filteredRecordingIds"
+								:key="recordingId"
+							>
+								<p class="recording-title">
+									<a
+										:href="`https://musicbrainz.org/recording/${recordingId}`"
+										target="_blank"
+									>
+										{{ recordingMap[recordingId].title }}
+									</a>
+									<span class="recording-title-right">
+										<span>
+											{{
+												utils.formatTime(
+													recordingMap[recordingId]
+														.length / 1000 || 0
+												)
+											}}
+										</span>
+										<span>
+											{{
+												recordingMap[recordingId][
+													"first-release-date"
+												]
+											}}
+										</span>
+									</span>
+								</p>
+								<div>
+									<p>Release groups</p>
+									<hr />
+									<div class="release-group-types">
+										<div
+											v-if="
+												releaseGroupTypeMapForRecordingMap[
+													recordingId
+												].typeKeys.length === 0
+											"
+										>
+											None
+										</div>
+										<div
+											class="release-group-type"
+											v-for="typeKey in releaseGroupTypeMapForRecordingMap[
+												recordingId
+											].typeKeys"
+											:key="typeKey"
+										>
+											<p class="release-group-type-title">
+												<span style="opacity: 0.75">
+													{{
+														releaseGroupTypeMapForRecordingMap[
+															recordingId
+														].typeNameMap[typeKey]
+															.primaryType
+													}}
+												</span>
+												<span
+													style="opacity: 0.75"
+													v-for="secondaryType in releaseGroupTypeMapForRecordingMap[
+														recordingId
+													].typeNameMap[typeKey]
+														.secondaryTypes"
+													:key="secondaryType"
+													>+ {{ secondaryType }}</span
+												>
+											</p>
+											<div class="release-groups">
+												<div
+													class="release-group"
+													v-for="releaseGroupId in releaseGroupTypeMapForRecordingMap[
+														recordingId
+													].typeMap[typeKey]"
+													:key="releaseGroupId"
+												>
+													<p
+														class="release-group-line"
+													>
+														<img
+															height="20"
+															width="20"
+															loading="lazy"
+															:src="`${configStore.urls.api}/caa/release-group/${releaseGroupId}/front-250`"
+															alt="Cover art"
+															onerror="this.src='/assets/notes.png'"
+														/>
+														<a
+															:href="`https://musicbrainz.org/release-group/${releaseGroupId}`"
+															target="_blank"
+														>
+															<span>
+																{{
+																	releaseGroupMap[
+																		releaseGroupId
+																	].title
+																}}
+															</span>
+															<span
+																class="disambiguation"
+																v-if="
+																	releaseGroupMap[
+																		releaseGroupId
+																	]
+																		.disambiguation
+																"
+															>
+																({{
+																	releaseGroupMap[
+																		releaseGroupId
+																	]
+																		.disambiguation
+																}})
+															</span>
+														</a>
+														<span
+															style="float: right"
+														>
+															<tippy
+																theme="info"
+																v-if="false"
+															>
+																<i
+																	class="material-icons"
+																	>info</i
+																>
+																<template
+																	#content
+																>
+																	<vue-json-pretty
+																		:data="
+																			releaseGroupMap[
+																				releaseGroupId
+																			]
+																		"
+																	/>
+																</template>
+															</tippy>
+															{{
+																releaseGroupMap[
+																	releaseGroupId
+																][
+																	"first-release-date"
+																]
+															}}
+														</span>
+													</p>
+													<div class="releases">
+														<div
+															class="release"
+															v-for="releaseId in releaseIdsPerReleaseGroupMap[
+																releaseGroupId
+															]"
+															:key="releaseId"
+														>
+															<p
+																class="release-line"
+															>
+																<img
+																	height="20"
+																	width="20"
+																	loading="lazy"
+																	:src="`${configStore.urls.api}/caa/release/${releaseId}/front-250`"
+																	alt="Cover art"
+																	onerror="this.src='/assets/notes.png'"
+																/>
+																<span
+																	:title="
+																		releaseMap[
+																			releaseId
+																		]
+																			.country ??
+																		'None'
+																	"
+																>
+																	{{
+																		utils.getEmojiFlagForCountryCode(
+																			releaseMap[
+																				releaseId
+																			]
+																				.country ??
+																				"XX"
+																		)
+																	}}
+																</span>
+																<a
+																	:href="`https://musicbrainz.org/release/${releaseId}`"
+																	target="_blank"
+																>
+																	<span>
+																		{{
+																			releaseMap[
+																				releaseId
+																			]
+																				.title
+																		}}
+																	</span>
+																	<span
+																		class="disambiguation"
+																		v-if="
+																			releaseMap[
+																				releaseId
+																			]
+																				.disambiguation
+																		"
+																	>
+																		({{
+																			releaseMap[
+																				releaseId
+																			]
+																				.disambiguation
+																		}})
+																	</span>
+																</a>
+																<span>
+																	{{
+																		releaseMap[
+																			releaseId
+																		]
+																			.quality
+																	}}
+																</span>
+																<span>
+																	{{
+																		releaseMap[
+																			releaseId
+																		].status
+																	}}
+																</span>
+																<span>
+																	{{
+																		releaseMap[
+																			releaseId
+																		].date
+																	}}
+																</span>
+															</p>
+														</div>
+													</div>
+												</div>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div>
+									<p>Works</p>
+									<hr />
+									<div class="works">
+										<div
+											v-if="
+												workIdsForRecordingIdMap[
+													recordingId
+												].length === 0
+											"
+											class="work"
+										>
+											None
+										</div>
+										<div
+											class="work"
+											v-for="workId in workIdsForRecordingIdMap[
+												recordingId
+											]"
+											:key="workId"
+										>
+											<p class="work-title">
+												<a
+													:href="`https://musicbrainz.org/work/${workId}`"
+													target="_blank"
+												>
+													<span>
+														{{
+															workMap[workId]
+																.title
+														}}
+													</span>
+													<span
+														class="disambiguation"
+														v-if="
+															workMap[workId]
+																.disambiguation
+														"
+													>
+														({{
+															workMap[workId]
+																.disambiguation
+														}})
+													</span>
+												</a>
+											</p>
+										</div>
+									</div>
+								</div>
+								<div>
+									<p>
+										URLs
+										<span
+											v-if="
+												urlSourceMap[recordingId].length
+											"
+											>({{
+												urlSourceMap[recordingId]
+													.length
+											}})</span
+										>
+									</p>
+									<hr />
+									<div class="urls">
+										<div
+											v-if="
+												urlSourceMap[recordingId]
+													.length === 0
+											"
+											class="url"
+										>
+											None
+										</div>
+										<div
+											v-if="
+												urlSourceMap[recordingId]
+													.length > 0 &&
+												!showURLsForRecordingId[
+													recordingId
+												]
+											"
+										>
+											<button
+												class="button is-primary"
+												@click="
+													showUrlsForRecording(
+														recordingId
+													)
+												"
+											>
+												Show URLs
+											</button>
+										</div>
+										<div
+											class="url"
+											v-show="
+												showURLsForRecordingId[
+													recordingId
+												]
+											"
+											v-for="urlSource in urlSourceMap[
+												recordingId
+											]"
+											:key="`${urlSource.type}-${urlSource.id}-${urlSource.url.id}`"
+										>
+											<p class="url-title">
+												<span
+													@click.prevent="
+														openMediaModal(
+															urlSource.url
+																.resource
+														)
+													"
+												>
+													{{ urlSource.type }} -
+													{{ urlSource.id }} -
+												</span>
+												<a
+													:href="
+														urlSource.url.resource
+													"
+													target="_blank"
+												>
+													{{ urlSource.url.resource }}
+												</a>
+											</p>
+										</div>
+									</div>
+								</div>
+							</div>
+						</div>
+					</div>
+					<div class="box">
+						<p class="box-title">Works</p>
+						<div class="works">
+							<div
+								class="work"
+								v-for="workId in workIds"
+								:key="workId"
+							>
+								<p class="work-title">
+									<a
+										:href="`https://musicbrainz.org/work/${workId}`"
+										target="_blank"
+									>
+										{{ workMap[workId].title }}
+										<span
+											v-if="
+												workMap[workId].disambiguation
+											"
+											>({{
+												workMap[workId].disambiguation
+											}})</span
+										>
+									</a>
+									<span class="work-title-right">
+										<span
+											v-if="
+												workMap[workId].languages
+													.length <= 1
+											"
+										>
+											{{ workMap[workId].language }}
+										</span>
+										<span v-else>
+											{{
+												workMap[workId].languages.join(
+													" - "
+												)
+											}}
+										</span>
+										<span>
+											{{
+												workMap[workId].iswcs.join(
+													" - "
+												)
+											}}
+										</span>
+										<span
+											v-if="
+												workMap[workId].type !== 'Song'
+											"
+										>
+											{{ workMap[workId].type }}
+										</span>
+										<span
+											v-if="
+												workMap[workId].attributes
+													?.length > 0
+											"
+										>
+											{{ workMap[workId].attributes }}
+										</span>
+									</span>
+								</p>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+		</template>
+	</modal>
+</template>
+
+<style lang="less">
+.night-mode {
+	.vjs-tree {
+		background: black;
+	}
+}
+</style>
+
+<style lang="less" scoped>
+.columns {
+	display: flex;
+	flex-direction: row;
+	gap: 16px;
+	flex: 1;
+}
+
+.left-column {
+	width: 400px;
+}
+
+.right-column {
+	flex: 1;
+}
+
+.box {
+	padding: 8px;
+	// border: 1px solid white;
+	border-radius: 8px;
+	background-color: var(--dark-grey-3);
+
+	.box-title {
+		font-size: 2rem;
+	}
+}
+
+.recordings,
+.works {
+	display: flex;
+	flex-direction: column;
+	gap: 8px;
+}
+
+.recording,
+.work {
+	padding: 8px;
+	border-radius: 8px;
+	background-color: var(--dark-grey-2);
+}
+
+.recording-title,
+.work-title {
+	display: flex;
+	justify-content: space-between;
+
+	> a:first-child {
+		font-size: 1.5rem;
+	}
+
+	.recording-title-right,
+	.work-title-right {
+		display: flex;
+		flex-direction: column;
+		align-items: flex-end;
+	}
+
+	.disambiguation {
+		opacity: 0.75;
+	}
+}
+
+.release-group-line,
+.release-line {
+	display: flex;
+	flex-direction: row;
+	gap: 4px;
+	flex: 1;
+
+	span:last-child {
+		flex: 1;
+		text-align: right;
+	}
+}
+
+.release-group {
+	flex-direction: column;
+}
+
+.release-group,
+.release {
+	display: flex;
+}
+
+.releases {
+	padding-left: 24px;
+}
+
+.release-group-type-title {
+	display: flex;
+	flex-direction: row;
+	gap: 4px;
+}
+
+.release-groups {
+	display: flex;
+	flex-direction: column;
+	gap: 8px;
+	padding-left: 16px;
+}
+</style>

+ 14 - 0
frontend/src/pages/Admin/Artists.vue

@@ -265,6 +265,20 @@ onMounted(() => {
 		>
 			<template #column-options="slotProps">
 				<div class="row-options">
+					<button
+						v-if="hasPermission('artists.update')"
+						class="button is-primary icon-with-button material-icons"
+						@click="
+							openModal({
+								modal: 'viewMBArtist',
+								props: { artistId: slotProps.item._id }
+							})
+						"
+						content="View Artist MB"
+						v-tippy
+					>
+						info
+					</button>
 					<button
 						v-if="hasPermission('artists.update')"
 						class="button is-primary icon-with-button material-icons"

+ 493 - 0
frontend/src/stores/viewMBArtist.ts

@@ -0,0 +1,493 @@
+import { defineStore } from "pinia";
+import {
+	ref,
+	computed
+	// watch
+} from "vue";
+import {
+	ArtistTemp,
+	RecordingTemp,
+	ReleaseGroupTemp,
+	// YoutubeVideoTemp,
+	ReleaseTemp,
+	URLTemp,
+	WorkTemp
+	// YoutubeChannelTemp,
+	// RecordingTemp
+} from "@/types/artist";
+
+// type LinkedVideos = { [k: string]: string[] };
+// type YoutubeVideoMap = { [k: string]: YoutubeVideoTemp };
+// type HideYoutubeChannel = { [k: string]: boolean };
+
+const measureStart = name => {
+	performance.mark(`${name}-started`);
+	console.log(`[MEASURE] START: ${name}`);
+};
+
+const measureFinish = name => {
+	performance.mark(`${name}-finished`);
+	const measure = performance.measure(
+		`${name}-duration`,
+		`${name}-started`,
+		`${name}-finished`
+	);
+	console.log(`[MEASURE] FINISH: ${name} - ${measure.duration}ms`);
+};
+
+type URLSourceTemp = {
+	type: string;
+	id: string;
+	url: URLTemp;
+};
+
+export const useViewMBArtistStore = ({ modalUuid }: { modalUuid: string }) =>
+	defineStore(`viewMBArtist-${modalUuid}`, () => {
+		const artist = ref<ArtistTemp | Partial<ArtistTemp>>({});
+		const recordingsReleasesReleaseGroups = ref<ReleaseTemp[]>([]);
+
+		const releaseMap = ref<{ [releaseId: string]: ReleaseTemp }>();
+		const releaseGroupMap = ref<{
+			[releaseId: string]: ReleaseGroupTemp;
+		}>();
+		const recordingMap = ref<{ [releaseId: string]: RecordingTemp }>();
+		const workMap = ref<{ [workId: string]: WorkTemp }>();
+		const urlMap = ref<{ [urlId: string]: URLTemp }>();
+		const urlSourceMap = ref<{ [recordingId: string]: URLSourceTemp[] }>(
+			{}
+		);
+
+		const releaseIds = ref();
+		const releaseGroupIds = ref();
+		const recordingIds = ref();
+		const workIds = ref();
+		const urlIds = ref();
+
+		const recordingsLimit = ref<number>(1);
+
+		const setArtist = ({ name, musicbrainzIdentifier }) => {
+			measureStart("setArtist");
+			artist.value = {
+				name,
+				musicbrainzIdentifier
+			};
+			measureFinish("setArtist");
+		};
+		const setMusicBrainzRecordingsReleasesReleaseGroups = (
+			artistReleases: ReleaseTemp[],
+			trackArtistReleases: ReleaseTemp[],
+			recordings: RecordingTemp[]
+		) => {
+			measureStart("setMusicBrainzRecordingsReleasesReleaseGroups");
+			// _recordingsReleasesReleaseGroups = _recordingsReleasesReleaseGroups.slice(0, 10);
+			console.log(
+				"albums.getMusicBrainzRecordingsReleasesReleaseGroups",
+				artistReleases,
+				trackArtistReleases
+			);
+			measureStart("setMusicBrainzRecordingsReleasesReleaseGroups 1");
+			recordingsReleasesReleaseGroups.value = [
+				...artistReleases,
+				...trackArtistReleases
+			];
+
+			const _recordingMap = {};
+			const _releaseMap = {};
+			const _releaseGroupMap = {};
+			const _workMap = {};
+			const _urlMap = {};
+			const _urlSourceMap = {};
+
+			recordings.forEach(recording => {
+				_recordingMap[recording.id] = recording;
+				_urlSourceMap[recording.id] = [];
+
+				recording.relations.forEach(relation => {
+					if (relation["target-type"] === "work") {
+						const { work } = relation;
+						if (!_workMap[work.id]) _workMap[work.id] = work;
+					}
+					if (relation["target-type"] === "url") {
+						const { url } = relation;
+						if (!_urlMap[url.id]) _urlMap[url.id] = url;
+						_urlSourceMap[recording.id].push({
+							type: "Recording",
+							id: recording.id,
+							url
+						});
+					}
+				});
+			});
+			measureFinish("setMusicBrainzRecordingsReleasesReleaseGroups 1");
+
+			measureStart("setMusicBrainzRecordingsReleasesReleaseGroups 2");
+			recordingsReleasesReleaseGroups.value.forEach(release => {
+				const releaseGroup = release["release-group"];
+
+				if (!_releaseMap[release.id]) _releaseMap[release.id] = release;
+				if (!_releaseGroupMap[releaseGroup.id])
+					_releaseGroupMap[releaseGroup.id] = releaseGroup;
+
+				const urlSources = [];
+				release.relations.forEach(relation => {
+					if (relation["target-type"] !== "url") return;
+					const { url } = relation;
+					if (!_urlMap[url.id]) _urlMap[url.id] = url;
+					urlSources.push({
+						type: "Release",
+						id: release.id,
+						url
+					});
+				});
+				releaseGroup.relations.forEach(relation => {
+					if (relation["target-type"] !== "url") return;
+					const { url } = relation;
+					if (!_urlMap[url.id]) _urlMap[url.id] = url;
+					urlSources.push({
+						type: "Release group",
+						id: releaseGroup.id,
+						url
+					});
+				});
+
+				release.media.forEach(media => {
+					if (!media.tracks) return;
+					media.tracks.forEach(track => {
+						const { recording } = track;
+
+						if (!_recordingMap[recording.id]) return;
+
+						_urlSourceMap[recording.id] = [
+							...urlSources,
+							..._urlSourceMap[recording.id]
+						];
+
+						// _urlSourceMap[recording.id] =
+						// console.log(`${recording.id}:`, urlMap);
+					});
+				});
+			});
+			measureFinish("setMusicBrainzRecordingsReleasesReleaseGroups 2");
+
+			measureStart("setMusicBrainzRecordingsReleasesReleaseGroups 3");
+			recordingMap.value = _recordingMap;
+			releaseMap.value = _releaseMap;
+			releaseGroupMap.value = _releaseGroupMap;
+			workMap.value = _workMap;
+			urlMap.value = _urlMap;
+			urlSourceMap.value = _urlSourceMap;
+
+			releaseIds.value = Object.keys(releaseMap.value);
+			releaseGroupIds.value = Object.keys(releaseGroupMap.value);
+			recordingIds.value = Object.keys(recordingMap.value);
+			workIds.value = Object.keys(workMap.value);
+			urlIds.value = Object.keys(urlMap.value);
+			measureFinish("setMusicBrainzRecordingsReleasesReleaseGroups 3");
+
+			measureFinish("setMusicBrainzRecordingsReleasesReleaseGroups");
+		};
+
+		const releaseGroupIdsForRecordingIdMap = computed<{
+			[recordingId: string]: string[];
+		}>(() => {
+			measureStart("computed releaseGroupIdsForRecordingIdMap");
+			const map = {};
+			recordingIds.value.forEach(recordingId => {
+				map[recordingId] = [];
+			});
+			releaseIds.value.forEach(releaseId => {
+				const release = releaseMap.value[releaseId];
+				const releaseGroupId = release["release-group"].id;
+				release.media.forEach(media => {
+					if (!media.tracks) return;
+					media.tracks.forEach(track => {
+						const { recording } = track;
+						const recordingId = recording.id;
+						if (!recordingIds.value.includes(recordingId)) return;
+						if (!map[recordingId].includes(releaseGroupId))
+							map[recordingId].push(releaseGroupId);
+					});
+				});
+			});
+			measureFinish("computed releaseGroupIdsForRecordingIdMap");
+			return map;
+		});
+		const releaseIdsForRecordingIdMap = computed<{
+			[recordingId: string]: string[];
+		}>(() => {
+			measureStart("computed releaseIdsForRecordingIdMap");
+			const map = {};
+			recordingIds.value.forEach(recordingId => {
+				map[recordingId] = [];
+			});
+			releaseIds.value.forEach(releaseId => {
+				const release = releaseMap.value[releaseId];
+				release.media.forEach(media => {
+					if (!media.tracks) return;
+					media.tracks.forEach(track => {
+						const { recording } = track;
+						const recordingId = recording.id;
+						if (!recordingIds.value.includes(recordingId)) return;
+						if (!map[recordingId].includes(releaseId))
+							map[recordingId].push(releaseId);
+					});
+				});
+			});
+			recordingIds.value.forEach(recordingId => {
+				map[recordingId].sort((releaseIdA, releaseIdB) => {
+					const releaseA = releaseMap.value[releaseIdA];
+					const releaseB = releaseMap.value[releaseIdB];
+
+					const dateA = releaseA.date ?? "X";
+					const dateB = releaseB.date ?? "X";
+
+					return dateA.localeCompare(dateB);
+				});
+			});
+			measureFinish("computed releaseIdsForRecordingIdMap");
+			return map;
+		});
+		const workIdsForRecordingIdMap = computed<{
+			[recordingId: string]: string[];
+		}>(() => {
+			measureStart("computed workIdsForRecordingIdMap");
+			const map = {};
+			recordingIds.value.forEach(recordingId => {
+				map[recordingId] = [];
+				const recording = recordingMap.value[recordingId];
+				// console.log(111222333, recording, recording.relations);
+				recording.relations.forEach(relation => {
+					if (relation["target-type"] !== "work") return;
+					const { work } = relation;
+					if (!map[recording.id].includes(work.id))
+						map[recording.id].push(work.id);
+				});
+			});
+			// releaseIds.value.forEach(releaseId => {
+			// 	const release = releaseMap.value[releaseId];
+			// 	// const releaseGroupId = release["release-group"].id;
+			// 	release.media.forEach(media => {
+			// 		if (!media.tracks) return;
+			// 		media.tracks.forEach(track => {
+			// 			const { recording } = track;
+			// 			if (!recordingIds.value.includes(recording.id)) return;
+			// 			console.log(111222333, recording, recording.relations);
+			// 			recording.relations.forEach(relation => {
+			// 				if (relation["target-type"] !== "work") return;
+			// 				const { work } = relation;
+			// 				if (!map[recording.id].includes(work.id))
+			// 					map[recording.id].push(work.id);
+			// 			});
+			// 		});
+			// 	});
+			// });
+			measureFinish("computed workIdsForRecordingIdMap");
+			return map;
+		});
+		// const urlIdsForRecordingIdMap = computed<{
+		// 	[recordingId: string]: string[];
+		// }>(() => {
+		// 	measureStart("computed urlIdsForRecordingIdMap");
+		// 	const map = {};
+		// 	recordingIds.value.forEach(recordingId => {
+		// 		map[recordingId] = [];
+		// 		urlSourceMap[recordingId].forEach(urlSource => {
+
+		// 		})
+		// 		// const recording = recordingMap.value[recordingId];
+		// 		// recording.relations.forEach(relation => {
+		// 		// 	if (relation["target-type"] !== "url") return;
+		// 		// 	const { url } = relation;
+		// 		// 	if (!map[recording.id].includes(url.id))
+		// 		// 		map[recording.id].push(url.id);
+		// 		// });
+		// 	});
+		// 	// releaseIds.value.forEach(releaseId => {
+		// 	// 	const release = releaseMap.value[releaseId];
+		// 	// 	release.media.forEach(media => {
+		// 	// 		if (!media.tracks) return;
+		// 	// 		media.tracks.forEach(track => {
+		// 	// 			const { recording } = track;
+		// 	// 			if (!recordingIds.value.includes(recording.id)) return;
+		// 	// 			recording.relations.forEach(relation => {
+		// 	// 				if (relation["target-type"] !== "url") return;
+		// 	// 				const { url } = relation;
+		// 	// 				if (!map[recording.id].includes(url.id))
+		// 	// 					map[recording.id].push(url.id);
+		// 	// 			});
+		// 	// 		});
+		// 	// 	});
+		// 	// });
+		// 	measureFinish("computed workIdsForRecordingIdMap");
+		// 	return map;
+		// });
+		const releaseGroupTypeMapForRecordingMap = computed(() => {
+			measureStart("releaseGroupTypeMapForRecordingMap");
+			const map = {};
+			recordingIds.value.forEach(recordingId => {
+				const releaseGroupIds =
+					releaseGroupIdsForRecordingIdMap.value[recordingId];
+				const typeMap = {};
+				const typeKeys = [];
+				const typeNameMap = {};
+				releaseGroupIds.forEach(releaseGroupId => {
+					const releaseGroup = releaseGroupMap.value[releaseGroupId];
+					const typeIds = [
+						releaseGroup["primary-type-id"],
+						...releaseGroup["secondary-type-ids"]
+					];
+					const typeKey = `${typeIds.length}-${typeIds.join("-")}`;
+					if (!typeKeys.includes(typeKey)) {
+						typeKeys.push(typeKey);
+						typeMap[typeKey] = [];
+						typeNameMap[typeKey] = {
+							primaryType: releaseGroup["primary-type"] ?? "N/A",
+							secondaryTypes:
+								releaseGroup["secondary-types"].toSorted()
+						};
+					}
+					typeMap[typeKey].push(releaseGroupId);
+				});
+				typeKeys.sort((typeKeyA, typeKeyB) => {
+					const typeNameA = typeNameMap[typeKeyA];
+					const typeNameB = typeNameMap[typeKeyB];
+
+					const typesA = [
+						typeNameA.primaryType,
+						...typeNameA.secondaryTypes
+					];
+					const typesB = [
+						typeNameB.primaryType,
+						...typeNameB.secondaryTypes
+					];
+
+					const typesLength = typesA.length - typesB.length;
+					if (typesLength !== 0) return typesLength;
+
+					for (let i = 0; i < typesA.length; i += 1) {
+						const typeA = typesA[i].toLowerCase();
+						const typeB = typesB[i].toLowerCase();
+						const compareResult = typeA.localeCompare(typeB);
+						if (compareResult !== 0) return compareResult;
+					}
+
+					return 0;
+				});
+				typeKeys.forEach(typeKey => {
+					typeMap[typeKey].sort(
+						(releaseGroupIdA, releaseGroupIdB) => {
+							const releaseGroupA =
+								releaseGroupMap.value[releaseGroupIdA];
+							const releaseGroupB =
+								releaseGroupMap.value[releaseGroupIdB];
+
+							const dateA =
+								releaseGroupA["first-release-date"] ?? "X";
+							const dateB =
+								releaseGroupB["first-release-date"] ?? "X";
+
+							return dateA.localeCompare(dateB);
+						}
+					);
+				});
+				map[recordingId] = {
+					typeMap,
+					typeKeys,
+					typeNameMap
+				};
+			});
+			measureFinish("releaseGroupTypeMapForRecordingMap");
+			return map;
+		});
+		const releaseIdsPerReleaseGroupMap = computed(() => {
+			measureStart("releaseIdsPerReleaseGroupMap");
+			const map = {};
+
+			releaseGroupIds.value.forEach(releaseGroupId => {
+				map[releaseGroupId] = [];
+			});
+			releaseIds.value.forEach(releaseId => {
+				const release = releaseMap.value[releaseId];
+				const releaseGroupId = release["release-group"].id;
+				map[releaseGroupId].push(releaseId);
+			});
+			releaseGroupIds.value.forEach(releaseGroupId => {
+				map[releaseGroupId].sort((releaseIdA, releaseIdB) => {
+					const releaseA = releaseMap.value[releaseIdA];
+					const releaseB = releaseMap.value[releaseIdB];
+
+					const dateA = releaseA.date ?? "X";
+					const dateB = releaseB.date ?? "X";
+
+					return dateA.localeCompare(dateB);
+				});
+			});
+			measureFinish("releaseIdsPerReleaseGroupMap");
+			return map;
+		});
+
+		const filteredRecordingIds = computed(() => {
+			measureStart("computed filteredRecordingIds");
+			const sortedRecordingIds = recordingIds.value.toSorted(
+				(recordingIdA, recordingIdB) => {
+					const recordingA = recordingMap.value[recordingIdA];
+					const recordingB = recordingMap.value[recordingIdB];
+					const recordingDisambiguationA = recordingA.disambiguation
+						? ` ${recordingA.disambiguation}`
+						: "";
+					const recordingDisambiguationB = recordingB.disambiguation
+						? ` ${recordingB.disambiguation}`
+						: "";
+					const recordingTitleA = `${recordingA.title}${recordingDisambiguationA}`;
+					const recordingTitleB = `${recordingB.title}${recordingDisambiguationB}`;
+					return recordingTitleA.localeCompare(recordingTitleB);
+				}
+			);
+			const returnValue = sortedRecordingIds.slice(
+				0,
+				recordingsLimit.value
+			);
+			measureFinish("computed filteredRecordingIds");
+			return returnValue;
+		});
+
+		return {
+			artist,
+			recordingsReleasesReleaseGroups,
+			//
+			releaseMap,
+			releaseGroupMap,
+			recordingMap,
+			workMap,
+			urlMap,
+			//
+			releaseIds,
+			releaseGroupIds,
+			recordingIds,
+			workIds,
+			urlIds,
+			//
+			releaseIdsForRecordingIdMap,
+			releaseGroupIdsForRecordingIdMap,
+			workIdsForRecordingIdMap,
+			// urlIdsForRecordingIdMap,
+			//
+			recordingsLimit,
+			filteredRecordingIds,
+			//
+			releaseGroupTypeMapForRecordingMap,
+			releaseIdsPerReleaseGroupMap,
+			//
+			urlSourceMap,
+			// getters
+			// recordings,
+			// youtubeVideoMapAdjusted,
+			// filteredRecordings,
+			// filteredYoutubeVideoIds,
+			// filteredYoutubeVideosIdsLength,
+			// testRecordings,
+			// methods
+			setArtist,
+			setMusicBrainzRecordingsReleasesReleaseGroups
+		};
+	});