- Fix how abrupt search cancellations work - Add a no results prompt if no results are found - Clean up how scraping model results are returned on error - Allow a base URL and dynamic base URL to be provided together Signed-off-by: kingbri <bdashore3@gmail.com>
309 lines
12 KiB
Swift
309 lines
12 KiB
Swift
//
|
|
// SourceViewModel.swift
|
|
// Ferrite
|
|
//
|
|
// Created by Brian Dashore on 7/25/22.
|
|
//
|
|
|
|
import CoreData
|
|
import Foundation
|
|
|
|
public class SourceManager: ObservableObject {
|
|
var toastModel: ToastViewModel?
|
|
|
|
@Published var availableSources: [SourceJson] = []
|
|
|
|
@Published var urlErrorAlertText = ""
|
|
@Published var showUrlErrorAlert = false
|
|
|
|
@MainActor
|
|
public func fetchSourcesFromUrl() async {
|
|
let sourceListRequest = SourceList.fetchRequest()
|
|
do {
|
|
let sourceLists = try PersistenceController.shared.backgroundContext.fetch(sourceListRequest)
|
|
var tempSourceUrls: [SourceJson] = []
|
|
|
|
for sourceList in sourceLists {
|
|
guard let url = URL(string: sourceList.urlString) else {
|
|
return
|
|
}
|
|
|
|
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
|
|
var sourceResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
|
|
|
for index in sourceResponse.sources.indices {
|
|
sourceResponse.sources[index].author = sourceList.author
|
|
sourceResponse.sources[index].listId = sourceList.id
|
|
}
|
|
|
|
tempSourceUrls += sourceResponse.sources
|
|
}
|
|
|
|
availableSources = tempSourceUrls
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
public func installSource(sourceJson: SourceJson, doUpsert: Bool = false) {
|
|
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 {
|
|
Task { @MainActor in
|
|
toastModel?.toastDescription = "Not adding this source because base URL parameters are malformed. Please contact the source dev."
|
|
}
|
|
|
|
print("Not adding this source because base URL parameters are malformed")
|
|
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 {
|
|
Task { @MainActor in
|
|
toastModel?.toastDescription = "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.author = sourceJson.author ?? "Unknown"
|
|
newSource.listId = sourceJson.listId
|
|
|
|
if let sourceApiJson = sourceJson.api {
|
|
addSourceApi(newSource: newSource, apiJson: sourceApiJson)
|
|
}
|
|
|
|
// 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.rssParser != nil {
|
|
newSource.preferredParser = Int16(SourcePreferredParser.rss.rawValue)
|
|
} else {
|
|
newSource.preferredParser = Int16(SourcePreferredParser.scraping.rawValue)
|
|
}
|
|
|
|
newSource.enabled = true
|
|
|
|
do {
|
|
try backgroundContext.save()
|
|
} catch {
|
|
Task { @MainActor in
|
|
toastModel?.toastDescription = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
func addSourceApi(newSource: Source, apiJson: SourceApiJson) {
|
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
|
|
|
let newSourceApi = SourceApi(context: backgroundContext)
|
|
newSourceApi.clientId = apiJson.clientId
|
|
|
|
if let clientId = apiJson.clientId {
|
|
newSourceApi.clientId = clientId
|
|
}
|
|
|
|
newSourceApi.dynamicClientId = apiJson.dynamicClientId ?? false
|
|
|
|
if apiJson.usesSecret {
|
|
newSourceApi.clientSecret = ""
|
|
}
|
|
|
|
newSource.api = newSourceApi
|
|
}
|
|
|
|
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.lookupAttribute = magnetLinkJson.lookupAttribute
|
|
|
|
newSourceRssParser.magnetLink = newSourceMagnetLink
|
|
}
|
|
|
|
if let magnetHashJson = rssParserJson.magnetHash {
|
|
let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext)
|
|
newSourceMagnetHash.query = magnetHashJson.query
|
|
newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text"
|
|
newSourceMagnetHash.lookupAttribute = magnetHashJson.lookupAttribute
|
|
|
|
newSourceRssParser.magnetHash = newSourceMagnetHash
|
|
}
|
|
|
|
if let titleJson = rssParserJson.title {
|
|
let newSourceTitle = SourceTitle(context: backgroundContext)
|
|
newSourceTitle.query = titleJson.query
|
|
newSourceTitle.attribute = titleJson.attribute ?? "text"
|
|
newSourceTitle.lookupAttribute = newSourceTitle.lookupAttribute
|
|
|
|
newSourceRssParser.title = newSourceTitle
|
|
}
|
|
|
|
if let sizeJson = rssParserJson.size {
|
|
let newSourceSize = SourceSize(context: backgroundContext)
|
|
newSourceSize.query = sizeJson.query
|
|
newSourceSize.attribute = sizeJson.attribute ?? "text"
|
|
newSourceSize.lookupAttribute = sizeJson.lookupAttribute
|
|
|
|
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.lookupAttribute = seedLeechJson.lookupAttribute
|
|
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
|
|
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
|
|
|
|
newSourceRssParser.seedLeech = newSourceSeedLeech
|
|
}
|
|
|
|
if let trackerJson = rssParserJson.trackers {
|
|
for urlString in trackerJson {
|
|
let newSourceTracker = SourceTracker(context: backgroundContext)
|
|
newSourceTracker.urlString = urlString
|
|
newSourceTracker.parentRssParser = newSourceRssParser
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
// 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
|
|
}
|
|
|
|
@MainActor
|
|
public func addSourceList(sourceUrl: String, existingSourceList: SourceList?) async -> Bool {
|
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
|
|
|
if sourceUrl.isEmpty || URL(string: sourceUrl) == nil {
|
|
urlErrorAlertText = "The provided source list is invalid. Please check if the URL is formatted properly."
|
|
showUrlErrorAlert.toggle()
|
|
|
|
return false
|
|
}
|
|
|
|
do {
|
|
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
|
|
let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
|
|
|
if let existingSourceList = existingSourceList {
|
|
existingSourceList.urlString = sourceUrl
|
|
existingSourceList.name = rawResponse.name
|
|
existingSourceList.author = rawResponse.author
|
|
} else {
|
|
let sourceListRequest = SourceList.fetchRequest()
|
|
let urlPredicate = NSPredicate(format: "urlString == %@", sourceUrl)
|
|
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name)
|
|
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
|
sourceListRequest.fetchLimit = 1
|
|
|
|
if (try? backgroundContext.fetch(sourceListRequest).first) != nil {
|
|
urlErrorAlertText = "An existing source with this information was found. Please try editing the source list instead."
|
|
showUrlErrorAlert.toggle()
|
|
|
|
return false
|
|
}
|
|
|
|
let newSourceUrl = SourceList(context: backgroundContext)
|
|
newSourceUrl.id = UUID()
|
|
newSourceUrl.urlString = sourceUrl
|
|
newSourceUrl.name = rawResponse.name
|
|
newSourceUrl.author = rawResponse.author
|
|
}
|
|
|
|
try backgroundContext.save()
|
|
|
|
return true
|
|
} catch {
|
|
print(error)
|
|
urlErrorAlertText = error.localizedDescription
|
|
showUrlErrorAlert.toggle()
|
|
|
|
return false
|
|
}
|
|
}
|
|
}
|