Skip to content

[Bug]: SMB item-detail page doesn't reflect watched progress immediately after playback #11

Description

@thatcube

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

  1. Have a title that exists on an SMB share (and optionally also on Plex, cross-source folded).
  2. From the SMB library, open the item and play it; watch a minute, press Back.
  3. 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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions