diff --git a/frontend/src/components/Details/SoundtrackPlayer.vue b/frontend/src/components/Details/SoundtrackPlayer.vue index eaa0c1b96..9423d32c0 100644 --- a/frontend/src/components/Details/SoundtrackPlayer.vue +++ b/frontend/src/components/Details/SoundtrackPlayer.vue @@ -4,11 +4,12 @@ import type { Emitter } from "mitt"; import { storeToRefs } from "pinia"; import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; +import type { + RomFileAudioMetaSchema, + SoundtrackTrackMetaSchema, +} from "@/__generated__"; import VolumeControl from "@/components/common/VolumeControl.vue"; -import romApi, { - type SoundtrackAudioMeta, - type SoundtrackTrackMeta, -} from "@/services/api/rom"; +import romApi from "@/services/api/rom"; import type { DetailedRom } from "@/stores/roms"; import useSoundtrackPlayer, { type PlayerMeta, @@ -74,7 +75,7 @@ const folderCoverUrl = computed(() => { return cover ? fileUrl(cover.id, cover.file_name) : null; }); -const tracksMeta = ref>(new Map()); +const tracksMeta = ref>(new Map()); const isLoadingMeta = ref(false); let metaAbort: AbortController | null = null; @@ -88,13 +89,13 @@ const activeTrack = computed(() => tracks.value.find((t) => t.id === activeTrackId.value), ); -const activeMeta = computed(() => +const activeMeta = computed(() => activeTrackId.value != null ? tracksMeta.value.get(activeTrackId.value) : undefined, ); -function coverUrlForMeta(m: SoundtrackAudioMeta | undefined): string | null { +function coverUrlForMeta(m: RomFileAudioMetaSchema | undefined): string | null { if (m?.cover_path) return `${FRONTEND_RESOURCES_PATH}/${m.cover_path}`; return null; } @@ -151,8 +152,8 @@ async function loadAllMetadata() { romId: props.rom.id, signal: metaAbort.signal, }); - const next = new Map(); - for (const row of data as SoundtrackTrackMeta[]) { + const next = new Map(); + for (const row of data as SoundtrackTrackMetaSchema[]) { if (row.audio_meta) next.set(row.file_id, row.audio_meta); } tracksMeta.value = next; @@ -171,7 +172,7 @@ async function loadAllMetadata() { } } -function toPlayerMeta(m: SoundtrackAudioMeta | undefined): PlayerMeta { +function toPlayerMeta(m: RomFileAudioMetaSchema | undefined): PlayerMeta { return { title: m?.title ?? undefined, artist: m?.artist ?? undefined, @@ -243,7 +244,7 @@ function onDelete(fileId: number) { emit("delete-track", fileId); } -function chips(meta: SoundtrackAudioMeta | undefined) { +function chips(meta: RomFileAudioMetaSchema | undefined) { if (!meta) return []; const items: { icon: string; label: string }[] = []; if (meta.album) items.push({ icon: "mdi-album", label: meta.album }); diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index 06e2221b3..0549ad8ba 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -10,6 +10,7 @@ import type { RomUserSchema, SearchRomSchema, SimpleRomSchema, + SoundtrackTrackMetaSchema, UserNoteSchema, RomFiltersDict, } from "@/__generated__"; @@ -635,26 +636,6 @@ async function removeSoundtrack({ return api.delete(`/roms/${romId}/soundtracks/${fileId}`); } -export interface SoundtrackAudioMeta { - title: string | null; - artist: string | null; - album: string | null; - year: string | null; - genre: string | null; - track: string | null; - disc: string | null; - duration_seconds: number | null; - has_embedded_cover: boolean; - cover_path: string | null; -} - -export interface SoundtrackTrackMeta { - file_id: number; - file_name: string; - file_size_bytes: number; - audio_meta: SoundtrackAudioMeta | null; -} - async function getSoundtrackMetadata({ romId, signal, @@ -662,9 +643,12 @@ async function getSoundtrackMetadata({ romId: number; signal?: AbortSignal; }) { - return api.get(`/roms/${romId}/soundtracks/metadata`, { - signal, - }); + return api.get( + `/roms/${romId}/soundtracks/metadata`, + { + signal, + }, + ); } async function uploadManualFiles({ diff --git a/frontend/src/stores/soundtrackPlayer.ts b/frontend/src/stores/soundtrackPlayer.ts index 02abd8f1c..aefe6f85e 100644 --- a/frontend/src/stores/soundtrackPlayer.ts +++ b/frontend/src/stores/soundtrackPlayer.ts @@ -2,6 +2,7 @@ import { useLocalStorage } from "@vueuse/core"; import { throttle } from "lodash"; import { defineStore } from "pinia"; import { computed, ref, shallowRef } from "vue"; +import type { RomFileAudioMetaSchema } from "@/__generated__"; const volumeStorage = useLocalStorage("soundtrack.volume", 1); const mutedStorage = useLocalStorage("soundtrack.muted", false); @@ -13,18 +14,24 @@ export interface PlayerTrack { url: string; } -export interface PlayerMeta { - title?: string; - artist?: string; - album?: string; - year?: string; - genre?: string; - track?: string; - disc?: string; +// Audio-tag fields are sourced from the generated schema; the rest (duration in +// seconds + resolved cover URLs) are UI-specific to the player. +type AudioTagKey = + | "title" + | "artist" + | "album" + | "year" + | "genre" + | "track" + | "disc"; + +export type PlayerMeta = { + [K in AudioTagKey]?: NonNullable; +} & { duration?: number; coverUrl?: string; folderCoverUrl?: string; -} +}; const useSoundtrackPlayer = defineStore("soundtrackPlayer", () => { const track = ref(null); diff --git a/frontend/src/v2/components/GameDetails/SoundtrackPanel.vue b/frontend/src/v2/components/GameDetails/SoundtrackPanel.vue index 6c8d88881..960b88c5c 100644 --- a/frontend/src/v2/components/GameDetails/SoundtrackPanel.vue +++ b/frontend/src/v2/components/GameDetails/SoundtrackPanel.vue @@ -21,10 +21,11 @@ import { watch, } from "vue"; import { useI18n } from "vue-i18n"; -import romApi, { - type SoundtrackAudioMeta, - type SoundtrackTrackMeta, -} from "@/services/api/rom"; +import type { + RomFileAudioMetaSchema, + SoundtrackTrackMetaSchema, +} from "@/__generated__"; +import romApi from "@/services/api/rom"; import type { DetailedRom } from "@/stores/roms"; import useSoundtrackPlayer, { type PlayerMeta, @@ -101,16 +102,16 @@ const folderCoverUrl = computed(() => { }); // ---------- Metadata fetch ---------- -const tracksMeta = ref>(new Map()); +const tracksMeta = ref>(new Map()); const isLoadingMeta = ref(false); let metaAbort: AbortController | null = null; -function coverUrlForMeta(m: SoundtrackAudioMeta | undefined): string | null { +function coverUrlForMeta(m: RomFileAudioMetaSchema | undefined): string | null { if (m?.cover_path) return `${FRONTEND_RESOURCES_PATH}/${m.cover_path}`; return null; } -function toPlayerMeta(m: SoundtrackAudioMeta | undefined): PlayerMeta { +function toPlayerMeta(m: RomFileAudioMetaSchema | undefined): PlayerMeta { return { title: m?.title ?? undefined, artist: m?.artist ?? undefined, @@ -149,8 +150,8 @@ async function loadAllMetadata() { romId: props.rom.id, signal: metaAbort.signal, }); - const next = new Map(); - for (const row of data as SoundtrackTrackMeta[]) { + const next = new Map(); + for (const row of data as SoundtrackTrackMetaSchema[]) { if (row.audio_meta) next.set(row.file_id, row.audio_meta); } tracksMeta.value = next; @@ -192,7 +193,7 @@ const activeTrack = computed(() => tracks.value.find((t) => t.id === activeTrackId.value), ); -const activeMeta = computed(() => +const activeMeta = computed(() => activeTrackId.value != null ? tracksMeta.value.get(activeTrackId.value) : undefined, @@ -236,7 +237,7 @@ function trackDurationFor(fileId: number): number | undefined { // Chips shown in the now-playing header. type ChipItem = { icon: string; label: string; color?: string }; -function headerChips(meta: SoundtrackAudioMeta | undefined): ChipItem[] { +function headerChips(meta: RomFileAudioMetaSchema | undefined): ChipItem[] { if (!meta) return []; const items: ChipItem[] = []; if (meta.album) items.push({ icon: "mdi-album", label: meta.album });