Merge branch 'master' into long-playlist-album-names/5068

This commit is contained in:
Thiago Sfredo 2026-02-21 19:21:14 -03:00 committed by GitHub
commit 4278799fc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 160 additions and 19 deletions

View File

@ -14,7 +14,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends ffmpeg
# Install TagLib from cross-taglib releases
ARG CROSS_TAGLIB_VERSION="2.1.1-1"
ARG CROSS_TAGLIB_VERSION="2.2.0-1"
ARG TARGETARCH
RUN DOWNLOAD_ARCH="linux-${TARGETARCH}" \
&& wget -q "https://github.com/navidrome/cross-taglib/releases/download/v${CROSS_TAGLIB_VERSION}/taglib-${DOWNLOAD_ARCH}.tar.gz" -O /tmp/cross-taglib.tar.gz \

View File

@ -8,7 +8,7 @@
// Options
"INSTALL_NODE": "true",
"NODE_VERSION": "v24",
"CROSS_TAGLIB_VERSION": "2.1.1-1"
"CROSS_TAGLIB_VERSION": "2.2.0-1"
}
},
"workspaceMount": "",

View File

@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: true
env:
CROSS_TAGLIB_VERSION: "2.1.1-2"
CROSS_TAGLIB_VERSION: "2.2.0-1"
CGO_CFLAGS_ALLOW: "--define-prefix"
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') && 'true' || 'false' }}

View File

@ -38,7 +38,7 @@ Before submitting a pull request, ensure that you go through the following:
### Commit Conventions
Each commit message must adhere to the following format:
```
<type>(scope): <description> - <issue number>
<type>(scope): <description>
[optional body]
```

View File

@ -28,7 +28,7 @@ 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.1.1-2
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

View File

@ -19,7 +19,7 @@ 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.1.1-2
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/*")
@ -201,8 +201,8 @@ docker-msi: ##@Cross_Compilation Build MSI installer for Windows
@du -h binaries/msi/*.msi
.PHONY: docker-msi
run-docker: ##@Development Run a Navidrome Docker image. Usage: make run-docker tag=<tag>
@if [ -z "$(tag)" ]; then echo "Usage: make run-docker tag=<tag>"; exit 1; fi
docker-run: ##@Development Run a Navidrome Docker image. Usage: make docker-run tag=<tag>
@if [ -z "$(tag)" ]; then echo "Usage: make docker-run tag=<tag>"; exit 1; fi
@TAG_DIR="tmp/$$(echo '$(tag)' | tr '/:' '_')"; mkdir -p "$$TAG_DIR"; \
VOLUMES="-v $(PWD)/$$TAG_DIR:/data"; \
if [ -f navidrome.toml ]; then \
@ -213,7 +213,7 @@ run-docker: ##@Development Run a Navidrome Docker image. Usage: make run-docker
fi; \
fi; \
echo "Running: docker run --rm -p 4533:4533 $$VOLUMES $(tag)"; docker run --rm -p 4533:4533 $$VOLUMES $(tag)
.PHONY: run-docker
.PHONY: docker-run
package: docker-build ##@Cross_Compilation Create binaries and packages for ALL supported platforms
@if [ -z `which goreleaser` ]; then echo "Please install goreleaser first: https://goreleaser.com/install/"; exit 1; fi

View File

@ -20,6 +20,7 @@ import (
"strings"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core/storage/local"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model/metadata"
@ -43,7 +44,7 @@ func (e extractor) Parse(files ...string) (map[string]metadata.Info, error) {
}
func (e extractor) Version() string {
return "go-taglib (TagLib 2.1.1 WASM)"
return "2.2 WASM"
}
func (e extractor) extractMetadata(filePath string) (*metadata.Info, error) {
@ -279,4 +280,7 @@ func init() {
local.RegisterExtractor("taglib", func(fsys fs.FS, baseDir string) local.Extractor {
return &extractor{fsys}
})
conf.AddHook(func() {
log.Debug("go-taglib version", "version", extractor{}.Version())
})
}

2
go.mod
View File

@ -7,7 +7,7 @@ replace (
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d
// Fork to implement raw tags support
go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260212150743-3f1b97cb0d1e
go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260221220301-2fab4903f48e
)
require (

4
go.sum
View File

@ -36,8 +36,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/deluan/go-taglib v0.0.0-20260212150743-3f1b97cb0d1e h1:pwx3kmHzl1N28coJV2C1zfm2ZF0qkQcGX+Z6BvXteB4=
github.com/deluan/go-taglib v0.0.0-20260212150743-3f1b97cb0d1e/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
github.com/deluan/go-taglib v0.0.0-20260221220301-2fab4903f48e h1:yQF3eOcI2dMMtxqdKXm3cgfYZlDcq9SUDDv90bsMj2I=
github.com/deluan/go-taglib v0.0.0-20260221220301-2fab4903f48e/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf h1:tb246l2Zmpt/GpF9EcHCKTtwzrd0HGfEmoODFA/qnk4=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55 h1:wSCnggTs2f2ji6nFwQmfwgINcmSMj0xF0oHnoyRSPe4=

12
ui/package-lock.json generated
View File

@ -4011,9 +4011,9 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@ -12709,9 +12709,9 @@
"license": "MIT"
},
"node_modules/workbox-build/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",

View File

@ -42,3 +42,4 @@ export * from './playlistUtils.js'
export * from './PathField.jsx'
export * from './ParticipantsInfo'
export * from './OverflowTooltip'
export * from './useSearchRefocus'

View File

@ -0,0 +1,50 @@
import { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
// Search field names used by SearchInput across different list views:
// - 'name': AlbumList, ArtistList, LibraryList, PlayerList, RadioList, UserList
// - 'title': SongList
// - 'q': PlaylistList
// If a new list view uses a different source field, add it here.
const SEARCH_FIELDS = ['name', 'title', 'q']
const getSearchValue = (filter) => {
for (const field of SEARCH_FIELDS) {
if (filter[field]) return filter[field]
}
return ''
}
export const useSearchRefocus = () => {
const location = useLocation()
const prevSearchValue = useRef(null)
useEffect(() => {
const params = new URLSearchParams(location.search)
const filterStr = params.get('filter') || '{}'
let filter = {}
try {
filter = JSON.parse(filterStr)
} catch (e) {
// Invalid JSON, ignore
}
const searchValue = getSearchValue(filter)
if (prevSearchValue.current && !searchValue) {
// Use requestAnimationFrame to wait for React to finish re-rendering
// after the URL change before focusing the input
requestAnimationFrame(() => {
// Selector depends on react-admin's internal class naming.
// If react-admin changes these class names, this will need updating.
const input = document.querySelector('[class*="RaSearchInput"] input')
if (input) {
input.focus()
}
})
}
prevSearchValue.current = searchValue
}, [location.search])
}

View File

@ -0,0 +1,84 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
import { renderHook } from '@testing-library/react-hooks'
import { useSearchRefocus } from './useSearchRefocus'
const mockLocation = { search: '' }
vi.mock('react-router-dom', () => ({
useLocation: () => mockLocation,
}))
describe('useSearchRefocus', () => {
let container
let rafCallbacks
beforeEach(() => {
rafCallbacks = []
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
rafCallbacks.push(cb)
return rafCallbacks.length
})
container = document.createElement('div')
container.innerHTML = `
<div class="RaSearchInput-input">
<input type="text" />
</div>
`
document.body.appendChild(container)
mockLocation.search = ''
})
afterEach(() => {
vi.restoreAllMocks()
document.body.removeChild(container)
})
const flushRAF = () => {
rafCallbacks.forEach((cb) => cb())
rafCallbacks = []
}
it('focuses the input when search filter is cleared', () => {
const input = container.querySelector('input')
const focusSpy = vi.spyOn(input, 'focus')
mockLocation.search = '?filter={"name":"test"}'
const { rerender } = renderHook(() => useSearchRefocus())
expect(focusSpy).not.toHaveBeenCalled()
mockLocation.search = '?filter={}'
rerender()
flushRAF()
expect(focusSpy).toHaveBeenCalledTimes(1)
})
it('does not focus if filter was already empty', () => {
const input = container.querySelector('input')
const focusSpy = vi.spyOn(input, 'focus')
mockLocation.search = '?filter={}'
const { rerender } = renderHook(() => useSearchRefocus())
mockLocation.search = '?filter={}'
rerender()
flushRAF()
expect(focusSpy).not.toHaveBeenCalled()
})
it('does not focus if filter value changed but not cleared', () => {
const input = container.querySelector('input')
const focusSpy = vi.spyOn(input, 'focus')
mockLocation.search = '?filter={"name":"test"}'
const { rerender } = renderHook(() => useSearchRefocus())
mockLocation.search = '?filter={"name":"other"}'
rerender()
flushRAF()
expect(focusSpy).not.toHaveBeenCalled()
})
})

View File

@ -7,6 +7,7 @@ import Menu from './Menu'
import AppBar from './AppBar'
import Notification from './Notification'
import useCurrentTheme from '../themes/useCurrentTheme'
import { useSearchRefocus } from '../common'
const useStyles = makeStyles({
root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) },
@ -17,6 +18,7 @@ const Layout = (props) => {
const queue = useSelector((state) => state.player?.queue)
const classes = useStyles({ addPadding: queue.length > 0 })
const dispatch = useDispatch()
useSearchRefocus()
const keyHandlers = {
TOGGLE_MENU: useCallback(() => dispatch(toggleSidebar()), [dispatch]),