From 74de06430a7a2912523db0cb25e066ec9ac103cb Mon Sep 17 00:00:00 2001 From: Senan Kelly Date: Sat, 22 Apr 2023 18:23:17 +0100 Subject: [PATCH] refactor: consolidate specid <-> filesystem mapping and always use abs paths (#309) --- cmd/gonic/gonic.go | 128 ++++++++++++------ db/model.go | 5 + jukebox/jukebox.go | 4 +- paths/paths.go | 61 --------- server/ctrlsubsonic/ctrl.go | 21 ++- server/ctrlsubsonic/ctrl_test.go | 5 +- server/ctrlsubsonic/handlers_common.go | 40 +++--- server/ctrlsubsonic/handlers_raw.go | 49 ++----- .../ctrlsubsonic/specidpaths/specidpaths.go | 77 +++++++++++ server/server.go | 15 +- 10 files changed, 232 insertions(+), 173 deletions(-) delete mode 100644 paths/paths.go create mode 100644 server/ctrlsubsonic/specidpaths/specidpaths.go diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index f8994e6..b5b97d3 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -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 +} diff --git a/db/model.go b/db/model.go index b552012..a6eca17 100644 --- a/db/model.go +++ b/db/model.go @@ -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 diff --git a/jukebox/jukebox.go b/jukebox/jukebox.go index 356d6a4..3a04aa0 100644 --- a/jukebox/jukebox.go +++ b/jukebox/jukebox.go @@ -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() diff --git a/paths/paths.go b/paths/paths.go deleted file mode 100644 index 34bc71e..0000000 --- a/paths/paths.go +++ /dev/null @@ -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 -} diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index cce0aea..fea6e77 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -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 diff --git a/server/ctrlsubsonic/ctrl_test.go b/server/ctrlsubsonic/ctrl_test.go index 034a9ac..ca33f12 100644 --- a/server/ctrlsubsonic/ctrl_test.go +++ b/server/ctrlsubsonic/ctrl_test.go @@ -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()} diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index 7de1b41..4853aa9 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -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 } diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 37cb8d2..5006314 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -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) } diff --git a/server/ctrlsubsonic/specidpaths/specidpaths.go b/server/ctrlsubsonic/specidpaths/specidpaths.go new file mode 100644 index 0000000..e330b18 --- /dev/null +++ b/server/ctrlsubsonic/specidpaths/specidpaths.go @@ -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 +} diff --git a/server/server.go b/server/server.go index 5a20f12..a88d020 100644 --- a/server/server.go +++ b/server/server.go @@ -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,