diff --git a/.pmomusic_audio/cache.db b/.pmomusic_audio/cache.db new file mode 100644 index 00000000..6bb75df6 Binary files /dev/null and b/.pmomusic_audio/cache.db differ diff --git a/pmoapp/webapp/.pmomusic_audio/cache.db b/pmoapp/webapp/.pmomusic_audio/cache.db new file mode 100644 index 00000000..6bb75df6 Binary files /dev/null and b/pmoapp/webapp/.pmomusic_audio/cache.db differ diff --git a/pmoapp/webapp/src/App.vue b/pmoapp/webapp/src/App.vue index 4f854052..d36820bc 100644 --- a/pmoapp/webapp/src/App.vue +++ b/pmoapp/webapp/src/App.vue @@ -13,6 +13,7 @@ 📋 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`; +}