mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
Pending deletes were lost across app restarts because recoverPersistedPendingChanges() never reconstructed CKRecord.ID objects from persisted record names. Additionally, incoming iCloud records for deleted playlists were blindly applied, and orphaned playlist items in CloudKit would recreate placeholder playlists. - Rebuild pendingDeletes array from UserDefaults on recovery - Guard applyRemoteRecord against records pending local deletion - Skip deferred items whose parent playlist is pending deletion - Queue all playlist item deletions when deleting a playlist - Clean up placeholder playlists for pending-delete playlists
171 lines
5.3 KiB
Swift
171 lines
5.3 KiB
Swift
//
|
|
// DataManager+Playlists.swift
|
|
// Yattee
|
|
//
|
|
// Local playlist operations for DataManager.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
extension DataManager {
|
|
// MARK: - Local Playlists
|
|
|
|
/// Creates a new local playlist.
|
|
func createPlaylist(title: String, description: String? = nil) -> LocalPlaylist {
|
|
let playlist = LocalPlaylist(title: title, description: description)
|
|
modelContext.insert(playlist)
|
|
save()
|
|
|
|
// Queue for CloudKit sync
|
|
cloudKitSync?.queuePlaylistSave(playlist)
|
|
|
|
NotificationCenter.default.post(name: .playlistsDidChange, object: nil)
|
|
return playlist
|
|
}
|
|
|
|
/// Updates a playlist's title and description.
|
|
func updatePlaylist(_ playlist: LocalPlaylist, title: String, description: String?) {
|
|
playlist.title = title
|
|
playlist.playlistDescription = description
|
|
playlist.updatedAt = Date()
|
|
save()
|
|
|
|
// Queue for CloudKit sync
|
|
cloudKitSync?.queuePlaylistSave(playlist)
|
|
|
|
NotificationCenter.default.post(name: .playlistsDidChange, object: nil)
|
|
}
|
|
|
|
/// Deletes a local playlist.
|
|
func deletePlaylist(_ playlist: LocalPlaylist) {
|
|
let playlistID = playlist.id
|
|
let itemIDs = playlist.sortedItems.map { $0.id }
|
|
|
|
modelContext.delete(playlist)
|
|
save()
|
|
|
|
// Queue playlist and all its items for CloudKit deletion
|
|
cloudKitSync?.queuePlaylistDelete(playlistID: playlistID)
|
|
for itemID in itemIDs {
|
|
cloudKitSync?.queuePlaylistItemDelete(itemID: itemID)
|
|
}
|
|
|
|
NotificationCenter.default.post(name: .playlistsDidChange, object: nil)
|
|
}
|
|
|
|
/// Gets all local playlists.
|
|
func playlists() -> [LocalPlaylist] {
|
|
let descriptor = FetchDescriptor<LocalPlaylist>(
|
|
sortBy: [SortDescriptor(\.updatedAt, order: .reverse)]
|
|
)
|
|
|
|
do {
|
|
return try modelContext.fetch(descriptor)
|
|
} catch {
|
|
LoggingService.shared.logCloudKitError("Failed to fetch playlists", error: error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
/// Gets a playlist by its ID.
|
|
func playlist(forID id: UUID) -> LocalPlaylist? {
|
|
let descriptor = FetchDescriptor<LocalPlaylist>(
|
|
predicate: #Predicate { $0.id == id }
|
|
)
|
|
return try? modelContext.fetch(descriptor).first
|
|
}
|
|
|
|
/// Gets a playlist item by its ID.
|
|
func playlistItem(forID id: UUID) -> LocalPlaylistItem? {
|
|
let descriptor = FetchDescriptor<LocalPlaylistItem>(
|
|
predicate: #Predicate { $0.id == id }
|
|
)
|
|
return try? modelContext.fetch(descriptor).first
|
|
}
|
|
|
|
/// Inserts a playlist into the database.
|
|
/// Used by CloudKitSyncEngine for applying remote playlists.
|
|
func insertPlaylist(_ playlist: LocalPlaylist) {
|
|
// Check for duplicates
|
|
let id = playlist.id
|
|
let descriptor = FetchDescriptor<LocalPlaylist>(
|
|
predicate: #Predicate { $0.id == id }
|
|
)
|
|
|
|
do {
|
|
let existing = try modelContext.fetch(descriptor)
|
|
if existing.isEmpty {
|
|
modelContext.insert(playlist)
|
|
save()
|
|
}
|
|
} catch {
|
|
// Insert anyway if we can't check
|
|
modelContext.insert(playlist)
|
|
save()
|
|
}
|
|
}
|
|
|
|
/// Inserts a playlist item into the database.
|
|
/// Used by CloudKitSyncEngine for applying remote playlist items.
|
|
func insertPlaylistItem(_ item: LocalPlaylistItem) {
|
|
// Check for duplicates
|
|
let id = item.id
|
|
let descriptor = FetchDescriptor<LocalPlaylistItem>(
|
|
predicate: #Predicate { $0.id == id }
|
|
)
|
|
|
|
do {
|
|
let existing = try modelContext.fetch(descriptor)
|
|
if existing.isEmpty {
|
|
modelContext.insert(item)
|
|
save()
|
|
}
|
|
} catch {
|
|
// Insert anyway if we can't check
|
|
modelContext.insert(item)
|
|
save()
|
|
}
|
|
}
|
|
|
|
/// Deletes a playlist item.
|
|
/// Used by CloudKitSyncEngine for applying remote deletions.
|
|
func deletePlaylistItem(_ item: LocalPlaylistItem) {
|
|
modelContext.delete(item)
|
|
save()
|
|
}
|
|
|
|
/// Adds a video to a playlist.
|
|
func addToPlaylist(_ video: Video, playlist: LocalPlaylist) {
|
|
guard !playlist.contains(videoID: video.id.videoID) else {
|
|
return
|
|
}
|
|
playlist.addVideo(video)
|
|
save()
|
|
|
|
// Queue for CloudKit sync (will sync playlist and all items)
|
|
cloudKitSync?.queuePlaylistSave(playlist)
|
|
|
|
NotificationCenter.default.post(name: .playlistsDidChange, object: nil)
|
|
}
|
|
|
|
/// Removes a video from a playlist.
|
|
func removeVideoFromPlaylist(at index: Int, playlist: LocalPlaylist) {
|
|
guard index < playlist.sortedItems.count else { return }
|
|
let item = playlist.sortedItems[index]
|
|
let itemID = item.id
|
|
|
|
// Remove from playlist
|
|
playlist.removeVideo(at: index)
|
|
save()
|
|
|
|
// Queue updated playlist for CloudKit sync
|
|
cloudKitSync?.queuePlaylistSave(playlist)
|
|
|
|
// Also delete the orphaned item from CloudKit
|
|
cloudKitSync?.queuePlaylistItemDelete(itemID: itemID)
|
|
|
|
NotificationCenter.default.post(name: .playlistsDidChange, object: nil)
|
|
}
|
|
}
|