summaryrefslogtreecommitdiff
path: root/player-server/internal/thumb/resolver.go
blob: a6614d6db1dbc71676a731a7e5d8520b560a6d9b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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)
	}
}