diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-07 00:12:15 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-07 00:12:15 +0300 |
| commit | 3b195a54b4c844f23415092b4293ff10e4a72dff (patch) | |
| tree | 2ded9ecd06335223836157f1641afc07a9f78dd8 /internal/service/write.go | |
| parent | d46b79d8926254c95b897bba4246f09ef7a9f462 (diff) | |
Task 21: move thumbnail regeneration to write service
Diffstat (limited to 'internal/service/write.go')
| -rw-r--r-- | internal/service/write.go | 99 |
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 { |
