Ferrite-backup/Ferrite/DataManagement/PersistenceController.swift
kingbri 9b7bc55a25 Library: Add support for RealDebrid cloud
RealDebrid saves a user's unrestricted links and "torrents" (magnet
links in this case). Add the ability to see and queue a user's RD
library in Ferrite itself.

This required a further abstraction of the debrid manager to allow
for more types other than search results to be passed to various
functions.

Deleting an item from RD's cloud list deletes the item from RD as well.

NOTE: This does not track download progress, but it does show if a
magnet is currently being downloaded or not.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-01-02 15:13:32 -05:00

223 lines
7.7 KiB
Swift

//
// PersistenceController.swift
// Ferrite
//
// Created by Brian Dashore on 7/24/22.
//
import CoreData
enum HistoryDeleteRange {
case day
case week
case month
case allTime
}
enum HistoryDeleteError: Error {
case noDate(String)
case unknown(String)
}
// No iCloud until finalized sources
struct PersistenceController {
static let shared = PersistenceController()
// Coredata storage
let container: NSPersistentContainer
// Background context for writes
let backgroundContext: NSManagedObjectContext
// Coredata load
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "FerriteDB")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
guard let description = container.persistentStoreDescriptions.first else {
fatalError("CoreData: Failed to find a persistent store description")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores { _, error in
if let error {
fatalError("CoreData init error: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
try? container.viewContext.setQueryGenerationFrom(.current)
backgroundContext = container.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
try? backgroundContext.setQueryGenerationFrom(.current)
}
func save(_ context: NSManagedObjectContext? = nil) {
let context = context ?? container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
debugPrint("Error in CoreData saving! \(error.localizedDescription)")
}
}
}
// By default, delete objects using the ViewContext unless specified
func delete(_ object: NSManagedObject, context: NSManagedObjectContext? = nil) {
let context = context ?? container.viewContext
if context != container.viewContext {
let wrappedObject = try? context.existingObject(with: object.objectID)
if let backgroundObject = wrappedObject {
context.delete(backgroundObject)
save(context)
return
}
}
container.viewContext.delete(object)
save()
}
func createBookmark(_ bookmarkJson: BookmarkJson) {
let bookmarkRequest = Bookmark.fetchRequest()
bookmarkRequest.predicate = NSPredicate(
format: "source == %@ AND title == %@ AND magnetLink == %@",
bookmarkJson.source,
bookmarkJson.title ?? "",
bookmarkJson.magnetLink ?? ""
)
if (try? backgroundContext.fetch(bookmarkRequest).first) != nil {
return
}
let newBookmark = Bookmark(context: backgroundContext)
newBookmark.title = bookmarkJson.title
newBookmark.source = bookmarkJson.source
newBookmark.magnetHash = bookmarkJson.magnetHash
newBookmark.magnetLink = bookmarkJson.magnetLink
newBookmark.seeders = bookmarkJson.seeders
newBookmark.leechers = bookmarkJson.leechers
save(backgroundContext)
}
func createHistory(_ entryJson: HistoryEntryJson, date: Double? = nil) {
let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate)
let newHistoryEntry = HistoryEntry(context: backgroundContext)
newHistoryEntry.source = entryJson.source
newHistoryEntry.name = entryJson.name
newHistoryEntry.url = entryJson.url
newHistoryEntry.subName = entryJson.subName
newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970
let historyRequest = History.fetchRequest()
historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString)
// Safely add entries to a parent history if it exists
if var histories = try? backgroundContext.fetch(historyRequest) {
for (i, history) in histories.enumerated() {
let existingEntries = history.entryArray.filter { $0.url == newHistoryEntry.url && $0.name == newHistoryEntry.name }
if !existingEntries.isEmpty {
for entry in existingEntries {
PersistenceController.shared.delete(entry, context: backgroundContext)
}
}
if history.entryArray.isEmpty {
PersistenceController.shared.delete(history, context: backgroundContext)
histories.remove(at: i)
}
}
newHistoryEntry.parentHistory = histories.first ?? History(context: backgroundContext)
} else {
newHistoryEntry.parentHistory = History(context: backgroundContext)
}
newHistoryEntry.parentHistory?.dateString = historyDateString
newHistoryEntry.parentHistory?.date = historyDate
save(backgroundContext)
}
func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? {
if range == .allTime {
return nil
}
var components = Calendar.current.dateComponents([.day, .month, .year], from: Date())
components.hour = 0
components.minute = 0
components.second = 0
guard let today = Calendar.current.date(from: components) else {
return nil
}
var offsetComponents = DateComponents(day: 1)
guard let tomorrow = Calendar.current.date(byAdding: offsetComponents, to: today) else {
return nil
}
switch range {
case .week:
offsetComponents.day = -7
case .month:
offsetComponents.day = -28
default:
break
}
guard var offsetDate = Calendar.current.date(byAdding: offsetComponents, to: today) else {
return nil
}
if TimeZone.current.isDaylightSavingTime(for: offsetDate) {
offsetDate = offsetDate.addingTimeInterval(3600)
}
let predicate = NSPredicate(format: "date >= %@ && date < %@", range == .day ? today as NSDate : offsetDate as NSDate, tomorrow as NSDate)
return predicate
}
// Always use the background context to batch delete
// Merge changes into both contexts to update views
func batchDeleteHistory(range: HistoryDeleteRange) throws {
let predicate = getHistoryPredicate(range: range)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "History")
if let predicate {
fetchRequest.predicate = predicate
} else if range != .allTime {
throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?")
}
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try backgroundContext.execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [container.viewContext, backgroundContext])
}
}