refactor: consolidate specid <-> filesystem mapping and always use abs paths (#309)

This commit is contained in:
Senan Kelly
2023-04-22 18:23:17 +01:00
committed by GitHub
parent efe72fc447
commit 74de06430a
10 changed files with 232 additions and 173 deletions

View File

@@ -4,6 +4,7 @@
package main
import (
"errors"
"flag"
"fmt"
"log"
@@ -21,38 +22,44 @@ import (
"go.senan.xyz/gonic"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/paths"
"go.senan.xyz/gonic/server"
"go.senan.xyz/gonic/server/ctrlsubsonic"
)
const (
cleanTimeDuration = 10 * time.Minute
cachePrefixAudio = "audio"
cachePrefixCovers = "covers"
)
func main() {
set := flag.NewFlagSet(gonic.Name, flag.ExitOnError)
confListenAddr := set.String("listen-addr", "0.0.0.0:4747", "listen address (optional)")
confTLSCert := set.String("tls-cert", "", "path to TLS certificate (optional)")
confTLSKey := set.String("tls-key", "", "path to TLS private key (optional)")
confPodcastPurgeAgeDays := set.Int("podcast-purge-age", 0, "age (in days) to purge podcast episodes if not accessed (optional)")
confPodcastPath := set.String("podcast-path", "", "path to podcasts")
confCachePath := set.String("cache-path", "", "path to cache")
var confMusicPaths pathAliases
set.Var(&confMusicPaths, "music-path", "path to music")
confDBPath := set.String("db-path", "gonic.db", "path to database (optional)")
confScanIntervalMins := set.Int("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)")
confScanAtStart := set.Bool("scan-at-start-enabled", false, "whether to perform an initial scan at startup (optional)")
confScanWatcher := set.Bool("scan-watcher-enabled", false, "whether to watch file system for new music and rescan (optional)")
confJukeboxEnabled := set.Bool("jukebox-enabled", false, "whether the subsonic jukebox api should be enabled (optional)")
confJukeboxMPVExtraArgs := set.String("jukebox-mpv-extra-args", "", "extra command line arguments to pass to the jukebox mpv daemon (optional)")
confPodcastPurgeAgeDays := set.Int("podcast-purge-age", 0, "age (in days) to purge podcast episodes if not accessed (optional)")
confProxyPrefix := set.String("proxy-prefix", "", "url path prefix to use if behind proxy. eg '/gonic' (optional)")
confGenreSplit := set.String("genre-split", "\n", "character or string to split genre tag data on (optional)")
confHTTPLog := set.Bool("http-log", true, "http request logging (optional)")
confGenreSplit := set.String("genre-split", "\n", "character or string to split genre tag data on (optional)")
confShowVersion := set.Bool("version", false, "show gonic version")
var confMusicPaths paths.MusicPaths
set.Var(&confMusicPaths, "music-path", "path to music")
_ = set.String("config-path", "", "path to config (optional)")
if err := ff.Parse(set, os.Args[1:],
@@ -68,40 +75,31 @@ func main() {
os.Exit(0)
}
log.Printf("starting gonic v%s\n", gonic.Version)
log.Printf("provided config\n")
set.VisitAll(func(f *flag.Flag) {
value := strings.ReplaceAll(f.Value.String(), "\n", "")
log.Printf(" %-25s %s\n", f.Name, value)
})
if len(confMusicPaths) == 0 {
log.Fatalf("please provide a music directory")
}
for _, confMusicPath := range confMusicPaths {
if _, err := os.Stat(confMusicPath.Path); os.IsNotExist(err) {
log.Fatalf("music directory %q not found", confMusicPath.Path)
var err error
for i, confMusicPath := range confMusicPaths {
if confMusicPaths[i].path, err = validatePath(confMusicPath.path); err != nil {
log.Fatalf("checking music dir %q: %v", confMusicPath.path, err)
}
}
if _, err := os.Stat(*confPodcastPath); os.IsNotExist(err) {
log.Fatal("please provide a valid podcast directory")
}
if *confCachePath == "" {
log.Fatal("please provide a cache directory")
if *confPodcastPath, err = validatePath(*confPodcastPath); err != nil {
log.Fatalf("checking podcast directory: %v", err)
}
if *confCachePath, err = validatePath(*confCachePath); err != nil {
log.Fatalf("checking cache directory: %v", err)
}
cacheDirAudio := path.Join(*confCachePath, cachePrefixAudio)
cacheDirCovers := path.Join(*confCachePath, cachePrefixCovers)
if _, err := os.Stat(cacheDirAudio); os.IsNotExist(err) {
if err := os.MkdirAll(cacheDirAudio, os.ModePerm); err != nil {
log.Fatalf("couldn't create audio cache path: %v\n", err)
}
cacheDirAudio := path.Join(*confCachePath, "audio")
cacheDirCovers := path.Join(*confCachePath, "covers")
if err := os.MkdirAll(cacheDirAudio, os.ModePerm); err != nil {
log.Fatalf("couldn't create audio cache path: %v\n", err)
}
if _, err := os.Stat(cacheDirCovers); os.IsNotExist(err) {
if err := os.MkdirAll(cacheDirCovers, os.ModePerm); err != nil {
log.Fatalf("couldn't create covers cache path: %v\n", err)
}
if err := os.MkdirAll(cacheDirCovers, os.ModePerm); err != nil {
log.Fatalf("couldn't create covers cache path: %v\n", err)
}
dbc, err := db.New(*confDBPath, db.DefaultOptions())
@@ -111,22 +109,27 @@ func main() {
defer dbc.Close()
err = dbc.Migrate(db.MigrationContext{
OriginalMusicPath: confMusicPaths[0].Path,
OriginalMusicPath: confMusicPaths[0].path,
})
if err != nil {
log.Panicf("error migrating database: %v\n", err)
}
var musicPaths []ctrlsubsonic.MusicPath
for _, pa := range confMusicPaths {
musicPaths = append(musicPaths, ctrlsubsonic.MusicPath{Alias: pa.alias, Path: pa.path})
}
proxyPrefixExpr := regexp.MustCompile(`^\/*(.*?)\/*$`)
*confProxyPrefix = proxyPrefixExpr.ReplaceAllString(*confProxyPrefix, `/$1`)
server, err := server.New(server.Options{
DB: dbc,
MusicPaths: confMusicPaths,
CachePath: filepath.Clean(cacheDirAudio),
MusicPaths: musicPaths,
CacheAudioPath: cacheDirAudio,
CoverCachePath: cacheDirCovers,
PodcastPath: *confPodcastPath,
ProxyPrefix: *confProxyPrefix,
GenreSplit: *confGenreSplit,
PodcastPath: filepath.Clean(*confPodcastPath),
HTTPLog: *confHTTPLog,
JukeboxEnabled: *confJukeboxEnabled,
})
@@ -134,6 +137,13 @@ func main() {
log.Panicf("error creating server: %v\n", err)
}
log.Printf("starting gonic v%s\n", gonic.Version)
log.Printf("provided config\n")
set.VisitAll(func(f *flag.Flag) {
value := strings.ReplaceAll(f.Value.String(), "\n", "")
log.Printf(" %-25s %s\n", f.Name, value)
})
var g run.Group
g.Add(server.StartHTTP(*confListenAddr, *confTLSCert, *confTLSKey))
g.Add(server.StartSessionClean(cleanTimeDuration))
@@ -160,3 +170,45 @@ func main() {
log.Panicf("error in job: %v", err)
}
}
const pathAliasSep = "->"
type pathAliases []pathAlias
type pathAlias struct{ alias, path string }
func (pa pathAliases) String() string {
var strs []string
for _, p := range pa {
if p.alias != "" {
strs = append(strs, fmt.Sprintf("%s %s %s", p.alias, pathAliasSep, p.path))
continue
}
strs = append(strs, p.path)
}
return strings.Join(strs, ", ")
}
func (pa *pathAliases) Set(value string) error {
if name, path, ok := strings.Cut(value, pathAliasSep); ok {
*pa = append(*pa, pathAlias{alias: name, path: path})
return nil
}
*pa = append(*pa, pathAlias{path: value})
return nil
}
var errNotExists = errors.New("path does not exist, please provide one")
func validatePath(p string) (string, error) {
if p == "" {
return "", errNotExists
}
if _, err := os.Stat(p); os.IsNotExist(err) {
return "", errNotExists
}
p, err := filepath.Abs(p)
if err != nil {
return "", fmt.Errorf("make absolute: %w", err)
}
return p, nil
}

View File

@@ -398,6 +398,7 @@ type PodcastEpisode struct {
Filename string
Status PodcastEpisodeStatus
Error string
AbsP string `gorm:"-"` // TODO: not this. instead we need some consistent way to get the AbsPath for both tracks and podcast episodes. or just files in general
}
func (pe *PodcastEpisode) AudioLength() int { return pe.Length }
@@ -423,6 +424,10 @@ func (pe *PodcastEpisode) MIME() string {
return mime.TypeByExtension(filepath.Ext(pe.Filename))
}
func (pe *PodcastEpisode) AbsPath() string {
return pe.AbsP
}
type Bookmark struct {
ID int `gorm:"primary_key"`
User *User

View File

@@ -125,10 +125,8 @@ func (j *Jukebox) SetPlaylist(items []string) error {
return item.Current
})
cwd, _ := os.Getwd()
currFilename, _ := filepath.Rel(cwd, current.Filename)
filteredItems, foundExistingTrack := filter(items, func(filename string) bool {
return filename != currFilename
return filename != current.Filename
})
tmp, cleanup, err := tmp()

View File

@@ -1,61 +0,0 @@
package paths
import (
"fmt"
"path/filepath"
"strings"
)
const sep = "->"
type MusicPaths []MusicPath
func (mps MusicPaths) String() string {
var strs []string
for _, path := range mps {
strs = append(strs, path.String())
}
return strings.Join(strs, ", ")
}
func (mps *MusicPaths) Set(value string) error {
alias, path, ok := strings.Cut(value, sep)
if !ok {
*mps = append(*mps, MusicPath{
Path: filepath.Clean(strings.TrimSpace(value)),
})
return nil
}
*mps = append(*mps, MusicPath{
Alias: strings.TrimSpace(alias),
Path: filepath.Clean(strings.TrimSpace(path)),
})
return nil
}
func (mps MusicPaths) Paths() []string {
var paths []string
for _, mp := range mps {
paths = append(paths, mp.Path)
}
return paths
}
type MusicPath struct {
Alias string
Path string
}
func (mp MusicPath) String() string {
if mp.Alias == "" {
return mp.Path
}
return fmt.Sprintf("%s %s %s", mp.Alias, sep, mp.Path)
}
func (mp MusicPath) DisplayAlias() string {
if mp.Alias == "" {
return filepath.Base(mp.Path)
}
return mp.Alias
}

View File

@@ -1,4 +1,4 @@
// Package ctrlsubsonic provides HTTP handlers for subsonic api
// Package ctrlsubsonic provides HTTP handlers for subsonic API
package ctrlsubsonic
import (
@@ -10,7 +10,6 @@ import (
"net/http"
"go.senan.xyz/gonic/jukebox"
"go.senan.xyz/gonic/paths"
"go.senan.xyz/gonic/podcasts"
"go.senan.xyz/gonic/scrobble"
"go.senan.xyz/gonic/server/ctrlbase"
@@ -27,12 +26,24 @@ const (
CtxParams
)
type MusicPath struct {
Alias, Path string
}
func PathsOf(paths []MusicPath) []string {
var r []string
for _, p := range paths {
r = append(r, p.Path)
}
return r
}
type Controller struct {
*ctrlbase.Controller
CachePath string
CoverCachePath string
MusicPaths []MusicPath
PodcastsPath string
MusicPaths paths.MusicPaths
CacheAudioPath string
CoverCachePath string
Jukebox *jukebox.Jukebox
Scrobblers []scrobble.Scrobbler
Podcasts *podcasts.Podcasts

View File

@@ -19,7 +19,6 @@ import (
"go.senan.xyz/gonic"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/mockfs"
"go.senan.xyz/gonic/paths"
"go.senan.xyz/gonic/server/ctrlbase"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/transcode"
@@ -159,9 +158,9 @@ func makec(t *testing.T, roots []string, audio bool) *Controller {
m.ScanAndClean()
m.ResetDates()
var absRoots paths.MusicPaths
var absRoots []MusicPath
for _, root := range roots {
absRoots = append(absRoots, paths.MusicPath{Alias: "", Path: filepath.Join(m.TmpDir(), root)})
absRoots = append(absRoots, MusicPath{Path: filepath.Join(m.TmpDir(), root)})
}
base := &ctrlbase.Controller{DB: m.DB()}

View File

@@ -6,7 +6,6 @@ import (
"log"
"math"
"net/http"
"os"
"path/filepath"
"time"
"unicode"
@@ -15,11 +14,11 @@ import (
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/multierr"
"go.senan.xyz/gonic/paths"
"go.senan.xyz/gonic/scanner"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
"go.senan.xyz/gonic/server/ctrlsubsonic/specidpaths"
)
func lowerUDecOrHash(in string) string {
@@ -30,7 +29,7 @@ func lowerUDecOrHash(in string) string {
return string(lower)
}
func getMusicFolder(musicPaths paths.MusicPaths, p params.Params) string {
func getMusicFolder(musicPaths []MusicPath, p params.Params) string {
idx, err := p.GetInt("musicFolderId")
if err != nil {
return ""
@@ -90,9 +89,12 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
func (c *Controller) ServeGetMusicFolders(r *http.Request) *spec.Response {
sub := spec.NewResponse()
sub.MusicFolders = &spec.MusicFolders{}
sub.MusicFolders.List = make([]*spec.MusicFolder, len(c.MusicPaths))
for i, path := range c.MusicPaths {
sub.MusicFolders.List[i] = &spec.MusicFolder{ID: i, Name: path.DisplayAlias()}
for i, mp := range c.MusicPaths {
alias := mp.Alias
if alias == "" {
alias = filepath.Base(mp.Path)
}
sub.MusicFolders.List = append(sub.MusicFolders.List, &spec.MusicFolder{ID: i, Name: alias})
}
return sub
}
@@ -289,17 +291,18 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
return sub
}
var errNotATrack = errors.New("not a track")
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response { // nolint:gocyclo
params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
trackPaths := func(ids []specid.ID) ([]string, error) {
var paths []string
for _, id := range ids {
var track db.Track
if err := c.DB.Preload("Album").Preload("TrackStar", "user_id=?", user.ID).Preload("TrackRating", "user_id=?", user.ID).First(&track, id.Value).Error; err != nil {
r, err := specidpaths.Locate(c.DB, c.PodcastsPath, id)
if err != nil {
return nil, fmt.Errorf("find track by id: %w", err)
}
paths = append(paths, track.AbsPath())
paths = append(paths, r.AbsPath())
}
return paths, nil
}
@@ -322,20 +325,15 @@ func (c *Controller) ServeJukebox(r *http.Request) *spec.Response { // nolint:go
return nil, fmt.Errorf("get playlist: %w", err)
}
for _, path := range playlist {
cwd, _ := os.Getwd()
path, _ = filepath.Rel(cwd, path)
var track db.Track
err := c.DB.
Preload("Album").
Where(`(albums.root_dir || ? || albums.left_path || albums.right_path || ? || tracks.filename)=?`,
string(filepath.Separator), string(filepath.Separator), path).
Joins(`JOIN albums ON tracks.album_id=albums.id`).
First(&track).
Error
file, err := specidpaths.Lookup(c.DB, PathsOf(c.MusicPaths), c.PodcastsPath, path)
if err != nil {
return nil, fmt.Errorf("fetch track: %w", err)
}
ret = append(ret, spec.NewTrackByTags(&track, track.Album))
track, ok := file.(*db.Track)
if !ok {
return nil, fmt.Errorf("%q: %w", path, errNotATrack)
}
ret = append(ret, spec.NewTrackByTags(track, track.Album))
}
return ret, nil
}

View File

@@ -17,6 +17,7 @@ import (
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
"go.senan.xyz/gonic/server/ctrlsubsonic/specidpaths"
"go.senan.xyz/gonic/transcode"
)
@@ -57,32 +58,6 @@ func streamGetTransPrefProfile(dbc *db.DB, userID int, client string) (mime stri
var errUnknownMediaType = fmt.Errorf("media type is unknown")
// TODO: there is a mismatch between abs paths for podcasts and music. if they were the same, db.AudioFile
// could have an AbsPath() method. and we wouldn't need to pass podcastsPath or return 3 values
func streamGetAudio(dbc *db.DB, podcastsPath string, user *db.User, id specid.ID) (db.AudioFile, string, error) {
switch t := id.Type; t {
case specid.Track:
var track db.Track
if err := dbc.Preload("Album").Preload("Artist").First(&track, id.Value).Error; err != nil {
return nil, "", fmt.Errorf("find track: %w", err)
}
if track.Artist != nil && track.Album != nil {
log.Printf("%s requests %s - %s from %s", user.Name, track.Artist.Name, track.TagTitle, track.Album.TagTitle)
}
return &track, path.Join(track.AbsPath()), nil
case specid.PodcastEpisode:
var podcast db.PodcastEpisode
if err := dbc.First(&podcast, id.Value).Error; err != nil {
return nil, "", fmt.Errorf("find podcast: %w", err)
}
return &podcast, path.Join(podcastsPath, podcast.Path), nil
default:
return nil, "", fmt.Errorf("%w: %q", errUnknownMediaType, t)
}
}
func streamUpdateStats(dbc *db.DB, userID, albumID int, playTime time.Time) error {
var play db.Play
err := dbc.
@@ -134,6 +109,7 @@ var (
errCoverEmpty = errors.New("no cover found for that folder")
)
// TODO: can we use specidpaths.Locate here?
func coverGetPath(dbc *db.DB, podcastPath string, id specid.ID) (string, error) {
switch id.Type {
case specid.Album:
@@ -281,12 +257,17 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
return spec.NewError(10, "please provide an `id` parameter")
}
file, audioPath, err := streamGetAudio(c.DB, c.PodcastsPath, user, id)
file, err := specidpaths.Locate(c.DB, c.PodcastsPath, id)
if err != nil {
return spec.NewError(70, "error finding media: %v", err)
return spec.NewError(0, "error looking up id %s: %v", id, err)
}
if track, ok := file.(*db.Track); ok && track.Album != nil {
audioFile, ok := file.(db.AudioFile)
if !ok {
return spec.NewError(0, "type of id does not contain audio")
}
if track, ok := audioFile.(*db.Track); ok && track.Album != nil {
defer func() {
if err := streamUpdateStats(c.DB, user.ID, track.Album.ID, time.Now()); err != nil {
log.Printf("error updating track status: %v", err)
@@ -294,7 +275,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
}()
}
if pe, ok := file.(*db.PodcastEpisode); ok {
if pe, ok := audioFile.(*db.PodcastEpisode); ok {
defer func() {
if err := streamUpdatePodcastEpisodeStats(c.DB, pe.ID); err != nil {
log.Printf("error updating podcast episode status: %v", err)
@@ -305,8 +286,8 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
maxBitRate, _ := params.GetInt("maxBitRate")
format, _ := params.Get("format")
if format == "raw" || maxBitRate >= file.AudioBitrate() {
http.ServeFile(w, r, audioPath)
if format == "raw" || maxBitRate >= audioFile.AudioBitrate() {
http.ServeFile(w, r, file.AbsPath())
return nil
}
@@ -315,7 +296,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
return spec.NewError(0, "couldn't find transcode preference: %v", err)
}
if pref == nil {
http.ServeFile(w, r, audioPath)
http.ServeFile(w, r, file.AbsPath())
return nil
}
@@ -330,7 +311,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
log.Printf("trancoding to %q with max bitrate %dk", profile.MIME(), profile.BitRate())
w.Header().Set("Content-Type", profile.MIME())
if err := c.Transcoder.Transcode(r.Context(), profile, audioPath, w); err != nil && !errors.Is(err, transcode.ErrFFmpegKilled) {
if err := c.Transcoder.Transcode(r.Context(), profile, file.AbsPath(), w); err != nil && !errors.Is(err, transcode.ErrFFmpegKilled) {
return spec.NewError(0, "error transcoding: %v", err)
}

View File

@@ -0,0 +1,77 @@
package specidpaths
import (
"errors"
"path/filepath"
"strings"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
)
var ErrNotAbs = errors.New("not abs")
var ErrNotFound = errors.New("not found")
type Result interface {
SID() *specid.ID
AbsPath() string
}
// Locate maps a specid to its location on the filesystem
func Locate(dbc *db.DB, podcastsPath string, id specid.ID) (Result, error) {
switch id.Type {
case specid.Track:
var track db.Track
if err := dbc.Preload("Album").Where("id=?", id.Value).Find(&track).Error; err == nil {
return &track, nil
}
case specid.PodcastEpisode:
var pe db.PodcastEpisode
if err := dbc.Where("id=?", id.Value).Find(&pe).Error; err == nil {
pe.AbsP = filepath.Join(podcastsPath, pe.Path)
return &pe, err
}
}
return nil, ErrNotFound
}
// Locate maps a location on the filesystem to a specid
func Lookup(dbc *db.DB, musicPaths []string, podcastsPath string, path string) (Result, error) {
if !filepath.IsAbs(path) {
return nil, ErrNotAbs
}
if strings.HasPrefix(path, podcastsPath) {
path, _ = filepath.Rel(podcastsPath, path)
var pe db.PodcastEpisode
if err := dbc.Where(`path=?`, path).First(&pe).Error; err == nil {
return &pe, nil
}
return nil, ErrNotFound
}
var musicPath string
for _, mp := range musicPaths {
if strings.HasPrefix(path, mp) {
musicPath = mp
}
}
if musicPath == "" {
return nil, ErrNotFound
}
relPath, _ := filepath.Rel(musicPath, path)
relDir, filename := filepath.Split(relPath)
leftPath, rightPath := filepath.Split(filepath.Clean(relDir))
q := dbc.
Where(`albums.root_dir=? AND albums.left_path=? AND albums.right_path=? AND tracks.filename=?`, musicPath, leftPath, rightPath, filename).
Joins(`JOIN albums ON tracks.album_id=albums.id`).
Preload("Album")
var track db.Track
if err := q.First(&track).Error; err == nil {
return &track, nil
}
return nil, ErrNotFound
}

View File

@@ -15,7 +15,6 @@ import (
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/jukebox"
"go.senan.xyz/gonic/paths"
"go.senan.xyz/gonic/podcasts"
"go.senan.xyz/gonic/scanner"
"go.senan.xyz/gonic/scanner/tags"
@@ -31,9 +30,9 @@ import (
type Options struct {
DB *db.DB
MusicPaths paths.MusicPaths
MusicPaths []ctrlsubsonic.MusicPath
PodcastPath string
CachePath string
CacheAudioPath string
CoverCachePath string
ProxyPrefix string
GenreSplit string
@@ -52,7 +51,7 @@ type Server struct {
func New(opts Options) (*Server, error) {
tagger := &tags.TagReader{}
scanner := scanner.New(opts.MusicPaths.Paths(), opts.DB, opts.GenreSplit, tagger)
scanner := scanner.New(ctrlsubsonic.PathsOf(opts.MusicPaths), opts.DB, opts.GenreSplit, tagger)
base := &ctrlbase.Controller{
DB: opts.DB,
ProxyPrefix: opts.ProxyPrefix,
@@ -85,7 +84,7 @@ func New(opts Options) (*Server, error) {
cacheTranscoder := transcode.NewCachingTranscoder(
transcode.NewFFmpegTranscoder(),
opts.CachePath,
opts.CacheAudioPath,
)
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast)
@@ -94,10 +93,10 @@ func New(opts Options) (*Server, error) {
}
ctrlSubsonic := &ctrlsubsonic.Controller{
Controller: base,
CachePath: opts.CachePath,
CoverCachePath: opts.CoverCachePath,
PodcastsPath: opts.PodcastPath,
MusicPaths: opts.MusicPaths,
PodcastsPath: opts.PodcastPath,
CacheAudioPath: opts.CacheAudioPath,
CoverCachePath: opts.CoverCachePath,
Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, &listenbrainz.Scrobbler{}},
Podcasts: podcast,
Transcoder: cacheTranscoder,