Aim a removal at the row the server counts

Remote playlist entries carried the display row number as their server
position, so a row the app could not parse shifted every removal after it
by one, and rows were paired back to entries by video id, which cannot
tell two copies of the same video apart.

- Piped entry positions are now the raw stream index; entries carry a
  separate displayRow for the row they are drawn as, filled in by both
  snapshot builders and used by the detail view's row map
- an emptied description clears a server-side playlist instead of being
  read as "leave it alone"
- a delete that reported failure forgets a playlist the refetch 404s on,
  and drops it from the account's cached list
- the browse view discards account-scoped content when a status change
  moves which account the instance resolves to
- every mutation re-reads the credential immediately before it is sent,
  which matters most after Piped's verify-before-remove fetch
- Handoff carries the owning account id, so a Piped playlist keeps its
  ownership on the receiving device
This commit is contained in:
Arkadiusz Fal
2026-09-02 23:56:59 +02:00
parent b3c4216013
commit 24b43ac866
11 changed files with 430 additions and 52 deletions

View File

@@ -444,16 +444,26 @@ actor PipedAPI: InstanceAPI {
nextpage: nil
).toPlaylist(instanceURL: instanceURL, playlistID: playlistID)
// Positions run 0-based across every page in fetch order and are taken
// over the same list the playlist displays, so `entries[n]` and
// `playlist.videos[n]` are always the same row. Piped removes by
// Positions run 0-based across every page in fetch order and are counted
// over the *raw* stream list, malformed rows included: that is the index
// `POST /user/playlists/remove` addresses, and renumbering over the
// filtered display list would aim a removal one row off for every row
// the app could not parse. `displayRow` is the separate number that says
// where the entry appears in `playlist.videos`. Piped removes by
// position, so these ids die with this snapshot.
let entries = playlist.videos.enumerated().map { offset, video in
RemotePlaylistEntry(
id: .piped(playlistID: playlistID, position: offset),
video: video,
position: offset
var displayRow = 0
var entries: [RemotePlaylistEntry] = []
for (position, item) in allStreams.enumerated() {
guard case .video(let video) = item else { continue }
entries.append(
RemotePlaylistEntry(
id: .piped(playlistID: playlistID, position: position),
video: video.toVideo(instanceURL: instanceURL),
position: position,
displayRow: displayRow
)
)
displayRow += 1
}
return RemotePlaylistSnapshot(playlist: playlist, entries: entries)