summaryrefslogtreecommitdiff
path: root/internal/service/write.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-07 00:12:15 +0300
committerPaul Buetow <paul@buetow.org>2026-05-07 00:12:15 +0300
commit3b195a54b4c844f23415092b4293ff10e4a72dff (patch)
tree2ded9ecd06335223836157f1641afc07a9f78dd8 /internal/service/write.go
parentd46b79d8926254c95b897bba4246f09ef7a9f462 (diff)
Task 21: move thumbnail regeneration to write service
Diffstat (limited to 'internal/service/write.go')
-rw-r--r--internal/service/write.go99
1 files changed, 99 insertions, 0 deletions
diff --git a/internal/service/write.go b/internal/service/write.go
index a83c7e1..b67e644 100644
--- a/internal/service/write.go
+++ b/internal/service/write.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
+ mrand "math/rand"
"os"
"path/filepath"
"strings"
@@ -97,6 +98,104 @@ func (s *writeService) UploadMedia(ctx context.Context, setID, userID int64, fil
return media, nil
}
+func (s *writeService) RegenerateThumbnail(ctx context.Context, mediaID, userID int64) error {
+ media, err := s.helper.verifyModifyAccess(ctx, mediaID, userID)
+ if err != nil {
+ return err
+ }
+ if media.Type != model.MediaTypeVideo && media.Type != model.MediaTypeImage {
+ return errors.New("thumbnails can only be generated for video and image files")
+ }
+
+ meta, err := s.prober.Probe(ctx, media.AbsPath)
+ if err != nil {
+ return fmt.Errorf("probe media: %w", err)
+ }
+
+ thumbDir := filepath.Join(filepath.Dir(media.AbsPath), ".thumbnails")
+ if err := os.MkdirAll(thumbDir, 0o755); err != nil {
+ return fmt.Errorf("mkdir thumbnails: %w", err)
+ }
+ thumbName := strings.TrimSuffix(filepath.Base(media.AbsPath), filepath.Ext(media.AbsPath)) + ".jpg"
+ thumbnailPath := filepath.Join(thumbDir, thumbName)
+
+ if err := s.thumbGen.Generate(ctx, media.AbsPath, thumbnailPath, meta.Duration); err != nil {
+ return fmt.Errorf("generate thumbnail: %w", err)
+ }
+
+ media.ThumbnailPath = thumbnailPath
+ if err := s.store.UpdateMedia(ctx, media); err != nil {
+ return fmt.Errorf("update media: %w", err)
+ }
+ return nil
+}
+
+func (s *writeService) RegenerateSetCover(ctx context.Context, setID int64, folder string, userID int64) error {
+ if err := s.helper.verifySetModifyAccess(ctx, setID, userID); err != nil {
+ return err
+ }
+
+ set, err := s.store.GetSetByID(ctx, setID)
+ if err != nil {
+ return fmt.Errorf("get set: %w", err)
+ }
+ if set == nil {
+ return ErrNotFound
+ }
+
+ media, err := s.store.ListMedia(ctx, repository.MediaFilter{SetID: &setID})
+ if err != nil {
+ return fmt.Errorf("list media: %w", err)
+ }
+
+ prefix := filepath.ToSlash(strings.Trim(folder, "/"))
+ var candidates []model.Media
+ for _, m := range media {
+ if m.DeletedAt != nil {
+ continue
+ }
+ rel := filepath.ToSlash(m.RelPath)
+ if prefix != "" {
+ if !strings.HasPrefix(rel, prefix+"/") {
+ continue
+ }
+ suffix := strings.TrimPrefix(rel, prefix+"/")
+ if strings.Contains(suffix, "/") {
+ continue
+ }
+ } else if strings.Contains(rel, "/") {
+ continue
+ }
+ if m.Type == model.MediaTypeVideo {
+ candidates = append(candidates, m)
+ }
+ }
+ if len(candidates) == 0 {
+ return errors.New("no video files available for cover")
+ }
+
+ candidate := candidates[0]
+ if len(candidates) > 1 {
+ candidate = candidates[mrand.Intn(len(candidates))]
+ }
+
+ baseDir := filepath.Join(s.mediaRoot, filepath.FromSlash(set.RootPath))
+ if prefix != "" {
+ baseDir = filepath.Join(baseDir, filepath.FromSlash(prefix))
+ }
+ coverPath := filepath.Join(filepath.Clean(baseDir), ".cover.jpg")
+ meta, err := s.prober.Probe(ctx, candidate.AbsPath)
+ if err != nil {
+ return fmt.Errorf("probe cover candidate: %w", err)
+ }
+
+ if err := s.thumbGen.Generate(ctx, candidate.AbsPath, coverPath, meta.Duration); err != nil {
+ return fmt.Errorf("generate cover: %w", err)
+ }
+
+ return nil
+}
+
func (s *writeService) saveUploadedMedia(ctx context.Context, setID int64, path string, data io.Reader, size int64) (*model.Media, error) {
f, err := os.Create(path)
if err != nil {