Commit Graph

353 Commits

Author SHA1 Message Date
zarzet 21aeaafced fix(metadata): write FLAC, M4A, and AC-4 tags atomically with fsync
go-flac's Save(samePath) rewrites the file in place by shifting the audio
body within the same inode, so a process kill or power loss mid-save
destroyed the file with no recovery copy. All tag writers now stream to a
sibling temp, fsync, and rename over the target, so an interruption leaves
either the old intact file or the new complete one. The M4A freeform and
AC-4 whole-file rewrites get the same treatment, and the WAV/AIFF writer
gains the missing fsync before its rename.
2026-07-11 12:44:01 +07:00
zarzet 270496b3d5 refactor(lyrics): classify provider errors by type, not message text
Replace the substring signal lists in isLyricsProviderUnavailableError
with typed errors (errLyricsNotFound / errLyricsServiceUnavailable in
the new lyrics_errors.go). Providers now classify failures at the point
of origin: HTTP statuses via lyricsHTTPStatusError (429/5xx cooldown),
netease API codes and paxsenix proxy failures as explicit unavailable,
and every "no lyrics/songs found" path as typed not-found.

Substring matching survives only for error payloads from third-party
proxies (rate limit, missing parameters), whose messages genuinely
arrive as free text.

Two deliberate behavior changes: LyricsPlus 429/5xx responses now cool
the provider down (previously unclassified), and proxy payloads saying
"not found" are treated as not-found instead of disabling the provider.
2026-07-10 15:11:34 +07:00
zarzet 87b46a9694 perf(dedupe): make the ISRC index incremental, cover-free, and batch-stable
The index was rebuilt from scratch every 5 minutes even while
AddToISRCIndex kept it write-consistent after every download, and each
rebuild parsed the FULL metadata of every FLAC in the library —
including multi-megabyte embedded cover art — sequentially, with the
triggering download blocked behind the build lock. For a few thousand
files that is gigabytes of flash I/O per rebuild, repeated throughout
a long batch.

- Indexing now reads only the Vorbis comment block (block headers are
  walked and picture/padding payloads seeked past, never loaded)
- A per-file (size, mtime) -> ISRC cache carries across rebuilds, so a
  rebuild is normally a stat walk that re-reads only changed files
- Add() refreshes the index timestamp, so the TTL no longer forces a
  rebuild in the middle of the workload the index exists to serve
- Cold builds parse with 4 workers, matching the library scanner
2026-07-10 13:08:55 +07:00
zarzet 364b2aa138 fix(metadata): stop the endless FLAC-write failures on misnamed files
When a provider delivers a lossy/unknown stream, the native finalizer
preserved the container but kept the requested .flac name, so every
metadata/ReplayGain write treated the file as FLAC and failed with
"failed to write FLAC metadata: failed to parse FLAC file: fLaC head
incorrect" — on the first download, on every retry (the file is never
deleted from public storage), on every manual edit attempt, and the
history backfill re-probed the same file on every app launch forever.

- Finalizer now renames a preserved lossy/unknown container away from
  its .flac name to match the real content (aac/MP4 -> .m4a, mp3, opus),
  mirroring the Dart pipeline, so the correct tag writer is picked
- EditFileMetadata sniffs MP4 content before taking the .flac branch
  and reports what is actually wrong (rename to .m4a) instead of the
  cryptic parse error
- The history audio-metadata backfill remembers paths whose probe
  failed permanently (unparseable content) and stops reselecting them
  on every launch; transient failures (missing file, unmounted volume)
  still retry
2026-07-10 12:07:11 +07:00
zarzet b4f66ff4eb chore(backend): align merged test file and fd stubs with lint conventions 2026-07-10 10:25:12 +07:00
zarzet b1f8224310 test(extensions): skip POSIX permission assertion on Windows
The signed-session test from #462 asserts the session file is 0600,
but Windows does not preserve Unix permission bits, so the suite failed
on Windows dev machines while passing in CI.
2026-07-10 10:24:25 +07:00
zarzet 9b7f1631f0 fix(native-worker): keep batches and retries intact across seam races
- Verification challenge no longer abandons the batch: batch mates the
  worker cancel would mark skipped are requeued and fenced from later
  snapshot applications, and the adoption loop restarts the queue for
  requeued items when the run ends
- Pause vs cancel race: a cancelled result with no pause flag set waits
  up to 1.5s for the pause intent to land on the main looper before
  being classified as a permanent skip
- Stale Go cancel flag: new ResetDownloadCancel export (wired through
  Android and iOS bridges) drops a pre-registered cancel with no active
  download; retryItem/retryAllFailed call it so the first retry of a
  cancelled-before-start item no longer aborts instantly
- Reconcile-loop errors now cancel the native worker and clear the run
  id before marking items failed, instead of leaving the service
  downloading a batch the UI already wrote off
2026-07-10 10:18:03 +07:00
zarzet d819878ec7 fix(downloads): stage direct-mode outputs and serialize same-path writes
The non-SAF filesystem path (Android direct mode and all of iOS) had
none of the safeguards the SAF path gained: extension downloads
streamed straight into the final filename, every error path stranded
the partial file there, and the ISRC duplicate check (exists + size>0
with a head-only FLAC parse) then accepted the truncated file as
complete forever. On iOS this was routine, not rare: the background
task grace is ~30s, after which the process is suspended mid-stream.

- fileDownload/fileDownloadChunked now stream into a .partial sibling
  and promote to the final name with an atomic rename on success;
  failures remove the staged file and never touch the final name
- fileWrite (full-content) uses the same stage-and-rename protocol
- writes are serialized per final output path, so concurrent queue
  items that resolve to the same filename can no longer interleave
  bytes into one file

The staged suffix keeps partials invisible to extension duplicate
checks, which match on the final audio extension.
2026-07-10 10:18:03 +07:00
zarzet 823b050744 chore(deps): update Go modules
go get -u across the backend: goja 2026-07-01, golang.org/x/* latest,
brotli 1.2.2, klauspost/compress 1.19. Toolchain stays on go1.25.9;
CI and docs now pin 1.25.9 explicitly.
2026-07-10 10:18:02 +07:00
zarzet 8abb99ac91 chore(backend): modernize interface{} to any, silence lint noise
gofmt -r rewrite of all 506 interface{} occurrences to the any alias
(identical semantics), drop unused spec constants into comments, rename
unused parameters to _, convert three if-else chains to tagged
switches, and use slices.ContainsFunc for the private-IP check. No
behavior change; the whole package is now gofmt-clean.
2026-07-10 10:18:00 +07:00
zarzet e85151643f fix(extensions): serialize storage/credential/salt file writes across runtimes
Each isolated download runtime carried its own storage write mutex, so
two concurrent downloads through the same extension could interleave
writes to the shared storage file, the encrypted credentials file, or -
worst - both generate different credential salts, making the loser's
credentials undecryptable. Guard all three files with a process-wide
per-path mutex and write via temp file + rename so readers never see a
torn file. Groundwork for concurrent downloads.
2026-07-10 10:18:00 +07:00
zarzet 2f53be7c0d fix(extensions): expire stale pending auth challenges
A pending verification challenge lived in a process-global map with no
timestamp, so a challenge created while the app was backgrounded (and
never completed) was served again verbatim on the next attempt - sending
the user to an already-expired CAPTCHA page that needed a second request.

Stamp PendingAuthRequest with CreatedAt at every writer and make
ensureExtensionPendingAuthRequest discard entries older than 5 minutes,
starting a fresh signed-session verification instead.
2026-07-10 10:12:18 +07:00
zarzet a1764b1aa0 fix(metadata): close FLAC file handles after parsing
flac.ParseFile leaks the *os.File on parse errors, and read-only callers
(ReadMetadata, ExtractCoverArt, extractLyricsFromFlac) never closed the
handle on success either. Long library or duplicate-index scans leaked
one fd per FLAC file, and on Windows the lingering handles kept files
locked (the source of the flaky TempDir cleanup failure in
TestExportsJSONWrappersAndExtensionManagerSurface).

Route all parsing through parseFlacFile, which closes on error, and
defer f.Close() at every call site. File.Save already closes the
underlying handle; the deferred second Close is a harmless no-op.
2026-07-10 10:12:17 +07:00
zarzet 99eed524c2 refactor(backend): split extension_providers.go into themed files
Pure mechanical move within package gobackend — no signature or behavior
change. extension_providers.go (3,798 lines) now holds only the
extensionManager provider-listing/orchestration methods; DTOs, goja value
conversion, the provider wrapper methods, download fallback, and priority
state moved to extension_{provider_types,goja_convert,provider_wrapper,
fallback,priority}.go.
2026-07-10 10:12:17 +07:00
zarzet d909067c49 refactor(backend): split exports.go into themed files
Pure mechanical move within package gobackend — no signature or behavior
change, so the gomobile binding surface is identical. exports.go (4,141
lines) now holds only availability/dir/duplicate/filename exports; the
rest moved to exports_{musicbrainz,download,reenrich,metadata,lyrics,
deezer,extensions,store,library}.go along existing responsibility
boundaries.
2026-07-10 10:12:17 +07:00
zarzet 4c83aeafb5 feat(metadata): surface explicit content flag with [E] badge
Port of the explicit-content indicator from SpotiFLAC-Web (issue #456):

- Go: add explicit to ExtTrackMetadata (parsed generically from any
  extension via explicit/is_explicit/isExplicit), TrackMetadata, and
  AlbumTrackMetadata; built-in Deezer maps explicit_lyrics and
  explicit_content_lyrics == 1
- Dart: add explicit to the Track model with a tolerant
  parseExplicitFlag helper wired into all backend track parsers
- UI: new ExplicitBadge rendered through buildQualityBadges on album,
  playlist, search, and home track rows
2026-07-10 10:12:14 +07:00
zarzet 8abb9d119b fix(store): harden extension repo registry handling
- Persist registry URL inside store_cache.json and restore it on init so
  the disk cache survives app restarts instead of being wiped every launch
- Fall back to the cached registry when the fetched body fails to parse,
  matching the existing network/HTTP error fallbacks
- Detect HTML responses and surface a clear message instead of the raw
  Go JSON error (invalid character '<') seen in issue #474
- Validate the registry loads before persisting a new URL, and roll the
  backend back to the previous URL on failure, so a broken link never
  survives a restart or Android auto-backup restore
2026-07-10 10:12:13 +07:00
Amix fc2818dd97 test(extensions): add coverage for signed session HMAC auth flow (#462)
extension_signed_session.go implements the HMAC-signed session
scheme extensions use to authenticate against their own backends
(namespace scoping, on-disk persistence, rolling-key signing,
expiry/refresh, and the grant exchange flow), but had zero direct
or indirect test coverage.

Adds table-driven tests for the pure helpers (namespace
sanitization, config defaults, time parsing, URL building,
Retry-After parsing, record scope normalization) plus
runtime-backed tests using a mocked http.RoundTripper for the
stateful flows: file persistence round-trips, status/clear,
grant exchange and refresh, 401-triggered session revocation, and
unauthenticated fetches surfacing the verification URL.

The centerpiece is TestDoSignedSessionRequestSignature, which
recomputes the rolling-key HMAC signature server-side from the
request headers the client actually sent, the same way a real
backend would validate it, to catch any accidental regression in
the signing scheme itself (field order, rolling-key derivation, or
header names).
2026-07-10 10:11:12 +07:00
zarzet 2b8ec744dd feat(extensions): embed full metadata and cover after extension downloads
Replace duplicate genre/label-only embedding with a shared post-download
step that writes complete FLAC tags and optional cover art when the output
file is available locally.
2026-07-02 01:24:22 +07:00
zarzet 0bf5a39a92 fix(ac4): reject truncated AC-4 sample entries safely
Validate audio sample entry header bounds before QuickTime v1
normalization and dac4 injection so malformed MP4 trees are left
unchanged or rejected instead of panicking on truncated boxes.
2026-07-02 01:24:21 +07:00
zarzet 2c2cf8cdf8 fix(extensions): bootstrap and clear pending signed-session auth
Ensure pending auth requests are created when verification is needed but
missing, and clear them after signed-session grant completion or clear
so stale challenges do not block later verification flows.
2026-07-02 01:24:19 +07:00
zarzet 3fd14e21eb feat(extension-repo): preserve package suffix from download URL
Detect .sflx and .spotiflac-ext from the registry download URL when saving
repo downloads, and extract findExtension so the destination path can use
the correct package extension.
2026-07-01 03:19:02 +07:00
zarzet 1a01147a95 fix(extensions): scope signed session files by endpoint and app context
Hash session cache paths from namespace, base URL, app version, and
platform so credentials do not leak across environments, and invalidate
stored sessions when that scope changes.
2026-07-01 02:03:14 +07:00
zarzet 8950907428 feat(extensions): route Deezer metadata through enabled extension
Prefer the enabled Deezer metadata extension over the built-in provider
when resolving album, track, and playlist metadata requests.
2026-07-01 02:03:10 +07:00
zarzet 7f82049beb fix(lyrics): prefer workers.dev LyricsPlus mirror first
Trim the LyricsPlus server list to the workers.dev and binimum mirrors
and update tests to match the new primary endpoint order.
2026-06-30 09:13:55 +07:00
zarzet dc8bb2cbc2 feat(extension-health): lengthen cache TTL and honor per-check minimum
Raise default extension health cache to 10 minutes with a 1-minute floor
and a shorter TTL for unknown status. Mirror the TTL rules in the Go
backend and stop force-refreshing health checks from the download
service picker on every open.
2026-06-30 06:20:10 +07:00
zarzet bd14c7dc63 fix(go): widen lyrics priority grace to 5s so priority provider wins
Apple Music (priority) was losing to LRCLIB when slightly slower than the
1.2s grace window. Widen to 5s so a slower priority provider still wins,
while still bounding the wait if it hangs/fails.
2026-06-29 06:47:04 +07:00
zarzet bede5ae8d7 feat(go): verification early-abort in fallback + album metadata from tracks
- DownloadWithExtensionFallback now immediately surfaces verification_required
  when any provider needs verification (availability + download stages),
  instead of letting later providers mask it
- classifyDownloadErrorType treats 'session is not authenticated' as
  verification_required (Go + Dart side)
- parseExtensionAlbumValue.withTrackFallbacks() derives album artist,
  release date, and audio traits from tracks when album-level missing
- albumAudioTraitsFromTracks detects dolby_atmos/hi_res_lossless/lossless
  from per-track audio_quality/audio_modes fields
- parseBitDepthSampleRate parses '24bit/96kHz' style quality labels
2026-06-28 22:16:36 +07:00
zarzet 445b186e3b fix(go): classify transient timeout/5xx in extension health as unknown (grey) 2026-06-28 22:15:55 +07:00
zarzet 95f5ae610e feat(banner): HLS motion-artwork header banners and audio-quality badges
Add a MotionHeaderBanner (video_player) that plays a looping muted HLS header video with a static image fallback for artist, album, and playlist screens. The Go backend now exposes header_video, header_image, and audio_traits from extensions. Album/playlist headers show the release year and Dolby Atmos / Lossless badges inline, full date and song count in a footer, a centered square cover when no video is present, and a full-bleed video when one is.
2026-06-28 06:06:54 +07:00
zarzet ce813bc216 feat(extension-health): cache results, force refresh, treat lookup errors as transient
Cache health results after each check, add a force flag to bypass the cache when the download picker opens or a service is selected, guard stale concurrent results via a per-extension request serial, and treat DNS lookup failures as indeterminate/transient.
2026-06-28 06:05:48 +07:00
zarzet 8558450378 fix(lyrics): improve provider fallback and health handling 2026-06-27 06:35:32 +07:00
zarzet 50509d0a16 chore: clean up redundant comments across Go backend and Flutter sources 2026-06-26 22:57:03 +07:00
zarzet c1c0494912 perf: optimize queue/library pagination, counts, scan, and extension runtime
- Queue/Library: switch from growing-limit refetch to offset-based append pagination per filter/search/sort, with page cache reassembly and protected-page eviction to avoid top-of-list gaps

- Queue/Library: watch only the active filter page instead of all/singles/albums simultaneously; filter mode follows PageView swipe and tab tap

- library_database: combine three union-based count queries into a single round-trip

- library_scan: parallel scan of non-CUE audio files with a bounded 2-4 worker pool, preserving order, progress, and cancellation; CUE handling stays sequential

- extension_manager: cache compiled goja.Program per extension and RunProgram per isolated download instead of re-reading and re-parsing index.js
2026-06-26 22:39:22 +07:00
zarzet f0acda0f01 feat(network): add opt-in allow local/private network setting
Add a setting that relaxes the SSRF guard so extensions and built-in network code can reach private/local/loopback targets, for users routing traffic through a local proxy or custom DNS. Disabled by default. Wired end-to-end: Go backend (SetAllowPrivateNetwork toggles isPrivateIP guard), Android/iOS platform bridge, Dart settings model/provider, and a toggle in Download settings.
2026-06-26 20:29:15 +07:00
zarzet af4e4561ec chore(deps): update Flutter and Go dependencies to latest stable
Flutter: bump connectivity_plus, device_info_plus, share_plus, receive_sharing_intent, path_provider, flutter_local_notifications to latest stable. Revert file_picker from 12.0.0-beta to 11.0.2 stable and migrate pickFile (singular) to pickFiles. Add win32 ^6.0.1 dependency_overrides to resolve the win32 5 vs 6 conflict between file_picker stable and device_info_plus (Windows-desktop-only transitive dep, not used on mobile targets). Go: update goja, golang.org/x/mobile, golang.org/x/tools, regexp2/v2 to latest.
2026-06-26 20:26:43 +07:00
zarzet 6d932386b0 feat(download): add {playlist_position} filename placeholder
Add a playlist position token usable in filename templates, plumbed from the playlist screen through the download queue to the Go backend. Auto-prefix playlist downloads with the position when the template lacks the token. Includes Go filename test.
2026-06-26 18:35:32 +07:00
zarzet 9c054b9e3a feat: handle extension verification and retry-after on download
Classify verification-required and rate-limit errors from extensions, propagate Retry-After seconds through the download fallback, report session-grant success/failure, and add a completeGrant action fallback in the runtime.
2026-06-26 07:12:12 +07:00
zarzet ee35f52baf feat: extension runtime and provider improvements 2026-06-26 04:14:51 +07:00
zarzet 21347420f3 feat(download): AC-4 passthrough support
Decrypt AC-4 via the FFmpeg mov muxer with a -f mov fallback, then repair the output to a standards-compliant ISO MP4: inject the dac4 config box from the encrypted source, normalize the QuickTime container/sample entry, and write iTunes metadata (incl. cover and lyrics) natively. Codec-keyed and generic, so it applies to any extension that returns AC-4 streams. Wired through PlatformBridge/MainActivity for both SAF and local decrypt paths.
2026-06-23 02:44:08 +07:00
zarzet 26987459f3 fix(metadata): cap oversized cover art and support QuickTime/MP4 tags
Re-encode/downscale cover art that exceeds the FLAC 24-bit picture block limit (go-flac silently truncated it into a corrupt file). Locate ilst in both ISO and QuickTime-style meta atoms, and skip freeform tags gracefully when no iTunes container exists.
2026-06-23 02:41:42 +07:00
zarzet ccc93f881a fix(metadata): write M4A ISRC/label natively and read all edited fields from file
FFmpeg's MP4 muxer drops ISRC and label, so write them as iTunes freeform atoms natively after every M4A embed pass. The metadata screen now reads all edited fields from the file on load (file is the source of truth), fixing edited values reverting to stale cached values after reopening.
2026-06-14 15:00:34 +07:00
zarzet ded8b68098 fix(download): honor requested quality when fallback provider recognizes it 2026-06-14 14:12:37 +07:00
zarzet 63c68b4d4d fix(download): honor selected provider when it equals the track source
When the chosen download service matched the track's source extension it was skipped in both the source preflight and the fallback loop, so downloads silently fell back to another provider. It is now attempted in the loop, and an explicitly selected provider bypasses the fallback allow-list.
2026-06-13 20:31:02 +07:00
zarzet 953ef37882 fix(download): request fallback provider's own highest quality 2026-06-13 16:29:30 +07:00
zarzet 49869792cf chore: trim redundant comments 2026-06-13 15:37:00 +07:00
zarzet fad4c4ea36 feat(lyrics): show actual lyrics source in metadata 2026-06-13 15:36:47 +07:00
zarzet 6b5345a6e5 fix(downloads/extensions): iOS background task, serialize extension mutations, safer batch convert sheet
- iOS: begin/end UIBackgroundTask while a download queue is active so in-flight downloads survive backgrounding for the limited window iOS allows

- extensions: serialize install/upgrade/remove in the Go manager (mutationMu) and in the Dart store provider to stop concurrent goja VM teardown/reload from hard-crashing the app

- main: add runZonedGuarded + FlutterError/PlatformDispatcher onError so uncaught Dart errors are logged, not fatal

- batch convert sheet: precompute localized title/label before showModalBottomSheet to avoid Localizations lookup via a deactivated context
2026-06-13 02:42:23 +07:00
zarzet b8b670642c feat(audio): add WAV and AIFF support + settings-style metadata menu
WAV/AIFF: library scan, quality probe, native tag read/write via embedded ID3 chunk (RIFF id3 / AIFF ID3), cover art, ReadFileMetadata, ExtractLyrics, and FLAC<->WAV/AIFF conversion (PCM, bit-depth preserved via ffprobe). Treat WAV/AIFF as lossless across all convert sheets (no bitrate picker, Lossless labels) via isLosslessConversionTarget. Native MIME maps for SAF. Redesign the track metadata three-dot menu to a settings-style grouped card with a single divider above Share.
2026-06-12 21:10:37 +07:00
zarzet 2a2e2924eb feat(lyrics,replaygain): add LyricsPlus provider and ReplayGain batch scanning
LyricsPlus (KPOE): word-by-word synced lyrics with multi-server failover, converted to enhanced LRC. ReplayGain: standalone EBU R128 (re)scan writing REPLAYGAIN_TRACK_* tags via native writers or FFmpeg, with batch action in queue/album screens and SAF support.
2026-06-12 01:59:26 +07:00