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>
223 lines
7.7 KiB
Swift
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])
|
|
}
|
|
}
|