Commit Graph
2226 Commits
Author SHA1 Message Date
Abelardo Ramirez 48dbe3679d fix(download): treat a short clean EOF as resumable, not complete
Follow-up to #491, which was fixed in 4.8.5 by making mid-download
resume opt-in (safe default: fail and delete the staged file, since
switching networks can route a stable URL to a different CDN object).

That fix only changes what happens on a *real* read error. It doesn't
help if a transport surfaces a mid-transfer connection drop as a plain
io.EOF instead of io.ErrUnexpectedEOF - fileDownload's copyBody loop
still breaks out and promotes the file on any clean EOF, regardless of
whether written bytes actually reached Content-Length. This app's
uTLS-based client (used for TLS-fingerprint spoofing) is exactly the
kind of custom transport where that guarantee isn't necessarily upheld.

Now a clean EOF short of a known Content-Length is routed through the
same resume-or-fail path as a real read error, so it respects the same
opt-in `resume` option: fails and cleans up the staged file by default,
or resumes via Range/If-Range when the caller explicitly requested it
and the server provided a validator.

extension_runtime_file_download_integrity_test.go adds three tests:
- fails by default even when a validator is present (matches the
  opt-in policy from 4.8.5)
- resumes correctly when `resume: true` is passed
- fails and cleans up when there's no validator at all

All three fail on the pre-fix code and pass with it.

Note: fileDownloadChunked's unknown-total-size path (used for
YouTube's CDN) has an analogous but harder-to-fix ambiguity - without
a known length there's no way to distinguish a legitimately short
final chunk from a truncated one - left as a follow-up.
2026-08-11 17:29:48 +07:00
Abelardo Ramirez bed95efb29 fix(playlists): thread the source provider id through to recent playlists
Fixes #368.

Reopening a Spotify playlist from "recent access" showed no tracks,
while the first view (right after pasting the URL) worked fine.

Root cause traced across two layers:

1. Go: ExtURLHandleResult (the parsed shape of an extension's handleUrl()
   return value) never captured a top-level `id` for the handled
   resource. Track/album/artist results carry their own id inside their
   nested metadata, but a plain playlist result has no such object, so
   its id was silently dropped everywhere from the goja parser through
   to the JSON the Dart side receives.

2. Dart: TrackState had nowhere to put that id even if it existed
   (only playlistName), so recording a "recent" playlist access stored
   the playlist's *name* as if it were its id. Reopening it later fed
   that name into PlaylistScreen's provider-guessing logic
   (legacyProviderIdFromResourceId, which only recognizes legacy
   "provider:id" prefixes), which naturally failed and fell through to
   a hardcoded Deezer metadata fetch using a Spotify playlist's name as
   the resource id - guaranteed to return nothing.

Fix, matching the "generic API, not per-provider checks" architecture
in CONTRIBUTING.md:

- go_backend/extension_provider_wrapper.go: add ExtURLHandleResult.ID.
- go_backend/extension_goja_convert.go: parse it from the handler's
  return value.
- go_backend/exports_extensions.go: surface it in the JSON response.
- lib/providers/track_provider.dart: add TrackState.playlistId,
  populated from the response's `id` field for playlist results.
- lib/screens/home_tab.dart: record the real playlist id (falling back
  to the name only if a provider never supplies one) and pass both the
  id and the already-known provider id forward to PlaylistScreen.
- lib/screens/playlist_screen.dart: PlaylistScreen gains a
  metadataProviderId param that takes priority over guessing from the
  id's shape.
- lib/screens/home_tab_recent.dart: pass the recent-access entry's
  stored providerId through when reopening a playlist.
- lib/utils/provider_resource_ids.dart: extract the
  known-id-vs-guessed-id preference into a small, directly testable
  resolvePreferredMetadataProviderId helper.

Note: this fixes the common case (an extension already reported its
own id as the source provider for the URL it handled). If a provider
never supplies an id for its handleUrl() playlist result, playlistId
stays null and behavior is unchanged from before this fix - no
regression, just not a complete fix for that narrower case, since
that would require changes in extension-side JS code outside this
repo.

Tests:
- go_backend/extension_goja_convert_url_handle_test.go: the new ID
  field round-trips from a handler's return value, and stays empty
  when the handler doesn't supply one.
- test/provider_resource_ids_test.dart: resolvePreferredMetadataProviderId
  prefers a known provider id, falls back to the legacy-prefix guess,
  treats a blank known id as unknown, and returns null for the
  unprefixed-id-with-no-known-provider case from #368 itself.

Verification: go build/vet/test and flutter analyze/test all green.
2026-08-11 17:29:41 +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 1d363d5166 fix(playlist): ignore container title during album verification #519 2026-08-11 17:02:17 +07:00
zarzet f7439c6793 feat(library): add combined quality label #511 2026-08-11 16:56:12 +07:00
zarzet 6dbca64a80 perf(history): batch SAF orphan inspection #511 2026-08-11 16:50:35 +07:00
zarzet bec9e33c94 feat(metadata): review batch tag enrichment #511 2026-08-11 16:40:06 +07:00
zarzet 28538395c2 fix(metadata): match by primary artist and review results #511 2026-08-11 16:20:03 +07:00
zarzet dacc5abde0 feat(logs): copy selected entries #511 2026-08-11 16:12:05 +07:00
zarzet e750c195ca feat(search): save max-quality cover on long press #511 2026-08-11 16:01:39 +07:00
zarzet f189687e08 feat(metadata): show cover dimensions and size #511 2026-08-11 15:53:55 +07:00
zarzet 6ad993e27d fix(lyrics): respect extension provider selection #511 2026-08-11 15:46:33 +07:00
zarzet 8cf1f1f517 fix(metadata): honor provider priority in autofill #511 2026-08-11 15:40:16 +07:00
zarzet d5bfc1e365 fix(metadata): prompt verification during autofill #511 2026-08-11 15:38:39 +07:00
zarzet 33db0cf297 fix(queue): restore user-paused downloads #511 2026-08-11 15:23:21 +07:00
zarzet cd9f43f5a0 fix(cover): preserve best max-quality candidate #511 2026-08-11 15:20:57 +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 2612da81c3 fix(db): prevent concurrent startup locks 2026-08-08 00:01:00 +07:00
zarzet 6742fe886f fix(a11y): label interactive controls 2026-08-04 16:40:48 +07:00
zarzet d3f9388ba8 fix(ci): align APK targets with supported ABIs v4.8.5 2026-08-02 01:42:50 +07:00
zarzet c04050459a Merge remote-tracking branch 'origin/main'
# Conflicts:
#	apps.json
2026-08-02 00:52:44 +07:00
zarzet e761dd604f docs(release): add 4.8.5 changelog 2026-08-01 19:39:09 +07:00
zarzet bad48ea0fd feat(settings): reveal nested search results 2026-08-01 19:23:58 +07:00
zarzet f7a0c8e993 feat(settings): search nested options 2026-08-01 19:08:35 +07:00
zarzet 7972fd64a4 chore(deps): update Flutter and Go dependencies 2026-08-01 18:46:11 +07:00
zarzet 7f4ace6d1b feat(metadata): show current cover dimensions 2026-08-01 18:34:25 +07:00
zarzet d4b617f49a feat(metadata): add embedded cover resize options 2026-08-01 18:24:27 +07:00
zarzet ed60cafb90 fix(search): expose go to album for sparse tracks 2026-08-01 18:01:16 +07:00
zarzet 19753ca005 fix(database): make playback session migration idempotent 2026-08-01 17:39:45 +07:00
zarzet 611f7377f6 fix(metadata): keep header details legible in light theme 2026-08-01 16:10:31 +07:00
zarzet 645a324a5f feat(navigation): add go to album actions 2026-08-01 16:06:08 +07:00
zarzet 6a3a85f7c9 fix(player): refresh lyrics after automatic track changes 2026-08-01 15:38:40 +07:00
zarzet b3bbe93c1e fix(metadata): preserve WAV and AIFF tags 2026-08-01 15:33:20 +07:00
zarzet 15b1b757fd fix(analysis): guard unsupported AC-4 codec 2026-08-01 15:02:20 +07:00
zarzet dc58353c07 fix(lyrics): reject empty embedded payloads 2026-08-01 15:02:03 +07:00
zarzet 3e41aa0eee fix(metadata): support extension custom search 2026-07-30 15:36:31 +07:00
zarzet d85c8f6a17 fix(analysis): detect full-band spectral cutoff 2026-07-30 15:14:36 +07:00
zarzet 5357e14b06 fix(ui): refine track action spacing 2026-07-30 14:42:26 +07:00
zarzet 49d0bd6a99 fix(ui): align track play button hitboxes 2026-07-30 14:16:21 +07:00
zarzet 5228b0030b fix(player): restore lyrics after cold start 2026-07-30 13:48:05 +07:00
zarzet 66928e259d fix(session): enforce provider retry modes 2026-07-30 13:31:54 +07:00
zarzet 113ad7ed9b fix(session): block challenged session generations 2026-07-30 13:09:42 +07:00
zarzet 177a176218 fix(session): honor canonical gateway error contracts 2026-07-30 12:59:48 +07:00
zarzet 169a811162 fix(session): coordinate verification across parallel downloads 2026-07-30 11:39:37 +07:00
zarzet 76f7b9c025 fix(selection): keep modal sheets above selection bar 2026-07-30 11:22:59 +07:00
zarzet 12f2b12570 style(settings): restore connected group layout 2026-07-29 21:03:24 +07:00
zarzet 04a8700a65 fix(player): restore playback position reliably 2026-07-29 21:03:11 +07:00
zarzet 43a8d08851 fix(download): make picker drag dismissal natural 2026-07-29 19:59:09 +07:00