Files
silo-server/internal/playback/directplay.go
T
RXWatcherandClaude Opus 4.7 ac31c9d17e chore(audiobooks): snapshot in-progress work as pre-redesign baseline
Bundles 21 in-progress files spanning scanner, image cache, ABS API
handlers, podcast/audiobook scan, cover enrichment, metadata sync
task, and an interim pass at the audiobook detail page (DetailHero
adoption + fixed-bottom player). Committed as a single baseline so
the upcoming UI redesign (per the 2026-05-24 plan) starts from a
clean working tree. Not split into logical commits because this
captures pre-existing local work, not a curated change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:24:11 +02:00

76 lines
1.8 KiB
Go

package playback
import (
"net/http"
"os"
"path/filepath"
"strings"
)
// MimeFromExtension returns a MIME type based on the file extension.
// Falls back to "application/octet-stream" for unknown extensions.
func MimeFromExtension(name string) string {
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".mp4", ".m4v":
return "video/mp4"
case ".mkv":
return "video/x-matroska"
case ".webm":
return "video/webm"
case ".avi":
return "video/x-msvideo"
case ".mov":
return "video/quicktime"
case ".ts":
return "video/mp2t"
case ".flv":
return "video/x-flv"
case ".wmv":
return "video/x-ms-wmv"
case ".m4b", ".m4a":
return "audio/mp4"
case ".mp3":
return "audio/mpeg"
case ".flac":
return "audio/flac"
case ".opus", ".ogg":
return "audio/ogg"
case ".wav":
return "audio/wav"
case ".aac":
return "audio/aac"
default:
return "application/octet-stream"
}
}
// ServeDirectPlay serves a media file with HTTP byte-range support.
// Uses http.ServeContent for proper range handling, which supports
// Range requests, conditional requests (If-Modified-Since, If-None-Match),
// and Content-Type detection.
func ServeDirectPlay(w http.ResponseWriter, r *http.Request, filePath string) error {
f, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
http.Error(w, "file not found", http.StatusNotFound)
return err
}
http.Error(w, "internal server error", http.StatusInternalServerError)
return err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return err
}
// Set Content-Type explicitly so ServeContent does not sniff.
w.Header().Set("Content-Type", MimeFromExtension(filePath))
http.ServeContent(w, r, stat.Name(), stat.ModTime(), f)
return nil
}