Merge branch 'master' into subsonic-reverse-proxy-auth/2557

This commit is contained in:
Jeremiah Menétrey 2024-04-26 17:03:27 +02:00
commit 3c4b9118a0
482 changed files with 18536 additions and 6422 deletions

View File

@ -2,7 +2,7 @@
# [Choice] Go version: 1, 1.15, 1.14
ARG VARIANT="1"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT}
# [Option] Install Node.js
ARG INSTALL_NODE="true"
@ -17,4 +17,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# RUN go get -x <your-dependency-or-tool>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View File

@ -4,10 +4,10 @@
"dockerfile": "Dockerfile",
"args": {
// Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14
"VARIANT": "1.21",
"VARIANT": "1.22",
// Options
"INSTALL_NODE": "true",
"NODE_VERSION": "v18"
"NODE_VERSION": "v20"
}
},
"workspaceMount": "",

View File

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

View File

@ -1,4 +1,4 @@
name: 'Pipeline: Test, Lint, Build'
name: "Pipeline: Test, Lint, Build"
on:
push:
branches:
@ -8,23 +8,20 @@ on:
pull_request:
branches:
- master
jobs:
go-lint:
name: Lint Go code
runs-on: ubuntu-latest
container: deluan/ci-goreleaser:1.22.2-1
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- uses: actions/checkout@v4
- name: Set up Go 1.21
uses: actions/setup-go@v3
with:
go-version: 1.21.x
- uses: actions/checkout@v3
- name: Config workspace folder as trusted
run: git config --global --add safe.directory $GITHUB_WORKSPACE; git describe --dirty --always --tags
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v4
with:
version: latest
github-token: ${{ secrets.GITHUB_TOKEN }}
@ -44,23 +41,15 @@ jobs:
fi
go:
name: Test with Go ${{ matrix.go_version }}
name: Test Go code
runs-on: ubuntu-latest
strategy:
matrix:
go_version: [1.21.x,1.20.x]
container: deluan/ci-goreleaser:1.22.2-1
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go_version }}
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go_version }}
cache: true
- name: Config workspace folder as trusted
run: git config --global --add safe.directory $GITHUB_WORKSPACE; git describe --dirty --always --tags
- name: Download dependencies
if: steps.cache-go.outputs.cache-hit != 'true'
@ -75,14 +64,14 @@ jobs:
name: Build JS bundle
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max_old_space_size=4096'
NODE_OPTIONS: "--max_old_space_size=4096"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: 20
cache: "npm"
cache-dependency-path: "**/package-lock.json"
- name: npm install dependencies
run: |
@ -104,7 +93,7 @@ jobs:
cd ui
npm run build
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: js-bundle
path: ui/build
@ -114,41 +103,34 @@ jobs:
name: Build binaries
needs: [js, go, go-lint]
runs-on: ubuntu-latest
container: deluan/ci-goreleaser:1.22.2-1
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v3
- name: Config workspace folder as trusted
run: git config --global --add safe.directory $GITHUB_WORKSPACE; git describe --dirty --always --tags
- uses: actions/download-artifact@v4
with:
name: js-bundle
path: ui/build
- name: Config /github/workspace folder as trusted
uses: docker://deluan/ci-goreleaser:1.21.0-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: /bin/bash -c "git config --global --add safe.directory /github/workspace; git describe --dirty --always --tags"
- name: Run GoReleaser - SNAPSHOT
if: startsWith(github.ref, 'refs/tags/') != true
uses: docker://deluan/ci-goreleaser:1.21.0-1
run: goreleaser release --clean --skip=publish --snapshot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist --skip-publish --snapshot
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.21.0-1
run: goreleaser release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: binaries
path: |
@ -166,18 +148,18 @@ jobs:
steps:
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
if: env.DOCKER_IMAGE != ''
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
if: env.DOCKER_IMAGE != ''
- uses: actions/checkout@v3
- uses: actions/checkout@v4
if: env.DOCKER_IMAGE != ''
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
if: env.DOCKER_IMAGE != ''
with:
name: binaries
@ -185,14 +167,14 @@ jobs:
- name: Login to Docker Hub
if: env.DOCKER_IMAGE != ''
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: env.DOCKER_IMAGE != ''
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -201,12 +183,12 @@ jobs:
- name: Extract metadata for Docker
if: env.DOCKER_IMAGE != ''
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
labels: |
maintainer=deluan
images: |
name=${{secrets.DOCKER_IMAGE}},enable=${{env.GITHUB_REF_TYPE == 'tag' || github.ref == format('refs/heads/{0}', 'master')}}
name=${{secrets.DOCKER_IMAGE}}
name=ghcr.io/${{ github.repository }}
tags: |
type=ref,event=pr
@ -215,7 +197,7 @@ jobs:
- name: Build and Push
if: env.DOCKER_IMAGE != ''
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: .github/workflows/pipeline.dockerfile

View File

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'navidrome' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get updated translations
env:
POEDITOR_PROJECTID: ${{ secrets.POEDITOR_PROJECTID }}
@ -20,7 +20,7 @@ jobs:
git status --porcelain
git diff
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.PAT }}
commit-message: Update translations

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ navidrome.db-wal
tags
.gitinfo
docker-compose.yml
!contrib/docker-compose.yml
!contrib/docker-compose.yml
test-123.db

2
.nvmrc
View File

@ -1 +1 @@
v18
v20

View File

@ -9,7 +9,7 @@ GIT_SHA=source_archive
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
endif
CI_RELEASER_VERSION=1.21.0-1 ## https://github.com/navidrome/ci-goreleaser
CI_RELEASER_VERSION=1.22.2-1 ## https://github.com/navidrome/ci-goreleaser
setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and prepare development environment
@echo Downloading Node dependencies...
@ -47,7 +47,7 @@ lintall: lint ##@Development Lint Go and JS code
format: ##@Development Format code
@(cd ./ui && npm run prettier)
@go run golang.org/x/tools/cmd/goimports -w `find . -name '*.go' | grep -v _gen.go$$`
@go run golang.org/x/tools/cmd/goimports@latest -w `find . -name '*.go' | grep -v _gen.go$$`
@go mod tidy
.PHONY: format
@ -94,8 +94,9 @@ buildjs: check_node_env ##@Build Build only frontend
.PHONY: buildjs
all: warning-noui-build ##@Cross_Compilation Build binaries for all supported platforms. It does not build the frontend
@echo "Building binaries for all platforms using builder ${CI_RELEASER_VERSION}"
docker run -t -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:$(CI_RELEASER_VERSION) \
goreleaser release --rm-dist --skip-publish --snapshot
goreleaser release --clean --skip=publish --snapshot
.PHONY: all
single: warning-noui-build ##@Cross_Compilation Build binaries for a single supported platforms. It does not build the frontend
@ -105,11 +106,17 @@ single: warning-noui-build ##@Cross_Compilation Build binaries for a single supp
grep -- "- id: navidrome_" .goreleaser.yml | sed 's/- id: navidrome_//g'; \
exit 1; \
fi
@echo "Building binaries for ${GOOS}/${GOARCH}"
@echo "Building binaries for ${GOOS}/${GOARCH} using builder ${CI_RELEASER_VERSION}"
docker run -t -v $(PWD):/workspace -e GOOS -e GOARCH -w /workspace deluan/ci-goreleaser:$(CI_RELEASER_VERSION) \
goreleaser build --rm-dist --snapshot --single-target --id navidrome_${GOOS}_${GOARCH}
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

View File

@ -7,7 +7,7 @@
[![Downloads](https://img.shields.io/github/downloads/navidrome/navidrome/total?logo=github&style=flat-square)](https://github.com/navidrome/navidrome/releases/latest)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?logo=docker&label=pulls&style=flat-square)](https://hub.docker.com/r/deluan/navidrome)
[![Dev Chat](https://img.shields.io/discord/671335427726114836?logo=discord&label=discord&style=flat-square)](https://discord.gg/xh7j7yF)
[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/navidrome?logo=reddit&label=/r/navidrome&style=flat-square)](https://www.reddit.com/r/navidrome/)
[![Subreddit](https://img.shields.io/badge/%2Fr%2Fnavidrome-%2B3000-red?logo=reddit)](https://www.reddit.com/r/navidrome/)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0-ff69b4.svg?style=flat-square)](CODE_OF_CONDUCT.md)
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your

99
cmd/inspect.go Normal file
View File

@ -0,0 +1,99 @@
package cmd
import (
"encoding/json"
"fmt"
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/scanner/metadata"
"github.com/navidrome/navidrome/tests"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
var (
extractor string
format string
)
func init() {
inspectCmd.Flags().StringVarP(&extractor, "extractor", "x", "", "extractor to use (ffmpeg or taglib, default: auto)")
inspectCmd.Flags().StringVarP(&format, "format", "f", "pretty", "output format (pretty, toml, yaml, json, jsonindent)")
rootCmd.AddCommand(inspectCmd)
}
var inspectCmd = &cobra.Command{
Use: "inspect [files to inspect]",
Short: "Inspect tags",
Long: "Show file tags as seen by Navidrome",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runInspector(args)
},
}
var marshalers = map[string]func(interface{}) ([]byte, error){
"pretty": prettyMarshal,
"toml": toml.Marshal,
"yaml": yaml.Marshal,
"json": json.Marshal,
"jsonindent": func(v interface{}) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
},
}
func prettyMarshal(v interface{}) ([]byte, error) {
out := v.([]inspectorOutput)
var res strings.Builder
for i := range out {
res.WriteString(fmt.Sprintf("====================\nFile: %s\n\n", out[i].File))
t, _ := toml.Marshal(out[i].RawTags)
res.WriteString(fmt.Sprintf("Raw tags:\n%s\n\n", t))
t, _ = toml.Marshal(out[i].MappedTags)
res.WriteString(fmt.Sprintf("Mapped tags:\n%s\n\n", t))
}
return []byte(res.String()), nil
}
type inspectorOutput struct {
File string
RawTags metadata.ParsedTags
MappedTags model.MediaFile
}
func runInspector(args []string) {
if extractor != "" {
conf.Server.Scanner.Extractor = extractor
}
log.Info("Using extractor", "extractor", conf.Server.Scanner.Extractor)
md, err := metadata.Extract(args...)
if err != nil {
log.Fatal("Error extracting tags", err)
}
mapper := scanner.NewMediaFileMapper(conf.Server.MusicFolder, &tests.MockedGenreRepo{})
marshal := marshalers[format]
if marshal == nil {
log.Fatal("Invalid format", "format", format)
}
var out []inspectorOutput
for k, v := range md {
if !model.IsAudioFile(k) {
continue
}
if len(v.Tags) == 0 {
continue
}
out = append(out, inspectorOutput{
File: k,
RawTags: v.Tags,
MappedTags: mapper.ToMediaFile(v),
})
}
data, _ := marshal(out)
fmt.Println(string(data))
}

View File

@ -183,6 +183,7 @@ func init() {
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will listen to")
rootCmd.Flags().String("baseurl", viper.GetString("baseurl"), "base URL to configure Navidrome behind a proxy (ex: /music or http://my.server.com)")
rootCmd.Flags().String("tlscert", viper.GetString("tlscert"), "optional path to a TLS cert file (enables HTTPS listening)")
rootCmd.Flags().String("unixsocketperm", viper.GetString("unixsocketperm"), "optional file permission for the unix socket")
rootCmd.Flags().String("tlskey", viper.GetString("tlskey"), "optional path to a TLS key file (enables HTTPS listening)")
rootCmd.Flags().Duration("sessiontimeout", viper.GetDuration("sessiontimeout"), "how long Navidrome will wait before closing web ui idle sessions")
@ -199,6 +200,7 @@ func init() {
_ = viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
_ = viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
_ = viper.BindPFlag("tlscert", rootCmd.Flags().Lookup("tlscert"))
_ = viper.BindPFlag("unixsocketperm", rootCmd.Flags().Lookup("unixsocketperm"))
_ = viper.BindPFlag("tlskey", rootCmd.Flags().Lookup("tlskey"))
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))

View File

@ -1,6 +1,6 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
@ -40,7 +40,8 @@ func CreateNativeAPIRouter() *nativeapi.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
share := core.NewShare(dataStore)
router := nativeapi.New(dataStore, share)
playlists := core.NewPlaylists(dataStore)
router := nativeapi.New(dataStore, share, playlists)
return router
}

View File

@ -12,7 +12,6 @@ import (
"github.com/kr/pretty"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/number"
"github.com/robfig/cron/v3"
"github.com/spf13/viper"
)
@ -21,6 +20,7 @@ type configOptions struct {
ConfigFile string
Address string
Port int
UnixSocketPerm string
MusicFolder string
DataFolder string
CacheFolder string
@ -51,11 +51,13 @@ type configOptions struct {
DefaultDownsamplingFormat string
SearchFullString bool
RecentlyAddedByModTime bool
PreferSortTags bool
IgnoredArticles string
IndexGroups string
SubsonicArtistParticipations bool
FFmpegPath string
MPVPath string
MPVCmdTemplate string
CoverArtPriority string
CoverJpegQuality int
ArtistArtPriority string
@ -96,6 +98,7 @@ type configOptions struct {
DevSidebarPlaylists bool
DevEnableBufferedScrobble bool
DevShowArtistPage bool
DevOffsetOptimize int
DevArtworkMaxRequests int
DevArtworkThrottleBacklogLimit int
DevArtworkThrottleBacklogTimeout time.Duration
@ -203,7 +206,7 @@ func Load() {
}
// Print current configuration if log level is Debug
if log.CurrentLevel() >= log.LevelDebug {
if log.IsGreaterOrEqualTo(log.LevelDebug) {
prettyConf := pretty.Sprintf("Loaded configuration from '%s': %# v", Server.ConfigFile, Server)
if Server.EnableLogRedacting {
prettyConf = log.Redact(prettyConf)
@ -273,6 +276,7 @@ func init() {
viper.SetDefault("loglevel", "info")
viper.SetDefault("address", "0.0.0.0")
viper.SetDefault("port", 4533)
viper.SetDefault("unixsocketperm", "0660")
viper.SetDefault("sessiontimeout", consts.DefaultSessionTimeout)
viper.SetDefault("scaninterval", -1)
viper.SetDefault("scanschedule", "@every 1m")
@ -295,10 +299,13 @@ func init() {
viper.SetDefault("defaultdownsamplingformat", consts.DefaultDownsamplingFormat)
viper.SetDefault("searchfullstring", false)
viper.SetDefault("recentlyaddedbymodtime", false)
viper.SetDefault("prefersorttags", false)
viper.SetDefault("ignoredarticles", "The El La Los Las Le Les Os As O A")
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")
@ -334,8 +341,8 @@ func init() {
viper.SetDefault("agents", "lastfm,spotify")
viper.SetDefault("lastfm.enabled", true)
viper.SetDefault("lastfm.language", "en")
viper.SetDefault("lastfm.apikey", consts.LastFMAPIKey)
viper.SetDefault("lastfm.secret", consts.LastFMAPISecret)
viper.SetDefault("lastfm.apikey", "")
viper.SetDefault("lastfm.secret", "")
viper.SetDefault("spotify.id", "")
viper.SetDefault("spotify.secret", "")
viper.SetDefault("listenbrainz.enabled", true)
@ -352,7 +359,8 @@ func init() {
viper.SetDefault("devenablebufferedscrobble", true)
viper.SetDefault("devsidebarplaylists", true)
viper.SetDefault("devshowartistpage", true)
viper.SetDefault("devartworkmaxrequests", number.Max(2, runtime.NumCPU()/3))
viper.SetDefault("devoffsetoptimize", 50000)
viper.SetDefault("devartworkmaxrequests", max(2, runtime.NumCPU()/3))
viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit)
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)

View File

@ -81,12 +81,6 @@ const (
DefaultCacheCleanUpInterval = 10 * time.Minute
)
// Shared secrets (only add here "secrets" that can be public)
const (
LastFMAPIKey = "9b94a5515ea66b2da3ec03c12300327e" // nolint:gosec
LastFMAPISecret = "74cb6557cec7171d921af5d7d887c587" // nolint:gosec
)
var (
DefaultDownsamplingFormat = "opus"
DefaultTranscodings = []map[string]interface{}{
@ -94,19 +88,19 @@ var (
"name": "mp3 audio",
"targetFormat": "mp3",
"defaultBitRate": 192,
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -f mp3 -",
"command": "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -f mp3 -",
},
{
"name": "opus audio",
"targetFormat": "opus",
"defaultBitRate": 128,
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -c:a libopus -f opus -",
"command": "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a libopus -f opus -",
},
{
"name": "aac audio",
"targetFormat": "aac",
"defaultBitRate": 256,
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -",
"command": "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -",
},
}

View File

@ -33,6 +33,7 @@ var audioFormats = map[string]format{
".dsf": {typ: "audio/dsd", lossless: true},
".wv": {typ: "audio/x-wavpack", lossless: true},
".wvp": {typ: "audio/x-wavpack", lossless: true},
".tak": {typ: "audio/tak", lossless: true},
".mka": {typ: "audio/x-matroska"},
}
var imageFormats = map[string]string{

View File

@ -134,7 +134,7 @@ func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, l
}
similar, err := agent.GetSimilarArtists(ctx, id, name, mbid, limit)
if len(similar) > 0 && err == nil {
if log.CurrentLevel() >= log.LevelTrace {
if log.IsGreaterOrEqualTo(log.LevelTrace) {
log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similar", similar, "elapsed", time.Since(start))
} else {
log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similarReceived", len(similar), "elapsed", time.Since(start))

View File

@ -311,12 +311,14 @@ func (l *lastfmAgent) IsAuthorized(ctx context.Context, userId string) bool {
func init() {
conf.AddHook(func() {
if conf.Server.LastFM.Enabled {
agents.Register(lastFMAgentName, func(ds model.DataStore) agents.Interface {
return lastFMConstructor(ds)
})
scrobbler.Register(lastFMAgentName, func(ds model.DataStore) scrobbler.Scrobbler {
return lastFMConstructor(ds)
})
if conf.Server.LastFM.ApiKey != "" && conf.Server.LastFM.Secret != "" {
agents.Register(lastFMAgentName, func(ds model.DataStore) agents.Interface {
return lastFMConstructor(ds)
})
scrobbler.Register(lastFMAgentName, func(ds model.DataStore) scrobbler.Scrobbler {
return lastFMConstructor(ds)
})
}
}
})
}

View File

@ -18,7 +18,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/req"
)
//go:embed token_received.html
@ -89,13 +89,14 @@ func (s *Router) unlink(w http.ResponseWriter, r *http.Request) {
}
func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
token := utils.ParamString(r, "token")
if token == "" {
p := req.Params(r)
token, err := p.String("token")
if err != nil {
_ = rest.RespondWithError(w, http.StatusBadRequest, "token not received")
return
}
uid := utils.ParamString(r, "uid")
if uid == "" {
uid, err := p.String("uid")
if err != nil {
_ = rest.RespondWithError(w, http.StatusBadRequest, "uid not received")
return
}
@ -103,7 +104,7 @@ func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
// Need to add user to context, as this is a non-authenticated endpoint, so it does not
// automatically contain any user info
ctx := request.WithUser(r.Context(), model.User{ID: uid})
err := s.fetchSessionKey(ctx, uid, token)
err = s.fetchSessionKey(ctx, uid, token)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)

View File

@ -8,13 +8,13 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/navidrome/navidrome/log"
"golang.org/x/exp/slices"
)
const (

View File

@ -150,7 +150,7 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med
var r io.ReadCloser
if format != "raw" && format != "" {
r, err = a.ms.DoStream(ctx, &mf, format, bitrate)
r, err = a.ms.DoStream(ctx, &mf, format, bitrate, 0)
} else {
r, err = os.Open(mf.Path)
}
@ -160,7 +160,7 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med
}
defer func() {
if err := r.Close(); err != nil && log.CurrentLevel() >= log.LevelDebug {
if err := r.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
log.Error(ctx, "Error closing stream", "id", mf.ID, "file", mf.Path, err)
}
}()

View File

@ -44,7 +44,7 @@ var _ = Describe("Archiver", func() {
}}).Return(mfs, nil)
ds.On("MediaFile", mock.Anything).Return(mfRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
out := new(bytes.Buffer)
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
@ -73,7 +73,7 @@ var _ = Describe("Archiver", func() {
}}).Return(mfs, nil)
ds.On("MediaFile", mock.Anything).Return(mfRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
out := new(bytes.Buffer)
err := arch.ZipArtist(context.Background(), "1", "mp3", 128, out)
@ -104,7 +104,7 @@ var _ = Describe("Archiver", func() {
}
sh.On("Load", mock.Anything, "1").Return(share, nil)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
out := new(bytes.Buffer)
err := arch.ZipShare(context.Background(), "1", out)
@ -136,7 +136,7 @@ var _ = Describe("Archiver", func() {
plRepo := &mockPlaylistRepository{}
plRepo.On("GetWithTracks", "1", true).Return(pls, nil)
ds.On("Playlist", mock.Anything).Return(plRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
out := new(bytes.Buffer)
err := arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)
@ -192,8 +192,8 @@ type mockMediaStreamer struct {
core.MediaStreamer
}
func (m *mockMediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, format string, bitrate int) (*core.Stream, error) {
args := m.Called(ctx, mf, format, bitrate)
func (m *mockMediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, reqOffset int) (*core.Stream, error) {
args := m.Called(ctx, mf, reqFormat, reqBitRate, reqOffset)
if args.Error(1) != nil {
return nil, args.Error(1)
}

View File

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

View File

@ -16,7 +16,6 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/number"
)
type resizedArtworkReader struct {
@ -113,7 +112,7 @@ func resizeImage(reader io.Reader, size int) (io.Reader, int, error) {
// Don't upscale the image
bounds := img.Bounds()
originalSize := number.Max(bounds.Max.X, bounds.Max.Y)
originalSize := max(bounds.Max.X, bounds.Max.Y)
if originalSize <= size {
return nil, originalSize, nil
}

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/go-chi/jwtauth/v5"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
@ -23,9 +24,10 @@ var (
func Init(ds model.DataStore) {
once.Do(func() {
log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)
secret, err := ds.Property(context.TODO()).DefaultGet(consts.JWTSecretKey, "not so secret")
if err != nil {
secret, err := ds.Property(context.TODO()).Get(consts.JWTSecretKey)
if err != nil || secret == "" {
log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
secret = uuid.NewString()
}
Secret = []byte(secret)
TokenAuth = jwtauth.New("HS256", Secret, nil)

View File

@ -18,7 +18,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/number"
. "github.com/navidrome/navidrome/utils/gg"
"golang.org/x/sync/errgroup"
)
@ -90,15 +90,16 @@ func (e *externalMetadata) UpdateAlbumInfo(ctx context.Context, id string) (*mod
return nil, err
}
if album.ExternalInfoUpdatedAt.IsZero() {
log.Debug(ctx, "AlbumInfo not cached. Retrieving it now", "updatedAt", album.ExternalInfoUpdatedAt, "id", id, "name", album.Name)
updatedAt := V(album.ExternalInfoUpdatedAt)
if updatedAt.IsZero() {
log.Debug(ctx, "AlbumInfo not cached. Retrieving it now", "updatedAt", updatedAt, "id", id, "name", album.Name)
err = e.populateAlbumInfo(ctx, album)
if err != nil {
return nil, err
}
}
if time.Since(album.ExternalInfoUpdatedAt) > conf.Server.DevAlbumInfoTimeToLive {
if time.Since(updatedAt) > conf.Server.DevAlbumInfoTimeToLive {
log.Debug("Found expired cached AlbumInfo, refreshing in the background", "updatedAt", album.ExternalInfoUpdatedAt, "name", album.Name)
enqueueRefresh(e.albumQueue, album)
}
@ -118,7 +119,7 @@ func (e *externalMetadata) populateAlbumInfo(ctx context.Context, album *auxAlbu
return err
}
album.ExternalInfoUpdatedAt = time.Now()
album.ExternalInfoUpdatedAt = P(time.Now())
album.ExternalUrl = info.URL
if info.Description != "" {
@ -202,8 +203,9 @@ func (e *externalMetadata) refreshArtistInfo(ctx context.Context, id string) (*a
}
// If we don't have any info, retrieves it now
if artist.ExternalInfoUpdatedAt.IsZero() {
log.Debug(ctx, "ArtistInfo not cached. Retrieving it now", "updatedAt", artist.ExternalInfoUpdatedAt, "id", id, "name", artist.Name)
updatedAt := V(artist.ExternalInfoUpdatedAt)
if updatedAt.IsZero() {
log.Debug(ctx, "ArtistInfo not cached. Retrieving it now", "updatedAt", updatedAt, "id", id, "name", artist.Name)
err := e.populateArtistInfo(ctx, artist)
if err != nil {
return nil, err
@ -211,8 +213,8 @@ func (e *externalMetadata) refreshArtistInfo(ctx context.Context, id string) (*a
}
// If info is expired, trigger a populateArtistInfo in the background
if time.Since(artist.ExternalInfoUpdatedAt) > conf.Server.DevArtistInfoTimeToLive {
log.Debug("Found expired cached ArtistInfo, refreshing in the background", "updatedAt", artist.ExternalInfoUpdatedAt, "name", artist.Name)
if time.Since(updatedAt) > conf.Server.DevArtistInfoTimeToLive {
log.Debug("Found expired cached ArtistInfo, refreshing in the background", "updatedAt", updatedAt, "name", artist.Name)
enqueueRefresh(e.artistQueue, artist)
}
return artist, nil
@ -242,7 +244,7 @@ func (e *externalMetadata) populateArtistInfo(ctx context.Context, artist *auxAr
return ctx.Err()
}
artist.ExternalInfoUpdatedAt = time.Now()
artist.ExternalInfoUpdatedAt = P(time.Now())
err := e.ds.Artist(ctx).Put(&artist.Artist)
if err != nil {
log.Error(ctx, "Error trying to update artist external information", "id", artist.ID, "name", artist.Name,
@ -272,7 +274,7 @@ func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count in
return ctx.Err()
}
topCount := number.Max(count, 20)
topCount := max(count, 20)
topSongs, err := e.getMatchingTopSongs(ctx, e.ag, &auxArtist{Name: a.Name, Artist: a}, topCount)
if err != nil {
log.Warn(ctx, "Error getting artist's top songs", "artist", a.Name, err)

View File

@ -16,12 +16,14 @@ import (
)
type FFmpeg interface {
Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error)
Transcode(ctx context.Context, command, path string, maxBitRate, offset int) (io.ReadCloser, error)
ExtractImage(ctx context.Context, path string) (io.ReadCloser, error)
ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error)
ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error)
Probe(ctx context.Context, files []string) (string, error)
CmdPath() (string, error)
IsAvailable() bool
Version() string
}
func New() FFmpeg {
@ -37,11 +39,11 @@ const (
type ffmpeg struct{}
func (e *ffmpeg) Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error) {
func (e *ffmpeg) Transcode(ctx context.Context, command, path string, maxBitRate, offset int) (io.ReadCloser, error) {
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
args := createFFmpegCommand(command, path, maxBitRate)
args := createFFmpegCommand(command, path, maxBitRate, offset)
return e.start(ctx, args)
}
@ -49,17 +51,17 @@ func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser,
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
args := createFFmpegCommand(extractImageCmd, path, 0)
args := createFFmpegCommand(extractImageCmd, path, 0, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createWavCmd, path, 0)
args := createFFmpegCommand(createWavCmd, path, 0, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createFLACCmd, path, 0)
args := createFFmpegCommand(createFLACCmd, path, 0, 0)
return e.start(ctx, args)
}
@ -78,6 +80,29 @@ func (e *ffmpeg) CmdPath() (string, error) {
return ffmpegCmd()
}
func (e *ffmpeg) IsAvailable() bool {
_, err := ffmpegCmd()
return err == nil
}
// Version executes ffmpeg -version and extracts the version from the output.
// Sample output: ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers
func (e *ffmpeg) Version() string {
cmd, err := ffmpegCmd()
if err != nil {
return "N/A"
}
out, err := exec.Command(cmd, "-version").CombinedOutput() // #nosec
if err != nil {
return "N/A"
}
parts := strings.Split(string(out), " ")
if len(parts) < 3 {
return "N/A"
}
return parts[2]
}
func (e *ffmpeg) start(ctx context.Context, args []string) (io.ReadCloser, error) {
log.Trace(ctx, "Executing ffmpeg command", "cmd", args)
j := &ffCmd{args: args}
@ -100,7 +125,7 @@ type ffCmd struct {
func (j *ffCmd) start() error {
cmd := exec.Command(j.args[0], j.args[1:]...) // #nosec
cmd.Stdout = j.out
if log.CurrentLevel() >= log.LevelTrace {
if log.IsGreaterOrEqualTo(log.LevelTrace) {
cmd.Stderr = os.Stderr
} else {
cmd.Stderr = io.Discard
@ -127,15 +152,25 @@ func (j *ffCmd) wait() {
}
// Path will always be an absolute path
func createFFmpegCommand(cmd, path string, maxBitRate int) []string {
func createFFmpegCommand(cmd, path string, maxBitRate, offset int) []string {
split := strings.Split(fixCmd(cmd), " ")
for i, s := range split {
s = strings.ReplaceAll(s, "%s", path)
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
split[i] = s
var parts []string
for _, s := range split {
if strings.Contains(s, "%s") {
s = strings.ReplaceAll(s, "%s", path)
parts = append(parts, s)
if offset > 0 && !strings.Contains(cmd, "%t") {
parts = append(parts, "-ss", strconv.Itoa(offset))
}
} else {
s = strings.ReplaceAll(s, "%t", strconv.Itoa(offset))
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
parts = append(parts, s)
}
}
return split
return parts
}
func createProbeCommand(cmd string, inputs []string) []string {

View File

@ -24,9 +24,22 @@ var _ = Describe("ffmpeg", func() {
})
Describe("createFFmpegCommand", func() {
It("creates a valid command line", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123)
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123, 0)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
})
Context("when command has time offset param", func() {
It("creates a valid command line with offset", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk -ss %t mp3 -", "/music library/file.mp3", 123, 456)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-b:a", "123k", "-ss", "456", "mp3", "-"}))
})
})
Context("when command does not have time offset param", func() {
It("adds time offset after the input file name", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123, 456)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-ss", "456", "-b:a", "123k", "mp3", "-"}))
})
})
})
Describe("createProbeCommand", func() {

View File

@ -19,8 +19,8 @@ import (
)
type MediaStreamer interface {
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error)
DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (*Stream, error)
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int, offset int) (*Stream, error)
DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, reqOffset int) (*Stream, error)
}
type TranscodingCache cache.FileCache
@ -40,22 +40,23 @@ type streamJob struct {
mf *model.MediaFile
format string
bitRate int
offset int
}
func (j *streamJob) Key() string {
return fmt.Sprintf("%s.%s.%d.%s", j.mf.ID, j.mf.UpdatedAt.Format(time.RFC3339Nano), j.bitRate, j.format)
return fmt.Sprintf("%s.%s.%d.%s.%d", j.mf.ID, j.mf.UpdatedAt.Format(time.RFC3339Nano), j.bitRate, j.format, j.offset)
}
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error) {
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int, reqOffset int) (*Stream, error) {
mf, err := ms.ds.MediaFile(ctx).Get(id)
if err != nil {
return nil, err
}
return ms.DoStream(ctx, mf, reqFormat, reqBitRate)
return ms.DoStream(ctx, mf, reqFormat, reqBitRate, reqOffset)
}
func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (*Stream, error) {
func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, reqOffset int) (*Stream, error) {
var format string
var bitRate int
var cached bool
@ -70,7 +71,7 @@ func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqF
if format == "raw" {
log.Debug(ctx, "Streaming RAW file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"requestBitrate", reqBitRate, "requestFormat", reqFormat, "requestOffset", reqOffset,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format)
f, err := os.Open(mf.Path)
@ -88,6 +89,7 @@ func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqF
mf: mf,
format: format,
bitRate: bitRate,
offset: reqOffset,
}
r, err := ms.cache.Get(ctx, job)
if err != nil {
@ -100,7 +102,7 @@ func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqF
s.Seeker = r.Seeker
log.Debug(ctx, "Streaming TRANSCODED file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"requestBitrate", reqBitRate, "requestFormat", reqFormat, "requestOffset", reqOffset,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format, "cached", cached, "seekable", s.Seekable())
@ -199,7 +201,7 @@ func NewTranscodingCache() TranscodingCache {
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
return nil, os.ErrInvalid
}
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate)
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate, job.offset)
if err != nil {
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
return nil, os.ErrInvalid

View File

@ -39,34 +39,34 @@ var _ = Describe("MediaStreamer", func() {
Context("NewStream", func() {
It("returns a seekable stream if format is 'raw'", func() {
s, err := streamer.NewStream(ctx, "123", "raw", 0)
s, err := streamer.NewStream(ctx, "123", "raw", 0, 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is 0", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 0)
s, err := streamer.NewStream(ctx, "123", "mp3", 0, 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is higher than file bitRate", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 320)
s, err := streamer.NewStream(ctx, "123", "mp3", 320, 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a NON seekable stream if transcode is required", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 64)
s, err := streamer.NewStream(ctx, "123", "mp3", 64, 0)
Expect(err).To(BeNil())
Expect(s.Seekable()).To(BeFalse())
Expect(s.Duration()).To(Equal(float32(257.0)))
})
It("returns a seekable stream if the file is complete in the cache", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 32)
s, err := streamer.NewStream(ctx, "123", "mp3", 32, 0)
Expect(err).To(BeNil())
_, _ = io.ReadAll(s)
_ = s.Close()
Eventually(func() bool { return ffmpeg.IsClosed() }, "3s").Should(BeTrue())
s, err = streamer.NewStream(ctx, "123", "mp3", 32)
s, err = streamer.NewStream(ctx, "123", "mp3", 32, 0)
Expect(err).To(BeNil())
Expect(s.Seekable()).To(BeTrue())
})

View File

@ -1,66 +0,0 @@
//go:build beep
package beepaudio
import (
"context"
"io"
"os"
"github.com/faiface/beep"
"github.com/faiface/beep/flac"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/wav"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/log"
)
func DecodeMp3(path string) (s beep.StreamSeekCloser, format beep.Format, err error) {
f, err := os.Open(path)
if err != nil {
return nil, beep.Format{}, err
}
return mp3.Decode(f)
}
func DecodeWAV(path string) (s beep.StreamSeekCloser, format beep.Format, err error) {
f, err := os.Open(path)
if err != nil {
return nil, beep.Format{}, err
}
return wav.Decode(f)
}
func DecodeFLAC(path string) (s beep.StreamSeekCloser, format beep.Format, fileToCleanup string, err error) {
// TODO: Turn this into a semi-parallel operation: start playing while still transcoding/copying
log.Debug("decode to FLAC", "filename", path)
fFmpeg := ffmpeg.New()
readCloser, err := fFmpeg.ConvertToFLAC(context.TODO(), path)
if err != nil {
log.Error("error converting file to FLAC", path, err)
return nil, beep.Format{}, "", err
}
tempFile, err := os.CreateTemp("", "*.flac")
if err != nil {
log.Error("error creating temp file", err)
return nil, beep.Format{}, "", err
}
log.Debug("created tempfile", "filename", tempFile.Name())
written, err := io.Copy(tempFile, readCloser)
if err != nil {
log.Error("error coping file", "dest", tempFile.Name())
}
log.Debug("copy pipe into tempfile", "bytes written", written, "filename", tempFile.Name())
f, err := os.Open(tempFile.Name())
if err != nil {
log.Error("could not re-open tempfile", "filename", tempFile.Name())
return nil, beep.Format{}, "", err
}
s, format, err = flac.Decode(f)
return s, format, tempFile.Name(), err
}

View File

@ -1,162 +0,0 @@
//go:build beep
package beepaudio
import (
"fmt"
"os"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/speaker"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type BeepTrack struct {
MediaFile model.MediaFile
Ctrl *beep.Ctrl
Volume *effects.Volume
ActiveStream beep.StreamSeekCloser
TempfileToCleanup string
SampleRate beep.SampleRate
PlaybackDone chan bool
}
func NewTrack(playbackDoneChannel chan bool, mf model.MediaFile) (*BeepTrack, error) {
t := BeepTrack{}
contentType := mf.ContentType()
log.Debug("loading track", "trackname", mf.Path, "mediatype", contentType)
var streamer beep.StreamSeekCloser
var format beep.Format
var err error
var tmpfileToCleanup = ""
switch contentType {
case "audio/mpeg":
streamer, format, err = DecodeMp3(mf.Path)
case "audio/x-wav":
streamer, format, err = DecodeWAV(mf.Path)
case "audio/mp4":
streamer, format, tmpfileToCleanup, err = DecodeFLAC(mf.Path)
default:
return nil, fmt.Errorf("unsupported content type: %s", contentType)
}
if err != nil {
log.Error(err)
return nil, err
}
// save running stream for closing when switching tracks
t.ActiveStream = streamer
t.TempfileToCleanup = tmpfileToCleanup
log.Debug("Setting up audio device")
t.Ctrl = &beep.Ctrl{Streamer: streamer, Paused: true}
t.Volume = &effects.Volume{Streamer: t.Ctrl, Base: 2}
t.SampleRate = format.SampleRate
t.PlaybackDone = playbackDoneChannel
t.MediaFile = mf
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
if err != nil {
log.Error(err)
}
log.Debug("speaker.Init() finished")
go func() {
speaker.Play(beep.Seq(t.Volume, beep.Callback(func() {
log.Info("Hitting end-of-stream, signalling on channel")
t.PlaybackDone <- true
log.Debug("Signalling finished")
})))
log.Debug("dropping out of speaker.Play()")
}()
return &t, nil
}
func (t *BeepTrack) String() string {
return fmt.Sprintf("Name: %s", t.MediaFile.Path)
}
func (t *BeepTrack) SetVolume(value float64) {
speaker.Lock()
t.Volume.Volume += value
speaker.Unlock()
}
func (t *BeepTrack) Unpause() {
speaker.Lock()
if t.Ctrl.Paused {
t.Ctrl.Paused = false
} else {
log.Debug("tried to unpause while not paused")
}
speaker.Unlock()
}
func (t *BeepTrack) Pause() {
speaker.Lock()
if t.Ctrl.Paused {
log.Debug("tried to pause while already paused")
} else {
t.Ctrl.Paused = true
}
speaker.Unlock()
}
func (t *BeepTrack) Close() {
if t.ActiveStream != nil {
log.Debug("closing activ stream")
t.ActiveStream.Close()
t.ActiveStream = nil
}
speaker.Close()
if t.TempfileToCleanup != "" {
log.Debug("Removing tempfile", "tmpfilename", t.TempfileToCleanup)
err := os.Remove(t.TempfileToCleanup)
if err != nil {
log.Error("error cleaning up tempfile: ", t.TempfileToCleanup)
}
}
}
// Position returns the playback position in seconds
func (t *BeepTrack) Position() int {
if t.Ctrl.Streamer == nil {
log.Debug("streamer is not setup (nil), could not get position")
return 0
}
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
if ok {
position := t.SampleRate.D(streamer.Position())
posSecs := position.Round(time.Second).Seconds()
return int(posSecs)
} else {
log.Debug("streamer is no beep.StreamSeeker, could not get position")
return 0
}
}
// offset = pd.PlaybackQueue.Offset
func (t *BeepTrack) SetPosition(offset int) error {
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
if ok {
sampleRatePerSecond := t.SampleRate.N(time.Second)
nextPosition := sampleRatePerSecond * offset
log.Debug("SetPosition", "samplerate", sampleRatePerSecond, "nextPosition", nextPosition)
return streamer.Seek(nextPosition)
}
return fmt.Errorf("streamer is not seekable")
}
func (t *BeepTrack) IsPlaying() bool {
return t.Ctrl != nil && !t.Ctrl.Paused
}

View File

@ -2,6 +2,7 @@ package playback
import (
"context"
"errors"
"fmt"
"github.com/navidrome/navidrome/core/playback/mpv"
@ -17,9 +18,10 @@ type Track interface {
Position() int
SetPosition(offset int) error
Close()
String() string
}
type PlaybackDevice struct {
type playbackDevice struct {
ParentPlaybackServer PlaybackServer
Default bool
User string
@ -43,7 +45,7 @@ const DefaultGain float32 = 1.0
var EmptyStatus = DeviceStatus{CurrentIndex: -1, Playing: false, Gain: DefaultGain, Position: 0}
func (pd *PlaybackDevice) getStatus() DeviceStatus {
func (pd *playbackDevice) getStatus() DeviceStatus {
pos := 0
if pd.ActiveTrack != nil {
pos = pd.ActiveTrack.Position()
@ -59,8 +61,8 @@ func (pd *PlaybackDevice) getStatus() DeviceStatus {
// NewPlaybackDevice creates a new playback device which implements all the basic Jukebox mode commands defined here:
// http://www.subsonic.org/pages/api.jsp#jukeboxControl
// Starts the trackSwitcher goroutine for the device.
func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName string) *PlaybackDevice {
return &PlaybackDevice{
func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName string) *playbackDevice {
return &playbackDevice{
ParentPlaybackServer: playbackServer,
User: "",
Name: name,
@ -72,22 +74,24 @@ func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName st
}
}
func (pd *PlaybackDevice) String() string {
func (pd *playbackDevice) String() string {
return fmt.Sprintf("Name: %s, Gain: %.4f, Loaded track: %s", pd.Name, pd.Gain, pd.ActiveTrack)
}
func (pd *PlaybackDevice) Get(ctx context.Context) (model.MediaFiles, DeviceStatus, error) {
log.Debug(ctx, "processing Get action")
func (pd *playbackDevice) Get(ctx context.Context) (model.MediaFiles, DeviceStatus, error) {
log.Debug(ctx, "Processing Get action", "device", pd)
return pd.PlaybackQueue.Get(), pd.getStatus(), nil
}
func (pd *PlaybackDevice) Status(ctx context.Context) (DeviceStatus, error) {
func (pd *playbackDevice) Status(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing Status action on: %s, queue: %s", pd, pd.PlaybackQueue))
return pd.getStatus(), nil
}
// set is similar to a clear followed by a add, but will not change the currently playing track.
func (pd *PlaybackDevice) Set(ctx context.Context, ids []string) (DeviceStatus, error) {
// Set is similar to a clear followed by a add, but will not change the currently playing track.
func (pd *playbackDevice) Set(ctx context.Context, ids []string) (DeviceStatus, error) {
log.Debug(ctx, "Processing Set action", "ids", ids, "device", pd)
_, err := pd.Clear(ctx)
if err != nil {
log.Error(ctx, "error setting tracks", ids)
@ -96,8 +100,8 @@ func (pd *PlaybackDevice) Set(ctx context.Context, ids []string) (DeviceStatus,
return pd.Add(ctx, ids)
}
func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Start action")
func (pd *playbackDevice) Start(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Start action", "device", pd)
if !pd.TrackSwitcherStarted {
log.Info(ctx, "Starting trackSwitcher goroutine")
@ -127,16 +131,16 @@ func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) {
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Stop(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Stop action")
func (pd *playbackDevice) Stop(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Stop action", "device", pd)
if pd.ActiveTrack != nil {
pd.ActiveTrack.Pause()
}
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Skip(ctx context.Context, index int, offset int) (DeviceStatus, error) {
log.Debug(ctx, "processing Skip action", "index", index, "offset", offset)
func (pd *playbackDevice) Skip(ctx context.Context, index int, offset int) (DeviceStatus, error) {
log.Debug(ctx, "Processing Skip action", "index", index, "offset", offset, "device", pd)
wasPlaying := pd.isPlaying()
@ -173,8 +177,11 @@ func (pd *PlaybackDevice) Skip(ctx context.Context, index int, offset int) (Devi
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) {
log.Debug(ctx, "processing Add action")
func (pd *playbackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) {
log.Debug(ctx, "Processing Add action", "ids", ids, "device", pd)
if len(ids) < 1 {
return pd.getStatus(), nil
}
items := model.MediaFiles{}
@ -191,8 +198,8 @@ func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus,
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing Clear action on: %s", pd))
func (pd *playbackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Clear action", "device", pd)
if pd.ActiveTrack != nil {
pd.ActiveTrack.Pause()
pd.ActiveTrack.Close()
@ -202,8 +209,8 @@ func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) {
log.Debug(ctx, "processing Remove action")
func (pd *playbackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) {
log.Debug(ctx, "Processing Remove action", "index", index, "device", pd)
// pausing if attempting to remove running track
if pd.isPlaying() && pd.PlaybackQueue.Index == index {
_, err := pd.Stop(ctx)
@ -221,17 +228,17 @@ func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus,
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Shuffle action")
func (pd *playbackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Shuffle action", "device", pd)
if pd.PlaybackQueue.Size() > 1 {
pd.PlaybackQueue.Shuffle()
}
return pd.getStatus(), nil
}
// Used to control the playback volume. A float value between 0.0 and 1.0.
func (pd *PlaybackDevice) SetGain(ctx context.Context, gain float32) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing SetGain action. Actual gain: %f, gain to set: %f", pd.Gain, gain))
// SetGain is used to control the playback volume. A float value between 0.0 and 1.0.
func (pd *playbackDevice) SetGain(ctx context.Context, gain float32) (DeviceStatus, error) {
log.Debug(ctx, "Processing SetGain action", "newGain", gain, "device", pd)
if pd.ActiveTrack != nil {
pd.ActiveTrack.SetVolume(gain)
@ -241,15 +248,15 @@ func (pd *PlaybackDevice) SetGain(ctx context.Context, gain float32) (DeviceStat
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) isPlaying() bool {
func (pd *playbackDevice) isPlaying() bool {
return pd.ActiveTrack != nil && pd.ActiveTrack.IsPlaying()
}
func (pd *PlaybackDevice) trackSwitcherGoroutine() {
log.Info("Starting trackSwitcher goroutine")
func (pd *playbackDevice) trackSwitcherGoroutine() {
log.Debug("Started trackSwitcher goroutine", "device", pd)
for {
<-pd.PlaybackDone
log.Info("track switching detected")
log.Debug("Track switching detected")
if pd.ActiveTrack != nil {
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
@ -260,7 +267,7 @@ func (pd *PlaybackDevice) trackSwitcherGoroutine() {
log.Debug("Switching to next song", "queue", pd.PlaybackQueue.String())
err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index)
if err != nil {
log.Error("error switching track", "error", err)
log.Error("Error switching track", err)
}
pd.ActiveTrack.Unpause()
} else {
@ -269,11 +276,11 @@ func (pd *PlaybackDevice) trackSwitcherGoroutine() {
}
}
func (pd *PlaybackDevice) switchActiveTrackByIndex(index int) error {
func (pd *playbackDevice) switchActiveTrackByIndex(index int) error {
pd.PlaybackQueue.SetIndex(index)
currentTrack := pd.PlaybackQueue.Current()
if currentTrack == nil {
return fmt.Errorf("could not get current track")
return errors.New("could not get current track")
}
track, err := mpv.NewTrack(pd.PlaybackDone, pd.DeviceName, *currentTrack)

View File

@ -2,14 +2,11 @@ package mpv
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
@ -17,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}
@ -54,7 +46,7 @@ func (j *Executor) start() error {
j.ctx = ctx
cmd := exec.CommandContext(ctx, j.args[0], j.args[1:]...) // #nosec
cmd.Stdout = j.out
if log.CurrentLevel() >= log.LevelTrace {
if log.IsGreaterOrEqualTo(log.LevelTrace) {
cmd.Stderr = os.Stderr
} else {
cmd.Stderr = io.Discard
@ -81,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
}
@ -133,10 +124,3 @@ var (
mpvPath string
mpvErr error
)
func TempFileName(prefix, suffix string) string {
randBytes := make([]byte, 16)
// we can savely ignore the return value since we're loading into a precreated, fixedsized buffer
_, _ = rand.Read(randBytes)
return filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+suffix)
}

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import (
"os"
"time"
"github.com/DexterLB/mpvipc"
"github.com/dexterlb/mpvipc"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
@ -25,25 +25,25 @@ type MpvTrack struct {
}
func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFile) (*MpvTrack, error) {
log.Debug("loading track", "trackname", mf.Path, "mediatype", mf.ContentType())
log.Debug("Loading track", "trackPath", mf.Path, "mediaType", mf.ContentType())
if _, err := mpvCommand(); err != nil {
return nil, err
}
tmpSocketName := 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", "error", err)
log.Error("Error starting mpv process", err)
return nil, err
}
// wait for socket to show up
err = waitForFile(tmpSocketName, 3*time.Second, 100*time.Millisecond)
err = waitForSocket(tmpSocketName, 3*time.Second, 100*time.Millisecond)
if err != nil {
log.Error("error or timeout waiting for control socket", "socketname", tmpSocketName, "error", err)
log.Error("Error or timeout waiting for control socket", "socketname", tmpSocketName, err)
return nil, err
}
@ -51,7 +51,7 @@ func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFi
err = conn.Open()
if err != nil {
log.Error("error opening new connection", "error", err)
log.Error("Error opening new connection", err)
return nil, err
}
@ -77,62 +77,57 @@ func (t *MpvTrack) SetVolume(value float32) {
// mpv's volume as described in the --volume parameter:
// Set the startup volume. 0 means silence, 100 means no volume reduction or amplification.
// Negative values can be passed for compatibility, but are treated as 0.
log.Debug("request for gain", "gain", value)
log.Debug("Setting volume", "volume", value, "track", t)
vol := int(value * 100)
err := t.Conn.Set("volume", vol)
if err != nil {
log.Error(err)
log.Error("Error setting volume", "volume", value, "track", t, err)
}
log.Debug("set volume", "volume", vol)
}
func (t *MpvTrack) Unpause() {
log.Debug("Unpausing track", "track", t)
err := t.Conn.Set("pause", false)
if err != nil {
log.Error(err)
log.Error("Error unpausing track", "track", t, err)
}
log.Info("unpaused track")
}
func (t *MpvTrack) Pause() {
log.Debug("Pausing track", "track", t)
err := t.Conn.Set("pause", true)
if err != nil {
log.Error(err)
log.Error("Error pausing track", "track", t, err)
}
log.Info("paused track")
}
func (t *MpvTrack) Close() {
log.Debug("closing resources")
log.Debug("Closing resources", "track", t)
t.CloseCalled = true
// trying to shutdown mpv process using socket
if t.isSocketfilePresent() {
if t.isSocketFilePresent() {
log.Debug("sending shutdown command")
_, err := t.Conn.Call("quit")
if err != nil {
log.Error("error sending quit command to mpv-ipc socket", "error", err)
log.Error("Error sending quit command to mpv-ipc socket", err)
if t.Exe != nil {
log.Debug("cancelling executor")
err = t.Exe.Cancel()
if err != nil {
log.Error("error canceling executor")
log.Error("Error canceling executor", err)
}
}
}
}
if t.isSocketfilePresent() {
log.Debug("Removing socketfile", "socketfile", t.IPCSocketName)
err := os.Remove(t.IPCSocketName)
if err != nil {
log.Error("error cleaning up socketfile: ", t.IPCSocketName)
}
if t.isSocketFilePresent() {
removeSocket(t.IPCSocketName)
}
}
func (t *MpvTrack) isSocketfilePresent() bool {
func (t *MpvTrack) isSocketFilePresent() bool {
if len(t.IPCSocketName) < 1 {
return false
}
@ -141,16 +136,16 @@ func (t *MpvTrack) isSocketfilePresent() bool {
return err == nil && fileInfo != nil && !fileInfo.IsDir()
}
// Position returns the playback position in seconds
// every now and then the mpv IPC interface returns "mpv error: property unavailable"
// Position returns the playback position in seconds.
// Every now and then the mpv IPC interface returns "mpv error: property unavailable"
// in this case we have to retry
func (t *MpvTrack) Position() int {
retryCount := 0
for {
position, err := t.Conn.Get("time-pos")
if err != nil && err.Error() == "mpv error: property unavailable" {
log.Debug("got the mpv error: property unavailable error, retry ...")
retryCount += 1
log.Debug("Got mpv error, retrying...", "retries", retryCount, err)
if retryCount > 5 {
return 0
}
@ -158,13 +153,13 @@ func (t *MpvTrack) Position() int {
}
if err != nil {
log.Error("error getting position in track", "error", err)
log.Error("Error getting position in track", "track", t, err)
return 0
}
pos, ok := position.(float64)
if !ok {
log.Error("could not cast position from mpv into float64")
log.Error("Could not cast position from mpv into float64", "position", position, "track", t)
return 0
} else {
return int(pos)
@ -174,36 +169,37 @@ func (t *MpvTrack) Position() int {
}
func (t *MpvTrack) SetPosition(offset int) error {
log.Debug("Setting position", "offset", offset, "track", t)
pos := t.Position()
if pos == offset {
log.Debug("no position difference, skipping operation")
log.Debug("No position difference, skipping operation", "track", t)
return nil
}
err := t.Conn.Set("time-pos", float64(offset))
if err != nil {
log.Error("could not set the position in track", "offset", offset, "error", err)
log.Error("Could not set the position in track", "track", t, "offset", offset, err)
return err
}
log.Info("set position", "offset", offset)
return nil
}
func (t *MpvTrack) IsPlaying() bool {
log.Debug("Checking if track is playing", "track", t)
pausing, err := t.Conn.Get("pause")
if err != nil {
log.Error("problem getting paused status", "error", err)
log.Error("Problem getting paused status", "track", t, err)
return false
}
pause, ok := pausing.(bool)
if !ok {
log.Error("could not cast pausing to boolean")
log.Error("Could not cast pausing to boolean", "track", t, "value", pausing)
return false
}
return !pause
}
func waitForFile(path string, timeout time.Duration, pause time.Duration) error {
func waitForSocket(path string, timeout time.Duration, pause time.Duration) error {
start := time.Now()
end := start.Add(timeout)
var retries int = 0
@ -211,7 +207,7 @@ func waitForFile(path string, timeout time.Duration, pause time.Duration) error
for {
fileInfo, err := os.Stat(path)
if err == nil && fileInfo != nil && !fileInfo.IsDir() {
log.Debug("file found", "retries", retries, "waittime", time.Since(start).Microseconds())
log.Debug("Socket found", "retries", retries, "waitTime", time.Since(start))
return nil
}
if time.Now().After(end) {

View File

@ -1,5 +1,5 @@
// Package playback implements audio playback using PlaybackDevices. It is used to implement the Jukebox mode in turn.
// It makes use of the BEEP library to do the playback. Major parts are:
// It makes use of the MPV library to do the playback. Major parts are:
// - decoder which includes decoding and transcoding of various audio file formats
// - device implementing the basic functions to work with audio devices like set, play, stop, skip, ...
// - queue a simple playlist
@ -19,7 +19,7 @@ import (
type PlaybackServer interface {
Run(ctx context.Context) error
GetDeviceForUser(user string) (*PlaybackDevice, error)
GetDeviceForUser(user string) (*playbackDevice, error)
GetMediaFile(id string) (*model.MediaFile, error)
GetCtx() *context.Context
}
@ -27,7 +27,7 @@ type PlaybackServer interface {
type playbackServer struct {
ctx *context.Context
datastore model.DataStore
playbackDevices []PlaybackDevice
playbackDevices []playbackDevice
}
// GetInstance returns the playback-server singleton
@ -63,12 +63,12 @@ func (ps *playbackServer) GetCtx() *context.Context {
return ps.ctx
}
func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition, defaultDevice string) ([]PlaybackDevice, error) {
pbDevices := make([]PlaybackDevice, max(1, len(devices)))
func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition, defaultDevice string) ([]playbackDevice, error) {
pbDevices := make([]playbackDevice, max(1, len(devices)))
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")
}
@ -76,13 +76,13 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
// if there is but only one entry and no default given, just use that.
if len(devices) == 1 {
if len(devices[0]) != 2 {
return []PlaybackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(devices[0]))
return []playbackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(devices[0]))
}
pbDevices[0] = *NewPlaybackDevice(ps, devices[0][0], devices[0][1])
}
if len(devices) > 1 {
return []PlaybackDevice{}, fmt.Errorf("number of audio device found is %d, but no default device defined. Set Jukebox.Default", len(devices))
return []playbackDevice{}, fmt.Errorf("number of audio device found is %d, but no default device defined. Set Jukebox.Default", len(devices))
}
pbDevices[0].Default = true
@ -91,7 +91,7 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
for idx, audioDevice := range devices {
if len(audioDevice) != 2 {
return []PlaybackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(audioDevice))
return []playbackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(audioDevice))
}
pbDevices[idx] = *NewPlaybackDevice(ps, audioDevice[0], audioDevice[1])
@ -103,18 +103,18 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
}
if !defaultDeviceFound {
return []PlaybackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
return []playbackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
}
return pbDevices, nil
}
func (ps *playbackServer) getDefaultDevice() (*PlaybackDevice, error) {
func (ps *playbackServer) getDefaultDevice() (*playbackDevice, error) {
for idx, audioDevice := range ps.playbackDevices {
if audioDevice.Default {
return &ps.playbackDevices[idx], nil
}
}
return &PlaybackDevice{}, fmt.Errorf("no default device found")
return &playbackDevice{}, fmt.Errorf("no default device found")
}
// GetMediaFile retrieves the MediaFile given by the id parameter
@ -123,12 +123,12 @@ func (ps *playbackServer) GetMediaFile(id string) (*model.MediaFile, error) {
}
// GetDeviceForUser returns the audio playback device for the given user. As of now this is but only the default device.
func (ps *playbackServer) GetDeviceForUser(user string) (*PlaybackDevice, error) {
log.Debug("processing GetDevice")
func (ps *playbackServer) GetDeviceForUser(user string) (*playbackDevice, error) {
log.Debug("Processing GetDevice", "user", user)
// README: here we might plug-in the user-device mapping one fine day
device, err := ps.getDefaultDevice()
if err != nil {
return &PlaybackDevice{}, err
return &playbackDevice{}, err
}
device.User = user
return device, nil

View File

@ -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"
@ -23,6 +24,7 @@ import (
type Playlists interface {
ImportFile(ctx context.Context, dir string, fname string) (*model.Playlist, error)
Update(ctx context.Context, playlistID string, name *string, comment *string, public *bool, idsToAdd []string, idxToRemove []int) error
ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error)
}
type playlists struct {
@ -47,6 +49,26 @@ func (s *playlists) ImportFile(ctx context.Context, dir string, fname string) (*
return pls, err
}
func (s *playlists) ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error) {
owner, _ := request.UserFrom(ctx)
pls := &model.Playlist{
OwnerID: owner.ID,
Public: false,
Sync: true,
}
pls, err := s.parseM3U(ctx, pls, "", reader)
if err != nil {
log.Error(ctx, "Error parsing playlist", err)
return nil, err
}
err = s.ds.Playlist(ctx).Put(pls)
if err != nil {
log.Error(ctx, "Error saving playlist", err)
return nil, err
}
return pls, nil
}
func (s *playlists) parsePlaylist(ctx context.Context, playlistFile string, baseDir string) (*model.Playlist, error) {
pls, err := s.newSyncedPlaylist(baseDir, playlistFile)
if err != nil {
@ -91,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)
@ -107,31 +130,40 @@ func (s *playlists) parseNSP(ctx context.Context, pls *model.Playlist, file io.R
return pls, nil
}
func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, baseDir string, file io.Reader) (*model.Playlist, error) {
func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, baseDir string, reader io.Reader) (*model.Playlist, error) {
mediaFileRepository := s.ds.MediaFile(ctx)
scanner := bufio.NewScanner(file)
scanner := bufio.NewScanner(reader)
scanner.Split(scanLines)
var mfs model.MediaFiles
for scanner.Scan() {
path := strings.TrimSpace(scanner.Text())
// Skip empty lines and extended info
if path == "" || strings.HasPrefix(path, "#") {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#PLAYLIST:") {
if split := strings.Split(line, ":"); len(split) >= 2 {
pls.Name = split[1]
}
continue
}
if strings.HasPrefix(path, "file://") {
path = strings.TrimPrefix(path, "file://")
path, _ = url.QueryUnescape(path)
// Skip empty lines and extended info
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
if strings.HasPrefix(line, "file://") {
line = strings.TrimPrefix(line, "file://")
line, _ = url.QueryUnescape(line)
}
mf, err := mediaFileRepository.FindByPath(path)
if baseDir != "" && !filepath.IsAbs(line) {
line = filepath.Join(baseDir, line)
}
mf, err := mediaFileRepository.FindByPath(line)
if err != nil {
log.Warn(ctx, "Path in playlist not found", "playlist", pls.Name, "path", path, err)
log.Warn(ctx, "Path in playlist not found", "playlist", pls.Name, "path", line, err)
continue
}
mfs = append(mfs, *mf)
}
if pls.Name == "" {
pls.Name = time.Now().Format(time.RFC3339)
}
pls.Tracks = nil
pls.AddMediaFiles(mfs)
@ -157,7 +189,7 @@ func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist)
newPls.Comment = pls.Comment
newPls.OwnerID = pls.OwnerID
newPls.Public = pls.Public
newPls.EvaluatedAt = time.Time{}
newPls.EvaluatedAt = &time.Time{}
} else {
log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName)
newPls.OwnerID = owner.ID

View File

@ -2,6 +2,11 @@ package core
import (
"context"
"os"
"time"
"github.com/navidrome/navidrome/model/criteria"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
@ -12,13 +17,16 @@ import (
var _ = Describe("Playlists", func() {
var ds model.DataStore
var ps Playlists
var mp mockedPlaylist
ctx := context.Background()
BeforeEach(func() {
mp = mockedPlaylist{}
ds = &tests.MockDataStore{
MockedMediaFile: &mockedMediaFile{},
MockedPlaylist: &mockedPlaylist{},
MockedPlaylist: &mp,
}
ctx = request.WithUser(ctx, model.User{ID: "123"})
})
Describe("ImportFile", func() {
@ -26,27 +34,76 @@ var _ = Describe("Playlists", func() {
ps = NewPlaylists(ds)
})
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))
})
})
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{}))
})
})
})
Describe("ImportM3U", func() {
BeforeEach(func() {
ps = NewPlaylists(ds)
ctx = request.WithUser(ctx, model.User{ID: "123"})
})
It("parses well-formed playlists", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/pls1.m3u")
f, _ := os.Open("tests/fixtures/playlists/pls-post-with-name.m3u")
defer f.Close()
pls, err := ps.ImportM3U(ctx, f)
Expect(pls.OwnerID).To(Equal("123"))
Expect(pls.Name).To(Equal("playlist 1"))
Expect(err).To(BeNil())
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))
f.Close()
})
It("parses playlists using LF ending", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "lf-ended.m3u")
It("sets the playlist name as a timestamp if the #PLAYLIST directive is not present", func() {
f, _ := os.Open("tests/fixtures/playlists/pls-post.m3u")
defer f.Close()
pls, err := ps.ImportM3U(ctx, f)
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")
_, err = time.Parse(time.RFC3339, pls.Name)
Expect(err).To(BeNil())
Expect(pls.Tracks).To(HaveLen(2))
})
})
})
@ -62,6 +119,7 @@ func (r *mockedMediaFile) FindByPath(s string) (*model.MediaFile, error) {
}
type mockedPlaylist struct {
last *model.Playlist
model.PlaylistRepository
}
@ -69,6 +127,7 @@ func (r *mockedPlaylist) FindByPath(string) (*model.Playlist, error) {
return nil, model.ErrNotFound
}
func (r *mockedPlaylist) Put(*model.Playlist) error {
func (r *mockedPlaylist) Put(pls *model.Playlist) error {
r.last = pls
return nil
}

View File

@ -5,10 +5,9 @@ import (
"sort"
"time"
"github.com/jellydator/ttlcache/v2"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/ReneKroon/ttlcache/v2"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"

View File

@ -10,6 +10,7 @@ import (
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
. "github.com/navidrome/navidrome/utils/gg"
"github.com/navidrome/navidrome/utils/slice"
)
@ -34,10 +35,11 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error
if err != nil {
return nil, err
}
if !share.ExpiresAt.IsZero() && share.ExpiresAt.Before(time.Now()) {
expiresAt := V(share.ExpiresAt)
if !expiresAt.IsZero() && expiresAt.Before(time.Now()) {
return nil, model.ErrExpired
}
share.LastVisitedAt = time.Now()
share.LastVisitedAt = P(time.Now())
share.VisitCount++
err = repo.(rest.Persistable).Update(id, share, "last_visited_at", "visit_count")
@ -90,8 +92,8 @@ func (r *shareRepositoryWrapper) Save(entity interface{}) (string, error) {
return "", err
}
s.ID = id
if s.ExpiresAt.IsZero() {
s.ExpiresAt = time.Now().Add(365 * 24 * time.Hour)
if V(s.ExpiresAt).IsZero() {
s.ExpiresAt = P(time.Now().Add(365 * 24 * time.Hour))
}
firstId := strings.SplitN(s.ResourceIDs, ",", 2)[0]
@ -128,7 +130,7 @@ func (r *shareRepositoryWrapper) Update(id string, entity interface{}, _ ...stri
cols := []string{"description", "downloadable"}
// TODO Better handling of Share expiration
if !entity.(*model.Share).ExpiresAt.IsZero() {
if !V(entity.(*model.Share).ExpiresAt).IsZero() {
cols = append(cols, "expires_at")
}
return r.Persistable.Update(id, entity, cols...)

View File

@ -60,19 +60,46 @@ func Init() {
}
gooseLogger := &logAdapter{silent: isSchemaEmpty(db)}
goose.SetLogger(gooseLogger)
goose.SetBaseFS(embedMigrations)
err = goose.SetDialect(Driver)
if err != nil {
log.Fatal("Invalid DB driver", "driver", Driver, err)
}
if !isSchemaEmpty(db) && hasPendingMigrations(db, migrationsFolder) {
log.Info("Upgrading DB Schema to latest version")
}
goose.SetLogger(gooseLogger)
err = goose.Up(db, migrationsFolder)
if err != nil {
log.Fatal("Failed to apply new migrations", err)
}
}
type statusLogger struct{ numPending int }
func (*statusLogger) Fatalf(format string, v ...interface{}) { log.Fatal(fmt.Sprintf(format, v...)) }
func (l *statusLogger) Printf(format string, v ...interface{}) {
if len(v) < 1 {
return
}
if v0, ok := v[0].(string); !ok {
return
} else if v0 == "Pending" {
l.numPending++
}
}
func hasPendingMigrations(db *sql.DB, folder string) bool {
l := &statusLogger{}
goose.SetLogger(l)
err := goose.Status(db, folder)
if err != nil {
log.Fatal("Failed to check for pending migrations", err)
}
return l.numPending > 0
}
func isSchemaEmpty(db *sql.DB) bool {
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='goose_db_version';") // nolint:rowserrcheck
if err != nil {

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"github.com/navidrome/navidrome/log"
@ -8,10 +9,10 @@ import (
)
func init() {
goose.AddMigration(Up20200130083147, Down20200130083147)
goose.AddMigrationContext(Up20200130083147, Down20200130083147)
}
func Up20200130083147(tx *sql.Tx) error {
func Up20200130083147(_ context.Context, tx *sql.Tx) error {
log.Info("Creating DB Schema")
_, err := tx.Exec(`
create table if not exists album
@ -178,6 +179,6 @@ create table if not exists user
return err
}
func Down20200130083147(tx *sql.Tx) error {
func Down20200130083147(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200131183653, Down20200131183653)
goose.AddMigrationContext(Up20200131183653, Down20200131183653)
}
func Up20200131183653(tx *sql.Tx) error {
func Up20200131183653(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table search_dg_tmp
(
@ -36,7 +37,7 @@ update annotation set item_type = 'media_file' where item_type = 'mediaFile';
return err
}
func Down20200131183653(tx *sql.Tx) error {
func Down20200131183653(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table search_dg_tmp
(

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200208222418, Down20200208222418)
goose.AddMigrationContext(Up20200208222418, Down20200208222418)
}
func Up20200208222418(tx *sql.Tx) error {
func Up20200208222418(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
update annotation set play_count = 0 where play_count is null;
update annotation set rating = 0 where rating is null;
@ -50,6 +51,6 @@ create index annotation_starred
return err
}
func Down20200208222418(tx *sql.Tx) error {
func Down20200208222418(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200220143731, Down20200220143731)
goose.AddMigrationContext(Up20200220143731, Down20200220143731)
}
func Up20200220143731(tx *sql.Tx) error {
func Up20200220143731(_ context.Context, tx *sql.Tx) error {
notice(tx, "This migration will force the next scan to be a full rescan!")
_, err := tx.Exec(`
create table media_file_dg_tmp
@ -124,6 +125,6 @@ update media_file set updated_at = '0001-01-01';
return err
}
func Down20200220143731(tx *sql.Tx) error {
func Down20200220143731(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200310171621, Down20200310171621)
goose.AddMigrationContext(Up20200310171621, Down20200310171621)
}
func Up20200310171621(tx *sql.Tx) error {
func Up20200310171621(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to enable search by Album Artist!")
return forceFullRescan(tx)
}
func Down20200310171621(tx *sql.Tx) error {
func Down20200310171621(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200310181627, Down20200310181627)
goose.AddMigrationContext(Up20200310181627, Down20200310181627)
}
func Up20200310181627(tx *sql.Tx) error {
func Up20200310181627(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table transcoding
(
@ -44,7 +45,7 @@ create table player
return err
}
func Down20200310181627(tx *sql.Tx) error {
func Down20200310181627(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
drop table transcoding;
drop table player;

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200319211049, Down20200319211049)
goose.AddMigrationContext(Up20200319211049, Down20200319211049)
}
func Up20200319211049(tx *sql.Tx) error {
func Up20200319211049(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add full_text varchar(255) default '';
@ -36,6 +37,6 @@ drop table if exists search;
return forceFullRescan(tx)
}
func Down20200319211049(tx *sql.Tx) error {
func Down20200319211049(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200325185135, Down20200325185135)
goose.AddMigrationContext(Up20200325185135, Down20200325185135)
}
func Up20200325185135(tx *sql.Tx) error {
func Up20200325185135(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album
add album_artist_id varchar(255) default '';
@ -29,6 +30,6 @@ create index media_file_artist_album_id
return forceFullRescan(tx)
}
func Down20200325185135(tx *sql.Tx) error {
func Down20200325185135(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200326090707, Down20200326090707)
goose.AddMigrationContext(Up20200326090707, Down20200326090707)
}
func Up20200326090707(tx *sql.Tx) error {
func Up20200326090707(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed!")
return forceFullRescan(tx)
}
func Down20200326090707(tx *sql.Tx) error {
func Down20200326090707(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200327193744, Down20200327193744)
goose.AddMigrationContext(Up20200327193744, Down20200327193744)
}
func Up20200327193744(tx *sql.Tx) error {
func Up20200327193744(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table album_dg_tmp
(
@ -75,6 +76,6 @@ create index album_max_year
return forceFullRescan(tx)
}
func Down20200327193744(tx *sql.Tx) error {
func Down20200327193744(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200404214704, Down20200404214704)
goose.AddMigrationContext(Up20200404214704, Down20200404214704)
}
func Up20200404214704(tx *sql.Tx) error {
func Up20200404214704(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists media_file_year
on media_file (year);
@ -24,6 +25,6 @@ create index if not exists media_file_track_number
return err
}
func Down20200404214704(tx *sql.Tx) error {
func Down20200404214704(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200409002249, Down20200409002249)
goose.AddMigrationContext(Up20200409002249, Down20200409002249)
}
func Up20200409002249(tx *sql.Tx) error {
func Up20200409002249(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to enable search by individual Artist in an Album!")
return forceFullRescan(tx)
}
func Down20200409002249(tx *sql.Tx) error {
func Down20200409002249(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200411164603, Down20200411164603)
goose.AddMigrationContext(Up20200411164603, Down20200411164603)
}
func Up20200411164603(tx *sql.Tx) error {
func Up20200411164603(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table playlist
add created_at datetime;
@ -22,6 +23,6 @@ update playlist
return err
}
func Down20200411164603(tx *sql.Tx) error {
func Down20200411164603(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200418110522, Down20200418110522)
goose.AddMigrationContext(Up20200418110522, Down20200418110522)
}
func Up20200418110522(tx *sql.Tx) error {
func Up20200418110522(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to fix search Albums by year")
return forceFullRescan(tx)
}
func Down20200418110522(tx *sql.Tx) error {
func Down20200418110522(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200419222708, Down20200419222708)
goose.AddMigrationContext(Up20200419222708, Down20200419222708)
}
func Up20200419222708(tx *sql.Tx) error {
func Up20200419222708(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to change the search behaviour")
return forceFullRescan(tx)
}
func Down20200419222708(tx *sql.Tx) error {
func Down20200419222708(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200423204116, Down20200423204116)
goose.AddMigrationContext(Up20200423204116, Down20200423204116)
}
func Up20200423204116(tx *sql.Tx) error {
func Up20200423204116(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add order_artist_name varchar(255) collate nocase;
@ -60,6 +61,6 @@ create index if not exists media_file_order_artist_name
return forceFullRescan(tx)
}
func Down20200423204116(tx *sql.Tx) error {
func Down20200423204116(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200508093059, Down20200508093059)
goose.AddMigrationContext(Up20200508093059, Down20200508093059)
}
func Up20200508093059(tx *sql.Tx) error {
func Up20200508093059(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add song_count integer default 0 not null;
@ -22,6 +23,6 @@ alter table artist
return forceFullRescan(tx)
}
func Down20200508093059(tx *sql.Tx) error {
func Down20200508093059(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200512104202, Down20200512104202)
goose.AddMigrationContext(Up20200512104202, Down20200512104202)
}
func Up20200512104202(tx *sql.Tx) error {
func Up20200512104202(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add disc_subtitle varchar(255);
@ -22,6 +23,6 @@ alter table media_file
return forceFullRescan(tx)
}
func Down20200512104202(tx *sql.Tx) error {
func Down20200512104202(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"strings"
@ -9,10 +10,10 @@ import (
)
func init() {
goose.AddMigration(Up20200516140647, Down20200516140647)
goose.AddMigrationContext(Up20200516140647, Down20200516140647)
}
func Up20200516140647(tx *sql.Tx) error {
func Up20200516140647(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists playlist_tracks
(
@ -95,6 +96,6 @@ func Up20200516140647UpdatePlaylistTracks(tx *sql.Tx, id string, tracks string)
return nil
}
func Down20200516140647(tx *sql.Tx) error {
func Down20200516140647(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200608153717, Down20200608153717)
goose.AddMigrationContext(Up20200608153717, Down20200608153717)
}
func Up20200608153717(tx *sql.Tx) error {
func Up20200608153717(_ context.Context, tx *sql.Tx) error {
// First delete dangling players
_, err := tx.Exec(`
delete from player where user_name not in (select user_name from user)`)
@ -132,6 +133,6 @@ create unique index playlist_tracks_pos
return err
}
func Down20200608153717(tx *sql.Tx) error {
func Down20200608153717(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"github.com/google/uuid"
@ -9,10 +10,10 @@ import (
)
func init() {
goose.AddMigration(upAddDefaultTranscodings, downAddDefaultTranscodings)
goose.AddMigrationContext(upAddDefaultTranscodings, downAddDefaultTranscodings)
}
func upAddDefaultTranscodings(tx *sql.Tx) error {
func upAddDefaultTranscodings(_ context.Context, tx *sql.Tx) error {
row := tx.QueryRow("SELECT COUNT(*) FROM transcoding")
var count int
err := row.Scan(&count)
@ -37,6 +38,6 @@ func upAddDefaultTranscodings(tx *sql.Tx) error {
return nil
}
func downAddDefaultTranscodings(tx *sql.Tx) error {
func downAddDefaultTranscodings(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddPlaylistPath, downAddPlaylistPath)
goose.AddMigrationContext(upAddPlaylistPath, downAddPlaylistPath)
}
func upAddPlaylistPath(tx *sql.Tx) error {
func upAddPlaylistPath(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table playlist
add path string default '' not null;
@ -22,6 +23,6 @@ alter table playlist
return err
}
func downAddPlaylistPath(tx *sql.Tx) error {
func downAddPlaylistPath(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upCreatePlayQueuesTable, downCreatePlayQueuesTable)
goose.AddMigrationContext(upCreatePlayQueuesTable, downCreatePlayQueuesTable)
}
func upCreatePlayQueuesTable(tx *sql.Tx) error {
func upCreatePlayQueuesTable(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table playqueue
(
@ -31,6 +32,6 @@ create table playqueue
return err
}
func downCreatePlayQueuesTable(tx *sql.Tx) error {
func downCreatePlayQueuesTable(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upCreateBookmarkTable, downCreateBookmarkTable)
goose.AddMigrationContext(upCreateBookmarkTable, downCreateBookmarkTable)
}
func upCreateBookmarkTable(tx *sql.Tx) error {
func upCreateBookmarkTable(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table bookmark
(
@ -48,6 +49,6 @@ alter table playqueue_dg_tmp rename to playqueue;
return err
}
func downCreateBookmarkTable(tx *sql.Tx) error {
func downCreateBookmarkTable(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upDropEmailUniqueConstraint, downDropEmailUniqueConstraint)
goose.AddMigrationContext(upDropEmailUniqueConstraint, downDropEmailUniqueConstraint)
}
func upDropEmailUniqueConstraint(tx *sql.Tx) error {
func upDropEmailUniqueConstraint(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table user_dg_tmp
(
@ -37,6 +38,6 @@ alter table user_dg_tmp rename to user;
return err
}
func downDropEmailUniqueConstraint(tx *sql.Tx) error {
func downDropEmailUniqueConstraint(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201003111749, Down20201003111749)
goose.AddMigrationContext(Up20201003111749, Down20201003111749)
}
func Up20201003111749(tx *sql.Tx) error {
func Up20201003111749(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists annotation_starred_at
on annotation (starred_at);
@ -18,6 +19,6 @@ create index if not exists annotation_starred_at
return err
}
func Down20201003111749(tx *sql.Tx) error {
func Down20201003111749(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201010162350, Down20201010162350)
goose.AddMigrationContext(Up20201010162350, Down20201010162350)
}
func Up20201010162350(tx *sql.Tx) error {
func Up20201010162350(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album
add size integer default 0 not null;
@ -27,7 +28,7 @@ where id not null;`)
return err
}
func Down20201010162350(tx *sql.Tx) error {
func Down20201010162350(_ context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201012210022, Down20201012210022)
goose.AddMigrationContext(Up20201012210022, Down20201012210022)
}
func Up20201012210022(tx *sql.Tx) error {
func Up20201012210022(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add size integer default 0 not null;
@ -39,6 +40,6 @@ update playlist set size = ifnull((
return err
}
func Down20201012210022(tx *sql.Tx) error {
func Down20201012210022(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201021085410, Down20201021085410)
goose.AddMigrationContext(Up20201021085410, Down20201021085410)
}
func Up20201021085410(tx *sql.Tx) error {
func Up20201021085410(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add mbz_track_id varchar(255);
@ -52,7 +53,7 @@ alter table artist
return forceFullRescan(tx)
}
func Down20201021085410(tx *sql.Tx) error {
func Down20201021085410(_ context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201021093209, Down20201021093209)
goose.AddMigrationContext(Up20201021093209, Down20201021093209)
}
func Up20201021093209(tx *sql.Tx) error {
func Up20201021093209(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists media_file_artist
on media_file (artist);
@ -22,6 +23,6 @@ create index if not exists media_file_mbz_track_id
return err
}
func Down20201021093209(tx *sql.Tx) error {
func Down20201021093209(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201021135455, Down20201021135455)
goose.AddMigrationContext(Up20201021135455, Down20201021135455)
}
func Up20201021135455(tx *sql.Tx) error {
func Up20201021135455(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists media_file_artist_id
on media_file (artist_id);
@ -18,6 +19,6 @@ create index if not exists media_file_artist_id
return err
}
func Down20201021135455(tx *sql.Tx) error {
func Down20201021135455(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddArtistImageUrl, downAddArtistImageUrl)
goose.AddMigrationContext(upAddArtistImageUrl, downAddArtistImageUrl)
}
func upAddArtistImageUrl(tx *sql.Tx) error {
func upAddArtistImageUrl(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add biography varchar(255) default '' not null;
@ -30,6 +31,6 @@ alter table artist
return err
}
func downAddArtistImageUrl(tx *sql.Tx) error {
func downAddArtistImageUrl(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201110205344, Down20201110205344)
goose.AddMigrationContext(Up20201110205344, Down20201110205344)
}
func Up20201110205344(tx *sql.Tx) error {
func Up20201110205344(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add comment varchar;
@ -27,6 +28,6 @@ alter table album
return forceFullRescan(tx)
}
func Down20201110205344(tx *sql.Tx) error {
func Down20201110205344(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201128100726, Down20201128100726)
goose.AddMigrationContext(Up20201128100726, Down20201128100726)
}
func Up20201128100726(tx *sql.Tx) error {
func Up20201128100726(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table player
add report_real_path bool default FALSE not null;
@ -18,6 +19,6 @@ alter table player
return err
}
func Down20201128100726(tx *sql.Tx) error {
func Down20201128100726(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"github.com/navidrome/navidrome/log"
@ -9,10 +10,10 @@ import (
)
func init() {
goose.AddMigration(Up20201213124814, Down20201213124814)
goose.AddMigrationContext(Up20201213124814, Down20201213124814)
}
func Up20201213124814(tx *sql.Tx) error {
func Up20201213124814(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album
add all_artist_ids varchar;
@ -58,6 +59,6 @@ select a.id, a.name, a.artist_id, a.album_artist_id, group_concat(mf.artist_id,
return rows.Err()
}
func Down20201213124814(tx *sql.Tx) error {
func Down20201213124814(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddTimestampIndexesGo, downAddTimestampIndexesGo)
goose.AddMigrationContext(upAddTimestampIndexesGo, downAddTimestampIndexesGo)
}
func upAddTimestampIndexesGo(tx *sql.Tx) error {
func upAddTimestampIndexesGo(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists album_updated_at
on album (updated_at);
@ -28,6 +29,6 @@ create index if not exists media_file_updated_at
return err
}
func downAddTimestampIndexesGo(tx *sql.Tx) error {
func downAddTimestampIndexesGo(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"strings"
@ -10,10 +11,10 @@ import (
)
func init() {
goose.AddMigration(upFixAlbumComments, downFixAlbumComments)
goose.AddMigrationContext(upFixAlbumComments, downFixAlbumComments)
}
func upFixAlbumComments(tx *sql.Tx) error {
func upFixAlbumComments(_ context.Context, tx *sql.Tx) error {
//nolint:gosec
rows, err := tx.Query(`
SELECT album.id, group_concat(media_file.comment, '` + consts.Zwsp + `') FROM album, media_file WHERE media_file.album_id = album.id GROUP BY album.id;
@ -48,7 +49,7 @@ func upFixAlbumComments(tx *sql.Tx) error {
return rows.Err()
}
func downFixAlbumComments(tx *sql.Tx) error {
func downFixAlbumComments(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddBpmMetadata, downAddBpmMetadata)
goose.AddMigrationContext(upAddBpmMetadata, downAddBpmMetadata)
}
func upAddBpmMetadata(tx *sql.Tx) error {
func upAddBpmMetadata(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add bpm integer;
@ -25,6 +26,6 @@ create index if not exists media_file_bpm
return forceFullRescan(tx)
}
func downAddBpmMetadata(tx *sql.Tx) error {
func downAddBpmMetadata(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upCreateSharesTable, downCreateSharesTable)
goose.AddMigrationContext(upCreateSharesTable, downCreateSharesTable)
}
func upCreateSharesTable(tx *sql.Tx) error {
func upCreateSharesTable(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table share
(
@ -29,6 +30,6 @@ create table share
return err
}
func downCreateSharesTable(tx *sql.Tx) error {
func downCreateSharesTable(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upUpdateShareFieldNames, downUpdateShareFieldNames)
goose.AddMigrationContext(upUpdateShareFieldNames, downUpdateShareFieldNames)
}
func upUpdateShareFieldNames(tx *sql.Tx) error {
func upUpdateShareFieldNames(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table share rename column expires to expires_at;
alter table share rename column created to created_at;
@ -20,6 +21,6 @@ alter table share rename column last_visited to last_visited_at;
return err
}
func downUpdateShareFieldNames(tx *sql.Tx) error {
func downUpdateShareFieldNames(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -12,10 +12,10 @@ import (
)
func init() {
goose.AddMigration(upEncodeAllPasswords, downEncodeAllPasswords)
goose.AddMigrationContext(upEncodeAllPasswords, downEncodeAllPasswords)
}
func upEncodeAllPasswords(tx *sql.Tx) error {
func upEncodeAllPasswords(ctx context.Context, tx *sql.Tx) error {
rows, err := tx.Query(`SELECT id, user_name, password from user;`)
if err != nil {
return err
@ -38,7 +38,7 @@ func upEncodeAllPasswords(tx *sql.Tx) error {
return err
}
password, err = utils.Encrypt(context.Background(), encKey, password)
password, err = utils.Encrypt(ctx, encKey, password)
if err != nil {
log.Error("Error encrypting user's password", "id", id, "username", username, err)
}
@ -51,6 +51,6 @@ func upEncodeAllPasswords(tx *sql.Tx) error {
return rows.Err()
}
func downEncodeAllPasswords(tx *sql.Tx) error {
func downEncodeAllPasswords(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upDropPlayerNameUniqueConstraint, downDropPlayerNameUniqueConstraint)
goose.AddMigrationContext(upDropPlayerNameUniqueConstraint, downDropPlayerNameUniqueConstraint)
}
func upDropPlayerNameUniqueConstraint(tx *sql.Tx) error {
func upDropPlayerNameUniqueConstraint(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table player_dg_tmp
(
@ -42,6 +43,6 @@ create index if not exists player_name
return err
}
func downDropPlayerNameUniqueConstraint(tx *sql.Tx) error {
func downDropPlayerNameUniqueConstraint(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddUserPrefsPlayerScrobblerEnabled, downAddUserPrefsPlayerScrobblerEnabled)
goose.AddMigrationContext(upAddUserPrefsPlayerScrobblerEnabled, downAddUserPrefsPlayerScrobblerEnabled)
}
func upAddUserPrefsPlayerScrobblerEnabled(tx *sql.Tx) error {
func upAddUserPrefsPlayerScrobblerEnabled(_ context.Context, tx *sql.Tx) error {
err := upAddUserPrefs(tx)
if err != nil {
return err
@ -39,6 +40,6 @@ alter table player add scrobble_enabled bool default true;
return err
}
func downAddUserPrefsPlayerScrobblerEnabled(tx *sql.Tx) error {
func downAddUserPrefsPlayerScrobblerEnabled(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddReferentialIntegrityToUserProps, downAddReferentialIntegrityToUserProps)
goose.AddMigrationContext(upAddReferentialIntegrityToUserProps, downAddReferentialIntegrityToUserProps)
}
func upAddReferentialIntegrityToUserProps(tx *sql.Tx) error {
func upAddReferentialIntegrityToUserProps(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table user_props_dg_tmp
(
@ -33,6 +34,6 @@ alter table user_props_dg_tmp rename to user_props;
return err
}
func downAddReferentialIntegrityToUserProps(tx *sql.Tx) error {
func downAddReferentialIntegrityToUserProps(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddScrobbleBuffer, downAddScrobbleBuffer)
goose.AddMigrationContext(upAddScrobbleBuffer, downAddScrobbleBuffer)
}
func upAddScrobbleBuffer(tx *sql.Tx) error {
func upAddScrobbleBuffer(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists scrobble_buffer
(
@ -33,6 +34,6 @@ create table if not exists scrobble_buffer
return err
}
func downAddScrobbleBuffer(tx *sql.Tx) error {
func downAddScrobbleBuffer(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddGenreTables, downAddGenreTables)
goose.AddMigrationContext(upAddGenreTables, downAddGenreTables)
}
func upAddGenreTables(tx *sql.Tx) error {
func upAddGenreTables(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to import multiple genres!")
_, err := tx.Exec(`
create table if not exists genre
@ -63,6 +64,6 @@ create table if not exists artist_genres
return forceFullRescan(tx)
}
func downAddGenreTables(tx *sql.Tx) error {
func downAddGenreTables(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddMediafileChannels, downAddMediafileChannels)
goose.AddMigrationContext(upAddMediafileChannels, downAddMediafileChannels)
}
func upAddMediafileChannels(tx *sql.Tx) error {
func upAddMediafileChannels(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add channels integer;
@ -25,6 +26,6 @@ create index if not exists media_file_channels
return forceFullRescan(tx)
}
func downAddMediafileChannels(tx *sql.Tx) error {
func downAddMediafileChannels(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddSmartPlaylist, downAddSmartPlaylist)
goose.AddMigrationContext(upAddSmartPlaylist, downAddSmartPlaylist)
}
func upAddSmartPlaylist(tx *sql.Tx) error {
func upAddSmartPlaylist(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table playlist
add column rules varchar null;
@ -32,6 +33,6 @@ create unique index playlist_fields_idx
return err
}
func downAddSmartPlaylist(tx *sql.Tx) error {
func downAddSmartPlaylist(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"strings"
@ -10,10 +11,10 @@ import (
)
func init() {
goose.AddMigration(upAddOrderTitleToMediaFile, downAddOrderTitleToMediaFile)
goose.AddMigrationContext(upAddOrderTitleToMediaFile, downAddOrderTitleToMediaFile)
}
func upAddOrderTitleToMediaFile(tx *sql.Tx) error {
func upAddOrderTitleToMediaFile(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table main.media_file
add order_title varchar null collate NOCASE;
@ -56,6 +57,6 @@ func upAddOrderTitleToMediaFile_populateOrderTitle(tx *sql.Tx) error {
return rows.Err()
}
func downAddOrderTitleToMediaFile(tx *sql.Tx) error {
func downAddOrderTitleToMediaFile(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"github.com/navidrome/navidrome/log"
@ -9,10 +10,10 @@ import (
)
func init() {
goose.AddMigration(upUnescapeLyricsAndComments, downUnescapeLyricsAndComments)
goose.AddMigrationContext(upUnescapeLyricsAndComments, downUnescapeLyricsAndComments)
}
func upUnescapeLyricsAndComments(tx *sql.Tx) error {
func upUnescapeLyricsAndComments(_ context.Context, tx *sql.Tx) error {
rows, err := tx.Query(`select id, comment, lyrics, title from media_file`)
if err != nil {
return err
@ -42,6 +43,6 @@ func upUnescapeLyricsAndComments(tx *sql.Tx) error {
return rows.Err()
}
func downUnescapeLyricsAndComments(tx *sql.Tx) error {
func downUnescapeLyricsAndComments(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddUseridToPlaylist, downAddUseridToPlaylist)
goose.AddMigrationContext(upAddUseridToPlaylist, downAddUseridToPlaylist)
}
func upAddUseridToPlaylist(tx *sql.Tx) error {
func upAddUseridToPlaylist(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table playlist_dg_tmp
(
@ -55,6 +56,6 @@ create index playlist_updated_at
return err
}
func downAddUseridToPlaylist(tx *sql.Tx) error {
func downAddUseridToPlaylist(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddAlphabeticalByArtistIndex, downAddAlphabeticalByArtistIndex)
goose.AddMigrationContext(upAddAlphabeticalByArtistIndex, downAddAlphabeticalByArtistIndex)
}
func upAddAlphabeticalByArtistIndex(tx *sql.Tx) error {
func upAddAlphabeticalByArtistIndex(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index album_alphabetical_by_artist
ON album(compilation, order_album_artist_name, order_album_name)
@ -18,6 +19,6 @@ create index album_alphabetical_by_artist
return err
}
func downAddAlphabeticalByArtistIndex(tx *sql.Tx) error {
func downAddAlphabeticalByArtistIndex(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,22 +1,23 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upRemoveInvalidArtistIds, downRemoveInvalidArtistIds)
goose.AddMigrationContext(upRemoveInvalidArtistIds, downRemoveInvalidArtistIds)
}
func upRemoveInvalidArtistIds(tx *sql.Tx) error {
func upRemoveInvalidArtistIds(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
update media_file set artist_id = '' where not exists(select 1 from artist where id = artist_id)
`)
return err
}
func downRemoveInvalidArtistIds(tx *sql.Tx) error {
func downRemoveInvalidArtistIds(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddMusicbrainzReleaseTrackId, downAddMusicbrainzReleaseTrackId)
goose.AddMigrationContext(upAddMusicbrainzReleaseTrackId, downAddMusicbrainzReleaseTrackId)
}
func upAddMusicbrainzReleaseTrackId(tx *sql.Tx) error {
func upAddMusicbrainzReleaseTrackId(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add mbz_release_track_id varchar(255);
@ -22,7 +23,7 @@ alter table media_file
return forceFullRescan(tx)
}
func downAddMusicbrainzReleaseTrackId(tx *sql.Tx) error {
func downAddMusicbrainzReleaseTrackId(_ context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddAlbumImagePaths, downAddAlbumImagePaths)
goose.AddMigrationContext(upAddAlbumImagePaths, downAddAlbumImagePaths)
}
func upAddAlbumImagePaths(tx *sql.Tx) error {
func upAddAlbumImagePaths(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table main.album add image_files varchar;
`)
@ -21,6 +22,6 @@ alter table main.album add image_files varchar;
return forceFullRescan(tx)
}
func downAddAlbumImagePaths(tx *sql.Tx) error {
func downAddAlbumImagePaths(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upRemoveCoverArtId, downRemoveCoverArtId)
goose.AddMigrationContext(upRemoveCoverArtId, downRemoveCoverArtId)
}
func upRemoveCoverArtId(tx *sql.Tx) error {
func upRemoveCoverArtId(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album drop column cover_art_id;
alter table album rename column cover_art_path to embed_art_path
@ -22,6 +23,6 @@ alter table album rename column cover_art_path to embed_art_path
return forceFullRescan(tx)
}
func downRemoveCoverArtId(tx *sql.Tx) error {
func downRemoveCoverArtId(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,21 +1,22 @@
package migrations
import (
"context"
"database/sql"
"path/filepath"
"slices"
"strings"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/pressly/goose/v3"
"golang.org/x/exp/slices"
)
func init() {
goose.AddMigration(upAddAlbumPaths, downAddAlbumPaths)
goose.AddMigrationContext(upAddAlbumPaths, downAddAlbumPaths)
}
func upAddAlbumPaths(tx *sql.Tx) error {
func upAddAlbumPaths(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`alter table album add paths varchar;`)
if err != nil {
return err
@ -62,6 +63,6 @@ func upAddAlbumPathsDirs(filePaths string) string {
return strings.Join(dirs, string(filepath.ListSeparator))
}
func downAddAlbumPaths(tx *sql.Tx) error {
func downAddAlbumPaths(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upTouchPlaylists, downTouchPlaylists)
goose.AddMigrationContext(upTouchPlaylists, downTouchPlaylists)
}
func upTouchPlaylists(tx *sql.Tx) error {
func upTouchPlaylists(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`update playlist set updated_at = datetime('now');`)
return err
}
func downTouchPlaylists(tx *sql.Tx) error {
func downTouchPlaylists(_ context.Context, tx *sql.Tx) error {
return nil
}

Some files were not shown because too many files have changed in this diff Show More