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 @@ + + + + 🎵 Audio Cache Manager + + {{ tracks.length }} tracks + {{ totalHits }} hits + + + + + + ➕ Add New Track + + + + + + {{ isAdding ? "Adding..." : "Add Track" }} + + + {{ addError }} + {{ addSuccess }} + + + + + + + Sort by: + + Most Used + Recently Used + Recently Added + + + + + {{ isLoading ? "Loading..." : "Refresh" }} + + + {{ isConsolidating ? "Consolidating..." : "Consolidate" }} + + + {{ isPurging ? "Purging..." : "Purge All" }} + + + + + + + Loading tracks... + + + + No tracks in cache. Add one using the form above! + + + + + + 🎵 + + {{ track.hits }} plays + + + + + {{ track.metadata?.title || "Unknown Title" }} + + + {{ track.metadata?.artist || "Unknown Artist" }} + + + {{ track.metadata.album }} + + {{ track.pk }} + + + {{ formatDuration(track.metadata.duration_ms) }} + + + {{ formatSampleRate(track.metadata.sample_rate) }} + + + {{ formatBitrate(track.metadata.bitrate) }} + + + + {{ track.collection }} + + + Last used: {{ formatDate(track.last_used) }} + + + + + ▶️ + + + {{ deletingTracks.has(track.pk) ? "..." : "🗑️" }} + + + + + + + + + ✕ + + 🎵 + Track Details + + + + Metadata + Title: {{ selectedTrack.metadata.title || "Unknown" }} + Artist: {{ selectedTrack.metadata.artist || "Unknown" }} + Album: {{ selectedTrack.metadata.album }} + Year: {{ selectedTrack.metadata.year }} + Genre: {{ selectedTrack.metadata.genre }} + Track: {{ selectedTrack.metadata.track_number }} + Duration: {{ formatDuration(selectedTrack.metadata.duration_ms) }} + Sample Rate: {{ formatSampleRate(selectedTrack.metadata.sample_rate) }} + Bitrate: {{ formatBitrate(selectedTrack.metadata.bitrate) }} + Channels: {{ selectedTrack.metadata.channels }} + + + Cache Info + PK: {{ selectedTrack.pk }} + Source URL: {{ selectedTrack.source_url }} + Hits: {{ selectedTrack.hits }} + Collection: {{ selectedTrack.collection }} + Last Used: {{ formatDate(selectedTrack.last_used) }} + + + + ▶️ Play + + + ⬇️ Download + + + 📋 Copy URL + + + 🗑️ Delete + + + + + + + + + + + + + + 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`; +}
{{ addError }}
{{ addSuccess }}
Title: {{ selectedTrack.metadata.title || "Unknown" }}
Artist: {{ selectedTrack.metadata.artist || "Unknown" }}
Album: {{ selectedTrack.metadata.album }}
Year: {{ selectedTrack.metadata.year }}
Genre: {{ selectedTrack.metadata.genre }}
Track: {{ selectedTrack.metadata.track_number }}
Duration: {{ formatDuration(selectedTrack.metadata.duration_ms) }}
Sample Rate: {{ formatSampleRate(selectedTrack.metadata.sample_rate) }}
Bitrate: {{ formatBitrate(selectedTrack.metadata.bitrate) }}
Channels: {{ selectedTrack.metadata.channels }}
PK: {{ selectedTrack.pk }}
Source URL: {{ selectedTrack.source_url }}
Hits: {{ selectedTrack.hits }}
Collection: {{ selectedTrack.collection }}
Last Used: {{ formatDate(selectedTrack.last_used) }}