Ferrite-backup/Ferrite/ViewModels/PluginManager.swift
kingbri 7202a95bb2 Ferrite: Parallel tasks and logging
Make all tasks run in parallel to increase responsiveness and efficiency
when fetching new data.

However, parallel tasks means that toast errors are no longer feasible.
Instead, add a logging system which has a more detailed view of
app messages and direct the user there if there is an error.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-03-09 18:48:28 -05:00

766 lines
32 KiB
Swift

//
// SourceManager.swift
// Ferrite
//
// Created by Brian Dashore on 7/25/22.
//
import Foundation
import SwiftUI
public class PluginManager: ObservableObject {
var logManager: LoggingManager?
let kodi: Kodi = .init()
@Published var availableSources: [SourceJson] = []
@Published var availableActions: [ActionJson] = []
@Published var showActionErrorAlert = false
@Published var actionErrorAlertMessage: String = ""
@Published var showActionSuccessAlert = false
@Published var actionSuccessAlertMessage: String = ""
@MainActor
func cleanAvailablePlugins() {
availableSources = []
availableActions = []
}
@MainActor
func updateAvailablePlugins(_ newPlugins: AvailablePlugins) {
availableSources += newPlugins.availableSources
availableActions += newPlugins.availableActions
}
public func fetchPluginsFromUrl() async {
let pluginListRequest = PluginList.fetchRequest()
guard let pluginLists = try? PersistenceController.shared.backgroundContext.fetch(pluginListRequest) else {
await logManager?.error("PluginManager: No plugin lists found")
return
}
// Clean availablePlugin arrays for repopulation
await cleanAvailablePlugins()
await logManager?.info("Starting fetch of plugin lists")
await withTaskGroup(of: (AvailablePlugins?, String).self) { group in
for pluginList in pluginLists {
guard let url = URL(string: pluginList.urlString) else {
return
}
group.addTask {
var availablePlugins: AvailablePlugins?
do {
availablePlugins = try await self.fetchPluginList(pluginList: pluginList, url: url)
} catch {
let error = error as NSError
switch error.code {
case -999:
await self.logManager?.info("PluginManager: \(pluginList.name): List fetch cancelled")
case -1009:
await self.logManager?.info("PluginManager: \(pluginList.name): The connection is offline")
default:
await self.logManager?.error("Plugin fetch: \(pluginList.name): \(error)", showToast: false)
}
}
return (availablePlugins, pluginList.name)
}
}
var failedLists: [String] = []
for await (availablePlugins, pluginListName) in group {
if let availablePlugins {
await updateAvailablePlugins(availablePlugins)
} else {
failedLists.append(pluginListName)
}
}
if !failedLists.isEmpty {
let joinedLists = failedLists.joined(separator: ", ")
await logManager?.info(
"Plugins: Errors in plugin lists \(joinedLists). See above.",
description: "There were errors in plugin lists \(joinedLists). Check the logs for more details."
)
}
}
await logManager?.info("Plugin list fetch finished")
}
func fetchPluginList(pluginList: PluginList, url: URL) async throws -> AvailablePlugins? {
var tempSources: [SourceJson] = []
var tempActions: [ActionJson] = []
// Always get the up-to-date source list
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
let (data, _) = try await URLSession.shared.data(for: request)
let pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data)
if let sources = pluginResponse.sources {
// Faster and more performant to map instead of a for loop
tempSources += sources.compactMap { inputJson in
if checkAppVersion(minVersion: inputJson.minVersion) {
return SourceJson(
name: inputJson.name,
version: inputJson.version,
minVersion: inputJson.minVersion,
baseUrl: inputJson.baseUrl,
fallbackUrls: inputJson.fallbackUrls,
dynamicBaseUrl: inputJson.dynamicBaseUrl,
trackers: inputJson.trackers,
api: inputJson.api,
jsonParser: inputJson.jsonParser,
rssParser: inputJson.rssParser,
htmlParser: inputJson.htmlParser,
author: pluginList.author,
listId: pluginList.id,
tags: inputJson.tags
)
} else {
return nil
}
}
}
if let actions = pluginResponse.actions {
tempActions += actions.compactMap { inputJson in
if checkAppVersion(minVersion: inputJson.minVersion) {
return ActionJson(
name: inputJson.name,
version: inputJson.version,
minVersion: inputJson.minVersion,
requires: inputJson.requires,
deeplink: inputJson.deeplink,
author: pluginList.author,
listId: pluginList.id,
tags: inputJson.tags
)
} else {
return nil
}
}
}
return AvailablePlugins(availableSources: tempSources, availableActions: tempActions)
}
// forType required to guide generic inferences
func fetchFilteredPlugins<PJ: PluginJson>(forType: PJ.Type,
installedPlugins: FetchedResults<some Plugin>,
searchText: String) -> [PJ]
{
let availablePlugins: [PJ] = fetchCastedPlugins(forType)
return availablePlugins
.filter { availablePlugin in
let pluginExists = installedPlugins.contains(where: {
availablePlugin.name == $0.name &&
availablePlugin.listId == $0.listId &&
availablePlugin.author == $0.author
})
if searchText.isEmpty {
return !pluginExists
} else {
return !pluginExists && availablePlugin.name.lowercased().contains(searchText.lowercased())
}
}
}
func fetchUpdatedPlugins<PJ: PluginJson>(forType: PJ.Type,
installedPlugins: FetchedResults<some Plugin>,
searchText: String) -> [PJ]
{
var updatedPlugins: [PJ] = []
let availablePlugins: [PJ] = fetchCastedPlugins(forType)
for plugin in installedPlugins {
if let availablePlugin = availablePlugins.first(where: {
plugin.listId == $0.listId &&
plugin.name == $0.name &&
plugin.author == $0.author
}),
availablePlugin.version > plugin.version
{
updatedPlugins.append(availablePlugin)
}
}
return updatedPlugins
.filter {
searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased())
}
}
func fetchCastedPlugins<PJ: PluginJson>(_ forType: PJ.Type) -> [PJ] {
switch String(describing: PJ.self) {
case "SourceJson":
return availableSources as? [PJ] ?? []
case "ActionJson":
return availableActions as? [PJ] ?? []
default:
return []
}
}
// Checks if the current app version is supported by the source
func checkAppVersion(minVersion: String?) -> Bool {
// If there's no min version, assume that every version is supported
guard let minVersion else {
return true
}
return Application.shared.appVersion >= minVersion
}
// Fetches sources using the background context
public func fetchInstalledSources() -> [Source] {
let backgroundContext = PersistenceController.shared.backgroundContext
if let sources = try? backgroundContext.fetch(Source.fetchRequest()) {
return sources.compactMap { $0 }
} else {
return []
}
}
@MainActor
public func runDebridAction(urlString: String?, navModel: NavigationViewModel) {
let context = PersistenceController.shared.backgroundContext
if
let defaultDebridActionName = UserDefaults.standard.string(forKey: "Actions.DefaultDebridName"),
let defaultDebridActionList = UserDefaults.standard.string(forKey: "Actions.DefaultDebridList")
{
let actionFetchRequest = Action.fetchRequest()
actionFetchRequest.fetchLimit = 1
actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultDebridActionName, defaultDebridActionList)
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
runDeeplinkAction(fetchedAction, urlString: urlString)
} else {
navModel.currentChoiceSheet = .action
UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName")
UserDefaults.standard.set(nil, forKey: "Action.DefaultDebridList")
actionErrorAlertMessage =
"The default action could not be run. The action choice sheet has been opened. \n\n" +
"Please check your default actions in Settings"
showActionErrorAlert.toggle()
}
} else {
navModel.currentChoiceSheet = .action
}
}
@MainActor
public func runMagnetAction(urlString: String?, navModel: NavigationViewModel) {
let context = PersistenceController.shared.backgroundContext
if
let defaultMagnetActionName = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetName"),
let defaultMagnetActionList = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetList")
{
let actionFetchRequest = Action.fetchRequest()
actionFetchRequest.fetchLimit = 1
actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultMagnetActionName, defaultMagnetActionList)
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
runDeeplinkAction(fetchedAction, urlString: urlString)
} else {
navModel.currentChoiceSheet = .action
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName")
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList")
actionErrorAlertMessage =
"The default action could not be run. The action choice sheet has been opened. \n\n" +
"Please check your default actions in Settings"
showActionErrorAlert.toggle()
}
} else {
navModel.currentChoiceSheet = .action
}
}
// The iOS version of Ferrite only runs deeplink actions
@MainActor
public func runDeeplinkAction(_ action: Action, urlString: String?) {
guard let deeplink = action.deeplink, let urlString else {
actionErrorAlertMessage = "Could not run action: \(action.name) since there is no deeplink to execute. Contact the action dev!"
showActionErrorAlert.toggle()
print("Could not run action: \(action.name) since there is no deeplink to execute.")
return
}
let playbackUrl = URL(string: deeplink.replacingOccurrences(of: "{link}", with: urlString))
if let playbackUrl {
UIApplication.shared.open(playbackUrl)
} else {
actionErrorAlertMessage = "Could not run action: \(action.name) because the created deeplink was invalid. Contact the action dev!"
showActionErrorAlert.toggle()
print("Could not run action: \(action.name) because the created deeplink (\(String(describing: playbackUrl))) was invalid")
}
}
@MainActor
public func sendToKodi(urlString: String?) async {
guard let urlString else {
actionErrorAlertMessage = "Could not send URL to Kodi since there is no playback URL to send"
showActionErrorAlert.toggle()
print("Could not send URL to Kodi since there is no playback URL to send")
return
}
do {
try await kodi.sendVideoUrl(urlString: urlString)
actionSuccessAlertMessage = "Your URL should be playing on Kodi"
showActionSuccessAlert.toggle()
} catch {
actionErrorAlertMessage = "Kodi Error: \(error)"
showActionErrorAlert.toggle()
print("Kodi action error: \(error)")
}
}
public func installAction(actionJson: ActionJson?, doUpsert: Bool = false) async {
guard let actionJson else {
await logManager?.error("Action addition: No action present. Contact the app dev!")
return
}
let backgroundContext = PersistenceController.shared.backgroundContext
if actionJson.requires.count < 1 {
await logManager?.error("Action addition: actions must require an input. Please contact the action dev!")
return
}
guard let deeplink = actionJson.deeplink else {
await logManager?.error("Action addition: only deeplink actions can be added to Ferrite iOS. Please contact the action dev!")
return
}
let existingActionRequest = Action.fetchRequest()
existingActionRequest.predicate = NSPredicate(format: "name == %@", actionJson.name)
existingActionRequest.fetchLimit = 1
if let existingAction = try? backgroundContext.fetch(existingActionRequest).first {
if doUpsert {
PersistenceController.shared.delete(existingAction, context: backgroundContext)
} else {
await logManager?.error("Action addition: Could not install action with name \(actionJson.name) because it is already installed")
return
}
}
let newAction = Action(context: backgroundContext)
newAction.id = UUID()
newAction.name = actionJson.name
newAction.version = actionJson.version
newAction.author = actionJson.author ?? "Unknown"
newAction.listId = actionJson.listId
newAction.requires = actionJson.requires.map(\.rawValue)
newAction.enabled = true
if let jsonTags = actionJson.tags {
for tag in jsonTags {
let newTag = PluginTag(context: backgroundContext)
newTag.name = tag.name
newTag.colorHex = tag.colorHex
newTag.parentAction = newAction
}
}
newAction.deeplink = deeplink
do {
try backgroundContext.save()
} catch {
await logManager?.error("Action addition: \(error)")
}
}
public func installSource(sourceJson: SourceJson?, doUpsert: Bool = false) async {
guard let sourceJson else {
await logManager?.error("Source addition: No source present. Contact the app dev!")
return
}
let backgroundContext = PersistenceController.shared.backgroundContext
// If there's no base URL and it isn't dynamic, return before any transactions occur
let dynamicBaseUrl = sourceJson.dynamicBaseUrl ?? false
if !dynamicBaseUrl, sourceJson.baseUrl == nil {
await logManager?.error("Not adding this source because base URL parameters are malformed. Please contact the source dev.")
return
}
// If a source exists, don't add the new one unless upserting
let existingSourceRequest = Source.fetchRequest()
existingSourceRequest.predicate = NSPredicate(format: "name == %@", sourceJson.name)
existingSourceRequest.fetchLimit = 1
if let existingSource = try? backgroundContext.fetch(existingSourceRequest).first {
if doUpsert {
PersistenceController.shared.delete(existingSource, context: backgroundContext)
} else {
await logManager?.error("Source addition: Could not install source with name \(sourceJson.name) because it is already installed.")
return
}
}
let newSource = Source(context: backgroundContext)
newSource.id = UUID()
newSource.name = sourceJson.name
newSource.version = sourceJson.version
newSource.dynamicBaseUrl = dynamicBaseUrl
newSource.baseUrl = sourceJson.baseUrl
newSource.fallbackUrls = dynamicBaseUrl ? nil : sourceJson.fallbackUrls
newSource.author = sourceJson.author ?? "Unknown"
newSource.listId = sourceJson.listId
newSource.trackers = sourceJson.trackers
if let jsonTags = sourceJson.tags {
for tag in jsonTags {
let newTag = PluginTag(context: backgroundContext)
newTag.name = tag.name
newTag.colorHex = tag.colorHex
newTag.parentSource = newSource
}
}
if let sourceApiJson = sourceJson.api {
addSourceApi(newSource: newSource, apiJson: sourceApiJson)
}
if let jsonParserJson = sourceJson.jsonParser {
addJsonParser(newSource: newSource, jsonParserJson: jsonParserJson)
}
// Adds an RSS parser if present
if let rssParserJson = sourceJson.rssParser {
addRssParser(newSource: newSource, rssParserJson: rssParserJson)
}
// Adds an HTML parser if present
if let htmlParserJson = sourceJson.htmlParser {
addHtmlParser(newSource: newSource, htmlParserJson: htmlParserJson)
}
// Add an API condition as well
if newSource.jsonParser != nil {
newSource.preferredParser = Int16(SourcePreferredParser.siteApi.rawValue)
} else if newSource.rssParser != nil {
newSource.preferredParser = Int16(SourcePreferredParser.rss.rawValue)
} else {
newSource.preferredParser = Int16(SourcePreferredParser.scraping.rawValue)
}
newSource.enabled = true
do {
try backgroundContext.save()
} catch {
await logManager?.error("Source addition error: \(error)")
}
}
func addSourceApi(newSource: Source, apiJson: SourceApiJson) {
let backgroundContext = PersistenceController.shared.backgroundContext
let newSourceApi = SourceApi(context: backgroundContext)
newSourceApi.apiUrl = apiJson.apiUrl
if let clientIdJson = apiJson.clientId {
let newClientId = SourceApiClientId(context: backgroundContext)
newClientId.query = clientIdJson.query
newClientId.urlString = clientIdJson.url
newClientId.dynamic = clientIdJson.dynamic ?? false
newClientId.value = clientIdJson.value
newClientId.responseType = clientIdJson.responseType?.rawValue ?? ApiCredentialResponseType.json.rawValue
newClientId.expiryLength = clientIdJson.expiryLength ?? 0
newClientId.timeStamp = Date()
newSourceApi.clientId = newClientId
}
if let clientSecretJson = apiJson.clientSecret {
let newClientSecret = SourceApiClientSecret(context: backgroundContext)
newClientSecret.query = clientSecretJson.query
newClientSecret.urlString = clientSecretJson.url
newClientSecret.dynamic = clientSecretJson.dynamic ?? false
newClientSecret.value = clientSecretJson.value
newClientSecret.responseType = clientSecretJson.responseType?.rawValue ?? ApiCredentialResponseType.json.rawValue
newClientSecret.expiryLength = clientSecretJson.expiryLength ?? 0
newClientSecret.timeStamp = Date()
newSourceApi.clientSecret = newClientSecret
}
newSource.api = newSourceApi
}
func addJsonParser(newSource: Source, jsonParserJson: SourceJsonParserJson) {
let backgroundContext = PersistenceController.shared.backgroundContext
let newSourceJsonParser = SourceJsonParser(context: backgroundContext)
newSourceJsonParser.searchUrl = jsonParserJson.searchUrl
newSourceJsonParser.results = jsonParserJson.results
newSourceJsonParser.subResults = jsonParserJson.subResults
// Tune these complex queries to the final JSON parser format
if let magnetLinkJson = jsonParserJson.magnetLink {
let newSourceMagnetLink = SourceMagnetLink(context: backgroundContext)
newSourceMagnetLink.query = magnetLinkJson.query
newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text"
newSourceMagnetLink.discriminator = magnetLinkJson.discriminator
newSourceJsonParser.magnetLink = newSourceMagnetLink
}
if let magnetHashJson = jsonParserJson.magnetHash {
let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext)
newSourceMagnetHash.query = magnetHashJson.query
newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text"
newSourceMagnetHash.discriminator = magnetHashJson.discriminator
newSourceJsonParser.magnetHash = newSourceMagnetHash
}
if let subNameJson = jsonParserJson.subName {
let newSourceSubName = SourceSubName(context: backgroundContext)
newSourceSubName.query = subNameJson.query
newSourceSubName.attribute = subNameJson.query
newSourceSubName.discriminator = subNameJson.discriminator
newSourceJsonParser.subName = newSourceSubName
}
if let titleJson = jsonParserJson.title {
let newSourceTitle = SourceTitle(context: backgroundContext)
newSourceTitle.query = titleJson.query
newSourceTitle.attribute = titleJson.attribute ?? "text"
newSourceTitle.discriminator = titleJson.discriminator
newSourceJsonParser.title = newSourceTitle
}
if let sizeJson = jsonParserJson.size {
let newSourceSize = SourceSize(context: backgroundContext)
newSourceSize.query = sizeJson.query
newSourceSize.attribute = sizeJson.attribute ?? "text"
newSourceSize.discriminator = sizeJson.discriminator
newSourceJsonParser.size = newSourceSize
}
if let seedLeechJson = jsonParserJson.sl {
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
newSourceSeedLeech.seeders = seedLeechJson.seeders
newSourceSeedLeech.leechers = seedLeechJson.leechers
newSourceSeedLeech.combined = seedLeechJson.combined
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
newSourceSeedLeech.discriminator = seedLeechJson.discriminator
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
newSourceJsonParser.seedLeech = newSourceSeedLeech
}
newSource.jsonParser = newSourceJsonParser
}
func addRssParser(newSource: Source, rssParserJson: SourceRssParserJson) {
let backgroundContext = PersistenceController.shared.backgroundContext
let newSourceRssParser = SourceRssParser(context: backgroundContext)
newSourceRssParser.rssUrl = rssParserJson.rssUrl
newSourceRssParser.searchUrl = rssParserJson.searchUrl
newSourceRssParser.items = rssParserJson.items
if let magnetLinkJson = rssParserJson.magnetLink {
let newSourceMagnetLink = SourceMagnetLink(context: backgroundContext)
newSourceMagnetLink.query = magnetLinkJson.query
newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text"
newSourceMagnetLink.discriminator = magnetLinkJson.discriminator
newSourceMagnetLink.regex = magnetLinkJson.regex
newSourceRssParser.magnetLink = newSourceMagnetLink
}
if let magnetHashJson = rssParserJson.magnetHash {
let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext)
newSourceMagnetHash.query = magnetHashJson.query
newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text"
newSourceMagnetHash.discriminator = magnetHashJson.discriminator
newSourceMagnetHash.regex = magnetHashJson.regex
newSourceRssParser.magnetHash = newSourceMagnetHash
}
if let subNameJson = rssParserJson.subName {
let newSourceSubName = SourceSubName(context: backgroundContext)
newSourceSubName.query = subNameJson.query
newSourceSubName.attribute = subNameJson.attribute ?? "text"
newSourceSubName.discriminator = subNameJson.discriminator
newSourceSubName.regex = subNameJson.regex
newSourceRssParser.subName = newSourceSubName
}
if let titleJson = rssParserJson.title {
let newSourceTitle = SourceTitle(context: backgroundContext)
newSourceTitle.query = titleJson.query
newSourceTitle.attribute = titleJson.attribute ?? "text"
newSourceTitle.discriminator = titleJson.discriminator
newSourceTitle.regex = titleJson.regex
newSourceRssParser.title = newSourceTitle
}
if let sizeJson = rssParserJson.size {
let newSourceSize = SourceSize(context: backgroundContext)
newSourceSize.query = sizeJson.query
newSourceSize.attribute = sizeJson.attribute ?? "text"
newSourceSize.discriminator = sizeJson.discriminator
newSourceSize.regex = sizeJson.regex
newSourceRssParser.size = newSourceSize
}
if let seedLeechJson = rssParserJson.sl {
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
newSourceSeedLeech.seeders = seedLeechJson.seeders
newSourceSeedLeech.leechers = seedLeechJson.leechers
newSourceSeedLeech.combined = seedLeechJson.combined
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
newSourceSeedLeech.discriminator = seedLeechJson.discriminator
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
newSourceRssParser.seedLeech = newSourceSeedLeech
}
newSource.rssParser = newSourceRssParser
}
func addHtmlParser(newSource: Source, htmlParserJson: SourceHtmlParserJson) {
let backgroundContext = PersistenceController.shared.backgroundContext
let newSourceHtmlParser = SourceHtmlParser(context: backgroundContext)
newSourceHtmlParser.searchUrl = htmlParserJson.searchUrl
newSourceHtmlParser.rows = htmlParserJson.rows
if let subNameJson = htmlParserJson.subName {
let newSourceSubName = SourceSubName(context: backgroundContext)
newSourceSubName.query = subNameJson.query
newSourceSubName.attribute = subNameJson.attribute ?? "text"
newSourceSubName.regex = subNameJson.regex
newSourceHtmlParser.subName = newSourceSubName
}
// Adds a title complex query if present
if let titleJson = htmlParserJson.title {
let newSourceTitle = SourceTitle(context: backgroundContext)
newSourceTitle.query = titleJson.query
newSourceTitle.attribute = titleJson.attribute ?? "text"
newSourceTitle.regex = titleJson.regex
newSourceHtmlParser.title = newSourceTitle
}
// Adds a size complex query if present
if let sizeJson = htmlParserJson.size {
let newSourceSize = SourceSize(context: backgroundContext)
newSourceSize.query = sizeJson.query
newSourceSize.attribute = sizeJson.attribute ?? "text"
newSourceSize.regex = sizeJson.regex
newSourceHtmlParser.size = newSourceSize
}
if let seedLeechJson = htmlParserJson.sl {
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
newSourceSeedLeech.seeders = seedLeechJson.seeders
newSourceSeedLeech.leechers = seedLeechJson.leechers
newSourceSeedLeech.combined = seedLeechJson.combined
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
newSourceHtmlParser.seedLeech = newSourceSeedLeech
}
// Adds a magnet complex query and its unique properties
let newSourceMagnet = SourceMagnetLink(context: backgroundContext)
newSourceMagnet.externalLinkQuery = htmlParserJson.magnet.externalLinkQuery
newSourceMagnet.query = htmlParserJson.magnet.query
newSourceMagnet.attribute = htmlParserJson.magnet.attribute
newSourceMagnet.regex = htmlParserJson.magnet.regex
newSourceHtmlParser.magnetLink = newSourceMagnet
newSource.htmlParser = newSourceHtmlParser
}
// Adds a plugin list
// Can move this to PersistenceController if needed
public func addPluginList(_ url: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws {
let backgroundContext = PersistenceController.shared.backgroundContext
if url.isEmpty || URL(string: url) == nil {
throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.")
}
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: url)!))
let rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data)
if let existingPluginList {
existingPluginList.urlString = url
existingPluginList.name = rawResponse.name
existingPluginList.author = rawResponse.author
try PersistenceController.shared.container.viewContext.save()
} else {
let pluginListRequest = PluginList.fetchRequest()
let urlPredicate = NSPredicate(format: "urlString == %@", url)
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name)
pluginListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
pluginListRequest.fetchLimit = 1
if let existingPluginList = try? backgroundContext.fetch(pluginListRequest).first, !isSheet {
PersistenceController.shared.delete(existingPluginList, context: backgroundContext)
} else if isSheet {
throw PluginManagerError.ListAddition(description: "An existing plugin list with this information was found. Please try editing an existing plugin list instead.")
}
let newPluginList = PluginList(context: backgroundContext)
newPluginList.id = UUID()
newPluginList.urlString = url
newPluginList.name = rawResponse.name
newPluginList.author = rawResponse.author
try backgroundContext.save()
}
}
}