mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Compare commits
9 Commits
8ea970b6a1
...
8b3a1800de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b3a1800de | ||
|
|
32e1313fc6 | ||
|
|
489d5c7760 | ||
|
|
0f1ede2581 | ||
|
|
395a36e10f | ||
|
|
0161a0958c | ||
|
|
d2a254077d | ||
|
|
1f3258ba08 | ||
|
|
d4152293ba |
2
Makefile
2
Makefile
@ -54,7 +54,7 @@ testall: test-race test-i18n test-js ##@Development Run Go and JS tests
|
||||
.PHONY: testall
|
||||
|
||||
test-race: ##@Development Run Go tests with race detector
|
||||
go test -tags netgo -race -shuffle=on ./...
|
||||
go test -tags netgo -race -shuffle=on $(PKG)
|
||||
.PHONY: test-race
|
||||
|
||||
test-js: ##@Development Run JS tests
|
||||
|
||||
@ -166,7 +166,7 @@ func (s *maintenanceService) getAffectedAlbumIDs(ctx context.Context, ids []stri
|
||||
if len(ids) > 0 {
|
||||
filters = squirrel.And{
|
||||
squirrel.Eq{"missing": true},
|
||||
squirrel.Eq{"id": ids},
|
||||
squirrel.Eq{"media_file.id": ids},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/core/agents"
|
||||
@ -22,8 +23,11 @@ var _ = Describe("Plugin Manager", func() {
|
||||
// but, as this is an integration test, we can't use configtest.SetupConfig() as it causes
|
||||
// data races.
|
||||
originalPluginsFolder := conf.Server.Plugins.Folder
|
||||
originalTimeout := conf.Server.DevPluginCompilationTimeout
|
||||
conf.Server.DevPluginCompilationTimeout = 2 * time.Minute
|
||||
DeferCleanup(func() {
|
||||
conf.Server.Plugins.Folder = originalPluginsFolder
|
||||
conf.Server.DevPluginCompilationTimeout = originalTimeout
|
||||
})
|
||||
conf.Server.Plugins.Enabled = true
|
||||
conf.Server.Plugins.Folder = testDataDir
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"artist": "Artista",
|
||||
"album": "Albuma",
|
||||
"path": "Fitxategiaren bidea",
|
||||
"libraryName": "Liburutegia",
|
||||
"genre": "Generoa",
|
||||
"compilation": "Konpilazioa",
|
||||
"year": "Urtea",
|
||||
@ -58,6 +59,7 @@
|
||||
"playCount": "Erreprodukzioak",
|
||||
"size": "Fitxategiaren tamaina",
|
||||
"name": "Izena",
|
||||
"libraryName": "Liburutegia",
|
||||
"genre": "Generoa",
|
||||
"compilation": "Konpilazioa",
|
||||
"year": "Urtea",
|
||||
@ -147,19 +149,26 @@
|
||||
"currentPassword": "Uneko pasahitza",
|
||||
"newPassword": "Pasahitz berria",
|
||||
"token": "Tokena",
|
||||
"lastAccessAt": "Azken sarbidea"
|
||||
"lastAccessAt": "Azken sarbidea",
|
||||
"libraries": "Liburutegiak"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "Aldaketak saioa hasten duzun hurrengoan islatuko dira"
|
||||
"name": "Aldaketak saioa hasten duzun hurrengoan islatuko dira",
|
||||
"libraries": "Hautatu erabiltzaile honentzat liburutegi jakinak, edo utzi hutsik defektuzko liburutegiak erabiltzeko"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Erabiltzailea sortu da",
|
||||
"updated": "Erabiltzailea eguneratu da",
|
||||
"deleted": "Erabiltzailea ezabatu da"
|
||||
},
|
||||
"validation": {
|
||||
"librariesRequired": "Gutxienez liburutegi bat hautatu behar da administratzaile ez diren erabiltzaileentzat"
|
||||
},
|
||||
"message": {
|
||||
"listenBrainzToken": "Idatzi zure ListenBrainz erabiltzailearen tokena",
|
||||
"clickHereForToken": "Egin klik hemen tokena lortzeko"
|
||||
"clickHereForToken": "Egin klik hemen tokena lortzeko",
|
||||
"selectAllLibraries": "Hautatu liburutegi guztiak",
|
||||
"adminAutoLibraries": "Administratzaileek automatikoki dute liburutegi guztietara sarbidea"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
@ -254,6 +263,7 @@
|
||||
"fields": {
|
||||
"path": "Bidea",
|
||||
"size": "Tamaina",
|
||||
"libraryName": "Liburutegia",
|
||||
"updatedAt": "Desagertze-data:"
|
||||
},
|
||||
"actions": {
|
||||
@ -263,6 +273,58 @@
|
||||
"notifications": {
|
||||
"removed": "Aurkitzen ez ziren fitxategiak kendu dira"
|
||||
}
|
||||
},
|
||||
"library": {
|
||||
"name": "Liburutegia |||| Liburutegiak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"path": "Fitxategiaren bidea",
|
||||
"remotePath": "Urruneko bidea",
|
||||
"lastScanAt": "Azken araketa",
|
||||
"songCount": "Abestiak",
|
||||
"albumCount": "Albumak",
|
||||
"artistCount": "Artistak",
|
||||
"totalSongs": "Abestiak",
|
||||
"totalAlbums": "Albumak",
|
||||
"totalArtists": "Artistak",
|
||||
"totalFolders": "Karpetak",
|
||||
"totalFiles": "Fitxategiak",
|
||||
"totalMissingFiles": "Fitxategiak faltan",
|
||||
"totalSize": "Tamaina guztira",
|
||||
"totalDuration": "Iraupena",
|
||||
"defaultNewUsers": "Defektuz erabiltzaile berrientzat",
|
||||
"createdAt": "Sortze-data",
|
||||
"updatedAt": "Eguneratze-data"
|
||||
},
|
||||
"sections": {
|
||||
"basic": "Oinarrizko informazioa",
|
||||
"statistics": "Estatistikak"
|
||||
},
|
||||
"actions": {
|
||||
"scan": "Arakatu liburutegia",
|
||||
"manageUsers": "Kudeatu erabiltzaileen sarbidea",
|
||||
"viewDetails": "Ikusi xehetasunak"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Liburutegia ondo sortu da",
|
||||
"updated": "Liburutegia ondo eguneratu da",
|
||||
"deleted": "Liburutegia ondo ezabatu da",
|
||||
"scanStarted": "Liburutegiaren araketa hasi da",
|
||||
"scanCompleted": "Liburutegiaren araketa amaitu da"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Liburutegiaren izena beharrezkoa da",
|
||||
"pathRequired": "Liburutegiaren bidea beharrezkoa da",
|
||||
"pathNotDirectory": "Liburutegiaren bidea direktorio bat izan behar da",
|
||||
"pathNotFound": "Ez da liburutegiaren bidea aurkitu",
|
||||
"pathNotAccessible": "Liburutegiaren bidea ez dago eskuragai",
|
||||
"pathInvalid": "Liburutegiaren bidea ez da baliozkoa"
|
||||
},
|
||||
"messages": {
|
||||
"deleteConfirm": "Ziur liburutegia ezabatu nahi duzula? Erlazionatutako datu guztiak eta erabiltzaileen sarbidea kenduko ditu.",
|
||||
"scanInProgress": "Araketa abian da…",
|
||||
"noLibrariesAssigned": "Ez da liburutegirik egokitu erabiltzaile honentzat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
@ -450,6 +512,12 @@
|
||||
},
|
||||
"menu": {
|
||||
"library": "Liburutegia",
|
||||
"librarySelector": {
|
||||
"allLibraries": "Liburutegi guztiak (%{count})",
|
||||
"multipleLibraries": "%{total} liburutegitik %{selected} hautatuta",
|
||||
"selectLibraries": "Hautatu liburutegiak",
|
||||
"none": "Bat ere ez"
|
||||
},
|
||||
"settings": "Ezarpenak",
|
||||
"version": "Bertsioa",
|
||||
"theme": "Itxura",
|
||||
|
||||
@ -12,7 +12,21 @@ const isAdmin = () => {
|
||||
const getSelectedLibraries = () => {
|
||||
try {
|
||||
const state = JSON.parse(localStorage.getItem('state'))
|
||||
return state?.library?.selectedLibraries || []
|
||||
const selectedLibraries = state?.library?.selectedLibraries || []
|
||||
const userLibraries = state?.library?.userLibraries || []
|
||||
|
||||
// Validate selected libraries against current user libraries
|
||||
const userLibraryIds = userLibraries.map((lib) => lib.id)
|
||||
const validatedSelection = selectedLibraries.filter((id) =>
|
||||
userLibraryIds.includes(id),
|
||||
)
|
||||
|
||||
// If user has only one library, return empty array (no filter needed)
|
||||
if (userLibraryIds.length === 1) {
|
||||
return []
|
||||
}
|
||||
|
||||
return validatedSelection
|
||||
} catch (err) {
|
||||
return []
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { cloneElement } from 'react'
|
||||
import { sanitizeListRestProps, TopToolbar } from 'react-admin'
|
||||
import { sanitizeListRestProps, TopToolbar, CreateButton } from 'react-admin'
|
||||
import LibraryScanButton from './LibraryScanButton'
|
||||
|
||||
const LibraryListActions = ({
|
||||
@ -23,6 +23,7 @@ const LibraryListActions = ({
|
||||
})}
|
||||
<LibraryScanButton fullScan={false} />
|
||||
<LibraryScanButton fullScan={true} />
|
||||
<CreateButton />
|
||||
</TopToolbar>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,18 +8,39 @@ const initialState = {
|
||||
export const libraryReducer = (previousState = initialState, payload) => {
|
||||
const { type, data } = payload
|
||||
switch (type) {
|
||||
case SET_USER_LIBRARIES:
|
||||
case SET_USER_LIBRARIES: {
|
||||
const newUserLibraryIds = data.map((lib) => lib.id)
|
||||
|
||||
// Validate and filter selected libraries to only include IDs that exist in new user libraries
|
||||
const validatedSelection = previousState.selectedLibraries.filter((id) =>
|
||||
newUserLibraryIds.includes(id),
|
||||
)
|
||||
|
||||
// Determine the final selection:
|
||||
// 1. If first time setting libraries (no previous user libraries), select all
|
||||
// 2. If user now has only one library, reset to empty (no filter needed)
|
||||
// 3. Otherwise, use validated selection (may be empty if all previous selections were invalid)
|
||||
let finalSelection
|
||||
if (
|
||||
previousState.selectedLibraries.length === 0 &&
|
||||
previousState.userLibraries.length === 0
|
||||
) {
|
||||
// First time: select all libraries
|
||||
finalSelection = newUserLibraryIds
|
||||
} else if (newUserLibraryIds.length === 1) {
|
||||
// Single library: reset selection (empty means "all accessible")
|
||||
finalSelection = []
|
||||
} else {
|
||||
// Multiple libraries: use validated selection
|
||||
finalSelection = validatedSelection
|
||||
}
|
||||
|
||||
return {
|
||||
...previousState,
|
||||
userLibraries: data,
|
||||
// If this is the first time setting user libraries and no selection exists,
|
||||
// default to all libraries
|
||||
selectedLibraries:
|
||||
previousState.selectedLibraries.length === 0 &&
|
||||
previousState.userLibraries.length === 0
|
||||
? data.map((lib) => lib.id)
|
||||
: previousState.selectedLibraries,
|
||||
selectedLibraries: finalSelection,
|
||||
}
|
||||
}
|
||||
case SET_SELECTED_LIBRARIES:
|
||||
return {
|
||||
...previousState,
|
||||
|
||||
186
ui/src/reducers/libraryReducer.test.js
Normal file
186
ui/src/reducers/libraryReducer.test.js
Normal file
@ -0,0 +1,186 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { libraryReducer } from './libraryReducer'
|
||||
import { SET_SELECTED_LIBRARIES, SET_USER_LIBRARIES } from '../actions'
|
||||
|
||||
describe('libraryReducer', () => {
|
||||
const mockLibraries = [
|
||||
{ id: '1', name: 'Music Library' },
|
||||
{ id: '2', name: 'Podcasts' },
|
||||
{ id: '3', name: 'Audiobooks' },
|
||||
]
|
||||
|
||||
const initialState = {
|
||||
userLibraries: [],
|
||||
selectedLibraries: [],
|
||||
}
|
||||
|
||||
describe('SET_USER_LIBRARIES', () => {
|
||||
it('should set user libraries and select all on first load', () => {
|
||||
const action = {
|
||||
type: SET_USER_LIBRARIES,
|
||||
data: mockLibraries,
|
||||
}
|
||||
|
||||
const result = libraryReducer(initialState, action)
|
||||
|
||||
expect(result.userLibraries).toEqual(mockLibraries)
|
||||
expect(result.selectedLibraries).toEqual(['1', '2', '3'])
|
||||
})
|
||||
|
||||
it('should reset selection to empty when user has only one library', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1', '2'],
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: SET_USER_LIBRARIES,
|
||||
data: [mockLibraries[0]], // Only one library now
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.userLibraries).toEqual([mockLibraries[0]])
|
||||
expect(result.selectedLibraries).toEqual([]) // Reset for single library
|
||||
})
|
||||
|
||||
it('should filter out invalid library IDs from selection', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1', '2', '3'],
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: SET_USER_LIBRARIES,
|
||||
data: [mockLibraries[0], mockLibraries[1]], // Only libraries 1 and 2 remain
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.userLibraries).toEqual([mockLibraries[0], mockLibraries[1]])
|
||||
expect(result.selectedLibraries).toEqual(['1', '2']) // Library 3 removed
|
||||
})
|
||||
|
||||
it('should keep valid selection when libraries change', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1'],
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: SET_USER_LIBRARIES,
|
||||
data: mockLibraries, // Same libraries
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.userLibraries).toEqual(mockLibraries)
|
||||
expect(result.selectedLibraries).toEqual(['1']) // Selection preserved
|
||||
})
|
||||
|
||||
it('should handle selection becoming empty after filtering invalid IDs', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1', '2'],
|
||||
}
|
||||
|
||||
const newLibraries = [{ id: '4', name: 'New Library' }]
|
||||
const action = {
|
||||
type: SET_USER_LIBRARIES,
|
||||
data: newLibraries,
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.userLibraries).toEqual(newLibraries)
|
||||
expect(result.selectedLibraries).toEqual([]) // All selected IDs were invalid
|
||||
})
|
||||
|
||||
it('should handle transition from multiple to single library with invalid selection', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['2', '3'], // User had libraries 2 and 3 selected
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: SET_USER_LIBRARIES,
|
||||
data: [mockLibraries[0]], // Now only has access to library 1
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.userLibraries).toEqual([mockLibraries[0]])
|
||||
expect(result.selectedLibraries).toEqual([]) // Reset for single library
|
||||
})
|
||||
|
||||
it('should handle empty library list', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1', '2'],
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: SET_USER_LIBRARIES,
|
||||
data: [],
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.userLibraries).toEqual([])
|
||||
expect(result.selectedLibraries).toEqual([]) // All selections filtered out
|
||||
})
|
||||
})
|
||||
|
||||
describe('SET_SELECTED_LIBRARIES', () => {
|
||||
it('should update selected libraries', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1'],
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: SET_SELECTED_LIBRARIES,
|
||||
data: ['2', '3'],
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.selectedLibraries).toEqual(['2', '3'])
|
||||
expect(result.userLibraries).toEqual(mockLibraries) // Unchanged
|
||||
})
|
||||
|
||||
it('should allow setting empty selection', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1', '2'],
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: SET_SELECTED_LIBRARIES,
|
||||
data: [],
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result.selectedLibraries).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown action', () => {
|
||||
it('should return previous state for unknown action', () => {
|
||||
const previousState = {
|
||||
userLibraries: mockLibraries,
|
||||
selectedLibraries: ['1'],
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: 'UNKNOWN_ACTION',
|
||||
data: null,
|
||||
}
|
||||
|
||||
const result = libraryReducer(previousState, action)
|
||||
|
||||
expect(result).toBe(previousState) // Same reference
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user