summaryrefslogtreecommitdiff
path: root/player-server/internal/api
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-20 14:18:26 +0300
committerPaul Buetow <paul@buetow.org>2026-05-20 14:18:26 +0300
commit30e95cd41fc26cc2cbef658c47c690eb5e388b04 (patch)
tree6750d3fa31670c6b3c1f2efade0e1348de4ef2d5 /player-server/internal/api
parent0e5cefc30a4f6bc9d47069d6a3844027afc8bc55 (diff)
Extract app wiring from main.go into internal/app package (9a)
Move all dependency wiring, background worker startup, server lifecycle (Wire, StartBackgroundWorkers, RunServer, RunWithSignal, BuildLogger) into internal/app so cmd/player/main.go becomes thin: parse flags, load config, delegate to app.RunWithSignal. Updated main_test.go to call app.Wire and app.StartBackgroundWorkers directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Diffstat (limited to 'player-server/internal/api')
-rw-r--r--player-server/internal/api/handlers_file.go16
-rw-r--r--player-server/internal/api/handlers_media.go68
-rw-r--r--player-server/internal/api/handlers_playback_test.go6
-rw-r--r--player-server/internal/api/handlers_podcast_test.go22
-rw-r--r--player-server/internal/api/handlers_progress.go18
-rw-r--r--player-server/internal/api/handlers_share.go32
-rw-r--r--player-server/internal/api/handlers_share_test.go16
-rw-r--r--player-server/internal/api/handlers_test.go22
-rw-r--r--player-server/internal/api/integration_test.go22
-rw-r--r--player-server/internal/api/server.go107
10 files changed, 172 insertions, 157 deletions
diff --git a/player-server/internal/api/handlers_file.go b/player-server/internal/api/handlers_file.go
index 97de92e..f1e8343 100644
--- a/player-server/internal/api/handlers_file.go
+++ b/player-server/internal/api/handlers_file.go
@@ -41,14 +41,14 @@ func (s *Server) fileHandler(fn func(context.Context, int64, int64) (*service.Fi
}
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
- s.fileHandler(s.browseSvc.StreamMedia)(w, r)
+ s.fileHandler(s.media.Browse.StreamMedia)(w, r)
}
func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
id, err := pathID(r, "id")
@@ -56,7 +56,7 @@ func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- res, err := s.browseSvc.DownloadMedia(r.Context(), id, userIDFromContext(r))
+ res, err := s.media.Browse.DownloadMedia(r.Context(), id, userIDFromContext(r))
if err != nil {
if errors.Is(err, service.ErrNotFound) {
notFound(w)
@@ -77,15 +77,15 @@ func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleThumbnail(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
w.Header().Set("Cache-Control", "no-cache")
- s.fileHandler(s.browseSvc.GetThumbnail)(w, r)
+ s.fileHandler(s.media.Browse.GetThumbnail)(w, r)
}
func (s *Server) handleRegenThumbnail(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.writeSvc) {
+ if !requireService(w, s.media.Write) {
return
}
id, err := pathID(r, "id")
@@ -93,7 +93,7 @@ func (s *Server) handleRegenThumbnail(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- if err := s.writeSvc.RegenerateThumbnail(r.Context(), id, userIDFromContext(r)); err != nil {
+ if err := s.media.Write.RegenerateThumbnail(r.Context(), id, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
notFound(w)
return
diff --git a/player-server/internal/api/handlers_media.go b/player-server/internal/api/handlers_media.go
index 0570d66..055447e 100644
--- a/player-server/internal/api/handlers_media.go
+++ b/player-server/internal/api/handlers_media.go
@@ -19,10 +19,10 @@ const multipartFormMemoryLimit = 32 << 20
// ------------------------------------------------------------------
func (s *Server) handleListSets(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
- sets, err := s.browseSvc.ListSets(r.Context(), userIDFromContext(r))
+ sets, err := s.media.Browse.ListSets(r.Context(), userIDFromContext(r))
if err != nil {
handleError(w, err)
return
@@ -31,7 +31,7 @@ func (s *Server) handleListSets(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleGetSetCover(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
setID, err := pathID(r, "id")
@@ -40,7 +40,7 @@ func (s *Server) handleGetSetCover(w http.ResponseWriter, r *http.Request) {
return
}
folder := r.URL.Query().Get("folder")
- fr, err := s.browseSvc.GetSetCover(r.Context(), setID, folder, userIDFromContext(r))
+ fr, err := s.media.Browse.GetSetCover(r.Context(), setID, folder, userIDFromContext(r))
if err != nil {
if errors.Is(err, service.ErrNotFound) {
notFound(w)
@@ -58,7 +58,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.writeSvc) {
+ if !requireService(w, s.media.Write) {
return
}
setID, err := pathID(r, "id")
@@ -67,7 +67,7 @@ func (s *Server) handlePostSetCover(w http.ResponseWriter, r *http.Request) {
return
}
folder := r.URL.Query().Get("folder")
- if err := s.writeSvc.RegenerateSetCover(r.Context(), setID, folder, userIDFromContext(r)); err != nil {
+ if err := s.media.Write.RegenerateSetCover(r.Context(), setID, folder, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
notFound(w)
return
@@ -83,7 +83,7 @@ func (s *Server) handlePostSetCover(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleBrowseSet(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
setID, err := pathID(r, "id")
@@ -92,7 +92,7 @@ func (s *Server) handleBrowseSet(w http.ResponseWriter, r *http.Request) {
return
}
parent := r.URL.Query().Get("parent")
- result, err := s.browseSvc.BrowseSet(r.Context(), setID, userIDFromContext(r), parent)
+ result, err := s.media.Browse.BrowseSet(r.Context(), setID, userIDFromContext(r), parent)
if err != nil {
if errors.Is(err, service.ErrForbidden) {
forbidden(w, "forbidden")
@@ -105,7 +105,7 @@ func (s *Server) handleBrowseSet(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.writeSvc) {
+ if !requireService(w, s.media.Write) {
return
}
setID, err := pathID(r, "id")
@@ -134,7 +134,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
}
defer file.Close()
- media, err := s.writeSvc.UploadMedia(r.Context(), setID, userIDFromContext(r), fh.Filename, file, fh.Size)
+ media, err := s.media.Write.UploadMedia(r.Context(), setID, userIDFromContext(r), fh.Filename, file, fh.Size)
if err != nil {
if errors.Is(err, service.ErrNotFound) {
notFound(w)
@@ -224,7 +224,7 @@ func parseMediaListQuery(q url.Values) service.MediaQueryFilter {
}
func (s *Server) handleListMedia(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
path := r.URL.Path
@@ -238,7 +238,7 @@ func (s *Server) handleListMedia(w http.ResponseWriter, r *http.Request) {
maxDur := q.Get("max_duration")
start := time.Now()
filter := parseMediaListQuery(q)
- media, err := s.browseSvc.ListMedia(r.Context(), userIDFromContext(r), filter)
+ media, err := s.media.Browse.ListMedia(r.Context(), userIDFromContext(r), filter)
dur := time.Since(start)
if err != nil {
s.logger.Error("api list media failed", "path", path, "set_id", setID, "set_ids", setIDs, "search", search, "type", typ, "favorites", fav, "min_duration", minDur, "max_duration", maxDur, "duration", dur, "err", err)
@@ -250,7 +250,7 @@ func (s *Server) handleListMedia(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleGetMedia(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.media.Browse) {
return
}
id, err := pathID(r, "id")
@@ -258,7 +258,7 @@ func (s *Server) handleGetMedia(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- detail, err := s.browseSvc.GetMediaDetail(r.Context(), id, userIDFromContext(r))
+ detail, err := s.media.Browse.GetMediaDetail(r.Context(), id, userIDFromContext(r))
if err != nil {
handleError(w, err)
return
@@ -271,7 +271,7 @@ func (s *Server) handleGetMedia(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.favSvc) {
+ if !requireService(w, s.media.Favorite) {
return
}
id, err := pathID(r, "id")
@@ -279,7 +279,7 @@ func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- fav, err := s.favSvc.ToggleFavorite(r.Context(), userIDFromContext(r), id)
+ fav, err := s.media.Favorite.ToggleFavorite(r.Context(), userIDFromContext(r), id)
if err != nil {
handleError(w, err)
return
@@ -288,10 +288,10 @@ func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleListTags(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.tagSvc) {
+ if !requireService(w, s.media.Tag) {
return
}
- tags, err := s.tagSvc.ListTags(r.Context(), userIDFromContext(r))
+ tags, err := s.media.Tag.ListTags(r.Context(), userIDFromContext(r))
if err != nil {
handleError(w, err)
return
@@ -300,7 +300,7 @@ func (s *Server) handleListTags(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleAddTag(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.tagSvc) {
+ if !requireService(w, s.media.Tag) {
return
}
id, err := pathID(r, "id")
@@ -315,7 +315,7 @@ func (s *Server) handleAddTag(w http.ResponseWriter, r *http.Request) {
badRequest(w, "tag required")
return
}
- if err := s.tagSvc.AssignTag(r.Context(), id, userIDFromContext(r), req.Tag); err != nil {
+ if err := s.media.Tag.AssignTag(r.Context(), id, userIDFromContext(r), req.Tag); err != nil {
handleError(w, err)
return
}
@@ -323,7 +323,7 @@ func (s *Server) handleAddTag(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleRemoveTag(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.tagSvc) {
+ if !requireService(w, s.media.Tag) {
return
}
id, err := pathID(r, "id")
@@ -332,7 +332,7 @@ func (s *Server) handleRemoveTag(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid parameters")
return
}
- if err := s.tagSvc.RemoveTag(r.Context(), id, userIDFromContext(r), tagName); err != nil {
+ if err := s.media.Tag.RemoveTag(r.Context(), id, userIDFromContext(r), tagName); err != nil {
handleError(w, err)
return
}
@@ -340,7 +340,7 @@ func (s *Server) handleRemoveTag(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleSoftDelete(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.writeSvc) {
+ if !requireService(w, s.media.Write) {
return
}
id, err := pathID(r, "id")
@@ -348,7 +348,7 @@ func (s *Server) handleSoftDelete(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- if err := s.writeSvc.SoftDeleteMedia(r.Context(), id, userIDFromContext(r)); err != nil {
+ if err := s.media.Write.SoftDeleteMedia(r.Context(), id, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
notFound(w)
return
@@ -364,7 +364,7 @@ func (s *Server) handleSoftDelete(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.writeSvc) {
+ if !requireService(w, s.media.Write) {
return
}
id, err := pathID(r, "id")
@@ -372,7 +372,7 @@ func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- if err := s.writeSvc.RestoreMedia(r.Context(), id, userIDFromContext(r)); err != nil {
+ if err := s.media.Write.RestoreMedia(r.Context(), id, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
notFound(w)
return
@@ -395,7 +395,7 @@ func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
// the client can decide whether to play natively or request a future transcoded
// variant. It performs no actual transcoding — only a DB lookup.
func (s *Server) handlePlaybackHints(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.playbackHintSvc) {
+ if !requireService(w, s.media.PlaybackHints) {
return
}
id, err := pathID(r, "id")
@@ -403,7 +403,7 @@ func (s *Server) handlePlaybackHints(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- hint, err := s.playbackHintSvc.GetPlaybackHint(r.Context(), id, userIDFromContext(r))
+ hint, err := s.media.PlaybackHints.GetPlaybackHint(r.Context(), id, userIDFromContext(r))
if err != nil {
handleError(w, err)
return
@@ -416,7 +416,7 @@ func (s *Server) handlePlaybackHints(w http.ResponseWriter, r *http.Request) {
// ------------------------------------------------------------------
func (s *Server) handleGetNote(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.noteSvc) {
+ if !requireService(w, s.media.Note) {
return
}
id, err := pathID(r, "id")
@@ -424,7 +424,7 @@ func (s *Server) handleGetNote(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- note, err := s.noteSvc.GetNote(r.Context(), id, userIDFromContext(r))
+ note, err := s.media.Note.GetNote(r.Context(), id, userIDFromContext(r))
if err != nil {
handleError(w, err)
return
@@ -437,7 +437,7 @@ func (s *Server) handleGetNote(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleUpsertNote(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.noteSvc) {
+ if !requireService(w, s.media.Note) {
return
}
id, err := pathID(r, "id")
@@ -453,7 +453,7 @@ func (s *Server) handleUpsertNote(w http.ResponseWriter, r *http.Request) {
return
}
note := &model.Note{MediaID: id, UserID: userIDFromContext(r), Content: req.Content}
- if err := s.noteSvc.UpsertNote(r.Context(), note); err != nil {
+ if err := s.media.Note.UpsertNote(r.Context(), note); err != nil {
handleError(w, err)
return
}
@@ -461,7 +461,7 @@ func (s *Server) handleUpsertNote(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleDeleteNote(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.noteSvc) {
+ if !requireService(w, s.media.Note) {
return
}
id, err := pathID(r, "id")
@@ -469,7 +469,7 @@ func (s *Server) handleDeleteNote(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- if err := s.noteSvc.DeleteNote(r.Context(), id, userIDFromContext(r)); err != nil {
+ if err := s.media.Note.DeleteNote(r.Context(), id, userIDFromContext(r)); err != nil {
handleError(w, err)
return
}
diff --git a/player-server/internal/api/handlers_playback_test.go b/player-server/internal/api/handlers_playback_test.go
index fcf0f80..bcd84d1 100644
--- a/player-server/internal/api/handlers_playback_test.go
+++ b/player-server/internal/api/handlers_playback_test.go
@@ -35,8 +35,10 @@ func newPlaybackTestServer(t *testing.T, store repository.Store, sm auth.Session
SessionManager: sm,
Config: &internal.Config{},
Services: ServerServices{
- Auth: authSvc,
- PlaybackHints: hintSvc,
+ Media: MediaServices{
+ PlaybackHints: hintSvc,
+ },
+ Auth: authSvc,
},
StaticFS: fs,
MediaStreamer: service.NewMediaStreamer(nil, ""),
diff --git a/player-server/internal/api/handlers_podcast_test.go b/player-server/internal/api/handlers_podcast_test.go
index 73e76e2..3dae97b 100644
--- a/player-server/internal/api/handlers_podcast_test.go
+++ b/player-server/internal/api/handlers_podcast_test.go
@@ -57,16 +57,18 @@ func newPodcastTestServer(t *testing.T, store repository.Store, hasher auth.Hash
SessionManager: sm,
Config: cfg,
Services: ServerServices{
- Browse: browseSvc,
- Write: writeSvc,
- Share: shareSvc,
- Tag: tagSvc,
- Favorite: favSvc,
- Note: noteSvc,
- Admin: adminSvc,
- Progress: progressSvc,
- Auth: authSvc,
- Podcast: podcastSvc,
+ Media: MediaServices{
+ Browse: browseSvc,
+ Write: writeSvc,
+ Share: shareSvc,
+ Tag: tagSvc,
+ Favorite: favSvc,
+ Note: noteSvc,
+ Progress: progressSvc,
+ },
+ Admin: adminSvc,
+ Auth: authSvc,
+ Podcast: podcastSvc,
},
StaticFS: fs,
MediaStreamer: service.NewMediaStreamer(nil, ""),
diff --git a/player-server/internal/api/handlers_progress.go b/player-server/internal/api/handlers_progress.go
index 0d82bb7..9eb2326 100644
--- a/player-server/internal/api/handlers_progress.go
+++ b/player-server/internal/api/handlers_progress.go
@@ -8,7 +8,7 @@ import (
)
func (s *Server) handleProgress(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.progressSvc) {
+ if !requireService(w, s.media.Progress) {
return
}
var req struct {
@@ -28,7 +28,7 @@ func (s *Server) handleProgress(w http.ResponseWriter, r *http.Request) {
badRequest(w, "session required")
return
}
- err := s.progressSvc.UpdateProgress(
+ err := s.media.Progress.UpdateProgress(
r.Context(),
sessionID,
userIDFromContext(r),
@@ -43,7 +43,7 @@ func (s *Server) handleProgress(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleBatchProgress(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.progressSvc) {
+ if !requireService(w, s.media.Progress) {
return
}
var req struct {
@@ -76,7 +76,7 @@ func (s *Server) handleBatchProgress(w http.ResponseWriter, r *http.Request) {
badRequest(w, "session required")
return
}
- if err := s.progressSvc.BatchUpdateProgress(r.Context(), sessionID, userIDFromContext(r), updates); err != nil {
+ if err := s.media.Progress.BatchUpdateProgress(r.Context(), sessionID, userIDFromContext(r), updates); err != nil {
handleError(w, err)
return
}
@@ -84,7 +84,7 @@ func (s *Server) handleBatchProgress(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleProgressStatus(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.progressSvc) {
+ if !requireService(w, s.media.Progress) {
return
}
var req struct {
@@ -103,9 +103,9 @@ func (s *Server) handleProgressStatus(w http.ResponseWriter, r *http.Request) {
var err error
switch req.Status {
case "finished":
- err = s.progressSvc.MarkFinished(r.Context(), userIDFromContext(r), req.MediaID)
+ err = s.media.Progress.MarkFinished(r.Context(), userIDFromContext(r), req.MediaID)
case "not_started":
- err = s.progressSvc.MarkNotStarted(r.Context(), userIDFromContext(r), req.MediaID)
+ err = s.media.Progress.MarkNotStarted(r.Context(), userIDFromContext(r), req.MediaID)
default:
badRequest(w, "invalid status")
return
@@ -118,10 +118,10 @@ func (s *Server) handleProgressStatus(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleInProgress(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.progressSvc) {
+ if !requireService(w, s.media.Progress) {
return
}
- media, err := s.progressSvc.ListInProgress(r.Context(), userIDFromContext(r))
+ media, err := s.media.Progress.ListInProgress(r.Context(), userIDFromContext(r))
if err != nil {
handleError(w, err)
return
diff --git a/player-server/internal/api/handlers_share.go b/player-server/internal/api/handlers_share.go
index ccf1632..8e63159 100644
--- a/player-server/internal/api/handlers_share.go
+++ b/player-server/internal/api/handlers_share.go
@@ -15,7 +15,7 @@ import (
// ------------------------------------------------------------------
func (s *Server) handleCreateShare(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
id, err := pathID(r, "id")
@@ -27,7 +27,7 @@ func (s *Server) handleCreateShare(w http.ResponseWriter, r *http.Request) {
// share-expiry semantics (e.g. assert that expiresAt is exactly
// ShareDefaultExpiryDays * 24h after the mock clock's T).
expiresAt := s.clk.Now().Add(time.Duration(s.cfg.ShareDefaultExpiryDays) * 24 * time.Hour)
- share, err := s.shareSvc.CreateShare(r.Context(), userIDFromContext(r), id, expiresAt)
+ share, err := s.media.Share.CreateShare(r.Context(), userIDFromContext(r), id, expiresAt)
if err != nil {
handleError(w, err)
return
@@ -36,7 +36,7 @@ func (s *Server) handleCreateShare(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleListShares(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
id, err := pathID(r, "id")
@@ -44,7 +44,7 @@ func (s *Server) handleListShares(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid media id")
return
}
- shares, err := s.shareSvc.ListShares(r.Context(), id, userIDFromContext(r))
+ shares, err := s.media.Share.ListShares(r.Context(), id, userIDFromContext(r))
if err != nil {
handleError(w, err)
return
@@ -53,7 +53,7 @@ func (s *Server) handleListShares(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
token := r.PathValue("token")
@@ -61,7 +61,7 @@ func (s *Server) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
badRequest(w, "token required")
return
}
- if err := s.shareSvc.RevokeShare(r.Context(), token, userIDFromContext(r)); err != nil {
+ if err := s.media.Share.RevokeShare(r.Context(), token, userIDFromContext(r)); err != nil {
handleError(w, err)
return
}
@@ -69,11 +69,11 @@ func (s *Server) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleSharePage(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
token := r.PathValue("token")
- res, err := s.shareSvc.GetSharedMedia(r.Context(), token)
+ res, err := s.media.Share.GetSharedMedia(r.Context(), token)
if err != nil || res == nil {
if err != nil && errors.Is(err, service.ErrShareExpired) {
http.Error(w, "gone", http.StatusGone)
@@ -115,11 +115,11 @@ func (s *Server) handleSharePage(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleShareThumbnail(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
token := r.PathValue("token")
- fr, err := s.shareSvc.GetSharedThumbnail(r.Context(), token)
+ fr, err := s.media.Share.GetSharedThumbnail(r.Context(), token)
if err != nil {
if errors.Is(err, service.ErrShareExpired) {
http.Error(w, "gone", http.StatusGone)
@@ -141,11 +141,11 @@ func (s *Server) handleShareThumbnail(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleShareStream(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
token := r.PathValue("token")
- res, err := s.shareSvc.StreamSharedMedia(r.Context(), token)
+ res, err := s.media.Share.StreamSharedMedia(r.Context(), token)
if err != nil {
if errors.Is(err, service.ErrShareExpired) {
http.Error(w, "gone", http.StatusGone)
@@ -166,11 +166,11 @@ func (s *Server) handleShareStream(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleShareDownload(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
token := r.PathValue("token")
- fr, err := s.shareSvc.StreamSharedMedia(r.Context(), token)
+ fr, err := s.media.Share.StreamSharedMedia(r.Context(), token)
if err != nil {
if errors.Is(err, service.ErrShareExpired) {
http.Error(w, "gone", http.StatusGone)
@@ -191,10 +191,10 @@ func (s *Server) handleShareDownload(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleMyShares(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.shareSvc) {
+ if !requireService(w, s.media.Share) {
return
}
- shares, err := s.shareSvc.ListMyShares(r.Context(), userIDFromContext(r))
+ shares, err := s.media.Share.ListMyShares(r.Context(), userIDFromContext(r))
if err != nil {
handleError(w, err)
return
diff --git a/player-server/internal/api/handlers_share_test.go b/player-server/internal/api/handlers_share_test.go
index 021e023..effdeb4 100644
--- a/player-server/internal/api/handlers_share_test.go
+++ b/player-server/internal/api/handlers_share_test.go
@@ -56,13 +56,15 @@ func TestCreateShare_UsesInjectedClock(t *testing.T) {
SessionManager: sm,
Config: cfg,
Services: ServerServices{
- Browse: ms,
- Write: ms,
- Share: ms,
- Tag: ms,
- Favorite: ms,
- Note: ms,
- Auth: authSvc,
+ Media: MediaServices{
+ Browse: ms,
+ Write: ms,
+ Share: ms,
+ Tag: ms,
+ Favorite: ms,
+ Note: ms,
+ },
+ Auth: authSvc,
},
StaticFS: newTestFS(map[string]string{"index.html": "x"}),
MediaStreamer: service.NewMediaStreamer(nil, ""),
diff --git a/player-server/internal/api/handlers_test.go b/player-server/internal/api/handlers_test.go
index 4f04f98..96c4552 100644
--- a/player-server/internal/api/handlers_test.go
+++ b/player-server/internal/api/handlers_test.go
@@ -81,16 +81,20 @@ func newTestServer(t *testing.T, store repository.Store, hasher auth.Hasher, sm
Hasher: hasher,
SessionManager: sm,
Config: cfg,
+ // Use the nested MediaServices group so tests wire all media-domain
+ // services in one block, mirroring the production ServerServices layout.
Services: ServerServices{
- Browse: browseSvc,
- Write: writeSvc,
- Share: shareSvc,
- Tag: tagSvc,
- Favorite: favSvc,
- Note: noteSvc,
- Admin: adminSvc,
- Progress: progressSvc,
- Auth: authSvc,
+ Media: MediaServices{
+ Browse: browseSvc,
+ Write: writeSvc,
+ Share: shareSvc,
+ Tag: tagSvc,
+ Favorite: favSvc,
+ Note: noteSvc,
+ Progress: progressSvc,
+ },
+ Admin: adminSvc,
+ Auth: authSvc,
},
StaticFS: fs,
MediaStreamer: mediaStreamer,
diff --git a/player-server/internal/api/integration_test.go b/player-server/internal/api/integration_test.go
index 08c0d86..9397de3 100644
--- a/player-server/internal/api/integration_test.go
+++ b/player-server/internal/api/integration_test.go
@@ -71,16 +71,18 @@ func newIntegrationServer(t *testing.T) *integrationEnv {
SessionManager: sm,
Config: cfg,
Services: ServerServices{
- Browse: buildBrowseMock(),
- Write: buildBrowseMock(),
- Share: buildBrowseMock(),
- Tag: buildBrowseMock(),
- Favorite: buildBrowseMock(),
- Note: buildBrowseMock(),
- Admin: buildAdminMock(),
- Progress: buildProgressMock(),
- Auth: authSvc,
- Podcast: &integrationPodcastService{},
+ Media: MediaServices{
+ Browse: buildBrowseMock(),
+ Write: buildBrowseMock(),
+ Share: buildBrowseMock(),
+ Tag: buildBrowseMock(),
+ Favorite: buildBrowseMock(),
+ Note: buildBrowseMock(),
+ Progress: buildProgressMock(),
+ },
+ Admin: buildAdminMock(),
+ Auth: authSvc,
+ Podcast: &integrationPodcastService{},
},
StaticFS: http.FS(staticFS),
MediaStreamer: service.NewMediaStreamer(nil, ""),
diff --git a/player-server/internal/api/server.go b/player-server/internal/api/server.go
index c2ee32c..fa2ca74 100644
--- a/player-server/internal/api/server.go
+++ b/player-server/internal/api/server.go
@@ -17,7 +17,24 @@ import (
"codeberg.org/snonux/player/internal/web"
)
+// MediaServices groups the media-domain service dependencies used by route
+// handlers. Keeping all media-related services in one sub-struct reduces the
+// width of Server/ServerServices and makes it easy to see which concerns belong
+// to the media vertical slice. If any service is nil its routes return 501.
+type MediaServices struct {
+ Browse service.MediaBrowseService
+ Write service.MediaWriteService
+ Share service.MediaShareService
+ Tag service.MediaTagService
+ Favorite service.MediaFavoriteService
+ Note service.MediaNoteService
+ Progress service.ProgressService
+ PlaybackHints service.PlaybackHintsService
+}
+
// Server holds HTTP handlers and dependencies.
+// Service dependencies are grouped into vertical slices (media, auth, admin,
+// podcast) to reduce the width of the struct and clarify ownership boundaries.
type Server struct {
store repository.Store
hasher auth.Hasher
@@ -27,41 +44,32 @@ type Server struct {
// expiry, session cookie Expires, API token expiry). Injected so tests
// can substitute a clock.MockClock and assert deterministic semantics
// instead of racing the wall clock.
- clk clock.Clock
- mux *http.ServeMux
- handler http.Handler
- browseSvc service.MediaBrowseService
- writeSvc service.MediaWriteService
- shareSvc service.MediaShareService
- tagSvc service.MediaTagService
- favSvc service.MediaFavoriteService
- noteSvc service.MediaNoteService
- adminSvc service.AdminService
- progressSvc service.ProgressService
- authSvc service.AuthService
- podcastSvc service.PodcastEpisodeService
- playbackHintSvc service.PlaybackHintsService
- streamer service.MediaStreamer
- staticFS http.FileSystem
- shareRenderer *web.SharePageRenderer
- logger *slog.Logger
- mw *Middleware
+ clk clock.Clock
+ mux *http.ServeMux
+ handler http.Handler
+ // media groups all media-domain services (browse, write, share, tags,
+ // favorites, notes, progress, playback hints) into a single vertical slice.
+ media MediaServices
+ authSvc service.AuthService
+ adminSvc service.AdminService
+ podcastSvc service.PodcastEpisodeService
+ streamer service.MediaStreamer
+ staticFS http.FileSystem
+ shareRenderer *web.SharePageRenderer
+ logger *slog.Logger
+ mw *Middleware
}
// ServerServices groups the optional service dependencies used by route handlers.
+// Media-related services are collected into the Media sub-struct to reduce
+// width and reflect the media vertical-slice boundary. Non-media services
+// (Auth, Admin, Podcast) remain as direct fields.
// If any service is nil, its respective routes return 501.
type ServerServices struct {
- Browse service.MediaBrowseService
- Write service.MediaWriteService
- Share service.MediaShareService
- Tag service.MediaTagService
- Favorite service.MediaFavoriteService
- Note service.MediaNoteService
- Admin service.AdminService
- Progress service.ProgressService
- Auth service.AuthService
- Podcast service.PodcastEpisodeService
- PlaybackHints service.PlaybackHintsService
+ Media MediaServices
+ Auth service.AuthService
+ Admin service.AdminService
+ Podcast service.PodcastEpisodeService
}
// ServerDeps contains the dependencies needed to construct a Server.
@@ -117,29 +125,24 @@ func NewServerWithLogger(deps ServerDeps, logger *slog.Logger) (*Server, error)
if deps.Clock == nil {
deps.Clock = clock.RealClock{}
}
+ // Populate the media vertical-slice sub-struct directly from the nested
+ // ServerServices.Media group so the Server never sees the flat list.
s := &Server{
- store: deps.Store,
- hasher: deps.Hasher,
- sm: deps.SessionManager,
- cfg: deps.Config,
- clk: deps.Clock,
- mux: http.NewServeMux(),
- browseSvc: deps.Services.Browse,
- writeSvc: deps.Services.Write,
- shareSvc: deps.Services.Share,
- tagSvc: deps.Services.Tag,
- favSvc: deps.Services.Favorite,
- noteSvc: deps.Services.Note,
- adminSvc: deps.Services.Admin,
- progressSvc: deps.Services.Progress,
- authSvc: deps.Services.Auth,
- podcastSvc: deps.Services.Podcast,
- playbackHintSvc: deps.Services.PlaybackHints,
- streamer: deps.MediaStreamer,
- staticFS: deps.StaticFS,
- shareRenderer: web.NewSharePageRenderer(deps.StaticFS),
- logger: logger,
- mw: NewMiddleware(deps.Services.Auth, deps.SessionManager),
+ store: deps.Store,
+ hasher: deps.Hasher,
+ sm: deps.SessionManager,
+ cfg: deps.Config,
+ clk: deps.Clock,
+ mux: http.NewServeMux(),
+ media: deps.Services.Media,
+ authSvc: deps.Services.Auth,
+ adminSvc: deps.Services.Admin,
+ podcastSvc: deps.Services.Podcast,
+ streamer: deps.MediaStreamer,
+ staticFS: deps.StaticFS,
+ shareRenderer: web.NewSharePageRenderer(deps.StaticFS),
+ logger: logger,
+ mw: NewMiddleware(deps.Services.Auth, deps.SessionManager),
}
s.routes()
s.handler = withCORS(s.cfg.CORSAllowedOrigins, s.mw.BootstrapRedirect(s.mux))