mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-01 03:18:13 -05:00
Compare commits
1 Commits
copilot/re
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05b5f5eb2 |
@@ -72,8 +72,7 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
||||
scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
watcher := scanner.GetWatcher(dataStore, scannerScanner)
|
||||
library := core.NewLibrary(dataStore, scannerScanner, watcher, broker)
|
||||
maintenance := core.NewMaintenance(dataStore)
|
||||
router := nativeapi.New(dataStore, share, playlists, insights, library, maintenance)
|
||||
router := nativeapi.New(dataStore, share, playlists, insights, library)
|
||||
return router
|
||||
}
|
||||
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
)
|
||||
|
||||
type Maintenance interface {
|
||||
// DeleteMissingFiles deletes specific missing files by their IDs
|
||||
DeleteMissingFiles(ctx context.Context, ids []string) error
|
||||
// DeleteAllMissingFiles deletes all files marked as missing
|
||||
DeleteAllMissingFiles(ctx context.Context) error
|
||||
// RefreshAlbums recalculates album attributes from media files
|
||||
RefreshAlbums(ctx context.Context, albumIDs []string) error
|
||||
}
|
||||
|
||||
type maintenanceService struct {
|
||||
ds model.DataStore
|
||||
}
|
||||
|
||||
func NewMaintenance(ds model.DataStore) Maintenance {
|
||||
return &maintenanceService{
|
||||
ds: ds,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *maintenanceService) DeleteMissingFiles(ctx context.Context, ids []string) error {
|
||||
return s.deleteMissing(ctx, ids)
|
||||
}
|
||||
|
||||
func (s *maintenanceService) DeleteAllMissingFiles(ctx context.Context) error {
|
||||
return s.deleteMissing(ctx, nil)
|
||||
}
|
||||
|
||||
// deleteMissing handles the deletion of missing files and triggers necessary cleanup operations
|
||||
func (s *maintenanceService) deleteMissing(ctx context.Context, ids []string) error {
|
||||
// Track affected album IDs before deletion for refresh
|
||||
affectedAlbumIDs, err := s.getAffectedAlbumIDs(ctx, ids)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error tracking affected albums for refresh", err)
|
||||
// Don't fail the operation, just log the warning
|
||||
}
|
||||
|
||||
// Delete missing files within a transaction
|
||||
err = s.ds.WithTx(func(tx model.DataStore) error {
|
||||
if len(ids) == 0 {
|
||||
_, err := tx.MediaFile(ctx).DeleteAllMissing()
|
||||
return err
|
||||
}
|
||||
return tx.MediaFile(ctx).DeleteMissing(ids)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error deleting missing tracks from DB", "ids", ids, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Run garbage collection to clean up orphaned records
|
||||
if err := s.ds.GC(ctx); err != nil {
|
||||
log.Error(ctx, "Error running GC after deleting missing tracks", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Refresh statistics in background
|
||||
s.refreshStatsAsync(ctx, affectedAlbumIDs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshAlbums recalculates album attributes (size, duration, song count, etc.) from media files.
|
||||
// It uses batch queries to minimize database round-trips for efficiency.
|
||||
func (s *maintenanceService) RefreshAlbums(ctx context.Context, albumIDs []string) error {
|
||||
if len(albumIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug(ctx, "Refreshing albums", "count", len(albumIDs))
|
||||
|
||||
// Process in chunks to avoid query size limits
|
||||
const chunkSize = 100
|
||||
for i := 0; i < len(albumIDs); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > len(albumIDs) {
|
||||
end = len(albumIDs)
|
||||
}
|
||||
chunk := albumIDs[i:end]
|
||||
|
||||
if err := s.refreshAlbumChunk(ctx, chunk); err != nil {
|
||||
return fmt.Errorf("refreshing album chunk: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug(ctx, "Successfully refreshed albums", "count", len(albumIDs))
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshAlbumChunk processes a single chunk of album IDs
|
||||
func (s *maintenanceService) refreshAlbumChunk(ctx context.Context, albumIDs []string) error {
|
||||
albumRepo := s.ds.Album(ctx)
|
||||
mfRepo := s.ds.MediaFile(ctx)
|
||||
|
||||
// Batch load existing albums
|
||||
albums, err := albumRepo.GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.id": albumIDs},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading albums: %w", err)
|
||||
}
|
||||
|
||||
// Create a map for quick lookup
|
||||
albumMap := make(map[string]*model.Album, len(albums))
|
||||
for i := range albums {
|
||||
albumMap[albums[i].ID] = &albums[i]
|
||||
}
|
||||
|
||||
// Batch load all media files for these albums
|
||||
mediaFiles, err := mfRepo.GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album_id": albumIDs},
|
||||
Sort: "album_id, path",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading media files: %w", err)
|
||||
}
|
||||
|
||||
// Group media files by album ID
|
||||
filesByAlbum := make(map[string]model.MediaFiles)
|
||||
for i := range mediaFiles {
|
||||
albumID := mediaFiles[i].AlbumID
|
||||
filesByAlbum[albumID] = append(filesByAlbum[albumID], mediaFiles[i])
|
||||
}
|
||||
|
||||
// Recalculate each album from its media files
|
||||
for albumID, oldAlbum := range albumMap {
|
||||
mfs, hasTracks := filesByAlbum[albumID]
|
||||
if !hasTracks {
|
||||
// Album has no tracks anymore, skip (will be cleaned up by GC)
|
||||
log.Debug(ctx, "Skipping album with no tracks", "albumID", albumID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Recalculate album from media files
|
||||
newAlbum := mfs.ToAlbum()
|
||||
|
||||
// Only update if something changed (avoid unnecessary writes)
|
||||
if !oldAlbum.Equals(newAlbum) {
|
||||
// Preserve original timestamps
|
||||
newAlbum.UpdatedAt = time.Now()
|
||||
newAlbum.CreatedAt = oldAlbum.CreatedAt
|
||||
|
||||
if err := albumRepo.Put(&newAlbum); err != nil {
|
||||
log.Error(ctx, "Error updating album during refresh", "albumID", albumID, err)
|
||||
// Continue with other albums instead of failing entirely
|
||||
continue
|
||||
}
|
||||
log.Trace(ctx, "Refreshed album", "albumID", albumID, "name", newAlbum.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAffectedAlbumIDs returns distinct album IDs from missing media files
|
||||
func (s *maintenanceService) getAffectedAlbumIDs(ctx context.Context, ids []string) ([]string, error) {
|
||||
var filters squirrel.Sqlizer = squirrel.Eq{"missing": true}
|
||||
if len(ids) > 0 {
|
||||
filters = squirrel.And{
|
||||
squirrel.Eq{"missing": true},
|
||||
squirrel.Eq{"id": ids},
|
||||
}
|
||||
}
|
||||
|
||||
mfs, err := s.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract unique album IDs
|
||||
albumIDMap := make(map[string]struct{}, len(mfs))
|
||||
for _, mf := range mfs {
|
||||
if mf.AlbumID != "" {
|
||||
albumIDMap[mf.AlbumID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
albumIDs := make([]string, 0, len(albumIDMap))
|
||||
for id := range albumIDMap {
|
||||
albumIDs = append(albumIDs, id)
|
||||
}
|
||||
|
||||
return albumIDs, nil
|
||||
}
|
||||
|
||||
// refreshStatsAsync refreshes artist and album statistics in background goroutines
|
||||
func (s *maintenanceService) refreshStatsAsync(ctx context.Context, affectedAlbumIDs []string) {
|
||||
// Refresh artist stats in background
|
||||
go func() {
|
||||
bgCtx := request.AddValues(context.Background(), ctx)
|
||||
if _, err := s.ds.Artist(bgCtx).RefreshStats(true); err != nil {
|
||||
log.Error(bgCtx, "Error refreshing artist stats after deleting missing files", err)
|
||||
} else {
|
||||
log.Debug(bgCtx, "Successfully refreshed artist stats after deleting missing files")
|
||||
}
|
||||
|
||||
// Refresh album stats in background if we have affected albums
|
||||
if len(affectedAlbumIDs) > 0 {
|
||||
if err := s.RefreshAlbums(bgCtx, affectedAlbumIDs); err != nil {
|
||||
log.Error(bgCtx, "Error refreshing album stats after deleting missing files", err)
|
||||
} else {
|
||||
log.Debug(bgCtx, "Successfully refreshed album stats after deleting missing files", "count", len(affectedAlbumIDs))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Maintenance", func() {
|
||||
var ds *tests.MockDataStore
|
||||
var mfRepo *extendedMediaFileRepo
|
||||
var service Maintenance
|
||||
var ctx context.Context
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user1", IsAdmin: true})
|
||||
|
||||
ds, mfRepo = createTestDataStore()
|
||||
service = NewMaintenance(ds)
|
||||
})
|
||||
|
||||
Describe("DeleteMissingFiles", func() {
|
||||
Context("with specific IDs", func() {
|
||||
It("deletes specific missing files", func() {
|
||||
// Setup: mock missing files with album IDs
|
||||
mfRepo.SetData(model.MediaFiles{
|
||||
{ID: "mf1", AlbumID: "album1", Missing: true},
|
||||
{ID: "mf2", AlbumID: "album2", Missing: true},
|
||||
})
|
||||
|
||||
err := service.DeleteMissingFiles(ctx, []string{"mf1", "mf2"})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mfRepo.deleteMissingCalled).To(BeTrue())
|
||||
Expect(mfRepo.deletedIDs).To(Equal([]string{"mf1", "mf2"}))
|
||||
})
|
||||
|
||||
It("returns error if deletion fails", func() {
|
||||
mfRepo.deleteMissingError = errors.New("delete failed")
|
||||
|
||||
err := service.DeleteMissingFiles(ctx, []string{"mf1"})
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("delete failed"))
|
||||
})
|
||||
|
||||
It("continues even if album tracking fails", func() {
|
||||
mfRepo.SetError(true)
|
||||
|
||||
err := service.DeleteMissingFiles(ctx, []string{"mf1"})
|
||||
|
||||
// Should not fail, just log warning
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mfRepo.deleteMissingCalled).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns error if GC fails", func() {
|
||||
mfRepo.SetData(model.MediaFiles{
|
||||
{ID: "mf1", AlbumID: "album1", Missing: true},
|
||||
})
|
||||
|
||||
// Create a wrapper that returns error on GC
|
||||
dsWithGCError := &mockDataStoreWithGCError{MockDataStore: ds}
|
||||
serviceWithError := NewMaintenance(dsWithGCError)
|
||||
|
||||
err := serviceWithError.DeleteMissingFiles(ctx, []string{"mf1"})
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("gc failed"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("album ID extraction", func() {
|
||||
It("extracts unique album IDs from missing files", func() {
|
||||
mfRepo.SetData(model.MediaFiles{
|
||||
{ID: "mf1", AlbumID: "album1", Missing: true},
|
||||
{ID: "mf2", AlbumID: "album1", Missing: true},
|
||||
{ID: "mf3", AlbumID: "album2", Missing: true},
|
||||
})
|
||||
|
||||
err := service.DeleteMissingFiles(ctx, []string{"mf1", "mf2", "mf3"})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("skips files without album IDs", func() {
|
||||
mfRepo.SetData(model.MediaFiles{
|
||||
{ID: "mf1", AlbumID: "", Missing: true},
|
||||
{ID: "mf2", AlbumID: "album1", Missing: true},
|
||||
})
|
||||
|
||||
err := service.DeleteMissingFiles(ctx, []string{"mf1", "mf2"})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("DeleteAllMissingFiles", func() {
|
||||
It("deletes all missing files", func() {
|
||||
mfRepo.SetData(model.MediaFiles{
|
||||
{ID: "mf1", AlbumID: "album1", Missing: true},
|
||||
{ID: "mf2", AlbumID: "album2", Missing: true},
|
||||
{ID: "mf3", AlbumID: "album3", Missing: true},
|
||||
})
|
||||
|
||||
err := service.DeleteAllMissingFiles(ctx)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error if deletion fails", func() {
|
||||
mfRepo.SetError(true)
|
||||
|
||||
err := service.DeleteAllMissingFiles(ctx)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("handles empty result gracefully", func() {
|
||||
mfRepo.SetData(model.MediaFiles{})
|
||||
|
||||
err := service.DeleteAllMissingFiles(ctx)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Test helper to create a mock DataStore with controllable behavior
|
||||
func createTestDataStore() (*tests.MockDataStore, *extendedMediaFileRepo) {
|
||||
ds := &tests.MockDataStore{}
|
||||
ds.MockedAlbum = tests.CreateMockAlbumRepo()
|
||||
ds.MockedArtist = tests.CreateMockArtistRepo()
|
||||
|
||||
// Create extended media file repo with DeleteMissing support
|
||||
mfRepo := &extendedMediaFileRepo{
|
||||
MockMediaFileRepo: tests.CreateMockMediaFileRepo(),
|
||||
}
|
||||
ds.MockedMediaFile = mfRepo
|
||||
|
||||
return ds, mfRepo
|
||||
}
|
||||
|
||||
// Extension of MockMediaFileRepo to add DeleteMissing method
|
||||
type extendedMediaFileRepo struct {
|
||||
*tests.MockMediaFileRepo
|
||||
deleteMissingCalled bool
|
||||
deletedIDs []string
|
||||
deleteMissingError error
|
||||
}
|
||||
|
||||
func (m *extendedMediaFileRepo) DeleteMissing(ids []string) error {
|
||||
m.deleteMissingCalled = true
|
||||
m.deletedIDs = ids
|
||||
if m.deleteMissingError != nil {
|
||||
return m.deleteMissingError
|
||||
}
|
||||
// Actually delete from the mock data
|
||||
for _, id := range ids {
|
||||
delete(m.Data, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wrapper to override GC method to return error
|
||||
type mockDataStoreWithGCError struct {
|
||||
*tests.MockDataStore
|
||||
}
|
||||
|
||||
func (ds *mockDataStoreWithGCError) GC(ctx context.Context) error {
|
||||
return errors.New("gc failed")
|
||||
}
|
||||
@@ -18,7 +18,6 @@ var Set = wire.NewSet(
|
||||
NewShare,
|
||||
NewPlaylists,
|
||||
NewLibrary,
|
||||
NewMaintenance,
|
||||
agents.GetAgents,
|
||||
external.NewProvider,
|
||||
wire.Bind(new(external.Agents), new(*agents.Agents)),
|
||||
|
||||
@@ -29,7 +29,7 @@ var _ = Describe("Config API", func() {
|
||||
conf.Server.DevUIShowConfig = true // Enable config endpoint for tests
|
||||
ds = &tests.MockDataStore{}
|
||||
auth.Init(ds)
|
||||
nativeRouter := New(ds, nil, nil, nil, core.NewMockLibraryService(), nil)
|
||||
nativeRouter := New(ds, nil, nil, nil, core.NewMockLibraryService())
|
||||
router = server.JWTVerifier(nativeRouter)
|
||||
|
||||
// Create test users
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
)
|
||||
|
||||
// User-library association endpoints (admin only)
|
||||
func (api *Router) addUserLibraryRoute(r chi.Router) {
|
||||
func (n *Router) addUserLibraryRoute(r chi.Router) {
|
||||
r.Route("/user/{id}/library", func(r chi.Router) {
|
||||
r.Use(parseUserIDMiddleware)
|
||||
r.Get("/", getUserLibraries(api.libs))
|
||||
r.Put("/", setUserLibraries(api.libs))
|
||||
r.Get("/", getUserLibraries(n.libs))
|
||||
r.Put("/", setUserLibraries(n.libs))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ var _ = Describe("Library API", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
ds = &tests.MockDataStore{}
|
||||
auth.Init(ds)
|
||||
nativeRouter := New(ds, nil, nil, nil, core.NewMockLibraryService(), nil)
|
||||
nativeRouter := New(ds, nil, nil, nil, core.NewMockLibraryService())
|
||||
router = server.JWTVerifier(nativeRouter)
|
||||
|
||||
// Create test users
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
@@ -63,32 +63,45 @@ func (r *missingRepository) EntityName() string {
|
||||
return "missing_files"
|
||||
}
|
||||
|
||||
func deleteMissingFiles(maintenance core.Maintenance) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
p := req.Params(r)
|
||||
ids, _ := p.Strings("id")
|
||||
|
||||
var err error
|
||||
func deleteMissingFiles(ds model.DataStore, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
p := req.Params(r)
|
||||
ids, _ := p.Strings("id")
|
||||
err := ds.WithTx(func(tx model.DataStore) error {
|
||||
if len(ids) == 0 {
|
||||
err = maintenance.DeleteAllMissingFiles(ctx)
|
||||
} else {
|
||||
err = maintenance.DeleteMissingFiles(ctx, ids)
|
||||
_, err := tx.MediaFile(ctx).DeleteAllMissing()
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ids) == 1 && errors.Is(err, model.ErrNotFound) {
|
||||
log.Warn(ctx, "Missing file not found", "id", ids[0])
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "failed to delete missing files", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeDeleteManyResponse(w, r, ids)
|
||||
return tx.MediaFile(ctx).DeleteMissing(ids)
|
||||
})
|
||||
if len(ids) == 1 && errors.Is(err, model.ErrNotFound) {
|
||||
log.Warn(ctx, "Missing file not found", "id", ids[0])
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error deleting missing tracks from DB", "ids", ids, err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = ds.GC(ctx)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error running GC after deleting missing tracks", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh artist stats in background after deleting missing files
|
||||
go func() {
|
||||
bgCtx := request.AddValues(context.Background(), r.Context())
|
||||
if _, err := ds.Artist(bgCtx).RefreshStats(true); err != nil {
|
||||
log.Error(bgCtx, "Error refreshing artist stats after deleting missing files", err)
|
||||
} else {
|
||||
log.Debug(bgCtx, "Successfully refreshed artist stats after deleting missing files")
|
||||
}
|
||||
}()
|
||||
|
||||
writeDeleteManyResponse(w, r, ids)
|
||||
}
|
||||
|
||||
var _ model.ResourceRepository = &missingRepository{}
|
||||
|
||||
@@ -22,71 +22,70 @@ import (
|
||||
|
||||
type Router struct {
|
||||
http.Handler
|
||||
ds model.DataStore
|
||||
share core.Share
|
||||
playlists core.Playlists
|
||||
insights metrics.Insights
|
||||
libs core.Library
|
||||
maintenance core.Maintenance
|
||||
ds model.DataStore
|
||||
share core.Share
|
||||
playlists core.Playlists
|
||||
insights metrics.Insights
|
||||
libs core.Library
|
||||
}
|
||||
|
||||
func New(ds model.DataStore, share core.Share, playlists core.Playlists, insights metrics.Insights, libraryService core.Library, maintenance core.Maintenance) *Router {
|
||||
r := &Router{ds: ds, share: share, playlists: playlists, insights: insights, libs: libraryService, maintenance: maintenance}
|
||||
func New(ds model.DataStore, share core.Share, playlists core.Playlists, insights metrics.Insights, libraryService core.Library) *Router {
|
||||
r := &Router{ds: ds, share: share, playlists: playlists, insights: insights, libs: libraryService}
|
||||
r.Handler = r.routes()
|
||||
return r
|
||||
}
|
||||
|
||||
func (api *Router) routes() http.Handler {
|
||||
func (n *Router) routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Public
|
||||
api.RX(r, "/translation", newTranslationRepository, false)
|
||||
n.RX(r, "/translation", newTranslationRepository, false)
|
||||
|
||||
// Protected
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(server.Authenticator(api.ds))
|
||||
r.Use(server.Authenticator(n.ds))
|
||||
r.Use(server.JWTRefresher)
|
||||
r.Use(server.UpdateLastAccessMiddleware(api.ds))
|
||||
api.R(r, "/user", model.User{}, true)
|
||||
api.R(r, "/song", model.MediaFile{}, false)
|
||||
api.R(r, "/album", model.Album{}, false)
|
||||
api.R(r, "/artist", model.Artist{}, false)
|
||||
api.R(r, "/genre", model.Genre{}, false)
|
||||
api.R(r, "/player", model.Player{}, true)
|
||||
api.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig)
|
||||
api.R(r, "/radio", model.Radio{}, true)
|
||||
api.R(r, "/tag", model.Tag{}, true)
|
||||
r.Use(server.UpdateLastAccessMiddleware(n.ds))
|
||||
n.R(r, "/user", model.User{}, true)
|
||||
n.R(r, "/song", model.MediaFile{}, false)
|
||||
n.R(r, "/album", model.Album{}, false)
|
||||
n.R(r, "/artist", model.Artist{}, false)
|
||||
n.R(r, "/genre", model.Genre{}, false)
|
||||
n.R(r, "/player", model.Player{}, true)
|
||||
n.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig)
|
||||
n.R(r, "/radio", model.Radio{}, true)
|
||||
n.R(r, "/tag", model.Tag{}, true)
|
||||
if conf.Server.EnableSharing {
|
||||
api.RX(r, "/share", api.share.NewRepository, true)
|
||||
n.RX(r, "/share", n.share.NewRepository, true)
|
||||
}
|
||||
|
||||
api.addPlaylistRoute(r)
|
||||
api.addPlaylistTrackRoute(r)
|
||||
api.addSongPlaylistsRoute(r)
|
||||
api.addQueueRoute(r)
|
||||
api.addMissingFilesRoute(r)
|
||||
api.addKeepAliveRoute(r)
|
||||
api.addInsightsRoute(r)
|
||||
n.addPlaylistRoute(r)
|
||||
n.addPlaylistTrackRoute(r)
|
||||
n.addSongPlaylistsRoute(r)
|
||||
n.addQueueRoute(r)
|
||||
n.addMissingFilesRoute(r)
|
||||
n.addKeepAliveRoute(r)
|
||||
n.addInsightsRoute(r)
|
||||
|
||||
r.With(adminOnlyMiddleware).Group(func(r chi.Router) {
|
||||
api.addInspectRoute(r)
|
||||
api.addConfigRoute(r)
|
||||
api.addUserLibraryRoute(r)
|
||||
api.RX(r, "/library", api.libs.NewRepository, true)
|
||||
n.addInspectRoute(r)
|
||||
n.addConfigRoute(r)
|
||||
n.addUserLibraryRoute(r)
|
||||
n.RX(r, "/library", n.libs.NewRepository, true)
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (api *Router) R(r chi.Router, pathPrefix string, model interface{}, persistable bool) {
|
||||
func (n *Router) R(r chi.Router, pathPrefix string, model interface{}, persistable bool) {
|
||||
constructor := func(ctx context.Context) rest.Repository {
|
||||
return api.ds.Resource(ctx, model)
|
||||
return n.ds.Resource(ctx, model)
|
||||
}
|
||||
api.RX(r, pathPrefix, constructor, persistable)
|
||||
n.RX(r, pathPrefix, constructor, persistable)
|
||||
}
|
||||
|
||||
func (api *Router) RX(r chi.Router, pathPrefix string, constructor rest.RepositoryConstructor, persistable bool) {
|
||||
func (n *Router) RX(r chi.Router, pathPrefix string, constructor rest.RepositoryConstructor, persistable bool) {
|
||||
r.Route(pathPrefix, func(r chi.Router) {
|
||||
r.Get("/", rest.GetAll(constructor))
|
||||
if persistable {
|
||||
@@ -103,9 +102,9 @@ func (api *Router) RX(r chi.Router, pathPrefix string, constructor rest.Reposito
|
||||
})
|
||||
}
|
||||
|
||||
func (api *Router) addPlaylistRoute(r chi.Router) {
|
||||
func (n *Router) addPlaylistRoute(r chi.Router) {
|
||||
constructor := func(ctx context.Context) rest.Repository {
|
||||
return api.ds.Resource(ctx, model.Playlist{})
|
||||
return n.ds.Resource(ctx, model.Playlist{})
|
||||
}
|
||||
|
||||
r.Route("/playlist", func(r chi.Router) {
|
||||
@@ -115,7 +114,7 @@ func (api *Router) addPlaylistRoute(r chi.Router) {
|
||||
rest.Post(constructor)(w, r)
|
||||
return
|
||||
}
|
||||
createPlaylistFromM3U(api.playlists)(w, r)
|
||||
createPlaylistFromM3U(n.playlists)(w, r)
|
||||
})
|
||||
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
@@ -127,53 +126,55 @@ func (api *Router) addPlaylistRoute(r chi.Router) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *Router) addPlaylistTrackRoute(r chi.Router) {
|
||||
func (n *Router) addPlaylistTrackRoute(r chi.Router) {
|
||||
r.Route("/playlist/{playlistId}/tracks", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
getPlaylist(api.ds)(w, r)
|
||||
getPlaylist(n.ds)(w, r)
|
||||
})
|
||||
r.With(server.URLParamsMiddleware).Route("/", func(r chi.Router) {
|
||||
r.Delete("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
deleteFromPlaylist(api.ds)(w, r)
|
||||
deleteFromPlaylist(n.ds)(w, r)
|
||||
})
|
||||
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
addToPlaylist(api.ds)(w, r)
|
||||
addToPlaylist(n.ds)(w, r)
|
||||
})
|
||||
})
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Use(server.URLParamsMiddleware)
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
getPlaylistTrack(api.ds)(w, r)
|
||||
getPlaylistTrack(n.ds)(w, r)
|
||||
})
|
||||
r.Put("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
reorderItem(api.ds)(w, r)
|
||||
reorderItem(n.ds)(w, r)
|
||||
})
|
||||
r.Delete("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
deleteFromPlaylist(api.ds)(w, r)
|
||||
deleteFromPlaylist(n.ds)(w, r)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (api *Router) addSongPlaylistsRoute(r chi.Router) {
|
||||
func (n *Router) addSongPlaylistsRoute(r chi.Router) {
|
||||
r.With(server.URLParamsMiddleware).Get("/song/{id}/playlists", func(w http.ResponseWriter, r *http.Request) {
|
||||
getSongPlaylists(api.ds)(w, r)
|
||||
getSongPlaylists(n.ds)(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (api *Router) addQueueRoute(r chi.Router) {
|
||||
func (n *Router) addQueueRoute(r chi.Router) {
|
||||
r.Route("/queue", func(r chi.Router) {
|
||||
r.Get("/", getQueue(api.ds))
|
||||
r.Post("/", saveQueue(api.ds))
|
||||
r.Put("/", updateQueue(api.ds))
|
||||
r.Delete("/", clearQueue(api.ds))
|
||||
r.Get("/", getQueue(n.ds))
|
||||
r.Post("/", saveQueue(n.ds))
|
||||
r.Put("/", updateQueue(n.ds))
|
||||
r.Delete("/", clearQueue(n.ds))
|
||||
})
|
||||
}
|
||||
|
||||
func (api *Router) addMissingFilesRoute(r chi.Router) {
|
||||
func (n *Router) addMissingFilesRoute(r chi.Router) {
|
||||
r.Route("/missing", func(r chi.Router) {
|
||||
api.RX(r, "/", newMissingRepository(api.ds), false)
|
||||
r.Delete("/", deleteMissingFiles(api.maintenance))
|
||||
n.RX(r, "/", newMissingRepository(n.ds), false)
|
||||
r.Delete("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
deleteMissingFiles(n.ds, w, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,7 +198,7 @@ func writeDeleteManyResponse(w http.ResponseWriter, r *http.Request, ids []strin
|
||||
}
|
||||
}
|
||||
|
||||
func (api *Router) addInspectRoute(r chi.Router) {
|
||||
func (n *Router) addInspectRoute(r chi.Router) {
|
||||
if conf.Server.Inspect.Enabled {
|
||||
r.Group(func(r chi.Router) {
|
||||
if conf.Server.Inspect.MaxRequests > 0 {
|
||||
@@ -206,26 +207,26 @@ func (api *Router) addInspectRoute(r chi.Router) {
|
||||
conf.Server.Inspect.BacklogTimeout)
|
||||
r.Use(middleware.ThrottleBacklog(conf.Server.Inspect.MaxRequests, conf.Server.Inspect.BacklogLimit, time.Duration(conf.Server.Inspect.BacklogTimeout)))
|
||||
}
|
||||
r.Get("/inspect", inspect(api.ds))
|
||||
r.Get("/inspect", inspect(n.ds))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (api *Router) addConfigRoute(r chi.Router) {
|
||||
func (n *Router) addConfigRoute(r chi.Router) {
|
||||
if conf.Server.DevUIShowConfig {
|
||||
r.Get("/config/*", getConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *Router) addKeepAliveRoute(r chi.Router) {
|
||||
func (n *Router) addKeepAliveRoute(r chi.Router) {
|
||||
r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"response":"ok", "id":"keepalive"}`))
|
||||
})
|
||||
}
|
||||
|
||||
func (api *Router) addInsightsRoute(r chi.Router) {
|
||||
func (n *Router) addInsightsRoute(r chi.Router) {
|
||||
r.Get("/insights/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
last, success := api.insights.LastRun(r.Context())
|
||||
last, success := n.insights.LastRun(r.Context())
|
||||
if conf.Server.EnableInsightsCollector {
|
||||
_, _ = w.Write([]byte(`{"id":"insights_status", "lastRun":"` + last.Format("2006-01-02 15:04:05") + `", "success":` + strconv.FormatBool(success) + `}`))
|
||||
} else {
|
||||
|
||||
@@ -95,7 +95,7 @@ var _ = Describe("Song Endpoints", func() {
|
||||
mfRepo.SetData(testSongs)
|
||||
|
||||
// Create the native API router and wrap it with the JWTVerifier middleware
|
||||
nativeRouter := New(ds, nil, nil, nil, core.NewMockLibraryService(), nil)
|
||||
nativeRouter := New(ds, nil, nil, nil, core.NewMockLibraryService())
|
||||
router = server.JWTVerifier(nativeRouter)
|
||||
w = httptest.NewRecorder()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user