Files
yattee/Yattee/Data/DataManager+Playlists.swift
Arkadiusz Fal 45e19e6536 Open more YouTube link types and playlists from links
Route YouTube search results links to search and nocookie embeds to the
video, match YouTube hosts exactly, and report YouTube pages without a
video as unsupported instead of sending them to yt-dlp. A watch link
inside a playlist shows both the video and the playlist when loaded, and
plays the video with the playlist queued around it. URL scheme play,
queue and bookmark actions accept playlist links.
2026-10-08 22:50:36 +02:00

195 lines
6.2 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
}
/// A title no local playlist has yet: the base, else "Base (2)", "Base (3)"…
func uniquePlaylistTitle(_ baseTitle: String) -> String {
let existingTitles = Set(playlists().map(\.title))
guard existingTitles.contains(baseTitle) else { return baseTitle }
var counter = 2
while existingTitles.contains("\(baseTitle) (\(counter))") {
counter += 1
}
return "\(baseTitle) (\(counter))"
}
/// Saves a remote playlist's videos as a new local playlist.
@discardableResult
func importPlaylist(_ playlist: Playlist) -> LocalPlaylist {
let localPlaylist = createPlaylist(
title: uniquePlaylistTitle(playlist.title),
description: playlist.description
)
for video in playlist.videos {
addToPlaylist(video, playlist: localPlaylist)
}
return localPlaylist
}
/// 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)
}
}