Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43bb075849 | ||
|
|
e8d409e7c0 | ||
|
|
8e1982c633 |
@@ -4,18 +4,16 @@
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
// Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14
|
||||
"VARIANT": "1.20",
|
||||
"VARIANT": "1.16",
|
||||
// Options
|
||||
"INSTALL_NODE": "true",
|
||||
"NODE_VERSION": "v18"
|
||||
"NODE_VERSION": "v14"
|
||||
}
|
||||
},
|
||||
"workspaceMount": "",
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined",
|
||||
"--volume=${localWorkspaceFolder}:/workspaces/${localWorkspaceFolderBasename}:Z"
|
||||
"seccomp=unconfined"
|
||||
],
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
|
||||
103
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,103 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Before opening a new issue, please search to see if an issue already exists for the bug you encountered.
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
#assignees:
|
||||
# - deluan
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to fill out this bug report!
|
||||
- type: checkboxes
|
||||
id: requirements
|
||||
attributes:
|
||||
label: "I confirm that:"
|
||||
options:
|
||||
- label: I have searched the existing [open AND closed issues](https://github.com/navidrome/navidrome/issues?q=is%3Aissue) to see if an issue already exists for the bug I've encountered
|
||||
required: true
|
||||
- label: I'm using the latest version (your issue may have been fixed already)
|
||||
required: false
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Navidrome are you running? (please try upgrading first, as your issue may have been fixed already).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this scenario...
|
||||
2. With this config...
|
||||
3. Click (or Execute) '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: env
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: Ubuntu 20.04
|
||||
- **Browser**: Chrome 110.0.5481.177 on Windows 11
|
||||
- **Client**: DSub 5.5.1
|
||||
value: |
|
||||
- OS:
|
||||
- Browser:
|
||||
- Client:
|
||||
render: markdown
|
||||
- type: dropdown
|
||||
id: distribution
|
||||
attributes:
|
||||
label: How Navidrome is installed?
|
||||
multiple: false
|
||||
options:
|
||||
- Docker
|
||||
- Binary (from downloads page)
|
||||
- Package
|
||||
- Built from sources
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: Please copy and paste your `navidrome.toml` (and/or `docker-compose.yml`) configuration. This will be automatically formatted into code, so no need for backticks.
|
||||
render: toml
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output (change your `LogLevel` (`ND_LOGLEVEL`) to debug). This will be automatically formatted into code, so no need for backticks. ([Where I can find the logs?](https://www.navidrome.org/docs/faq/#where-are-the-logs))
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach screenshots by clicking this area to highlight it and then dragging files in.
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/navidrome/navidrome/blob/master/CODE_OF_CONDUCT.md).
|
||||
options:
|
||||
- label: I agree to follow Navidrome's Code of Conduct
|
||||
required: true
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ideas for new features
|
||||
url: https://github.com/navidrome/navidrome/discussions/categories/ideas
|
||||
about: This is the place to share and discuss new ideas and potentially new features.
|
||||
- name: Support requests
|
||||
url: https://github.com/navidrome/navidrome/discussions/categories/q-a
|
||||
about: This is the place to ask questions.
|
||||
BIN
.github/screenshots/ss-desktop-player.png
vendored
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.7 MiB |
BIN
.github/screenshots/ss-mobile-album-view.png
vendored
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 236 KiB |
BIN
.github/screenshots/ss-mobile-login.png
vendored
|
Before Width: | Height: | Size: 735 KiB After Width: | Height: | Size: 736 KiB |
BIN
.github/screenshots/ss-mobile-player.png
vendored
|
Before Width: | Height: | Size: 885 KiB After Width: | Height: | Size: 886 KiB |
22
.github/workflows/docker-tags.sh
vendored
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
GIT_TAG="${GITHUB_REF##refs/tags/}"
|
||||
GIT_BRANCH="${GITHUB_REF##refs/heads/}"
|
||||
GIT_SHA=$(git rev-parse --short HEAD)
|
||||
PR_NUM=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
|
||||
|
||||
DOCKER_IMAGE_TAG="--tag ${DOCKER_IMAGE}:sha-${GIT_SHA}"
|
||||
|
||||
if [[ $PR_NUM != "null" ]]; then
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:pr-${PR_NUM}"
|
||||
fi
|
||||
|
||||
if [[ $GITHUB_REF != "$GIT_TAG" ]]; then
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:${GIT_TAG#v} --tag ${DOCKER_IMAGE}:latest"
|
||||
elif [[ $GITHUB_REF == "refs/heads/master" ]]; then
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:develop"
|
||||
elif [[ $GIT_BRANCH = feature/* ]]; then
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:$(echo $GIT_BRANCH | tr / -)"
|
||||
fi
|
||||
|
||||
echo ${DOCKER_IMAGE_TAG}
|
||||
54
.github/workflows/download-link-on-pr.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Add download link to PR
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Pipeline: Test, Lint, Build']
|
||||
types: [completed]
|
||||
jobs:
|
||||
pr_comment:
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
# This snippet is public-domain, taken from
|
||||
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
||||
script: |
|
||||
const {owner, repo} = context.repo;
|
||||
const run_id = ${{github.event.workflow_run.id}};
|
||||
const pull_head_sha = '${{github.event.workflow_run.head_sha}}';
|
||||
const pull_user_id = ${{github.event.sender.id}};
|
||||
|
||||
const issue_number = await (async () => {
|
||||
const pulls = await github.pulls.list({owner, repo});
|
||||
for await (const {data} of github.paginate.iterator(pulls)) {
|
||||
for (const pull of data) {
|
||||
if (pull.head.sha === pull_head_sha && pull.user.id === pull_user_id) {
|
||||
return pull.number;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
if (issue_number) {
|
||||
core.info(`Using pull request ${issue_number}`);
|
||||
} else {
|
||||
return core.error(`No matching pull request found`);
|
||||
}
|
||||
|
||||
const {data: {artifacts}} = await github.actions.listWorkflowRunArtifacts({owner, repo, run_id});
|
||||
if (!artifacts.length) {
|
||||
return core.error(`No artifacts found`);
|
||||
}
|
||||
let body = `Download the artifacts for this pull request:\n`;
|
||||
for (const art of artifacts) {
|
||||
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
}
|
||||
|
||||
const {data: comments} = await github.issues.listComments({repo, owner, issue_number});
|
||||
const existing_comment = comments.find((c) => c.user.login === 'github-actions[bot]');
|
||||
if (existing_comment) {
|
||||
core.info(`Updating comment ${existing_comment.id}`);
|
||||
await github.issues.updateComment({repo, owner, comment_id: existing_comment.id, body});
|
||||
} else {
|
||||
core.info(`Creating a comment`);
|
||||
await github.issues.createComment({repo, owner, issue_number, body});
|
||||
}
|
||||
3
.github/workflows/pipeline.dockerfile
vendored
@@ -6,8 +6,7 @@ ARG TARGETPLATFORM
|
||||
RUN echo "Target Platform = ${TARGETPLATFORM}"
|
||||
|
||||
COPY dist .
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then cp navidrome_linux_amd64_linux_amd64_v1/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/386" ]; then cp navidrome_linux_386_linux_386/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then cp navidrome_linux_amd64_linux_amd64/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then cp navidrome_linux_arm64_linux_arm64/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then cp navidrome_linux_arm_linux_arm_6/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then cp navidrome_linux_arm_linux_arm_7/navidrome /navidrome; fi
|
||||
|
||||
181
.github/workflows/pipeline.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 'Pipeline: Test, Lint, Build'
|
||||
name: Pipeline
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -8,82 +8,50 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
go-lint:
|
||||
name: Lint Go code
|
||||
golangci-lint:
|
||||
name: Lint Server
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install taglib
|
||||
run: sudo apt-get install libtag1-dev
|
||||
|
||||
- name: Set up Go 1.20
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: latest
|
||||
version: v1.38
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --timeout 2m
|
||||
|
||||
- name: Install goimports
|
||||
run: go install golang.org/x/tools/cmd/goimports
|
||||
|
||||
- run: goimports -w `find . -name '*.go' | grep -v '_gen.go$'`
|
||||
- run: go mod tidy
|
||||
- name: Verify no changes from goimports and go mod tidy
|
||||
run: |
|
||||
git status --porcelain
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo 'To fix this check, run "goimports -w $(find . -name '*.go' | grep -v '_gen.go$') && go mod tidy"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Build and Lint OpenAPI spec
|
||||
run: make lintapi gen
|
||||
|
||||
- name: Verify no changes
|
||||
run: |
|
||||
git status --porcelain
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo 'Changes to OpenAPI spec caused changes to the code. Please review and commit the changes.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: openapi.yaml
|
||||
path: api/openapi.yaml
|
||||
|
||||
go:
|
||||
name: Test with Go ${{ matrix.go_version }}
|
||||
name: Test Server with Go ${{ matrix.go_version }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go_version: [1.20.x,1.19.x]
|
||||
go_version: [1.16.x]
|
||||
steps:
|
||||
- name: Install taglib
|
||||
run: sudo apt-get install libtag1-dev
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ matrix.go_version }}
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
stable: '!contains(${{ matrix.go_version }}, "beta") && !contains(${{ matrix.go_version }}, "rc")'
|
||||
go-version: ${{ matrix.go_version }}
|
||||
cache: true
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: cache-go
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ matrix.go_version }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.go_version }}-
|
||||
|
||||
- name: Download dependencies
|
||||
if: steps.cache-go.outputs.cache-hit != 'true'
|
||||
@@ -92,30 +60,35 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
continue-on-error: ${{contains(matrix.go_version, 'beta') || contains(matrix.go_version, 'rc')}}
|
||||
run: go test -shuffle=on -race -cover ./... -v
|
||||
|
||||
run: go test -cover ./... -v
|
||||
js:
|
||||
name: Build JS bundle
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: 14
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: cache-npm
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('ui/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: npm install dependencies
|
||||
run: |
|
||||
cd ui
|
||||
npm ci
|
||||
|
||||
- name: npm lint
|
||||
- name: npm check-formatting
|
||||
run: |
|
||||
cd ui
|
||||
npm run check-formatting && npm run lint
|
||||
npm run check-formatting
|
||||
|
||||
- name: npm test
|
||||
run: |
|
||||
@@ -127,36 +100,35 @@ jobs:
|
||||
cd ui
|
||||
npm run build
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: js-bundle
|
||||
path: ui/build
|
||||
|
||||
binaries:
|
||||
name: Build binaries
|
||||
needs: [js, go, go-lint]
|
||||
name: Binaries
|
||||
needs: [js, go, golangci-lint]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: js-bundle
|
||||
path: ui/build
|
||||
|
||||
- name: Config /github/workspace folder as trusted
|
||||
uses: docker://deluan/ci-goreleaser:1.20.3-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: Show Tags
|
||||
run: git tag
|
||||
|
||||
- name: Show Version
|
||||
run: git describe --tags
|
||||
|
||||
- name: Run GoReleaser - SNAPSHOT
|
||||
if: startsWith(github.ref, 'refs/tags/') != true
|
||||
uses: docker://deluan/ci-goreleaser:1.20.3-1
|
||||
uses: docker://deluan/ci-goreleaser:1.16.2-1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -164,13 +136,13 @@ jobs:
|
||||
|
||||
- name: Run GoReleaser - RELEASE
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: docker://deluan/ci-goreleaser:1.20.3-1
|
||||
uses: docker://deluan/ci-goreleaser:1.16.2-1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: goreleaser release --rm-dist
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: binaries
|
||||
path: |
|
||||
@@ -179,7 +151,7 @@ jobs:
|
||||
!dist/*.zip
|
||||
|
||||
docker:
|
||||
name: Build and publish Docker images
|
||||
name: Docker images
|
||||
needs: [binaries]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -187,59 +159,28 @@ jobs:
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v1
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v2
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
with:
|
||||
name: binaries
|
||||
path: dist
|
||||
|
||||
- name: Login to Docker Hub
|
||||
- name: Build the Docker image and push
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
labels: |
|
||||
maintainer=deluan
|
||||
images: |
|
||||
name=${{secrets.DOCKER_IMAGE}},enable=${{env.GITHUB_REF_TYPE == 'tag' || github.ref == format('refs/heads/{0}', 'master')}}
|
||||
name=ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=develop,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and Push
|
||||
if: env.DOCKER_IMAGE != ''
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: .github/workflows/pipeline.dockerfile
|
||||
platforms: linux/amd64,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
env:
|
||||
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
|
||||
DOCKER_PLATFORM: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
run: |
|
||||
echo ${{secrets.DOCKER_PASSWORD}} | docker login -u ${{secrets.DOCKER_USERNAME}} --password-stdin
|
||||
docker buildx build --platform ${DOCKER_PLATFORM} `.github/workflows/docker-tags.sh` -f .github/workflows/pipeline.dockerfile --push .
|
||||
|
||||
18
.github/workflows/remove-old-artifacts.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Remove old artifacts
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every day at 1am
|
||||
- cron: '0 1 * * *'
|
||||
|
||||
jobs:
|
||||
remove-old-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Remove old artifacts
|
||||
uses: c-hive/gha-remove-artifacts@v1
|
||||
with:
|
||||
age: '7 days'
|
||||
skip-tags: false
|
||||
55
.github/workflows/stale.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
stale:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4.0.0
|
||||
with:
|
||||
issue-inactive-days: 120
|
||||
pr-inactive-days: 120
|
||||
log-output: true
|
||||
add-issue-labels: 'frozen-due-to-age'
|
||||
add-pr-labels: 'frozen-due-to-age'
|
||||
issue-comment: >
|
||||
This issue has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
pr-comment: >
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
- uses: actions/stale@v7
|
||||
with:
|
||||
operations-per-run: 999
|
||||
days-before-issue-stale: 180
|
||||
days-before-pr-stale: 180
|
||||
days-before-issue-close: 30
|
||||
days-before-pr-close: 30
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Navidrome team are limited, and so we are asking for your help.
|
||||
|
||||
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
|
||||
|
||||
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
|
||||
|
||||
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-pr-message: This PR has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Navidrome team are limited, and so we are asking for your help.
|
||||
|
||||
Please check https://github.com/navidrome/navidrome/blob/master/CONTRIBUTING.md#pull-requests and verify that this code contribution fits with the description. If yes, tell it in a comment.
|
||||
|
||||
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'keep,security'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'keep,security'
|
||||
28
.github/workflows/update-translations.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: POEditor import
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
jobs:
|
||||
update-translations:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'navidrome' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get updated translations
|
||||
env:
|
||||
POEDITOR_PROJECTID: ${{ secrets.POEDITOR_PROJECTID }}
|
||||
POEDITOR_APIKEY: ${{ secrets.POEDITOR_APIKEY }}
|
||||
run: |
|
||||
./update-translations.sh
|
||||
- name: Show changes, if any
|
||||
run: |
|
||||
git status --porcelain
|
||||
git diff
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update translations
|
||||
title: Update translations from POEditor
|
||||
branch: update-translations
|
||||
7
.gitignore
vendored
@@ -14,15 +14,12 @@ navidrome.toml
|
||||
master.zip
|
||||
testDB
|
||||
navidrome.db
|
||||
cache/*
|
||||
*.swp
|
||||
embedded_gen.go
|
||||
dist
|
||||
music
|
||||
docker-compose.yml
|
||||
navidrome.db-shm
|
||||
navidrome.db-wal
|
||||
tags
|
||||
.gitinfo
|
||||
docker-compose.yml
|
||||
!contrib/docker-compose.yml
|
||||
/api/openapi.yaml
|
||||
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
run:
|
||||
go: "1.19"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- gocyclo
|
||||
- goimports
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
- rowserrcheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
|
||||
@@ -10,7 +10,7 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=embed,netgo
|
||||
ldflags:
|
||||
- "-extldflags '-static -lz'"
|
||||
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
|
||||
@@ -18,13 +18,12 @@ builds:
|
||||
- id: navidrome_linux_386
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- PKG_CONFIG_PATH=/i386/lib/pkgconfig
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- "386"
|
||||
- 386
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=embed,netgo
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
|
||||
@@ -34,17 +33,16 @@ builds:
|
||||
- CGO_ENABLED=1
|
||||
- CC=arm-linux-gnueabi-gcc
|
||||
- CXX=arm-linux-gnueabi-g++
|
||||
- PKG_CONFIG_PATH=/arm/lib/pkgconfig
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=embed,netgo
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
|
||||
@@ -54,18 +52,17 @@ builds:
|
||||
- CGO_ENABLED=1
|
||||
- CC=aarch64-linux-gnu-gcc
|
||||
- CXX=aarch64-linux-gnu-g++
|
||||
- PKG_CONFIG_PATH=/arm64/lib/pkgconfig
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm64
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=embed,netgo
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_windows_386
|
||||
- id: navidrome_windows_i686
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=i686-w64-mingw32-gcc
|
||||
@@ -74,14 +71,14 @@ builds:
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- "386"
|
||||
- 386
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=embed,netgo
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_windows_amd64
|
||||
- id: navidrome_windows_x64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
@@ -92,12 +89,12 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=embed,netgo
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_darwin_amd64
|
||||
- id: navidrome_darwin
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=o64-clang
|
||||
@@ -108,7 +105,7 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=embed,netgo
|
||||
ldflags:
|
||||
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# Navidrome Contribution Guide
|
||||
|
||||
Navidrome is a streaming service which allows you to enjoy your music collection from anywhere. We'd welcome you to contribute to our open source project and make Navidrome even better. There are some basic guidelines which you need to follow if you like to contribute to Navidrome.
|
||||
|
||||
- [Asking Support Questions](#asking-support-questions)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Issues](#issues)
|
||||
- [Pull Requests](#pull-requests)
|
||||
|
||||
|
||||
## Asking Support Questions
|
||||
We have an active [discussion forum](https://github.com/navidrome/navidrome/discussions) where users and developers can ask questions. Please don't use the GitHub issue tracker to ask questions.
|
||||
|
||||
## Code of Conduct
|
||||
Please read the following [Code of Conduct](https://github.com/navidrome/navidrome/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Issues
|
||||
Found any issue or bug in our codebase? Have a great idea you want to propose or discuss with
|
||||
the developers? You can help by submitting an [issue](https://github.com/navidrome/navidrome/issues/new/choose)
|
||||
to the GitHub repository.
|
||||
|
||||
**Before opening a new issue, please check if the issue has not been already made by searching
|
||||
the [issues](https://github.com/navidrome/navidrome/issues)**
|
||||
|
||||
## Pull requests
|
||||
Before submitting a pull request, ensure that you go through the following:
|
||||
- Open a corresponding issue for the Pull Request, if not existing. The issue can be opened following [these guidelines](#issues)
|
||||
- Ensure that there is no open or closed Pull Request corresponding to your submission to avoid duplication of effort.
|
||||
- Setup the [development environment](https://www.navidrome.org/docs/developers/dev-environment/)
|
||||
- Create a new branch on your forked repo and make the changes in it. Naming conventions for branch are: `<Issue Title>/<Issue Number>`. Example:
|
||||
```
|
||||
git checkout -b adding-docs/834 master
|
||||
```
|
||||
- The commits should follow a [specific convention](#commit-conventions)
|
||||
- Ensure that a DCO sign-off for commits is provided via `--signoff` option of git commit
|
||||
- Provide a link to the issue that will be closed via your Pull request.
|
||||
|
||||
### Commit Conventions
|
||||
Each commit message must adhere to the following format:
|
||||
```
|
||||
<type>(scope): <description> - <issue number>
|
||||
|
||||
[optional body]
|
||||
```
|
||||
This improves the readability of the messages
|
||||
|
||||
#### Type
|
||||
It can be one of the following:
|
||||
1. **feat**: Addition of a new feature
|
||||
2. **fix**: Bug fix
|
||||
3. **docs**: Documentation Changes
|
||||
4. **style**: Changes to styling
|
||||
5. **refactor**: Refactoring of code
|
||||
6. **perf**: Code that affects performance
|
||||
7. **test**: Updating or improving the current tests
|
||||
8. **build**: Changes to Build process
|
||||
9. **revert**: Reverting to a previous commit
|
||||
10. **chore** : updating grunt tasks etc
|
||||
|
||||
If there is a breaking change in your Pull Request, please add `BREAKING CHANGE` in the optional body section
|
||||
|
||||
#### Scope
|
||||
The file or folder where the changes are made. If there are more than one, you can mention any
|
||||
|
||||
#### Description
|
||||
A short description of the issue
|
||||
|
||||
#### Issue number
|
||||
The issue fixed by this Pull Request.
|
||||
|
||||
The body is optional. It may contain short description of changes made.
|
||||
|
||||
Following all the guidelines an ideal commit will look like:
|
||||
```
|
||||
git commit --signoff -m "feat(themes): New-theme - #834"
|
||||
```
|
||||
|
||||
After committing, push your commits to your forked branch and create a Pull Request from there.
|
||||
The Pull Request Title can be the same as `<type>(scope): <description> - <issue number>`
|
||||
A demo layout of how the Pull request body can look:
|
||||
```
|
||||
Closes <Issue number along with link>
|
||||
|
||||
Description (What does the pull request do)
|
||||
|
||||
Changes (What changes were made )
|
||||
|
||||
Screenshots or Videos
|
||||
|
||||
Related Issues and Pull Requests(if any)
|
||||
|
||||
```
|
||||
218
Makefile
@@ -1,136 +1,118 @@
|
||||
GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ')
|
||||
NODE_VERSION=$(shell cat .nvmrc)
|
||||
|
||||
ifneq ("$(wildcard .git/HEAD)","")
|
||||
GIT_SHA=$(shell git rev-parse --short HEAD)
|
||||
GIT_TAG=$(shell git describe --tags `git rev-list --tags --max-count=1`)
|
||||
else
|
||||
GIT_SHA=source_archive
|
||||
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
|
||||
endif
|
||||
|
||||
CI_RELEASER_VERSION=1.20.3-1 ## https://github.com/navidrome/ci-goreleaser
|
||||
## Default target just build the Go project.
|
||||
default:
|
||||
go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=master"
|
||||
.PHONY: default
|
||||
|
||||
setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and prepare development environment
|
||||
@echo Downloading Node dependencies...
|
||||
@(cd ./ui && npm ci)
|
||||
.PHONY: setup
|
||||
|
||||
dev: check_env ##@Development Start Navidrome in development mode, with hot-reload for both frontend and backend
|
||||
dev: check_dev_env
|
||||
npx foreman -j Procfile.dev -p 4533 start
|
||||
.PHONY: dev
|
||||
|
||||
server: check_go_env ##@Development Start the backend in development mode
|
||||
@go run github.com/cespare/reflex@latest -d none -c reflex.conf
|
||||
server: check_go_dev_env
|
||||
@go run github.com/cespare/reflex -d none -c reflex.conf
|
||||
.PHONY: server
|
||||
|
||||
watch: ##@Development Start Go tests in watch mode (re-run when code changes)
|
||||
go run github.com/onsi/ginkgo/v2/ginkgo@latest watch -notify ./...
|
||||
wire: check_go_env
|
||||
go run github.com/google/wire/cmd/wire ./...
|
||||
.PHONY: wire
|
||||
|
||||
watch: check_go_env
|
||||
go run github.com/onsi/ginkgo/ginkgo watch -notify ./...
|
||||
.PHONY: watch
|
||||
|
||||
test: ##@Development Run Go tests
|
||||
go test -race -shuffle=on ./...
|
||||
test: check_go_env
|
||||
go test ./... -v
|
||||
.PHONY: test
|
||||
|
||||
testall: test ##@Development Run Go and JS tests, and validate OpenAPI spec
|
||||
testall: check_go_env test
|
||||
@(cd ./ui && npm test -- --watchAll=false)
|
||||
.PHONY: testall
|
||||
|
||||
lint: ##@Development Lint Go code
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run -v --timeout 5m
|
||||
lint:
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint run -v
|
||||
.PHONY: lint
|
||||
|
||||
lintapi: api/openapi.yaml ##@Development Lint OpenAPI spec
|
||||
npx @redocly/cli lint api/openapi.yaml
|
||||
.PHONY: lintapi
|
||||
update-snapshots: check_go_env
|
||||
UPDATE_SNAPSHOTS=true go run github.com/onsi/ginkgo/ginkgo ./server/subsonic/...
|
||||
.PHONY: update-snapshots
|
||||
|
||||
lintall: lint lintapi ##@Development Lint Go and JS code
|
||||
@(cd ./ui && npm run check-formatting) || (echo "\n\nPlease run 'npm run prettier' to fix formatting issues." && exit 1)
|
||||
@(cd ./ui && npm run lint)
|
||||
.PHONY: lintall
|
||||
|
||||
gen: check_go_env api ##@Development Update Generated Code (wire, mocks, openapi, etc)
|
||||
go run github.com/google/wire/cmd/wire@latest ./...
|
||||
.PHONY: wire
|
||||
|
||||
api: check_go_env api/openapi.yaml
|
||||
go generate ./server/api/...
|
||||
.PHONY: api
|
||||
|
||||
spec_parts=$(shell find api -name '*.yml')
|
||||
api/openapi.yaml: $(spec_parts)
|
||||
@echo "Bundling OpenAPI spec..."
|
||||
npx @redocly/cli bundle api/spec.yml -o api/openapi.yaml
|
||||
|
||||
snapshots: ##@Development Update (GoLang) Snapshot tests
|
||||
UPDATE_SNAPSHOTS=true go run github.com/onsi/ginkgo/v2/ginkgo@latest ./server/subsonic/...
|
||||
.PHONY: snapshots
|
||||
|
||||
migration-sql: ##@Development Create an empty SQL migration file
|
||||
@if [ -z "${name}" ]; then echo "Usage: make migration-sql name=name_of_migration_file"; exit 1; fi
|
||||
go run github.com/pressly/goose/v3/cmd/goose@latest -dir db/migration create ${name} sql
|
||||
migration:
|
||||
@if [ -z "${name}" ]; then echo "Usage: make migration name=name_of_migration_file"; exit 1; fi
|
||||
go run github.com/pressly/goose/cmd/goose -dir db/migration create ${name}
|
||||
.PHONY: migration
|
||||
|
||||
migration-go: ##@Development Create an empty Go migration file
|
||||
@if [ -z "${name}" ]; then echo "Usage: make migration-go name=name_of_migration_file"; exit 1; fi
|
||||
go run github.com/pressly/goose/v3/cmd/goose@latest -dir db/migration create ${name}
|
||||
.PHONY: migration
|
||||
setup: download-deps
|
||||
@echo Installing tools from tools.go
|
||||
@cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install %
|
||||
.PHONY: setup
|
||||
|
||||
setup-dev: setup
|
||||
download-deps:
|
||||
@echo Downloading Go dependencies...
|
||||
@go mod download -x
|
||||
@go mod tidy # To revert any changes made by the `go mod download` command
|
||||
@echo Downloading Node dependencies...
|
||||
@(cd ./ui && npm ci)
|
||||
.PHONY: download-deps
|
||||
|
||||
setup-dev: setup setup-git
|
||||
.PHONY: setup-dev
|
||||
|
||||
setup-git: ##@Development Setup Git hooks (pre-commit and pre-push)
|
||||
setup-git:
|
||||
@echo Setting up git hooks
|
||||
@mkdir -p .git/hooks
|
||||
@(cd .git/hooks && ln -sf ../../git/* .)
|
||||
.PHONY: setup-git
|
||||
|
||||
buildall: buildjs build ##@Build Build the project, both frontend and backend
|
||||
.PHONY: buildall
|
||||
check_dev_env: check_go_dev_env check_node_dev_env
|
||||
.PHONY: check_dev_env
|
||||
|
||||
build: warning-noui-build check_go_env ##@Build Build only backend
|
||||
go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo
|
||||
check_go_dev_env:
|
||||
@(hash go) || (echo "\nERROR: GO environment not setup properly!\n"; exit 1)
|
||||
@current_go_version=`go version | cut -d ' ' -f 3 | cut -c3-` && \
|
||||
echo "$(GO_VERSION) $$current_go_version" | \
|
||||
tr ' ' '\n' | sort -V | tail -1 | \
|
||||
grep -q "^$${current_go_version}$$" || \
|
||||
(echo "\nERROR: Please upgrade your GO version\nThis project requires at least the version $(GO_VERSION)"; exit 1)
|
||||
.PHONY: check_go_dev_env
|
||||
|
||||
check_node_dev_env:
|
||||
@(hash node) || (echo "\nERROR: Node environment not setup properly!\n"; exit 1)
|
||||
@current_node_version=`node --version` && \
|
||||
echo "$(NODE_VERSION) $$current_node_version" | \
|
||||
tr ' ' '\n' | sort -V | tail -1 | \
|
||||
grep -q "^$${current_node_version}$$" || \
|
||||
(echo "\nERROR: Please check your Node version. Should be at least $(NODE_VERSION)\n"; exit 1)
|
||||
.PHONY: check_node_dev_env
|
||||
|
||||
check_env: check_go_env check_node_env
|
||||
.PHONY: check_env
|
||||
|
||||
check_go_env:
|
||||
@(hash go) || (echo "\nERROR: GO environment not setup properly!\n"; exit 1)
|
||||
@go version | grep -q $(GO_VERSION) || (echo "\nERROR: Please upgrade your GO version\nThis project requires version $(GO_VERSION)"; exit 1)
|
||||
.PHONY: check_go_env
|
||||
|
||||
check_node_env:
|
||||
@(hash node) || (echo "\nERROR: Node environment not setup properly!\n"; exit 1)
|
||||
@node --version | grep -q $(NODE_VERSION) || (echo "\nERROR: Please check your Node version. Should be $(NODE_VERSION)\n"; exit 1)
|
||||
.PHONY: check_node_env
|
||||
|
||||
build: check_go_env
|
||||
go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT"
|
||||
.PHONY: build
|
||||
|
||||
buildjs: check_node_env ##@Build Build only frontend
|
||||
buildall: check_env
|
||||
@(cd ./ui && npm run build)
|
||||
.PHONY: buildjs
|
||||
go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo
|
||||
.PHONY: buildall
|
||||
|
||||
all: warning-noui-build ##@Cross_Compilation Build binaries for all supported platforms. It does not build the frontend
|
||||
docker run -t -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:$(CI_RELEASER_VERSION) \
|
||||
goreleaser release --rm-dist --skip-publish --snapshot
|
||||
.PHONY: all
|
||||
|
||||
single: warning-noui-build ##@Cross_Compilation Build binaries for a single supported platforms. It does not build the frontend
|
||||
@if [ -z "${GOOS}" -o -z "${GOARCH}" ]; then \
|
||||
echo "Usage: GOOS=<os> GOARCH=<arch> make single"; \
|
||||
echo "Options:"; \
|
||||
grep -- "- id: navidrome_" .goreleaser.yml | sed 's/- id: navidrome_//g'; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Building binaries for ${GOOS}/${GOARCH}"
|
||||
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}
|
||||
.PHONY: single
|
||||
|
||||
warning-noui-build:
|
||||
@echo "WARNING: This command does not build the frontend, it uses the latest built with 'make buildjs'"
|
||||
.PHONY: warning-noui-build
|
||||
|
||||
get-music: ##@Development Download some free music from Navidrome's demo instance
|
||||
mkdir -p music
|
||||
( cd music; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=ec2093ec4801402f1e17cc462195cdbb" > brock.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=b376eeb4652d2498aa2b25ba0696725e" > back_on_earth.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=e49c609b542fc51899ee8b53aa858cb4" > ugress.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=350bcab3a4c1d93869e39ce496464f03" > voodoocuts.zip; \
|
||||
for file in *.zip; do unzip -n $${file}; done )
|
||||
@echo "Done. Remember to set your MusicFolder to ./music"
|
||||
.PHONY: get-music
|
||||
|
||||
|
||||
##########################################
|
||||
#### Miscellaneous
|
||||
pre-push: lint test
|
||||
.PHONY: pre-push
|
||||
|
||||
release:
|
||||
@if [[ ! "${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "Usage: make release V=X.X.X"; exit 1; fi
|
||||
@@ -141,44 +123,6 @@ release:
|
||||
git push origin v${V} --no-verify
|
||||
.PHONY: release
|
||||
|
||||
download-deps:
|
||||
@echo Downloading Go dependencies...
|
||||
@go mod download
|
||||
@go mod tidy # To revert any changes made by the `go mod download` command
|
||||
.PHONY: download-deps
|
||||
|
||||
check_env: check_go_env check_node_env
|
||||
.PHONY: check_env
|
||||
|
||||
check_go_env:
|
||||
@(hash go) || (echo "\nERROR: GO environment not setup properly!\n"; exit 1)
|
||||
@current_go_version=`go version | cut -d ' ' -f 3 | cut -c3-` && \
|
||||
echo "$(GO_VERSION) $$current_go_version" | \
|
||||
tr ' ' '\n' | sort -V | tail -1 | \
|
||||
grep -q "^$${current_go_version}$$" || \
|
||||
(echo "\nERROR: Please upgrade your GO version\nThis project requires at least the version $(GO_VERSION)"; exit 1)
|
||||
.PHONY: check_go_env
|
||||
|
||||
check_node_env:
|
||||
@(hash node) || (echo "\nERROR: Node environment not setup properly!\n"; exit 1)
|
||||
@current_node_version=`node --version` && \
|
||||
echo "$(NODE_VERSION) $$current_node_version" | \
|
||||
tr ' ' '\n' | sort -V | tail -1 | \
|
||||
grep -q "^$${current_node_version}$$" || \
|
||||
(echo "\nERROR: Please check your Node version. Should be at least $(NODE_VERSION)\n"; exit 1)
|
||||
.PHONY: check_node_env
|
||||
|
||||
pre-push: lintall testall
|
||||
.PHONY: pre-push
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
HELP_FUN = \
|
||||
%help; while(<>){push@{$$help{$$2//'options'}},[$$1,$$3] \
|
||||
if/^([\w-_]+)\s*:.*\#\#(?:@(\w+))?\s(.*)$$/}; \
|
||||
print"$$_:\n", map" $$_->[0]".(" "x(20-length($$_->[0])))."$$_->[1]\n",\
|
||||
@{$$help{$$_}},"\n" for sort keys %help; \
|
||||
|
||||
help: ##@Miscellaneous Show this help
|
||||
@echo "Usage: make [target] ...\n"
|
||||
@perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)
|
||||
snapshot:
|
||||
docker run -t -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:1.16.2-1 goreleaser release --rm-dist --skip-publish --snapshot
|
||||
.PHONY: snapshot
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
JS: sh -c "cd ./ui && npm start"
|
||||
GO: go run github.com/cespare/reflex@latest -d none -c reflex.conf
|
||||
GO: go run github.com/cespare/reflex -c reflex.conf
|
||||
|
||||
39
README.md
@@ -1,25 +1,18 @@
|
||||
<a href="https://www.navidrome.org"><img src="resources/logo-192x192.png" alt="Navidrome logo" title="navidrome" align="right" height="60px" /></a>
|
||||
# Navidrome Music Server
|
||||
|
||||
# Navidrome Music Server [](https://twitter.com/intent/tweet?text=Tired%20of%20paying%20for%20music%20subscriptions%2C%20and%20not%20finding%20what%20you%20really%20like%3F%20Roll%20your%20own%20streaming%20service%21&url=https://navidrome.org&via=navidrome)
|
||||
|
||||
[](https://github.com/navidrome/navidrome/releases)
|
||||
[](https://nightly.link/navidrome/navidrome/workflows/pipeline/master)
|
||||
[](https://github.com/navidrome/navidrome/releases/latest)
|
||||
[](https://hub.docker.com/r/deluan/navidrome)
|
||||
[](https://discord.gg/xh7j7yF)
|
||||
[](https://www.reddit.com/r/navidrome/)
|
||||
[](CODE_OF_CONDUCT.md)
|
||||
|
||||
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your
|
||||
music collection from any browser or mobile device. It's like your personal Spotify!
|
||||
|
||||
|
||||
**Note**: The `master` branch may be in an unstable or even broken state during development.
|
||||
Please use [releases](https://github.com/navidrome/navidrome/releases) instead of
|
||||
the `master` branch in order to get a stable set of binaries.
|
||||
[](https://github.com/navidrome/navidrome/releases)
|
||||
[](https://github.com/navidrome/navidrome/actions)
|
||||
[](https://github.com/navidrome/navidrome/releases/latest)
|
||||
[](https://hub.docker.com/r/deluan/navidrome)
|
||||
[](https://discord.gg/xh7j7yF)
|
||||
[](https://www.reddit.com/r/navidrome/)
|
||||
[](code_of_conduct.md)
|
||||
|
||||
## [Check out our Live Demo!](https://www.navidrome.org/demo/)
|
||||
|
||||
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your
|
||||
music collection from any browser or mobile device. It's like your personal Spotify!
|
||||
|
||||
__Any feedback is welcome!__ If you need/want a new feature, find a bug or think of any way to improve Navidrome,
|
||||
please file a [GitHub issue](https://github.com/navidrome/navidrome/issues) or join the discussion in our
|
||||
[Subreddit](https://www.reddit.com/r/navidrome/). If you want to contribute to the project in any other way
|
||||
@@ -30,15 +23,7 @@ please file a [GitHub issue](https://github.com/navidrome/navidrome/issues) or j
|
||||
|
||||
## Installation
|
||||
|
||||
See instructions on the [project's website](https://www.navidrome.org/docs/installation/)
|
||||
|
||||
## Cloud Hosting
|
||||
|
||||
[PikaPods](https://www.pikapods.com) has partnered with us to offer you an
|
||||
[officially supported, cloud-hosted solution](https://www.navidrome.org/docs/installation/managed/#pikapods).
|
||||
A share of the revenue helps fund the development of Navidrome at no additional cost for you.
|
||||
|
||||
[](https://www.pikapods.com/pods?run=navidrome)
|
||||
See instructions in the [project's website](https://www.navidrome.org/docs/installation/)
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
pageOffset:
|
||||
$ref: './query/pageOffset.yml'
|
||||
pageLimit:
|
||||
$ref: './query/pageLimit.yml'
|
||||
filterEquals:
|
||||
$ref: './query/filterEquals.yml'
|
||||
filterLessThan:
|
||||
$ref: './query/filterLessThan.yml'
|
||||
filterLessOrEqual:
|
||||
$ref: './query/filterLessOrEqual.yml'
|
||||
filterGreaterThan:
|
||||
$ref: './query/filterGreaterThan.yml'
|
||||
filterGreaterOrEqual:
|
||||
$ref: './query/filterGreaterOrEqual.yml'
|
||||
filterContains:
|
||||
$ref: './query/filterContains.yml'
|
||||
filterStartsWith:
|
||||
$ref: './query/filterStartsWith.yml'
|
||||
filterEndsWith:
|
||||
$ref: './query/filterEndsWith.yml'
|
||||
sort:
|
||||
$ref: './query/sort.yml'
|
||||
include:
|
||||
$ref: './query/include.yml'
|
||||
includeForTracks:
|
||||
$ref: './query/includeForTracks.yml'
|
||||
includeForAlbums:
|
||||
$ref: './query/includeForAlbums.yml'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[contains]
|
||||
in: query
|
||||
description: 'Filter by any property containing text. Usage: filter[contains]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[endsWith]
|
||||
in: query
|
||||
description: 'Filter by any property that ends with text. Usage: filter[endsWith]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[equals]
|
||||
in: query
|
||||
description: 'Filter by any property with an exact match. Usage: filter[equals]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[greaterOrEqual]
|
||||
in: query
|
||||
description: 'Filter by any numeric property greater than or equal to a value. Usage: filter[greaterOrEqual]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[greaterThan]
|
||||
in: query
|
||||
description: 'Filter by any numeric property greater than a value. Usage: filter[greaterThan]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[lessOrEqual]
|
||||
in: query
|
||||
description: 'Filter by any numeric property less than or equal to a value. Usage: filter[lessOrEqual]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[lessThan]
|
||||
in: query
|
||||
description: 'Filter by any numeric property less than a value. Usage: filter[lessThan]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
@@ -1,9 +0,0 @@
|
||||
name: filter[startsWith]
|
||||
in: query
|
||||
description: 'Filter by any property that starts with text. Usage: filter[startsWith]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
@@ -1,6 +0,0 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1,13 +0,0 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: array
|
||||
x-go-type: includeSlice
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- track
|
||||
- artist
|
||||
@@ -1,12 +0,0 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: array
|
||||
x-go-type: includeSlice
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- artist
|
||||
@@ -1,13 +0,0 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: array
|
||||
x-go-type: includeSlice
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- album
|
||||
- artist
|
||||
@@ -1,9 +0,0 @@
|
||||
name: page[limit]
|
||||
in: query
|
||||
description: The number of items per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
default: 10
|
||||
@@ -1,9 +0,0 @@
|
||||
name: page[offset]
|
||||
in: query
|
||||
description: The offset for pagination
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
default: 0
|
||||
@@ -1,6 +0,0 @@
|
||||
name: sort
|
||||
in: query
|
||||
description: Sort the results by one or more properties, separated by commas. Prefix the property with '-' for descending order.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1,33 +0,0 @@
|
||||
get:
|
||||
summary: Retrieve an individual album
|
||||
operationId: getAlbum
|
||||
parameters:
|
||||
- $ref: '../parameters/query/includeForAlbum.yml'
|
||||
- name: albumId
|
||||
in: path
|
||||
description: The unique identifier of the album
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: An album object
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/Album.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'404':
|
||||
$ref: '../responses/NotFound.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
@@ -1,44 +0,0 @@
|
||||
get:
|
||||
summary: Retrieve a list of albums
|
||||
operationId: getAlbums
|
||||
parameters:
|
||||
- $ref: '../parameters/query/pageLimit.yml'
|
||||
- $ref: '../parameters/query/pageOffset.yml'
|
||||
- $ref: '../parameters/query/filterEquals.yml'
|
||||
- $ref: '../parameters/query/filterContains.yml'
|
||||
- $ref: '../parameters/query/filterLessThan.yml'
|
||||
- $ref: '../parameters/query/filterLessOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterGreaterThan.yml'
|
||||
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterStartsWith.yml'
|
||||
- $ref: '../parameters/query/filterEndsWith.yml'
|
||||
- $ref: '../parameters/query/sort.yml'
|
||||
- $ref: '../parameters/query/includeForAlbums.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: A list of albums
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data, links]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/Album.yml'
|
||||
links:
|
||||
$ref: '../schemas/PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: '../schemas/PaginationMeta.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'400':
|
||||
$ref: '../responses/BadRequest.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
@@ -1,28 +0,0 @@
|
||||
get:
|
||||
summary: Retrieve an individual artist
|
||||
operationId: getArtist
|
||||
parameters:
|
||||
- $ref: '../parameters/query/include.yml'
|
||||
- name: artistId
|
||||
in: path
|
||||
description: The unique identifier of the artist
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: An artist object
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/Artist.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'404':
|
||||
$ref: '../responses/NotFound.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
@@ -1,39 +0,0 @@
|
||||
get:
|
||||
summary: Retrieve a list of artists
|
||||
operationId: getArtists
|
||||
parameters:
|
||||
- $ref: '../parameters/query/pageLimit.yml'
|
||||
- $ref: '../parameters/query/pageOffset.yml'
|
||||
- $ref: '../parameters/query/filterEquals.yml'
|
||||
- $ref: '../parameters/query/filterContains.yml'
|
||||
- $ref: '../parameters/query/filterLessThan.yml'
|
||||
- $ref: '../parameters/query/filterLessOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterGreaterThan.yml'
|
||||
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterStartsWith.yml'
|
||||
- $ref: '../parameters/query/filterEndsWith.yml'
|
||||
- $ref: '../parameters/query/sort.yml'
|
||||
- $ref: '../parameters/query/include.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: A list of artists
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data, links]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/Artist.yml'
|
||||
links:
|
||||
$ref: '../schemas/PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: '../schemas/PaginationMeta.yml'
|
||||
'400':
|
||||
$ref: '../responses/BadRequest.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
@@ -1,18 +0,0 @@
|
||||
get:
|
||||
summary: Get server's global info
|
||||
operationId: getServerInfo
|
||||
responses:
|
||||
'200':
|
||||
description: The response’s data key maps to a resource object dictionary representing the server.
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/ServerInfo.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
@@ -1,33 +0,0 @@
|
||||
get:
|
||||
summary: Retrieve an individual track
|
||||
operationId: getTrack
|
||||
parameters:
|
||||
- $ref: '../parameters/query/includeForTracks.yml'
|
||||
- name: trackId
|
||||
in: path
|
||||
description: The unique identifier of the track
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: A track object
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/Track.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'404':
|
||||
$ref: '../responses/NotFound.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
@@ -1,44 +0,0 @@
|
||||
get:
|
||||
summary: Retrieve a list of tracks
|
||||
operationId: getTracks
|
||||
parameters:
|
||||
- $ref: '../parameters/query/pageLimit.yml'
|
||||
- $ref: '../parameters/query/pageOffset.yml'
|
||||
- $ref: '../parameters/query/filterEquals.yml'
|
||||
- $ref: '../parameters/query/filterContains.yml'
|
||||
- $ref: '../parameters/query/filterLessThan.yml'
|
||||
- $ref: '../parameters/query/filterLessOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterGreaterThan.yml'
|
||||
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterStartsWith.yml'
|
||||
- $ref: '../parameters/query/filterEndsWith.yml'
|
||||
- $ref: '../parameters/query/sort.yml'
|
||||
- $ref: '../parameters/query/includeForTracks.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: A list of tracks
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data, links]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/Track.yml'
|
||||
links:
|
||||
$ref: '../schemas/PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: '../schemas/PaginationMeta.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'400':
|
||||
$ref: '../responses/BadRequest.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
@@ -1,5 +0,0 @@
|
||||
description: Bad Request
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
@@ -1,5 +0,0 @@
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
@@ -1,5 +0,0 @@
|
||||
description: Not Authorized
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
@@ -1,5 +0,0 @@
|
||||
description: Not Found
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
@@ -1,8 +0,0 @@
|
||||
NotFound:
|
||||
$ref: './NotFound.yml'
|
||||
NotAuthorized:
|
||||
$ref: './NotAuthorized.yml'
|
||||
BadRequest:
|
||||
$ref: './BadRequest.yml'
|
||||
InternalServerError:
|
||||
$ref: './InternalServerError.yml'
|
||||
@@ -1,20 +0,0 @@
|
||||
allOf:
|
||||
- $ref: './ResourceObject.yml'
|
||||
- type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: './AlbumAttributes.yml'
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
artists:
|
||||
type: array
|
||||
items:
|
||||
$ref: './AlbumArtistRelationship.yml'
|
||||
tracks:
|
||||
type: array
|
||||
items:
|
||||
$ref: './AlbumTrackRelationship.yml'
|
||||
required:
|
||||
- artists
|
||||
- tracks
|
||||
@@ -1,9 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
@@ -1,23 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: The title of the album
|
||||
artist:
|
||||
type: string
|
||||
description: The artist of the album
|
||||
releaseDate:
|
||||
type: string
|
||||
description: The release date of the album
|
||||
tracktotal:
|
||||
type: integer
|
||||
description: The number of tracks on the album
|
||||
disctotal:
|
||||
type: integer
|
||||
description: The number of discs in the album
|
||||
genre:
|
||||
type: string
|
||||
description: The genre of the album
|
||||
required:
|
||||
- title
|
||||
- artist
|
||||
@@ -1,6 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- data
|
||||
@@ -1,27 +0,0 @@
|
||||
allOf:
|
||||
- $ref: './ResourceObject.yml'
|
||||
- type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: './ArtistAttributes.yml'
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
tracks:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './ArtistTrackRelationship.yml'
|
||||
required:
|
||||
- data
|
||||
albums:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './ArtistAlbumRelationship.yml'
|
||||
required:
|
||||
- data
|
||||
@@ -1,9 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
@@ -1,10 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the artist
|
||||
bio:
|
||||
type: string
|
||||
description: A short biography of the artist
|
||||
required:
|
||||
- name
|
||||
@@ -1,6 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
$ref: './ArtistRole.yml'
|
||||
required:
|
||||
- role
|
||||
@@ -1,5 +0,0 @@
|
||||
type: string
|
||||
enum:
|
||||
- artist
|
||||
- albumArtist
|
||||
description: The role of an artist in a track or album
|
||||
@@ -1,14 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
$ref: './ArtistRole.yml'
|
||||
required:
|
||||
- role
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
@@ -1,7 +0,0 @@
|
||||
type: object
|
||||
required: [errors]
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: './ErrorObject.yml'
|
||||
@@ -1,10 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
detail:
|
||||
type: string
|
||||
@@ -1,10 +0,0 @@
|
||||
oneOf:
|
||||
- $ref: './Track.yml'
|
||||
- $ref: './Album.yml'
|
||||
- $ref: './Artist.yml'
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
track: './Track.yml'
|
||||
album: './Album.yml'
|
||||
artist: './Artist.yml'
|
||||
@@ -1,14 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
first:
|
||||
type: string
|
||||
format: uri
|
||||
prev:
|
||||
type: string
|
||||
format: uri
|
||||
next:
|
||||
type: string
|
||||
format: uri
|
||||
last:
|
||||
type: string
|
||||
format: uri
|
||||
@@ -1,14 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
currentPage:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The current page in the collection
|
||||
totalPages:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The total number of pages in the collection
|
||||
totalItems:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The total number of items in the collection
|
||||
@@ -1,18 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
oneOf:
|
||||
- $ref: './Track.yml'
|
||||
- $ref: './Album.yml'
|
||||
- $ref: './Artist.yml'
|
||||
- type: array
|
||||
items:
|
||||
$ref: './ResourceObject.yml'
|
||||
included:
|
||||
type: array
|
||||
items:
|
||||
$ref: './IncludedResource.yml'
|
||||
links:
|
||||
$ref: './PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: './PaginationMeta.yml'
|
||||
@@ -1,8 +0,0 @@
|
||||
type: object
|
||||
required: [id, type]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The unique identifier for the resource
|
||||
type:
|
||||
$ref: './ResourceType.yml'
|
||||
@@ -1,6 +0,0 @@
|
||||
type: string
|
||||
description: The type of the resource
|
||||
enum:
|
||||
- album
|
||||
- artist
|
||||
- track
|
||||
@@ -1,24 +0,0 @@
|
||||
type: object
|
||||
required: [server, serverVersion, authRequired, features]
|
||||
properties:
|
||||
server:
|
||||
type: string
|
||||
description: The name of the server software.
|
||||
example: "navidrome"
|
||||
serverVersion:
|
||||
type: string
|
||||
description: The version number of the server.
|
||||
example: "0.60.0"
|
||||
authRequired:
|
||||
type: boolean
|
||||
description: Whether the user has access to the server.
|
||||
example: true
|
||||
features:
|
||||
type: array
|
||||
description: A list of optional features the server supports.
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- albums
|
||||
- artists
|
||||
- images
|
||||
@@ -1,8 +0,0 @@
|
||||
allOf:
|
||||
- $ref: './ResourceObject.yml'
|
||||
- type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: './TrackAttributes.yml'
|
||||
relationships:
|
||||
$ref: './TrackRelationships.yml'
|
||||
@@ -1,9 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
@@ -1,55 +0,0 @@
|
||||
type: object
|
||||
required: [title, artist, album, albumartist, track, mimetype, duration, channels, bitrate, size]
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: The title of the track
|
||||
artist: # TODO: Remove
|
||||
type: string
|
||||
description: The name of the artist who performed the track
|
||||
albumartist: # TODO: Remove
|
||||
type: string
|
||||
description: The primary artist of the album the track belongs to.
|
||||
album: # TODO Remove
|
||||
type: string
|
||||
description: The name of the album the track belongs to
|
||||
genre: # TODO Remove
|
||||
type: string
|
||||
description: The genre of the track.
|
||||
track:
|
||||
type: integer
|
||||
description: The track number within the album.
|
||||
disc:
|
||||
type: integer
|
||||
description: The disc number within a multi-disc album.
|
||||
year:
|
||||
type: integer
|
||||
description: The release year of the track or album.
|
||||
bpm:
|
||||
type: integer
|
||||
description: The beats per minute (BPM) of the track.
|
||||
recording-mbid:
|
||||
type: string
|
||||
description: The MusicBrainz identifier for the recording of the track.
|
||||
track-mbid:
|
||||
type: string
|
||||
description: The MusicBrainz identifier for the track.
|
||||
comments:
|
||||
type: string
|
||||
description: Any additional comments or notes about the track.
|
||||
mimetype:
|
||||
type: string
|
||||
description: The MIME type of the audio file.
|
||||
duration:
|
||||
type: number
|
||||
format: float
|
||||
description: The duration of the track in seconds
|
||||
channels:
|
||||
type: integer
|
||||
description: The number of audio channels in the track.
|
||||
bitrate:
|
||||
type: integer
|
||||
description: The bitrate of the audio file in kilobits per second (kbps).
|
||||
size:
|
||||
type: integer
|
||||
description: The size of the audio file in bytes.
|
||||
@@ -1,12 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
artists:
|
||||
type: array
|
||||
items:
|
||||
$ref: './TrackArtistRelationship.yml'
|
||||
albums:
|
||||
type: array
|
||||
items:
|
||||
$ref: './AlbumTrackRelationship.yml'
|
||||
required:
|
||||
- artists
|
||||
@@ -1,46 +0,0 @@
|
||||
ServerInfo:
|
||||
$ref: './ServerInfo.yml'
|
||||
ResourceObject:
|
||||
$ref: './ResourceObject.yml'
|
||||
ResourceType:
|
||||
$ref: './ResourceType.yml'
|
||||
ResourceList:
|
||||
$ref: './ResourceList.yml'
|
||||
IncludedResource:
|
||||
$ref: './IncludedResource.yml'
|
||||
Track:
|
||||
$ref: './Track.yml'
|
||||
TrackAttributes:
|
||||
$ref: './TrackAttributes.yml'
|
||||
TrackRelationships:
|
||||
$ref: './TrackRelationships.yml'
|
||||
TrackArtistRelationship:
|
||||
$ref: './TrackArtistRelationship.yml'
|
||||
ArtistRole:
|
||||
$ref: './ArtistRole.yml'
|
||||
Artist:
|
||||
$ref: './Artist.yml'
|
||||
ArtistAttributes:
|
||||
$ref: './ArtistAttributes.yml'
|
||||
ArtistAlbumRelationship:
|
||||
$ref: './ArtistAlbumRelationship.yml'
|
||||
ArtistTrackRelationship:
|
||||
$ref: './ArtistTrackRelationship.yml'
|
||||
ArtistMetaObject:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
Album:
|
||||
$ref: './Album.yml'
|
||||
AlbumAttributes:
|
||||
$ref: './AlbumAttributes.yml'
|
||||
AlbumArtistRelationship:
|
||||
$ref: './AlbumArtistRelationship.yml'
|
||||
AlbumTrackRelationship:
|
||||
$ref: './AlbumTrackRelationship.yml'
|
||||
PaginationLinks:
|
||||
$ref: './PaginationLinks.yml'
|
||||
PaginationMeta:
|
||||
$ref: './PaginationMeta.yml'
|
||||
ErrorList:
|
||||
$ref: './ErrorList.yml'
|
||||
ErrorObject:
|
||||
$ref: './ErrorObject.yml'
|
||||
52
api/spec.yml
@@ -1,52 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
version: 0.2.0
|
||||
title: Navidrome API
|
||||
description: >
|
||||
This spec describes the Navidrome API, which allows users to browse and manage their music library via a JSON:API
|
||||
based interface. The API provides endpoints for albums, tracks, artists, playlists and images, along with their
|
||||
relationships. Clients can retrieve information about the items in the library, filter and sort results, and
|
||||
perform actions such as creating and deleting playlists. With this API, developers can build music apps and
|
||||
services that integrate with Navidrome music server, providing a seamless experience for users to access and
|
||||
manage their music collection.
|
||||
contact:
|
||||
name: Navidrome
|
||||
url: https://navidrome.org
|
||||
license:
|
||||
name: GNU General Public License v3.0
|
||||
url: https://github.com/navidrome/navidrome/blob/master/LICENSE
|
||||
|
||||
servers:
|
||||
- url: /api/v2
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
paths:
|
||||
/server:
|
||||
$ref: './resources/server.yml'
|
||||
/tracks:
|
||||
$ref: './resources/tracks.yml'
|
||||
/tracks/{trackId}:
|
||||
$ref: './resources/track.yml'
|
||||
/artists:
|
||||
$ref: './resources/artists.yml'
|
||||
/artists/{artistId}:
|
||||
$ref: './resources/artist.yml'
|
||||
/albums:
|
||||
$ref: './resources/albums.yml'
|
||||
/albums/{albumId}:
|
||||
$ref: './resources/album.yml'
|
||||
|
||||
components:
|
||||
parameters:
|
||||
$ref: './parameters/_index.yml'
|
||||
schemas:
|
||||
$ref: './schemas/_index.yml'
|
||||
responses:
|
||||
$ref: './responses/_index.yml'
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
71
cmd/pls.go
@@ -1,71 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
playlistID string
|
||||
outputFile string
|
||||
)
|
||||
|
||||
func init() {
|
||||
plsCmd.Flags().StringVarP(&playlistID, "playlist", "p", "", "playlist name or ID")
|
||||
plsCmd.Flags().StringVarP(&outputFile, "output", "o", "", "output file (default stdout)")
|
||||
_ = plsCmd.MarkFlagRequired("playlist")
|
||||
rootCmd.AddCommand(plsCmd)
|
||||
}
|
||||
|
||||
var plsCmd = &cobra.Command{
|
||||
Use: "pls",
|
||||
Short: "Export playlists",
|
||||
Long: "Export Navidrome playlists to M3U files",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runExporter()
|
||||
},
|
||||
}
|
||||
|
||||
func runExporter() {
|
||||
sqlDB := db.Db()
|
||||
ds := persistence.New(sqlDB)
|
||||
ctx := auth.WithAdminUser(context.Background(), ds)
|
||||
playlist, err := ds.Playlist(ctx).GetWithTracks(playlistID, true)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
log.Fatal("Error retrieving playlist", "name", playlistID, err)
|
||||
}
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
playlists, err := ds.Playlist(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"playlist.name": playlistID}})
|
||||
if err != nil {
|
||||
log.Fatal("Error retrieving playlist", "name", playlistID, err)
|
||||
}
|
||||
if len(playlists) > 0 {
|
||||
playlist, err = ds.Playlist(ctx).GetWithTracks(playlists[0].ID, true)
|
||||
if err != nil {
|
||||
log.Fatal("Error retrieving playlist", "name", playlistID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if playlist == nil {
|
||||
log.Fatal("Playlist not found", "name", playlistID)
|
||||
}
|
||||
pls := playlist.ToM3U8()
|
||||
if outputFile == "-" || outputFile == "" {
|
||||
println(pls)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputFile, []byte(pls), 0600)
|
||||
if err != nil {
|
||||
log.Fatal("Error writing to the output file", "file", outputFile, err)
|
||||
}
|
||||
}
|
||||
150
cmd/root.go
@@ -2,30 +2,19 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/resources"
|
||||
"github.com/navidrome/navidrome/scheduler"
|
||||
"github.com/navidrome/navidrome/server/backgrounds"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var interrupted = errors.New("service was interrupted")
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
noBanner bool
|
||||
@@ -41,7 +30,7 @@ Complete documentation is available at https://www.navidrome.org/docs`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runNavidrome()
|
||||
},
|
||||
Version: consts.Version,
|
||||
Version: consts.Version(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,96 +44,62 @@ func Execute() {
|
||||
|
||||
func preRun() {
|
||||
if !noBanner {
|
||||
println(resources.Banner())
|
||||
println(consts.Banner())
|
||||
}
|
||||
conf.Load()
|
||||
}
|
||||
|
||||
func runNavidrome() {
|
||||
db.Init()
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
log.Error("Error closing DB", err)
|
||||
}
|
||||
log.Info("Navidrome stopped, bye.")
|
||||
}()
|
||||
db.EnsureLatestVersion()
|
||||
|
||||
g, ctx := errgroup.WithContext(context.Background())
|
||||
g.Go(startServer(ctx))
|
||||
g.Go(startSignaler(ctx))
|
||||
g.Go(startScheduler(ctx))
|
||||
g.Go(schedulePeriodicScan(ctx))
|
||||
var g run.Group
|
||||
g.Add(startServer())
|
||||
g.Add(startScanner())
|
||||
|
||||
if err := g.Wait(); err != nil && !errors.Is(err, interrupted) {
|
||||
if err := g.Run(); err != nil {
|
||||
log.Error("Fatal error in Navidrome. Aborting", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startServer(ctx context.Context) func() error {
|
||||
func startServer() (func() error, func(err error)) {
|
||||
return func() error {
|
||||
a := CreateServer(conf.Server.MusicFolder)
|
||||
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
|
||||
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
|
||||
a.MountRouter("Public Endpoints", consts.URLPathPublic, CreatePublicRouter())
|
||||
if conf.Server.LastFM.Enabled {
|
||||
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
|
||||
a := CreateServer(conf.Server.MusicFolder)
|
||||
a.MountRouter(consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
|
||||
a.MountRouter(consts.URLPathUI, CreateAppRouter())
|
||||
return a.Run(fmt.Sprintf("%s:%d", conf.Server.Address, conf.Server.Port))
|
||||
}, func(err error) {
|
||||
if err != nil {
|
||||
log.Error("Shutting down Server due to error", err)
|
||||
} else {
|
||||
log.Info("Shutting down Server")
|
||||
}
|
||||
}
|
||||
if conf.Server.ListenBrainz.Enabled {
|
||||
a.MountRouter("ListenBrainz Auth", consts.URLPathNativeAPI+"/listenbrainz", CreateListenBrainzRouter())
|
||||
}
|
||||
if conf.Server.Prometheus.Enabled {
|
||||
// blocking call because takes <1ms but useful if fails
|
||||
core.WriteInitialMetrics()
|
||||
a.MountRouter("Prometheus metrics", conf.Server.Prometheus.MetricsPath, promhttp.Handler())
|
||||
}
|
||||
if conf.Server.DevEnableProfiler {
|
||||
a.MountRouter("Profiling", "/debug", middleware.Profiler())
|
||||
}
|
||||
if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
|
||||
a.MountRouter("Background images", consts.DefaultUILoginBackgroundURL, backgrounds.NewHandler())
|
||||
}
|
||||
a.MountRouter("New Native API", consts.URLPathAPI, CreateNewNativeAPIRouter())
|
||||
return a.Run(ctx, conf.Server.Address, conf.Server.Port, conf.Server.TLSCert, conf.Server.TLSKey)
|
||||
}
|
||||
}
|
||||
|
||||
func schedulePeriodicScan(ctx context.Context) func() error {
|
||||
func startScanner() (func() error, func(err error)) {
|
||||
interval := conf.Server.ScanInterval
|
||||
log.Info("Starting scanner", "interval", interval.String())
|
||||
scanner := GetScanner()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return func() error {
|
||||
schedule := conf.Server.ScanSchedule
|
||||
if schedule == "" {
|
||||
log.Warn("Periodic scan is DISABLED")
|
||||
if interval != 0 {
|
||||
time.Sleep(2 * time.Second) // Wait 2 seconds before the first scan
|
||||
scanner.Run(ctx, interval)
|
||||
} else {
|
||||
log.Warn("Periodic scan is DISABLED", "interval", interval)
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(err error) {
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Error("Shutting down Scanner due to error", err)
|
||||
} else {
|
||||
log.Info("Shutting down Scanner")
|
||||
}
|
||||
}
|
||||
|
||||
scanner := GetScanner()
|
||||
schedulerInstance := scheduler.GetInstance()
|
||||
|
||||
log.Info("Scheduling periodic scan", "schedule", schedule)
|
||||
err := schedulerInstance.Add(schedule, func() {
|
||||
_ = scanner.RescanAll(ctx, false)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Error scheduling periodic scan", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second) // Wait 2 seconds before the initial scan
|
||||
log.Debug("Executing initial scan")
|
||||
if err := scanner.RescanAll(ctx, false); err != nil {
|
||||
log.Error("Error executing initial scan", err)
|
||||
}
|
||||
log.Debug("Finished initial scan")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func startScheduler(ctx context.Context) func() error {
|
||||
log.Info(ctx, "Starting scheduler")
|
||||
schedulerInstance := scheduler.GetInstance()
|
||||
|
||||
return func() error {
|
||||
schedulerInstance.Run(ctx)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement some struct tags to map flags to viper
|
||||
@@ -156,45 +111,30 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "configfile", "c", "", `config file (default "./navidrome.toml")`)
|
||||
rootCmd.PersistentFlags().BoolVarP(&noBanner, "nobanner", "n", false, `don't show banner`)
|
||||
rootCmd.PersistentFlags().String("musicfolder", viper.GetString("musicfolder"), "folder where your music is stored")
|
||||
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB), needs write access")
|
||||
rootCmd.PersistentFlags().String("cachefolder", viper.GetString("cachefolder"), "folder to store cache data (transcoding, images...), needs write access")
|
||||
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB, cache...), needs write access")
|
||||
rootCmd.PersistentFlags().StringP("loglevel", "l", viper.GetString("loglevel"), "log level, possible values: error, info, debug, trace")
|
||||
|
||||
_ = viper.BindPFlag("musicfolder", rootCmd.PersistentFlags().Lookup("musicfolder"))
|
||||
_ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder"))
|
||||
_ = viper.BindPFlag("cachefolder", rootCmd.PersistentFlags().Lookup("cachefolder"))
|
||||
_ = viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
|
||||
|
||||
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind to")
|
||||
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("tlskey", viper.GetString("tlskey"), "optional path to a TLS key file (enables HTTPS listening)")
|
||||
|
||||
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind")
|
||||
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will use")
|
||||
rootCmd.Flags().Duration("sessiontimeout", viper.GetDuration("sessiontimeout"), "how long Navidrome will wait before closing web ui idle sessions")
|
||||
rootCmd.Flags().Duration("scaninterval", viper.GetDuration("scaninterval"), "how frequently to scan for changes in your music library")
|
||||
rootCmd.Flags().String("baseurl", viper.GetString("baseurl"), "base URL (only the path part) to configure Navidrome behind a proxy (ex: /music)")
|
||||
rootCmd.Flags().String("uiloginbackgroundurl", viper.GetString("uiloginbackgroundurl"), "URL to a backaground image used in the Login page")
|
||||
rootCmd.Flags().Bool("enabletranscodingconfig", viper.GetBool("enabletranscodingconfig"), "enables transcoding configuration in the UI")
|
||||
rootCmd.Flags().String("transcodingcachesize", viper.GetString("transcodingcachesize"), "size of transcoding cache")
|
||||
rootCmd.Flags().String("imagecachesize", viper.GetString("imagecachesize"), "size of image (art work) cache. set to 0 to disable cache")
|
||||
rootCmd.Flags().Bool("autoimportplaylists", viper.GetBool("autoimportplaylists"), "enable/disable .m3u playlist auto-import`")
|
||||
|
||||
rootCmd.Flags().Bool("prometheus.enabled", viper.GetBool("prometheus.enabled"), "enable/disable prometheus metrics endpoint`")
|
||||
rootCmd.Flags().String("prometheus.metricspath", viper.GetString("prometheus.metricspath"), "http endpoint for prometheus metrics")
|
||||
|
||||
_ = viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
|
||||
_ = viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
|
||||
_ = viper.BindPFlag("tlscert", rootCmd.Flags().Lookup("tlscert"))
|
||||
_ = viper.BindPFlag("tlskey", rootCmd.Flags().Lookup("tlskey"))
|
||||
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
|
||||
|
||||
_ = viper.BindPFlag("sessiontimeout", rootCmd.Flags().Lookup("sessiontimeout"))
|
||||
_ = viper.BindPFlag("scaninterval", rootCmd.Flags().Lookup("scaninterval"))
|
||||
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
|
||||
_ = viper.BindPFlag("uiloginbackgroundurl", rootCmd.Flags().Lookup("uiloginbackgroundurl"))
|
||||
|
||||
_ = viper.BindPFlag("prometheus.enabled", rootCmd.Flags().Lookup("prometheus.enabled"))
|
||||
_ = viper.BindPFlag("prometheus.metricspath", rootCmd.Flags().Lookup("prometheus.metricspath"))
|
||||
|
||||
_ = viper.BindPFlag("enabletranscodingconfig", rootCmd.Flags().Lookup("enabletranscodingconfig"))
|
||||
_ = viper.BindPFlag("transcodingcachesize", rootCmd.Flags().Lookup("transcodingcachesize"))
|
||||
_ = viper.BindPFlag("imagecachesize", rootCmd.Flags().Lookup("imagecachesize"))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var fullRescan bool
|
||||
@@ -24,6 +24,8 @@ var scanCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func runScanner() {
|
||||
conf.Server.DevPreCacheAlbumArtwork = false
|
||||
|
||||
scanner := GetScanner()
|
||||
_ = scanner.RescanAll(context.Background(), fullRescan)
|
||||
if fullRescan {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//go:build windows || plan9
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
func startSignaler(ctx context.Context) func() error {
|
||||
log.Info(ctx, "Starting signaler")
|
||||
|
||||
return func() error {
|
||||
var sigChan = make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
select {
|
||||
case sig := <-sigChan:
|
||||
log.Info(ctx, "Received termination signal", "signal", sig)
|
||||
return interrupted
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//go:build !windows && !plan9
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
const triggerScanSignal = syscall.SIGUSR1
|
||||
|
||||
func startSignaler(ctx context.Context) func() error {
|
||||
log.Info(ctx, "Starting signaler")
|
||||
scanner := GetScanner()
|
||||
|
||||
return func() error {
|
||||
var sigChan = make(chan os.Signal, 1)
|
||||
signal.Notify(
|
||||
sigChan,
|
||||
os.Interrupt,
|
||||
triggerScanSignal,
|
||||
syscall.SIGHUP,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGABRT,
|
||||
)
|
||||
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigChan:
|
||||
if sig != triggerScanSignal {
|
||||
log.Info(ctx, "Received termination signal", "signal", sig)
|
||||
return interrupted
|
||||
}
|
||||
log.Info(ctx, "Received signal, triggering a new scan", "signal", sig)
|
||||
start := time.Now()
|
||||
err := scanner.RescanAll(ctx, false)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error scanning", err)
|
||||
}
|
||||
log.Info(ctx, "Triggered scan complete", "elapsed", time.Since(start).Round(100*time.Millisecond))
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
cmd/wire_gen.go
@@ -1,28 +1,19 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate go run github.com/google/wire/cmd/wire
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
//+build !wireinject
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/agents"
|
||||
"github.com/navidrome/navidrome/core/agents/lastfm"
|
||||
"github.com/navidrome/navidrome/core/agents/listenbrainz"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/core/transcoder"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/api"
|
||||
"github.com/navidrome/navidrome/server/app"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/nativeapi"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic"
|
||||
"sync"
|
||||
)
|
||||
@@ -30,97 +21,51 @@ import (
|
||||
// Injectors from wire_injectors.go:
|
||||
|
||||
func CreateServer(musicFolder string) *server.Server {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
broker := events.GetBroker()
|
||||
serverServer := server.New(dataStore, broker)
|
||||
dataStore := persistence.New()
|
||||
serverServer := server.New(dataStore)
|
||||
return serverServer
|
||||
}
|
||||
|
||||
func CreateNativeAPIRouter() *nativeapi.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
share := core.NewShare(dataStore)
|
||||
router := nativeapi.New(dataStore, share)
|
||||
return router
|
||||
}
|
||||
|
||||
func CreateNewNativeAPIRouter() *api.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
router := api.New(dataStore)
|
||||
func CreateAppRouter() *app.Router {
|
||||
dataStore := persistence.New()
|
||||
broker := GetBroker()
|
||||
router := app.New(dataStore, broker)
|
||||
return router
|
||||
}
|
||||
|
||||
func CreateSubsonicAPIRouter() *subsonic.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
agentsAgents := agents.New(dataStore)
|
||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
||||
dataStore := persistence.New()
|
||||
artworkCache := core.GetImageCache()
|
||||
artwork := core.NewArtwork(dataStore, artworkCache)
|
||||
transcoderTranscoder := transcoder.New()
|
||||
transcodingCache := core.GetTranscodingCache()
|
||||
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||
share := core.NewShare(dataStore)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||
mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)
|
||||
archiver := core.NewArchiver(dataStore)
|
||||
players := core.NewPlayers(dataStore)
|
||||
externalMetadata := core.NewExternalMetadata(dataStore)
|
||||
scanner := GetScanner()
|
||||
broker := events.GetBroker()
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
|
||||
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker, share)
|
||||
return router
|
||||
}
|
||||
|
||||
func CreatePublicRouter() *public.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
agentsAgents := agents.New(dataStore)
|
||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
||||
transcodingCache := core.GetTranscodingCache()
|
||||
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||
share := core.NewShare(dataStore)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||
router := public.New(dataStore, artworkArtwork, mediaStreamer, share, archiver)
|
||||
return router
|
||||
}
|
||||
|
||||
func CreateLastFMRouter() *lastfm.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
router := lastfm.NewRouter(dataStore)
|
||||
return router
|
||||
}
|
||||
|
||||
func CreateListenBrainzRouter() *listenbrainz.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
router := listenbrainz.NewRouter(dataStore)
|
||||
router := subsonic.New(dataStore, artwork, mediaStreamer, archiver, players, externalMetadata, scanner)
|
||||
return router
|
||||
}
|
||||
|
||||
func createScanner() scanner.Scanner {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
agentsAgents := agents.New(dataStore)
|
||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
broker := events.GetBroker()
|
||||
scannerScanner := scanner.New(dataStore, playlists, cacheWarmer, broker)
|
||||
dataStore := persistence.New()
|
||||
artworkCache := core.GetImageCache()
|
||||
artwork := core.NewArtwork(dataStore, artworkCache)
|
||||
cacheWarmer := core.NewCacheWarmer(artwork, artworkCache)
|
||||
broker := GetBroker()
|
||||
scannerScanner := scanner.New(dataStore, cacheWarmer, broker)
|
||||
return scannerScanner
|
||||
}
|
||||
|
||||
func createBroker() events.Broker {
|
||||
broker := events.NewBroker()
|
||||
return broker
|
||||
}
|
||||
|
||||
// wire_injectors.go:
|
||||
|
||||
var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, api.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
|
||||
var allProviders = wire.NewSet(core.Set, subsonic.New, app.New, persistence.New)
|
||||
|
||||
// Scanner must be a Singleton
|
||||
var (
|
||||
@@ -134,3 +79,16 @@ func GetScanner() scanner.Scanner {
|
||||
})
|
||||
return scannerInstance
|
||||
}
|
||||
|
||||
// Broker must be a Singleton
|
||||
var (
|
||||
onceBroker sync.Once
|
||||
brokerInstance events.Broker
|
||||
)
|
||||
|
||||
func GetBroker() events.Broker {
|
||||
onceBroker.Do(func() {
|
||||
brokerInstance = createBroker()
|
||||
})
|
||||
return brokerInstance
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build wireinject
|
||||
//+build wireinject
|
||||
|
||||
package cmd
|
||||
|
||||
@@ -7,32 +7,19 @@ import (
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/agents/lastfm"
|
||||
"github.com/navidrome/navidrome/core/agents/listenbrainz"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/api"
|
||||
"github.com/navidrome/navidrome/server/app"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/nativeapi"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic"
|
||||
)
|
||||
|
||||
var allProviders = wire.NewSet(
|
||||
core.Set,
|
||||
artwork.Set,
|
||||
subsonic.New,
|
||||
nativeapi.New,
|
||||
api.New,
|
||||
public.New,
|
||||
app.New,
|
||||
persistence.New,
|
||||
lastfm.NewRouter,
|
||||
listenbrainz.NewRouter,
|
||||
events.GetBroker,
|
||||
db.Db,
|
||||
)
|
||||
|
||||
func CreateServer(musicFolder string) *server.Server {
|
||||
@@ -42,15 +29,10 @@ func CreateServer(musicFolder string) *server.Server {
|
||||
))
|
||||
}
|
||||
|
||||
func CreateNativeAPIRouter() *nativeapi.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateNewNativeAPIRouter() *api.Router {
|
||||
func CreateAppRouter() *app.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
GetBroker,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -61,24 +43,6 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
|
||||
))
|
||||
}
|
||||
|
||||
func CreatePublicRouter() *public.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateLastFMRouter() *lastfm.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateListenBrainzRouter() *listenbrainz.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
// Scanner must be a Singleton
|
||||
var (
|
||||
onceScanner sync.Once
|
||||
@@ -95,6 +59,26 @@ func GetScanner() scanner.Scanner {
|
||||
func createScanner() scanner.Scanner {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
GetBroker,
|
||||
scanner.New,
|
||||
))
|
||||
}
|
||||
|
||||
// Broker must be a Singleton
|
||||
var (
|
||||
onceBroker sync.Once
|
||||
brokerInstance events.Broker
|
||||
)
|
||||
|
||||
func GetBroker() events.Broker {
|
||||
onceBroker.Do(func() {
|
||||
brokerInstance = createBroker()
|
||||
})
|
||||
return brokerInstance
|
||||
}
|
||||
|
||||
func createBroker() events.Broker {
|
||||
panic(wire.Build(
|
||||
events.NewBroker,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package configtest
|
||||
|
||||
import "github.com/navidrome/navidrome/conf"
|
||||
|
||||
func SetupConfig() func() {
|
||||
oldValues := *conf.Server
|
||||
return func() {
|
||||
conf.Server = &oldValues
|
||||
}
|
||||
}
|
||||
@@ -2,113 +2,68 @@ package conf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type configOptions struct {
|
||||
ConfigFile string
|
||||
Address string
|
||||
Port int
|
||||
MusicFolder string
|
||||
DataFolder string
|
||||
CacheFolder string
|
||||
DbPath string
|
||||
LogLevel string
|
||||
ScanInterval time.Duration
|
||||
ScanSchedule string
|
||||
SessionTimeout time.Duration
|
||||
BaseURL string
|
||||
BasePath string
|
||||
BaseHost string
|
||||
BaseScheme string
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
UILoginBackgroundURL string
|
||||
UIWelcomeMessage string
|
||||
MaxSidebarPlaylists int
|
||||
EnableTranscodingConfig bool
|
||||
EnableDownloads bool
|
||||
EnableExternalServices bool
|
||||
EnableMediaFileCoverArt bool
|
||||
TranscodingCacheSize string
|
||||
ImageCacheSize string
|
||||
EnableArtworkPrecache bool
|
||||
AutoImportPlaylists bool
|
||||
PlaylistsPath string
|
||||
AutoTranscodeDownload bool
|
||||
DefaultDownsamplingFormat string
|
||||
SearchFullString bool
|
||||
RecentlyAddedByModTime bool
|
||||
IgnoredArticles string
|
||||
IndexGroups string
|
||||
SubsonicArtistParticipations bool
|
||||
FFmpegPath string
|
||||
CoverArtPriority string
|
||||
CoverJpegQuality int
|
||||
ArtistArtPriority string
|
||||
EnableGravatar bool
|
||||
EnableFavourites bool
|
||||
EnableStarRating bool
|
||||
EnableUserEditing bool
|
||||
EnableSharing bool
|
||||
DefaultDownloadableShare bool
|
||||
DefaultTheme string
|
||||
DefaultLanguage string
|
||||
DefaultUIVolume int
|
||||
EnableReplayGain bool
|
||||
EnableCoverAnimation bool
|
||||
GATrackingID string
|
||||
EnableLogRedacting bool
|
||||
AuthRequestLimit int
|
||||
AuthWindowLength time.Duration
|
||||
PasswordEncryptionKey string
|
||||
ReverseProxyUserHeader string
|
||||
ReverseProxyWhitelist string
|
||||
Prometheus prometheusOptions
|
||||
Scanner scannerOptions
|
||||
ConfigFile string
|
||||
Address string
|
||||
Port int
|
||||
MusicFolder string
|
||||
DataFolder string
|
||||
DbPath string
|
||||
LogLevel string
|
||||
ScanInterval time.Duration
|
||||
SessionTimeout time.Duration
|
||||
BaseURL string
|
||||
UILoginBackgroundURL string
|
||||
EnableTranscodingConfig bool
|
||||
EnableDownloads bool
|
||||
TranscodingCacheSize string
|
||||
ImageCacheSize string
|
||||
AutoImportPlaylists bool
|
||||
|
||||
Agents string
|
||||
LastFM lastfmOptions
|
||||
Spotify spotifyOptions
|
||||
ListenBrainz listenBrainzOptions
|
||||
SearchFullString bool
|
||||
RecentlyAddedByModTime bool
|
||||
IgnoredArticles string
|
||||
IndexGroups string
|
||||
ProbeCommand string
|
||||
CoverArtPriority string
|
||||
CoverJpegQuality int
|
||||
UIWelcomeMessage string
|
||||
EnableGravatar bool
|
||||
GATrackingID string
|
||||
AuthRequestLimit int
|
||||
AuthWindowLength time.Duration
|
||||
|
||||
Scanner scannerOptions
|
||||
|
||||
Agents string
|
||||
LastFM lastfmOptions
|
||||
Spotify spotifyOptions
|
||||
|
||||
// DevFlags. These are used to enable/disable debugging and incomplete features
|
||||
DevLogSourceLine bool
|
||||
DevLogLevels map[string]string
|
||||
DevEnableProfiler bool
|
||||
DevAutoCreateAdminPassword string
|
||||
DevAutoLoginUsername string
|
||||
DevActivityPanel bool
|
||||
DevSidebarPlaylists bool
|
||||
DevEnableBufferedScrobble bool
|
||||
DevShowArtistPage bool
|
||||
DevArtworkMaxRequests int
|
||||
DevArtworkThrottleBacklogLimit int
|
||||
DevArtworkThrottleBacklogTimeout time.Duration
|
||||
DevArtistInfoTimeToLive time.Duration
|
||||
DevAlbumInfoTimeToLive time.Duration
|
||||
DevLogSourceLine bool
|
||||
DevAutoCreateAdminPassword string
|
||||
DevPreCacheAlbumArtwork bool
|
||||
DevFastAccessCoverArt bool
|
||||
DevOldCacheLayout bool
|
||||
DevActivityPanel bool
|
||||
}
|
||||
|
||||
type scannerOptions struct {
|
||||
Extractor string
|
||||
GenreSeparators string
|
||||
GroupAlbumReleases bool
|
||||
Extractor string
|
||||
}
|
||||
|
||||
type lastfmOptions struct {
|
||||
Enabled bool
|
||||
ApiKey string
|
||||
Secret string
|
||||
Language string
|
||||
@@ -119,16 +74,6 @@ type spotifyOptions struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
type listenBrainzOptions struct {
|
||||
Enabled bool
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
type prometheusOptions struct {
|
||||
Enabled bool
|
||||
MetricsPath string
|
||||
}
|
||||
|
||||
var (
|
||||
Server = &configOptions{}
|
||||
hooks []func()
|
||||
@@ -136,73 +81,29 @@ var (
|
||||
|
||||
func LoadFromFile(confFile string) {
|
||||
viper.SetConfigFile(confFile)
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error reading config file:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Load()
|
||||
}
|
||||
|
||||
func Load() {
|
||||
err := viper.Unmarshal(&Server)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config:", err)
|
||||
fmt.Println("Error parsing config:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = os.MkdirAll(Server.DataFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating data path:", "path", Server.DataFolder, err)
|
||||
fmt.Println("Error creating data path:", "path", Server.DataFolder, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if Server.CacheFolder == "" {
|
||||
Server.CacheFolder = filepath.Join(Server.DataFolder, "cache")
|
||||
}
|
||||
err = os.MkdirAll(Server.CacheFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating cache path:", "path", Server.CacheFolder, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Server.ConfigFile = viper.GetViper().ConfigFileUsed()
|
||||
if Server.DbPath == "" {
|
||||
Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath)
|
||||
}
|
||||
|
||||
log.SetLevelString(Server.LogLevel)
|
||||
log.SetLogLevels(Server.DevLogLevels)
|
||||
log.SetLogSourceLine(Server.DevLogSourceLine)
|
||||
log.SetRedacting(Server.EnableLogRedacting)
|
||||
|
||||
if err := validateScanSchedule(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if Server.BaseURL != "" {
|
||||
u, err := url.Parse(Server.BaseURL)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "FATAL: Invalid BaseURL %s: %s\n", Server.BaseURL, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
Server.BasePath = u.Path
|
||||
u.Path = ""
|
||||
u.RawQuery = ""
|
||||
Server.BaseHost = u.Host
|
||||
Server.BaseScheme = u.Scheme
|
||||
}
|
||||
|
||||
// Print current configuration if log level is Debug
|
||||
if log.CurrentLevel() >= log.LevelDebug {
|
||||
prettyConf := pretty.Sprintf("Loaded configuration from '%s': %# v", Server.ConfigFile, Server)
|
||||
if Server.EnableLogRedacting {
|
||||
prettyConf = log.Redact(prettyConf)
|
||||
}
|
||||
_, _ = fmt.Fprintln(os.Stderr, prettyConf)
|
||||
}
|
||||
|
||||
if !Server.EnableExternalServices {
|
||||
disableExternalServices()
|
||||
pretty.Printf("Loaded configuration from '%s': %# v\n", Server.ConfigFile, Server)
|
||||
}
|
||||
|
||||
// Call init hooks
|
||||
@@ -211,46 +112,6 @@ func Load() {
|
||||
}
|
||||
}
|
||||
|
||||
func disableExternalServices() {
|
||||
log.Info("All external integrations are DISABLED!")
|
||||
Server.LastFM.Enabled = false
|
||||
Server.Spotify.ID = ""
|
||||
Server.ListenBrainz.Enabled = false
|
||||
Server.Agents = ""
|
||||
if Server.UILoginBackgroundURL == consts.DefaultUILoginBackgroundURL {
|
||||
Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURLOffline
|
||||
}
|
||||
}
|
||||
|
||||
func validateScanSchedule() error {
|
||||
if Server.ScanInterval != -1 {
|
||||
log.Warn("ScanInterval is DEPRECATED. Please use ScanSchedule. See docs at https://navidrome.org/docs/usage/configuration-options/")
|
||||
if Server.ScanSchedule != "@every 1m" {
|
||||
log.Error("You cannot specify both ScanInterval and ScanSchedule, ignoring ScanInterval")
|
||||
} else {
|
||||
if Server.ScanInterval == 0 {
|
||||
Server.ScanSchedule = ""
|
||||
} else {
|
||||
Server.ScanSchedule = fmt.Sprintf("@every %s", Server.ScanInterval)
|
||||
}
|
||||
log.Warn("Setting ScanSchedule", "schedule", Server.ScanSchedule)
|
||||
}
|
||||
}
|
||||
if Server.ScanSchedule == "0" || Server.ScanSchedule == "" {
|
||||
Server.ScanSchedule = ""
|
||||
return nil
|
||||
}
|
||||
if _, err := time.ParseDuration(Server.ScanSchedule); err == nil {
|
||||
Server.ScanSchedule = "@every " + Server.ScanSchedule
|
||||
}
|
||||
c := cron.New()
|
||||
_, err := c.AddFunc(Server.ScanSchedule, func() {})
|
||||
if err != nil {
|
||||
log.Error("Invalid ScanSchedule. Please read format spec at https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format", "schedule", Server.ScanSchedule, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AddHook is used to register initialization code that should run as soon as the config is loaded
|
||||
func AddHook(hook func()) {
|
||||
hooks = append(hooks, hook)
|
||||
@@ -258,91 +119,49 @@ func AddHook(hook func()) {
|
||||
|
||||
func init() {
|
||||
viper.SetDefault("musicfolder", filepath.Join(".", "music"))
|
||||
viper.SetDefault("cachefolder", "")
|
||||
viper.SetDefault("datafolder", ".")
|
||||
viper.SetDefault("loglevel", "info")
|
||||
viper.SetDefault("address", "0.0.0.0")
|
||||
viper.SetDefault("port", 4533)
|
||||
viper.SetDefault("sessiontimeout", consts.DefaultSessionTimeout)
|
||||
viper.SetDefault("scaninterval", -1)
|
||||
viper.SetDefault("scanschedule", "@every 1m")
|
||||
viper.SetDefault("scaninterval", time.Minute)
|
||||
viper.SetDefault("baseurl", "")
|
||||
viper.SetDefault("tlscert", "")
|
||||
viper.SetDefault("tlskey", "")
|
||||
viper.SetDefault("uiloginbackgroundurl", consts.DefaultUILoginBackgroundURL)
|
||||
viper.SetDefault("uiwelcomemessage", "")
|
||||
viper.SetDefault("maxsidebarplaylists", consts.DefaultMaxSidebarPlaylists)
|
||||
viper.SetDefault("uiloginbackgroundurl", "https://source.unsplash.com/random/1600x900?music")
|
||||
viper.SetDefault("enabletranscodingconfig", false)
|
||||
viper.SetDefault("transcodingcachesize", "100MB")
|
||||
viper.SetDefault("imagecachesize", "100MB")
|
||||
viper.SetDefault("enableartworkprecache", true)
|
||||
viper.SetDefault("autoimportplaylists", true)
|
||||
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
|
||||
viper.SetDefault("enabledownloads", true)
|
||||
viper.SetDefault("enableexternalservices", true)
|
||||
viper.SetDefault("enableMediaFileCoverArt", true)
|
||||
viper.SetDefault("autotranscodedownload", false)
|
||||
viper.SetDefault("defaultdownsamplingformat", consts.DefaultDownsamplingFormat)
|
||||
|
||||
// Config options only valid for file/env configuration
|
||||
viper.SetDefault("searchfullstring", false)
|
||||
viper.SetDefault("recentlyaddedbymodtime", 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("coverartpriority", "cover.*, folder.*, front.*, embedded, external")
|
||||
viper.SetDefault("probecommand", "ffmpeg %s -f ffmetadata")
|
||||
viper.SetDefault("coverartpriority", "embedded, cover.*, folder.*, front.*")
|
||||
viper.SetDefault("coverjpegquality", 75)
|
||||
viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external")
|
||||
viper.SetDefault("uiwelcomemessage", "")
|
||||
viper.SetDefault("enablegravatar", false)
|
||||
viper.SetDefault("enablefavourites", true)
|
||||
viper.SetDefault("enablestarrating", true)
|
||||
viper.SetDefault("enableuserediting", true)
|
||||
viper.SetDefault("defaulttheme", "Dark")
|
||||
viper.SetDefault("defaultlanguage", "")
|
||||
viper.SetDefault("defaultuivolume", consts.DefaultUIVolume)
|
||||
viper.SetDefault("enablereplaygain", true)
|
||||
viper.SetDefault("enablecoveranimation", true)
|
||||
viper.SetDefault("gatrackingid", "")
|
||||
viper.SetDefault("enablelogredacting", true)
|
||||
viper.SetDefault("authrequestlimit", 5)
|
||||
viper.SetDefault("authwindowlength", 20*time.Second)
|
||||
viper.SetDefault("passwordencryptionkey", "")
|
||||
|
||||
viper.SetDefault("reverseproxyuserheader", "Remote-User")
|
||||
viper.SetDefault("reverseproxywhitelist", "")
|
||||
|
||||
viper.SetDefault("prometheus.enabled", false)
|
||||
viper.SetDefault("prometheus.metricspath", "/metrics")
|
||||
|
||||
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
|
||||
viper.SetDefault("scanner.genreseparators", ";/,")
|
||||
viper.SetDefault("scanner.groupalbumreleases", false)
|
||||
|
||||
viper.SetDefault("scanner.extractor", "taglib")
|
||||
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)
|
||||
viper.SetDefault("listenbrainz.baseurl", "https://api.listenbrainz.org/1/")
|
||||
|
||||
// DevFlags. These are used to enable/disable debugging and incomplete features
|
||||
viper.SetDefault("devlogsourceline", false)
|
||||
viper.SetDefault("devenableprofiler", false)
|
||||
viper.SetDefault("devautocreateadminpassword", "")
|
||||
viper.SetDefault("devautologinusername", "")
|
||||
viper.SetDefault("devprecachealbumartwork", false)
|
||||
viper.SetDefault("devoldcachelayout", false)
|
||||
viper.SetDefault("devFastAccessCoverArt", false)
|
||||
viper.SetDefault("devactivitypanel", true)
|
||||
viper.SetDefault("enablesharing", false)
|
||||
viper.SetDefault("defaultdownloadableshare", false)
|
||||
viper.SetDefault("devenablebufferedscrobble", true)
|
||||
viper.SetDefault("devsidebarplaylists", true)
|
||||
viper.SetDefault("devshowartistpage", true)
|
||||
viper.SetDefault("devartworkmaxrequests", number.Max(2, runtime.NumCPU()/3))
|
||||
viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit)
|
||||
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
|
||||
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)
|
||||
viper.SetDefault("devalbuminfotimetolive", consts.AlbumInfoTimeToLive)
|
||||
}
|
||||
|
||||
func InitConfig(cfgFile string) {
|
||||
@@ -363,8 +182,8 @@ func InitConfig(cfgFile string) {
|
||||
viper.AutomaticEnv()
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if viper.ConfigFileUsed() != "" && err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Navidrome could not open config file: ", err)
|
||||
if cfgFile != "" && err != nil {
|
||||
fmt.Println("Navidrome could not open config file: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
20
consts/banner.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package consts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/navidrome/navidrome/resources"
|
||||
)
|
||||
|
||||
func getBanner() string {
|
||||
data, _ := resources.Asset("banner.txt")
|
||||
return strings.TrimRightFunc(string(data), unicode.IsSpace)
|
||||
}
|
||||
|
||||
func Banner() string {
|
||||
version := "Version: " + Version()
|
||||
padding := strings.Repeat(" ", 52-len(version))
|
||||
return fmt.Sprintf("%s\n%s%s\n", getBanner(), padding, version)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package consts
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -14,113 +13,64 @@ const (
|
||||
DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL&_foreign_keys=on"
|
||||
InitialSetupFlagKey = "InitialSetup"
|
||||
|
||||
UIAuthorizationHeader = "X-ND-Authorization"
|
||||
UIClientUniqueIDHeader = "X-ND-Client-Unique-Id"
|
||||
JWTSecretKey = "JWTSecret"
|
||||
JWTIssuer = "ND"
|
||||
DefaultSessionTimeout = 24 * time.Hour
|
||||
CookieExpiry = 365 * 24 * 3600 // One year
|
||||
|
||||
// DefaultEncryptionKey This is the encryption key used if none is specified in the `PasswordEncryptionKey` option
|
||||
// Never ever change this! Or it will break all Navidrome installations that don't set the config option
|
||||
DefaultEncryptionKey = "just for obfuscation"
|
||||
PasswordsEncryptedKey = "PasswordsEncryptedKey"
|
||||
PasswordAutogenPrefix = "__NAVIDROME_AUTOGEN__" //nolint:gosec
|
||||
UIAuthorizationHeader = "X-ND-Authorization"
|
||||
JWTSecretKey = "JWTSecret"
|
||||
JWTIssuer = "ND"
|
||||
DefaultSessionTimeout = 24 * time.Hour
|
||||
|
||||
DevInitialUserName = "admin"
|
||||
DevInitialName = "Dev Admin"
|
||||
|
||||
URLPathUI = "/app"
|
||||
URLPathNativeAPI = "/api"
|
||||
URLPathAPI = "/api/v2"
|
||||
URLPathSubsonicAPI = "/rest"
|
||||
URLPathPublic = "/share"
|
||||
URLPathPublicImages = URLPathPublic + "/img"
|
||||
|
||||
// DefaultUILoginBackgroundURL uses Navidrome curated background images collection,
|
||||
// available at https://unsplash.com/collections/20072696/navidrome
|
||||
DefaultUILoginBackgroundURL = "/backgrounds"
|
||||
|
||||
// DefaultUILoginBackgroundOffline Background image used in case external integrations are disabled
|
||||
DefaultUILoginBackgroundOffline = "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAABGdBTUEAALGPC/xhBQAAAiJJREFUeF7t0IEAAAAAw6D5Ux/khVBhwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDDwMDDVlwABBWcSrQAAAABJRU5ErkJggg=="
|
||||
DefaultUILoginBackgroundURLOffline = "data:image/png;base64," + DefaultUILoginBackgroundOffline
|
||||
DefaultMaxSidebarPlaylists = 100
|
||||
URLPathUI = "/app"
|
||||
URLPathSubsonicAPI = "/rest"
|
||||
|
||||
RequestThrottleBacklogLimit = 100
|
||||
RequestThrottleBacklogTimeout = time.Minute
|
||||
|
||||
ServerReadHeaderTimeout = 3 * time.Second
|
||||
|
||||
ArtistInfoTimeToLive = 24 * time.Hour
|
||||
AlbumInfoTimeToLive = 7 * 24 * time.Hour
|
||||
ArtistInfoTimeToLive = 1 * time.Hour
|
||||
|
||||
I18nFolder = "i18n"
|
||||
SkipScanFile = ".ndignore"
|
||||
|
||||
PlaceholderArtistArt = "artist-placeholder.webp"
|
||||
PlaceholderAlbumArt = "placeholder.png"
|
||||
PlaceholderAvatar = "logo-192x192.png"
|
||||
UICoverArtSize = 300
|
||||
DefaultUIVolume = 100
|
||||
PlaceholderAlbumArt = "navidrome-600x600.png"
|
||||
PlaceholderAvatar = "logo-192x192.png"
|
||||
|
||||
DefaultHttpClientTimeOut = 10 * time.Second
|
||||
|
||||
DefaultScannerExtractor = "taglib"
|
||||
|
||||
Zwsp = string('\u200b')
|
||||
DefaultCachedHttpClientTTL = 10 * time.Second
|
||||
)
|
||||
|
||||
// Cache options
|
||||
const (
|
||||
TranscodingCacheDir = "transcoding"
|
||||
TranscodingCacheDir = "cache/transcoding"
|
||||
DefaultTranscodingCacheMaxItems = 0 // Unlimited
|
||||
|
||||
ImageCacheDir = "images"
|
||||
ImageCacheDir = "cache/images"
|
||||
DefaultImageCacheMaxItems = 0 // Unlimited
|
||||
|
||||
DefaultCacheSize = 100 * 1024 * 1024 // 100MB
|
||||
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{}{
|
||||
DefaultTranscodings = []map[string]interface{}{
|
||||
{
|
||||
"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 -map 0: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 -",
|
||||
},
|
||||
{
|
||||
"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 -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
|
||||
},
|
||||
}
|
||||
|
||||
DefaultPlaylistsPath = strings.Join([]string{".", "**/**"}, string(filepath.ListSeparator))
|
||||
)
|
||||
|
||||
var (
|
||||
VariousArtists = "Various Artists"
|
||||
VariousArtistsID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(VariousArtists))))
|
||||
UnknownAlbum = "[Unknown Album]"
|
||||
UnknownArtist = "[Unknown Artist]"
|
||||
UnknownArtistID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(UnknownArtist))))
|
||||
VariousArtistsMbzId = "89ad4ac3-39f7-470e-963a-56509c546377"
|
||||
VariousArtists = "Various Artists"
|
||||
VariousArtistsID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(VariousArtists))))
|
||||
UnknownArtist = "[Unknown Artist]"
|
||||
|
||||
ServerStart = time.Now()
|
||||
)
|
||||
|
||||
@@ -1,64 +1,38 @@
|
||||
package consts
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type format struct {
|
||||
typ string
|
||||
lossless bool
|
||||
}
|
||||
|
||||
var audioFormats = map[string]format{
|
||||
".mp3": {typ: "audio/mpeg"},
|
||||
".ogg": {typ: "audio/ogg"},
|
||||
".oga": {typ: "audio/ogg"},
|
||||
".opus": {typ: "audio/ogg"},
|
||||
".aac": {typ: "audio/mp4"},
|
||||
".alac": {typ: "audio/mp4", lossless: true},
|
||||
".m4a": {typ: "audio/mp4"},
|
||||
".m4b": {typ: "audio/mp4"},
|
||||
".flac": {typ: "audio/flac", lossless: true},
|
||||
".wav": {typ: "audio/x-wav", lossless: true},
|
||||
".wma": {typ: "audio/x-ms-wma"},
|
||||
".ape": {typ: "audio/x-monkeys-audio", lossless: true},
|
||||
".mpc": {typ: "audio/x-musepack"},
|
||||
".shn": {typ: "audio/x-shn", lossless: true},
|
||||
".aif": {typ: "audio/x-aiff"},
|
||||
".aiff": {typ: "audio/x-aiff"},
|
||||
".m3u": {typ: "audio/x-mpegurl"},
|
||||
".pls": {typ: "audio/x-scpls"},
|
||||
".dsf": {typ: "audio/dsd", lossless: true},
|
||||
".wv": {typ: "audio/x-wavpack", lossless: true},
|
||||
".wvp": {typ: "audio/x-wavpack", lossless: true},
|
||||
".mka": {typ: "audio/x-matroska"},
|
||||
}
|
||||
var imageFormats = map[string]string{
|
||||
".gif": "image/gif",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".png": "image/png",
|
||||
".bmp": "image/bmp",
|
||||
}
|
||||
|
||||
var LosslessFormats []string
|
||||
import "mime"
|
||||
|
||||
func init() {
|
||||
for ext, fmt := range audioFormats {
|
||||
_ = mime.AddExtensionType(ext, fmt.typ)
|
||||
if fmt.lossless {
|
||||
LosslessFormats = append(LosslessFormats, strings.TrimPrefix(ext, "."))
|
||||
}
|
||||
}
|
||||
sort.Strings(LosslessFormats)
|
||||
for ext, typ := range imageFormats {
|
||||
_ = mime.AddExtensionType(ext, typ)
|
||||
mt := map[string]string{
|
||||
".mp3": "audio/mpeg",
|
||||
".ogg": "audio/ogg",
|
||||
".oga": "audio/ogg",
|
||||
".opus": "audio/ogg",
|
||||
".aac": "audio/mp4",
|
||||
".m4a": "audio/mp4",
|
||||
".m4b": "audio/mp4",
|
||||
".flac": "audio/flac",
|
||||
".wav": "audio/x-wav",
|
||||
".wma": "audio/x-ms-wma",
|
||||
".ape": "audio/x-monkeys-audio",
|
||||
".mpc": "audio/x-musepack",
|
||||
".shn": "audio/x-shn",
|
||||
".aif": "audio/x-aiff",
|
||||
".aiff": "audio/x-aiff",
|
||||
".m3u": "audio/x-mpegurl",
|
||||
".pls": "audio/x-scpls",
|
||||
".dsf": "audio/dsd",
|
||||
".wv": "audio/x-wavpack",
|
||||
".wvp": "audio/x-wavpack",
|
||||
".gif": "image/gif",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".png": "image/png",
|
||||
".bmp": "image/bmp",
|
||||
}
|
||||
|
||||
// In some circumstances, Windows sets JS mime-type to `text/plain`!
|
||||
_ = mime.AddExtensionType(".js", "text/javascript")
|
||||
_ = mime.AddExtensionType(".css", "text/css")
|
||||
for ext, typ := range mt {
|
||||
_ = mime.AddExtensionType(ext, typ)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,15 @@ var (
|
||||
gitSha string
|
||||
)
|
||||
|
||||
// Version holds the version string, with tag and git sha info.
|
||||
// Examples:
|
||||
// Formats:
|
||||
// dev
|
||||
// v0.2.0 (5b84188)
|
||||
// v0.3.2-SNAPSHOT (715f552)
|
||||
// master (9ed35cb)
|
||||
var Version = func() string {
|
||||
func Version() string {
|
||||
if gitSha == "" {
|
||||
return "dev"
|
||||
}
|
||||
gitTag = strings.TrimPrefix(gitTag, "v")
|
||||
return fmt.Sprintf("%s (%s)", gitTag, gitSha)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
https://your.website {
|
||||
reverse_proxy * navidrome:4533 {
|
||||
header_up Host {http.reverse_proxy.upstream.hostport}
|
||||
header_up X-Forwarded-For {http.request.remote}
|
||||
header_up X-Real-IP {http.reverse_proxy.upstream.port}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
version: '3.6'
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
navidrome_data:
|
||||
|
||||
services:
|
||||
|
||||
caddy:
|
||||
container_name: "caddy"
|
||||
image: caddy:2.6-alpine
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
volumes:
|
||||
- "caddy_data:/data:rw"
|
||||
- "./Caddyfile:/etc/caddy/Caddyfile:ro"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
navidrome:
|
||||
container_name: "navidrome"
|
||||
image: deluan/navidrome:latest
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
# user: 1000:1000
|
||||
ports:
|
||||
- "4533:4533"
|
||||
volumes:
|
||||
- "navidrome_data:/data"
|
||||
#- "/mnt/music:/music:ro"
|
||||
@@ -1,51 +0,0 @@
|
||||
version: "3.6"
|
||||
|
||||
volumes:
|
||||
traefik_data:
|
||||
navidrome_data:
|
||||
|
||||
services:
|
||||
|
||||
traefik:
|
||||
container_name: "traefik"
|
||||
image: traefik:2.9
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
command:
|
||||
- "--log.level=ERROR"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.tc.acme.tlschallenge=true"
|
||||
#- "--certificatesresolvers.tc.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
- "--certificatesresolvers.tc.acme.email=foo@foo.com"
|
||||
- "--certificatesresolvers.tc.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "traefik_data:/letsencrypt"
|
||||
#- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
|
||||
navidrome:
|
||||
container_name: "navidrome"
|
||||
image: deluan/navidrome:latest
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
# user: 1000:1000
|
||||
ports:
|
||||
- "4533:4533"
|
||||
environment:
|
||||
ND_SCANINTERVAL: 6h
|
||||
ND_LOGLEVEL: info
|
||||
ND_SESSIONTIMEOUT: 168h
|
||||
ND_BASEURL: ""
|
||||
volumes:
|
||||
- "navidrome_data:/data"
|
||||
#- "/mnt/music:/music:ro"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.navidrome.rule=Host(`foo.com`)"
|
||||
- "traefik.http.routers.navidrome.entrypoints=websecure"
|
||||
- "traefik.http.routers.navidrome.tls=true"
|
||||
- "traefik.http.routers.navidrome.tls.certresolver=tc"
|
||||
- "traefik.http.services.navidrome.loadbalancer.server.port=4533"
|
||||
@@ -1,18 +0,0 @@
|
||||
version: '3.6'
|
||||
|
||||
volumes:
|
||||
navidrome_data:
|
||||
|
||||
services:
|
||||
|
||||
navidrome:
|
||||
container_name: "navidrome"
|
||||
image: deluan/navidrome:latest
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
# user: 1000:1000
|
||||
ports:
|
||||
- "4533:4533"
|
||||
volumes:
|
||||
- "navidrome_data:/data"
|
||||
#- "/mnt/music:/music:ro"
|
||||
@@ -1,11 +0,0 @@
|
||||
# Kubernetes
|
||||
|
||||
A couple things to keep in mind with this manifest:
|
||||
|
||||
1. This creates a namespace called `navidrome`. Adjust this as needed.
|
||||
1. This manifest was created on [K3s](https://github.com/k3s-io/k3s), which uses its own storage provisioner called [local-path-provisioner](https://github.com/rancher/local-path-provisioner). Be sure to change the `storageClassName` of the `PersistentVolumeClaim` as needed.
|
||||
1. The `PersistentVolumeClaim` sets up a 2Gi volume for Navidrome's database. Adjust this as needed.
|
||||
1. Be sure to change the `image` tag from `ghcr.io/navidrome/navidrome:0.49.3` to whatever the newest version is.
|
||||
1. This assumes your music is mounted on the host using `hostPath` at `/path/to/your/music/on/the/host`. Adjust this as needed.
|
||||
1. The `Ingress` is already configured for `cert-manager` to obtain a Let's Encrypt TLS certificate and uses Traefik for routing. Adjust this as needed.
|
||||
1. The `Ingress` presents the service at `navidrome.${SECRET_INTERNAL_DOMAIN_NAME}`, which needs to already be setup in DNS.
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: navidrome
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: navidrome-data-pvc
|
||||
namespace: navidrome
|
||||
annotations:
|
||||
volumeType: local
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
storageClassName: local-path
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: navidrome-deployment
|
||||
namespace: navidrome
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: navidrome
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: navidrome
|
||||
spec:
|
||||
containers:
|
||||
- name: navidrome
|
||||
image: ghcr.io/navidrome/navidrome:0.49.3
|
||||
ports:
|
||||
- containerPort: 4533
|
||||
env:
|
||||
- name: ND_SCANSCHEDULE
|
||||
value: "12h"
|
||||
- name: ND_SESSIONTIMEOUT
|
||||
value: "24h"
|
||||
- name: ND_LOGLEVEL
|
||||
value: "info"
|
||||
- name: ND_ENABLETRANSCODINGCONFIG
|
||||
value: "false"
|
||||
- name: ND_TRANSCODINGCACHESIZE
|
||||
value: "512MB"
|
||||
- name: ND_ENABLESTARRATING
|
||||
value: "false"
|
||||
- name: ND_ENABLEFAVOURITES
|
||||
value: "false"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: music
|
||||
mountPath: /music
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: navidrome-data-pvc
|
||||
- name: music
|
||||
hostPath:
|
||||
path: /path/to/your/music/on/the/host
|
||||
type: Directory
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: navidrome-service
|
||||
namespace: navidrome
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
targetPort: 4533
|
||||
port: 4533
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: navidrome
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: navidrome-ingress
|
||||
namespace: navidrome
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-production
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
spec:
|
||||
rules:
|
||||
- host: navidrome.${SECRET_INTERNAL_DOMAIN_NAME}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: navidrome-service
|
||||
port:
|
||||
number: 4533
|
||||
tls:
|
||||
- hosts:
|
||||
- navidrome.${SECRET_INTERNAL_DOMAIN_NAME}
|
||||
secretName: navidrome-tls
|
||||
@@ -3,6 +3,7 @@
|
||||
[Unit]
|
||||
Description=Navidrome Music Server and Streamer compatible with Subsonic/Airsonic
|
||||
After=remote-fs.target network.target
|
||||
AssertPathExists=/var/lib/navidrome
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -12,7 +13,6 @@ User=navidrome
|
||||
Group=navidrome
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/navidrome
|
||||
StateDirectory=navidrome
|
||||
WorkingDirectory=/var/lib/navidrome
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
@@ -21,26 +21,18 @@ Restart=on-failure
|
||||
EnvironmentFile=-/etc/sysconfig/navidrome
|
||||
|
||||
# See https://www.freedesktop.org/software/systemd/man/systemd.exec.html
|
||||
CapabilityBoundingSet=
|
||||
DevicePolicy=closed
|
||||
NoNewPrivileges=yes
|
||||
LockPersonality=yes
|
||||
PrivateTmp=yes
|
||||
PrivateUsers=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
SystemCallFilter=setrlimit
|
||||
SystemCallArchitectures=native
|
||||
UMask=0066
|
||||
SystemCallFilter=~@clock @debug @module @mount @obsolete @privileged @reboot @setuid @swap
|
||||
ReadWritePaths=/var/lib/navidrome
|
||||
|
||||
# You can uncomment the following line if you're not using the jukebox This
|
||||
# will prevent navidrome from accessing any real (physical) devices
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
This folder abstracts metadata lookup into "agents". Each agent can be implemented to get as
|
||||
much info as the external source provides, by using a granular set of interfaces
|
||||
(see [interfaces](interfaces.go)).
|
||||
(see [interfaces](interfaces.go)].
|
||||
|
||||
A new agent must comply with these simple implementation rules:
|
||||
1) Implement the `AgentName()` method. It just returns the name of the agent for logging purposes.
|
||||
@@ -9,4 +9,4 @@ A new agent must comply with these simple implementation rules:
|
||||
|
||||
For an agent to be used it needs to be listed in the `Agents` config option (default is `"lastfm,spotify"`). The order dictates the priority of the agents
|
||||
|
||||
For a simple Agent example, look at the [local_agent](local_agent.go) agent source code.
|
||||
For a simple Agent example, look at the [placeholders.go](placeholders.go) agent source code.
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
type Agents struct {
|
||||
ds model.DataStore
|
||||
agents []Interface
|
||||
}
|
||||
|
||||
func New(ds model.DataStore) *Agents {
|
||||
var order []string
|
||||
if conf.Server.Agents != "" {
|
||||
order = strings.Split(conf.Server.Agents, ",")
|
||||
}
|
||||
order = append(order, LocalAgentName)
|
||||
var res []Interface
|
||||
for _, name := range order {
|
||||
init, ok := Map[name]
|
||||
if !ok {
|
||||
log.Error("Agent not available. Check configuration", "name", name)
|
||||
continue
|
||||
}
|
||||
|
||||
res = append(res, init(ds))
|
||||
}
|
||||
|
||||
return &Agents{ds: ds, agents: res}
|
||||
}
|
||||
|
||||
func (a *Agents) AgentName() string {
|
||||
return "agents"
|
||||
}
|
||||
|
||||
func (a *Agents) GetArtistMBID(ctx context.Context, id string, name string) (string, error) {
|
||||
switch id {
|
||||
case consts.UnknownArtistID:
|
||||
return "", ErrNotFound
|
||||
case consts.VariousArtistsID:
|
||||
return "", nil
|
||||
}
|
||||
start := time.Now()
|
||||
for _, ag := range a.agents {
|
||||
if utils.IsCtxDone(ctx) {
|
||||
break
|
||||
}
|
||||
agent, ok := ag.(ArtistMBIDRetriever)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
mbid, err := agent.GetArtistMBID(ctx, id, name)
|
||||
if mbid != "" && err == nil {
|
||||
log.Debug(ctx, "Got MBID", "agent", ag.AgentName(), "artist", name, "mbid", mbid, "elapsed", time.Since(start))
|
||||
return mbid, nil
|
||||
}
|
||||
}
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
func (a *Agents) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) {
|
||||
switch id {
|
||||
case consts.UnknownArtistID:
|
||||
return "", ErrNotFound
|
||||
case consts.VariousArtistsID:
|
||||
return "", nil
|
||||
}
|
||||
start := time.Now()
|
||||
for _, ag := range a.agents {
|
||||
if utils.IsCtxDone(ctx) {
|
||||
break
|
||||
}
|
||||
agent, ok := ag.(ArtistURLRetriever)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
url, err := agent.GetArtistURL(ctx, id, name, mbid)
|
||||
if url != "" && err == nil {
|
||||
log.Debug(ctx, "Got External Url", "agent", ag.AgentName(), "artist", name, "url", url, "elapsed", time.Since(start))
|
||||
return url, nil
|
||||
}
|
||||
}
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
func (a *Agents) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) {
|
||||
switch id {
|
||||
case consts.UnknownArtistID:
|
||||
return "", ErrNotFound
|
||||
case consts.VariousArtistsID:
|
||||
return "", nil
|
||||
}
|
||||
start := time.Now()
|
||||
for _, ag := range a.agents {
|
||||
if utils.IsCtxDone(ctx) {
|
||||
break
|
||||
}
|
||||
agent, ok := ag.(ArtistBiographyRetriever)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
bio, err := agent.GetArtistBiography(ctx, id, name, mbid)
|
||||
if err == nil {
|
||||
log.Debug(ctx, "Got Biography", "agent", ag.AgentName(), "artist", name, "len", len(bio), "elapsed", time.Since(start))
|
||||
return bio, nil
|
||||
}
|
||||
}
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) {
|
||||
switch id {
|
||||
case consts.UnknownArtistID:
|
||||
return nil, ErrNotFound
|
||||
case consts.VariousArtistsID:
|
||||
return nil, nil
|
||||
}
|
||||
start := time.Now()
|
||||
for _, ag := range a.agents {
|
||||
if utils.IsCtxDone(ctx) {
|
||||
break
|
||||
}
|
||||
agent, ok := ag.(ArtistSimilarRetriever)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
similar, err := agent.GetSimilarArtists(ctx, id, name, mbid, limit)
|
||||
if len(similar) > 0 && err == nil {
|
||||
if log.CurrentLevel() >= 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))
|
||||
}
|
||||
return similar, err
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (a *Agents) GetArtistImages(ctx context.Context, id, name, mbid string) ([]ExternalImage, error) {
|
||||
switch id {
|
||||
case consts.UnknownArtistID:
|
||||
return nil, ErrNotFound
|
||||
case consts.VariousArtistsID:
|
||||
return nil, nil
|
||||
}
|
||||
start := time.Now()
|
||||
for _, ag := range a.agents {
|
||||
if utils.IsCtxDone(ctx) {
|
||||
break
|
||||
}
|
||||
agent, ok := ag.(ArtistImageRetriever)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
images, err := agent.GetArtistImages(ctx, id, name, mbid)
|
||||
if len(images) > 0 && err == nil {
|
||||
log.Debug(ctx, "Got Images", "agent", ag.AgentName(), "artist", name, "images", images, "elapsed", time.Since(start))
|
||||
return images, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (a *Agents) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) {
|
||||
switch id {
|
||||
case consts.UnknownArtistID:
|
||||
return nil, ErrNotFound
|
||||
case consts.VariousArtistsID:
|
||||
return nil, nil
|
||||
}
|
||||
start := time.Now()
|
||||
for _, ag := range a.agents {
|
||||
if utils.IsCtxDone(ctx) {
|
||||
break
|
||||
}
|
||||
agent, ok := ag.(ArtistTopSongsRetriever)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
songs, err := agent.GetArtistTopSongs(ctx, id, artistName, mbid, count)
|
||||
if len(songs) > 0 && err == nil {
|
||||
log.Debug(ctx, "Got Top Songs", "agent", ag.AgentName(), "artist", artistName, "songs", songs, "elapsed", time.Since(start))
|
||||
return songs, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (a *Agents) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*AlbumInfo, error) {
|
||||
if name == consts.UnknownAlbum {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
start := time.Now()
|
||||
for _, ag := range a.agents {
|
||||
if utils.IsCtxDone(ctx) {
|
||||
break
|
||||
}
|
||||
agent, ok := ag.(AlbumInfoRetriever)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
album, err := agent.GetAlbumInfo(ctx, name, artist, mbid)
|
||||
if err == nil {
|
||||
log.Debug(ctx, "Got Album Info", "agent", ag.AgentName(), "album", name, "artist", artist,
|
||||
"mbid", mbid, "elapsed", time.Since(start))
|
||||
return album, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
var _ Interface = (*Agents)(nil)
|
||||
var _ ArtistMBIDRetriever = (*Agents)(nil)
|
||||
var _ ArtistURLRetriever = (*Agents)(nil)
|
||||
var _ ArtistBiographyRetriever = (*Agents)(nil)
|
||||
var _ ArtistSimilarRetriever = (*Agents)(nil)
|
||||
var _ ArtistImageRetriever = (*Agents)(nil)
|
||||
var _ ArtistTopSongsRetriever = (*Agents)(nil)
|
||||
var _ AlbumInfoRetriever = (*Agents)(nil)
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestAgents(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
log.SetLevel(log.LevelFatal)
|
||||
log.SetLevel(log.LevelCritical)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Agents Test Suite")
|
||||
}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Agents", func() {
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
var ds model.DataStore
|
||||
var mfRepo *tests.MockMediaFileRepo
|
||||
BeforeEach(func() {
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
mfRepo = tests.CreateMockMediaFileRepo()
|
||||
ds = &tests.MockDataStore{MockedMediaFile: mfRepo}
|
||||
})
|
||||
|
||||
Describe("Local", func() {
|
||||
var ag *Agents
|
||||
BeforeEach(func() {
|
||||
conf.Server.Agents = ""
|
||||
ag = New(ds)
|
||||
})
|
||||
|
||||
It("calls the placeholder GetArtistImages", func() {
|
||||
mfRepo.SetData(model.MediaFiles{{ID: "1", Title: "One", MbzReleaseTrackID: "111"}, {ID: "2", Title: "Two", MbzReleaseTrackID: "222"}})
|
||||
songs, err := ag.GetArtistTopSongs(ctx, "123", "John Doe", "mb123", 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).To(ConsistOf([]Song{{Name: "One", MBID: "111"}, {Name: "Two", MBID: "222"}}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Agents", func() {
|
||||
var ag *Agents
|
||||
var mock *mockAgent
|
||||
BeforeEach(func() {
|
||||
mock = &mockAgent{}
|
||||
Register("fake", func(ds model.DataStore) Interface {
|
||||
return mock
|
||||
})
|
||||
Register("empty", func(ds model.DataStore) Interface {
|
||||
return struct {
|
||||
Interface
|
||||
}{}
|
||||
})
|
||||
conf.Server.Agents = "empty,fake"
|
||||
ag = New(ds)
|
||||
Expect(ag.AgentName()).To(Equal("agents"))
|
||||
})
|
||||
|
||||
Describe("GetArtistMBID", func() {
|
||||
It("returns on first match", func() {
|
||||
Expect(ag.GetArtistMBID(ctx, "123", "test")).To(Equal("mbid"))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test"))
|
||||
})
|
||||
It("returns empty if artist is Various Artists", func() {
|
||||
mbid, err := ag.GetArtistMBID(ctx, consts.VariousArtistsID, consts.VariousArtists)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mbid).To(BeEmpty())
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
It("returns not found if artist is Unknown Artist", func() {
|
||||
mbid, err := ag.GetArtistMBID(ctx, consts.VariousArtistsID, consts.VariousArtists)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mbid).To(BeEmpty())
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
It("skips the agent if it returns an error", func() {
|
||||
mock.Err = errors.New("error")
|
||||
_, err := ag.GetArtistMBID(ctx, "123", "test")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test"))
|
||||
})
|
||||
It("interrupts if the context is canceled", func() {
|
||||
cancel()
|
||||
_, err := ag.GetArtistMBID(ctx, "123", "test")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetArtistURL", func() {
|
||||
It("returns on first match", func() {
|
||||
Expect(ag.GetArtistURL(ctx, "123", "test", "mb123")).To(Equal("url"))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
|
||||
})
|
||||
It("returns empty if artist is Various Artists", func() {
|
||||
url, err := ag.GetArtistURL(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(url).To(BeEmpty())
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
It("returns not found if artist is Unknown Artist", func() {
|
||||
url, err := ag.GetArtistURL(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(url).To(BeEmpty())
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
It("skips the agent if it returns an error", func() {
|
||||
mock.Err = errors.New("error")
|
||||
_, err := ag.GetArtistURL(ctx, "123", "test", "mb123")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
|
||||
})
|
||||
It("interrupts if the context is canceled", func() {
|
||||
cancel()
|
||||
_, err := ag.GetArtistURL(ctx, "123", "test", "mb123")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetArtistBiography", func() {
|
||||
It("returns on first match", func() {
|
||||
Expect(ag.GetArtistBiography(ctx, "123", "test", "mb123")).To(Equal("bio"))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
|
||||
})
|
||||
It("returns empty if artist is Various Artists", func() {
|
||||
bio, err := ag.GetArtistBiography(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(bio).To(BeEmpty())
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
It("returns not found if artist is Unknown Artist", func() {
|
||||
bio, err := ag.GetArtistBiography(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(bio).To(BeEmpty())
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
It("skips the agent if it returns an error", func() {
|
||||
mock.Err = errors.New("error")
|
||||
_, err := ag.GetArtistBiography(ctx, "123", "test", "mb123")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
|
||||
})
|
||||
It("interrupts if the context is canceled", func() {
|
||||
cancel()
|
||||
_, err := ag.GetArtistBiography(ctx, "123", "test", "mb123")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetArtistImages", func() {
|
||||
It("returns on first match", func() {
|
||||
Expect(ag.GetArtistImages(ctx, "123", "test", "mb123")).To(Equal([]ExternalImage{{
|
||||
URL: "imageUrl",
|
||||
Size: 100,
|
||||
}}))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
|
||||
})
|
||||
It("skips the agent if it returns an error", func() {
|
||||
mock.Err = errors.New("error")
|
||||
_, err := ag.GetArtistImages(ctx, "123", "test", "mb123")
|
||||
Expect(err).To(MatchError("not found"))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
|
||||
})
|
||||
It("interrupts if the context is canceled", func() {
|
||||
cancel()
|
||||
_, err := ag.GetArtistImages(ctx, "123", "test", "mb123")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetSimilarArtists", func() {
|
||||
It("returns on first match", func() {
|
||||
Expect(ag.GetSimilarArtists(ctx, "123", "test", "mb123", 1)).To(Equal([]Artist{{
|
||||
Name: "Joe Dohn",
|
||||
MBID: "mbid321",
|
||||
}}))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
|
||||
})
|
||||
It("skips the agent if it returns an error", func() {
|
||||
mock.Err = errors.New("error")
|
||||
_, err := ag.GetSimilarArtists(ctx, "123", "test", "mb123", 1)
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
|
||||
})
|
||||
It("interrupts if the context is canceled", func() {
|
||||
cancel()
|
||||
_, err := ag.GetSimilarArtists(ctx, "123", "test", "mb123", 1)
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetArtistTopSongs", func() {
|
||||
It("returns on first match", func() {
|
||||
Expect(ag.GetArtistTopSongs(ctx, "123", "test", "mb123", 2)).To(Equal([]Song{{
|
||||
Name: "A Song",
|
||||
MBID: "mbid444",
|
||||
}}))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
|
||||
})
|
||||
It("skips the agent if it returns an error", func() {
|
||||
mock.Err = errors.New("error")
|
||||
_, err := ag.GetArtistTopSongs(ctx, "123", "test", "mb123", 2)
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
|
||||
})
|
||||
It("interrupts if the context is canceled", func() {
|
||||
cancel()
|
||||
_, err := ag.GetArtistTopSongs(ctx, "123", "test", "mb123", 2)
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetAlbumInfo", func() {
|
||||
It("returns meaningful data", func() {
|
||||
Expect(ag.GetAlbumInfo(ctx, "album", "artist", "mbid")).To(Equal(&AlbumInfo{
|
||||
Name: "A Song",
|
||||
MBID: "mbid444",
|
||||
Description: "A Description",
|
||||
URL: "External URL",
|
||||
Images: []ExternalImage{
|
||||
{
|
||||
Size: 174,
|
||||
URL: "https://lastfm.freetls.fastly.net/i/u/174s/00000000000000000000000000000000.png",
|
||||
}, {
|
||||
Size: 64,
|
||||
URL: "https://lastfm.freetls.fastly.net/i/u/64s/00000000000000000000000000000000.png",
|
||||
}, {
|
||||
Size: 34,
|
||||
URL: "https://lastfm.freetls.fastly.net/i/u/34s/00000000000000000000000000000000.png",
|
||||
},
|
||||
},
|
||||
}))
|
||||
Expect(mock.Args).To(ConsistOf("album", "artist", "mbid"))
|
||||
})
|
||||
It("skips the agent if it returns an error", func() {
|
||||
mock.Err = errors.New("error")
|
||||
_, err := ag.GetAlbumInfo(ctx, "album", "artist", "mbid")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(ConsistOf("album", "artist", "mbid"))
|
||||
})
|
||||
It("interrupts if the context is canceled", func() {
|
||||
cancel()
|
||||
_, err := ag.GetAlbumInfo(ctx, "album", "artist", "mbid")
|
||||
Expect(err).To(MatchError(ErrNotFound))
|
||||
Expect(mock.Args).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
type mockAgent struct {
|
||||
Args []interface{}
|
||||
Err error
|
||||
}
|
||||
|
||||
func (a *mockAgent) AgentName() string {
|
||||
return "fake"
|
||||
}
|
||||
|
||||
func (a *mockAgent) GetArtistMBID(_ context.Context, id string, name string) (string, error) {
|
||||
a.Args = []interface{}{id, name}
|
||||
if a.Err != nil {
|
||||
return "", a.Err
|
||||
}
|
||||
return "mbid", nil
|
||||
}
|
||||
|
||||
func (a *mockAgent) GetArtistURL(_ context.Context, id, name, mbid string) (string, error) {
|
||||
a.Args = []interface{}{id, name, mbid}
|
||||
if a.Err != nil {
|
||||
return "", a.Err
|
||||
}
|
||||
return "url", nil
|
||||
}
|
||||
|
||||
func (a *mockAgent) GetArtistBiography(_ context.Context, id, name, mbid string) (string, error) {
|
||||
a.Args = []interface{}{id, name, mbid}
|
||||
if a.Err != nil {
|
||||
return "", a.Err
|
||||
}
|
||||
return "bio", nil
|
||||
}
|
||||
|
||||
func (a *mockAgent) GetArtistImages(_ context.Context, id, name, mbid string) ([]ExternalImage, error) {
|
||||
a.Args = []interface{}{id, name, mbid}
|
||||
if a.Err != nil {
|
||||
return nil, a.Err
|
||||
}
|
||||
return []ExternalImage{{
|
||||
URL: "imageUrl",
|
||||
Size: 100,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (a *mockAgent) GetSimilarArtists(_ context.Context, id, name, mbid string, limit int) ([]Artist, error) {
|
||||
a.Args = []interface{}{id, name, mbid, limit}
|
||||
if a.Err != nil {
|
||||
return nil, a.Err
|
||||
}
|
||||
return []Artist{{
|
||||
Name: "Joe Dohn",
|
||||
MBID: "mbid321",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (a *mockAgent) GetArtistTopSongs(_ context.Context, id, artistName, mbid string, count int) ([]Song, error) {
|
||||
a.Args = []interface{}{id, artistName, mbid, count}
|
||||
if a.Err != nil {
|
||||
return nil, a.Err
|
||||
}
|
||||
return []Song{{
|
||||
Name: "A Song",
|
||||
MBID: "mbid444",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (a *mockAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*AlbumInfo, error) {
|
||||
a.Args = []interface{}{name, artist, mbid}
|
||||
if a.Err != nil {
|
||||
return nil, a.Err
|
||||
}
|
||||
return &AlbumInfo{
|
||||
Name: "A Song",
|
||||
MBID: "mbid444",
|
||||
Description: "A Description",
|
||||
URL: "External URL",
|
||||
Images: []ExternalImage{
|
||||
{
|
||||
Size: 174,
|
||||
URL: "https://lastfm.freetls.fastly.net/i/u/174s/00000000000000000000000000000000.png",
|
||||
}, {
|
||||
Size: 64,
|
||||
URL: "https://lastfm.freetls.fastly.net/i/u/64s/00000000000000000000000000000000.png",
|
||||
}, {
|
||||
Size: 34,
|
||||
URL: "https://lastfm.freetls.fastly.net/i/u/34s/00000000000000000000000000000000.png",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package agents
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -46,7 +47,6 @@ func NewCachedHTTPClient(wrapped httpDoer, ttl time.Duration) *CachedHTTPClient
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return c.serializeResponse(resp), ttl, nil
|
||||
})
|
||||
c.cache.SetNewItemCallback(func(key string, value interface{}) {
|
||||
@@ -71,7 +71,7 @@ func (c *CachedHTTPClient) serializeReq(req *http.Request) string {
|
||||
URL: req.URL.String(),
|
||||
}
|
||||
if req.Body != nil {
|
||||
bodyData, _ := io.ReadAll(req.Body)
|
||||
bodyData, _ := ioutil.ReadAll(req.Body)
|
||||
bodyStr := base64.StdEncoding.EncodeToString(bodyData)
|
||||
data.Body = &bodyStr
|
||||
}
|
||||