3 Commits af56e19ba5 ... d08b95b676

Auteur SHA1 Message Date
  Kristian Vos d08b95b676 refactor: get even more msucibrainz data when requesting recordings and such il y a 1 mois
  Kristian Vos 2964bf0726 feat: add proxying/caching for coverartarchive artwork il y a 1 mois
  Kristian Vos c02f9cdf22 chore: added example modal that can be used as a template/base il y a 1 mois

+ 184 - 10
backend/logic/musicbrainz.js

@@ -3,6 +3,9 @@ import axios from "axios";
 import CoreClass from "../core";
 import { MUSARE_VERSION } from "..";
 
+export const MUSICBRAINZ_CAA_CACHED_RESPONSE_REDIS_TABLE = "musicbrainzCAACachedResponse";
+export const MUSICBRAINZ_RECORDINGS_RELEASES_RELEASE_GROUPS_REDIS_TABLE = "musicbrainzRecordingsReleasesReleaseGroups";
+
 class RateLimitter {
 	/**
 	 * Constructor
@@ -35,6 +38,7 @@ class RateLimitter {
 let MusicBrainzModule;
 let DBModule;
 let CacheModule;
+let AppModule;
 
 class _MusicBrainzModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
@@ -53,6 +57,7 @@ class _MusicBrainzModule extends CoreClass {
 	async initialize() {
 		DBModule = this.moduleManager.modules.db;
 		CacheModule = this.moduleManager.modules.cache;
+		AppModule = this.moduleManager.modules.app;
 
 		this.genericApiRequestModel = this.GenericApiRequestModel = await DBModule.runJob("GET_MODEL", {
 			modelName: "genericApiRequest"
@@ -62,6 +67,71 @@ class _MusicBrainzModule extends CoreClass {
 		this.requestTimeout = 5000;
 
 		this.axios = axios.create();
+
+		const { app } = await AppModule.runJob("GET_APP", {});
+
+		// Proxy with caching for coverartarchive.org
+		app.get(
+			[
+				"/caa/release/:mbid",
+				"/caa/release/:mbid/:type",
+				"/caa/release-group/:mbid",
+				"/caa/release-group/:mbid/:type"
+			],
+			async (req, res) => {
+				// TODO add config option to proxy or redirect these requests
+
+				// Remove /caa/ from the path
+				const path = req.path.substring(5);
+				console.log(`Request for ${path}`);
+
+				const cachedResponse = await CacheModule.runJob("HGET", {
+					table: MUSICBRAINZ_CAA_CACHED_RESPONSE_REDIS_TABLE,
+					key: path
+				});
+				if (cachedResponse) {
+					const { contentType, data, status } = cachedResponse;
+					if (status === "404") {
+						console.log(contentType, data, status);
+					}
+					res.set("Content-Type", contentType);
+					res.status(status);
+					res.send(Buffer.from(data, "base64"));
+					return;
+				}
+
+				let CAARes;
+				try {
+					CAARes = await this.axios.get(`https://coverartarchive.org/${path}`, {
+						responseType: "arraybuffer"
+					});
+				} catch (err) {
+					if (err.response) CAARes = err.response;
+					else {
+						console.log(`Non-normal error when requesting coverartarchive.org: ${err.message}`);
+						return;
+					}
+				}
+
+				const contentType = CAARes.headers["content-type"];
+				const data = Buffer.from(CAARes.data, "binary").toString("base64");
+				const { status } = CAARes;
+
+				await CacheModule.runJob("HSET", {
+					table: MUSICBRAINZ_CAA_CACHED_RESPONSE_REDIS_TABLE,
+					key: path,
+					value: JSON.stringify({
+						contentType,
+						data,
+						status
+					})
+				});
+
+				res.set("Content-Type", contentType);
+				res.status(status);
+				res.send(Buffer.from(data, "base64"));
+			}
+		);
 	}
 
 	/**
@@ -137,7 +207,7 @@ class _MusicBrainzModule extends CoreClass {
 		const existingResponse = await CacheModule.runJob(
 			"HGET",
 			{
-				table: "musicbrainzRecordingsReleasesReleaseGroups",
+				table: MUSICBRAINZ_RECORDINGS_RELEASES_RELEASE_GROUPS_REDIS_TABLE,
 				key: artistId
 			},
 			this
@@ -145,30 +215,59 @@ class _MusicBrainzModule extends CoreClass {
 		if (existingResponse) return existingResponse;
 
 		const fetchDate = new Date();
-		let maxReleases = 0;
-		let releases = [];
+
+		let maxArtistReleases = 0;
+		let artistReleases = [];
 		do {
-			const offset = releases.length;
+			const offset = artistReleases.length;
 			// eslint-disable-next-line no-await-in-loop
 			const response = await MusicBrainzModule.runJob(
 				"GET_RECORDINGS_RELEASES_RELEASE_GROUPS_PAGE",
 				{ artistId, offset },
 				this
 			);
-			maxReleases = response["release-count"];
-			releases = [...releases, ...response.releases];
+			maxArtistReleases = response["release-count"];
+			artistReleases = [...artistReleases, ...response.releases];
 			if (response.releases.length === 0) break;
-		} while (maxReleases >= releases.length);
+		} while (maxArtistReleases >= artistReleases.length);
+
+		let maxTrackArtistReleases = 0;
+		let trackArtistReleases = [];
+		do {
+			const offset = trackArtistReleases.length;
+			// eslint-disable-next-line no-await-in-loop
+			const response = await MusicBrainzModule.runJob(
+				"GET_RECORDINGS_RELEASES_RELEASE_GROUPS_PAGE_WITH_TRACK_ARTIST",
+				{ artistId, offset },
+				this
+			);
+			maxTrackArtistReleases = response["release-count"];
+			trackArtistReleases = [...trackArtistReleases, ...response.releases];
+			if (response.releases.length === 0) break;
+		} while (maxTrackArtistReleases >= trackArtistReleases.length);
+
+		let maxRecordings = 0;
+		let recordings = [];
+		do {
+			const offset = recordings.length;
+			// eslint-disable-next-line no-await-in-loop
+			const response = await MusicBrainzModule.runJob("GET_RECORDINGS_PAGE", { artistId, offset }, this);
+			maxRecordings = response["recording-count"];
+			recordings = [...recordings, ...response.recordings];
+			if (response.recordings.length === 0) break;
+		} while (maxRecordings >= recordings.length);
 
 		const response = {
-			releases,
+			artistReleases,
+			trackArtistReleases,
+			recordings,
 			fetchDate
 		};
 
 		await CacheModule.runJob(
 			"HSET",
 			{
-				table: "musicbrainzRecordingsReleasesReleaseGroups",
+				table: MUSICBRAINZ_RECORDINGS_RELEASES_RELEASE_GROUPS_REDIS_TABLE,
 				key: artistId,
 				value: JSON.stringify(response)
 			},
@@ -195,7 +294,82 @@ class _MusicBrainzModule extends CoreClass {
 				params: {
 					fmt: "json",
 					artist: artistId,
-					inc: "release-groups+recordings",
+					inc: [
+						"url-rels",
+						"release-groups",
+						"release-group-level-rels",
+						"url-rels",
+						"recordings",
+						"recording-level-rels",
+						"work-rels",
+						"url-rels"
+					].join("+"),
+					limit: 100,
+					offset
+				}
+			},
+			this
+		);
+
+		return response;
+	}
+
+	/**
+	 * Gets a list of recordings, releases and release groups for a given track artist, for a given page
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.artistId - MusicBrainz artist id
+	 * @param {string} payload.offset - offset by how much
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async GET_RECORDINGS_RELEASES_RELEASE_GROUPS_PAGE_WITH_TRACK_ARTIST(payload) {
+		const { artistId, offset } = payload;
+
+		const response = await MusicBrainzModule.runJob(
+			"API_CALL",
+			{
+				url: `https://musicbrainz.org/ws/2/release`,
+				params: {
+					fmt: "json",
+					track_artist: artistId,
+					inc: [
+						"url-rels",
+						"release-groups",
+						"release-group-level-rels",
+						"url-rels",
+						"recordings"
+						// "recording-level-rels",
+						// "work-rels",
+						// "url-rels",
+						// "artist-credits"
+					].join("+"),
+					limit: 100,
+					offset
+				}
+			},
+			this
+		);
+
+		return response;
+	}
+
+	/**
+	 * Gets a list of recordings for an artist, for a given page
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.artistId - MusicBrainz artist id
+	 * @param {string} payload.offset - offset by how much
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async GET_RECORDINGS_PAGE(payload) {
+		const { artistId, offset } = payload;
+
+		const response = await MusicBrainzModule.runJob(
+			"API_CALL",
+			{
+				url: `https://musicbrainz.org/ws/2/recording`,
+				params: {
+					fmt: "json",
+					artist: artistId,
+					inc: ["artist-credits", "isrcs", "work-rels", "url-rels"].join("+"),
 					limit: 100,
 					offset
 				}

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

@@ -8,6 +8,7 @@ const { modals, activeModals } = storeToRefs(modalsStore);
 
 const modalComponents = shallowRef(
 	useModalComponents("components/modals", {
+		example: "Example.vue",
 		editUser: "EditUser.vue",
 		login: "Login.vue",
 		register: "Register.vue",

+ 55 - 0
frontend/src/components/modals/Example.vue

@@ -0,0 +1,55 @@
+<script setup lang="ts">
+// import Toast from "toasters";
+import {
+	defineAsyncComponent
+	// ref,
+	// computed,
+	// onMounted
+} from "vue";
+// import { useWebsocketsStore } from "@/stores/websockets";
+// import { useModalsStore } from "@/stores/modals";
+
+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, openModal } = useModalsStore();
+</script>
+
+<template>
+	<modal class="example-modal" title="Example" :size="'wide'" :split="true">
+		<template #body>
+			<div class="flex flex-column w-full"></div>
+		</template>
+		<template #footer>
+			<div>
+				<button class="button is-primary">Button</button>
+			</div>
+		</template>
+	</modal>
+</template>
+
+<style lang="less" scoped>
+.flex {
+	display: flex;
+}
+
+.flex-column {
+	flex-direction: column;
+}
+
+.flex-row {
+	flex-direction: row;
+}
+
+.w-full {
+	width: 100%;
+}
+</style>