Merge branch 'master' into subsonic-reverse-proxy-auth/2557
This commit is contained in:
commit
3c4b9118a0
|
@ -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
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -24,4 +24,5 @@ navidrome.db-wal
|
|||
tags
|
||||
.gitinfo
|
||||
docker-compose.yml
|
||||
!contrib/docker-compose.yml
|
||||
!contrib/docker-compose.yml
|
||||
test-123.db
|
||||
|
|
17
Makefile
17
Makefile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 -",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//go:build !windows
|
||||
|
||||
package mpv
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
func socketName(prefix, suffix string) string {
|
||||
return utils.TempFileName(prefix, suffix)
|
||||
}
|
||||
|
||||
func removeSocket(socketName string) {
|
||||
log.Debug("Removing socketfile", "socketfile", socketName)
|
||||
err := os.Remove(socketName)
|
||||
if err != nil {
|
||||
log.Error("Error cleaning up socketfile", "socketfile", socketName, err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//go:build windows
|
||||
|
||||
package mpv
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func socketName(prefix, suffix string) string {
|
||||
// Windows needs to use a named pipe for the socket
|
||||
// see https://mpv.io/manual/master#using-mpv-from-other-programs-or-scripts
|
||||
return filepath.Join(`\\.\pipe\mpvsocket`, prefix+uuid.NewString()+suffix)
|
||||
}
|
||||
|
||||
func removeSocket(string) {
|
||||
// Windows automatically handles cleaning up named pipe
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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...)
|
||||
|
|
29
db/db.go
29
db/db.go
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue