mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Read a playlist back before writing over it
Ten review findings in the remote-playlist work, all of them the same shape: something acted on what it remembered instead of what the server says now. - A confirmed-gone playlist is forgotten whole — snapshot, list entry, and both ordering fences — so a refresh that started before the delete can no longer republish it, and a list refetch that fails offline no longer leaves it on screen. - `.accountsDidChange` discards account-scoped content when the account it belonged to is no longer the effective one, as the status notification already did. - A tab that stops being offered no longer stays selected: the browse view moves to the first available one, which reloads by itself. - A Handoff account id is accepted only when that account is on the instance the playlist lives on. - Composite writes moved up into `AccountService`, over a transport whose every method is exactly one request: create-with-privacy, rename, and set-description. The credential is rechecked between each pair, so a sign-out between the two halves of a create or an edit can no longer send the second half under the previous user's secret. - Editing a playlist no longer forces it private. Invidious serialises visibility as `isListed` — `privacy.public?`, so unlisted and private are indistinguishable — which the snapshot now carries; the picker starts there, and sends nothing at all unless it was moved. Invidious keeps the stored visibility when the PATCH omits it (measured), so an untouched picker leaves it exactly as it was. - Tapping Edit on a server-side playlist awaits an authenticated refetch first and populates the form from it; a failed refetch shows an error and no sheet. - Playlist rows are keyed by row rather than by video id, which two rows of the same video share. - The add-to-playlist sheet offers only accounts whose instance is enabled and that hold a credential, alongside the write capability.
This commit is contained in:
@@ -307,77 +307,54 @@ actor ContentService: ContentServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a playlist for the credential's owner.
|
||||
/// Creates a playlist for the credential's owner. **One request.**
|
||||
///
|
||||
/// Neither backend takes a description when creating, so this does not take
|
||||
/// one either: setting it is a separate write, sequenced by the caller with
|
||||
/// a credential recheck in between.
|
||||
///
|
||||
/// The returned playlist is the little the server says about what it just
|
||||
/// made — an id and a title. Nothing else is invented here: callers refetch
|
||||
/// the list, which is the only description that is actually the server's.
|
||||
/// - Parameters:
|
||||
/// - privacy: Ignored by backends without a visibility concept (Piped),
|
||||
/// and defaulted to private on those that have one when not given.
|
||||
/// and defaulted to private on those that have one when not given —
|
||||
/// Invidious rejects a creation that carries no visibility at all.
|
||||
func createPlaylist(
|
||||
title: String,
|
||||
description: String?,
|
||||
privacy: PlaylistPrivacy?,
|
||||
instance: Instance,
|
||||
credential: String
|
||||
) async throws -> Playlist {
|
||||
switch instance.type {
|
||||
case .invidious:
|
||||
let api = await invidiousAPI(for: instance)
|
||||
let playlist = try await api.createPlaylist(
|
||||
return try await invidiousAPI(for: instance).createPlaylist(
|
||||
title: title,
|
||||
privacy: privacy ?? .private,
|
||||
instance: instance,
|
||||
sid: credential
|
||||
)
|
||||
// Creation takes no description, so a wanted one is a second call.
|
||||
// Its failure is the caller's to see: a playlist that exists with
|
||||
// the wrong description is not a silent success.
|
||||
if let description, !description.isEmpty {
|
||||
try await api.updatePlaylist(
|
||||
id: playlist.id.playlistID,
|
||||
title: title,
|
||||
description: description,
|
||||
privacy: privacy ?? .private,
|
||||
instance: instance,
|
||||
sid: credential
|
||||
)
|
||||
}
|
||||
return playlist
|
||||
|
||||
case .piped:
|
||||
let api = await pipedAPI(for: instance)
|
||||
let playlistID = try await api.createPlaylist(
|
||||
let playlistID = try await pipedAPI(for: instance).createPlaylist(
|
||||
name: title,
|
||||
instance: instance,
|
||||
authToken: credential
|
||||
)
|
||||
if let description, !description.isEmpty {
|
||||
try await api.updatePlaylistDescription(
|
||||
id: playlistID,
|
||||
description: description,
|
||||
instance: instance,
|
||||
authToken: credential
|
||||
)
|
||||
}
|
||||
return Playlist(id: .global(playlistID), title: title, description: description)
|
||||
return Playlist(id: .global(playlistID), title: title)
|
||||
|
||||
default:
|
||||
throw APIError.notSupported
|
||||
}
|
||||
}
|
||||
|
||||
/// Edits one of the credential owner's playlists.
|
||||
/// Renames one of the credential owner's playlists. **One request.**
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - description: `nil` leaves whatever the playlist has.
|
||||
/// - privacy: `nil` keeps the private default on backends that have
|
||||
/// visibility; ignored entirely on those that do not.
|
||||
func updatePlaylist(
|
||||
/// - Parameter privacy: the visibility to store, or `nil` to leave whatever
|
||||
/// the playlist has. Ignored on backends without a visibility concept.
|
||||
func renamePlaylist(
|
||||
id: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
privacy: PlaylistPrivacy?,
|
||||
instance: Instance,
|
||||
credential: String
|
||||
@@ -387,32 +364,55 @@ actor ContentService: ContentServiceProtocol {
|
||||
try await invidiousAPI(for: instance).updatePlaylist(
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
privacy: privacy ?? .private,
|
||||
description: nil,
|
||||
privacy: privacy,
|
||||
instance: instance,
|
||||
sid: credential
|
||||
)
|
||||
|
||||
case .piped:
|
||||
// Two calls, no transaction: Piped edits the name and the
|
||||
// description through separate endpoints. The rename goes first so
|
||||
// a failure of the second leaves the visible half applied, and the
|
||||
// caller's refetch shows exactly what landed.
|
||||
let api = await pipedAPI(for: instance)
|
||||
try await api.renamePlaylist(
|
||||
try await pipedAPI(for: instance).renamePlaylist(
|
||||
id: id,
|
||||
newName: title,
|
||||
instance: instance,
|
||||
authToken: credential
|
||||
)
|
||||
if let description {
|
||||
try await api.updatePlaylistDescription(
|
||||
id: id,
|
||||
description: description,
|
||||
instance: instance,
|
||||
authToken: credential
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
throw APIError.notSupported
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a playlist's description. **One request.**
|
||||
///
|
||||
/// - Parameter title: the playlist's title, resent unchanged. Invidious
|
||||
/// edits every attribute through one endpoint and refuses a body without
|
||||
/// a title; Piped has a description endpoint of its own and ignores this.
|
||||
func setPlaylistDescription(
|
||||
id: String,
|
||||
description: String,
|
||||
title: String,
|
||||
instance: Instance,
|
||||
credential: String
|
||||
) async throws {
|
||||
switch instance.type {
|
||||
case .invidious:
|
||||
try await invidiousAPI(for: instance).updatePlaylist(
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
privacy: nil,
|
||||
instance: instance,
|
||||
sid: credential
|
||||
)
|
||||
|
||||
case .piped:
|
||||
try await pipedAPI(for: instance).updatePlaylistDescription(
|
||||
id: id,
|
||||
description: description,
|
||||
instance: instance,
|
||||
authToken: credential
|
||||
)
|
||||
|
||||
default:
|
||||
throw APIError.notSupported
|
||||
|
||||
Reference in New Issue
Block a user