Conversation
Signed-off-by: LYJW131 <lyjw2007@gmail.com>
The detail bar showed one fraction and the track list showed another; the
bar could not say how many tracks were actually finished. It now carries
both at once, like the web detail page's `.jd-bar`:
- a solid segment for tracks that are done — (done + failed) / total, so
it only moves a whole notch at a time and always lands on a tick;
- a translucent segment at `DownloadDetail.progress`, which is the very
number printed beside the bar, so the two cannot disagree. What it
shows past the solid segment is the part-finished work of whatever is
in flight;
- per-track ticks, drawn in the page background so they cut the bar
rather than draw a line on it. Skipped above 24 tracks, where they
stop being legible — the same ceiling the web page uses.
Failed tracks count toward the solid segment on purpose: they will never
progress again, and leaving them outside it parks the bar short of the end
where it reads as stuck.
The two fractions are computed by the caller, not the view, because the two
clients do not agree on what "progress" means and should not: iOS averages
per-track fractions (`ItemProgress.fraction`, weighted from measurement),
the web page uses finished-over-total. Nothing about the existing number,
the bar's height, or the surrounding layout changed.
Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>
With enough tracks the last one's shortfall averages away: 199 of 200 done is 0.995, and printed to whole percent that is 100% — while the last track may not have moved a byte. The job then sits at "100%" for as long as the final track takes. `DownloadDetail.progress` now refuses to report full while any item is still in a live state, capping at 0.99. Terminal-only item sets are left alone, so a genuinely finished job still reads 1 and a job that ended with failures still reads whatever its average actually is. Capped on the value rather than at each display site because all five readers share this one number: the detail percentage, the segmented bar's translucent segment, and the Live Activity's lock screen and both Dynamic Island layouts (LiveActivityGateway assigns detail.progress straight through). Signed-off-by: LYJW131 <lyjw2007@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: LYJW131 <lyjw2007@gmail.com>
A job added from the share extension opens on `amdl://download/<id>`, which pushes the detail page straight onto the navigation stack — the overview list never appears, so nothing ever requested its 256px cover. Backing out of the detail page therefore landed on a row holding the type placeholder until that fetch finished, even though the full-size cover was already decoded and in the cache. The two pages cache under different keys because they request different sizes, and `fallbackCacheKey` only ever resolved one way: detail borrowing the overview's small image. It is now symmetric — whichever size is missing borrows the other. The row shows the hero image on its first frame (the memory-cache lookup in `CachedAsyncImage.init` is synchronous), then swaps in the 256 without a fade, because the fade keys on nil-to-non-nil and the slot was never empty. Nothing new is downloaded: this reuses an image already in the cache rather than prefetching a second one ahead of time. `fallbackCacheKey` moved onto `JobArtworkLoader` so the direction it picks is testable, and the literal 256 became `overviewPixelSize` — it is part of a cache key, not just a request size. Private playlists are unaffected: both sizes share one unsized key, the two lookups return the same string, and the fallback stays nil rather than having a view borrow from itself. Signed-off-by: LYJW131 <lyjw2007@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: LYJW131 <lyjw2007@gmail.com>
The gateway now puts `emby_deep_link` on a finished album's banner, after it has refreshed Emby and confirmed the album is actually there. Tapping the banner goes straight to that album. Every other case keeps the existing behaviour and they are not rare — a non-album job, no Emby configured, a scan that had not caught up, a name that did not match, or Emby not installed at all. `UIApplication.open` reports whether it could open the URL, so the last one falls back rather than dropping the tap on the floor, and `job_id` still rides along for it. Only the `emby` scheme is accepted. The value arrives inside a push payload, so taking it at face value would let anything that can push to this device name a URL for the app to open. Signed-off-by: LYJW131 <lyjw2007@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: LYJW131 <lyjw2007@gmail.com>
The deep link was on the push and the app parsed it, but tapping the banner just opened this app. `didReceive` runs before the root view exists on a cold launch — the app is not active yet, and iOS ignores `UIApplication.open` from there. `PendingDownloadRoute` already existed for exactly this reason: the same callback cannot set navigation state either, so it registers the target and the root view acts on it. The Emby URL now rides along and is opened from the same place, falling back to the in-app detail page when open() reports it could not (no Emby installed). Signed-off-by: LYJW131 <lyjw2007@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: LYJW131 <lyjw2007@gmail.com>
amdl-portal is gone, so the session it issued goes with it. PortalAuth.swift becomes GatewayAuth.swift: the credential is now the Apple identity token itself, sent as Authorization: Bearer to oauth2-proxy, which verifies it against Apple's JWKS and checks an email allow-list. Deleted along the way: the access/refresh pair, the exchange against POST /api/gw/auth/apple/native, the refresh single-flight actor (its whole reason for existing was that a concurrent refresh would rotate the token twice and get the family revoked as a replay — there is nothing to rotate now), the 401-refresh-retry, GET /api/gw/me, and the pending-approval state that every new account used to land in. ## The ten-minute token is back, and it is stated rather than hidden An Apple identity token lives ~10 minutes with no silent renewal, which is exactly what the portal was built to paper over. It is written down in GatewayCredential, in AGENTS.md, and in the gateway's own compose file, because the failure it produces — "I have to sign in again constantly" — reads as a bug unless you know it is a trade. Two consequences the code now has to be honest about: - isSignedIn checks the credential is still usable, not just present. The old one only asked "have you signed in", because an expired access token was refreshable. Keeping that would show a signed-in UI while every request 401s. - Expiry is parsed from the token's own `exp`, not received-time + 10 minutes. Ten minutes is measured, not promised. ## Cross-target contract ShareViewController hand-copies the Keychain decoder (extensions cannot see the main target). Its keys moved from accessToken to identityToken/expiresAt, and its service from …amdl.portal to …amdl.gateway; a test pins both, including that expiresAt encodes as a Double, since the extension decodes with a default JSONDecoder. Startup now purges two dead credentials: the plaintext identity token an old build left in UserDefaults, and the portal's 60-day refresh token. BackendEndpoint keeps its "portal" identifiers on purpose — the host name comes from the build setting AMDL_PORTAL_HOST, and renaming that key would silently empty the user's local Config/Portal.xcconfig. The file explains it. Verified: xcodebuild build and test both succeed on iPhone 17 (iOS 26.4.1). Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>
It lasts about a day. Reading `exp` off a real token on 2026-07-30 gave ~23.4 hours; the app's own "会话 剩余 N 分" line, which parses that claim, showed 1400+ minutes. The wrong number has a history worth keeping. This repo first said "about 24 hours", someone then "corrected" it to "about 10 minutes" and annotated the 24 hours as an observation error — and the correction was the error. It spread into four files and, worse, into a label the user reads on screen: "Apple 的登录凭据 只有约十分钟有效期". So the fix is not a better number. Nothing states a lifetime any more: - the on-screen line already shows real time remaining, parsed from `exp` - the warning text below it no longer names a duration - comments point at `exp` instead of asserting a value The 10-minute fallback in GatewayCredential.init stays, now labelled for what it is: a deliberately pessimistic floor for a token whose `exp` cannot be parsed, not an estimate of the real lifetime. This also right-sizes a product decision I had been describing wrongly. Dropping amdl-portal costs a sign-in roughly once a day, not one every ten minutes — still a regression against the portal's 60-day refresh, but nowhere near the one the old wording implied. No behaviour change: comments and one user-facing string. Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Mirror the backend's new library_sync runtime config: a toggle and a poll interval, under 后端 in the settings list. The backend polls the signed-in Apple Music library and submits the albums of newly added songs, so a song saved on the phone downloads on the NAS without a manual submit. Only send the section when the backend returned it. PUT /api/v1/config rejects unknown fields with 400, so an unconditional library_sync would make every settings save fail against a backend that predates the feature — and the failure would land on whichever setting the user actually came to change. Without backend support the row reads 后端不支持 and the toggle is disabled, rather than showing an inviting 已关闭 that silently discards the change. Interval is validated locally against the same 1..1440 range the backend enforces, and only when the section will actually be sent. Bump MARKETING_VERSION to 3.4. Signed-off-by: LYJW131 <lyjw2007@gmail.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
镜像后端新增的
library_sync运行时配置:设置列表「后端」分组下新增「资料库同步」,包含开关和轮询间隔。后端会定期检查已登录的 Apple Music 资料库,把新加入曲目所属的整张专辑提交下载——手机上收藏一首歌,NAS 那边自动下整张。配套后端:apple-music-downloader-backend#91(v1.7.0)。
对旧后端的兼容保护
只有后端 GET 返回过
library_sync段时才回发这一段。后端的PUT /api/v1/config会拒绝未知字段并返回 400,无条件发送会让对着旧后端的每一次设置保存都失败——而且失败的是用户当时真正想改的那一项。不支持时设置页那一行显示「后端不支持」并把开关置灰,而不是显示成一个改了也不生效的「已关闭」。
其它
MARKETING_VERSION3.3 → 3.4🤖 Generated with Claude Code