mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
Merge branch 'master' into long-playlist-album-names/5068
This commit is contained in:
commit
4278799fc5
@ -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 \
|
||||
|
||||
@ -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": "",
|
||||
|
||||
2
.github/workflows/pipeline.yml
vendored
2
.github/workflows/pipeline.yml
vendored
@ -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' }}
|
||||
|
||||
|
||||
@ -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]
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
8
Makefile
8
Makefile
@ -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
|
||||
|
||||
@ -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
2
go.mod
@ -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
4
go.sum
@ -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
12
ui/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -42,3 +42,4 @@ export * from './playlistUtils.js'
|
||||
export * from './PathField.jsx'
|
||||
export * from './ParticipantsInfo'
|
||||
export * from './OverflowTooltip'
|
||||
export * from './useSearchRefocus'
|
||||
|
||||
50
ui/src/common/useSearchRefocus.js
Normal file
50
ui/src/common/useSearchRefocus.js
Normal 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])
|
||||
}
|
||||
84
ui/src/common/useSearchRefocus.test.js
Normal file
84
ui/src/common/useSearchRefocus.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
@ -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]),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user