mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
Support downloading full album and artist discography through Subsonic API
This commit is contained in:
parent
f745b8d223
commit
2c370cae28
7 changed files with 124 additions and 11 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
89
core/archiver.go
Normal file
89
core/archiver.go
Normal file
|
@ -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
|
||||
}
|
|
@ -10,5 +10,6 @@ var Set = wire.NewSet(
|
|||
NewMediaStreamer,
|
||||
NewTranscodingCache,
|
||||
NewImageCache,
|
||||
NewArchiver,
|
||||
transcoder.New,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue