* ci(windows): add skeleton go-windows job (compile-only smoke test)
* ci(windows): fix comment to reference Task 7 not Task 6
* ci(windows): harden PATH visibility and set explicit bash shell
* ci(windows): enable full go test suite and ndpgen check
* test(gotaglib): skip Unix-only permission tests on Windows
* test(lyrics): skip Windows-incompatible tests
* test(utils): skip Windows-incompatible tests
* test(mpv): skip Windows-incompatible playback tests
Skip 3 subprocess-execution tests that rely on Unix-style mpv
invocation; .bat output includes \r-terminated lines that break
argument parsing (#TBD-mpv-windows).
* test(storage): skip Windows-incompatible tests
Skip relative-path test where filepath.Join uses backslash but the
storage implementation returns a forward-slash URL path
(#TBD-path-sep-storage).
* test(storage/local): skip Windows-incompatible tests
Skip 13 tests that fail because url.Parse("file://" + windowsPath)
treats the drive letter colon as an invalid port; also skip the
Windows drive-letter path test that exposes a backslash vs
forward-slash normalisation bug (#TBD-path-sep-storage-local).
* test(playlists): skip Windows-incompatible tests
* test(model): skip Windows-incompatible tests
* test(model/metadata): skip Windows-incompatible tests
* test(core): skip Windows-incompatible tests
AbsolutePath uses filepath.Join which produces OS-native path separators;
skip the assertion test on Windows until the production code is fixed
(#TBD-path-sep-core).
* test(artwork): skip Windows-incompatible tests
Artwork readers produce OS-native path separators on Windows while tests
assert forward-slash paths; skip 11 affected tests pending a fix in
production code (#TBD-path-sep-artwork).
* test(persistence): skip Windows-incompatible tests
Skip flaky timestamp comparison (#TBD-flake-persistence) and path-separator
real-bugs (#TBD-path-sep-persistence) in FolderRepository.GetFolderUpdateInfo
which uses filepath.Clean/os.PathSeparator converting stored forward-slash paths
to backslashes on Windows.
* test(scanner): skip Windows-incompatible tests
Skip symlink tests (Unix-assumption), ndignore path-separator bugs
(#TBD-path-sep-scanner) in processLibraryEvents/resolveFolderPath where
filepath.Rel/filepath.Split return backslash paths incompatible with fs.FS
forward-slash expectations, error message mismatch on Windows, and file
format upgrade detection (#TBD-path-sep-scanner).
* test(plugins): skip Windows-incompatible tests
Add //go:build !windows tags to test files that reference the suite
bootstrap (testManager, testdataDir, createTestManager) which is only
compiled on non-Windows. Add a Windows-only suite stub that skips all
specs via BeforeEach to prevent [build failed] on Windows CI.
* test(server): skip Windows-incompatible tests
Skip createUnixSocketFile tests that rely on Unix file permission bits
(chmod/fchmod) which are not supported on Windows.
* test(nativeapi): skip Windows-incompatible tests
Skip the i18n JSON validation test that uses filepath.Join to build
embedded-FS paths; filepath.Join produces backslashes on Windows which
breaks fs.Open (embedded FS always uses forward slashes).
* test(e2e): skip Windows-incompatible tests
On Windows, SQLite holds file locks that prevent the Ginkgo TempDir
DeferCleanup from deleting the DB file. Register an explicit db.Close
DeferCleanup (LIFO before TempDir cleanup) on Windows so the file lock
is released before the temp directory is removed.
* test(windows): fix e2e AfterSuite and skip remaining scanner path test
* test(scanner): skip another Windows path-sep test (#TBD-path-sep-scanner)
* test(subsonic): skip timing-flaky test on Windows (#TBD-flake-time-resolution-subsonic)
* test(scanner): skip 'detects file moved to different folder' on Windows
* test(scanner): consolidate 'Library changes' Windows skips into BeforeEach
* test(scanner): close DB before TempDir cleanup to fix Windows file lock
* test(scanner): skip ScanFolders suite on Windows instead of closing shared DB
* ci: retrigger for Windows soak run 2/3
* ci: retrigger for Windows soak run 3/3
* ci: retrigger for Windows soak run 3/3 (take 2)
* test(scanner): skip Multi-Library suite on Windows (SQLite file lock)
* ci(windows): promote go-windows to blocking status check
* test(plugins): run platform-neutral specs on Windows, drop blanket Skip
* test(windows): make tests cross-platform instead of skipping
- subsonic: back-date submissionTime baseline by 1s so
BeTemporally(">") passes under millisecond clock resolution
- persistence: sleep briefly between Put calls so UpdatedAt is
strictly after CreatedAt on low-resolution clocks
- utils/files: close tempFile before os.Remove so the test works on
Windows (where an open handle holds a file lock)
- tests.TempFile: close the handle before returning; metadata tests
no longer leak the open file into Ginkgo's TempDir cleanup
Resolves Copilot review comments on #5380.
* test(tests): add SkipOnWindows helper to reduce boilerplate
Introduces tests.SkipOnWindows(reason) that wraps the 3-line
runtime.GOOS guard pattern used in every Windows-skipped spec.
* test(adapters): use tests.SkipOnWindows helper
* test(core): use tests.SkipOnWindows helper
* test(model): use tests.SkipOnWindows helper
* test(persistence): use tests.SkipOnWindows helper
* test(scanner): use tests.SkipOnWindows helper
* test(server): use tests.SkipOnWindows helper
* test(plugins): run pure-Go unit tests on Windows
config_validation_test, manager_loader_test, and migrate_test have no
WASM/exec dependencies and don't rely on the make-built test plugins
from plugins_suite_test.go. Let them run on Windows too.
* fix(subsonic): always emit required `created` field on AlbumID3
Strict OpenSubsonic clients (e.g. Navic via dev.zt64.subsonic) reject
search3/getAlbum/getAlbumList2 responses that omit the `created` field,
which the spec marks as required. Navidrome was dropping it whenever
the album's CreatedAt was zero.
Root cause was threefold:
1. buildAlbumID3/childFromAlbum conditionally emitted `created`, so a
zero CreatedAt became a missing JSON key.
2. ToAlbum's `older()` helper treated a zero BirthTime as the minimum,
so a single track with missing filesystem birth time could poison
the album aggregation.
3. phase_1_folders' CopyAttributes copied `created_at` from the previous
album row unconditionally, propagating an already-zero value forward
on every metadata-driven album ID change. Since sql_base_repository
drops `created_at` on UPDATE, a poisoned row could never self-heal.
Fixes:
- Always emit `created`, falling back to UpdatedAt/ImportedAt when
CreatedAt is zero. Adds albumCreatedAt() helper used by both
buildAlbumID3 and childFromAlbum.
- Guard `older()` against a zero second argument.
- Skip the CopyAttributes call in phase_1_folders when the previous
album's created_at is zero, so the freshly-computed value survives.
- New migration backfills existing broken rows from media_file.birth_time
(falling back to updated_at).
Tested against a real DB: repaired 605/6922 affected rows, no side
effects on healthy rows.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): return albumCreatedAt by value to avoid heap escape
Returning *time.Time from albumCreatedAt caused Go escape analysis to
move the entire model.Album parameter to the heap, since the returned
pointer aliased a field of the value receiver. For hot endpoints like
getAlbumList2 and search3, this meant one full-struct heap allocation
per album result.
Return time.Time by value and let callers wrap it with gg.P() to take
the address locally. Only the small time.Time value escapes; the
model.Album struct stays on the stack. Also corrects the doc comment
to reflect the actual guarantee ("best-effort" rather than "non-zero"),
matching the test case that exercises the all-zero fallback.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(artwork): fallback mediafile cover art to disc artwork before album
Changed the mediafile cover art fallback chain to go through disc artwork
before album artwork (mediafile → disc → album). Previously, mediafiles
without embedded art fell back directly to album cover, bypassing any
disc-specific artwork. Renamed AlbumCoverArtID() to DiscCoverArtID() to
encapsulate the disc-vs-album decision in a single method, used by both
CoverArtID() and the mediafile artwork reader.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(artwork): fix cache invalidation for mediafile and album cover art
Include imagesUpdatedAt from album folders in the mediafile artwork
reader's cache key, so that when a cover image file changes on disk
(without audio metadata changes) the mediafile cache properly
invalidates. Also include CoverArtPriority unconditionally in the album
artwork reader's cache key hash, so that changing the priority order
with external services disabled correctly invalidates the album cache.
* fix(artwork): skip disc artwork resolution for single-disc albums
Single-disc albums with DiscNumber=1 were unnecessarily routed through
discArtworkReader, which does extra DB queries only to fall through to
album art anyway. Now only multi-disc albums use the disc fallback path.
* refactor(artwork): restore AlbumCoverArtID as a separate method
Extract AlbumCoverArtID back out of DiscCoverArtID so the single-disc
fallback path in reader_mediafile can reference it by name instead of
inlining the artwork ID construction.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement transcode decision logic and codec handling for media files
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): update codec limitation structure and decision logic for improved clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcoding): update bitrate handling to use kilobits per second (kbps) across transcode decision logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): simplify container alias handling in matchesContainer function
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcoding): enforce POST method for GetTranscodeDecision and handle non-POST requests
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(transcoding): add enums for protocol, comparison operators, limitations, and codec profiles in transcode decision logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): streamline limitation checks and applyLimitation logic for improved readability and maintainability
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): replace strings.EqualFold with direct comparison for protocol and limitation checks
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): rename token methods to CreateTranscodeParams and ParseTranscodeParams for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance logging for transcode decision process and client info conversion
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): rename TranscodeDecision to Decider and update related methods for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance transcoding config lookup logic for audio codecs
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance transcoding options with sample rate support and improve command handling
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): add bit depth support for audio transcoding and enhance related logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance AAC command handling and support for audio channels in streaming
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): streamline transcoding logic by consolidating stream parameter handling and enhancing alias mapping
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): update default command handling and add codec support for transcoding
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: implement noopDecider for transcoding decision handling in tests
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: address review findings for OpenSubsonic transcoding PR
Fix multiple issues identified during code review of the transcoding
extension: add missing return after error in shared stream handler
preventing nil pointer panic, replace dead r.Body nil check with
MaxBytesReader size limit, distinguish not-found from other DB errors,
fix bpsToKbps integer truncation with rounding, add "pcm" to
isLosslessFormat for consistency with model.IsLossless(), add
sampleRate/bitDepth/channels to streaming log, fix outdated test
comment, and add tests for conversion functions and GetTranscodeStream
parameter passing.
* feat(transcoding): add sourceUpdatedAt to decision and validate transcode parameters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: small issues
Updated mock AAC transcoding command to use the new default (ipod with
fragmented MP4) matching the migration, ensuring tests exercise the same
buildDynamicArgs code path as production. Improved archiver test mock to
match on the whole StreamRequest struct instead of decomposing fields,
making it resilient to future field additions. Added named constants for
JWT claim keys in the transcode token and wrapped ParseTranscodeParams
errors with ErrTokenInvalid for consistency. Documented the IsLossless
BitDepth fallback heuristic as temporary until Codec column is populated.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcoding): adapt transcode claims to struct-based auth.Claims
Updated transcode token handling to use the struct-based auth.Claims
introduced on master, replacing the previous map[string]any approach.
Extended auth.Claims with transcoding-specific fields (MediaID, DirectPlay,
UpdatedAt, Channels, SampleRate, BitDepth) and added float64 fallback in
ClaimsFromToken for numeric claims that lose their Go type during JWT
string serialization. Also added the missing lyrics parameter to all
subsonic.New() calls in test files.
* feat(model): add ProbeData field and UpdateProbeData repository method
Add probe_data TEXT column to media_file for caching ffprobe results.
Add UpdateProbeData to MediaFileRepository interface and implementations.
Use hash:"ignore" tag so probe data doesn't affect MediaFile fingerprints.
* feat(ffmpeg): add ProbeAudioStream for authoritative audio metadata
Add ProbeAudioStream to FFmpeg interface, using ffprobe to extract
codec, profile, bitrate, sample rate, bit depth, and channels.
Parse bits_per_raw_sample as fallback for FLAC/ALAC bit depth.
Normalize "unknown" profile to empty string.
All parseProbeOutput tests use real ffprobe JSON from actual files.
* feat(transcoding): integrate ffprobe into transcode decisions
Add ensureProbed to probe media files on first transcode decision,
caching results in probe_data. Build SourceStream from probe data
with fallback to tag-based metadata.
Refactor decision logic to pass StreamDetails instead of MediaFile,
enabling codec profile limitations (e.g., audioProfile) to use
probe data. Add normalizeProbeCodec to map ffprobe codec names
(dsd_lsbf_planar, pcm_s16le) to internal names (dsd, pcm).
NewDecider now accepts ffmpeg.FFmpeg; wire_gen.go regenerated.
* feat(transcoding): add DevEnableMediaFileProbe config flag
Add DevEnableMediaFileProbe (default true) to allow disabling ffprobe-
based media file probing as a safety fallback. When disabled, the
decider uses tag-based metadata from the scanner instead.
* test(transcode): add ensureProbed unit tests
Test probing when ProbeData is empty, skipping when already set,
error propagation from ffprobe, and DevEnableMediaFileProbe flag.
* refactor(ffmpeg): use command constant and select_streams for ProbeAudioStream
Move ffprobe arguments to a probeAudioStreamCmd constant, following the
same pattern as extractImageCmd and probeCmd. Add -select_streams a:0 to
only probe the first audio stream, avoiding unnecessary parsing of video
and artwork streams. Derive the ffprobe binary path safely using
filepath.Dir/Base instead of replacing within the full path string.
* refactor(transcode): decouple transcode token claims from auth.Claims
Remove six transcode-specific fields (MediaID, DirectPlay, UpdatedAt,
Channels, SampleRate, BitDepth) from auth.Claims, which is shared with
session and share tokens. Transcode tokens are signed parameter-passing
tokens, not authentication tokens, so coupling them to auth created
misleading dependencies.
The transcode package now owns its own JWT claim serialization via
Decision.toClaimsMap() and paramsFromToken(), using generic
auth.EncodeToken/DecodeAndVerifyToken wrappers that keep TokenAuth
encapsulated. Wire format (JWT claim keys) is unchanged, so in-flight
tokens remain compatible.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcode): simplify code after review
Extract getIntClaim helper to eliminate repeated int/int64/float64 JWT
claim extraction pattern in paramsFromToken and ClaimsFromToken. Rewrite
checkIntLimitation as a one-liner delegating to applyIntLimitation.
Return probe result from ensureProbed to avoid redundant JSON round-trip.
Extract toResponseStreamDetails helper and mediaTypeSong constant in
the API layer, and use transcode.ProtocolHTTP constant instead of
hardcoded string.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ffmpeg): enhance bit_rate parsing logic for audio streams
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcode): improve code review findings across transcode implementation
- Fix parseProbeData to return nil on JSON unmarshal failure instead of
a zero-valued struct, preventing silent degradation of source stream details
- Use probe-resolved codec for lossless detection in buildSourceStream
instead of the potentially stale scanner data
- Remove MediaFile.IsLossless() (dead code) and consolidate lossless
detection in isLosslessFormat(), using codec name only — bit depth is
not reliable since lossy codecs like ADPCM report non-zero values
- Add "wavpack" to lossless codec list (ffprobe codec_name for WavPack)
- Guard bpsToKbps against negative input values
- Fix misleading comment in buildTemplateArgs about conditional injection
- Avoid leaking internal error details in Subsonic API responses
- Add missing test for ErrNotFound branch in GetTranscodeDecision
- Add TODO for hardcoded protocol in toResponseStreamDetails
* refactor(transcode): streamline transcoding command lookup and format resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(transcode): implement server-side transcoding override for player formats
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcode): honor bit depth and channel constraints in transcoding selection
selectTranscodingOptions only checked sample rate when deciding whether
same-format transcoding was needed, ignoring requested bit depth and
channel reductions. This caused the streamer to return raw audio when
the transcode decision requested downmix or bit-depth conversion.
* refactor(transcode): unify streaming decision engine via MakeDecision
Move transcoding decision-making out of mediaStreamer and into the
subsonic Stream/Download handlers, using transcode.Decider.MakeDecision
as the single decision engine. This eliminates selectTranscodingOptions
and the mismatch between decision and streaming code paths (decision
used LookupTranscodeCommand with built-in fallbacks, while streaming
used FindByFormat which only checked the DB).
- Add DecisionOptions with SkipProbe to MakeDecision so the legacy
streaming path never calls ffprobe
- Add buildLegacyClientInfo to translate legacy stream params (format,
maxBitRate, DefaultDownsamplingFormat) into a synthetic ClientInfo
- Add resolveStreamRequest on the subsonic Router to resolve legacy
params into a fully specified StreamRequest via MakeDecision
- Simplify DoStream to a dumb executor that receives pre-resolved params
- Remove selectTranscodingOptions entirely
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcode): move MediaStreamer into core/transcode and unify StreamRequest
Moved MediaStreamer, Stream, TranscodingCache and related types from
core/media_streamer.go into core/transcode/, eliminating the duplicate
StreamRequest type. The transcode.StreamRequest now carries all fields
(ID, Format, BitRate, SampleRate, BitDepth, Channels, Offset) and
ResolveStream returns a fully-populated value, removing manual field
copying at every call site. Also moved buildLegacyClientInfo into the
transcode package alongside ResolveStream, and unexported
ParseTranscodeParams since it was only used internally by
ValidateTranscodeParams.
* refactor(transcode): rename Decider methods and unexport Params type
Rename ResolveStream → ResolveRequest and ValidateTranscodeParams →
ResolveRequestFromToken for clarity and consistency. The new
ResolveRequestFromToken returns a StreamRequest directly (instead of
the intermediate Params type), eliminating manual Params→StreamRequest
conversion in callers. Unexport Params to params since it is now only
used internally for JWT token parsing.
* test(transcode): remove redundant tests and use constants
Remove tests that duplicate coverage from integration-level tests
(toClaimsMap, paramsFromToken round-trips, applyServerOverride direct
call, duplicate 410 handler test). Replace raw "http" strings with
ProtocolHTTP constant. Consolidate lossy -sample_fmt tests into
DescribeTable.
* refactor(transcode): split oversized files into focused modules
Split transcode.go and transcode_test.go into focused files by concern:
- decider.go: decision engine (MakeDecision, direct play/transcode evaluation, probe)
- token.go: JWT token encode/decode (params, toClaimsMap, paramsFromToken, CreateTranscodeParams, ResolveRequestFromToken)
- legacy_client.go: legacy Subsonic bridge (buildLegacyClientInfo, ResolveRequest)
- codec_test.go: isLosslessFormat and normalizeProbeCodec tests
- token_test.go: token round-trip and ResolveRequestFromToken tests
Moved the Decider interface from types.go to decider.go to keep it near
its implementation, and cleaned up types.go to contain only pure type
definitions and constants. No public API changes.
* refactor(transcode): reorder parameters in applyServerOverride function
Signed-off-by: Deluan <deluan@navidrome.org>
* test(e2e): add NewTestStream function and implement spyStreamer for testing
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): append album version to album names in Subsonic API responses
Add AppendAlbumVersion config option (default: true) that appends the
album version tag to album names in Subsonic API responses, similar to
how AppendSubtitle works for track titles. This affects album names in
childFromAlbum and buildAlbumID3 responses.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): append album version to media file album names in Subsonic API
Add FullAlbumName() to MediaFile that appends the album version tag,
mirroring the Album.FullName() behavior. Use it in childFromMediaFile
and fakePath to ensure media file responses also show the album version.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): use len() check for album version tag to prevent panic on empty slice
Use len(tags) > 0 instead of != nil to safely guard against empty
slices when accessing the first element of the album version tag.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): use FullName in buildAlbumDirectory and deduplicate FullName calls
Apply album.FullName() in buildAlbumDirectory (getMusicDirectory) so
album names are consistent across all Subsonic endpoints. Also compute
al.FullName() once in childFromAlbum to avoid redundant calls.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: use len() check in MediaFile.FullTitle() to prevent panic on empty slice
Apply the same safety improvement as FullAlbumName() and Album.FullName()
for consistency.
Signed-off-by: Deluan <deluan@navidrome.org>
* test: add tests for Album.FullName, MediaFile.FullTitle, and MediaFile.FullAlbumName
Cover all cases: config enabled/disabled, tag present, tag absent, and
empty tag slice.
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>