Skip to content

fix(local): restore scanned audio playback and metadata writes - #551

Merged
cwuom merged 13 commits into
masterfrom
intention/fix-local-playback-saf
Oct 21, 2026
Merged

cwuom merged 13 commits into
masterfrom
intention/fix-local-playback-saf

Conversation

@cwuom

@cwuom cwuom commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

描述 / Description

Folder-scanned songs restored remote identities from .npmeta.json and were treated as managed downloads. Playback failed when the chosen local file was outside the configured download tree or its old MediaStore/path reference needed recovery through a persisted folder grant.

Persist explicit scan provenance for both the Local Files system playlist and ordinary user playlists. Match it against the exact preferred reference, preserve it for surviving members during same-device sync application, and honor remote member/playlist deletions. Sync payloads omit local paths and provenance. Playback history and both main/shuffle queues keep the exact explicit-source marker beside their selected reference through JSON, Room, metadata updates, and incremental position writes. Room schema 21 adds nullable marker columns for history and queue entries without inventing provenance for old records. Same-device sync retains path-only local history; sync and backup projections exclude local provenance.

Carry a source context bound to the complete song snapshot and actual playback reference through playback admission, retries, metadata editing, and remote recovery. A recovered folder alias must prove the same canonical file and granted volume/directory; MediaStore proof requires a specific audio item and exactly one data row. Explicit or unknown manual provenance prevents same-identity download bridges and automatic remote replacement, including exact references inside the current download root and references used when no root is configured. An explicit rescan can replace an unmarked legacy or managed source in an ordinary playlist, while a valid existing explicit source remains protected.

  • Retain the preferred source anchor for a readable declared raw/file alias only after opening that exact declaration and proving the preferred reference resolves to the same canonical file. Unknown or conflicting aliases remain unverified.
  • Prefer authorized SAF references during scans while retaining sidecar identities, and preserve initialization/provider failures as unknown or temporary states.
  • Hide download actions for local selections and reject local songs at download admission.
  • Write metadata/artwork sidecars through the recovery transaction, preserve original and published cover references, and stop alias retries after incomplete rollback.
  • Keep synchronous startup predicates free of file/provider probes. Resolve restored local sources through normal permitted download admission on IO, allowing managed downloads to rebind after directory migration, and actually open the chosen reference, including content URIs whose grants remain after deletion or provider failure. Then recheck the pending restore, complete source snapshot, playback request, queue selection, and Listen Together role on Main before consuming the saved position. Unreadable, cancelled, and stale probes preserve pending progress and permit later retries.
  • Validate ordinary app-bootstrap SYNC starts without automatically playing readable media or consuming the pending restore. After a confirmed unavailable restored source, stop the matching idle service and remove its notification while retaining the pending queue and position. Revalidate the source/request and service start ID before cleanup so a late failure cannot stop newer playback; the idle-timeout path keeps its existing paused-state persistence.
  • Add bounded playback diagnostics with sanitized references and regression tests for scan provenance, sync round trips, alias isolation, playback, download actions, and metadata edits.
  • Stabilize native lyric-control tests against viewport/mini-player movement, stop the unrelated SDK Setup process, and fix the dedicated External Lyrics emulator to a real offline network state so virtual Wi-Fi reconnects cannot repeatedly move the mini-player during a gesture fixture. All native gesture and preference assertions remain enabled.

类型 / Type

  • Bug 修复 / Bug Fix
  • 新功能 / New Feature
  • 文档更新 / Documentation Update
  • 其他(请描述)/ Other (please describe):

修复或解决的问题 / Issues Fixed or Closed by This PR

Follow-up to #545 for manually scanned audio failing to resolve a playback URL, local songs exposing download actions, and inconsistent metadata/artwork sidecar writes.

清单 / Checklist

  • 我已阅读并遵循贡献指南 / I have read and followed the contribution guidelines
  • 我已在本地测试这些更改 / I have tested these changes locally
  • 我已更新相关文档或注释(如适用) / I have updated relevant documentation or comments (if applicable)
  • 我确认本次更改没有破坏任何原有逻辑 / I confirm this change does not break existing behavior
  • 我确认本次更改保持向下兼容(如适用) / I confirm this change remains backward compatible (if applicable)
  • 我已运行必要的构建、测试或静态检查 / I have run the required build, tests, or static checks
  • 我确认没有提交密钥、Token 或其他敏感信息 / I confirm no secrets, tokens, or other sensitive information were committed
  • 涉及 UI 变更时,我已补充截图或录屏(如适用) / I have added screenshots or recordings for UI changes (if applicable)

其他信息 / Additional Information

Validation after the latest review fixes:

  • Latest-head GitHub Actions run 39604832140 completed successfully for 6d1b8de11: JVM/CRAP/lint, Worker, Listen Together, all three Android test groups, Native USB verification, and the unsigned Release build passed. CI Android reports contain 1,400 cases with zero actual failures/errors and five prerequisite assumption skips; all 14 migration cases that failed in the preceding run now pass.
  • JVM reports across app and library modules: 13,970 tests, zero failures/errors, seven existing skips. New targeted coverage includes 19 scan-provenance cases, 49 external-source admission cases, seven startup snapshot cases, 58 restore admission cases, and eight service cleanup cases. Readable-alias provenance and granted-but-unopenable content regressions failed before their respective fixes and passed afterward.
  • Local, local sync integration, download runtime, playback runtime, and aggregate app CRAP checks passed with no scoped violations. Module boundaries and app domain dependencies passed without changing thresholds or exclusions.
  • Model/database/local/download runtime/playback runtime/app lint completed with zero errors and no diagnostics in this follow-up's changed files. Existing repository diagnostics remain.
  • Debug application and instrumented-test APK builds passed.
  • Dedicated API 35 phone emulator: all 23 focused cases passed without skips: the two raw/file startup/restore and service-lifecycle cases, 16 SAF/MediaStore cases, and five startup/navigation/input/external-delete cases. The startup case verifies zero main-thread disk reads during bootstrap, retention of the queue/song/1,500 ms position while the file is absent, and successful restore admission at the same position after creating the WAV. The startup case releases immediately after admission and does not assert decoding or audible playback. The service case uses a real Android start ID, verifies foreground cleanup, then unbinds and confirms actual service destruction while the queue/song/position and retry state remain available. The previous app APK failed the service regression after reaching the unreadable-source branch. A second negative run reproduced the same issue on ordinary app-bootstrap SYNC starts. The final APK passed raw and file variants for both null-action restarts and app-bootstrap SYNC starts.
  • Database instrumentation: all four upgrade cases passed, including every historical schema to 21 and explicit 20-to-21 checks that history, queue position, lyrics, and metadata survive while old marker columns remain null. The real history Room incremental-write case also passed with source-marker readback. The generated schema changes only the two nullable columns; the other 40 tables are unchanged. These five cases plus the app cases above total 28 passing device tests without skips.
  • App upgrade fixtures now register the complete migration chain through schema 21. The full legacy-download recovery and app database migration classes passed all 30 device tests without skips after CI exposed their three incomplete migration lists. App lint and both debug APK builds passed again.
  • External Lyrics instrumentation on a dedicated API 35 phone emulator: the original three native-touch cases passed, the same three passed with a real fixed-offline state, and the expanded all-controls case passed during a controlled online-to-offline switch. Recorded mini-player positions changed by 153 pixels during the switch while every gesture and persistence assertion passed. The prior CI failure was a hardware-frame timeout during repeated virtual Wi-Fi disconnects; the workflow fixture changes only that dedicated emulator group. YAML and folded shell syntax checks passed.

Earlier physical-device validation, before these review follow-ups: API 36 PLK110 played the previously failing track and advanced from about 12.1 s to 17.6 s after a signed release upgrade. The latest follow-up was validated on the emulator and was not reinstalled on the physical device. Local download action visibility/admission were also covered by instrumented tests during the original implementation; screenshots are not attached.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T11:36:50.959538Z 81aa829 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 868b5a54b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +191 to +194
val localFiles = LocalFilesPlaylist.firstOrNull(_playlists.value) ?: return false
return localFiles.songs.any { song ->
preferredLocalMediaReference(song.localFilePath, song.mediaUri) == reference
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize scanned sources saved to user playlists

When a user chooses the scan preview's secondary “add to playlist” action, createPlaylistWithScannedSongs or addScannedSongsToPlaylist persists the selected file only in a normal playlist, but this lookup searches only the Local Files system playlist. If that file's sidecar restores a remote identity, playback classifies it as a managed download; with a different configured download root, the exact scanned SAF URI is then rejected rather than played (and a missing file may incorrectly recover the remote source). Include scanned entries in user playlists in the explicit-source evidence, or persist their manual-import provenance separately.

Useful? React with 👍 / 👎.

)
}
}
?: grantedDocumentSource(song, context)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit-source evidence for recovered folder URIs

When a stored MediaStore URI or raw path is unreadable under scoped storage, this fallback returns a newly constructed document URI from the persisted folder grant. For a manually scanned song whose sidecar restored a remote identity, the download coordinator treats the song as managed and asks peekExplicitLocalFileReference about this new URI; that cache contains only the original preferred reference, so it returns false and the readable recovered URI is rejected whenever the configured download root differs. Carry verified alias provenance into the explicit-reference check (or update the stored source) so the new folder-grant recovery also works for the remote-identity songs this change targets.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: cwuom/NeriPlayer/.coderabbit.yaml
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: 2f81f628-836a-4852-bd68-7351446ad426
📥 Commits

Reviewing files that changed from the base of the PR and between 868b5a5 and abb72be.

⛔ Files ignored due to path filters: 1
  • modules/local/README.md is excluded by !**/*.md
📒 Files selected for processing: 30
  • .github/workflows/android_ci.yml
  • app/src/androidTest/java/moe/ouom/neriplayer/data/local/audioimport/LocalAudioImportStandaloneFlacTest.kt
  • app/src/androidTest/java/moe/ouom/neriplayer/ui/screen/tab/settings/component/floating/SettingsExternalLyricsPageTouchTest.kt
  • app/src/main/java/moe/ouom/neriplayer/core/di/player/AppPlayerDownloads.kt
  • modules/download/runtime/src/main/java/moe/ouom/neriplayer/core/player/download/AudioDownloadManager.kt
  • modules/download/runtime/src/main/java/moe/ouom/neriplayer/core/player/download/playback/AudioDownloadPlaybackCoordinator.kt
  • modules/download/runtime/src/test/java/moe/ouom/neriplayer/core/player/download/playback/AudioDownloadExternalScannedPlaybackTest.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/history/PlayHistoryRepository.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/identity/SongIdentityExtensions.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/media/source/ResolvedLocalMediaSource.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/playlist/LocalPlaylistRepository.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/playlist/LocalPlaylistRepositoryBootstrap.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/playlist/LocalPlaylistRepositoryMetadata.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/playlist/LocalPlaylistRepositoryOrdering.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/sync/mapping/SyncLocalRestoreMapping.kt
  • modules/local/src/test/java/moe/ouom/neriplayer/data/history/PlayHistoryRepositoryPolicyTest.kt
  • modules/local/src/test/java/moe/ouom/neriplayer/data/local/media/ResolvedLocalMediaSourceTest.kt
  • modules/local/src/test/java/moe/ouom/neriplayer/data/local/playlist/LocalPlaylistScannedSourceProvenanceTest.kt
  • modules/model/src/main/java/moe/ouom/neriplayer/data/model/SongItem.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/PlayerManager.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/host/PlayerDownloadAccess.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/media/LocalPlaybackMediaResolver.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerPersistenceExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/resolver/netease/PlayerManagerNeteaseLocalFallback.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/url/PlayerLocalPlaybackRecovery.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/url/PlayerManagerUrlExtensions.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/media/LocalPlaybackMediaResolverSourceConflictTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/media/LocalPlaybackMediaResolverTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerLocalMetadataEditRegressionTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/url/PlayerLocalPlaybackSourceRecoveryTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Walkthrough

本次变更过滤本地歌曲下载候选,并在下载入口拒绝本地歌曲。扫描流程保留 SAF 音频引用;播放解析增加授权目录恢复和结构化诊断。元数据及封面侧载写入统一处理事务与回滚。新增单元、Robolectric 和 Android 测试覆盖这些行为。

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to abb72

This change restores playback and metadata writes for folder-scanned local audio. Based on the supplied context, no merge-blocking issue remains. The reported test coverage is broad, and the remaining uncertainty is ordinary SAF/MediaStore device variability.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.87% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 343 functions across 55 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed 标题准确概括了 PR 的主要变更:恢复扫描音频的播放和元数据写入。标题简洁、具体,且与变更范围相关。
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.87% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 343 functions across 55 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: cwuom/NeriPlayer/.coderabbit.yaml
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: 87e3472f-786a-474d-9ff3-eadd32ed3b25
📥 Commits

Reviewing files that changed from the base of the PR and between e694ffb and 868b5a5.

⛔ Files ignored due to path filters: 1
  • modules/local/README.md is excluded by !**/*.md
📒 Files selected for processing: 36
  • app/src/androidTest/java/moe/ouom/neriplayer/core/download/execution/LocalSongDownloadAdmissionTest.kt
  • app/src/androidTest/java/moe/ouom/neriplayer/data/local/audioimport/LocalAudioImportStandaloneFlacTest.kt
  • app/src/androidTest/java/moe/ouom/neriplayer/data/local/media/LocalMediaStorePlaybackReferenceTest.kt
  • app/src/androidTest/java/moe/ouom/neriplayer/ui/screen/playlist/LocalPlaylistDownloadSelectionTest.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/playlist/LocalArtistDetailScreen.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/playlist/LocalPlaylistDetailModernContent.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/playlist/PlaylistDownloadCandidates.kt
  • app/src/test/java/moe/ouom/neriplayer/ui/screen/playlist/PlaylistDownloadCandidatesTest.kt
  • modules/common/src/main/java/moe/ouom/neriplayer/common/logging/PlaybackLogReference.kt
  • modules/common/src/test/java/moe/ouom/neriplayer/common/logging/PlaybackLogReferenceTest.kt
  • modules/download/logic/src/main/java/moe/ouom/neriplayer/core/download/storage/reference/ManagedDownloadReferenceIo.kt
  • modules/download/logic/src/test/java/moe/ouom/neriplayer/core/download/storage/reference/ManagedDownloadReferenceIoTest.kt
  • modules/download/runtime/src/main/java/moe/ouom/neriplayer/core/download/manager/batch/GlobalDownloadManagerBatchPrepare.kt
  • modules/download/runtime/src/main/java/moe/ouom/neriplayer/core/download/manager/runtime/GlobalDownloadManagerRequest.kt
  • modules/download/runtime/src/main/java/moe/ouom/neriplayer/core/download/storage/facade/ManagedDownloadStorageFacadeCatalog.kt
  • modules/download/runtime/src/main/java/moe/ouom/neriplayer/core/player/download/playback/AudioDownloadPlaybackCoordinator.kt
  • modules/download/runtime/src/test/java/moe/ouom/neriplayer/core/download/ManagedDownloadClassificationDiagnosticsTest.kt
  • modules/download/runtime/src/test/java/moe/ouom/neriplayer/core/player/download/playback/AudioDownloadExternalScannedPlaybackTest.kt
  • modules/download/runtime/src/test/java/moe/ouom/neriplayer/core/player/download/playback/AudioDownloadPlaybackDiagnosticsTest.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/audioimport/LocalAudioImportManagerSetup.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/media/LocalMediaSupport.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/media/LocalMediaSupportFacadeMetadata.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/local/playlist/LocalPlaylistRepository.kt
  • modules/local/src/test/java/moe/ouom/neriplayer/data/local/audioimport/LocalAudioImportMediaStoreSafReferenceTest.kt
  • modules/local/src/test/java/moe/ouom/neriplayer/data/local/media/LocalMediaLyricTransactionTest.kt
  • modules/local/src/test/java/moe/ouom/neriplayer/data/local/playlist/LocalPlaylistExplicitLocalReferenceTest.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/media/LocalPlaybackMediaResolver.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerPersistenceExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/playback/PlaybackFailureRecoveryOwner.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/url/PlayerLocalPlaybackDiagnostics.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/url/PlayerLocalPlaybackRecovery.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/url/PlayerManagerUrlExtensions.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/media/LocalPlaybackDiagnosticsTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/media/LocalPlaybackMediaResolverTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerLocalMetadataEditRegressionTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/url/PlayerLocalPlaybackRecoveryTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbd48b3402

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +484 to +485
if (compatible && (verifiedAnchor == null || verifiedAnchor == reference)) {
return null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor explicit provenance for compatible references

When the configured SAF root is absent, every reference is considered compatible; references inside the configured tree are compatible as well. This early return therefore skips explicitLocalFileReference even when the exact preferred reference was persisted from a scan. For a scanned song whose sidecar supplied a remote identity, the subsequent completed-reference bridge can replace the user-selected file with another download of that identity before the selected reference is probed, and a missing selected file can enter managed rebind logic. Check explicit provenance before applying this compatibility shortcut so exact scanned sources retain the same protection as recovered aliases.

Useful? React with 👍 / 👎.

Comment on lines +119 to +120
val replacesRemoteSource = allowScannedSourceReplacement && updatedExplicitReference != null &&
!LocalSongSupport.isLocalSong(currentSong, null) && currentSong.sameIdentityAs(newSongInfo)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow explicit rescans to replace unmarked local sources

When an ordinary playlist already contains a legacy or managed local entry with the same remote identity but no valid explicit marker, adding a newly scanned file reaches this merge with allowScannedSourceReplacement = true. Because the current entry is already considered local, this condition remains false; shouldPreserveEntryPlaybackSource then sees two different local references and copies the old playback fields back, silently discarding both the newly selected file and its provenance. Permit replacement when the current local entry has no valid explicit provenance, while retaining the existing protection for an already selected explicit file.

Useful? React with 👍 / 👎.

return listOfNotNull(preferred, song.localFilePath, song.mediaUri)
.distinct()
.any { isRestorable(it, context) }
.any { isRestorable(it, context) } || ResolvedLocalMediaSource.recoverFromFolderGrant(context, song) != null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep folder-grant recovery off the main thread

When all stored references are revoked, this fallback calls recoverFromFolderGrant, which can synchronously query MediaStore and open a document descriptor. isRestorableSong is reached through transportSnapshot(includeRestore = true) during PlayerStartupBootstrapper's main-dispatcher LaunchedEffect, and the same bootstrap predicate is also invoked from the Compose service-start callback, so a slow or stalled DocumentsProvider can block startup UI or trigger an ANR. Perform the recovery probe on Dispatchers.IO or consume an asynchronously prepared/cache-only result from this synchronous predicate.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ff6fd146a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 1417 to 1420
PlayerManager.scheduleRestoredPlaybackResume(serviceScope) {
NPLogger.w("NERI-APS", "Restored playback after process restart")
updateAll()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop the service when asynchronous restore validation fails

When the restored queue points to a deleted raw path or file: URI, the new lightweight bootstrap check admits it and ensureForegroundStarted() runs, but this job returns null after its IO probe and invokes no callback. Execution has already selected START_STICKY because the queue still has items, so the user is left with an unnecessary foreground playback service and notification until the default 60-minute idle timeout. Handle the unsuccessful completion by disabling restart and stopping the foreground service while preserving the pending restore for a later retry.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e07ea6b1db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

val conflicting = readable != null && readable != preferred &&
ResolvedLocalMediaSource.hasConflictingDeclaredFile(context, song)
val resolved = if (conflicting) null else {
readable?.let { ResolvedLocalMediaSource.direct(song, it) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve provenance for verified readable aliases

When the preferred content URI has become unreadable but localFilePath still opens the same file (for example on devices with legacy filesystem access), the preceding conflict check verifies that the two declarations resolve to the same canonical file, but this line creates a direct source whose anchor is the fallback path itself. unverifiedDeclaredReferenceFor consequently treats the preferred URI as unverified, and the coordinator rejects the otherwise readable fallback whenever that preferred URI is recorded as an explicit scanned source. Carry the verified preferred reference as the anchor in this non-conflicting-alias case so revoked provider access does not make the scanned song unplayable.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b44dc94592

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -110,7 +111,7 @@ fun PlayedEntry.toSongItem(): SongItem {
album = album,
durationMs = durationMs,
coverUrl = coverUrl,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist explicit provenance in playback history

When a scanned song whose sidecar restores a remote identity is later opened from Recent after its last playlist membership was removed, toPlayedEntry has discarded explicitLocalSourceReference and this reconstruction restores only its URI/path. peekExplicitLocalFileReference consequently returns false, so the download coordinator can classify the history item as managed and reject or replace the readable user-selected file when it lies outside the configured download root. Persist and restore the explicit-source marker with PlayedEntry so history remains independently playable.

Useful? React with 👍 / 👎.

private suspend fun PlayerManager.restoredSourceAvailable(song: SongItem): Boolean {
if (!isLocalSong(song)) return true
return try {
withContext(Dispatchers.IO) { isReadableLocalMediaUri(localMediaSource(song)) }.also { available ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate restored managed sources through the download resolver

When the restored song is a managed download whose persisted reference still points to the pre-migration directory, this direct readability check returns false even if the download catalog already contains a valid replacement in the new root. Normal playback handles exactly this case through the coordinator's managed-reference rebind, but restore validation never reaches it, so automatic resume is rejected and the service may be stopped despite an immediately playable source. Resolve restored managed songs through the same permitted-local-playback path, or distinguish recoverable managed references before declaring the source unavailable.

Useful? React with 👍 / 👎.

@cwuom
cwuom merged commit a029975 into master Oct 21, 2026
8 checks passed
@cwuom
cwuom deleted the intention/fix-local-playback-saf branch October 23, 2026 05:11
liaolon added a commit to liaolon/NeriPlayer that referenced this pull request Oct 25, 2026
带入上游:
- fix(ui): preserve page state and back navigation across rotation (cwuom#570)
- fix(download): prevent cancelled mobile batches from restoring (cwuom#566)
- fix(nowplaying): sync matched favorites and refine song sheets (cwuom#562)
- feat(lyrics): add ColorOS 16/17 lockscreen lyrics with Root and Shizuku (cwuom#563)
- fix(lyrics): preserve Bluetooth lyrics in display metadata (cwuom#560)
- fix(ui): stabilize cover morphs and improve Clean switch contrast (cwuom#561)
- fix(netease): isolate rejected songs during playlist sync (cwuom#559)
- feat(backup): protect configuration exports and warn on HTTP WebDAV (cwuom#557)
- perf(ui): defer expensive UI work and complete Clean theme styling (cwuom#556)
- fix(auth): persist accounts when Android Keystore is unavailable (cwuom#554)
- fix(lyrics): apply translation mode before publishing songs (cwuom#553)
- fix(local): restore scanned audio playback and metadata writes (cwuom#551)

与心动模式改动的重叠文件(NowPlayingScreen、NowPlayingQueueSheet、
MoreOptionsMainContent、PersistedPlayerState、PersistedPlayerStateMapping、
PlayerManager、PlayerManagerPersistenceExtensions、NeteaseClient)均为自动合并,
无冲突;heartbeatMode 持久化字段与恢复调用保持完整。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant