mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-04 13:07:36 +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()
|
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
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,
|
NewMediaStreamer,
|
||||||
NewTranscodingCache,
|
NewTranscodingCache,
|
||||||
NewImageCache,
|
NewImageCache,
|
||||||
|
NewArchiver,
|
||||||
transcoder.New,
|
transcoder.New,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue