summaryrefslogtreecommitdiff
path: root/internal/service/media_write.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-01 20:14:19 +0300
committerPaul Buetow <paul@buetow.org>2026-05-01 20:14:19 +0300
commit74c2e0982f336ab135d8b63d26fa49bcd58e57a9 (patch)
tree689b764fadd7d6ca35598c131d0552b3d9374e3d /internal/service/media_write.go
parent4e35889c33491be33f8d557affb1c9d822372e7a (diff)
service: split media.go (745 lines) into 5 focused files
Split the monolithic media.go into cohesive files to fix KISS/SRP violation: - media_access.go: permission verification helpers (verifyAccess, verifyModifyAccess, verifySetModifyAccess) - media_browse.go: browsing, listing, streaming, thumbnails, set cover, tags and favorites - media_write.go: upload, create, soft-delete, restore - media_share.go: share token creation, validation, revocation, streaming via token - media_notes.go: note get/upsert/delete The shared mediaService struct, NewMediaService constructor, and common helpers (supported extensions, type guessing, thumbnail generation) remain in media.go. No functional changes — all tests pass.
Diffstat (limited to 'internal/service/media_write.go')
-rw-r--r--internal/service/media_write.go156
1 files changed, 156 insertions, 0 deletions
diff --git a/internal/service/media_write.go b/internal/service/media_write.go
new file mode 100644
index 0000000..ff316c7
--- /dev/null
+++ b/internal/service/media_write.go
@@ -0,0 +1,156 @@
+package service
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "codeberg.org/snonux/player/internal/model"
+)
+
+func (s *mediaService) SoftDeleteMedia(ctx context.Context, mediaID, userID int64) error {
+ _, err := s.verifyModifyAccess(ctx, mediaID, userID)
+ if err != nil {
+ return err
+ }
+ return s.store.SoftDeleteMedia(ctx, mediaID)
+}
+
+func (s *mediaService) RestoreMedia(ctx context.Context, mediaID, userID int64) error {
+ _, err := s.verifyModifyAccess(ctx, mediaID, userID)
+ if err != nil {
+ return err
+ }
+ return s.store.RestoreMedia(ctx, mediaID)
+}
+
+func (s *mediaService) uniqueFilename(dir, filename string) string {
+ filename = filepath.Base(filename)
+ if filename == "." || filename == ".." || filename == "" {
+ return ""
+ }
+ ext := filepath.Ext(filename)
+ base := strings.TrimSuffix(filename, ext)
+
+ candidate := filepath.Join(dir, filename)
+ if _, err := os.Stat(candidate); os.IsNotExist(err) {
+ return candidate
+ }
+
+ for i := 1; ; i++ {
+ candidate = filepath.Join(dir, fmt.Sprintf("%s(%d)%s", base, i, ext))
+ if _, err := os.Stat(candidate); os.IsNotExist(err) {
+ return candidate
+ }
+ }
+}
+
+func (s *mediaService) UploadMedia(ctx context.Context, setID, userID int64, filename string, data io.Reader, size int64) (*model.Media, error) {
+ set, err := s.store.GetSetByID(ctx, setID)
+ if err != nil {
+ return nil, fmt.Errorf("get set: %w", err)
+ }
+ if set == nil {
+ return nil, ErrNotFound
+ }
+
+ if err := s.verifySetModifyAccess(ctx, setID, userID); err != nil {
+ return nil, err
+ }
+
+ if !isSupportedExtension(filename) {
+ return nil, fmt.Errorf("%w: %s", ErrUnsupportedExtension, filepath.Ext(filename))
+ }
+
+ dir := filepath.Clean(filepath.Join(s.mediaRoot, set.RootPath))
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return nil, fmt.Errorf("mkdir: %w", err)
+ }
+
+ path := s.uniqueFilename(dir, filename)
+ if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dir)+string(filepath.Separator)) {
+ return nil, errors.New("invalid filename")
+ }
+
+ media, err := s.saveUploadedMedia(ctx, setID, path, data, size)
+ if err != nil {
+ os.Remove(path)
+ return nil, err
+ }
+
+ meta, err := s.probeMedia(ctx, path)
+ if err != nil {
+ os.Remove(path)
+ s.store.HardDeleteMedia(ctx, media.ID)
+ return nil, err
+ }
+ media.Duration = meta.Duration
+ media.Codec = meta.Codec
+ media.Resolution = meta.Resolution
+ media.Bitrate = meta.Bitrate
+
+ if media.Type == model.MediaTypeVideo {
+ if err := s.generateThumbnail(ctx, media, meta.Duration); err != nil {
+ os.Remove(path)
+ _ = s.store.HardDeleteMedia(ctx, media.ID)
+ return nil, err
+ }
+ }
+
+ if err := s.store.UpdateMedia(ctx, media); err != nil {
+ os.Remove(path)
+ _ = s.store.HardDeleteMedia(ctx, media.ID)
+ return nil, fmt.Errorf("update media metadata: %w", err)
+ }
+
+ return media, nil
+}
+
+// saveUploadedMedia writes data to disk and creates a minimal media row.
+func (s *mediaService) saveUploadedMedia(ctx context.Context, setID int64, path string, data io.Reader, size int64) (*model.Media, error) {
+ f, err := os.Create(path)
+ if err != nil {
+ return nil, fmt.Errorf("create file: %w", err)
+ }
+ defer f.Close()
+
+ n, err := io.Copy(f, data)
+ if err != nil {
+ return nil, fmt.Errorf("write file: %w", err)
+ }
+
+ now := s.clock.Now()
+ media := &model.Media{
+ SetID: setID,
+ RelPath: filepath.Base(path),
+ FileName: filepath.Base(path),
+ AbsPath: path,
+ Type: guessMediaType(filepath.Base(path)),
+ FileSizeBytes: n,
+ CreatedAt: now,
+ }
+ _ = size
+
+ id, err := s.store.CreateMedia(ctx, media)
+ if err != nil {
+ return nil, fmt.Errorf("create media: %w", err)
+ }
+ media.ID = id
+ return media, nil
+}
+
+// probeMedia extracts metadata from the uploaded file.
+func (s *mediaService) probeMedia(ctx context.Context, path string) (*model.Metadata, error) {
+ if s.prober == nil {
+ return &model.Metadata{}, nil
+ }
+ meta, err := s.prober.Probe(ctx, path)
+ if err != nil {
+ return nil, fmt.Errorf("probe media: %w", err)
+ }
+ return meta, nil
+}