54de0dbc52
* build: add sqlite_fts5 build tag to enable FTS5 support
* feat: add SearchBackend config option (default: fts)
* feat: add buildFTS5Query for safe FTS5 query preprocessing
* feat: add FTS5 search backend with config toggle, refactor legacy search
- Add searchExprFunc type and getSearchExpr() for backend selection
- Rename fullTextExpr to legacySearchExpr
- Add ftsSearchExpr using FTS5 MATCH subquery
- Update fullTextFilter in sql_restful.go to use configured backend
* feat: add FTS5 migration with virtual tables, triggers, and search_participants
Creates FTS5 virtual tables for media_file, album, and artist with
unicode61 tokenizer and diacritic folding. Adds search_participants
column, populates from JSON, and sets up INSERT/UPDATE/DELETE triggers.
* feat: populate search_participants in PostMapArgs for FTS5 indexing
* test: add FTS5 search integration tests
* fix: exclude FTS5 virtual tables from e2e DB restore
The restoreDB function iterates all tables in sqlite_master and
runs DELETE + INSERT to reset state. FTS5 contentless virtual tables
cannot be directly deleted from. Since triggers handle FTS5 sync
automatically, simply skip tables matching *_fts and *_fts_* patterns.
* build: add compile-time guard for sqlite_fts5 build tag
Same pattern as netgo: compilation fails with a clear error if
the sqlite_fts5 build tag is missing.
* build: add sqlite_fts5 tag to reflex dev server config
* build: extract GO_BUILD_TAGS variable in Makefile to avoid duplication
* fix: strip leading * from FTS5 queries to prevent "unknown special query" error
* feat: auto-append prefix wildcard to FTS5 search tokens for broader matching
Every plain search token now gets a trailing * appended (e.g., "love" becomes
"love*"), so searching for "love" also matches "lovelace", "lovely", etc.
Quoted phrases are preserved as exact matches without wildcards. Results are
ordered alphabetically by name/title, so shorter exact matches naturally
appear first.
* fix: clarify comments about FTS5 operator neutralization
The comments said "strip" but the code lowercases operators to
neutralize them (FTS5 operators are case-sensitive). Updated comments
to accurately describe the behavior.
* fix: use fmt.Sprintf for FTS5 phrase placeholders
The previous encoding used rune('0'+index) which silently breaks with
10+ quoted phrases. Use fmt.Sprintf for arbitrary index support.
* fix: validate and normalize SearchBackend config option
Normalize the value to lowercase and fall back to "fts" with a log
warning for unrecognized values. This prevents silent misconfiguration
from typos like "FTS", "Legacy", or "fts5".
* refactor: improve documentation for build tags and FTS5 requirements
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: convert FTS5 query and search backend normalization tests to DescribeTable format
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: add sqlite_fts5 build tag to golangci configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add UISearchDebounceMs configuration option and update related components
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: fall back to legacy search when SearchFullString is enabled
FTS5 is token-based and cannot match substrings within words, so
getSearchExpr now returns legacySearchExpr when SearchFullString
is true, regardless of SearchBackend setting.
* fix: add sqlite_fts5 build tag to CI pipeline and Dockerfile
* fix: add WHEN clauses to FTS5 AFTER UPDATE triggers
Added WHEN clauses to the media_file_fts_au, album_fts_au, and
artist_fts_au triggers so they only fire when FTS-indexed columns
actually change. Previously, every row update (e.g., play count, rating,
starred status) triggered an unnecessary delete+insert cycle in the FTS
shadow tables. The WHEN clauses use IS NOT for NULL-safe comparison of
each indexed column, avoiding FTS index churn for non-indexed updates.
* feat: add SearchBackend configuration option to data and insights components
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: enhance input sanitization for FTS5 by stripping additional punctuation and special characters
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add search_normalized column for punctuated name search (R.E.M., AC/DC)
Add index-time normalization and query-time single-letter collapsing to
fix FTS5 search for punctuated names. A new search_normalized column
stores concatenated forms of punctuated words (e.g., "R.E.M." → "REM",
"AC/DC" → "ACDC") and is indexed in FTS5 tables. At query time, runs of
consecutive single letters (from dot-stripping) are collapsed into OR
expressions like ("R E M" OR REM*) to match both the original tokens and
the normalized form. This enables searching by "R.E.M.", "REM", "AC/DC",
"ACDC", "A-ha", or "Aha" and finding the correct results.
* refactor: simplify isSingleUnicodeLetter to avoid []rune allocation
Use utf8.DecodeRuneInString to check for a single Unicode letter
instead of converting the entire string to a []rune slice.
* feat: define ftsSearchColumns for flexible FTS5 search column inclusion
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: update collapseSingleLetterRuns to return quoted phrases for abbreviations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: implement extractPunctuatedWords to handle artist/album names with embedded punctuation
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: implement extractPunctuatedWords to handle artist/album names with embedded punctuation
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: punctuated word handling to improve processing of artist/album names
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add CJK support for search queries with LIKE filters
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance FTS5 search by adding album version support and CJK handling
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: search configuration to use structured options
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance search functionality to support punctuation-only queries and update related tests
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
186 lines
6.2 KiB
Go
186 lines
6.2 KiB
Go
package consts
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/model/id"
|
|
)
|
|
|
|
const (
|
|
AppName = "navidrome"
|
|
|
|
DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL&_foreign_keys=on&synchronous=normal"
|
|
InitialSetupFlagKey = "InitialSetup"
|
|
FullScanAfterMigrationFlagKey = "FullScanAfterMigration"
|
|
LastScanErrorKey = "LastScanError"
|
|
LastScanTypeKey = "LastScanType"
|
|
LastScanStartTimeKey = "LastScanStartTime"
|
|
|
|
UIAuthorizationHeader = "X-ND-Authorization"
|
|
UIClientUniqueIDHeader = "X-ND-Client-Unique-Id"
|
|
JWTSecretKey = "JWTSecret"
|
|
JWTIssuer = "ND"
|
|
DefaultSessionTimeout = 48 * time.Hour
|
|
CookieExpiry = 365 * 24 * 3600 // One year
|
|
|
|
OptimizeDBSchedule = "@every 24h"
|
|
|
|
// 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
|
|
|
|
DevInitialUserName = "admin"
|
|
DevInitialName = "Dev Admin"
|
|
|
|
URLPathUI = "/app"
|
|
URLPathNativeAPI = "/api"
|
|
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
|
|
|
|
RequestThrottleBacklogLimit = 100
|
|
RequestThrottleBacklogTimeout = time.Minute
|
|
|
|
ServerReadHeaderTimeout = 3 * time.Second
|
|
|
|
DefaultInfoLanguage = "en"
|
|
|
|
ArtistInfoTimeToLive = 24 * time.Hour
|
|
AlbumInfoTimeToLive = 7 * 24 * time.Hour
|
|
UpdateLastAccessFrequency = time.Minute
|
|
UpdatePlayerFrequency = time.Minute
|
|
|
|
I18nFolder = "i18n"
|
|
ScanIgnoreFile = ".ndignore"
|
|
|
|
PlaceholderArtistArt = "artist-placeholder.webp"
|
|
PlaceholderAlbumArt = "album-placeholder.webp"
|
|
PlaceholderAvatar = "logo-192x192.png"
|
|
UICoverArtSize = 300
|
|
DefaultUIVolume = 100
|
|
DefaultUISearchDebounceMs = 200
|
|
|
|
DefaultHttpClientTimeOut = 10 * time.Second
|
|
|
|
DefaultListenBrainzBaseURL = "https://api.listenbrainz.org/1/"
|
|
DefaultListenBrainzArtistAlgorithm = "session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30"
|
|
DefaultListenBrainzTrackAlgorithm = "session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30"
|
|
|
|
DefaultScannerExtractor = "taglib"
|
|
DefaultWatcherWait = 5 * time.Second
|
|
Zwsp = string('\u200b')
|
|
)
|
|
|
|
// Prometheus options
|
|
const (
|
|
PrometheusDefaultPath = "/metrics"
|
|
PrometheusAuthUser = "navidrome"
|
|
)
|
|
|
|
// Cache options
|
|
const (
|
|
TranscodingCacheDir = "transcoding"
|
|
DefaultTranscodingCacheMaxItems = 0 // Unlimited
|
|
|
|
ImageCacheDir = "images"
|
|
DefaultImageCacheMaxItems = 0 // Unlimited
|
|
|
|
DefaultCacheSize = 100 * 1024 * 1024 // 100MB
|
|
DefaultCacheCleanUpInterval = 10 * time.Minute
|
|
)
|
|
|
|
const (
|
|
AlbumPlayCountModeAbsolute = "absolute"
|
|
AlbumPlayCountModeNormalized = "normalized"
|
|
)
|
|
|
|
const (
|
|
//DefaultAlbumPID = "album_legacy"
|
|
DefaultAlbumPID = "musicbrainz_albumid|albumartistid,album,albumversion,releasedate"
|
|
DefaultTrackPID = "musicbrainz_trackid|albumid,discnumber,tracknumber,title"
|
|
PIDAlbumKey = "PIDAlbum"
|
|
PIDTrackKey = "PIDTrack"
|
|
)
|
|
|
|
const (
|
|
InsightsIDKey = "InsightsID"
|
|
InsightsEndpoint = "https://insights.navidrome.org/collect"
|
|
InsightsUpdateInterval = 24 * time.Hour
|
|
InsightsInitialDelay = 30 * time.Minute
|
|
)
|
|
|
|
const (
|
|
PurgeMissingNever = "never"
|
|
PurgeMissingAlways = "always"
|
|
PurgeMissingFull = "full"
|
|
)
|
|
|
|
var (
|
|
DefaultDownsamplingFormat = "opus"
|
|
DefaultTranscodings = []struct {
|
|
Name string
|
|
TargetFormat string
|
|
DefaultBitRate int
|
|
Command string
|
|
}{
|
|
{
|
|
Name: "mp3 audio",
|
|
TargetFormat: "mp3",
|
|
DefaultBitRate: 192,
|
|
Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -f mp3 -",
|
|
},
|
|
{
|
|
Name: "opus audio",
|
|
TargetFormat: "opus",
|
|
DefaultBitRate: 128,
|
|
Command: "ffmpeg -i %s -ss %t -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 -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -",
|
|
},
|
|
}
|
|
)
|
|
|
|
var HTTPUserAgent = "Navidrome" + "/" + Version
|
|
|
|
var (
|
|
VariousArtists = "Various Artists"
|
|
// TODO This will be dynamic when using disambiguation
|
|
VariousArtistsID = "63sqASlAfjbGMuLP4JhnZU"
|
|
UnknownAlbum = "[Unknown Album]"
|
|
UnknownArtist = "[Unknown Artist]"
|
|
// TODO This will be dynamic when using disambiguation
|
|
UnknownArtistID = id.NewHash(strings.ToLower(UnknownArtist))
|
|
VariousArtistsMbzId = "89ad4ac3-39f7-470e-963a-56509c546377"
|
|
|
|
ArtistJoiner = " • "
|
|
)
|
|
|
|
var (
|
|
ServerStart = time.Now()
|
|
|
|
InContainer = func() bool {
|
|
// Check if the /.nddockerenv file exists
|
|
if _, err := os.Stat("/.nddockerenv"); err == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}()
|
|
)
|