From 2392a0343352461ae8c0a3882b8c50e20e685895 Mon Sep 17 00:00:00 2001 From: GitHub Agentic AI Date: Sat, 28 Feb 2026 10:21:29 +0530 Subject: [PATCH 1/2] fix: automated fix by GitHub Agentic AI --- GEMINI.md | 464 +++++++++++++++++++++++++++++++++ server/public/handle_images.go | 4 +- 2 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 GEMINI.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..4044b8556 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,464 @@ +# Task: Fix Issue #5112 + +## Issue to Solve +**Title:** [Bug]: /share/img/ returns 404 for artists without artwork, triggers security tool false positives (CrowdSec, fail2ban) +**Number:** #5112 + +### I confirm that: + +- [x] 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 +- [x] I'm using the latest version (your issue may have been fixed already) + +### Version + +0.60.3 + +### Current Behavior + + When an artist has no artwork, the /share/img/{jwt} endpoint returns 404 Not Found. When a client displays a folder/artist list containing multiple artists without artwork, this generates a burst of concurrent 404 responses. Security tools running on the same host (CrowdSec http-probing scenario, fail2ban, WAFs) interpret this burst as an attack and ban the client's IP address — including legitimate users on the same network as the server. + +### Expected Behavior + +/share/img/{jwt} should return either: + - 204 No Content when no artwork is available for the resource, or + - a generic placeholder image (200 OK) + +A 404 is semantically valid but operationally harmful: it is indistinguishable from probing/scanning at the HTTP layer. + +### Steps To Reproduce + + 1. Set up Navidrome behind a reverse proxy with CrowdSec (or any security tool monitoring 404 rates) + 2. Have a music library where some artists have no embedded artwork + 3. Connect with a client that uses /share/img/ URLs to display artist images (e.g. Substreamer, "Folders" view) + 4. Open a view that loads multiple artist images simultaneously (Substreamer's "Folders" view) + +### Environment + +```markdown +- Navidrome version: 0.60.3 (34c6f12a) +- OS: Debian GNU/Linux 13 (trixie), aarch64 (Raspberry Pi 4) +- Client: Substreamer (Android WebView), "Folders" view +- Reverse proxy: Nginx with CrowdSec firewall bouncer +``` + +### How Navidrome is installed? + +Binary (from downloads page) + +### Configuration + +```toml + +``` + +### Relevant log output + +```shell + +``` + +### Anything else? + + My temporary workaround: + + Intercept 404s at the Nginx reverse proxy level: + + location /share/img/ { + proxy_pass http://127.0.0.1:4533; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_intercept_errors on; + error_page 404 = @noimage; + } + + location @noimage { + return 204; + } + + +### Code of Conduct + +- [x] I agree to follow Navidrome's Code of Conduct + +## Repository Info +**Repository:** navidrome/navidrome + +### Key Files +- .devcontainer/devcontainer.json +- .github/FUNDING.yml +- .github/ISSUE_TEMPLATE/bug_report.yml +- .github/ISSUE_TEMPLATE/config.yml +- .github/actions/download-taglib/action.yml +- .github/actions/prepare-docker/action.yml +- .github/dependabot.yml +- .github/workflows/download-link-on-pr.yml +- .github/workflows/pipeline.yml +- .github/workflows/push-translations.yml +- .github/workflows/stale.yml +- .github/workflows/update-translations.yml +- .golangci.yml +- adapters/deezer/client.go +- adapters/deezer/client_auth.go +- adapters/deezer/client_auth_test.go +- adapters/deezer/client_test.go +- adapters/deezer/deezer.go +- adapters/deezer/deezer_suite_test.go +- adapters/deezer/deezer_test.go +- adapters/deezer/responses.go +- adapters/deezer/responses_test.go +- adapters/gotaglib/end_to_end_test.go +- adapters/gotaglib/gotaglib.go +- adapters/gotaglib/gotaglib_suite_test.go +- adapters/gotaglib/gotaglib_test.go +- adapters/lastfm/agent.go +- adapters/lastfm/agent_test.go +- adapters/lastfm/auth_router.go +- adapters/lastfm/client.go +- adapters/lastfm/client_test.go +- adapters/lastfm/lastfm_suite_test.go +- adapters/lastfm/responses.go +- adapters/lastfm/responses_test.go +- adapters/listenbrainz/agent.go +- adapters/listenbrainz/agent_test.go +- adapters/listenbrainz/auth_router.go +- adapters/listenbrainz/auth_router_test.go +- adapters/listenbrainz/client.go +- adapters/listenbrainz/client_test.go +- adapters/listenbrainz/listenbrainz_suite_test.go +- adapters/spotify/client.go +- adapters/spotify/client_test.go +- adapters/spotify/responses.go +- adapters/spotify/responses_test.go +- adapters/spotify/spotify.go +- adapters/spotify/spotify_suite_test.go +- adapters/taglib/end_to_end_test.go +- adapters/taglib/get_filename.go +- adapters/taglib/get_filename_win.go + +## How to Build & Test +**Setup:** `go mod tidy` +**Build:** `go build -v ./...` +**Test:** `go test -v ./...` + +## CI Configuration +### `.github/workflows/download-link-on-pr.yml` +``` +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, re +``` +### `.github/workflows/pipeline.yml` +``` +name: "Pipeline: Test, Lint, Build" +on: + push: + branches: + - master + tags: + - "v*" + pull_request: + branches: + - master + +concurrency: + group: ${{ startsWith(github.ref, 'refs/tags/v') && 'tag' || 'branch' }}-${{ github.ref }} + cancel-in-progress: true + +env: + CROSS_TAGLIB_VERSION: "2.2.0-1" + CGO_CFLAGS_ALLOW: "--define-prefix" + IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') && 'true' || 'false' }} + +jobs: + git-version: + name: Get version info + runs-on: ubuntu-latest + outputs: + git_tag: ${{ steps.git-version.outputs.GIT_TAG }} + git_sha: ${{ steps.git-version.outputs.GIT_SHA }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Show git version info + run: | + echo "git describe (dirty): $(git describe --dirty --always --tags)" + echo "git describe --tags: $(git describe --tags `git rev-list --tags --max-count=1`)" + echo "git tag: $(git tag --sort=-committerdate | head -n 1)" + echo "github_ref: $GITHUB_REF" + echo "github_head_sha: ${{ github.event.pull_request.head.sha }}" + git tag -l + - name: Determine git current SHA and latest tag + id: git-version + run: | + GIT_TAG=$(git tag --sort=-committerdate | head -n 1) + if [ -n "$GIT_TAG" ]; then + if [[ "$GITHUB_REF" != refs/tags/* ]]; then + GIT_TAG=${GIT_TAG}-SNAPSHOT + fi + +``` +### `.github/workflows/push-translations.sh` +``` +#!/bin/sh + +set -e + +I18N_DIR=resources/i18n + +# Normalize JSON for deterministic comparison: +# remove empty/null attributes, sort keys alphabetically +process_json() { + jq 'walk(if type == "object" then with_entries(select(.value != null and .value != "" and .value != [] and .value != {})) | to_entries | sort_by(.key) | from_entries else . end)' "$1" +} + +# Get list of all languages configured in the POEditor project +get_language_list() { + curl -s -X POST https://api.poeditor.com/v2/languages/list \ + -d api_token="${POEDITOR_APIKEY}" \ + -d id="${POEDITOR_PROJECTID}" +} + +# Extract language name from the language list JSON given a language code +get_language_name() { + lang_code="$1" + lang_list="$2" + echo "$lang_list" | jq -r ".result.languages[] | select(.code == \"$lang_code\") | .name" +} + +# Extract language code from a file path (e.g., "resources/i18n/fr.json" -> "fr") +get_lang_code() { + filepath="$1" + filename=$(basename "$filepath") + echo "${filename%.*}" +} + +# Export the current translation for a language from POEditor (v2 API) +export_language() { + lang_code="$1" + response=$(curl -s -X POST https://api.poeditor.com/v2/projects/export \ + -d api_token="${POEDITOR_APIKEY}" \ + -d id="${POEDITOR_PROJECTID}" \ + -d language="$lang_code" \ + -d type="key_value_json") + + url=$(echo "$response" | jq -r '.result.url') + if [ -z "$url" ] || [ "$url" = "null" ]; then + echo "Failed to export $lang_code: $response" >&2 + return 1 + fi + echo "$url" +} + +# Flatten ne +``` +### `.github/workflows/push-translations.yml` +``` +name: POEditor export + +on: + push: + branches: + - master + paths: + - 'resources/i18n/*.json' + +jobs: + push-translations: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'navidrome' }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 + + - name: Detect changed translation files + id: changed + run: | + CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- 'resources/i18n/*.json' | tr '\n' ' ') + echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT + echo "Changed translation files: $CHANGED_FILES" + + - name: Push translations to POEditor + if: ${{ steps.changed.outputs.files != '' }} + env: + POEDITOR_APIKEY: ${{ secrets.POEDITOR_APIKEY }} + POEDITOR_PROJECTID: ${{ secrets.POEDITOR_PROJECTID }} + run: | + .github/workflows/push-translations.sh ${{ steps.changed.outputs.files }} + +``` +### `.github/workflows/stale.yml` +``` +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@v6 + with: + process-only: 'issues, prs' + 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@v9 + 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 master +``` +### `Makefile` +``` +GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ') +NODE_VERSION=$(shell cat .nvmrc) +GO_BUILD_TAGS=netgo,sqlite_fts5 + +# Set global environment variables, required for most targets +export CGO_CFLAGS_ALLOW=--define-prefix +export ND_ENABLEINSIGHTSCOLLECTOR=false + +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`)-SNAPSHOT +else +GIT_SHA=source_archive +GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))-SNAPSHOT +endif + +SUPPORTED_PLATFORMS ?= linux/amd64,linux/arm64,linux/arm/v5,linux/arm/v6,linux/arm/v7,linux/386,linux/riscv64,darwin/amd64,darwin/arm64,windows/amd64,windows/386 +IMAGE_PLATFORMS ?= $(shell echo $(SUPPORTED_PLATFORMS) | tr ',' '\n' | grep "linux" | grep -v "arm/v5" | tr '\n' ',' | sed 's/,$$//') +PLATFORMS ?= $(SUPPORTED_PLATFORMS) +DOCKER_TAG ?= deluan/navidrome:develop + +# Taglib version to use in cross-compilation, from https://github.com/navidrome/cross-taglib +CROSS_TAGLIB_VERSION ?= 2.2.0-1 +GOLANGCI_LINT_VERSION ?= v2.10.0 + +UI_SRC_FILES := $(shell find ui -type f -not -path "ui/build/*" -not -path "ui/node_modules/*") + +setup: check_env download-deps install-golangci-lint 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 + npx foreman -j Procfi +``` +### `Dockerfile` +``` +FROM --platform=$BUILDPLATFORM ghcr.io/crazy-max/osxcross:14.5-debian AS osxcross + +######################################################################################################################## +### Build xx (original image: tonistiigi/xx) +FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:3.20 AS xx-build + +# v1.9.0 +ENV XX_VERSION=a5592eab7a57895e8d385394ff12241bc65ecd50 + +RUN apk add -U --no-cache git +RUN git clone https://github.com/tonistiigi/xx && \ + cd xx && \ + git checkout ${XX_VERSION} && \ + mkdir -p /out && \ + cp src/xx-* /out/ + +RUN cd /out && \ + ln -s xx-cc /out/xx-clang && \ + ln -s xx-cc /out/xx-clang++ && \ + ln -s xx-cc /out/xx-c++ && \ + ln -s xx-apt /out/xx-apt-get + +# xx mimics the original tonistiigi/xx image +FROM scratch AS xx +COPY --from=xx-build /out/ /usr/bin/ + +######################################################################################################################## +### Get TagLib +FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:3.20 AS taglib-build +ARG TARGETPLATFORM +ARG CROSS_TAGLIB_VERSION=2.2.0-1 +ENV CROSS_TAGLIB_RELEASES_URL=https://github.com/navidrome/cross-taglib/releases/download/v${CROSS_TAGLIB_VERSION}/ + +# wget in busybox can't follow redirects +RUN < Date: Sat, 28 Feb 2026 10:26:15 +0530 Subject: [PATCH 2/2] Delete GEMINI.md --- GEMINI.md | 464 ------------------------------------------------------ 1 file changed, 464 deletions(-) delete mode 100644 GEMINI.md diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 4044b8556..000000000 --- a/GEMINI.md +++ /dev/null @@ -1,464 +0,0 @@ -# Task: Fix Issue #5112 - -## Issue to Solve -**Title:** [Bug]: /share/img/ returns 404 for artists without artwork, triggers security tool false positives (CrowdSec, fail2ban) -**Number:** #5112 - -### I confirm that: - -- [x] 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 -- [x] I'm using the latest version (your issue may have been fixed already) - -### Version - -0.60.3 - -### Current Behavior - - When an artist has no artwork, the /share/img/{jwt} endpoint returns 404 Not Found. When a client displays a folder/artist list containing multiple artists without artwork, this generates a burst of concurrent 404 responses. Security tools running on the same host (CrowdSec http-probing scenario, fail2ban, WAFs) interpret this burst as an attack and ban the client's IP address — including legitimate users on the same network as the server. - -### Expected Behavior - -/share/img/{jwt} should return either: - - 204 No Content when no artwork is available for the resource, or - - a generic placeholder image (200 OK) - -A 404 is semantically valid but operationally harmful: it is indistinguishable from probing/scanning at the HTTP layer. - -### Steps To Reproduce - - 1. Set up Navidrome behind a reverse proxy with CrowdSec (or any security tool monitoring 404 rates) - 2. Have a music library where some artists have no embedded artwork - 3. Connect with a client that uses /share/img/ URLs to display artist images (e.g. Substreamer, "Folders" view) - 4. Open a view that loads multiple artist images simultaneously (Substreamer's "Folders" view) - -### Environment - -```markdown -- Navidrome version: 0.60.3 (34c6f12a) -- OS: Debian GNU/Linux 13 (trixie), aarch64 (Raspberry Pi 4) -- Client: Substreamer (Android WebView), "Folders" view -- Reverse proxy: Nginx with CrowdSec firewall bouncer -``` - -### How Navidrome is installed? - -Binary (from downloads page) - -### Configuration - -```toml - -``` - -### Relevant log output - -```shell - -``` - -### Anything else? - - My temporary workaround: - - Intercept 404s at the Nginx reverse proxy level: - - location /share/img/ { - proxy_pass http://127.0.0.1:4533; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_intercept_errors on; - error_page 404 = @noimage; - } - - location @noimage { - return 204; - } - - -### Code of Conduct - -- [x] I agree to follow Navidrome's Code of Conduct - -## Repository Info -**Repository:** navidrome/navidrome - -### Key Files -- .devcontainer/devcontainer.json -- .github/FUNDING.yml -- .github/ISSUE_TEMPLATE/bug_report.yml -- .github/ISSUE_TEMPLATE/config.yml -- .github/actions/download-taglib/action.yml -- .github/actions/prepare-docker/action.yml -- .github/dependabot.yml -- .github/workflows/download-link-on-pr.yml -- .github/workflows/pipeline.yml -- .github/workflows/push-translations.yml -- .github/workflows/stale.yml -- .github/workflows/update-translations.yml -- .golangci.yml -- adapters/deezer/client.go -- adapters/deezer/client_auth.go -- adapters/deezer/client_auth_test.go -- adapters/deezer/client_test.go -- adapters/deezer/deezer.go -- adapters/deezer/deezer_suite_test.go -- adapters/deezer/deezer_test.go -- adapters/deezer/responses.go -- adapters/deezer/responses_test.go -- adapters/gotaglib/end_to_end_test.go -- adapters/gotaglib/gotaglib.go -- adapters/gotaglib/gotaglib_suite_test.go -- adapters/gotaglib/gotaglib_test.go -- adapters/lastfm/agent.go -- adapters/lastfm/agent_test.go -- adapters/lastfm/auth_router.go -- adapters/lastfm/client.go -- adapters/lastfm/client_test.go -- adapters/lastfm/lastfm_suite_test.go -- adapters/lastfm/responses.go -- adapters/lastfm/responses_test.go -- adapters/listenbrainz/agent.go -- adapters/listenbrainz/agent_test.go -- adapters/listenbrainz/auth_router.go -- adapters/listenbrainz/auth_router_test.go -- adapters/listenbrainz/client.go -- adapters/listenbrainz/client_test.go -- adapters/listenbrainz/listenbrainz_suite_test.go -- adapters/spotify/client.go -- adapters/spotify/client_test.go -- adapters/spotify/responses.go -- adapters/spotify/responses_test.go -- adapters/spotify/spotify.go -- adapters/spotify/spotify_suite_test.go -- adapters/taglib/end_to_end_test.go -- adapters/taglib/get_filename.go -- adapters/taglib/get_filename_win.go - -## How to Build & Test -**Setup:** `go mod tidy` -**Build:** `go build -v ./...` -**Test:** `go test -v ./...` - -## CI Configuration -### `.github/workflows/download-link-on-pr.yml` -``` -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, re -``` -### `.github/workflows/pipeline.yml` -``` -name: "Pipeline: Test, Lint, Build" -on: - push: - branches: - - master - tags: - - "v*" - pull_request: - branches: - - master - -concurrency: - group: ${{ startsWith(github.ref, 'refs/tags/v') && 'tag' || 'branch' }}-${{ github.ref }} - cancel-in-progress: true - -env: - CROSS_TAGLIB_VERSION: "2.2.0-1" - CGO_CFLAGS_ALLOW: "--define-prefix" - IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') && 'true' || 'false' }} - -jobs: - git-version: - name: Get version info - runs-on: ubuntu-latest - outputs: - git_tag: ${{ steps.git-version.outputs.GIT_TAG }} - git_sha: ${{ steps.git-version.outputs.GIT_SHA }} - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Show git version info - run: | - echo "git describe (dirty): $(git describe --dirty --always --tags)" - echo "git describe --tags: $(git describe --tags `git rev-list --tags --max-count=1`)" - echo "git tag: $(git tag --sort=-committerdate | head -n 1)" - echo "github_ref: $GITHUB_REF" - echo "github_head_sha: ${{ github.event.pull_request.head.sha }}" - git tag -l - - name: Determine git current SHA and latest tag - id: git-version - run: | - GIT_TAG=$(git tag --sort=-committerdate | head -n 1) - if [ -n "$GIT_TAG" ]; then - if [[ "$GITHUB_REF" != refs/tags/* ]]; then - GIT_TAG=${GIT_TAG}-SNAPSHOT - fi - -``` -### `.github/workflows/push-translations.sh` -``` -#!/bin/sh - -set -e - -I18N_DIR=resources/i18n - -# Normalize JSON for deterministic comparison: -# remove empty/null attributes, sort keys alphabetically -process_json() { - jq 'walk(if type == "object" then with_entries(select(.value != null and .value != "" and .value != [] and .value != {})) | to_entries | sort_by(.key) | from_entries else . end)' "$1" -} - -# Get list of all languages configured in the POEditor project -get_language_list() { - curl -s -X POST https://api.poeditor.com/v2/languages/list \ - -d api_token="${POEDITOR_APIKEY}" \ - -d id="${POEDITOR_PROJECTID}" -} - -# Extract language name from the language list JSON given a language code -get_language_name() { - lang_code="$1" - lang_list="$2" - echo "$lang_list" | jq -r ".result.languages[] | select(.code == \"$lang_code\") | .name" -} - -# Extract language code from a file path (e.g., "resources/i18n/fr.json" -> "fr") -get_lang_code() { - filepath="$1" - filename=$(basename "$filepath") - echo "${filename%.*}" -} - -# Export the current translation for a language from POEditor (v2 API) -export_language() { - lang_code="$1" - response=$(curl -s -X POST https://api.poeditor.com/v2/projects/export \ - -d api_token="${POEDITOR_APIKEY}" \ - -d id="${POEDITOR_PROJECTID}" \ - -d language="$lang_code" \ - -d type="key_value_json") - - url=$(echo "$response" | jq -r '.result.url') - if [ -z "$url" ] || [ "$url" = "null" ]; then - echo "Failed to export $lang_code: $response" >&2 - return 1 - fi - echo "$url" -} - -# Flatten ne -``` -### `.github/workflows/push-translations.yml` -``` -name: POEditor export - -on: - push: - branches: - - master - paths: - - 'resources/i18n/*.json' - -jobs: - push-translations: - runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'navidrome' }} - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 2 - - - name: Detect changed translation files - id: changed - run: | - CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- 'resources/i18n/*.json' | tr '\n' ' ') - echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT - echo "Changed translation files: $CHANGED_FILES" - - - name: Push translations to POEditor - if: ${{ steps.changed.outputs.files != '' }} - env: - POEDITOR_APIKEY: ${{ secrets.POEDITOR_APIKEY }} - POEDITOR_PROJECTID: ${{ secrets.POEDITOR_PROJECTID }} - run: | - .github/workflows/push-translations.sh ${{ steps.changed.outputs.files }} - -``` -### `.github/workflows/stale.yml` -``` -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@v6 - with: - process-only: 'issues, prs' - 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@v9 - 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 master -``` -### `Makefile` -``` -GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ') -NODE_VERSION=$(shell cat .nvmrc) -GO_BUILD_TAGS=netgo,sqlite_fts5 - -# Set global environment variables, required for most targets -export CGO_CFLAGS_ALLOW=--define-prefix -export ND_ENABLEINSIGHTSCOLLECTOR=false - -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`)-SNAPSHOT -else -GIT_SHA=source_archive -GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))-SNAPSHOT -endif - -SUPPORTED_PLATFORMS ?= linux/amd64,linux/arm64,linux/arm/v5,linux/arm/v6,linux/arm/v7,linux/386,linux/riscv64,darwin/amd64,darwin/arm64,windows/amd64,windows/386 -IMAGE_PLATFORMS ?= $(shell echo $(SUPPORTED_PLATFORMS) | tr ',' '\n' | grep "linux" | grep -v "arm/v5" | tr '\n' ',' | sed 's/,$$//') -PLATFORMS ?= $(SUPPORTED_PLATFORMS) -DOCKER_TAG ?= deluan/navidrome:develop - -# Taglib version to use in cross-compilation, from https://github.com/navidrome/cross-taglib -CROSS_TAGLIB_VERSION ?= 2.2.0-1 -GOLANGCI_LINT_VERSION ?= v2.10.0 - -UI_SRC_FILES := $(shell find ui -type f -not -path "ui/build/*" -not -path "ui/node_modules/*") - -setup: check_env download-deps install-golangci-lint 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 - npx foreman -j Procfi -``` -### `Dockerfile` -``` -FROM --platform=$BUILDPLATFORM ghcr.io/crazy-max/osxcross:14.5-debian AS osxcross - -######################################################################################################################## -### Build xx (original image: tonistiigi/xx) -FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:3.20 AS xx-build - -# v1.9.0 -ENV XX_VERSION=a5592eab7a57895e8d385394ff12241bc65ecd50 - -RUN apk add -U --no-cache git -RUN git clone https://github.com/tonistiigi/xx && \ - cd xx && \ - git checkout ${XX_VERSION} && \ - mkdir -p /out && \ - cp src/xx-* /out/ - -RUN cd /out && \ - ln -s xx-cc /out/xx-clang && \ - ln -s xx-cc /out/xx-clang++ && \ - ln -s xx-cc /out/xx-c++ && \ - ln -s xx-apt /out/xx-apt-get - -# xx mimics the original tonistiigi/xx image -FROM scratch AS xx -COPY --from=xx-build /out/ /usr/bin/ - -######################################################################################################################## -### Get TagLib -FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:3.20 AS taglib-build -ARG TARGETPLATFORM -ARG CROSS_TAGLIB_VERSION=2.2.0-1 -ENV CROSS_TAGLIB_RELEASES_URL=https://github.com/navidrome/cross-taglib/releases/download/v${CROSS_TAGLIB_VERSION}/ - -# wget in busybox can't follow redirects -RUN <