diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index e462c1102..cf8a4e69b 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -52,8 +52,9 @@ func CreateSubsonicAPIRouter() (*subsonic.Router, error) { transcoderTranscoder := transcoder.New() transcodingCache := core.NewTranscodingCache() mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache) + archiver := core.NewArchiver(dataStore) players := engine.NewPlayers(dataStore) - router := subsonic.New(browser, artwork, listGenerator, users, playlists, scrobbler, search, mediaStreamer, players, dataStore) + router := subsonic.New(browser, artwork, listGenerator, users, playlists, scrobbler, search, mediaStreamer, archiver, players, dataStore) return router, nil } diff --git a/core/archiver.go b/core/archiver.go new file mode 100644 index 000000000..0d3823e1d --- /dev/null +++ b/core/archiver.go @@ -0,0 +1,89 @@ +package core + +import ( + "archive/zip" + "context" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Masterminds/squirrel" + "github.com/deluan/navidrome/log" + "github.com/deluan/navidrome/model" +) + +type Archiver interface { + Zip(ctx context.Context, id string, w io.Writer) error +} + +func NewArchiver(ds model.DataStore) Archiver { + return &archiver{ds: ds} +} + +type archiver struct { + ds model.DataStore +} + +func (a *archiver) Zip(ctx context.Context, id string, out io.Writer) error { + mfs, err := a.loadTracks(ctx, id) + if err != nil { + log.Error(ctx, "Error loading media", "id", id, err) + return err + } + z := zip.NewWriter(out) + for _, mf := range mfs { + _ = a.addFileToZip(ctx, z, mf) + } + err = z.Close() + if err != nil { + log.Error(ctx, "Error closing zip file", "id", id, err) + } + return err +} + +func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile) error { + _, file := filepath.Split(mf.Path) + w, err := z.Create(fmt.Sprintf("%s/%s", mf.Album, file)) + if err != nil { + log.Error(ctx, "Error creating zip entry", "file", mf.Path, err) + return err + } + f, err := os.Open(mf.Path) + defer func() { _ = f.Close() }() + if err != nil { + log.Error(ctx, "Error opening file for zipping", "file", mf.Path, err) + return err + } + _, err = io.Copy(w, f) + if err != nil { + log.Error(ctx, "Error zipping file", "file", mf.Path, err) + return err + } + return nil +} + +func (a *archiver) loadTracks(ctx context.Context, id string) (model.MediaFiles, error) { + exist, err := a.ds.Album(ctx).Exists(id) + if err != nil { + return nil, err + } + if exist { + return a.ds.MediaFile(ctx).FindByAlbum(id) + } + exist, err = a.ds.Artist(ctx).Exists(id) + if err != nil { + return nil, err + } + if exist { + return a.ds.MediaFile(ctx).GetAll(model.QueryOptions{ + Sort: "album", + Filters: squirrel.Eq{"album_artist_id": id}, + }) + } + mf, err := a.ds.MediaFile(ctx).Get(id) + if err != nil { + return nil, err + } + return model.MediaFiles{*mf}, nil +} diff --git a/core/wire_providers.go b/core/wire_providers.go index 45c94da79..9fdb9dbbf 100644 --- a/core/wire_providers.go +++ b/core/wire_providers.go @@ -10,5 +10,6 @@ var Set = wire.NewSet( NewMediaStreamer, NewTranscodingCache, NewImageCache, + NewArchiver, transcoder.New, ) diff --git a/server/subsonic/api.go b/server/subsonic/api.go index ad01afb53..4db481381 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -31,6 +31,7 @@ type Router struct { Search engine.Search Users engine.Users Streamer core.MediaStreamer + Archiver core.Archiver Players engine.Players DataStore model.DataStore @@ -39,10 +40,10 @@ type Router struct { func New(browser engine.Browser, artwork core.Artwork, listGenerator engine.ListGenerator, users engine.Users, playlists engine.Playlists, scrobbler engine.Scrobbler, search engine.Search, - streamer core.MediaStreamer, players engine.Players, ds model.DataStore) *Router { + streamer core.MediaStreamer, archiver core.Archiver, players engine.Players, ds model.DataStore) *Router { r := &Router{Browser: browser, Artwork: artwork, ListGenerator: listGenerator, Playlists: playlists, - Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer, Players: players, - DataStore: ds} + Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer, Archiver: archiver, + Players: players, DataStore: ds} r.mux = r.routes() return r } diff --git a/server/subsonic/stream.go b/server/subsonic/stream.go index bc25bb405..9afc3c334 100644 --- a/server/subsonic/stream.go +++ b/server/subsonic/stream.go @@ -7,16 +7,19 @@ import ( "github.com/deluan/navidrome/core" "github.com/deluan/navidrome/log" + "github.com/deluan/navidrome/model" "github.com/deluan/navidrome/server/subsonic/responses" "github.com/deluan/navidrome/utils" ) type StreamController struct { streamer core.MediaStreamer + archiver core.Archiver + ds model.DataStore } -func NewStreamController(streamer core.MediaStreamer) *StreamController { - return &StreamController{streamer: streamer} +func NewStreamController(streamer core.MediaStreamer, archiver core.Archiver, ds model.DataStore) *StreamController { + return &StreamController{streamer: streamer, archiver: archiver, ds: ds} } func (c *StreamController) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { @@ -73,11 +76,25 @@ func (c *StreamController) Download(w http.ResponseWriter, r *http.Request) (*re return nil, err } - stream, err := c.streamer.NewStream(r.Context(), id, "raw", 0) + isTrack, err := c.ds.MediaFile(r.Context()).Exists(id) if err != nil { return nil, err } - http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) + if isTrack { + stream, err := c.streamer.NewStream(r.Context(), id, "raw", 0) + if err != nil { + return nil, err + } + + http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) + } else { + w.Header().Set("Content-Type", "application/zip") + err := c.archiver.Zip(r.Context(), id, w) + + if err != nil { + return nil, err + } + } return nil, nil } diff --git a/server/subsonic/wire_gen.go b/server/subsonic/wire_gen.go index 82b2b413f..9357f014f 100644 --- a/server/subsonic/wire_gen.go +++ b/server/subsonic/wire_gen.go @@ -60,7 +60,9 @@ func initMediaRetrievalController(router *Router) *MediaRetrievalController { func initStreamController(router *Router) *StreamController { mediaStreamer := router.Streamer - streamController := NewStreamController(mediaStreamer) + archiver := router.Archiver + dataStore := router.DataStore + streamController := NewStreamController(mediaStreamer, archiver, dataStore) return streamController } @@ -82,5 +84,6 @@ var allProviders = wire.NewSet( NewUsersController, NewMediaRetrievalController, NewStreamController, - NewBookmarksController, wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Scrobbler", "Search", "Streamer", "DataStore"), + NewBookmarksController, wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Scrobbler", + "Search", "Streamer", "Archiver", "DataStore"), ) diff --git a/server/subsonic/wire_injectors.go b/server/subsonic/wire_injectors.go index 3673eed3e..a533ef786 100644 --- a/server/subsonic/wire_injectors.go +++ b/server/subsonic/wire_injectors.go @@ -17,7 +17,8 @@ var allProviders = wire.NewSet( NewMediaRetrievalController, NewStreamController, NewBookmarksController, - wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Scrobbler", "Search", "Streamer", "DataStore"), + wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Scrobbler", + "Search", "Streamer", "Archiver", "DataStore"), ) func initSystemController(router *Router) *SystemController {