From ecd49869b3f952ca6ed443a648f857cf3bb33e1e Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 18 Oct 2025 14:35:19 +0200 Subject: [PATCH] =?UTF-8?q?nouvelle=20mise=20=C3=A0=20jour=20de=20la=20web?= =?UTF-8?q?app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pmomusic_audio/cache.db | Bin 0 -> 20480 bytes pmoapp/webapp/.pmomusic_audio/cache.db | Bin 0 -> 20480 bytes pmoapp/webapp/src/App.vue | 3 +- .../src/components/AudioCacheManager.vue | 771 ++++++++++++++++++ pmoapp/webapp/src/router/index.ts | 2 + pmoapp/webapp/src/services/audioCache.ts | 192 +++++ 6 files changed, 967 insertions(+), 1 deletion(-) create mode 100644 .pmomusic_audio/cache.db create mode 100644 pmoapp/webapp/.pmomusic_audio/cache.db create mode 100644 pmoapp/webapp/src/components/AudioCacheManager.vue create mode 100644 pmoapp/webapp/src/services/audioCache.ts diff --git a/.pmomusic_audio/cache.db b/.pmomusic_audio/cache.db new file mode 100644 index 0000000000000000000000000000000000000000..6bb75df69dceb4727044f982454cf1a9d6b66f71 GIT binary patch literal 20480 zcmeI#L2J}N6u|LGck4=F+u|kHCuN}vMHCUQrrS}LuG_ke(4IovM2t;0Z8CxNsCe=- z^zO&-Yk4#YEt@u7Kfv-INHRlS@@9TBmv{1E6q!&yC+UT6^_O*9>e;U+-L1c>-TkdLuebheJ}o*l1Q0*~0R#|0009ILKmdVd0`qpq ze$}(S??kg8oJYaj^Yb7|Jd^s<^URCWygptZOtj-_8IBM1sXP=dM`NjfA-l1k886Sm zK%A4oo_vW+R`&YeS{-{(WVRp9JWb*_oSG<^)qmIi{{$8vHRrK-*Z(0_$L{s4pDp8m zio<$>dVlREnd|J2^u1MW$h~%XF0MXx<#;lD?@Z3*P@nBJg0dt}r=gdpaTVMQSWI$B zK*cv4yZW7;$bo+Ad>py*y!oSrdO54+`-{-{fp2{8YnIHaTz4khf4%m|KI~bl+MMhv zE^7JWWSFEH-#sHQ>Skpv|F}GC-SW0w-e?FQfB*srAbqfW2LS;D5I_I{1Q0*~0R#|00D;vPp#ER|J!XXn UAb^_O*9>e;U+-L1c>-TkdLuebheJ}o*l1Q0*~0R#|0009ILKmdVd0`qpq ze$}(S??kg8oJYaj^Yb7|Jd^s<^URCWygptZOtj-_8IBM1sXP=dM`NjfA-l1k886Sm zK%A4oo_vW+R`&YeS{-{(WVRp9JWb*_oSG<^)qmIi{{$8vHRrK-*Z(0_$L{s4pDp8m zio<$>dVlREnd|J2^u1MW$h~%XF0MXx<#;lD?@Z3*P@nBJg0dt}r=gdpaTVMQSWI$B zK*cv4yZW7;$bo+Ad>py*y!oSrdO54+`-{-{fp2{8YnIHaTz4khf4%m|KI~bl+MMhv zE^7JWWSFEH-#sHQ>Skpv|F}GC-SW0w-e?FQfB*srAbqfW2LS;D5I_I{1Q0*~0R#|00D;vPp#ER|J!XXn UAb📋 Logs 🎵 UPnP Explorer 🎨 Cover Cache + 🎵 Audio Cache 🚀 API Dashboard @@ -31,7 +32,7 @@ const showDebugMenu = ref(false) const route = useRoute() const isDebugRoute = computed(() => { - return ['/logs', '/upnp', '/covers-cache', '/api-dashboard'].includes(route.path) + return ['/logs', '/upnp', '/covers-cache', '/audio-cache', '/api-dashboard'].includes(route.path) }) diff --git a/pmoapp/webapp/src/components/AudioCacheManager.vue b/pmoapp/webapp/src/components/AudioCacheManager.vue new file mode 100644 index 00000000..e6cd108c --- /dev/null +++ b/pmoapp/webapp/src/components/AudioCacheManager.vue @@ -0,0 +1,771 @@ + + + + + diff --git a/pmoapp/webapp/src/router/index.ts b/pmoapp/webapp/src/router/index.ts index 5b505243..8be42c12 100644 --- a/pmoapp/webapp/src/router/index.ts +++ b/pmoapp/webapp/src/router/index.ts @@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from "vue-router"; import HelloWorld from "../components/HelloWorld.vue"; import LogView from "../components/LogView.vue"; import CoverCacheManager from "../components/CoverCacheManager.vue"; +import AudioCacheManager from "../components/AudioCacheManager.vue"; import UpnpExplorer from "../components/UpnpExplorer.vue"; import APIDashboard from "../components/APIDashboard.vue"; @@ -9,6 +10,7 @@ const routes = [ { path: "/", name: "home", component: HelloWorld }, { path: "/logs", name: "logs", component: LogView }, { path: "/covers-cache", name: "covers-cache", component: CoverCacheManager }, + { path: "/audio-cache", name: "audio-cache", component: AudioCacheManager }, { path: "/upnp", name: "upnp", component: UpnpExplorer }, { path: "/api-dashboard", name: "api-dashboard", component: APIDashboard }, ]; diff --git a/pmoapp/webapp/src/services/audioCache.ts b/pmoapp/webapp/src/services/audioCache.ts new file mode 100644 index 00000000..c3d9f244 --- /dev/null +++ b/pmoapp/webapp/src/services/audioCache.ts @@ -0,0 +1,192 @@ +/** + * Service API pour interagir avec le cache de pistes audio + */ + +export interface AudioMetadata { + title?: string; + artist?: string; + album?: string; + year?: number; + genre?: string; + track_number?: number; + disc_number?: number; + duration_ms?: number; + sample_rate?: number; + bitrate?: number; + channels?: number; +} + +export interface AudioCacheEntry { + pk: string; + source_url: string; + hits: number; + last_used: string | null; + collection?: string; + metadata?: AudioMetadata; +} + +export interface AddTrackRequest { + url: string; + collection?: string; +} + +export interface AddTrackResponse { + pk: string; + url: string; + message: string; +} + +export interface DownloadStatus { + pk: string; + status: "pending" | "downloading" | "completed" | "failed"; + progress?: number; + error?: string; +} + +export interface ApiError { + error: string; + message: string; +} + +/** + * Liste toutes les pistes en cache + */ +export async function listTracks(): Promise { + const response = await fetch("/api/audio"); + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || "Failed to fetch tracks"); + } + return response.json(); +} + +/** + * Récupère les informations d'une piste spécifique + */ +export async function getTrackInfo(pk: string): Promise { + const response = await fetch(`/api/audio/${pk}`); + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || "Failed to fetch track info"); + } + return response.json(); +} + +/** + * Récupère le statut de téléchargement d'une piste + */ +export async function getDownloadStatus(pk: string): Promise { + const response = await fetch(`/api/audio/${pk}/status`); + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || "Failed to fetch download status"); + } + return response.json(); +} + +/** + * Ajoute une nouvelle piste au cache depuis une URL + */ +export async function addTrack(url: string, collection?: string): Promise { + const body: AddTrackRequest = { url }; + if (collection) { + body.collection = collection; + } + + const response = await fetch("/api/audio", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || "Failed to add track"); + } + return response.json(); +} + +/** + * Supprime une piste du cache + */ +export async function deleteTrack(pk: string): Promise { + const response = await fetch(`/api/audio/${pk}`, { + method: "DELETE", + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || "Failed to delete track"); + } +} + +/** + * Purge complètement le cache + */ +export async function purgeCache(): Promise { + const response = await fetch("/api/audio", { + method: "DELETE", + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || "Failed to purge cache"); + } +} + +/** + * Consolide le cache (re-télécharge les pistes manquantes) + */ +export async function consolidateCache(): Promise { + const response = await fetch("/api/audio/consolidate", { + method: "POST", + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || "Failed to consolidate cache"); + } +} + +/** + * Génère l'URL pour streamer une piste + */ +export function getTrackUrl(pk: string): string { + return `/audio/tracks/${pk}`; +} + +/** + * Génère l'URL pour télécharger la piste originale + */ +export function getOriginalTrackUrl(pk: string): string { + return `/audio/tracks/${pk}/orig`; +} + +/** + * Formatte la durée en millisecondes au format MM:SS + */ +export function formatDuration(ms?: number): string { + if (!ms) return "Unknown"; + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; +} + +/** + * Formatte le bitrate en kbps + */ +export function formatBitrate(bitrate?: number): string { + if (!bitrate) return "Unknown"; + return `${Math.round(bitrate / 1000)} kbps`; +} + +/** + * Formatte le sample rate en kHz + */ +export function formatSampleRate(sampleRate?: number): string { + if (!sampleRate) return "Unknown"; + return `${(sampleRate / 1000).toFixed(1)} kHz`; +}