Support downloading full album and artist discography through Subsonic API

This commit is contained in:
Deluan 2020-08-04 12:34:40 -04:00
parent f745b8d223
commit 2c370cae28
7 changed files with 124 additions and 11 deletions

View file

@ -52,8 +52,9 @@ func CreateSubsonicAPIRouter() (*subsonic.Router, error) {
transcoderTranscoder := transcoder.New() transcoderTranscoder := transcoder.New()
transcodingCache := core.NewTranscodingCache() transcodingCache := core.NewTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache) mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)
archiver := core.NewArchiver(dataStore)
players := engine.NewPlayers(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 return router, nil
} }

89
core/archiver.go Normal file
View 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
}

View file

@ -10,5 +10,6 @@ var Set = wire.NewSet(
NewMediaStreamer, NewMediaStreamer,
NewTranscodingCache, NewTranscodingCache,
NewImageCache, NewImageCache,
NewArchiver,
transcoder.New, transcoder.New,
) )

View file

@ -31,6 +31,7 @@ type Router struct {
Search engine.Search Search engine.Search
Users engine.Users Users engine.Users
Streamer core.MediaStreamer Streamer core.MediaStreamer
Archiver core.Archiver
Players engine.Players Players engine.Players
DataStore model.DataStore DataStore model.DataStore
@ -39,10 +40,10 @@ type Router struct {
func New(browser engine.Browser, artwork core.Artwork, listGenerator engine.ListGenerator, users engine.Users, func New(browser engine.Browser, artwork core.Artwork, listGenerator engine.ListGenerator, users engine.Users,
playlists engine.Playlists, scrobbler engine.Scrobbler, search engine.Search, 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, r := &Router{Browser: browser, Artwork: artwork, ListGenerator: listGenerator, Playlists: playlists,
Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer, Players: players, Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer, Archiver: archiver,
DataStore: ds} Players: players, DataStore: ds}
r.mux = r.routes() r.mux = r.routes()
return r return r
} }

View file

@ -7,16 +7,19 @@ import (
"github.com/deluan/navidrome/core" "github.com/deluan/navidrome/core"
"github.com/deluan/navidrome/log" "github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/server/subsonic/responses" "github.com/deluan/navidrome/server/subsonic/responses"
"github.com/deluan/navidrome/utils" "github.com/deluan/navidrome/utils"
) )
type StreamController struct { type StreamController struct {
streamer core.MediaStreamer streamer core.MediaStreamer
archiver core.Archiver
ds model.DataStore
} }
func NewStreamController(streamer core.MediaStreamer) *StreamController { func NewStreamController(streamer core.MediaStreamer, archiver core.Archiver, ds model.DataStore) *StreamController {
return &StreamController{streamer: streamer} return &StreamController{streamer: streamer, archiver: archiver, ds: ds}
} }
func (c *StreamController) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { 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 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 { if err != nil {
return nil, err 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 return nil, nil
} }

View file

@ -60,7 +60,9 @@ func initMediaRetrievalController(router *Router) *MediaRetrievalController {
func initStreamController(router *Router) *StreamController { func initStreamController(router *Router) *StreamController {
mediaStreamer := router.Streamer mediaStreamer := router.Streamer
streamController := NewStreamController(mediaStreamer) archiver := router.Archiver
dataStore := router.DataStore
streamController := NewStreamController(mediaStreamer, archiver, dataStore)
return streamController return streamController
} }
@ -82,5 +84,6 @@ var allProviders = wire.NewSet(
NewUsersController, NewUsersController,
NewMediaRetrievalController, NewMediaRetrievalController,
NewStreamController, 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"),
) )

View file

@ -17,7 +17,8 @@ var allProviders = wire.NewSet(
NewMediaRetrievalController, NewMediaRetrievalController,
NewStreamController, NewStreamController,
NewBookmarksController, 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 { func initSystemController(router *Router) *SystemController {