Compare commits
58 Commits
a0c9edc803
...
97464606c1
Author | SHA1 | Date |
---|---|---|
Deluan Quintão | 97464606c1 | |
Deluan | 2c9ccfe9e1 | |
Deluan | d0f5a653bc | |
Deluan | 44a3710608 | |
Deluan | 03dccbeed8 | |
Deluan | 876de16c58 | |
Deluan | 774bbbd97a | |
Deluan | dc1fdd5dcd | |
Deluan | d6b82154c9 | |
Deluan | bbfe8727ca | |
Deluan | 7a7827e50c | |
Deluan | 33e40d3eb6 | |
Deluan | 0e8042d344 | |
Deluan | 9596061464 | |
Deluan | e47ddd740a | |
Deluan | 8614a20f7e | |
Deluan | 336ea2ebac | |
Deluan | 7d73b9cdd4 | |
Deluan | 968df3c2a3 | |
Deluan | fd8bc1ae9c | |
Deluan | 03957f2050 | |
Deluan | 34f87bedc3 | |
Deluan | dbabb9f9bf | |
Deluan | a5b9edf50d | |
Deluan | 92a98cd558 | |
Deluan | 5d50558610 | |
vvdveen | 8bff1ad512 | |
crazygolem | 1e96b858a9 | |
Deluan | aafd5a952c | |
Deluan Quintão | d9cd5efd67 | |
Deluan | affa9c3478 | |
Anna Smith | 651a8fdaf9 | |
Deluan | f7fc17c0f7 | |
Deluan | f5df948eb1 | |
crazygolem | 18143fa5a1 | |
Tim | 8f9ed1b994 | |
dependabot[bot] | cf66594b6d | |
Deluan | ca005f6457 | |
Deluan | 6dcfe4d455 | |
Deluan | 7871d69adb | |
Deluan | 78182f40d6 | |
Deluan | 9aeaaa6610 | |
dependabot[bot] | 068c1e9a23 | |
Jonathan | bcec15dc13 | |
dependabot[bot] | cf6603e3ec | |
dependabot[bot] | 88d6757121 | |
Andrew Katsikas | c2f932c21c | |
Deluan | d968f7f530 | |
dependabot[bot] | 5fc78f120c | |
dependabot[bot] | 52dfa97262 | |
dependabot[bot] | c1eef058a4 | |
Deluan | 7f551a7932 | |
oftenoccur | bcb71b85c0 | |
Deluan | 8720bd154f | |
Cyrille | 699be19bb9 | |
looklose | 22cc9e0cd5 | |
dependabot[bot] | 6e36abdd62 | |
dependabot[bot] | e98c7374a9 |
|
@ -16,11 +16,13 @@ RUN chmod +x /navidrome
|
|||
|
||||
#####################################################
|
||||
### Build Final Image
|
||||
FROM alpine as release
|
||||
FROM alpine:3.18
|
||||
LABEL maintainer="deluan@navidrome.org"
|
||||
|
||||
# Install ffmpeg and output build config
|
||||
RUN apk add --no-cache ffmpeg
|
||||
# Install ffmpeg and mpv
|
||||
RUN apk add -U --no-cache ffmpeg mpv
|
||||
|
||||
# Show ffmpeg build info, for troubleshooting purposes
|
||||
RUN ffmpeg -buildconf
|
||||
|
||||
COPY --from=copy-binary /navidrome /app/
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
args: --timeout 2m
|
||||
|
||||
- name: Install goimports
|
||||
run: go install golang.org/x/tools/cmd/goimports
|
||||
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||
|
||||
- run: goimports -w `find . -name '*.go' | grep -v '_gen.go$'`
|
||||
- run: go mod tidy
|
||||
|
|
6
Makefile
6
Makefile
|
@ -111,6 +111,12 @@ single: warning-noui-build ##@Cross_Compilation Build binaries for a single supp
|
|||
goreleaser build --clean --snapshot -p 2 --single-target --id navidrome_${GOOS}_${GOARCH}
|
||||
.PHONY: single
|
||||
|
||||
docker: buildjs ##@Build Build Docker linux/amd64 image (tagged as `deluan/navidrome:develop`)
|
||||
GOOS=linux GOARCH=amd64 make single
|
||||
@echo "Building Docker image"
|
||||
docker build . --platform linux/amd64 -t deluan/navidrome:develop -f .github/workflows/pipeline.dockerfile
|
||||
.PHONY: docker
|
||||
|
||||
warning-noui-build:
|
||||
@echo "WARNING: This command does not build the frontend, it uses the latest built with 'make buildjs'"
|
||||
.PHONY: warning-noui-build
|
||||
|
|
16
cmd/root.go
16
cmd/root.go
|
@ -87,15 +87,15 @@ func runNavidrome() {
|
|||
|
||||
func startServer(ctx context.Context) func() error {
|
||||
return func() error {
|
||||
a := CreateServer(conf.Server.MusicFolder)
|
||||
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
|
||||
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
|
||||
a.MountRouter("Public Endpoints", consts.URLPathPublic, CreatePublicRouter())
|
||||
a := CreateServer(ctx)
|
||||
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter(ctx))
|
||||
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter(ctx))
|
||||
a.MountRouter("Public Endpoints", consts.URLPathPublic, CreatePublicRouter(ctx))
|
||||
if conf.Server.LastFM.Enabled {
|
||||
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
|
||||
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter(ctx))
|
||||
}
|
||||
if conf.Server.ListenBrainz.Enabled {
|
||||
a.MountRouter("ListenBrainz Auth", consts.URLPathNativeAPI+"/listenbrainz", CreateListenBrainzRouter())
|
||||
a.MountRouter("ListenBrainz Auth", consts.URLPathNativeAPI+"/listenbrainz", CreateListenBrainzRouter(ctx))
|
||||
}
|
||||
if conf.Server.Prometheus.Enabled {
|
||||
// blocking call because takes <1ms but useful if fails
|
||||
|
@ -120,7 +120,7 @@ func schedulePeriodicScan(ctx context.Context) func() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
scanner := GetScanner()
|
||||
scanner := GetScanner(ctx)
|
||||
schedulerInstance := scheduler.GetInstance()
|
||||
|
||||
log.Info("Scheduling periodic scan", "schedule", schedule)
|
||||
|
@ -192,6 +192,7 @@ func init() {
|
|||
rootCmd.Flags().Bool("enabletranscodingconfig", viper.GetBool("enabletranscodingconfig"), "enables transcoding configuration in the UI")
|
||||
rootCmd.Flags().String("transcodingcachesize", viper.GetString("transcodingcachesize"), "size of transcoding cache")
|
||||
rootCmd.Flags().String("imagecachesize", viper.GetString("imagecachesize"), "size of image (art work) cache. set to 0 to disable cache")
|
||||
rootCmd.Flags().String("albumplaycountmode", viper.GetString("albumplaycountmode"), "how to compute playcount for albums. absolute (default) or normalized")
|
||||
rootCmd.Flags().Bool("autoimportplaylists", viper.GetBool("autoimportplaylists"), "enable/disable .m3u playlist auto-import`")
|
||||
|
||||
rootCmd.Flags().Bool("prometheus.enabled", viper.GetBool("prometheus.enabled"), "enable/disable prometheus metrics endpoint`")
|
||||
|
@ -214,4 +215,5 @@ func init() {
|
|||
_ = viper.BindPFlag("enabletranscodingconfig", rootCmd.Flags().Lookup("enabletranscodingconfig"))
|
||||
_ = viper.BindPFlag("transcodingcachesize", rootCmd.Flags().Lookup("transcodingcachesize"))
|
||||
_ = viper.BindPFlag("imagecachesize", rootCmd.Flags().Lookup("imagecachesize"))
|
||||
_ = viper.BindPFlag("albumplaycountmode", rootCmd.Flags().Lookup("albumplaycountmode"))
|
||||
}
|
||||
|
|
|
@ -24,8 +24,9 @@ var scanCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func runScanner() {
|
||||
scanner := GetScanner()
|
||||
_ = scanner.RescanAll(context.Background(), fullRescan)
|
||||
ctx := context.Background()
|
||||
scanner := GetScanner(ctx)
|
||||
_ = scanner.RescanAll(ctx, fullRescan)
|
||||
if fullRescan {
|
||||
log.Info("Finished full rescan")
|
||||
} else {
|
||||
|
|
|
@ -16,7 +16,7 @@ const triggerScanSignal = syscall.SIGUSR1
|
|||
|
||||
func startSignaler(ctx context.Context) func() error {
|
||||
log.Info(ctx, "Starting signaler")
|
||||
scanner := GetScanner()
|
||||
scanner := GetScanner(ctx)
|
||||
|
||||
return func() error {
|
||||
var sigChan = make(chan os.Signal, 1)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/wire"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/agents"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/scanner2"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/nativeapi"
|
||||
|
@ -28,7 +30,7 @@ import (
|
|||
|
||||
// Injectors from wire_injectors.go:
|
||||
|
||||
func CreateServer(musicFolder string) *server.Server {
|
||||
func CreateServer(ctx context.Context) *server.Server {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
broker := events.GetBroker()
|
||||
|
@ -36,7 +38,7 @@ func CreateServer(musicFolder string) *server.Server {
|
|||
return serverServer
|
||||
}
|
||||
|
||||
func CreateNativeAPIRouter() *nativeapi.Router {
|
||||
func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
share := core.NewShare(dataStore)
|
||||
|
@ -45,7 +47,7 @@ func CreateNativeAPIRouter() *nativeapi.Router {
|
|||
return router
|
||||
}
|
||||
|
||||
func CreateSubsonicAPIRouter() *subsonic.Router {
|
||||
func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
|
@ -58,7 +60,7 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
|
|||
share := core.NewShare(dataStore)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||
players := core.NewPlayers(dataStore)
|
||||
scanner := GetScanner()
|
||||
scanner := GetScanner(ctx)
|
||||
broker := events.GetBroker()
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
|
||||
|
@ -66,7 +68,7 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
|
|||
return router
|
||||
}
|
||||
|
||||
func CreatePublicRouter() *public.Router {
|
||||
func CreatePublicRouter(ctx context.Context) *public.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
|
@ -82,32 +84,24 @@ func CreatePublicRouter() *public.Router {
|
|||
return router
|
||||
}
|
||||
|
||||
func CreateLastFMRouter() *lastfm.Router {
|
||||
func CreateLastFMRouter(ctx context.Context) *lastfm.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
router := lastfm.NewRouter(dataStore)
|
||||
return router
|
||||
}
|
||||
|
||||
func CreateListenBrainzRouter() *listenbrainz.Router {
|
||||
func CreateListenBrainzRouter(ctx context.Context) *listenbrainz.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
router := listenbrainz.NewRouter(dataStore)
|
||||
return router
|
||||
}
|
||||
|
||||
func createScanner() scanner.Scanner {
|
||||
func createScanner(ctx context.Context) scanner.Scanner {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
agentsAgents := agents.New(dataStore)
|
||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
broker := events.GetBroker()
|
||||
scannerScanner := scanner.New(dataStore, playlists, cacheWarmer, broker)
|
||||
scannerScanner := scanner2.New(ctx, dataStore)
|
||||
return scannerScanner
|
||||
}
|
||||
|
||||
|
@ -121,9 +115,9 @@ var (
|
|||
scannerInstance scanner.Scanner
|
||||
)
|
||||
|
||||
func GetScanner() scanner.Scanner {
|
||||
func GetScanner(ctx context.Context) scanner.Scanner {
|
||||
onceScanner.Do(func() {
|
||||
scannerInstance = createScanner()
|
||||
scannerInstance = createScanner(ctx)
|
||||
})
|
||||
return scannerInstance
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/google/wire"
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/scanner2"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/nativeapi"
|
||||
|
@ -33,39 +35,39 @@ var allProviders = wire.NewSet(
|
|||
db.Db,
|
||||
)
|
||||
|
||||
func CreateServer(musicFolder string) *server.Server {
|
||||
func CreateServer(ctx context.Context) *server.Server {
|
||||
panic(wire.Build(
|
||||
server.New,
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateNativeAPIRouter() *nativeapi.Router {
|
||||
func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateSubsonicAPIRouter() *subsonic.Router {
|
||||
func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
GetScanner,
|
||||
))
|
||||
}
|
||||
|
||||
func CreatePublicRouter() *public.Router {
|
||||
func CreatePublicRouter(ctx context.Context) *public.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateLastFMRouter() *lastfm.Router {
|
||||
func CreateLastFMRouter(ctx context.Context) *lastfm.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateListenBrainzRouter() *listenbrainz.Router {
|
||||
func CreateListenBrainzRouter(ctx context.Context) *listenbrainz.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
|
@ -77,16 +79,16 @@ var (
|
|||
scannerInstance scanner.Scanner
|
||||
)
|
||||
|
||||
func GetScanner() scanner.Scanner {
|
||||
func GetScanner(ctx context.Context) scanner.Scanner {
|
||||
onceScanner.Do(func() {
|
||||
scannerInstance = createScanner()
|
||||
scannerInstance = createScanner(ctx)
|
||||
})
|
||||
return scannerInstance
|
||||
}
|
||||
|
||||
func createScanner() scanner.Scanner {
|
||||
func createScanner(ctx context.Context) scanner.Scanner {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
scanner.New,
|
||||
scanner2.New,
|
||||
))
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ type configOptions struct {
|
|||
EnableMediaFileCoverArt bool
|
||||
TranscodingCacheSize string
|
||||
ImageCacheSize string
|
||||
AlbumPlayCountMode string
|
||||
EnableArtworkPrecache bool
|
||||
AutoImportPlaylists bool
|
||||
PlaylistsPath string
|
||||
|
@ -57,6 +58,7 @@ type configOptions struct {
|
|||
SubsonicArtistParticipations bool
|
||||
FFmpegPath string
|
||||
MPVPath string
|
||||
MPVCmdTemplate string
|
||||
CoverArtPriority string
|
||||
CoverJpegQuality int
|
||||
ArtistArtPriority string
|
||||
|
@ -288,6 +290,7 @@ func init() {
|
|||
viper.SetDefault("enabletranscodingconfig", false)
|
||||
viper.SetDefault("transcodingcachesize", "100MB")
|
||||
viper.SetDefault("imagecachesize", "100MB")
|
||||
viper.SetDefault("albumplaycountmode", consts.AlbumPlayCountModeAbsolute)
|
||||
viper.SetDefault("enableartworkprecache", true)
|
||||
viper.SetDefault("autoimportplaylists", true)
|
||||
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
|
||||
|
@ -303,6 +306,8 @@ func init() {
|
|||
viper.SetDefault("indexgroups", "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)")
|
||||
viper.SetDefault("subsonicartistparticipations", false)
|
||||
viper.SetDefault("ffmpegpath", "")
|
||||
viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display --pause %f --input-ipc-server=%s")
|
||||
|
||||
viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external")
|
||||
viper.SetDefault("coverjpegquality", 75)
|
||||
viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external")
|
||||
|
|
|
@ -81,6 +81,11 @@ const (
|
|||
DefaultCacheCleanUpInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
const (
|
||||
AlbumPlayCountModeAbsolute = "absolute"
|
||||
AlbumPlayCountModeNormalized = "normalized"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultDownsamplingFormat = "opus"
|
||||
DefaultTranscodings = []map[string]interface{}{
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# navidrome_enable (bool): Set to YES to enable navidrome
|
||||
# Default: NO
|
||||
# navidrome_config (str): navidrome configration file
|
||||
# navidrome_config (str): navidrome configuration file
|
||||
# Default: /usr/local/etc/navidrome/config.toml
|
||||
# navidrome_datafolder (str): navidrome Folder to store application data
|
||||
# Default: www
|
||||
|
|
|
@ -131,7 +131,7 @@ func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) erro
|
|||
|
||||
r, _, err := a.artwork.Get(ctx, id, consts.UICoverArtSize)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error cacheing id='%s': %w", id, err)
|
||||
return fmt.Errorf("error caching id='%s': %w", id, err)
|
||||
}
|
||||
defer r.Close()
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
|
|
|
@ -14,11 +14,6 @@ import (
|
|||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
// mpv --no-audio-display --pause 'Jack Johnson/On And On/01 Times Like These.m4a' --input-ipc-server=/tmp/gonzo.socket
|
||||
const (
|
||||
mpvComdTemplate = "mpv --audio-device=%d --no-audio-display --pause %f --input-ipc-server=%s"
|
||||
)
|
||||
|
||||
func start(args []string) (Executor, error) {
|
||||
log.Debug("Executing mpv command", "cmd", args)
|
||||
j := Executor{args: args}
|
||||
|
@ -78,15 +73,14 @@ func (j *Executor) wait() {
|
|||
}
|
||||
|
||||
// Path will always be an absolute path
|
||||
func createMPVCommand(cmd, deviceName string, filename string, socketName string) []string {
|
||||
split := strings.Split(fixCmd(cmd), " ")
|
||||
func createMPVCommand(deviceName string, filename string, socketName string) []string {
|
||||
split := strings.Split(fixCmd(conf.Server.MPVCmdTemplate), " ")
|
||||
for i, s := range split {
|
||||
s = strings.ReplaceAll(s, "%d", deviceName)
|
||||
s = strings.ReplaceAll(s, "%f", filename)
|
||||
s = strings.ReplaceAll(s, "%s", socketName)
|
||||
split[i] = s
|
||||
}
|
||||
|
||||
return split
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//go:build !windows
|
||||
|
||||
package mpv
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
func socketName(prefix, suffix string) string {
|
||||
return utils.TempFileName(prefix, suffix)
|
||||
}
|
||||
|
||||
func removeSocket(socketName string) {
|
||||
log.Debug("Removing socketfile", "socketfile", socketName)
|
||||
err := os.Remove(socketName)
|
||||
if err != nil {
|
||||
log.Error("Error cleaning up socketfile", "socketfile", socketName, err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//go:build windows
|
||||
|
||||
package mpv
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func socketName(prefix, suffix string) string {
|
||||
// Windows needs to use a named pipe for the socket
|
||||
// see https://mpv.io/manual/master#using-mpv-from-other-programs-or-scripts
|
||||
return filepath.Join(`\\.\pipe\mpvsocket`, prefix+uuid.NewString()+suffix)
|
||||
}
|
||||
|
||||
func removeSocket(string) {
|
||||
// Windows automatically handles cleaning up named pipe
|
||||
}
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/dexterlb/mpvipc"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
type MpvTrack struct {
|
||||
|
@ -32,9 +31,9 @@ func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFi
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tmpSocketName := utils.TempFileName("mpv-ctrl-", ".socket")
|
||||
tmpSocketName := socketName("mpv-ctrl-", ".socket")
|
||||
|
||||
args := createMPVCommand(mpvComdTemplate, deviceName, mf.Path, tmpSocketName)
|
||||
args := createMPVCommand(deviceName, mf.Path, tmpSocketName)
|
||||
exe, err := start(args)
|
||||
if err != nil {
|
||||
log.Error("Error starting mpv process", err)
|
||||
|
@ -124,11 +123,7 @@ func (t *MpvTrack) Close() {
|
|||
}
|
||||
|
||||
if t.isSocketFilePresent() {
|
||||
log.Debug("Removing socketfile", "socketfile", t.IPCSocketName)
|
||||
err := os.Remove(t.IPCSocketName)
|
||||
if err != nil {
|
||||
log.Error("Error cleaning up socketfile", "socketfile", t.IPCSocketName, err)
|
||||
}
|
||||
removeSocket(t.IPCSocketName)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
|
|||
defaultDeviceFound := false
|
||||
|
||||
if defaultDevice == "" {
|
||||
// if there are no devices given and no default device, we create a sythetic device named "auto"
|
||||
// if there are no devices given and no default device, we create a synthetic device named "auto"
|
||||
if len(devices) == 0 {
|
||||
pbDevices[0] = *NewPlaybackDevice(ps, "auto", "auto")
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/RaveNoX/go-jsoncommentstrip"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/criteria"
|
||||
|
@ -112,7 +113,8 @@ func (s *playlists) newSyncedPlaylist(baseDir string, playlistFile string) (*mod
|
|||
|
||||
func (s *playlists) parseNSP(ctx context.Context, pls *model.Playlist, file io.Reader) (*model.Playlist, error) {
|
||||
nsp := &nspFile{}
|
||||
dec := json.NewDecoder(file)
|
||||
reader := jsoncommentstrip.NewReader(file)
|
||||
dec := json.NewDecoder(reader)
|
||||
err := dec.Decode(nsp)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error parsing SmartPlaylist", "playlist", pls.Name, err)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model/criteria"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
|
@ -33,29 +34,45 @@ var _ = Describe("Playlists", func() {
|
|||
ps = NewPlaylists(ds)
|
||||
})
|
||||
|
||||
It("parses well-formed playlists", func() {
|
||||
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/pls1.m3u")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(pls.OwnerID).To(Equal("123"))
|
||||
Expect(pls.Tracks).To(HaveLen(3))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/test.mp3"))
|
||||
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/test.ogg"))
|
||||
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
|
||||
Expect(mp.last).To(Equal(pls))
|
||||
Describe("M3U", func() {
|
||||
It("parses well-formed playlists", func() {
|
||||
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/pls1.m3u")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(pls.OwnerID).To(Equal("123"))
|
||||
Expect(pls.Tracks).To(HaveLen(3))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/test.mp3"))
|
||||
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/test.ogg"))
|
||||
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
|
||||
Expect(mp.last).To(Equal(pls))
|
||||
})
|
||||
|
||||
It("parses playlists using LF ending", func() {
|
||||
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "lf-ended.m3u")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(pls.Tracks).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("parses playlists using CR ending (old Mac format)", func() {
|
||||
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "cr-ended.m3u")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(pls.Tracks).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
|
||||
It("parses playlists using LF ending", func() {
|
||||
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "lf-ended.m3u")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(pls.Tracks).To(HaveLen(2))
|
||||
Describe("NSP", func() {
|
||||
It("parses well-formed playlists", func() {
|
||||
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/recently_played.nsp")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(mp.last).To(Equal(pls))
|
||||
Expect(pls.OwnerID).To(Equal("123"))
|
||||
Expect(pls.Name).To(Equal("Recently Played"))
|
||||
Expect(pls.Comment).To(Equal("Recently played tracks"))
|
||||
Expect(pls.Rules.Sort).To(Equal("lastPlayed"))
|
||||
Expect(pls.Rules.Order).To(Equal("desc"))
|
||||
Expect(pls.Rules.Limit).To(Equal(100))
|
||||
Expect(pls.Rules.Expression).To(BeAssignableToTypeOf(criteria.All{}))
|
||||
})
|
||||
})
|
||||
|
||||
It("parses playlists using CR ending (old Mac format)", func() {
|
||||
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "cr-ended.m3u")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(pls.Tracks).To(HaveLen(2))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Describe("ImportM3U", func() {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upAddLibraryTable, downAddLibraryTable)
|
||||
}
|
||||
|
||||
func upAddLibraryTable(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
create table library (
|
||||
id integer primary key autoincrement,
|
||||
name varchar not null unique,
|
||||
path varchar not null unique,
|
||||
remote_path varchar null default '',
|
||||
extractor varchar null default 'taglib',
|
||||
last_scan_at datetime not null default '0000-00-00 00:00:00',
|
||||
updated_at datetime not null default current_timestamp,
|
||||
created_at datetime not null default current_timestamp
|
||||
);`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, fmt.Sprintf(`
|
||||
insert into library(id, name, path, extractor, last_scan_at) values(1, 'Music Library', '%s', '%s', current_timestamp);
|
||||
delete from property where id like 'LastScan-%%';
|
||||
`, conf.Server.MusicFolder, conf.Server.Scanner.Extractor))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
alter table media_file add column library_id integer not null default 1
|
||||
references library(id) on delete cascade;
|
||||
alter table album add column library_id integer not null default 1
|
||||
references library(id) on delete cascade;
|
||||
|
||||
create table if not exists library_artist
|
||||
(
|
||||
library_id integer not null default 1
|
||||
references library(id)
|
||||
on delete cascade,
|
||||
artist_id varchar not null default null
|
||||
references artist(id)
|
||||
on delete cascade,
|
||||
constraint library_artist_ux
|
||||
unique (library_id, artist_id)
|
||||
);
|
||||
|
||||
insert into library_artist(library_id, artist_id) select 1, id from artist;
|
||||
`)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func downAddLibraryTable(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
alter table media_file drop column library_id;
|
||||
alter table album drop column library_id;
|
||||
drop table library_artist;
|
||||
drop table library;
|
||||
`)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upAddFolderTable, downAddFolderTable)
|
||||
}
|
||||
|
||||
func upAddFolderTable(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
create table if not exists folder(
|
||||
id varchar not null
|
||||
primary key,
|
||||
library_id integer not null
|
||||
references library (id)
|
||||
on delete cascade,
|
||||
path varchar default '' not null,
|
||||
name varchar default '' not null,
|
||||
updated_at timestamp default current_timestamp not null,
|
||||
created_at timestamp default current_timestamp not null,
|
||||
parent_id varchar default '' not null
|
||||
);
|
||||
|
||||
alter table media_file
|
||||
add column folder_id varchar default "" not null;
|
||||
alter table media_file
|
||||
add column pid varchar default id not null;
|
||||
alter table media_file
|
||||
add column album_pid varchar default album_id not null;
|
||||
alter table media_file
|
||||
add column tags JSONB default '{}' not null;
|
||||
|
||||
create index if not exists media_file_folder_id_ix
|
||||
on media_file (folder_id);
|
||||
create unique index if not exists media_file_pid_ix
|
||||
on media_file (pid);
|
||||
create index if not exists media_file_album_pid_ix
|
||||
on media_file (album_pid);
|
||||
|
||||
-- FIXME Needs to process current media_file.paths, creating folders as needed
|
||||
|
||||
create table if not exists tag(
|
||||
id varchar not null primary key,
|
||||
name varchar default '' not null,
|
||||
value varchar default '' not null,
|
||||
constraint tags_name_value_ux
|
||||
unique (name, value)
|
||||
);
|
||||
|
||||
create table if not exists item_tags(
|
||||
item_id varchar not null,
|
||||
item_type varchar not null,
|
||||
tag_name varchar not null,
|
||||
tag_id varchar not null,
|
||||
constraint item_tags_ux
|
||||
unique (item_id, item_type, tag_id)
|
||||
);
|
||||
|
||||
create index if not exists item_tag_name_ix on item_tags(item_id, tag_name)
|
||||
`)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func downAddFolderTable(ctx context.Context, tx *sql.Tx) error {
|
||||
// This code is executed when the migration is rolled back.
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upAddIdToScrobbleBuffer, downAddIdToScrobbleBuffer)
|
||||
}
|
||||
|
||||
func upAddIdToScrobbleBuffer(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
delete from scrobble_buffer where user_id <> '';
|
||||
alter table scrobble_buffer add id varchar not null default '';
|
||||
create unique index scrobble_buffer_id_ix
|
||||
on scrobble_buffer (id);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func downAddIdToScrobbleBuffer(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
drop index scrobble_buffer_id_ix;
|
||||
alter table scrobble_buffer drop id;
|
||||
`)
|
||||
return err
|
||||
}
|
43
go.mod
43
go.mod
|
@ -4,11 +4,12 @@ go 1.21
|
|||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
|
||||
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf
|
||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
||||
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37
|
||||
github.com/dhowden/tag v0.0.0-20240122214204-713ab0e94639
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/djherbis/atime v1.1.0
|
||||
github.com/djherbis/fscache v0.10.2-0.20220222230828-2909c950912d
|
||||
|
@ -19,7 +20,8 @@ require (
|
|||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httprate v0.9.0
|
||||
github.com/go-chi/jwtauth/v5 v5.3.0
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1
|
||||
github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.6.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
|
@ -32,19 +34,19 @@ require (
|
|||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/mileusna/useragent v1.3.4
|
||||
github.com/onsi/ginkgo/v2 v2.17.1
|
||||
github.com/onsi/gomega v1.32.0
|
||||
github.com/pelletier/go-toml/v2 v2.1.1
|
||||
github.com/onsi/gomega v1.33.0
|
||||
github.com/pelletier/go-toml/v2 v2.2.1
|
||||
github.com/pocketbase/dbx v1.10.1
|
||||
github.com/pressly/goose/v3 v3.19.2
|
||||
github.com/pressly/goose/v3 v3.20.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/unrolled/secure v1.14.0
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/text v0.14.0
|
||||
|
@ -55,9 +57,9 @@ require (
|
|||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
|
@ -78,25 +80,26 @@ require (
|
|||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.2.4 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/tools v0.20.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
|
||||
|
|
585
go.sum
585
go.sum
|
@ -1,86 +1,23 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
|
||||
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
|
||||
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf h1:tb246l2Zmpt/GpF9EcHCKTtwzrd0HGfEmoODFA/qnk4=
|
||||
|
@ -89,8 +26,8 @@ github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 h1:mGvOb3zxl4vCLv+
|
|||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1/go.mod h1:ZNCLJfehvEf34B7BbLKjgpsL9lyW7q938w/GY1XgV4E=
|
||||
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37 h1:s+qNFsO3VsdsKroqapcogQxcQBHrRPDK1nVxGc+HBbg=
|
||||
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37/go.mod h1:CXCwawNJCtFDip7gvbaQVgw0cGjldpyHDIp7oA5prOg=
|
||||
github.com/dhowden/tag v0.0.0-20240122214204-713ab0e94639 h1:U3nSi6C8fCzav9CPYMC4SMSRW8HivuqENikr2aFT+yI=
|
||||
github.com/dhowden/tag v0.0.0-20240122214204-713ab0e94639/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/djherbis/atime v1.1.0 h1:rgwVbP/5by8BvvjBNrbh64Qz33idKT3pSnMSJsxhi0g=
|
||||
|
@ -101,127 +38,44 @@ github.com/djherbis/stream v1.4.0 h1:aVD46WZUiq5kJk55yxJAyw6Kuera6kmC3i2vEQyW/AE
|
|||
github.com/djherbis/stream v1.4.0/go.mod h1:cqjC1ZRq3FFwkGmUtHwcldbnW8f0Q4YuVsGW1eAFtOk=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
|
||||
github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4=
|
||||
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/httprate v0.9.0 h1:21A+4WDMDA5FyWcg7mNrhj63aNT8CGh+Z1alOE/piU8=
|
||||
github.com/go-chi/httprate v0.9.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.0 h1:X7RKGks1lrVeIe2omGyz47pNaNjG2YmwlRN5UKhN8qg=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.0/go.mod h1:2PoGm/KbnzRN9ILY6HFZAI6fTnb1gEZAKogAyqkd6fY=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
||||
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
|
||||
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdxohPneMrhGH2YfQuH5hRIK9u4/XCeUtw=
|
||||
github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc=
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
|
@ -230,42 +84,18 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
|
|||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jellydator/ttlcache/v2 v2.11.1 h1:AZGME43Eh2Vv3giG6GeqeLeFXxwxn1/qHItqWZl6U64=
|
||||
github.com/jellydator/ttlcache/v2 v2.11.1/go.mod h1:RtE5Snf0/57e+2cLWFYWCCsLas2Hy3c5Z4n14XmSvTI=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
||||
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
@ -289,8 +119,6 @@ github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55F
|
|||
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0=
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475/go.mod h1:20nXSmcf0nAscrzqsXeC2/tA3KkV2eCiJqYuyAgl+ss=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
||||
|
@ -310,41 +138,24 @@ github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3
|
|||
github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
|
||||
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
||||
github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk=
|
||||
github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss=
|
||||
github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8=
|
||||
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
|
||||
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
||||
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
|
||||
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
|
||||
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
|
||||
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pressly/goose/v3 v3.19.2 h1:z1yuD41jS4iaqLkyjkzGkKBz4rgyz/BYtCyMMGHlgzQ=
|
||||
github.com/pressly/goose/v3 v3.19.2/go.mod h1:BHkf3LzSBmO8E5FTMPupUYIpMTIh/ZuQVy+YTfhZLD4=
|
||||
github.com/pressly/goose/v3 v3.20.0 h1:uPJdOxF/Ipj7ABVNOAMJXSxwFXZGwMGHNqjC8e61VA0=
|
||||
github.com/pressly/goose/v3 v3.20.0/go.mod h1:BRfF2GcG4FTG12QfdBVy3q1yveaf4ckL9vWwEcIO3lA=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
|
@ -355,17 +166,18 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
|||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
||||
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
@ -373,18 +185,18 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
|
|||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
|
@ -393,7 +205,6 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
@ -401,159 +212,55 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898 h1:1MvEhzI5pvP27e9Dzz861mxk9WzXZLSJwzOU67cKTbU=
|
||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898/go.mod h1:9bKuHS7eZh/0mJndbUOrCx8Ej3PlsRDszj4L7oVYMPQ=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
|
||||
github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
|
||||
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf h1:ckwNHVo4bv2tqNkgx3W3HANh3ta1j6TR5qw08J1A7Tw=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1 h1:Ebo6J5AMXgJ3A438ECYotA0aK7ETqjQx9WoZvVxzKBE=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1/go.mod h1:udNPW8eupyH/EZocecFmaSNJacKKYjzQa7cVgX5U2nc=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
|
||||
go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
|
||||
go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
|
||||
go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
|
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -561,251 +268,68 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 h1:DKU1r6Tj5s1vlU/moGhuGz7E3xRfwjdAfDzbsaQJtEY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||
|
@ -814,14 +338,9 @@ modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
|||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
|
||||
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
||||
modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4=
|
||||
modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
@ -274,6 +274,8 @@ func addFields(logger *logrus.Entry, keyValuePairs []interface{}) *logrus.Entry
|
|||
} else {
|
||||
logger = logger.WithField(name, v.String())
|
||||
}
|
||||
case []string:
|
||||
logger = logger.WithField(name, strings.Join(v, ","))
|
||||
default:
|
||||
logger = logger.WithField(name, v)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ type Album struct {
|
|||
Annotations `structs:"-"`
|
||||
|
||||
ID string `structs:"id" json:"id"`
|
||||
PID string `structs:"pid" json:"pid"`
|
||||
LibraryID string `structs:"library_id" json:"libraryId"`
|
||||
Name string `structs:"name" json:"name"`
|
||||
EmbedArtPath string `structs:"embed_art_path" json:"embedArtPath"`
|
||||
ArtistID string `structs:"artist_id" json:"artistId"`
|
||||
|
|
|
@ -20,11 +20,13 @@ type ResourceRepository interface {
|
|||
}
|
||||
|
||||
type DataStore interface {
|
||||
Library(ctx context.Context) LibraryRepository
|
||||
Folder(ctx context.Context) FolderRepository
|
||||
Album(ctx context.Context) AlbumRepository
|
||||
Artist(ctx context.Context) ArtistRepository
|
||||
MediaFile(ctx context.Context) MediaFileRepository
|
||||
MediaFolder(ctx context.Context) MediaFolderRepository
|
||||
Genre(ctx context.Context) GenreRepository
|
||||
Tag(ctx context.Context) TagRepository
|
||||
Playlist(ctx context.Context) PlaylistRepository
|
||||
PlayQueue(ctx context.Context) PlayQueueRepository
|
||||
Transcoding(ctx context.Context) TranscodingRepository
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Folder struct {
|
||||
ID string `structs:"id"`
|
||||
LibraryID int `structs:"library_id"`
|
||||
Path string `structs:"path"`
|
||||
Name string `structs:"name"`
|
||||
ParentID string `structs:"parent_id"`
|
||||
UpdateAt time.Time `structs:"updated_at"`
|
||||
CreatedAt time.Time `structs:"created_at"`
|
||||
}
|
||||
|
||||
func FolderID(lib Library, path string) string {
|
||||
path = strings.TrimPrefix(path, lib.Path)
|
||||
key := fmt.Sprintf("%d:%s", lib.ID, path)
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(key)))
|
||||
}
|
||||
func NewFolder(lib Library, path string) *Folder {
|
||||
id := FolderID(lib, path)
|
||||
dir, name := filepath.Split(path)
|
||||
dir = filepath.Clean(dir)
|
||||
var parentID string
|
||||
if dir == "." {
|
||||
dir = ""
|
||||
parentID = ""
|
||||
} else {
|
||||
parentID = FolderID(lib, dir)
|
||||
}
|
||||
return &Folder{
|
||||
LibraryID: lib.ID,
|
||||
ID: id,
|
||||
Path: dir,
|
||||
Name: name,
|
||||
ParentID: parentID,
|
||||
UpdateAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type FolderRepository interface {
|
||||
Get(lib Library, path string) (*Folder, error)
|
||||
GetAll(lib Library) ([]Folder, error)
|
||||
GetLastUpdates(lib Library) (map[string]time.Time, error)
|
||||
Put(lib Library, path string) error
|
||||
Touch(lib Library, path string, t time.Time) error
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Library struct {
|
||||
ID int
|
||||
Name string
|
||||
Path string
|
||||
RemotePath string
|
||||
Extractor string
|
||||
LastScanAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (f Library) FS() fs.FS {
|
||||
return os.DirFS(f.Path)
|
||||
}
|
||||
|
||||
type Libraries []Library
|
||||
|
||||
type LibraryRepository interface {
|
||||
Get(id int) (*Library, error)
|
||||
Put(*Library) error
|
||||
StoreMusicFolder() error
|
||||
UpdateLastScan(id int, t time.Time) error
|
||||
GetAll(...QueryOptions) (Libraries, error)
|
||||
}
|
|
@ -21,6 +21,9 @@ type MediaFile struct {
|
|||
Bookmarkable `structs:"-"`
|
||||
|
||||
ID string `structs:"id" json:"id"`
|
||||
PID string `structs:"pid" json:"pid"`
|
||||
LibraryID int `structs:"library_id" json:"libraryId"`
|
||||
FolderID string `structs:"folder_id" json:"folderId"`
|
||||
Path string `structs:"path" json:"path"`
|
||||
Title string `structs:"title" json:"title"`
|
||||
Album string `structs:"album" json:"album"`
|
||||
|
@ -29,6 +32,7 @@ type MediaFile struct {
|
|||
AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"`
|
||||
AlbumArtist string `structs:"album_artist" json:"albumArtist"`
|
||||
AlbumID string `structs:"album_id" json:"albumId"`
|
||||
AlbumPID string `structs:"album_pid" json:"albumPid"`
|
||||
HasCoverArt bool `structs:"has_cover_art" json:"hasCoverArt"`
|
||||
TrackNumber int `structs:"track_number" json:"trackNumber"`
|
||||
DiscNumber int `structs:"disc_number" json:"discNumber"`
|
||||
|
@ -72,6 +76,8 @@ type MediaFile struct {
|
|||
RgTrackGain float64 `structs:"rg_track_gain" json:"rgTrackGain"`
|
||||
RgTrackPeak float64 `structs:"rg_track_peak" json:"rgTrackPeak"`
|
||||
|
||||
Tags Tags `structs:"tags" json:"tags,omitempty"` // All tags from the original file
|
||||
|
||||
CreatedAt time.Time `structs:"created_at" json:"createdAt"` // Time this entry was created in the DB
|
||||
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` // Time of file last update (mtime)
|
||||
}
|
||||
|
@ -271,4 +277,5 @@ type MediaFileRepository interface {
|
|||
|
||||
AnnotatedRepository
|
||||
BookmarkableRepository
|
||||
GetByFolder(folderID string) (MediaFiles, error)
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
type MediaFolder struct {
|
||||
ID int32
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
func (f MediaFolder) FS() fs.FS {
|
||||
return os.DirFS(f.Path)
|
||||
}
|
||||
|
||||
type MediaFolders []MediaFolder
|
||||
|
||||
type MediaFolderRepository interface {
|
||||
Get(id int32) (*MediaFolder, error)
|
||||
GetAll() (MediaFolders, error)
|
||||
}
|
|
@ -1,10 +1,5 @@
|
|||
package model
|
||||
|
||||
const (
|
||||
// TODO Move other prop keys to here
|
||||
PropLastScan = "LastScan"
|
||||
)
|
||||
|
||||
type PropertyRepository interface {
|
||||
Put(id string, value string) error
|
||||
Get(id string) (string, error)
|
||||
|
|
|
@ -16,6 +16,7 @@ const (
|
|||
Player = contextKey("player")
|
||||
Transcoding = contextKey("transcoding")
|
||||
ClientUniqueId = contextKey("clientUniqueId")
|
||||
ReverseProxyIp = contextKey("reverseProxyIp")
|
||||
)
|
||||
|
||||
func WithUser(ctx context.Context, u model.User) context.Context {
|
||||
|
@ -46,6 +47,10 @@ func WithClientUniqueId(ctx context.Context, clientUniqueId string) context.Cont
|
|||
return context.WithValue(ctx, ClientUniqueId, clientUniqueId)
|
||||
}
|
||||
|
||||
func WithReverseProxyIp(ctx context.Context, reverseProxyIp string) context.Context {
|
||||
return context.WithValue(ctx, ReverseProxyIp, reverseProxyIp)
|
||||
}
|
||||
|
||||
func UserFrom(ctx context.Context) (model.User, bool) {
|
||||
v, ok := ctx.Value(User).(model.User)
|
||||
return v, ok
|
||||
|
@ -80,3 +85,26 @@ func ClientUniqueIdFrom(ctx context.Context) (string, bool) {
|
|||
v, ok := ctx.Value(ClientUniqueId).(string)
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func ReverseProxyIpFrom(ctx context.Context) (string, bool) {
|
||||
v, ok := ctx.Value(ReverseProxyIp).(string)
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func AddValues(ctx, requestCtx context.Context) context.Context {
|
||||
keys := []contextKey{
|
||||
User,
|
||||
Username,
|
||||
Client,
|
||||
Version,
|
||||
Player,
|
||||
Transcoding,
|
||||
ClientUniqueId,
|
||||
}
|
||||
for _, key := range keys {
|
||||
if v := requestCtx.Value(key); v != nil {
|
||||
ctx = context.WithValue(ctx, key, v)
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
|
|
@ -3,11 +3,13 @@ package model
|
|||
import "time"
|
||||
|
||||
type ScrobbleEntry struct {
|
||||
MediaFile
|
||||
ID string
|
||||
Service string
|
||||
UserID string `structs:"user_id"`
|
||||
UserID string
|
||||
PlayTime time.Time
|
||||
EnqueueTime time.Time
|
||||
MediaFileID string
|
||||
MediaFile
|
||||
}
|
||||
|
||||
type ScrobbleEntries []ScrobbleEntry
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
ID string
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type FlattenedTags []Tag
|
||||
|
||||
func (t Tag) String() string {
|
||||
return fmt.Sprintf("%s=%s", t.Name, t.Value)
|
||||
}
|
||||
|
||||
type Tags map[string][]string
|
||||
|
||||
func (t Tags) Values(name string) []string {
|
||||
return t[name]
|
||||
}
|
||||
|
||||
func (t Tags) Flatten(name string) FlattenedTags {
|
||||
var tags FlattenedTags
|
||||
for _, v := range t[name] {
|
||||
tags = append(tags, NewTag(name, v))
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func (t Tags) FlattenAll() FlattenedTags {
|
||||
var tags FlattenedTags
|
||||
for name, values := range t {
|
||||
for _, v := range values {
|
||||
tags = append(tags, NewTag(name, v))
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func NewTag(name, value string) Tag {
|
||||
name = strings.ToLower(name)
|
||||
id := fmt.Sprintf("%x", md5.Sum([]byte(name+consts.Zwsp+strings.ToLower(value))))
|
||||
return Tag{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type TagRepository interface {
|
||||
Add(...Tag) error
|
||||
}
|
|
@ -4,11 +4,13 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/pocketbase/dbx"
|
||||
|
@ -153,7 +155,7 @@ func (r *albumRepository) Get(id string) (*model.Album, error) {
|
|||
}
|
||||
|
||||
func (r *albumRepository) Put(m *model.Album) error {
|
||||
_, err := r.put(m.ID, &dbAlbum{Album: m})
|
||||
_, err := r.put("pid", m.PID, &dbAlbum{Album: m})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -172,6 +174,9 @@ func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, e
|
|||
func (r *albumRepository) toModels(dba []dbAlbum) model.Albums {
|
||||
res := model.Albums{}
|
||||
for i := range dba {
|
||||
if conf.Server.AlbumPlayCountMode == consts.AlbumPlayCountModeNormalized && dba[i].Album.SongCount != 0 {
|
||||
dba[i].Album.PlayCount = int64(math.Round(float64(dba[i].Album.PlayCount) / float64(dba[i].Album.SongCount)))
|
||||
}
|
||||
res = append(res, *dba[i].Album)
|
||||
}
|
||||
return res
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
|
@ -89,4 +91,60 @@ var _ = Describe("AlbumRepository", func() {
|
|||
Expect(other.Album.Discs).To(Equal(a.Discs))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("toModels", func() {
|
||||
var repo *albumRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx := request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe"})
|
||||
repo = NewAlbumRepository(ctx, getDBXBuilder()).(*albumRepository)
|
||||
})
|
||||
|
||||
It("converts dbAlbum to model.Album", func() {
|
||||
dba := []dbAlbum{
|
||||
{Album: &model.Album{ID: "1", Name: "name", SongCount: 2, Annotations: model.Annotations{PlayCount: 4}}},
|
||||
{Album: &model.Album{ID: "2", Name: "name2", SongCount: 3, Annotations: model.Annotations{PlayCount: 6}}},
|
||||
}
|
||||
albums := repo.toModels(dba)
|
||||
Expect(len(albums)).To(Equal(2))
|
||||
Expect(albums[0].ID).To(Equal("1"))
|
||||
Expect(albums[1].ID).To(Equal("2"))
|
||||
})
|
||||
|
||||
DescribeTable("normalizes play count when AlbumPlayCountMode is absolute",
|
||||
func(songCount, playCount, expected int) {
|
||||
conf.Server.AlbumPlayCountMode = consts.AlbumPlayCountModeAbsolute
|
||||
dba := []dbAlbum{
|
||||
{Album: &model.Album{ID: "1", Name: "name", SongCount: songCount, Annotations: model.Annotations{PlayCount: int64(playCount)}}},
|
||||
}
|
||||
albums := repo.toModels(dba)
|
||||
Expect(albums[0].PlayCount).To(Equal(int64(expected)))
|
||||
},
|
||||
Entry("1 song, 0 plays", 1, 0, 0),
|
||||
Entry("1 song, 4 plays", 1, 4, 4),
|
||||
Entry("3 songs, 6 plays", 3, 6, 6),
|
||||
Entry("10 songs, 6 plays", 10, 6, 6),
|
||||
Entry("70 songs, 70 plays", 70, 70, 70),
|
||||
Entry("10 songs, 50 plays", 10, 50, 50),
|
||||
Entry("120 songs, 121 plays", 120, 121, 121),
|
||||
)
|
||||
|
||||
DescribeTable("normalizes play count when AlbumPlayCountMode is normalized",
|
||||
func(songCount, playCount, expected int) {
|
||||
conf.Server.AlbumPlayCountMode = consts.AlbumPlayCountModeNormalized
|
||||
dba := []dbAlbum{
|
||||
{Album: &model.Album{ID: "1", Name: "name", SongCount: songCount, Annotations: model.Annotations{PlayCount: int64(playCount)}}},
|
||||
}
|
||||
albums := repo.toModels(dba)
|
||||
Expect(albums[0].PlayCount).To(Equal(int64(expected)))
|
||||
},
|
||||
Entry("1 song, 0 plays", 1, 0, 0),
|
||||
Entry("1 song, 4 plays", 1, 4, 4),
|
||||
Entry("3 songs, 6 plays", 3, 6, 2),
|
||||
Entry("10 songs, 6 plays", 10, 6, 1),
|
||||
Entry("70 songs, 70 plays", 70, 70, 1),
|
||||
Entry("10 songs, 50 plays", 10, 50, 5),
|
||||
Entry("120 songs, 121 plays", 120, 121, 1),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -95,7 +95,7 @@ func (r *artistRepository) Exists(id string) (bool, error) {
|
|||
func (r *artistRepository) Put(a *model.Artist, colsToUpdate ...string) error {
|
||||
a.FullText = getFullText(a.Name, a.SortArtistName)
|
||||
dba := &dbArtist{Artist: a}
|
||||
_, err := r.put(dba.ID, dba, colsToUpdate...)
|
||||
_, err := r.put("id", dba.ID, dba, colsToUpdate...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
type folderRepository struct {
|
||||
sqlRepository
|
||||
}
|
||||
|
||||
func newFolderRepository(ctx context.Context, db dbx.Builder) model.FolderRepository {
|
||||
r := &folderRepository{}
|
||||
r.ctx = ctx
|
||||
r.db = db
|
||||
r.tableName = "folder"
|
||||
return r
|
||||
}
|
||||
|
||||
func (r folderRepository) Get(lib model.Library, path string) (*model.Folder, error) {
|
||||
id := model.NewFolder(lib, path).ID
|
||||
sq := r.newSelect().Where(Eq{"id": id})
|
||||
var res model.Folder
|
||||
err := r.queryOne(sq, res)
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (r folderRepository) GetAll(lib model.Library) ([]model.Folder, error) {
|
||||
sq := r.newSelect().Columns("*").Where(Eq{"library_id": lib.ID})
|
||||
var res []model.Folder
|
||||
err := r.queryAll(sq, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r folderRepository) GetLastUpdates(lib model.Library) (map[string]time.Time, error) {
|
||||
sq := r.newSelect().Columns("id", "updated_at").Where(Eq{"library_id": lib.ID})
|
||||
var res []struct {
|
||||
ID string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
err := r.queryAll(sq, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]time.Time, len(res))
|
||||
for _, f := range res {
|
||||
m[f.ID] = f.UpdatedAt
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (r folderRepository) Put(lib model.Library, path string) error {
|
||||
folder := model.NewFolder(lib, path)
|
||||
_, err := r.put("id", folder.ID, folder)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r folderRepository) Touch(lib model.Library, path string, t time.Time) error {
|
||||
id := model.FolderID(lib, path)
|
||||
sq := Update(r.tableName).Set("updated_at", t).Where(Eq{"id": id})
|
||||
_, err := r.executeSQL(sq)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ model.FolderRepository = (*folderRepository)(nil)
|
|
@ -0,0 +1,73 @@
|
|||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
type libraryRepository struct {
|
||||
sqlRepository
|
||||
}
|
||||
|
||||
func NewLibraryRepository(ctx context.Context, db dbx.Builder) model.LibraryRepository {
|
||||
r := &libraryRepository{}
|
||||
r.ctx = ctx
|
||||
r.db = db
|
||||
r.tableName = "library"
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *libraryRepository) Get(id int) (*model.Library, error) {
|
||||
sq := r.newSelect().Columns("*").Where(Eq{"id": id})
|
||||
var res model.Library
|
||||
err := r.queryOne(sq, &res)
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (r *libraryRepository) Put(l *model.Library) error {
|
||||
cols := map[string]any{
|
||||
"name": l.Name,
|
||||
"path": l.Path,
|
||||
"remote_path": l.RemotePath,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if l.ID != 0 {
|
||||
cols["id"] = l.ID
|
||||
}
|
||||
|
||||
sq := Insert(r.tableName).SetMap(cols).
|
||||
Suffix(`ON CONFLICT(id) DO UPDATE set name = excluded.name, path = excluded.path,
|
||||
remote_path = excluded.remote_path, updated_at = excluded.updated_at`)
|
||||
_, err := r.executeSQL(sq)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO Remove this and the StoreMusicFolder method when we have a proper UI to add libraries
|
||||
const hardCodedMusicFolderID = 1
|
||||
|
||||
func (r *libraryRepository) StoreMusicFolder() error {
|
||||
sq := Update(r.tableName).Set("path", conf.Server.MusicFolder).Set("updated_at", time.Now()).
|
||||
Set("extractor", conf.Server.Scanner.Extractor).Where(Eq{"id": hardCodedMusicFolderID})
|
||||
_, err := r.executeSQL(sq)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *libraryRepository) UpdateLastScan(id int, t time.Time) error {
|
||||
sq := Update(r.tableName).Set("last_scan_at", t).Where(Eq{"id": id})
|
||||
_, err := r.executeSQL(sq)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *libraryRepository) GetAll(ops ...model.QueryOptions) (model.Libraries, error) {
|
||||
sq := r.newSelect(ops...).Columns("*")
|
||||
res := model.Libraries{}
|
||||
err := r.queryAll(sq, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ model.LibraryRepository = (*libraryRepository)(nil)
|
|
@ -2,6 +2,8 @@ package persistence
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -21,7 +23,51 @@ type mediaFileRepository struct {
|
|||
sqlRestful
|
||||
}
|
||||
|
||||
func NewMediaFileRepository(ctx context.Context, db dbx.Builder) *mediaFileRepository {
|
||||
type dbMediaFile struct {
|
||||
*model.MediaFile `structs:",flatten"`
|
||||
Tags string `structs:"-" json:"tags"`
|
||||
}
|
||||
|
||||
func (m *dbMediaFile) PostScan() error {
|
||||
if m.Tags == "" {
|
||||
m.MediaFile.Tags = make(map[string][]string)
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(m.Tags), &m.MediaFile.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Map genres from tags
|
||||
for _, g := range m.MediaFile.Tags.Flatten("genre") {
|
||||
m.MediaFile.Genres = append(m.MediaFile.Genres, model.Genre{Name: g.Value, ID: g.ID})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dbMediaFile) PostMapArgs(args map[string]any) error {
|
||||
if len(m.MediaFile.Tags) == 0 {
|
||||
args["tags"] = "{}"
|
||||
return nil
|
||||
}
|
||||
b, err := json.Marshal(m.MediaFile.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args["tags"] = string(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbMediaFiles []dbMediaFile
|
||||
|
||||
func (m *dbMediaFiles) toModels() model.MediaFiles {
|
||||
res := make(model.MediaFiles, len(*m))
|
||||
for i, mf := range *m {
|
||||
res[i] = *mf.MediaFile
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func NewMediaFileRepository(ctx context.Context, db dbx.Builder) model.MediaFileRepository {
|
||||
r := &mediaFileRepository{}
|
||||
r.ctx = ctx
|
||||
r.db = db
|
||||
|
@ -50,7 +96,8 @@ func NewMediaFileRepository(ctx context.Context, db dbx.Builder) *mediaFileRepos
|
|||
|
||||
func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||
sql := r.newSelectWithAnnotation("media_file.id")
|
||||
sql = r.withGenres(sql) // Required for filtering by genre
|
||||
// FIXME Genres
|
||||
//sql = r.withGenres(sql) // Required for filtering by genre
|
||||
return r.count(sql, options...)
|
||||
}
|
||||
|
||||
|
@ -59,66 +106,89 @@ func (r *mediaFileRepository) Exists(id string) (bool, error) {
|
|||
}
|
||||
|
||||
func (r *mediaFileRepository) Put(m *model.MediaFile) error {
|
||||
if m.ID == "" || m.PID == "" {
|
||||
return errors.New("id and pid are required")
|
||||
}
|
||||
m.FullText = getFullText(m.Title, m.Album, m.Artist, m.AlbumArtist,
|
||||
m.SortTitle, m.SortAlbumName, m.SortArtistName, m.SortAlbumArtistName, m.DiscSubtitle)
|
||||
_, err := r.put(m.ID, m)
|
||||
_, err := r.put("pid", m.PID, &dbMediaFile{MediaFile: m})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.updateGenres(m.ID, r.tableName, m.Genres)
|
||||
|
||||
return r.updateTags(m.ID, m.Tags)
|
||||
|
||||
// FIXME Genres
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//return r.updateGenres(m.ID, r.tableName, m.Genres)
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
|
||||
sql := r.newSelectWithAnnotation("media_file.id", options...).Columns("media_file.*")
|
||||
sql = r.withBookmark(sql, "media_file.id")
|
||||
if len(options) > 0 && options[0].Filters != nil {
|
||||
s, _, _ := options[0].Filters.ToSql()
|
||||
// If there's any reference of genre in the filter, joins with genre
|
||||
if strings.Contains(s, "genre") {
|
||||
sql = r.withGenres(sql)
|
||||
// If there's no filter on genre_id, group the results by media_file.id
|
||||
if !strings.Contains(s, "genre_id") {
|
||||
sql = sql.GroupBy("media_file.id")
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME Genres
|
||||
//if len(options) > 0 && options[0].Filters != nil {
|
||||
// s, _, _ := options[0].Filters.ToSql()
|
||||
// // If there's any reference of genre in the filter, joins with genre
|
||||
// if strings.Contains(s, "genre") {
|
||||
// sql = r.withGenres(sql)
|
||||
// // If there's no filter on genre_id, group the results by media_file.id
|
||||
// if !strings.Contains(s, "genre_id") {
|
||||
// sql = sql.GroupBy("media_file.id")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
return sql
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) Get(id string) (*model.MediaFile, error) {
|
||||
sel := r.selectMediaFile().Where(Eq{"media_file.id": id})
|
||||
var res model.MediaFiles
|
||||
var res dbMediaFiles
|
||||
if err := r.queryAll(sel, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(res) == 0 {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
err := r.loadMediaFileGenres(&res)
|
||||
return &res[0], err
|
||||
// FIXME Genres
|
||||
//err := r.loadMediaFileGenres(&res)
|
||||
return res[0].MediaFile, nil
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
sq := r.selectMediaFile(options...)
|
||||
res := model.MediaFiles{}
|
||||
var res dbMediaFiles
|
||||
err := r.queryAll(sq, &res, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.loadMediaFileGenres(&res)
|
||||
return res, err
|
||||
// FIXME Genres
|
||||
//err = r.loadMediaFileGenres(&rows)
|
||||
return res.toModels(), nil
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) GetByFolder(folderID string) (model.MediaFiles, error) {
|
||||
sq := r.newSelect().Columns("*").Where(Eq{"folder_id": folderID})
|
||||
var res dbMediaFiles
|
||||
err := r.queryAll(sq, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.toModels(), nil
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) FindByPath(path string) (*model.MediaFile, error) {
|
||||
sel := r.newSelect().Columns("*").Where(Like{"path": path})
|
||||
var res model.MediaFiles
|
||||
var res dbMediaFiles
|
||||
if err := r.queryAll(sel, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(res) == 0 {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
return &res[0], nil
|
||||
return res[0].MediaFile, nil
|
||||
}
|
||||
|
||||
func cleanPath(path string) string {
|
||||
|
@ -144,9 +214,9 @@ func (r *mediaFileRepository) FindAllByPath(path string) (model.MediaFiles, erro
|
|||
sel := r.newSelect().Columns("*", "item NOT GLOB '*"+string(os.PathSeparator)+"*' AS isLast").
|
||||
Where(Eq{"isLast": 1}).FromSelect(sel0, "sel0")
|
||||
|
||||
res := model.MediaFiles{}
|
||||
res := dbMediaFiles{}
|
||||
err := r.queryAll(sel, &res)
|
||||
return res, err
|
||||
return res.toModels(), err
|
||||
}
|
||||
|
||||
// FindPathsRecursively returns a list of all subfolders of basePath, recursively
|
||||
|
@ -195,13 +265,14 @@ func (r *mediaFileRepository) removeNonAlbumArtistIds() error {
|
|||
}
|
||||
|
||||
func (r *mediaFileRepository) Search(q string, offset int, size int) (model.MediaFiles, error) {
|
||||
results := model.MediaFiles{}
|
||||
results := dbMediaFiles{}
|
||||
err := r.doSearch(q, offset, size, &results, "title")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.loadMediaFileGenres(&results)
|
||||
return results, err
|
||||
// FIXME Genres
|
||||
//err = r.loadMediaFileGenres(&results)
|
||||
return results.toModels(), err
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) Count(options ...rest.QueryOptions) (int64, error) {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
type mediaFolderRepository struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewMediaFolderRepository(ctx context.Context, _ dbx.Builder) model.MediaFolderRepository {
|
||||
return &mediaFolderRepository{ctx}
|
||||
}
|
||||
|
||||
func (r *mediaFolderRepository) Get(id int32) (*model.MediaFolder, error) {
|
||||
mediaFolder := hardCoded()
|
||||
return &mediaFolder, nil
|
||||
}
|
||||
|
||||
func (*mediaFolderRepository) GetAll() (model.MediaFolders, error) {
|
||||
mediaFolder := hardCoded()
|
||||
result := make(model.MediaFolders, 1)
|
||||
result[0] = mediaFolder
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func hardCoded() model.MediaFolder {
|
||||
mediaFolder := model.MediaFolder{ID: 0, Path: conf.Server.MusicFolder}
|
||||
mediaFolder.Name = "Music Library"
|
||||
return mediaFolder
|
||||
}
|
||||
|
||||
var _ model.MediaFolderRepository = (*mediaFolderRepository)(nil)
|
|
@ -31,14 +31,22 @@ func (s *SQLStore) MediaFile(ctx context.Context) model.MediaFileRepository {
|
|||
return NewMediaFileRepository(ctx, s.getDBXBuilder())
|
||||
}
|
||||
|
||||
func (s *SQLStore) MediaFolder(ctx context.Context) model.MediaFolderRepository {
|
||||
return NewMediaFolderRepository(ctx, s.getDBXBuilder())
|
||||
func (s *SQLStore) Library(ctx context.Context) model.LibraryRepository {
|
||||
return NewLibraryRepository(ctx, s.getDBXBuilder())
|
||||
}
|
||||
|
||||
func (s *SQLStore) Folder(ctx context.Context) model.FolderRepository {
|
||||
return newFolderRepository(ctx, s.getDBXBuilder())
|
||||
}
|
||||
|
||||
func (s *SQLStore) Genre(ctx context.Context) model.GenreRepository {
|
||||
return NewGenreRepository(ctx, s.getDBXBuilder())
|
||||
}
|
||||
|
||||
func (s *SQLStore) Tag(ctx context.Context) model.TagRepository {
|
||||
return NewTagRepository(ctx, s.getDBXBuilder())
|
||||
}
|
||||
|
||||
func (s *SQLStore) PlayQueue(ctx context.Context) model.PlayQueueRepository {
|
||||
return NewPlayQueueRepository(ctx, s.getDBXBuilder())
|
||||
}
|
||||
|
|
|
@ -60,10 +60,10 @@ var (
|
|||
)
|
||||
|
||||
var (
|
||||
songDayInALife = model.MediaFile{ID: "1001", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "101", Album: "Sgt Peppers", Genre: "Rock", Genres: model.Genres{genreRock}, Path: P("/beatles/1/sgt/a day.mp3"), FullText: " a beatles day in life peppers sgt the"}
|
||||
songComeTogether = model.MediaFile{ID: "1002", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "102", Album: "Abbey Road", Genre: "Rock", Genres: model.Genres{genreRock}, Path: P("/beatles/1/come together.mp3"), FullText: " abbey beatles come road the together"}
|
||||
songRadioactivity = model.MediaFile{ID: "1003", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Album: "Radioactivity", Genre: "Electronic", Genres: model.Genres{genreElectronic}, Path: P("/kraft/radio/radio.mp3"), FullText: " kraftwerk radioactivity"}
|
||||
songAntenna = model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk",
|
||||
songDayInALife = model.MediaFile{ID: "1001", PID: "1001", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "101", Album: "Sgt Peppers", Genre: "Rock", Genres: model.Genres{genreRock}, Path: P("/beatles/1/sgt/a day.mp3"), FullText: " a beatles day in life peppers sgt the"}
|
||||
songComeTogether = model.MediaFile{ID: "1002", PID: "1002", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "102", Album: "Abbey Road", Genre: "Rock", Genres: model.Genres{genreRock}, Path: P("/beatles/1/come together.mp3"), FullText: " abbey beatles come road the together"}
|
||||
songRadioactivity = model.MediaFile{ID: "1003", PID: "1003", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Album: "Radioactivity", Genre: "Electronic", Genres: model.Genres{genreElectronic}, Path: P("/kraft/radio/radio.mp3"), FullText: " kraftwerk radioactivity"}
|
||||
songAntenna = model.MediaFile{ID: "1004", PID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk",
|
||||
AlbumID: "103", Genre: "Electronic", Genres: model.Genres{genreElectronic, genreRock},
|
||||
Path: P("/kraft/radio/antenna.mp3"), FullText: " antenna kraftwerk",
|
||||
RgAlbumGain: 1.0, RgAlbumPeak: 2.0, RgTrackGain: 3.0, RgTrackPeak: 4.0,
|
||||
|
|
|
@ -27,7 +27,7 @@ func NewPlayerRepository(ctx context.Context, db dbx.Builder) model.PlayerReposi
|
|||
}
|
||||
|
||||
func (r *playerRepository) Put(p *model.Player) error {
|
||||
_, err := r.put(p.ID, p)
|
||||
_, err := r.put("id", p.ID, p)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ func (r *playerRepository) Save(entity interface{}) (string, error) {
|
|||
if !r.isPermitted(t) {
|
||||
return "", rest.ErrPermissionDenied
|
||||
}
|
||||
id, err := r.put(t.ID, t)
|
||||
id, err := r.put("id", t.ID, t)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return "", rest.ErrNotFound
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ func (r *playerRepository) Update(id string, entity interface{}, cols ...string)
|
|||
if !r.isPermitted(t) {
|
||||
return rest.ErrPermissionDenied
|
||||
}
|
||||
_, err := r.put(id, t, cols...)
|
||||
_, err := r.put("id", id, t, cols...)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return rest.ErrNotFound
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ func (r *playlistRepository) Put(p *model.Playlist) error {
|
|||
}
|
||||
pls.UpdatedAt = time.Now()
|
||||
|
||||
id, err := r.put(pls.ID, pls)
|
||||
id, err := r.put("id", pls.ID, pls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -309,7 +309,7 @@ func (r *playlistRepository) addTracks(playlistId string, startingPos int, media
|
|||
return r.refreshCounters(&model.Playlist{ID: playlistId})
|
||||
}
|
||||
|
||||
// RefreshStatus updates total playlist duration, size and count
|
||||
// refreshCounters updates total playlist duration, size and count
|
||||
func (r *playlistRepository) refreshCounters(pls *model.Playlist) error {
|
||||
statsSql := Select(
|
||||
"coalesce(sum(duration), 0) as duration",
|
||||
|
@ -399,18 +399,25 @@ func (r *playlistRepository) Save(entity interface{}) (string, error) {
|
|||
}
|
||||
|
||||
func (r *playlistRepository) Update(id string, entity interface{}, cols ...string) error {
|
||||
pls := dbPlaylist{Playlist: *entity.(*model.Playlist)}
|
||||
current, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usr := loggedUser(r.ctx)
|
||||
if !usr.IsAdmin && current.OwnerID != usr.ID {
|
||||
return rest.ErrPermissionDenied
|
||||
if !usr.IsAdmin {
|
||||
// Only the owner can update the playlist
|
||||
if current.OwnerID != usr.ID {
|
||||
return rest.ErrPermissionDenied
|
||||
}
|
||||
// Regular users can't change the ownership of a playlist
|
||||
if pls.OwnerID != "" && pls.OwnerID != usr.ID {
|
||||
return rest.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
pls := dbPlaylist{Playlist: *entity.(*model.Playlist)}
|
||||
pls.ID = id
|
||||
pls.UpdatedAt = time.Now()
|
||||
_, err = r.put(id, pls, append(cols, "updatedAt")...)
|
||||
_, err = r.put("id", id, pls, append(cols, "updatedAt")...)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return rest.ErrNotFound
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ func (r *playQueueRepository) Store(q *model.PlayQueue) error {
|
|||
pq.CreatedAt = time.Now()
|
||||
}
|
||||
pq.UpdatedAt = time.Now()
|
||||
_, err = r.put(pq.ID, pq)
|
||||
_, err = r.put("id", pq.ID, pq)
|
||||
if err != nil {
|
||||
log.Error(r.ctx, "Error saving playqueue", "user", u.UserName, err)
|
||||
return err
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
@ -37,6 +38,7 @@ func (r *scrobbleBufferRepository) UserIDs(service string) ([]string, error) {
|
|||
|
||||
func (r *scrobbleBufferRepository) Enqueue(service, userId, mediaFileId string, playTime time.Time) error {
|
||||
ins := Insert(r.tableName).SetMap(map[string]interface{}{
|
||||
"id": uuid.NewString(),
|
||||
"user_id": userId,
|
||||
"service": service,
|
||||
"media_file_id": mediaFileId,
|
||||
|
@ -48,7 +50,8 @@ func (r *scrobbleBufferRepository) Enqueue(service, userId, mediaFileId string,
|
|||
}
|
||||
|
||||
func (r *scrobbleBufferRepository) Next(service string, userId string) (*model.ScrobbleEntry, error) {
|
||||
sql := Select().Columns("s.*, m.*").
|
||||
// Put `s.*` last or else m.id overrides s.id
|
||||
sql := Select().Columns("m.*, s.*").
|
||||
From(r.tableName+" s").
|
||||
LeftJoin("media_file m on m.id = s.media_file_id").
|
||||
Where(And{
|
||||
|
@ -57,24 +60,20 @@ func (r *scrobbleBufferRepository) Next(service string, userId string) (*model.S
|
|||
}).
|
||||
OrderBy("play_time", "s.rowid").Limit(1)
|
||||
|
||||
res := model.ScrobbleEntries{}
|
||||
// TODO Rewrite queryOne to use QueryRows, to workaround the recursive embedded structs issue
|
||||
err := r.queryAll(sql, &res)
|
||||
if errors.Is(err, model.ErrNotFound) || len(res) == 0 {
|
||||
res := &model.ScrobbleEntry{}
|
||||
err := r.queryOne(sql, res)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res[0], nil
|
||||
res.MediaFile.ID = res.MediaFileID
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *scrobbleBufferRepository) Dequeue(entry *model.ScrobbleEntry) error {
|
||||
return r.delete(And{
|
||||
Eq{"service": entry.Service},
|
||||
Eq{"media_file_id": entry.MediaFile.ID},
|
||||
Eq{"play_time": entry.PlayTime},
|
||||
})
|
||||
return r.delete(Eq{"id": entry.ID})
|
||||
}
|
||||
|
||||
func (r *scrobbleBufferRepository) Length() (int64, error) {
|
||||
|
|
|
@ -138,7 +138,7 @@ func (r *shareRepository) Update(id string, entity interface{}, cols ...string)
|
|||
s.ID = id
|
||||
s.UpdatedAt = time.Now()
|
||||
cols = append(cols, "updated_at")
|
||||
_, err := r.put(id, s, cols...)
|
||||
_, err := r.put("id", id, s, cols...)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return rest.ErrNotFound
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ func (r *shareRepository) Save(entity interface{}) (string, error) {
|
|||
}
|
||||
s.CreatedAt = time.Now()
|
||||
s.UpdatedAt = time.Now()
|
||||
id, err := r.put(s.ID, s)
|
||||
id, err := r.put("id", s.ID, s)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return "", rest.ErrNotFound
|
||||
}
|
||||
|
|
|
@ -157,8 +157,6 @@ func (r sqlRepository) toSQL(sq Sqlizer) (string, dbx.Params, error) {
|
|||
return query, params, nil
|
||||
}
|
||||
|
||||
// Note: Due to a bug in the QueryRow method, this function does not map any embedded structs (ex: annotations)
|
||||
// In this case, use the queryAll method and get the first item of the returned list
|
||||
func (r sqlRepository) queryOne(sq Sqlizer, response interface{}) error {
|
||||
query, args, err := r.toSQL(sq)
|
||||
if err != nil {
|
||||
|
@ -239,10 +237,10 @@ func (r sqlRepository) count(countQuery SelectBuilder, options ...model.QueryOpt
|
|||
return res.Count, err
|
||||
}
|
||||
|
||||
func (r sqlRepository) put(id string, m interface{}, colsToUpdate ...string) (newId string, err error) {
|
||||
func (r sqlRepository) put(idCol string, idVal string, m interface{}, colsToUpdate ...string) (newId string, err error) {
|
||||
values, _ := toSQLArgs(m)
|
||||
// If there's an ID, try to update first
|
||||
if id != "" {
|
||||
if idVal != "" {
|
||||
updateValues := map[string]interface{}{}
|
||||
|
||||
// This is a map of the columns that need to be updated, if specified
|
||||
|
@ -257,23 +255,23 @@ func (r sqlRepository) put(id string, m interface{}, colsToUpdate ...string) (ne
|
|||
}
|
||||
|
||||
delete(updateValues, "created_at")
|
||||
update := Update(r.tableName).Where(Eq{"id": id}).SetMap(updateValues)
|
||||
update := Update(r.tableName).Where(Eq{idCol: idVal}).SetMap(updateValues)
|
||||
count, err := r.executeSQL(update)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if count > 0 {
|
||||
return id, nil
|
||||
return idVal, nil
|
||||
}
|
||||
}
|
||||
// If it does not have an ID OR the ID was not found (when it is a new record with predefined id)
|
||||
if id == "" {
|
||||
id = uuid.NewString()
|
||||
values["id"] = id
|
||||
if idVal == "" {
|
||||
idVal = uuid.NewString()
|
||||
values[idCol] = idVal
|
||||
}
|
||||
insert := Insert(r.tableName).SetMap(values)
|
||||
_, err = r.executeSQL(insert)
|
||||
return id, err
|
||||
return idVal, err
|
||||
}
|
||||
|
||||
func (r sqlRepository) delete(cond Sqlizer) error {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
type tagRepository struct {
|
||||
sqlRepository
|
||||
}
|
||||
|
||||
func NewTagRepository(ctx context.Context, db dbx.Builder) model.TagRepository {
|
||||
r := &tagRepository{}
|
||||
r.ctx = ctx
|
||||
r.db = db
|
||||
r.tableName = "tag"
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *tagRepository) Add(tags ...model.Tag) error {
|
||||
return slice.RangeByChunks(tags, 200, func(chunk []model.Tag) error {
|
||||
sq := Insert(r.tableName).Columns("id", "name", "value").
|
||||
Suffix("on conflict (id) do nothing")
|
||||
for _, t := range chunk {
|
||||
sq = sq.Values(t.ID, t.Name, t.Value)
|
||||
}
|
||||
_, err := r.executeSQL(sq)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (r *sqlRepository) updateTags(itemID string, tags model.Tags) error {
|
||||
sqd := Delete("item_tags").Where(Eq{"item_id": itemID, "item_type": r.tableName})
|
||||
_, err := r.executeSQL(sqd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
sqi := Insert("item_tags").Columns("item_id", "item_type", "tag_name", "tag_id").
|
||||
Suffix("on conflict (item_id, item_type, tag_id) do nothing")
|
||||
for name, values := range tags {
|
||||
for _, value := range values {
|
||||
tag := model.NewTag(name, value)
|
||||
sqi = sqi.Values(itemID, r.tableName, tag.Name, tag.ID)
|
||||
}
|
||||
}
|
||||
_, err = r.executeSQL(sqi)
|
||||
return err
|
||||
}
|
|
@ -42,7 +42,7 @@ func (r *transcodingRepository) FindByFormat(format string) (*model.Transcoding,
|
|||
}
|
||||
|
||||
func (r *transcodingRepository) Put(t *model.Transcoding) error {
|
||||
_, err := r.put(t.ID, t)
|
||||
_, err := r.put("id", t.ID, t)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ func (r *transcodingRepository) NewInstance() interface{} {
|
|||
|
||||
func (r *transcodingRepository) Save(entity interface{}) (string, error) {
|
||||
t := entity.(*model.Transcoding)
|
||||
id, err := r.put(t.ID, t)
|
||||
id, err := r.put("id", t.ID, t)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return "", rest.ErrNotFound
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ func (r *transcodingRepository) Save(entity interface{}) (string, error) {
|
|||
func (r *transcodingRepository) Update(id string, entity interface{}, cols ...string) error {
|
||||
t := entity.(*model.Transcoding)
|
||||
t.ID = id
|
||||
_, err := r.put(id, t)
|
||||
_, err := r.put("id", id, t)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return rest.ErrNotFound
|
||||
}
|
||||
|
|
|
@ -148,9 +148,9 @@
|
|||
"name": "Nom",
|
||||
"duration": "Durée",
|
||||
"ownerName": "Propriétaire",
|
||||
"public": "Public",
|
||||
"public": "Publique",
|
||||
"updatedAt": "Mise à jour le",
|
||||
"createdAt": "Créé le",
|
||||
"createdAt": "Créée le",
|
||||
"songCount": "Titres",
|
||||
"comment": "Commentaire",
|
||||
"sync": "Import automatique",
|
||||
|
@ -174,7 +174,7 @@
|
|||
"name": "Nom",
|
||||
"streamUrl": "Lien du stream",
|
||||
"homePageUrl": "Lien de la page d'accueil",
|
||||
"updatedAt": "Mis à jour le",
|
||||
"updatedAt": "Mise à jour le",
|
||||
"createdAt": "Créée le"
|
||||
},
|
||||
"actions": {
|
||||
|
@ -195,7 +195,7 @@
|
|||
"maxBitRate": "Bitrate maximum",
|
||||
"updatedAt": "Mis à jour le",
|
||||
"createdAt": "Créé le",
|
||||
"downloadable": "Autoriser les téléchargements?"
|
||||
"downloadable": "Autoriser les téléchargements ?"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -274,7 +274,7 @@
|
|||
"not_found": "Page manquante",
|
||||
"show": "%{name} #%{id}",
|
||||
"empty": "Pas encore de %{name}.",
|
||||
"invite": "Voulez-vous en créer un ?"
|
||||
"invite": "Voulez-vous en créer ?"
|
||||
},
|
||||
"input": {
|
||||
"file": {
|
||||
|
@ -362,18 +362,18 @@
|
|||
"musicbrainz": "Ouvrir dans MusicBrainz"
|
||||
},
|
||||
"lastfmLink": "Lire plus...",
|
||||
"listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur: %{user}",
|
||||
"listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz: %{error}",
|
||||
"listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur : %{user}",
|
||||
"listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz : %{error}",
|
||||
"listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés",
|
||||
"listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz",
|
||||
"downloadOriginalFormat": "Télécharger au format original",
|
||||
"shareOriginalFormat": "Partager avec le format original",
|
||||
"shareDialogTitle": "Partager %{resource} '%{name}'",
|
||||
"shareBatchDialogTitle": "Partager 1 %{resource} |||| Partager %{smart_count} %{resource}",
|
||||
"shareSuccess": "Lien copié vers le presse-papier: %{url}",
|
||||
"shareSuccess": "Lien copié vers le presse-papier : %{url}",
|
||||
"shareFailure": "Erreur en copiant le lien %{url} vers le presse-papier",
|
||||
"downloadDialogTitle": "Télécharger %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "Copier vers le presse-papier: Ctrl+C, Enter"
|
||||
"shareCopyToClipboard": "Copier vers le presse-papier : Ctrl+C, Enter"
|
||||
},
|
||||
"menu": {
|
||||
"library": "Bibliothèque",
|
||||
|
@ -390,7 +390,7 @@
|
|||
"lastfmScrobbling": "Scrobbler vers Last.fm",
|
||||
"listenBrainzScrobbling": "Scrobbler vers ListenBrainz",
|
||||
"replaygain": "Mode ReplayGain",
|
||||
"preAmp": "Pre-amplification ReplayGain (dB)",
|
||||
"preAmp": "Pré-amplification ReplayGain (dB)",
|
||||
"gain": {
|
||||
"none": "Désactivé",
|
||||
"album": "Utiliser le gain de l'album",
|
||||
|
@ -457,4 +457,4 @@
|
|||
"current_song": "Aller à la chanson en cours"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -222,6 +222,7 @@ func (t Tags) Channels() int { return t.getInt("channels") }
|
|||
func (t Tags) ModificationTime() time.Time { return t.fileInfo.ModTime() }
|
||||
func (t Tags) Size() int64 { return t.fileInfo.Size() }
|
||||
func (t Tags) FilePath() string { return t.filePath }
|
||||
func (t Tags) Folder() string { return path.Dir(t.filePath) }
|
||||
func (t Tags) Suffix() string { return strings.ToLower(strings.TrimPrefix(path.Ext(t.filePath), ".")) }
|
||||
func (t Tags) BirthTime() time.Time {
|
||||
if ts := times.Get(t.fileInfo); ts.HasBirthTime() {
|
||||
|
@ -230,7 +231,7 @@ func (t Tags) BirthTime() time.Time {
|
|||
return time.Now()
|
||||
}
|
||||
|
||||
// ReplayGain Properties
|
||||
// ReplayGain Properties TODO: check rg_* tags
|
||||
|
||||
func (t Tags) RGAlbumGain() float64 { return t.getGainValue("replaygain_album_gain") }
|
||||
func (t Tags) RGAlbumPeak() float64 { return t.getPeakValue("replaygain_album_peak") }
|
||||
|
@ -384,3 +385,83 @@ func (t Tags) getFloat(tagNames ...string) float64 {
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// We exclude all tags that are already first-class citizens in the model
|
||||
var excludedTags = map[string]struct{}{
|
||||
"duration": {},
|
||||
"length": {},
|
||||
"tlen": {},
|
||||
"bitrate": {},
|
||||
"channels": {},
|
||||
"has_picture": {},
|
||||
"apic": {},
|
||||
"bpm": {},
|
||||
"tbpm": {},
|
||||
"tipl": {},
|
||||
"tcom": {},
|
||||
"tit2": {},
|
||||
"talb": {},
|
||||
"tcon": {},
|
||||
"tpe1": {},
|
||||
"tpe2": {},
|
||||
"title": {},
|
||||
"album": {},
|
||||
"artist": {},
|
||||
"artists": {},
|
||||
"albumartist": {},
|
||||
"albumartists": {},
|
||||
"composer": {},
|
||||
"track": {},
|
||||
"tracknumber": {},
|
||||
"tracktotal": {},
|
||||
"totaltracks": {},
|
||||
"disc": {},
|
||||
"discnumber": {},
|
||||
"disctotal": {},
|
||||
"totaldiscs": {},
|
||||
"year": {},
|
||||
"date": {},
|
||||
"originaldate": {},
|
||||
"releasedate": {},
|
||||
"comm": {},
|
||||
"comment": {},
|
||||
"comment:itunnorm": {},
|
||||
"uslt": {},
|
||||
}
|
||||
|
||||
// Also exclude any tag that starts with one of these prefixes
|
||||
var excludedPrefixes = []string{
|
||||
"musicbrainz",
|
||||
"replaygain",
|
||||
"sort",
|
||||
"lyrics",
|
||||
}
|
||||
|
||||
func isExcludedTag(tagName string) bool {
|
||||
if _, ok := excludedTags[tagName]; ok {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range excludedPrefixes {
|
||||
if strings.HasPrefix(tagName, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t Tags) ModelTags() model.Tags {
|
||||
models := model.Tags{}
|
||||
for tagName, values := range t.Tags {
|
||||
if isExcludedTag(tagName) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
models[tagName] = append(models[tagName], value)
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ func (e *Extractor) extractMetadata(filePath string) (metadata.ParsedTags, error
|
|||
if duration := float64(millis) / 1000.0; duration > 0 {
|
||||
tags["duration"] = []string{strconv.FormatFloat(duration, 'f', 2, 32)}
|
||||
}
|
||||
delete(tags, "lengthinmilliseconds")
|
||||
}
|
||||
// Adjust some ID3 tags
|
||||
parseTIPL(tags)
|
||||
|
|
|
@ -126,7 +126,11 @@ func do_put_map(id C.ulong, key string, val *C.char) {
|
|||
}
|
||||
|
||||
/*
|
||||
As I'm working on the new scanner, I see that the `properties` from TagLib is ill-suited to extract multi-valued ID3 frames. I'll have to change the way we do it for ID3, probably by sending the raw frames to Go and mapping there, instead of relying on the auto-mapped `properties`. I think this would reduce our reliance on C++, while also giving us more flexibility, including parsing the USLT / SYLT frames in Go
|
||||
TODO: Validate this assumption:
|
||||
"As I'm working on the new scanner, I see that the `properties` from TagLib is ill-suited to extract multi-valued
|
||||
ID3 frames. I'll have to change the way we do it for ID3, probably by sending the raw frames to Go and mapping there,
|
||||
instead of relying on the auto-mapped `properties`. I think this would reduce our reliance on C++, while also giving
|
||||
us more flexibility, including parsing the USLT / SYLT frames in Go (https://github.com/n10v/id3v2/pull/64)"
|
||||
*/
|
||||
|
||||
//export go_map_put_int
|
||||
|
|
|
@ -59,7 +59,7 @@ var _ = Describe("playlistImporter", func() {
|
|||
conf.Server.PlaylistsPath = "."
|
||||
ps = newPlaylistImporter(ds, pls, cw, "tests/fixtures/playlists")
|
||||
|
||||
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists")).To(Equal(int64(5)))
|
||||
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists")).To(Equal(int64(6)))
|
||||
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists/subfolder1")).To(Equal(int64(0)))
|
||||
})
|
||||
|
||||
|
|
|
@ -3,11 +3,10 @@ package scanner
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
|
@ -17,11 +16,10 @@ import (
|
|||
|
||||
type Scanner interface {
|
||||
RescanAll(ctx context.Context, fullRescan bool) error
|
||||
Status(mediaFolder string) (*StatusInfo, error)
|
||||
Status(context.Context) (*StatusInfo, error)
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
MediaFolder string
|
||||
Scanning bool
|
||||
LastScan time.Time
|
||||
Count uint32
|
||||
|
@ -41,7 +39,10 @@ type FolderScanner interface {
|
|||
var isScanning sync.Mutex
|
||||
|
||||
type scanner struct {
|
||||
once sync.Once
|
||||
folders map[string]FolderScanner
|
||||
lastScans map[string]time.Time
|
||||
libIds map[string]int
|
||||
status map[string]*scanStatus
|
||||
lock *sync.RWMutex
|
||||
ds model.DataStore
|
||||
|
@ -63,55 +64,56 @@ func New(ds model.DataStore, playlists core.Playlists, cacheWarmer artwork.Cache
|
|||
pls: playlists,
|
||||
broker: broker,
|
||||
folders: map[string]FolderScanner{},
|
||||
lastScans: map[string]time.Time{},
|
||||
libIds: map[string]int{},
|
||||
status: map[string]*scanStatus{},
|
||||
lock: &sync.RWMutex{},
|
||||
cacheWarmer: cacheWarmer,
|
||||
}
|
||||
s.loadFolders()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *scanner) rescan(ctx context.Context, mediaFolder string, fullRescan bool) error {
|
||||
folderScanner := s.folders[mediaFolder]
|
||||
func (s *scanner) rescan(ctx context.Context, library string, fullRescan bool) error {
|
||||
folderScanner := s.folders[library]
|
||||
start := time.Now()
|
||||
|
||||
s.setStatusStart(mediaFolder)
|
||||
defer s.setStatusEnd(mediaFolder, start)
|
||||
s.setStatusStart(library)
|
||||
defer s.setStatusEnd(library, start)
|
||||
|
||||
lastModifiedSince := time.Time{}
|
||||
if !fullRescan {
|
||||
lastModifiedSince = s.getLastModifiedSince(ctx, mediaFolder)
|
||||
log.Debug("Scanning folder", "folder", mediaFolder, "lastModifiedSince", lastModifiedSince)
|
||||
lastModifiedSince = s.lastScans[library]
|
||||
log.Debug("Scanning folder", "folder", library, "lastModifiedSince", lastModifiedSince)
|
||||
} else {
|
||||
log.Debug("Scanning folder (full scan)", "folder", mediaFolder)
|
||||
log.Debug("Scanning folder (full scan)", "folder", library)
|
||||
}
|
||||
|
||||
progress, cancel := s.startProgressTracker(mediaFolder)
|
||||
progress, cancel := s.startProgressTracker(library)
|
||||
defer cancel()
|
||||
|
||||
changeCount, err := folderScanner.Scan(ctx, lastModifiedSince, progress)
|
||||
if err != nil {
|
||||
log.Error("Error importing MediaFolder", "folder", mediaFolder, err)
|
||||
log.Error("Error scanning Library", "folder", library, err)
|
||||
}
|
||||
|
||||
if changeCount > 0 {
|
||||
log.Debug(ctx, "Detected changes in the music folder. Sending refresh event",
|
||||
"folder", mediaFolder, "changeCount", changeCount)
|
||||
"folder", library, "changeCount", changeCount)
|
||||
// Don't use real context, forcing a refresh in all open windows, including the one that triggered the scan
|
||||
s.broker.SendMessage(context.Background(), &events.RefreshResource{})
|
||||
}
|
||||
|
||||
s.updateLastModifiedSince(mediaFolder, start)
|
||||
s.updateLastModifiedSince(library, start)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *scanner) startProgressTracker(mediaFolder string) (chan uint32, context.CancelFunc) {
|
||||
func (s *scanner) startProgressTracker(library string) (chan uint32, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
progress := make(chan uint32, 100)
|
||||
go func() {
|
||||
s.broker.SendMessage(ctx, &events.ScanStatus{Scanning: true, Count: 0, FolderCount: 0})
|
||||
defer func() {
|
||||
if status, ok := s.getStatus(mediaFolder); ok {
|
||||
if status, ok := s.getStatus(library); ok {
|
||||
s.broker.SendMessage(ctx, &events.ScanStatus{
|
||||
Scanning: false,
|
||||
Count: int64(status.fileCount),
|
||||
|
@ -127,7 +129,7 @@ func (s *scanner) startProgressTracker(mediaFolder string) (chan uint32, context
|
|||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
totalFolders, totalFiles := s.incStatusCounter(mediaFolder, count)
|
||||
totalFolders, totalFiles := s.incStatusCounter(library, count)
|
||||
s.broker.SendMessage(ctx, &events.ScanStatus{
|
||||
Scanning: true,
|
||||
Count: int64(totalFiles),
|
||||
|
@ -179,6 +181,8 @@ func (s *scanner) setStatusEnd(folder string, lastUpdate time.Time) {
|
|||
|
||||
func (s *scanner) RescanAll(ctx context.Context, fullRescan bool) error {
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
s.once.Do(s.loadFolders)
|
||||
|
||||
if !isScanning.TryLock() {
|
||||
log.Debug(ctx, "Scanner already running, ignoring request for rescan.")
|
||||
return ErrAlreadyScanning
|
||||
|
@ -198,13 +202,14 @@ func (s *scanner) RescanAll(ctx context.Context, fullRescan bool) error {
|
|||
core.WriteAfterScanMetrics(ctx, s.ds, true)
|
||||
return nil
|
||||
}
|
||||
func (s *scanner) Status(mediaFolder string) (*StatusInfo, error) {
|
||||
status, ok := s.getStatus(mediaFolder)
|
||||
|
||||
func (s *scanner) Status(context.Context) (*StatusInfo, error) {
|
||||
s.once.Do(s.loadFolders)
|
||||
status, ok := s.getStatus(conf.Server.MusicFolder)
|
||||
if !ok {
|
||||
return nil, errors.New("mediaFolder not found")
|
||||
return nil, errors.New("library not found")
|
||||
}
|
||||
return &StatusInfo{
|
||||
MediaFolder: mediaFolder,
|
||||
Scanning: status.active,
|
||||
LastScan: status.lastUpdate,
|
||||
Count: status.fileCount,
|
||||
|
@ -212,40 +217,31 @@ func (s *scanner) Status(mediaFolder string) (*StatusInfo, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *scanner) getLastModifiedSince(ctx context.Context, folder string) time.Time {
|
||||
ms, err := s.ds.Property(ctx).Get(model.PropLastScan + "-" + folder)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if ms == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
i, _ := strconv.ParseInt(ms, 10, 64)
|
||||
return time.Unix(0, i*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func (s *scanner) updateLastModifiedSince(folder string, t time.Time) {
|
||||
millis := t.UnixNano() / int64(time.Millisecond)
|
||||
if err := s.ds.Property(context.TODO()).Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis)); err != nil {
|
||||
id := s.libIds[folder]
|
||||
if err := s.ds.Library(context.Background()).UpdateLastScan(id, t); err != nil {
|
||||
log.Error("Error updating DB after scan", err)
|
||||
}
|
||||
s.lastScans[folder] = t
|
||||
}
|
||||
|
||||
func (s *scanner) loadFolders() {
|
||||
ctx := context.TODO()
|
||||
fs, _ := s.ds.MediaFolder(ctx).GetAll()
|
||||
fs, _ := s.ds.Library(ctx).GetAll()
|
||||
for _, f := range fs {
|
||||
log.Info("Configuring Media Folder", "name", f.Name, "path", f.Path)
|
||||
s.folders[f.Path] = s.newScanner(f)
|
||||
s.lastScans[f.Path] = f.LastScanAt
|
||||
s.libIds[f.Path] = f.ID
|
||||
s.status[f.Path] = &scanStatus{
|
||||
active: false,
|
||||
fileCount: 0,
|
||||
folderCount: 0,
|
||||
lastUpdate: s.getLastModifiedSince(ctx, f.Path),
|
||||
lastUpdate: f.LastScanAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scanner) newScanner(f model.MediaFolder) FolderScanner {
|
||||
func (s *scanner) newScanner(f model.Library) FolderScanner {
|
||||
return NewTagScanner(f.Path, s.ds, s.pls, s.cacheWarmer)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package scanner2
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type folderEntry struct {
|
||||
scanCtx *scanContext
|
||||
path string // Full path
|
||||
id string // DB ID
|
||||
updTime time.Time // From DB
|
||||
modTime time.Time // From FS
|
||||
audioFiles map[string]fs.DirEntry
|
||||
imageFiles map[string]fs.DirEntry
|
||||
playlists []fs.DirEntry
|
||||
imagesUpdatedAt time.Time
|
||||
tracks model.MediaFiles
|
||||
albums model.Albums
|
||||
artists model.Artists
|
||||
tags model.FlattenedTags
|
||||
missingTracks model.MediaFiles
|
||||
}
|
||||
|
||||
func (f *folderEntry) isOutdated() bool {
|
||||
return f.updTime.Before(f.modTime)
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
package scanner2
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/deluan/sanitize"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/scanner/metadata"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
type mediaFileMapper struct {
|
||||
entry *folderEntry
|
||||
}
|
||||
|
||||
func newMediaFileMapper(entry *folderEntry) *mediaFileMapper {
|
||||
return &mediaFileMapper{
|
||||
entry: entry,
|
||||
}
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
|
||||
mf := &model.MediaFile{}
|
||||
mf.ID = s.trackID(md)
|
||||
mf.Year, mf.Date, mf.OriginalYear, mf.OriginalDate, mf.ReleaseYear, mf.ReleaseDate = s.mapDates(md)
|
||||
mf.Title = s.mapTrackTitle(md)
|
||||
mf.Album = md.Album()
|
||||
mf.AlbumID = s.albumID(md, mf.ReleaseDate)
|
||||
mf.Album = s.mapAlbumName(md)
|
||||
mf.ArtistID = s.artistID(md)
|
||||
mf.Artist = s.mapArtistName(md)
|
||||
mf.AlbumArtistID = s.albumArtistID(md)
|
||||
mf.AlbumArtist = s.mapAlbumArtistName(md)
|
||||
mf.Genre, mf.Genres = s.mapGenres(md.Genres())
|
||||
mf.Compilation = md.Compilation()
|
||||
mf.TrackNumber, _ = md.TrackNumber()
|
||||
mf.DiscNumber, _ = md.DiscNumber()
|
||||
mf.DiscSubtitle = md.DiscSubtitle()
|
||||
mf.Duration = md.Duration()
|
||||
mf.BitRate = md.BitRate()
|
||||
mf.Channels = md.Channels()
|
||||
mf.Path = md.FilePath()
|
||||
mf.Suffix = md.Suffix()
|
||||
mf.Size = md.Size()
|
||||
mf.HasCoverArt = md.HasPicture()
|
||||
mf.SortTitle = md.SortTitle()
|
||||
mf.SortAlbumName = md.SortAlbum()
|
||||
mf.SortArtistName = md.SortArtist()
|
||||
mf.SortAlbumArtistName = md.SortAlbumArtist()
|
||||
mf.OrderTitle = strings.TrimSpace(sanitize.Accents(mf.Title))
|
||||
mf.OrderAlbumName = sanitizeFieldForSorting(mf.Album)
|
||||
mf.OrderArtistName = sanitizeFieldForSorting(mf.Artist)
|
||||
mf.OrderAlbumArtistName = sanitizeFieldForSorting(mf.AlbumArtist)
|
||||
mf.CatalogNum = md.CatalogNum()
|
||||
mf.MbzRecordingID = md.MbzRecordingID()
|
||||
mf.MbzReleaseTrackID = md.MbzReleaseTrackID()
|
||||
mf.MbzAlbumID = md.MbzAlbumID()
|
||||
mf.MbzArtistID = md.MbzArtistID()
|
||||
mf.MbzAlbumArtistID = md.MbzAlbumArtistID()
|
||||
mf.MbzAlbumType = md.MbzAlbumType()
|
||||
mf.MbzAlbumComment = md.MbzAlbumComment()
|
||||
mf.RgAlbumGain = md.RGAlbumGain()
|
||||
mf.RgAlbumPeak = md.RGAlbumPeak()
|
||||
mf.RgTrackGain = md.RGTrackGain()
|
||||
mf.RgTrackPeak = md.RGTrackPeak()
|
||||
mf.Comment = utils.SanitizeText(md.Comment())
|
||||
mf.Lyrics = md.Lyrics()
|
||||
mf.Bpm = md.Bpm()
|
||||
mf.CreatedAt = md.BirthTime()
|
||||
mf.UpdatedAt = md.ModificationTime()
|
||||
mf.Tags = md.ModelTags()
|
||||
mf.FolderID = s.entry.id
|
||||
mf.LibraryID = s.entry.scanCtx.lib.ID
|
||||
mf.PID = mf.ID
|
||||
|
||||
return *mf
|
||||
}
|
||||
|
||||
func sanitizeFieldForSorting(originalValue string) string {
|
||||
v := strings.TrimSpace(sanitize.Accents(originalValue))
|
||||
return utils.NoArticle(v)
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapTrackTitle(md metadata.Tags) string {
|
||||
if md.Title() == "" {
|
||||
s := strings.TrimPrefix(md.FilePath(), s.entry.path+string(os.PathSeparator))
|
||||
e := filepath.Ext(s)
|
||||
return strings.TrimSuffix(s, e)
|
||||
}
|
||||
return md.Title()
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapAlbumArtistName(md metadata.Tags) string {
|
||||
switch {
|
||||
case md.AlbumArtist() != "":
|
||||
return md.AlbumArtist()
|
||||
case md.Compilation():
|
||||
return consts.VariousArtists
|
||||
case md.Artist() != "":
|
||||
return md.Artist()
|
||||
default:
|
||||
return consts.UnknownArtist
|
||||
}
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapArtistName(md metadata.Tags) string {
|
||||
if md.Artist() != "" {
|
||||
return md.Artist()
|
||||
}
|
||||
return consts.UnknownArtist
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapAlbumName(md metadata.Tags) string {
|
||||
name := md.Album()
|
||||
if name == "" {
|
||||
return consts.UnknownAlbum
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) trackID(md metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(md.FilePath())))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) albumID(md metadata.Tags, releaseDate string) string {
|
||||
albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapAlbumArtistName(md), s.mapAlbumName(md)))
|
||||
if !conf.Server.Scanner.GroupAlbumReleases {
|
||||
if len(releaseDate) != 0 {
|
||||
albumPath = fmt.Sprintf("%s\\%s", albumPath, releaseDate)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(albumPath)))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) artistID(md metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapArtistName(md)))))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) albumArtistID(md metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapAlbumArtistName(md)))))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapGenres(genres []string) (string, model.Genres) {
|
||||
var result model.Genres
|
||||
unique := map[string]struct{}{}
|
||||
var all []string
|
||||
for i := range genres {
|
||||
gs := strings.FieldsFunc(genres[i], func(r rune) bool {
|
||||
return strings.ContainsRune(conf.Server.Scanner.GenreSeparators, r)
|
||||
})
|
||||
for j := range gs {
|
||||
g := strings.TrimSpace(gs[j])
|
||||
key := strings.ToLower(g)
|
||||
if _, ok := unique[key]; ok {
|
||||
continue
|
||||
}
|
||||
all = append(all, g)
|
||||
unique[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, g := range all {
|
||||
result = append(result, model.Genre{Name: g})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return result[0].Name, result
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapDates(md metadata.Tags) (year int, date string,
|
||||
originalYear int, originalDate string,
|
||||
releaseYear int, releaseDate string) {
|
||||
// Start with defaults
|
||||
year, date = md.Date()
|
||||
originalYear, originalDate = md.OriginalDate()
|
||||
releaseYear, releaseDate = md.ReleaseDate()
|
||||
|
||||
// MusicBrainz Picard writes the Release Date of an album to the Date tag, and leaves the Release Date tag empty
|
||||
taggedLikePicard := (originalYear != 0) &&
|
||||
(releaseYear == 0) &&
|
||||
(year >= originalYear)
|
||||
if taggedLikePicard {
|
||||
return originalYear, originalDate, originalYear, originalDate, year, date
|
||||
}
|
||||
// when there's no Date, first fall back to Original Date, then to Release Date.
|
||||
if year == 0 {
|
||||
if originalYear > 0 {
|
||||
year, date = originalYear, originalDate
|
||||
} else {
|
||||
year, date = releaseYear, releaseDate
|
||||
}
|
||||
}
|
||||
return year, date, originalYear, originalDate, releaseYear, releaseDate
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package scanner2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/go-pipeline/pkg/pipeline"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
)
|
||||
|
||||
func persistChanges(ctx context.Context) pipeline.StageFn[*folderEntry] {
|
||||
return func(entry *folderEntry) (*folderEntry, error) {
|
||||
err := entry.scanCtx.ds.WithTx(func(tx model.DataStore) error {
|
||||
// Save all tags to DB
|
||||
err := slice.RangeByChunks(entry.tags, 100, func(chunk []model.Tag) error {
|
||||
err := tx.Tag(ctx).Add(chunk...)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error adding tags to DB", "folder", entry.path, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save all tracks to DB
|
||||
err = slice.RangeByChunks(entry.tracks, 100, func(chunk []model.MediaFile) error {
|
||||
for i := range chunk {
|
||||
track := chunk[i]
|
||||
err = tx.MediaFile(ctx).Put(&track)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error adding/updating mediafile to DB", "folder", entry.path, "track", track, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save folder to DB
|
||||
err = tx.Folder(ctx).Put(entry.scanCtx.lib, entry.path)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error adding/updating folder to DB", "folder", entry.path, err)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error persisting changes to DB", "folder", entry.path, err)
|
||||
}
|
||||
return entry, err
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package scanner2
|
|
@ -0,0 +1,96 @@
|
|||
package scanner2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/go-pipeline/pkg/pipeline"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/scanner/metadata"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
const (
|
||||
// filesBatchSize used for batching file metadata extraction
|
||||
filesBatchSize = 100
|
||||
)
|
||||
|
||||
func processFolder(ctx context.Context) pipeline.StageFn[*folderEntry] {
|
||||
return func(entry *folderEntry) (*folderEntry, error) {
|
||||
// Load children mediafiles from DB
|
||||
mfs, err := entry.scanCtx.ds.MediaFile(ctx).GetByFolder(entry.id)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error loading mediafiles from DB", "folder", entry.path, err)
|
||||
return entry, err
|
||||
}
|
||||
dbTracks := slice.ToMap(mfs, func(mf model.MediaFile) (string, model.MediaFile) { return mf.Path, mf })
|
||||
|
||||
// Get list of files to import, leave in dbTracks only tracks that are missing
|
||||
var filesToImport []string
|
||||
for afPath, af := range entry.audioFiles {
|
||||
fullPath := filepath.Join(entry.path, afPath)
|
||||
dbTrack, foundInDB := dbTracks[afPath]
|
||||
if !foundInDB || entry.scanCtx.fullRescan {
|
||||
filesToImport = append(filesToImport, fullPath)
|
||||
} else {
|
||||
info, err := af.Info()
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error getting file info", "folder", entry.path, "file", af.Name(), err)
|
||||
return nil, err
|
||||
}
|
||||
if info.ModTime().After(dbTrack.UpdatedAt) {
|
||||
filesToImport = append(filesToImport, fullPath)
|
||||
}
|
||||
}
|
||||
delete(dbTracks, afPath)
|
||||
}
|
||||
|
||||
// Remaining dbTracks are tracks that were not found in the folder, so they should be marked as missing
|
||||
entry.missingTracks = maps.Values(dbTracks)
|
||||
|
||||
if len(filesToImport) > 0 {
|
||||
entry.tracks, entry.tags, err = loadTagsFromFiles(ctx, entry, filesToImport)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error loading tags from files. Skipping", "folder", entry.path, err)
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
entry.albums = loadAlbumsFromTags(ctx, entry)
|
||||
entry.artists = loadArtistsFromTags(ctx, entry)
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
|
||||
func loadTagsFromFiles(ctx context.Context, entry *folderEntry, toImport []string) (model.MediaFiles, model.FlattenedTags, error) {
|
||||
tracks := model.MediaFiles{}
|
||||
uniqueTags := make(map[string]model.Tag)
|
||||
mapper := newMediaFileMapper(entry)
|
||||
err := slice.RangeByChunks(toImport, filesBatchSize, func(chunk []string) error {
|
||||
allFileTags, err := metadata.Extract(toImport...)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error extracting tags from files. Skipping", "folder", entry.path, err)
|
||||
return err
|
||||
}
|
||||
for _, fileTags := range allFileTags {
|
||||
track := mapper.toMediaFile(fileTags)
|
||||
tracks = append(tracks, track)
|
||||
for _, t := range track.Tags.FlattenAll() {
|
||||
uniqueTags[t.ID] = t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return tracks, maps.Values(uniqueTags), err
|
||||
}
|
||||
|
||||
func loadAlbumsFromTags(ctx context.Context, entry *folderEntry) model.Albums {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
func loadArtistsFromTags(ctx context.Context, entry *folderEntry) model.Artists {
|
||||
return nil // TODO
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
package scanner2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-pipeline/pkg/pipeline"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/pl"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func produceFolders(ctx context.Context, ds model.DataStore, libs []model.Library, fullRescan bool) pipeline.ProducerFn[*folderEntry] {
|
||||
scanCtxChan := make(chan *scanContext, len(libs))
|
||||
go func() {
|
||||
defer close(scanCtxChan)
|
||||
for _, lib := range libs {
|
||||
scanCtx, err := newScannerContext(ctx, ds, lib, fullRescan)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error creating scan context", "lib", lib.Name, err)
|
||||
continue
|
||||
}
|
||||
scanCtxChan <- scanCtx
|
||||
}
|
||||
}()
|
||||
return func(put func(entry *folderEntry)) error {
|
||||
// TODO Parallelize multiple scanCtx
|
||||
var total int64
|
||||
for scanCtx := range pl.ReadOrDone(ctx, scanCtxChan) {
|
||||
outputChan, err := walkDirTree(ctx, scanCtx)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error scanning library", "lib", scanCtx.lib.Name, err)
|
||||
}
|
||||
for folder := range pl.ReadOrDone(ctx, outputChan) {
|
||||
put(folder)
|
||||
}
|
||||
total += scanCtx.numFolders.Load()
|
||||
}
|
||||
log.Info(ctx, "Scanner: Finished loading all folders", "numFolders", total)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func walkDirTree(ctx context.Context, scanCtx *scanContext) (<-chan *folderEntry, error) {
|
||||
results := make(chan *folderEntry)
|
||||
go func() {
|
||||
defer close(results)
|
||||
rootFolder := scanCtx.lib.Path
|
||||
err := walkFolder(ctx, scanCtx, rootFolder, results)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: There were errors reading directories from filesystem", "path", rootFolder, err)
|
||||
return
|
||||
}
|
||||
log.Debug(ctx, "Scanner: Finished reading folders", "lib", scanCtx.lib.Name, "path", rootFolder, "numFolders", scanCtx.numFolders.Load())
|
||||
}()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func walkFolder(ctx context.Context, scanCtx *scanContext, currentFolder string, results chan<- *folderEntry) error {
|
||||
folder, children, err := loadDir(ctx, scanCtx, currentFolder)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error loading dir. Skipping", "path", currentFolder, err)
|
||||
return nil
|
||||
}
|
||||
scanCtx.numFolders.Add(1)
|
||||
for _, c := range children {
|
||||
err := walkFolder(ctx, scanCtx, c, results)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !folder.isOutdated() && !scanCtx.fullRescan {
|
||||
return nil
|
||||
}
|
||||
dir := filepath.Clean(currentFolder)
|
||||
log.Trace(ctx, "Scanner: Found directory", "_path", dir, "audioFiles", maps.Keys(folder.audioFiles),
|
||||
"images", maps.Keys(folder.imageFiles), "playlists", folder.playlists, "imagesUpdatedAt", folder.imagesUpdatedAt,
|
||||
"updTime", folder.updTime, "modTime", folder.modTime, "numChildren", len(children))
|
||||
folder.path = dir
|
||||
results <- folder
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDir(ctx context.Context, scanCtx *scanContext, dirPath string) (folder *folderEntry, children []string, err error) {
|
||||
folder = &folderEntry{scanCtx: scanCtx, path: dirPath}
|
||||
folder.id = model.FolderID(scanCtx.lib, dirPath)
|
||||
folder.updTime = scanCtx.getLastUpdatedInDB(folder.id)
|
||||
folder.audioFiles = make(map[string]fs.DirEntry)
|
||||
folder.imageFiles = make(map[string]fs.DirEntry)
|
||||
|
||||
dirInfo, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error stating dir", "path", dirPath, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
folder.modTime = dirInfo.ModTime()
|
||||
|
||||
dir, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error in Opening directory", "path", dirPath, err)
|
||||
return folder, children, err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
for _, entry := range fullReadDir(ctx, dir) {
|
||||
if ctx.Err() != nil {
|
||||
return folder, children, ctx.Err()
|
||||
}
|
||||
isDir, err := isDirOrSymlinkToDir(dirPath, entry)
|
||||
// Skip invalid symlinks
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Invalid symlink", "dir", filepath.Join(dirPath, entry.Name()), err)
|
||||
continue
|
||||
}
|
||||
if isDir && !isDirIgnored(dirPath, entry) && isDirReadable(ctx, dirPath, entry) {
|
||||
children = append(children, filepath.Join(dirPath, entry.Name()))
|
||||
} else {
|
||||
fileInfo, err := entry.Info()
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error getting fileInfo", "name", entry.Name(), err)
|
||||
return folder, children, err
|
||||
}
|
||||
if fileInfo.ModTime().After(folder.modTime) {
|
||||
folder.modTime = fileInfo.ModTime()
|
||||
}
|
||||
switch {
|
||||
case model.IsAudioFile(entry.Name()):
|
||||
folder.audioFiles[entry.Name()] = entry
|
||||
case model.IsValidPlaylist(entry.Name()):
|
||||
folder.playlists = append(folder.playlists, entry)
|
||||
case model.IsImageFile(entry.Name()):
|
||||
folder.imageFiles[entry.Name()] = entry
|
||||
if fileInfo.ModTime().After(folder.imagesUpdatedAt) {
|
||||
folder.imagesUpdatedAt = fileInfo.ModTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return folder, children, nil
|
||||
}
|
||||
|
||||
// fullReadDir reads all files in the folder, skipping the ones with errors.
|
||||
// It also detects when it is "stuck" with an error in the same directory over and over.
|
||||
// In this case, it stops and returns whatever it was able to read until it got stuck.
|
||||
// See discussion here: https://github.com/navidrome/navidrome/issues/1164#issuecomment-881922850
|
||||
func fullReadDir(ctx context.Context, dir fs.ReadDirFile) []fs.DirEntry {
|
||||
var allEntries []fs.DirEntry
|
||||
var prevErrStr = ""
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return []fs.DirEntry{}
|
||||
}
|
||||
entries, err := dir.ReadDir(-1)
|
||||
allEntries = append(allEntries, entries...)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
log.Warn(ctx, "Skipping DirEntry", err)
|
||||
if prevErrStr == err.Error() {
|
||||
log.Error(ctx, "Scanner: Duplicate DirEntry failure, bailing", err)
|
||||
break
|
||||
}
|
||||
prevErrStr = err.Error()
|
||||
}
|
||||
sort.Slice(allEntries, func(i, j int) bool { return allEntries[i].Name() < allEntries[j].Name() })
|
||||
return allEntries
|
||||
}
|
||||
|
||||
// isDirOrSymlinkToDir returns true if and only if the dirEnt represents a file
|
||||
// system directory, or a symbolic link to a directory. Note that if the dirEnt
|
||||
// is not a directory but is a symbolic link, this method will resolve by
|
||||
// sending a request to the operating system to follow the symbolic link.
|
||||
// originally copied from github.com/karrick/godirwalk, modified to use dirEntry for
|
||||
// efficiency for go 1.16 and beyond
|
||||
func isDirOrSymlinkToDir(baseDir string, dirEnt fs.DirEntry) (bool, error) {
|
||||
if dirEnt.IsDir() {
|
||||
return true, nil
|
||||
}
|
||||
if dirEnt.Type()&os.ModeSymlink == 0 {
|
||||
return false, nil
|
||||
}
|
||||
// Does this symlink point to a directory?
|
||||
fileInfo, err := os.Stat(filepath.Join(baseDir, dirEnt.Name()))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return fileInfo.IsDir(), nil
|
||||
}
|
||||
|
||||
// isDirReadable returns true if the directory represented by dirEnt is readable
|
||||
func isDirReadable(ctx context.Context, baseDir string, dirEnt fs.DirEntry) bool {
|
||||
path := filepath.Join(baseDir, dirEnt.Name())
|
||||
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Warn("Scanner: Skipping unreadable directory", "path", path, err)
|
||||
return false
|
||||
}
|
||||
|
||||
err = dir.Close()
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error closing directory", "path", path, err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isDirIgnored returns true if the directory represented by dirEnt contains an
|
||||
// `ignore` file (named after skipScanFile)
|
||||
func isDirIgnored(baseDir string, dirEnt fs.DirEntry) bool {
|
||||
// allows Album folders for albums which eg start with ellipses
|
||||
name := dirEnt.Name()
|
||||
if strings.HasPrefix(name, ".") && !strings.HasPrefix(name, "..") {
|
||||
return true
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" && strings.EqualFold(name, "$RECYCLE.BIN") {
|
||||
return true
|
||||
}
|
||||
_, err := os.Stat(filepath.Join(baseDir, name, consts.SkipScanFile))
|
||||
return err == nil
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package scanner2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type scanContext struct {
|
||||
lib model.Library
|
||||
ds model.DataStore
|
||||
startTime time.Time
|
||||
lastUpdates map[string]time.Time
|
||||
lock sync.RWMutex
|
||||
fullRescan bool
|
||||
numFolders atomic.Int64
|
||||
}
|
||||
|
||||
func newScannerContext(ctx context.Context, ds model.DataStore, lib model.Library, fullRescan bool) (*scanContext, error) {
|
||||
lastUpdates, err := ds.Folder(ctx).GetLastUpdates(lib)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting last updates: %w", err)
|
||||
}
|
||||
return &scanContext{
|
||||
lib: lib,
|
||||
ds: ds,
|
||||
startTime: time.Now(),
|
||||
lastUpdates: lastUpdates,
|
||||
fullRescan: fullRescan,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *scanContext) getLastUpdatedInDB(id string) time.Time {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
t, ok := s.lastUpdates[id]
|
||||
if !ok {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package scanner2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-pipeline/pkg/pipeline"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
)
|
||||
|
||||
type scanner2 struct {
|
||||
processCtx context.Context
|
||||
ds model.DataStore
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ds model.DataStore) scanner.Scanner {
|
||||
return &scanner2{processCtx: ctx, ds: ds}
|
||||
}
|
||||
|
||||
func (s *scanner2) RescanAll(requestCtx context.Context, fullRescan bool) error {
|
||||
ctx := request.AddValues(s.processCtx, requestCtx)
|
||||
|
||||
libs, err := s.ds.Library(ctx).GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
log.Info(ctx, "Scanner: Starting scan", "fullRescan", fullRescan, "numLibraries", len(libs))
|
||||
|
||||
err = s.runPipeline(
|
||||
pipeline.NewProducer(produceFolders(ctx, s.ds, libs, fullRescan), pipeline.Name("read folders from disk")),
|
||||
pipeline.NewStage(processFolder(ctx), pipeline.Name("process folder")),
|
||||
pipeline.NewStage(persistChanges(ctx), pipeline.Name("persist changes")),
|
||||
pipeline.NewStage(logFolder(ctx), pipeline.Name("log results")),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error scanning libraries", "duration", time.Since(startTime), err)
|
||||
} else {
|
||||
log.Info(ctx, "Scanner: Finished scanning all libraries", "duration", time.Since(startTime))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *scanner2) runPipeline(producer pipeline.Producer[*folderEntry], stages ...pipeline.Stage[*folderEntry]) error {
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
metrics, err := pipeline.Measure(producer, stages...)
|
||||
log.Info(metrics.String(), err)
|
||||
return err
|
||||
}
|
||||
return pipeline.Do(producer, stages...)
|
||||
}
|
||||
|
||||
func logFolder(ctx context.Context) func(folder *folderEntry) (out *folderEntry, err error) {
|
||||
return func(folder *folderEntry) (out *folderEntry, err error) {
|
||||
log.Debug(ctx, "Scanner: Completed processing folder", "_path", folder.path,
|
||||
"audioCount", len(folder.audioFiles), "imageCount", len(folder.imageFiles), "plsCount", len(folder.playlists))
|
||||
return folder, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scanner2) Status(context.Context) (*scanner.StatusInfo, error) {
|
||||
return &scanner.StatusInfo{}, nil
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (s *scanner2) doScan(ctx context.Context, fullRescan bool, folders <-chan string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ scanner.Scanner = (*scanner2)(nil)
|
|
@ -199,8 +199,13 @@ func UsernameFromReverseProxyHeader(r *http.Request) string {
|
|||
if conf.Server.ReverseProxyWhitelist == "" && !strings.HasPrefix(conf.Server.Address, "unix:") {
|
||||
return ""
|
||||
}
|
||||
if !validateIPAgainstList(r.RemoteAddr, conf.Server.ReverseProxyWhitelist) {
|
||||
log.Warn(r.Context(), "IP is not whitelisted for reverse proxy login", "ip", r.RemoteAddr)
|
||||
reverseProxyIp, ok := request.ReverseProxyIpFrom(r.Context())
|
||||
if !ok {
|
||||
log.Error("ReverseProxyWhitelist enabled but no proxy IP found in request context. Please report this error.")
|
||||
return ""
|
||||
}
|
||||
if !validateIPAgainstList(reverseProxyIp, conf.Server.ReverseProxyWhitelist) {
|
||||
log.Warn(r.Context(), "IP is not whitelisted for reverse proxy login", "proxy-ip", reverseProxyIp, "client-ip", r.RemoteAddr)
|
||||
return ""
|
||||
}
|
||||
username := r.Header.Get(conf.Server.ReverseProxyUserHeader)
|
||||
|
|
|
@ -11,6 +11,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
|
@ -62,6 +66,13 @@ var _ = Describe("Auth", func() {
|
|||
})
|
||||
|
||||
Describe("Login from HTTP headers", func() {
|
||||
const (
|
||||
trustedIpv4 = "192.168.0.42"
|
||||
untrustedIpv4 = "8.8.8.8"
|
||||
trustedIpv6 = "2001:4860:4860:1234:5678:0000:4242:8888"
|
||||
untrustedIpv6 = "5005:0:3003"
|
||||
)
|
||||
|
||||
fs := os.DirFS("tests/fixtures")
|
||||
|
||||
BeforeEach(func() {
|
||||
|
@ -75,7 +86,7 @@ var _ = Describe("Auth", func() {
|
|||
})
|
||||
|
||||
It("sets auth data if IPv4 matches whitelist", func() {
|
||||
req.RemoteAddr = "192.168.0.42:25293"
|
||||
req = req.WithContext(request.WithReverseProxyIp(req.Context(), trustedIpv4))
|
||||
serveIndex(ds, fs, nil)(resp, req)
|
||||
|
||||
config := extractAppConfig(resp.Body.String())
|
||||
|
@ -85,7 +96,7 @@ var _ = Describe("Auth", func() {
|
|||
})
|
||||
|
||||
It("sets no auth data if IPv4 does not match whitelist", func() {
|
||||
req.RemoteAddr = "8.8.8.8:25293"
|
||||
req = req.WithContext(request.WithReverseProxyIp(req.Context(), untrustedIpv4))
|
||||
serveIndex(ds, fs, nil)(resp, req)
|
||||
|
||||
config := extractAppConfig(resp.Body.String())
|
||||
|
@ -93,7 +104,7 @@ var _ = Describe("Auth", func() {
|
|||
})
|
||||
|
||||
It("sets auth data if IPv6 matches whitelist", func() {
|
||||
req.RemoteAddr = "[2001:4860:4860:1234:5678:0000:4242:8888]:25293"
|
||||
req = req.WithContext(request.WithReverseProxyIp(req.Context(), trustedIpv6))
|
||||
serveIndex(ds, fs, nil)(resp, req)
|
||||
|
||||
config := extractAppConfig(resp.Body.String())
|
||||
|
@ -103,23 +114,28 @@ var _ = Describe("Auth", func() {
|
|||
})
|
||||
|
||||
It("sets no auth data if IPv6 does not match whitelist", func() {
|
||||
req.RemoteAddr = "[5005:0:3003]:25293"
|
||||
req = req.WithContext(request.WithReverseProxyIp(req.Context(), untrustedIpv6))
|
||||
serveIndex(ds, fs, nil)(resp, req)
|
||||
|
||||
config := extractAppConfig(resp.Body.String())
|
||||
Expect(config["auth"]).To(BeNil())
|
||||
})
|
||||
|
||||
It("sets no auth data if user does not exist", func() {
|
||||
req.Header.Set("Remote-User", "INVALID_USER")
|
||||
It("creates user and sets auth data if user does not exist", func() {
|
||||
newUser := "NEW_USER_" + uuid.NewString()
|
||||
|
||||
req = req.WithContext(request.WithReverseProxyIp(req.Context(), trustedIpv4))
|
||||
req.Header.Set("Remote-User", newUser)
|
||||
serveIndex(ds, fs, nil)(resp, req)
|
||||
|
||||
config := extractAppConfig(resp.Body.String())
|
||||
Expect(config["auth"]).To(BeNil())
|
||||
parsed := config["auth"].(map[string]interface{})
|
||||
|
||||
Expect(parsed["username"]).To(Equal(newUser))
|
||||
})
|
||||
|
||||
It("sets auth data if user exists", func() {
|
||||
req.RemoteAddr = "192.168.0.42:25293"
|
||||
req = req.WithContext(request.WithReverseProxyIp(req.Context(), trustedIpv4))
|
||||
serveIndex(ds, fs, nil)(resp, req)
|
||||
|
||||
config := extractAppConfig(resp.Body.String())
|
||||
|
|
|
@ -15,8 +15,13 @@ import (
|
|||
)
|
||||
|
||||
func initialSetup(ds model.DataStore) {
|
||||
ctx := context.TODO()
|
||||
_ = ds.WithTx(func(tx model.DataStore) error {
|
||||
properties := ds.Property(context.TODO())
|
||||
if err := ds.Library(ctx).StoreMusicFolder(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
properties := ds.Property(ctx)
|
||||
_, err := properties.Get(consts.InitialSetupFlagKey)
|
||||
if err == nil {
|
||||
return nil
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
@ -158,13 +159,35 @@ func clientUniqueIDMiddleware(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
// realIPMiddleware wraps the middleware.RealIP middleware function, bypassing it if the
|
||||
// ReverseProxyWhitelist configuration option is set.
|
||||
func realIPMiddleware(h http.Handler) http.Handler {
|
||||
if conf.Server.ReverseProxyWhitelist == "" {
|
||||
return middleware.RealIP(h)
|
||||
// realIPMiddleware applies middleware.RealIP, and additionally saves the request's original RemoteAddr to the request's
|
||||
// context if navidrome is behind a trusted reverse proxy.
|
||||
func realIPMiddleware(next http.Handler) http.Handler {
|
||||
if conf.Server.ReverseProxyWhitelist != "" {
|
||||
return chi.Chain(
|
||||
reqToCtx(request.ReverseProxyIp, func(r *http.Request) any { return r.RemoteAddr }),
|
||||
middleware.RealIP,
|
||||
).Handler(next)
|
||||
}
|
||||
|
||||
// The middleware is applied without a trusted reverse proxy to support other use-cases such as multiple clients
|
||||
// behind a caching proxy. In this case, navidrome only uses the request's RemoteAddr for logging, so the security
|
||||
// impact of reading the headers from untrusted sources is limited.
|
||||
return middleware.RealIP(next)
|
||||
}
|
||||
|
||||
// reqToCtx creates a middleware that updates the request's context with a value computed from the request. A given key
|
||||
// can only be set once.
|
||||
func reqToCtx(key any, fn func(req *http.Request) any) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Context().Value(key) == nil {
|
||||
ctx := context.WithValue(r.Context(), key, fn(r))
|
||||
r = r.WithContext(ctx)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// serverAddressMiddleware is a middleware function that modifies the request object
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
|
@ -18,10 +17,10 @@ import (
|
|||
)
|
||||
|
||||
func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) {
|
||||
mediaFolderList, _ := api.ds.MediaFolder(r.Context()).GetAll()
|
||||
folders := make([]responses.MusicFolder, len(mediaFolderList))
|
||||
for i, f := range mediaFolderList {
|
||||
folders[i].Id = f.ID
|
||||
libraries, _ := api.ds.Library(r.Context()).GetAll()
|
||||
folders := make([]responses.MusicFolder, len(libraries))
|
||||
for i, f := range libraries {
|
||||
folders[i].Id = int32(f.ID)
|
||||
folders[i].Name = f.Name
|
||||
}
|
||||
response := newResponse()
|
||||
|
@ -29,24 +28,16 @@ func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error)
|
|||
return response, nil
|
||||
}
|
||||
|
||||
func (api *Router) getArtistIndex(r *http.Request, mediaFolderId int, ifModifiedSince time.Time) (*responses.Indexes, error) {
|
||||
func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince time.Time) (*responses.Indexes, error) {
|
||||
ctx := r.Context()
|
||||
folder, err := api.ds.MediaFolder(ctx).Get(int32(mediaFolderId))
|
||||
lib, err := api.ds.Library(ctx).Get(libId)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving MediaFolder", "id", mediaFolderId, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := api.ds.Property(ctx).DefaultGet(model.PropLastScan+"-"+folder.Path, "-1")
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving LastScan property", err)
|
||||
log.Error(ctx, "Error retrieving Library", "id", libId, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var indexes model.ArtistIndexes
|
||||
ms, _ := strconv.ParseInt(l, 10, 64)
|
||||
lastModified := utils.ToTime(ms)
|
||||
if lastModified.After(ifModifiedSince) {
|
||||
if lib.LastScanAt.After(ifModifiedSince) {
|
||||
indexes, err = api.ds.Artist(ctx).GetIndex()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving Indexes", err)
|
||||
|
@ -56,7 +47,7 @@ func (api *Router) getArtistIndex(r *http.Request, mediaFolderId int, ifModified
|
|||
|
||||
res := &responses.Indexes{
|
||||
IgnoredArticles: conf.Server.IgnoredArticles,
|
||||
LastModified: utils.ToMillis(lastModified),
|
||||
LastModified: utils.ToMillis(lib.LastScanAt),
|
||||
}
|
||||
|
||||
res.Index = make([]responses.Index, len(indexes))
|
||||
|
|
|
@ -183,6 +183,7 @@ func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child
|
|||
TrackPeak: mf.RgTrackPeak,
|
||||
AlbumPeak: mf.RgAlbumPeak,
|
||||
}
|
||||
child.ChannelCount = int32(mf.Channels)
|
||||
return child
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
|
@ -12,10 +11,8 @@ import (
|
|||
)
|
||||
|
||||
func (api *Router) GetScanStatus(r *http.Request) (*responses.Subsonic, error) {
|
||||
// TODO handle multiple mediafolders
|
||||
ctx := r.Context()
|
||||
mediaFolder := conf.Server.MusicFolder
|
||||
status, err := api.scanner.Status(mediaFolder)
|
||||
status, err := api.scanner.Status(ctx)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving Scanner status", err)
|
||||
return nil, newError(responses.ErrorGeneric, "Internal Error")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package subsonic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
@ -19,6 +18,7 @@ import (
|
|||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/navidrome/navidrome/utils/gg"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
|
@ -44,7 +44,15 @@ func postFormToQueryParams(next http.Handler) http.Handler {
|
|||
|
||||
func checkRequiredParameters(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requiredParameters := []string{"u", "v", "c"}
|
||||
var requiredParameters []string
|
||||
var username string
|
||||
|
||||
if username = server.UsernameFromReverseProxyHeader(r); username != "" {
|
||||
requiredParameters = []string{"v", "c"}
|
||||
} else {
|
||||
requiredParameters = []string{"u", "v", "c"}
|
||||
}
|
||||
|
||||
p := req.Params(r)
|
||||
for _, param := range requiredParameters {
|
||||
if _, err := p.String(param); err != nil {
|
||||
|
@ -54,17 +62,19 @@ func checkRequiredParameters(next http.Handler) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
username, _ := p.String("u")
|
||||
if username == "" {
|
||||
username, _ = p.String("u")
|
||||
}
|
||||
client, _ := p.String("c")
|
||||
version, _ := p.String("v")
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = request.WithUsername(ctx, username)
|
||||
ctx = request.WithClient(ctx, client)
|
||||
ctx = request.WithVersion(ctx, version)
|
||||
log.Debug(ctx, "API: New request "+r.URL.Path, "username", username, "client", client, "version", version)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -72,19 +82,36 @@ func authenticate(ds model.DataStore) func(next http.Handler) http.Handler {
|
|||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
p := req.Params(r)
|
||||
username, _ := p.String("u")
|
||||
|
||||
pass, _ := p.String("p")
|
||||
token, _ := p.String("t")
|
||||
salt, _ := p.String("s")
|
||||
jwt, _ := p.String("jwt")
|
||||
var usr *model.User
|
||||
var err error
|
||||
|
||||
usr, err := validateUser(ctx, ds, username, pass, token, salt, jwt)
|
||||
if errors.Is(err, model.ErrInvalidAuth) {
|
||||
log.Warn(ctx, "API: Invalid login", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
} else if err != nil {
|
||||
log.Error(ctx, "API: Error authenticating username", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
if username := server.UsernameFromReverseProxyHeader(r); username != "" {
|
||||
usr, err = ds.User(ctx).FindByUsername(username)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
log.Warn(ctx, "API: Invalid login", "auth", "reverse-proxy", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
} else if err != nil {
|
||||
log.Error(ctx, "API: Error authenticating username", "auth", "reverse-proxy", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
}
|
||||
} else {
|
||||
p := req.Params(r)
|
||||
username, _ := p.String("u")
|
||||
pass, _ := p.String("p")
|
||||
token, _ := p.String("t")
|
||||
salt, _ := p.String("s")
|
||||
jwt, _ := p.String("jwt")
|
||||
|
||||
usr, err = ds.User(ctx).FindByUsernameWithPassword(username)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
log.Warn(ctx, "API: Invalid login", "auth", "subsonic", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
} else if err != nil {
|
||||
log.Error(ctx, "API: Error authenticating username", "auth", "subsonic", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
}
|
||||
|
||||
err = validateCredentials(usr, pass, token, salt, jwt)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "API: Invalid login", "auth", "subsonic", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -100,23 +127,13 @@ func authenticate(ds model.DataStore) func(next http.Handler) http.Handler {
|
|||
// }
|
||||
//}()
|
||||
|
||||
ctx = log.NewContext(r.Context(), "username", username)
|
||||
ctx = request.WithUser(ctx, *usr)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validateUser(ctx context.Context, ds model.DataStore, username, pass, token, salt, jwt string) (*model.User, error) {
|
||||
user, err := ds.User(ctx).FindByUsernameWithPassword(username)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, model.ErrInvalidAuth
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func validateCredentials(user *model.User, pass, token, salt, jwt string) error {
|
||||
valid := false
|
||||
|
||||
switch {
|
||||
|
@ -136,9 +153,9 @@ func validateUser(ctx context.Context, ds model.DataStore, username, pass, token
|
|||
}
|
||||
|
||||
if !valid {
|
||||
return nil, model.ErrInvalidAuth
|
||||
return model.ErrInvalidAuth
|
||||
}
|
||||
return user, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPlayer(players core.Players) func(next http.Handler) http.Handler {
|
||||
|
@ -148,11 +165,11 @@ func getPlayer(players core.Players) func(next http.Handler) http.Handler {
|
|||
userName, _ := request.UsernameFrom(ctx)
|
||||
client, _ := request.ClientFrom(ctx)
|
||||
playerId := playerIDFromCookie(r, userName)
|
||||
ip, _, _ := net.SplitHostPort(realIP(r))
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
userAgent := canonicalUserAgent(r)
|
||||
player, trc, err := players.Register(ctx, playerId, client, userAgent, ip)
|
||||
if err != nil {
|
||||
log.Error(r.Context(), "Could not register player", "username", userName, "client", client, err)
|
||||
log.Error(ctx, "Could not register player", "username", userName, "client", client, err)
|
||||
} else {
|
||||
ctx = request.WithPlayer(ctx, *player)
|
||||
if trc != nil {
|
||||
|
@ -185,19 +202,6 @@ func canonicalUserAgent(r *http.Request) string {
|
|||
return userAgent
|
||||
}
|
||||
|
||||
func realIP(r *http.Request) string {
|
||||
if xrip := r.Header.Get("X-Real-IP"); xrip != "" {
|
||||
return xrip
|
||||
} else if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
i := strings.Index(xff, ", ")
|
||||
if i == -1 {
|
||||
i = len(xff)
|
||||
}
|
||||
return xff[:i]
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
func playerIDFromCookie(r *http.Request, userName string) string {
|
||||
cookieName := playerIDCookieName(userName)
|
||||
var playerId string
|
||||
|
|
|
@ -76,7 +76,7 @@ var _ = Describe("Middlewares", func() {
|
|||
})
|
||||
|
||||
Describe("CheckParams", func() {
|
||||
It("passes when all required params are available", func() {
|
||||
It("passes when all required params are available (subsonicauth case)", func() {
|
||||
r := newGetRequest("u=user", "v=1.15", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
@ -91,6 +91,27 @@ var _ = Describe("Middlewares", func() {
|
|||
Expect(next.called).To(BeTrue())
|
||||
})
|
||||
|
||||
It("passes when all required params are available (reverse-proxy case)", func() {
|
||||
conf.Server.ReverseProxyWhitelist = "127.0.0.234/32"
|
||||
conf.Server.ReverseProxyUserHeader = "Remote-User"
|
||||
|
||||
r := newGetRequest("v=1.15", "c=test")
|
||||
r.Header.Add("Remote-User", "user")
|
||||
r = r.WithContext(request.WithReverseProxyIp(r.Context(), "127.0.0.234"))
|
||||
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
username, _ := request.UsernameFrom(next.req.Context())
|
||||
Expect(username).To(Equal("user"))
|
||||
version, _ := request.VersionFrom(next.req.Context())
|
||||
Expect(version).To(Equal("1.15"))
|
||||
client, _ := request.ClientFrom(next.req.Context())
|
||||
Expect(client).To(Equal("test"))
|
||||
|
||||
Expect(next.called).To(BeTrue())
|
||||
})
|
||||
|
||||
It("fails when user is missing", func() {
|
||||
r := newGetRequest("v=1.15", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
|
@ -127,6 +148,7 @@ var _ = Describe("Middlewares", func() {
|
|||
NewPassword: "wordpass",
|
||||
})
|
||||
})
|
||||
|
||||
It("passes authentication with correct credentials", func() {
|
||||
r := newGetRequest("u=admin", "p=wordpass")
|
||||
cp := authenticate(ds)(next)
|
||||
|
@ -226,77 +248,85 @@ var _ = Describe("Middlewares", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("validateUser", func() {
|
||||
Describe("validateCredentials", func() {
|
||||
var usr *model.User
|
||||
|
||||
BeforeEach(func() {
|
||||
ur := ds.User(context.TODO())
|
||||
_ = ur.Put(&model.User{
|
||||
UserName: "admin",
|
||||
NewPassword: "wordpass",
|
||||
})
|
||||
|
||||
var err error
|
||||
usr, err = ur.FindByUsernameWithPassword("admin")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
Context("Plaintext password", func() {
|
||||
It("authenticates with plaintext password ", func() {
|
||||
usr, err := validateUser(context.TODO(), ds, "admin", "wordpass", "", "", "")
|
||||
err := validateCredentials(usr, "wordpass", "", "", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(usr.UserName).To(Equal("admin"))
|
||||
})
|
||||
|
||||
It("fails authentication with wrong password", func() {
|
||||
_, err := validateUser(context.TODO(), ds, "admin", "INVALID", "", "", "")
|
||||
err := validateCredentials(usr, "INVALID", "", "", "")
|
||||
Expect(err).To(MatchError(model.ErrInvalidAuth))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Encoded password", func() {
|
||||
It("authenticates with simple encoded password ", func() {
|
||||
usr, err := validateUser(context.TODO(), ds, "admin", "enc:776f726470617373", "", "", "")
|
||||
err := validateCredentials(usr, "enc:776f726470617373", "", "", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(usr.UserName).To(Equal("admin"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Token based authentication", func() {
|
||||
It("authenticates with token based authentication", func() {
|
||||
usr, err := validateUser(context.TODO(), ds, "admin", "", "23b342970e25c7928831c3317edd0b67", "retnlmjetrymazgkt", "")
|
||||
err := validateCredentials(usr, "", "23b342970e25c7928831c3317edd0b67", "retnlmjetrymazgkt", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(usr.UserName).To(Equal("admin"))
|
||||
})
|
||||
|
||||
It("fails if salt is missing", func() {
|
||||
_, err := validateUser(context.TODO(), ds, "admin", "", "23b342970e25c7928831c3317edd0b67", "", "")
|
||||
err := validateCredentials(usr, "", "23b342970e25c7928831c3317edd0b67", "", "")
|
||||
Expect(err).To(MatchError(model.ErrInvalidAuth))
|
||||
})
|
||||
})
|
||||
|
||||
Context("JWT based authentication", func() {
|
||||
var usr *model.User
|
||||
var validToken string
|
||||
|
||||
BeforeEach(func() {
|
||||
conf.Server.SessionTimeout = time.Minute
|
||||
auth.Init(ds)
|
||||
|
||||
u := &model.User{UserName: "admin"}
|
||||
usr = &model.User{UserName: "admin"}
|
||||
var err error
|
||||
validToken, err = auth.CreateToken(u)
|
||||
validToken, err = auth.CreateToken(usr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
It("authenticates with JWT token based authentication", func() {
|
||||
usr, err := validateUser(context.TODO(), ds, "admin", "", "", "", validToken)
|
||||
err := validateCredentials(usr, "", "", "", validToken)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(usr.UserName).To(Equal("admin"))
|
||||
})
|
||||
|
||||
It("fails if JWT token is invalid", func() {
|
||||
_, err := validateUser(context.TODO(), ds, "admin", "", "", "", "invalid.token")
|
||||
err := validateCredentials(usr, "", "", "", "invalid.token")
|
||||
Expect(err).To(MatchError(model.ErrInvalidAuth))
|
||||
})
|
||||
|
||||
It("fails if JWT token sub is different than username", func() {
|
||||
u := &model.User{UserName: "hacker"}
|
||||
validToken, _ = auth.CreateToken(u)
|
||||
_, err := validateUser(context.TODO(), ds, "admin", "", "", "", validToken)
|
||||
err := validateCredentials(usr, "", "", "", validToken)
|
||||
Expect(err).To(MatchError(model.ErrInvalidAuth))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<albumList>
|
||||
<album id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<album id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</album>
|
||||
</albumList>
|
||||
|
|
|
@ -85,7 +85,8 @@
|
|||
"albumPeak": 4,
|
||||
"baseGain": 5,
|
||||
"fallbackGain": 6
|
||||
}
|
||||
},
|
||||
"channelCount": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<discTitles disc="3" title=""></discTitles>
|
||||
<originalReleaseDate year="1994" month="2" day="4"></originalReleaseDate>
|
||||
<releaseDate year="2000" month="5" day="10"></releaseDate>
|
||||
<song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321">
|
||||
<song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321" channelCount="0">
|
||||
<genres name="rock"></genres>
|
||||
<genres name="progressive"></genres>
|
||||
<replayGain trackGain="1" albumGain="2" trackPeak="3" albumPeak="4" baseGain="5" fallbackGain="6"></replayGain>
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
},
|
||||
"position": 123,
|
||||
"username": "user2",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<bookmarks>
|
||||
<bookmark position="123" username="user2" comment="a comment" created="0001-01-01T00:00:00Z" changed="0001-01-01T00:00:00Z">
|
||||
<entry id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<entry id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</entry>
|
||||
</bookmark>
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
"albumPeak": 4,
|
||||
"baseGain": 5,
|
||||
"fallbackGain": 6
|
||||
}
|
||||
},
|
||||
"channelCount": 2
|
||||
}
|
||||
],
|
||||
"id": "1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<directory id="1" name="N">
|
||||
<child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="" mediaType="song" musicBrainzId="4321">
|
||||
<child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="" mediaType="song" musicBrainzId="4321" channelCount="2">
|
||||
<genres name="rock"></genres>
|
||||
<genres name="progressive"></genres>
|
||||
<replayGain trackGain="1" albumGain="2" trackPeak="3" albumPeak="4" baseGain="5" fallbackGain="6"></replayGain>
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
],
|
||||
"id": "",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<directory id="" name="">
|
||||
<child id="1" isDir="false" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<child id="1" isDir="false" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</child>
|
||||
</directory>
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
],
|
||||
"id": "1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<directory id="1" name="N">
|
||||
<child id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<child id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</child>
|
||||
</directory>
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
],
|
||||
"current": "111",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<playQueue current="111" position="243" username="user1" changed="0001-01-01T00:00:00Z" changedBy="a_client">
|
||||
<entry id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<entry id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</entry>
|
||||
</playQueue>
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
|
@ -38,7 +39,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
],
|
||||
"id": "ABC123",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<shares>
|
||||
<share id="ABC123" url="http://localhost/p/ABC123" description="Check it out!" username="deluan" created="0001-01-01T00:00:00Z" expires="0001-01-01T00:00:00Z" lastVisited="0001-01-01T00:00:00Z" visitCount="2">
|
||||
<entry id="1" isDir="false" title="title" album="album" artist="artist" duration="120" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<entry id="1" isDir="false" title="title" album="album" artist="artist" duration="120" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</entry>
|
||||
<entry id="2" isDir="false" title="title 2" album="album" artist="artist" duration="300" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<entry id="2" isDir="false" title="title 2" album="album" artist="artist" duration="300" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</entry>
|
||||
</share>
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<similarSongs>
|
||||
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</song>
|
||||
</similarSongs>
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<similarSongs2>
|
||||
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</song>
|
||||
</similarSongs2>
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"mediaType": "",
|
||||
"musicBrainzId": "",
|
||||
"genres": [],
|
||||
"replayGain": {}
|
||||
"replayGain": {},
|
||||
"channelCount": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<topSongs>
|
||||
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="">
|
||||
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0">
|
||||
<replayGain></replayGain>
|
||||
</song>
|
||||
</topSongs>
|
||||
|
|
|
@ -163,6 +163,7 @@ type Child struct {
|
|||
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
|
||||
Genres ItemGenres `xml:"genres" json:"genres"`
|
||||
ReplayGain ReplayGain `xml:"replayGain" json:"replayGain"`
|
||||
ChannelCount int32 `xml:"channelCount,attr" json:"channelCount"`
|
||||
}
|
||||
|
||||
type Songs struct {
|
||||
|
|
|
@ -142,7 +142,7 @@ var _ = Describe("Responses", func() {
|
|||
Year: 1985, Genre: "Rock", CoverArt: "1", Size: 8421341, ContentType: "audio/flac",
|
||||
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
|
||||
Duration: 146, BitRate: 320, Starred: &t, Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
|
||||
Comment: "a comment", Bpm: 127, MediaType: MediaTypeSong, MusicBrainzId: "4321",
|
||||
Comment: "a comment", Bpm: 127, MediaType: MediaTypeSong, MusicBrainzId: "4321", ChannelCount: 2,
|
||||
ReplayGain: ReplayGain{TrackGain: 1, AlbumGain: 2, TrackPeak: 3, AlbumPeak: 4, BaseGain: 5, FallbackGain: 6},
|
||||
}
|
||||
response.Directory.Child = child
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
Top Level Comment
|
||||
*/
|
||||
{
|
||||
"name": "Recently Played",
|
||||
"comment": "Recently played tracks",
|
||||
"all": [
|
||||
// This is an inline comment
|
||||
{"inTheLast": {"lastPlayed": 30}}
|
||||
],
|
||||
"sort": "lastPlayed",
|
||||
"order": "desc",
|
||||
"limit": 100
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue