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

@@ -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
}