diff --git a/backend/workers/thumbnails.go b/backend/workers/thumbnails.go index e30d332..e0d65c0 100644 --- a/backend/workers/thumbnails.go +++ b/backend/workers/thumbnails.go @@ -91,6 +91,8 @@ func (m *ThumbnailManager) worker(id int) { err = generateImageThumbnail(fullSourcePath, destPath) } else if strings.HasPrefix(job.MimeType, "video/") { err = generateVideoThumbnail(fullSourcePath, destPath) + } else if strings.HasPrefix(job.MimeType, "audio/") { + err = generateAudioThumbnail(fullSourcePath, destPath) } else if strings.HasSuffix(job.FilePath, ".docx") || strings.HasSuffix(job.FilePath, ".pptx") || strings.HasSuffix(job.FilePath, ".xlsx") { err = generateOfficeThumbnail(job.FilePath, destPath, m.Config) } else { @@ -135,6 +137,25 @@ func generateVideoThumbnail(src, dest string) error { return nil } +func generateAudioThumbnail(src, dest string) error { + // Extract embedded cover art from audio file, scale to 256x256 + cmd := exec.Command("ffmpeg", "-y", "-i", src, "-an", "-vcodec", "mjpeg", "-vframes", "1", "-vf", "scale=256:256:force_original_aspect_ratio=decrease", dest) + + timer := time.AfterFunc(10*time.Second, func() { + if cmd.Process != nil { + cmd.Process.Kill() + } + }) + + err := cmd.Run() + timer.Stop() + + if err != nil { + return fmt.Errorf("ffmpeg audio cover extraction failed: %w", err) + } + return nil +} + func generateOfficeThumbnail(filePath, destPath string, cfg *config.Config) error { // Generate a download token for the Conversion API to fetch the file from our backend token, err := middleware.GenerateDownloadToken(0, "system_worker", cfg.JWTSecret) diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx index d6ce06c..af8d921 100644 --- a/frontend/src/components/FileExplorer.tsx +++ b/frontend/src/components/FileExplorer.tsx @@ -1173,7 +1173,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext) || file.mime_type?.startsWith('image/'); const isVideo = ['mp4', 'mkv', 'avi', 'mov', 'webm', 'ogg'].includes(ext) || file.mime_type?.startsWith('video/'); const isOffice = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(ext) || file.mime_type?.includes('officedocument'); - const isMedia = isImage || isVideo || isOffice; + const isAudio = ['mp3', 'flac', 'wav', 'm4a', 'wma', 'aac'].includes(ext) || file.mime_type?.startsWith('audio/'); + const isMedia = isImage || isVideo || isOffice || isAudio; if (ext === 'pdf') { return (