Bind the playlist import to the login that listed it

Checking that some credential exists was the wrong question. Everything
on screen was read with one particular login, and a replacement is a
different person's account on the same server — its playlist IDs mean
something else entirely, so importing "the same" playlist under it
fetches whatever that ID happens to name there. The credential the list
was loaded with is now recorded, and every import is checked against it
rather than against whatever is current.

The two checks around creating the local playlist are a suspension
apart, so the login can still go between them. Nothing has been read at
that point, and an empty playlist named after somebody else's is not
worth keeping.
This commit is contained in:
Arkadiusz Fal
2026-08-30 23:23:36 +02:00
parent 13b1fe9c0f
commit 26cc4aba25

View File

@@ -29,6 +29,11 @@ struct ImportPlaylistsView: View {
/// The merge started from the confirmation dialog, which runs detached
/// from the batch that queued it. Held so the batch can wait for it.
@State private var mergeImportTask: Task<Void, Never>?
/// The credential the listed playlists were read with. Everything on
/// screen belongs to it, so every import is checked against this rather
/// than against whatever is current — a replacement is a different
/// person's account, and its playlist IDs mean something else entirely.
@State private var loadedCredential: String?
// Merge warning state
@State private var showMergeWarning = false
@@ -278,6 +283,8 @@ struct ImportPlaylistsView: View {
isLoading = true
error = nil
loadedCredential = credential
loginChangedDuringImport = false
do {
let fetchedPlaylists: [Playlist]
@@ -319,7 +326,7 @@ struct ImportPlaylistsView: View {
// Checked before anything is created: a playlist made for a login that
// has already gone would be left behind empty, because the import
// aborts on its first write.
guard appEnvironment.credential(forInstance: instance) != nil else {
guard appEnvironment.credential(forInstance: instance) == loadedCredential else {
loginChangedDuringImport = true
return
}
@@ -338,15 +345,25 @@ struct ImportPlaylistsView: View {
// Create new local playlist and import
let localPlaylist = appEnvironment.dataManager.createPlaylist(title: playlist.title)
await performImport(playlist, into: localPlaylist)
// The guard above and the one inside are a suspension apart, so the
// login can still go in between. Nothing was read, and an empty
// playlist named after somebody else's is not worth keeping.
if loginChangedDuringImport, localPlaylist.items?.isEmpty ?? true {
appEnvironment.dataManager.deletePlaylist(localPlaylist)
}
}
private func performImport(_ playlist: Playlist, into localPlaylist: LocalPlaylist) async {
guard let appEnvironment else { return }
// The login can go between choosing this playlist and reading it —
// the merge dialog in particular waits on the user. Returning quietly
// would leave the row importing forever and let a batch carry on.
guard let credential = appEnvironment.credential(forInstance: instance) else {
// The login can go, or be replaced, between choosing this playlist and
// reading it — the merge dialog in particular waits on the user.
// Returning quietly would leave the row importing forever and let a
// batch carry on; reading it under a replacement would fetch whatever
// that ID means to somebody else.
guard let credential = appEnvironment.credential(forInstance: instance),
credential == loadedCredential else {
loginChangedDuringImport = true
importingPlaylistID = nil
importProgress = nil