// Package api — media endpoints serve raw bytes (cover art, audio streams). // The helpers below duplicate shape with internal/subsonic/stream.go by // design; the two packages are kept independent so subsonic can freeze as a // compatibility surface while /api evolves. See project memory // `project_subsonic_legacy.md` for the rationale. package api import ( "context" "os" "path/filepath" "strings" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // sidecarNames is the lookup order for cover art living next to audio files. // Matches common library conventions: cover.* wins over folder.*; JPEG wins // over PNG when both exist. var sidecarNames = []string{ "cover.jpg", "cover.jpeg", "cover.png", "folder.jpg", "folder.jpeg", "folder.png", } // findSidecarCover looks for a conventional cover image in the directory that // contains trackPath. Returns "" if no sidecar exists. func findSidecarCover(trackPath string) string { dir := filepath.Dir(trackPath) for _, name := range sidecarNames { candidate := filepath.Join(dir, name) if info, err := os.Stat(candidate); err == nil && !info.IsDir() { return candidate } } return "" } // resolveAlbumCoverPath returns the filesystem path to the album's cover art. // It prefers an explicit cover_art_path (set by the scanner in a future // milestone) and falls back to a sidecar next to the first track in the // album's directory. "" means no art was found. func resolveAlbumCoverPath(ctx context.Context, q *dbq.Queries, album dbq.Album) string { if album.CoverArtPath != nil && *album.CoverArtPath != "" { if _, err := os.Stat(*album.CoverArtPath); err == nil { return *album.CoverArtPath } } tracks, err := q.ListTracksByAlbum(ctx, album.ID) if err != nil || len(tracks) == 0 { return "" } return findSidecarCover(tracks[0].FilePath) } // audioContentType maps the short file_format recorded on tracks (mp3, flac, // ogg, opus, m4a, aac, wav) to a MIME type for the Content-Type header. // Unknown formats fall back to octet-stream so the browser downloads them // rather than attempting to decode. func audioContentType(format string) string { switch strings.ToLower(format) { case "mp3": return "audio/mpeg" case "flac": return "audio/flac" case "ogg", "opus": return "audio/ogg" case "m4a": return "audio/mp4" case "aac": return "audio/aac" case "wav": return "audio/wav" } return "application/octet-stream" } // imageContentType maps a file extension to a MIME type for cover art. // Unknown extensions fall back to octet-stream. func imageContentType(path string) string { switch strings.ToLower(filepath.Ext(path)) { case ".jpg", ".jpeg": return "image/jpeg" case ".png": return "image/png" case ".webp": return "image/webp" case ".gif": return "image/gif" } return "application/octet-stream" }