summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/api/handlers_file.go4
-rw-r--r--internal/api/handlers_media.go4
-rw-r--r--internal/service/browse.go106
-rw-r--r--internal/service/browse_test.go26
-rw-r--r--internal/service/media.go2
-rw-r--r--internal/service/service.go16
-rw-r--r--internal/service/write.go99
7 files changed, 126 insertions, 131 deletions
diff --git a/internal/api/handlers_file.go b/internal/api/handlers_file.go
index 3228962..0638603 100644
--- a/internal/api/handlers_file.go
+++ b/internal/api/handlers_file.go
@@ -85,7 +85,7 @@ func (s *Server) handleThumbnail(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleRegenThumbnail(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.writeSvc) {
return
}
id := pathID(r, "id")
@@ -93,7 +93,7 @@ func (s *Server) handleRegenThumbnail(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
return
}
- if err := s.browseSvc.RegenerateThumbnail(r.Context(), id, userIDFromContext(r)); err != nil {
+ if err := s.writeSvc.RegenerateThumbnail(r.Context(), id, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
return
diff --git a/internal/api/handlers_media.go b/internal/api/handlers_media.go
index c761bce..ee74d38 100644
--- a/internal/api/handlers_media.go
+++ b/internal/api/handlers_media.go
@@ -56,7 +56,7 @@ func (s *Server) handleGetSetCover(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handlePostSetCover(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.writeSvc) {
return
}
setID := pathID(r, "id")
@@ -65,7 +65,7 @@ func (s *Server) handlePostSetCover(w http.ResponseWriter, r *http.Request) {
return
}
folder := r.URL.Query().Get("folder")
- if err := s.browseSvc.RegenerateSetCover(r.Context(), setID, folder, userIDFromContext(r)); err != nil {
+ if err := s.writeSvc.RegenerateSetCover(r.Context(), setID, folder, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
return
diff --git a/internal/service/browse.go b/internal/service/browse.go
index 0df399b..7425d35 100644
--- a/internal/service/browse.go
+++ b/internal/service/browse.go
@@ -12,9 +12,7 @@ import (
"codeberg.org/snonux/player/internal/clock"
"codeberg.org/snonux/player/internal/model"
- "codeberg.org/snonux/player/internal/probe"
"codeberg.org/snonux/player/internal/repository"
- "codeberg.org/snonux/player/internal/thumb"
)
// browseService handles read-only browsing and media streaming operations.
@@ -22,19 +20,15 @@ type browseService struct {
store repository.BrowseServiceStore
clock clock.Clock
mediaRoot string
- thumbGen thumb.Generator
- prober probe.Prober
helper *accessHelper
}
// NewBrowseService creates a BrowseService.
-func NewBrowseService(store repository.BrowseServiceStore, clk clock.Clock, mediaRoot string, thumbGen thumb.Generator, prober probe.Prober, helper *accessHelper) MediaBrowseService {
+func NewBrowseService(store repository.BrowseServiceStore, clk clock.Clock, mediaRoot string, helper *accessHelper) MediaBrowseService {
return &browseService{
store: store,
clock: clk,
mediaRoot: mediaRoot,
- thumbGen: thumbGen,
- prober: prober,
helper: helper,
}
}
@@ -205,104 +199,6 @@ func (s *browseService) GetThumbnail(ctx context.Context, mediaID, userID int64)
return nil, fmt.Errorf("stat thumbnail: %w", err)
}
-func (s *browseService) 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 *browseService) 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
-}
-
// prefixForParent builds the slash-terminated prefix used for matching paths under parent.
func prefixForParent(parent string) string {
if parent == "" {
diff --git a/internal/service/browse_test.go b/internal/service/browse_test.go
index 7a05c9f..479d05e 100644
--- a/internal/service/browse_test.go
+++ b/internal/service/browse_test.go
@@ -15,14 +15,14 @@ func TestBrowseService_BrowseSet(t *testing.T) {
tmpDir := t.TempDir()
tests := []struct {
- name string
- set *model.Set
- setErr error
- media []model.Media
- mediaErr error
- wantErr bool
- wantMedia int
- wantFolders int
+ name string
+ set *model.Set
+ setErr error
+ media []model.Media
+ mediaErr error
+ wantErr bool
+ wantMedia int
+ wantFolders int
}{
{
name: "ok empty set",
@@ -39,10 +39,10 @@ func TestBrowseService_BrowseSet(t *testing.T) {
wantFolders: 0,
},
{
- name: "list media error",
- set: &model.Set{ID: 1, RootPath: "music"},
- mediaErr: errors.New("boom"),
- wantErr: true,
+ name: "list media error",
+ set: &model.Set{ID: 1, RootPath: "music"},
+ mediaErr: errors.New("boom"),
+ wantErr: true,
},
{
name: "set not found",
@@ -76,7 +76,7 @@ func TestBrowseService_BrowseSet(t *testing.T) {
},
},
}
- svc := NewBrowseService(store, clock.RealClock{}, tmpDir, nil, nil, &accessHelper{store: store})
+ svc := NewBrowseService(store, clock.RealClock{}, tmpDir, &accessHelper{store: store})
res, err := svc.BrowseSet(ctx, 1, 1, "")
if tt.wantErr {
if err == nil {
diff --git a/internal/service/media.go b/internal/service/media.go
index 6dbd5af..893f1a3 100644
--- a/internal/service/media.go
+++ b/internal/service/media.go
@@ -32,7 +32,7 @@ type mediaService struct {
func NewMediaService(store repository.MediaServiceStore, clk clock.Clock, mediaRoot string, thumbGen thumb.Generator, prober probe.Prober) MediaService {
helper := &accessHelper{store: store}
return &mediaService{
- MediaBrowseService: NewBrowseService(store, clk, mediaRoot, thumbGen, prober, helper),
+ MediaBrowseService: NewBrowseService(store, clk, mediaRoot, helper),
MediaWriteService: NewWriteService(store, clk, mediaRoot, thumbGen, prober, helper),
MediaShareService: NewShareService(store, clk, helper),
MediaTagService: NewTagService(store, helper),
diff --git a/internal/service/service.go b/internal/service/service.go
index 6a5f6e6..16cd5f1 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -54,10 +54,6 @@ type MediaBrowseService interface {
DownloadMedia(ctx context.Context, mediaID, userID int64) (*FileResult, error)
// GetThumbnail returns a media thumbnail for an authorized user.
GetThumbnail(ctx context.Context, mediaID, userID int64) (*FileResult, error)
- // RegenerateThumbnail refreshes a media item's thumbnail.
- RegenerateThumbnail(ctx context.Context, mediaID, userID int64) error
- // RegenerateSetCover refreshes a folder cover image for a set.
- RegenerateSetCover(ctx context.Context, setID int64, folder string, userID int64) error
// BrowseSet returns folders and media below a set path.
BrowseSet(ctx context.Context, setID, userID int64, parent string) (*BrowseResult, error)
// GetSetCover returns the cover image for a set folder.
@@ -72,6 +68,10 @@ type MediaWriteService interface {
RestoreMedia(ctx context.Context, mediaID, userID int64) error
// UploadMedia stores an uploaded media file in a set.
UploadMedia(ctx context.Context, setID, userID int64, filename string, data io.Reader, size int64) (*model.Media, error)
+ // RegenerateThumbnail refreshes a media item's thumbnail.
+ RegenerateThumbnail(ctx context.Context, mediaID, userID int64) error
+ // RegenerateSetCover refreshes a folder cover image for a set.
+ RegenerateSetCover(ctx context.Context, setID int64, folder string, userID int64) error
}
// BrowseFolder is a named folder within a set's directory tree.
@@ -82,10 +82,10 @@ type BrowseFolder struct {
// BrowseResult is the content of one directory inside a set.
type BrowseResult struct {
- CurrentPath string `json:"current_path"`
- Folders []BrowseFolder `json:"folders"`
- Media []model.Media `json:"media"`
- Episodes []model.PodcastEpisodeWithStatus `json:"episodes,omitempty"`
+ CurrentPath string `json:"current_path"`
+ Folders []BrowseFolder `json:"folders"`
+ Media []model.Media `json:"media"`
+ Episodes []model.PodcastEpisodeWithStatus `json:"episodes,omitempty"`
}
// SharedMediaView exposes only the metadata fields needed for a public share page.
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 {