Commit Graph

10 Commits

Author SHA1 Message Date
Deluan Quintão ae1e0ddb11 feat(subsonic): implement OpenSubsonic Transcoding extension (#4990)
* 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>
2026-03-08 23:57:49 -04:00
Deluan bbb3182bc9 refactor(server): remove ffmpeg unused code 2024-10-20 19:35:16 -04:00
Deluan b67d1c0830 Show taglib and ffmpeg versions in the log 2024-02-18 12:52:06 -05:00
Kendall Garner 814161d78d Add OS Lyrics extension (#2656)
* draft commit

* time to fight pipeline

* round 2 changes

* remove unnecessary line

* fight taglib. again

* make taglib work again???

* add id3 tags

* taglib 1.12 vs 1.13

* use int instead for windows

* store as json now

* add migration, more tests

* support repeated line, multiline

* fix ms and support .m, .mm, .mmm

* address some concerns, make cpp a bit safer

* separate responses from model

* remove [:]

* Add trace log

* Try to unblock pipeline

* Fix merge errors

* Fix SIGSEGV error (proper handling of empty frames)

* Add fallback artist/title to structured lyrics

* Rename conflicting named vars

* Fix tests

* Do we still need ffmpeg in the pipeline?

* Revert "Do we still need ffmpeg in the pipeline?"

Yes we do.

This reverts commit 87df7f6df79bccee83f48c4b7a8118a7636a5e66.

* Does this passes now, with a newer ffmpeg version?

* Revert "Does this passes now, with a newer ffmpeg version?"

No, it does not :(

This reverts commit 372eb4b0ae05d9ffe98078e9bc4e56a9b2921f32.

* My OCD made me do it :P

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2023-12-27 20:20:29 -05:00
Deluan 812dc2090f Add support for timeOffset in /stream endpoint 2023-12-02 13:10:36 -05:00
Matthias Schmidt 1b16e1140f Jukebox mode (#2289)
* Adding cache directory to ignore-list

* Adding jukebox-related config options

* Adding DevEnableJukebox config option pls. dummy server

* Adding types and routers

* Now without panic

* First draft on parsing the action

* Some cleanups

* Adding playback server

* Verify audio device configuration

* Adding debug-build target to have full symbol support

* Adding beep sound library pls some example code. Not working yet

* Play a fixed mp3 on any interface access for testing purposes

* Put action code into separate file, adding stringer, more debug output, prepare structs, validation

* Put action parameter parser code where it belongs

* Have a single Action transporting all information

* User fmt.Errorf for error-generation

* Adding wide playback interface

* Use action map for parsing, stringer instead switch stmt.

* Use but only one switch case and direct dispatch, refactoring

* Add error handling and pushing to client

* send decent errormessage, no internal server error

* Adding playback devices slice and load it from config

* Combine config-verification and structure init

* Return user-specific device

* Separate playback server from device

* Use dataStore to retrieve mediafile by id

* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now

* WIP: set, start and stop work on one single song. More to come

* Dont need to wait for the end

* Merge jukebox_action.go into jukebox.go

* Remove getParameterAsInt64(). Use existing requiredParamInt() instead

* Dont need to call newFailure() explicitly

* Remove int64, use int instead.

* Add and set action now accept multiple ids

* Kickout copy of childFromMediaFile(). It is not needed here.

* Refactoring devices and playbackServer

* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int

* Now we have a position and playing status

* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug

* Now with volume control

* Start working the queue

* Remove user from device interface

* Rename function GetDevice -> GetDeviceForUser to make intention clearer

* Have a nice stringer for the queue

* User Prepared boolean for now to allow pause/unpause

* Skipping works, but without offsets

* Make ChildFromMediaFile public to be used in jukebox get() implementation

* Return position in seconds and implement offset-skip in seconds

* Default offset to 0

* Adding a simple setGain implementation

* Prepare for transcoding AAC

* WIP: transcode to WAV to use beeps wav decoder. Not done yet.

* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.

* Use FLAC as intermediate format to play Apple AAC

* A bit of cleanup

* Catching the end-of-stream event for further reactions

* Have a trackSwitching goroutine waiting on channel when track ends

* Move decoder code into own file. Restructure code a bit

* Now with going on to play the next song in the playlist

* Adding shuffle feature

* Implementing remove action

* Cleanup code

* Remove templates for ffmpeg mp3 generation. Not needed anymore.

* Adding some documentation

* Check whether offset into track is in range. Fixing potential remove track bug. Documentation

* Make golangci-lint happy: handling return values

* Adding test suite and example dummy for playback package

* Adding some basic queue tests

* Only use Jukebox.Enabled config option

* Adding stream closing handling

* Pass context.Context to all PlaybackDevice methods

* Remove unneeded function

* Correct spelling

* Reduce visibility of ChildFromMediaFile

* Decomplicate action-parsing

* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.

* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.

* Do a synchronous copy of the tempfile. Racecondition detected

* More debugging statements and fixing the play/pause bug. More work needed

* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output

* Moving all track-handling code into own module

* Fix typo. Do not pass ctx around when not applicable

* WIP: More refactoring, debugging output

* Fix nil pointer

* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto

* Do not forget to cleanup after a skip action

* Make resync with master easy

* Adding missing mocks

* Adding missing error-handling found by linter

* Updating github.com/hajimehoshi/oto

* Removing duplicate function

* Move BEEP-related code into own package

* Juggle beep-related code around as preparation for interface access

* More refactoring for interface separation

* Gather CloseDevice() behind Track interface.

* Adding skeleton, draft audio-interface using mpv.io

* Adding majority of interface commands using messages to mpv socket.

* Adding end-of-stream handling

* MPV: start/stop are working

* postition is given in float in mpv

* Unify Close() and CloseDevice(). Using temp filename for controlling socket

* Wait until control-socket shows up. Cleanup socket in Close()

* Use canceable command. Rename to Executor

* Skipping tracks works now

* Now with actually setting the position

* Fix regain

* Add missing error-handling found by linter

* Adding retry mode on time-pos property getter

* Remove unneeded code on queue

* Putting build-tag beep onto beep files

* Remove deprecated call to rand.Seed()

"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."

* Using int32 to conform to Subsonic API spec

* Fix merge error

* Minor style changes

* Get username from context

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 11:25:22 -04:00
Deluan a134b1b608 Use sync/atomic package, now that we are at Go 1.19 2023-02-15 21:21:59 -05:00
Deluan 759ff844e2 Make ffmpeg path configurable, also finds it automatically in current folder. Fixes #1932 2023-02-07 13:46:09 -05:00
Deluan bce7b163ba Skip trying to read cover art from mediafile if it does not have one 2022-12-28 15:31:56 -05:00
Deluan 92b42b35b3 Fallback extracting tags using ffmpeg 2022-12-28 15:31:56 -05:00