refactor: consolidate specid <-> filesystem mapping and always use abs paths (#309)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
77
server/ctrlsubsonic/specidpaths/specidpaths.go
Normal file
77
server/ctrlsubsonic/specidpaths/specidpaths.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user