Summary
After playing a media-share (SMB) movie and returning to its item-detail page, the page does not immediately reflect the new watched position / resume state. The progress is saved — it appears in the Continue Watching row, and on the detail page after a manual reload or app relaunch — but the in-place optimistic refresh that Plex/Jellyfin get does not fire for the SMB copy. This is a UX-polish gap, not data loss.
Steps to reproduce
- Have a title that exists on an SMB share (and optionally also on Plex, cross-source folded).
- From the SMB library, open the item and play it; watch a minute, press Back.
- Look at the item-detail page: the Resume affordance / progress does not update in place. (Continue Watching does.)
Expected behavior
Leaving the player updates the detail page's resume position / progress bar in place immediately, the same way it does for Plex/Jellyfin titles ("watched N min, pressed Back, page reflects it").
Actual behavior
The SMB detail page keeps showing the pre-playback state until a reload/relaunch. Continue Watching updates correctly.
Affected backend(s)
SMB share
Plozz version & build
Dev branch thatcube-assess-emby-support.
tvOS version
tvOS (Apple TV 4K, current dev toolchain)
Apple TV model
Apple TV 4K
Logs / screenshots
Not scan-dependent. The refresh path is notification-driven, independent of library scanning:
- On stop,
makePlaybackStoppedHandler (Sources/AppShell/MainTabView.swift ~1268) builds a WatchMutation via WatchMutationFactory.playbackStop and calls watchBridge.finishPlayback(liveAccountID, liveItemID, percent, mutation) (~1304).
AppState.publishOptimisticWatchState (Sources/AppShell/AppState.swift ~566) posts a MediaItemMutation (resumePosition + playedPercentage) scoped to Set(mutation.targets.map(\.id)) where .id == "accountID:itemID".
ItemDetailView observes .mediaItemDidMutate (~226) → viewModel.applyWatchedState(mutation).
MediaItemMutation.targets(_:) (Sources/CoreModels/MediaItemActionHandling.swift ~140) matches when matches(accountID: item.sourceAccountID, itemID: item.id) OR item.sources.contains { scopedItemIDs.contains($0.id) }.
Root-cause direction (to confirm): For a cross-source item opened from the SMB library, the detail item's own sourceAccountID is typically the merge primary (which can be the Plex account), while playback correctly retargets to the SMB source (liveAccountID). The optimistic mutation's scoped targets come from convergingItem.sourceAccountID/item.id ∪ the identity-index union (additionalSources). The likely gaps to check:
- The scoped id of the played SMB source (
share:host/share#user:<smbItemID>) may not be present in the mutation's scopedItemIDs, or does not equal any detail.item.sources[].id, so targets(_:) returns false and applyWatchedState no-ops. Verify that WatchMutationFactory.targets(for:) includes the played SMB (accountID,itemID) (not just the merge-primary), and that the detail item's sources carry the SMB source with the same accountID:itemID.
- Confirm
MediaItemMutation.applied(to:) actually writes resumePosition/playedPercentage onto a movie detail item when matched (vs. only series/episode cascade).
Diagnostic note: the stop path logs via FanoutDiagnostics to os_log (not the on-disk playback-trace.log), so the mutation targets aren't in the pulled journal. To confirm on device, temporarily mirror FanoutDiagnostics.stopLine (mutationTargets) into HandoffDiagnostics.emit, reproduce one SMB play, and compare the target scoped ids against the detail item's sources[].id.
Deprioritized vs. the branch merge; filing with the trace so it isn't lost.
Summary
After playing a media-share (SMB) movie and returning to its item-detail page, the page does not immediately reflect the new watched position / resume state. The progress is saved — it appears in the Continue Watching row, and on the detail page after a manual reload or app relaunch — but the in-place optimistic refresh that Plex/Jellyfin get does not fire for the SMB copy. This is a UX-polish gap, not data loss.
Steps to reproduce
Expected behavior
Leaving the player updates the detail page's resume position / progress bar in place immediately, the same way it does for Plex/Jellyfin titles ("watched N min, pressed Back, page reflects it").
Actual behavior
The SMB detail page keeps showing the pre-playback state until a reload/relaunch. Continue Watching updates correctly.
Affected backend(s)
SMB share
Plozz version & build
Dev branch
thatcube-assess-emby-support.tvOS version
tvOS (Apple TV 4K, current dev toolchain)
Apple TV model
Apple TV 4K
Logs / screenshots
Not scan-dependent. The refresh path is notification-driven, independent of library scanning:
makePlaybackStoppedHandler(Sources/AppShell/MainTabView.swift ~1268) builds aWatchMutationviaWatchMutationFactory.playbackStopand callswatchBridge.finishPlayback(liveAccountID, liveItemID, percent, mutation)(~1304).AppState.publishOptimisticWatchState(Sources/AppShell/AppState.swift ~566) posts aMediaItemMutation(resumePosition+playedPercentage) scoped toSet(mutation.targets.map(\.id))where.id == "accountID:itemID".ItemDetailViewobserves.mediaItemDidMutate(~226) →viewModel.applyWatchedState(mutation).MediaItemMutation.targets(_:)(Sources/CoreModels/MediaItemActionHandling.swift ~140) matches whenmatches(accountID: item.sourceAccountID, itemID: item.id)ORitem.sources.contains { scopedItemIDs.contains($0.id) }.Root-cause direction (to confirm): For a cross-source item opened from the SMB library, the detail item's own
sourceAccountIDis typically the merge primary (which can be the Plex account), while playback correctly retargets to the SMB source (liveAccountID). The optimistic mutation's scoped targets come fromconvergingItem.sourceAccountID/item.id∪ the identity-index union (additionalSources). The likely gaps to check:share:host/share#user:<smbItemID>) may not be present in the mutation'sscopedItemIDs, or does not equal anydetail.item.sources[].id, sotargets(_:)returns false andapplyWatchedStateno-ops. Verify thatWatchMutationFactory.targets(for:)includes the played SMB(accountID,itemID)(not just the merge-primary), and that the detail item'ssourcescarry the SMB source with the sameaccountID:itemID.MediaItemMutation.applied(to:)actually writesresumePosition/playedPercentageonto a movie detail item when matched (vs. only series/episode cascade).Diagnostic note: the stop path logs via
FanoutDiagnosticsto os_log (not the on-diskplayback-trace.log), so the mutation targets aren't in the pulled journal. To confirm on device, temporarily mirrorFanoutDiagnostics.stopLine(mutationTargets) intoHandoffDiagnostics.emit, reproduce one SMB play, and compare the target scoped ids against the detail item'ssources[].id.Deprioritized vs. the branch merge; filing with the trace so it isn't lost.