summaryrefslogtreecommitdiff
path: root/player-server/internal/thumb/maker.go
blob: 52b15db53b2330ce104adef313dd85c6f09e9b2d (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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package thumb

import (
	"context"
	"fmt"
	"log/slog"
	"os"
)

// Maker creates thumbnail files for media items. The scanner uses it to
// off-load thumbnail creation policy (mkdir of the .thumbnails directory,
// canonical thumbnail path derivation, generator invocation, and the
// failure-tolerant warn-and-continue behaviour) so the scanner only
// orchestrates the higher-level scan and does not own thumbnail policy.
//
// MakeVideo / MakeImage return the resolved thumbnail path on success, or
// an empty string (and nil error) when the underlying generator failed —
// matching the scanner's pre-extraction "skip-and-continue" semantics.
// A non-nil error is reserved for genuine filesystem problems such as
// failing to create the .thumbnails directory.
type Maker interface {
	// MakeVideo produces a thumbnail for a video file. duration is the
	// video duration in seconds and is forwarded to the underlying
	// Generator so it can pick a sensible frame.
	MakeVideo(ctx context.Context, srcPath, parent string, duration float64) (string, error)
	// MakeImage produces a thumbnail for an image file. No duration is
	// applicable so 0 is passed to the underlying Generator.
	MakeImage(ctx context.Context, srcPath, parent string) (string, error)
}

// MakerFS is the small filesystem surface FSMaker needs. It mirrors the
// scanner's FS interface for the single operation FSMaker performs
// (creating the .thumbnails directory) so tests can inject an in-memory
// fake without depending on the scanner package.
type MakerFS interface {
	MkdirAll(path string, perm os.FileMode) error
}

// osMakerFS delegates MkdirAll to the standard library; it is used as
// the default when NewFSMaker is called without an explicit MakerFS.
type osMakerFS struct{}

func (osMakerFS) MkdirAll(path string, perm os.FileMode) error {
	return os.MkdirAll(path, perm)
}

// FSMaker is the production Maker implementation. It wraps a Generator
// (which performs the actual frame extraction) together with a small
// filesystem dependency for directory creation, and a logger so the
// "skipped thumbnail" warning is consistent with the scanner's previous
// behaviour.
type FSMaker struct {
	gen    Generator
	fs     MakerFS
	logger *slog.Logger
}

var _ Maker = (*FSMaker)(nil)

// NewFSMaker constructs an FSMaker around gen. A nil fs defaults to the
// real OS filesystem; a nil logger defaults to slog.Default(). gen must
// not be nil — callers always have a Generator available in production
// and tests can pass MockGenerator.
func NewFSMaker(gen Generator, fs MakerFS, logger *slog.Logger) *FSMaker {
	if fs == nil {
		fs = osMakerFS{}
	}
	if logger == nil {
		logger = slog.Default()
	}
	return &FSMaker{gen: gen, fs: fs, logger: logger}
}

// MakeVideo creates a thumbnail for a video file inside parent/.thumbnails/.
// The path layout mirrors ThumbnailPathFor so importers, the scanner, and
// the resolver all agree on where thumbnails live. Generator failures are
// logged and swallowed so a single bad file does not abort the scan.
func (m *FSMaker) MakeVideo(ctx context.Context, srcPath, parent string, duration float64) (string, error) {
	return m.make(ctx, srcPath, parent, duration)
}

// MakeImage creates a thumbnail for an image file. Duration is irrelevant
// for static images so 0 is forwarded to the Generator.
func (m *FSMaker) MakeImage(ctx context.Context, srcPath, parent string) (string, error) {
	return m.make(ctx, srcPath, parent, 0)
}

// make is the shared implementation behind MakeVideo / MakeImage. It
// ensures the .thumbnails directory exists, derives the canonical
// thumbnail path via ThumbnailPathFor, and delegates the actual frame
// extraction to the Generator. Generator errors are logged and reported
// as ("", nil) so the scanner can continue with the next file.
func (m *FSMaker) make(ctx context.Context, srcPath, parent string, duration float64) (string, error) {
	dir := ThumbnailDir(parent)
	if err := m.fs.MkdirAll(dir, 0o755); err != nil {
		return "", fmt.Errorf("mkdir thumbnails %q: %w", dir, err)
	}
	thumbnailPath := ThumbnailPathFor(srcPath, parent)
	if err := m.gen.Generate(ctx, srcPath, thumbnailPath, duration); err != nil {
		m.logger.Warn("thumb maker skipping thumbnail", "path", srcPath, "err", err)
		return "", nil
	}
	return thumbnailPath, nil
}