Commit Graph
259 Commits
Author SHA1 Message Date
zarzet 887a8ede12 feat(metadata): limit embedded cover resolution 2026-08-28 13:21:05 +07:00
zarzet 45d9bbd7b2 perf: harden download and persistence lifecycle 2026-08-28 12:51:03 +07:00
zarzet 433f8ed685 perf: reduce queue and extension runtime overhead 2026-08-28 01:47:51 +07:00
zarzet 1a7c20111a feat(library): add lyrics filtering and metadata actions 2026-08-27 01:44:06 +07:00
zarzet 04c42bd511 perf(downloads): bound native queue state 2026-08-27 00:17:25 +07:00
zarzet 0e3fca9967 perf(downloads): stream concurrent native queue work 2026-08-26 18:37:56 +07:00
zarzet ea63291db8 refactor(metadata): move cover resolution to extensions 2026-08-19 13:12:29 +07:00
zarzet 8b231be19a fix(download): unblock queue after verification cancel 2026-08-18 20:50:48 +07:00
zarzet 290923e88f feat(tagging): write release identity tags 2026-08-18 19:04:26 +07:00
zarzet 48883d51d4 feat(extensions): preserve provider metadata 2026-08-18 12:28:35 +07:00
zarzet 7fc27e8314 perf(library): stream scans and optimize queue queries 2026-08-18 11:03:05 +07:00
zarzet 2e115308b7 feat(filename): add provider traceability tags 2026-08-17 20:51:27 +07:00
zarzet efc03db308 fix(metadata): keep collaboration albums grouped 2026-08-11 20:58:27 +07:00
Abelardo Ramirez 555fd77cc8 fix(ios): stop dropping the security-scoped download folder bookmark
Fixes #439, likely also #302.

Reported: on iOS, the download folder silently reverts to the default
SpotiFLAC folder after starting a download (and stays reverted after
closing and reopening the app).

_processQueue ran an iCloud/writable-path shape check against the
persisted downloadDirectory string and, if it looked like iCloud Drive
or failed the structural writable-path check, called
setDownloadDirectory() to reset it to the default folder. That call
passes no iosBookmark argument, so it also wipes the user's
security-scoped bookmark in the persisted settings - permanently, not
just for the current run.

This check ran unconditionally, even when a bookmark was already
present for that folder. A folder picked from Files can legitimately
have a persisted path that looks like iCloud Drive, or that fails this
generic structural check, while the bookmark itself still grants real
write access - the bookmark is the actual source of truth, and the
queue already has a separate, correct bookmark-resolution step right
after this one (StartAccessingIosBookmark, with its own proper
failure handling that fails the queued items with a clear message
instead of destroying the setting). The earlier path-shape check just
never deferred to it.

lib/utils/file_access.dart: extract the run/skip decision into
shouldValidateIosOutputDir(isIOS, isSafMode, outputDir,
downloadDirectoryBookmark) - skips whenever a bookmark is present -
and use it in download_queue_provider.dart in place of the inline
condition. Kept as a plain function taking isIOS as a parameter since
Platform.isIOS itself can't be exercised from a host test run.

test/file_access_ios_test.dart covers: runs for a plain app-folder
path, skips when a bookmark is present (the exact #439 case), skips
off iOS, skips in SAF mode, and skips when there's no output dir yet.

Verification: flutter analyze and flutter test (224 tests, all green,
including the 5 new ones) both clean.
2026-08-11 17:28:56 +07:00
zarzet 95f2879110 fix(download): recover queue after worker timeout (#524) 2026-08-11 17:23:29 +07:00
zarzet 33db0cf297 fix(queue): restore user-paused downloads #511 2026-08-11 15:23:21 +07:00
zarzet 5237aed25e feat(download): add automatic lossy conversion 2026-08-11 15:19:45 +07:00
zarzet a59c749089 fix(download): preserve SAF and defer background starts 2026-08-09 05:23:47 +07:00
zarzet e7fbaf7b75 fix(download): suffix only colliding quality variants 2026-07-29 02:43:35 +07:00
zarzet 8dcf7fa998 fix(storage): recover from unwritable download folders 2026-07-29 02:03:09 +07:00
zarzet 73846c40ca fix(download): complete quality variants reliably 2026-07-27 14:22:24 +07:00
zarzet 9da3607932 feat(queue): long-press menu with move up/down for queued items 2026-07-27 01:24:14 +07:00
zarzet 8fe2662c0a fix(queue): keep cancelled downloads retryable across every cancel path and app restarts 2026-07-27 01:13:23 +07:00
zarzet 21bc31e0b6 refactor(queue): move single-item download pipeline into a part file 2026-07-26 23:45:22 +07:00
zarzet 49bfefb750 refactor(queue): split progress, verification, and connectivity into part files 2026-07-26 22:47:15 +07:00
zarzet 11fb4365a2 feat(queue): offer one-tap retry when connectivity returns
The connectivity listener now stays alive while network-failed items
remain and, on reconnect, shows a debounced snackbar counting them
with a Retry action wired to retryAllFailed scoped to network
failures. Also regenerates localizations for the recent feature
strings.
2026-07-26 19:00:49 +07:00
zarzet cb8064a317 refactor(dart): consolidate duplicated lookup, filename, and search-provider helpers 2026-07-26 18:52:26 +07:00
zarzet 04a71d0211 refactor: remove orphaned bridge, database, and queue helpers 2026-07-26 18:43:53 +07:00
zarzet c9470ef751 feat(queue): add download-next action on queued items
Queued rows get a skip-next button that reinserts the item ahead of all
other queued items, so the next free slot picks it up. During an active
native worker run the current batch keeps its order; the new order
applies from the next run.
2026-07-26 17:39:28 +07:00
zarzet c4abbf8743 fix(verification): recover signed-session bootstrap failures 2026-07-26 11:09:18 +07:00
zarzet 20081b14bd refactor(dart): extract queue search and media policies 2026-07-26 01:13:52 +07:00
zarzet 0d8bc7b269 fix(native-worker): harden background queue handling 2026-07-24 12:15:55 +07:00
zarzet 5856729e9a fix(library): persist records before download completion 2026-07-23 18:00:22 +07:00
zarzet cdbb0d6bd7 fix(notifications): restore verification alert sound 2026-07-23 13:23:54 +07:00
zarzet c9114fef01 fix(android): sanitize restored installation state 2026-07-23 10:39:31 +07:00
zarzet 480a96bd0e fix(queue): clarify and coalesce verification progress 2026-07-23 09:57:02 +07:00
zarzet ac7840c4e5 fix(download): persist and reconcile history reliably 2026-07-22 15:57:39 +07:00
zarzet b69f67a786 fix(download): name variants by measured quality 2026-07-16 09:20:35 +07:00
zarzet 0961c09a6d feat(download): preserve quality variants 2026-07-16 08:45:01 +07:00
zarzet 5763a087c9 perf(queue): persist and index incremental updates 2026-07-16 08:09:28 +07:00
zarzet e37f13275f perf(embed): native-first tag writes and overlapped fetches in finalization 2026-07-14 14:55:59 +07:00
zarzet 547e6493dd perf(net): fetch embed covers once per release instead of per track 2026-07-14 09:10:01 +07:00
zarzet 2fa4aa5b70 perf(network): mobile stability and data-usage fixes from network audit
Stability:
- transient timeouts no longer classified as ISP blocking; they fall
  through to retry backoff (hard blocks - DNS/RST/cert - still abort)
- connectivity change now closes idle Go connections in every network
  mode (debounced), so pooled sockets from the old interface are not
  reused after a wifi/cellular handoff
- download body reads get a 60s stall watchdog that cancels and
  surfaces a retryable network error instead of hanging; distinct
  from user cancellation
- ResponseHeaderTimeout 45s on all transports; IdleConnTimeout 90->60s;
  dial timeout 30->10s; retry backoff gains full jitter; Retry-After
  honored on 5xx and Deezer 429

Data usage / speed:
- cover downloads deduplicated with singleflight plus a 24MB/15min
  in-memory cache keyed by final URL (album batches fetched the same
  1800px cover once per track before)
- song.link availability cached (30min positive / 5min negative)
  in front of the 9-req/min rate limiter
- uTLS Cloudflare path now pools one HTTP/2 connection per host with
  a shared TLS session cache instead of a full handshake per request

Deezer artist track-count N+1 kept: counts feed the discography
download UI and are already amortized by the artist TTL cache.
2026-07-13 08:39:05 +07:00
zarzet fb7d101019 fix(queue): restore pre-refactor guarantees found by commit audit
- _replaceSafFileVia: op now registers produced files via addCleanup
  the moment they exist, so converted temp files are deleted even
  when a later step throws (old code's outer-variable finally)
- native worker history: recompute format from the final file path
  at history-write time and gate bitrate on lossy output again -
  the extracted helper was recording pre-conversion format/bitrate
  (FLAC conversions logged as m4a with nulled bit depth on SAF)
- drop dead ignore_for_file header in the finalization part
2026-07-13 06:21:42 +07:00
zarzet 2d52f3251e refactor(queue): split download_queue_provider into concern part files
Pure move: paths, native worker, finalization, ReplayGain, and
metadata embedding move to part files as private extensions on
DownloadQueueNotifier; queue orchestration, state, and public API
stay in the main file (7598 -> 4022 lines).
2026-07-12 21:31:38 +07:00
zarzet de32b8b5f6 refactor(queue): extract shared payload, history, and polling helpers
- _buildDownloadRequestPayload/_historyItemFromResult shared by the
  inline and native-worker paths; _buildOutputDir now derives from
  _buildRelativeOutputDir
- shouldAbortWork extended (deleteFileOnAbort, null-item check) and
  used at all ten guard sites; _purgeAlbumRgEntry replaces six copies
- ProgressStreamPoller shared by download queue and local library;
  openVerificationAndAwaitGrant shared by queue and track provider
2026-07-12 21:09:31 +07:00
zarzet 85949eef8c refactor: unify extension call skeleton and download finalization paths
Go:
- callExtensionScript generic replaces the ~40-line perf/lock/run/
  timeout skeleton in ten wrapper methods; PostProcess V1/V2 share
  postProcessCommon (V1 keeps its postProcess-only probe)
- attemptExtensionDownload extracts the duplicated provider download
  attempt from the source-extension and priority-loop paths;
  overlayStr/overlayInt collapse the enrichment overlay chains;
  buildDownloadFilename shared by both output-path builders

Dart (download_queue_provider):
- _replaceSafFileVia wraps the SAF copy/operate/publish/cleanup
  roundtrip used at eight sites
- decrypt and external-LRC finalization unified between the inline
  and native-worker pipelines (divergences parametrized: AC-4 repair,
  per-stage error messages, base-name resolution, extension-state
  snapshot); HIGH/container conversion keep separate lenient/strict
  policies and share only the SAF mechanics
2026-07-12 20:19:54 +07:00
zarzet 0d39002af0 perf(native-worker): stop shipping the full item payload on every poll
The 1s snapshot poll re-read and re-parsed the whole worker state file
— which retains every completed item's result (history row, possibly
full synced lyrics; 1.5-5 KB each) for the entire run — then
re-serialized it across the channel and re-iterated it on the Dart UI
isolate. Late in a 1000-track batch that is megabytes parsed several
times per second, on the Android main thread.

Pollers now echo back the state_serial of the last snapshot they fully
processed; when the state has not advanced, the native side serves a
cached compact header (no items, no results, no settings) merged with
the small progress delta — steady-state polls are O(1) regardless of
batch size, and the full payload is delivered exactly once per state
transition. The channel handler also reads and parses off the main
thread now, matching its neighbours.
2026-07-10 13:08:57 +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 22151b0988 fix(native-worker): recover from dead runs and make queue replacement safe
Stale snapshot (queue wedged forever): the service is START_NOT_STICKY,
so a process kill or reboot mid-batch froze the state file at
is_running:true with nothing ever rewriting it. Adoption then set
isProcessing and polled that flag indefinitely; restored items sat at
downloading/queued until the user intervened. Adoption and the
reconcile poll now check DownloadService.isServiceRunning() (three
consecutive dead polls in the loop), reconcile whatever the worker
finished, requeue in-flight items, clear the run id, and hand the batch
back to the Dart queue.

Adoption gate race (duplicate executors): adoption was gated on
_canUseAndroidNativeWorker, which reads extension state that loads
asynchronously after startup and settings toggles the user may have
flipped mid-run. A background batch surviving task removal was then
orphaned while the Dart queue re-downloaded the same items. Adoption
now only requires Android; request contexts that cannot be built yet
(extensions still loading) are retried on each reconcile poll.

Queue replacement race (superseded worker kept running): startNativeWorker
only cancelled the coroutine, which cannot interrupt the blocking
gomobile call, and it reset the shared cancel flag the old loop checks -
so a replaced worker kept downloading its old batch, and its finally
block wrote is_running:false over the new run's snapshot and stopSelf'd
the service underneath it. Replacement now cancels the old run's Go
downloads and FFmpeg work first, and workers carry a generation token:
a superseded generation exits at the next checkpoint and skips the
final snapshot write and service teardown. An invalid replacement
payload no longer stops the service while a run is active.
2026-07-10 10:18:01 +07:00