summaryrefslogtreecommitdiff
path: root/player-server/internal/thumb/resolver.go
diff options
context:
space:
mode:
Diffstat (limited to 'player-server/internal/thumb/resolver.go')
-rw-r--r--player-server/internal/thumb/resolver.go80
1 files changed, 80 insertions, 0 deletions
diff --git a/player-server/internal/thumb/resolver.go b/player-server/internal/thumb/resolver.go
new file mode 100644
index 0000000..a6614d6
--- /dev/null
+++ b/player-server/internal/thumb/resolver.go
@@ -0,0 +1,80 @@
+package thumb
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "codeberg.org/snonux/player/internal/model"
+)
+
+// ErrNotFound is returned by a Resolver when a media item has no thumbnail
+// (and no acceptable fallback) available. Callers translate this sentinel
+// to their domain-specific not-found error (e.g. service.ErrNotFound).
+var ErrNotFound = errors.New("thumbnail not found")
+
+// ResolvedFile describes a thumbnail file that has been located on (or
+// abstractly resolved from) some backing store. It contains everything a
+// caller needs to construct a HTTP file response without itself touching
+// the filesystem.
+type ResolvedFile struct {
+ Path string
+ FileName string
+ FileSize int64
+}
+
+// Resolver abstracts the "given a media item, return a thumbnail file" step.
+// Pulling this out of browseService lets the service layer avoid direct
+// os.Stat calls and lets tests inject a fake resolver instead of writing
+// temporary files.
+type Resolver interface {
+ // Resolve returns a ResolvedFile for the media's thumbnail. If the
+ // media has no thumbnail and no fallback is available, it returns
+ // ErrNotFound. Any other error indicates a real I/O problem.
+ Resolve(media *model.Media) (*ResolvedFile, error)
+}
+
+// FSResolver is the production Resolver that stats files on the local
+// filesystem. It encapsulates the original logic that lived in
+// browseService.GetThumbnail: prefer the generated thumbnail, fall back to
+// the original file for images so that cover.jpg-style assets still render.
+type FSResolver struct{}
+
+// NewFSResolver returns a Resolver backed by os.Stat against real paths.
+func NewFSResolver() *FSResolver {
+ return &FSResolver{}
+}
+
+// Resolve looks up the thumbnail file on disk for media, falling back to
+// the original AbsPath for image media when the generated thumbnail is
+// missing. A missing or empty thumbnail path with no fallback yields
+// ErrNotFound so callers can map it to a 404 cleanly.
+func (FSResolver) Resolve(media *model.Media) (*ResolvedFile, error) {
+ if media == nil {
+ return nil, ErrNotFound
+ }
+ if media.ThumbnailPath == "" {
+ return nil, ErrNotFound
+ }
+ if info, err := os.Stat(media.ThumbnailPath); err == nil {
+ return &ResolvedFile{
+ Path: media.ThumbnailPath,
+ FileName: filepath.Base(media.ThumbnailPath),
+ FileSize: info.Size(),
+ }, nil
+ } else if media.Type == model.MediaTypeImage {
+ // Generated thumbnail missing: for images, fall back to the
+ // original file so cover.jpg / folder.jpg etc. still render.
+ if info, statErr := os.Stat(media.AbsPath); statErr == nil {
+ return &ResolvedFile{
+ Path: media.AbsPath,
+ FileName: filepath.Base(media.AbsPath),
+ FileSize: info.Size(),
+ }, nil
+ }
+ return nil, fmt.Errorf("stat thumbnail: %w", err)
+ } else {
+ return nil, fmt.Errorf("stat thumbnail: %w", err)
+ }
+}