99 lines
3.2 KiB
Go
99 lines
3.2 KiB
Go
// 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.
|
|
//
|
|
// Divergences from internal/subsonic/types.go's contentTypeForFormat are
|
|
// intentional: opus→audio/ogg (library .opus files are Ogg-encapsulated, so
|
|
// this matches real library contents), aac→audio/aac (raw AAC is ADTS, not
|
|
// MP4, so audio/mp4 would mislead codec sniffers), and there is no "oga" case
|
|
// (we don't record that format). Don't "fix" these to match subsonic.
|
|
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"
|
|
}
|