Sources: Add fallback URLs
If a website times out, use the fallback options. The data URL request now has a hard timeout of 15 seconds. This only works for the base URL of a source, if an RSS url or API url is provided, fallback URLs won't be used and the request will fail. Signed-off-by: kingbri <bdashore3@gmail.com>
This commit is contained in:
parent
e0182a700f
commit
79d88ffab6
5 changed files with 97 additions and 14 deletions
|
|
@ -16,6 +16,7 @@ public extension Source {
|
||||||
|
|
||||||
@NSManaged var id: UUID
|
@NSManaged var id: UUID
|
||||||
@NSManaged var baseUrl: String?
|
@NSManaged var baseUrl: String?
|
||||||
|
@NSManaged var fallbackUrls: [String]?
|
||||||
@NSManaged var dynamicBaseUrl: Bool
|
@NSManaged var dynamicBaseUrl: Bool
|
||||||
@NSManaged var enabled: Bool
|
@NSManaged var enabled: Bool
|
||||||
@NSManaged var name: String
|
@NSManaged var name: String
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<attribute name="baseUrl" optional="YES" attributeType="String"/>
|
<attribute name="baseUrl" optional="YES" attributeType="String"/>
|
||||||
<attribute name="dynamicBaseUrl" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="dynamicBaseUrl" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="fallbackUrls" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="listId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="listId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ public struct SourceJson: Codable, Hashable {
|
||||||
let name: String
|
let name: String
|
||||||
let version: Int16
|
let version: Int16
|
||||||
let baseUrl: String?
|
let baseUrl: String?
|
||||||
|
let fallbackUrls: [String]?
|
||||||
var dynamicBaseUrl: Bool?
|
var dynamicBaseUrl: Bool?
|
||||||
var author: String?
|
var author: String?
|
||||||
var listId: UUID?
|
var listId: UUID?
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,16 @@ class ScrapingViewModel: ObservableObject {
|
||||||
switch preferredParser {
|
switch preferredParser {
|
||||||
case .scraping:
|
case .scraping:
|
||||||
if let htmlParser = source.htmlParser {
|
if let htmlParser = source.htmlParser {
|
||||||
let urlString = baseUrl + htmlParser.searchUrl
|
let replacedSearchUrl = htmlParser.searchUrl
|
||||||
.replacingOccurrences(of: "{query}", with: encodedQuery)
|
.replacingOccurrences(of: "{query}", with: encodedQuery)
|
||||||
|
|
||||||
if let data = await fetchWebsiteData(urlString: urlString),
|
let data = await handleUrls(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
replacedSearchUrl: replacedSearchUrl,
|
||||||
|
fallbackUrls: source.fallbackUrls
|
||||||
|
)
|
||||||
|
|
||||||
|
if let data = data,
|
||||||
let html = String(data: data, encoding: .utf8)
|
let html = String(data: data, encoding: .utf8)
|
||||||
{
|
{
|
||||||
let sourceResults = await scrapeHtml(source: source, baseUrl: baseUrl, html: html)
|
let sourceResults = await scrapeHtml(source: source, baseUrl: baseUrl, html: html)
|
||||||
|
|
@ -89,10 +95,19 @@ class ScrapingViewModel: ObservableObject {
|
||||||
.replacingOccurrences(of: "{secret}", with: source.api?.clientSecret?.value ?? "")
|
.replacingOccurrences(of: "{secret}", with: source.api?.clientSecret?.value ?? "")
|
||||||
.replacingOccurrences(of: "{query}", with: encodedQuery)
|
.replacingOccurrences(of: "{query}", with: encodedQuery)
|
||||||
|
|
||||||
// If there is an RSS base URL, use that instead
|
// Do not use fallback URLs if the base URL isn't used
|
||||||
let urlString = (rssParser.rssUrl ?? baseUrl) + replacedSearchUrl
|
let data: Data?
|
||||||
|
if let rssUrl = rssParser.rssUrl {
|
||||||
|
data = await fetchWebsiteData(urlString: rssUrl + replacedSearchUrl)
|
||||||
|
} else {
|
||||||
|
data = await handleUrls(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
replacedSearchUrl: replacedSearchUrl,
|
||||||
|
fallbackUrls: source.fallbackUrls
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if let data = await fetchWebsiteData(urlString: urlString),
|
if let data = data,
|
||||||
let rss = String(data: data, encoding: .utf8)
|
let rss = String(data: data, encoding: .utf8)
|
||||||
{
|
{
|
||||||
let sourceResults = scrapeRss(source: source, rss: rss)
|
let sourceResults = scrapeRss(source: source, rss: rss)
|
||||||
|
|
@ -131,9 +146,14 @@ class ScrapingViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let urlString = (source.api?.apiUrl ?? baseUrl) + replacedSearchUrl
|
let passedUrl = source.api?.apiUrl ?? baseUrl
|
||||||
|
let data = await handleUrls(
|
||||||
|
baseUrl: passedUrl,
|
||||||
|
replacedSearchUrl: replacedSearchUrl,
|
||||||
|
fallbackUrls: source.fallbackUrls
|
||||||
|
)
|
||||||
|
|
||||||
if let data = await fetchWebsiteData(urlString: urlString) {
|
if let data = data {
|
||||||
let sourceResults = scrapeJson(source: source, jsonData: data)
|
let sourceResults = scrapeJson(source: source, jsonData: data)
|
||||||
tempResults += sourceResults
|
tempResults += sourceResults
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +172,23 @@ class ScrapingViewModel: ObservableObject {
|
||||||
searchResults = tempResults
|
searchResults = tempResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks the base URL for any website data then iterates through the fallback URLs
|
||||||
|
func handleUrls(baseUrl: String, replacedSearchUrl: String, fallbackUrls: [String]?) async -> Data? {
|
||||||
|
if let data = await fetchWebsiteData(urlString: baseUrl + replacedSearchUrl) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if let fallbackUrls = fallbackUrls {
|
||||||
|
for fallbackUrl in fallbackUrls {
|
||||||
|
if let data = await fetchWebsiteData(urlString: fallbackUrl + replacedSearchUrl) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
public func handleApiCredential(_ credential: SourceApiCredential,
|
public func handleApiCredential(_ credential: SourceApiCredential,
|
||||||
replacement: String,
|
replacement: String,
|
||||||
searchUrl: String,
|
searchUrl: String,
|
||||||
|
|
@ -220,8 +257,18 @@ class ScrapingViewModel: ObservableObject {
|
||||||
return String(data: data, encoding: .utf8)
|
return String(data: data, encoding: .utf8)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
let error = error as NSError
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
toastModel?.toastDescription = "Error in fetching an API credential \(error)"
|
switch error.code {
|
||||||
|
case -999:
|
||||||
|
toastModel?.toastType = .info
|
||||||
|
toastModel?.toastDescription = "Search cancelled"
|
||||||
|
case -1001:
|
||||||
|
toastModel?.toastDescription = "Credentials request timed out"
|
||||||
|
default:
|
||||||
|
toastModel?.toastDescription = "Error in fetching an API credential \(error)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
print("Error in fetching an API credential \(error)")
|
print("Error in fetching an API credential \(error)")
|
||||||
|
|
||||||
|
|
@ -241,8 +288,10 @@ class ScrapingViewModel: ObservableObject {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let request = URLRequest(url: url, timeoutInterval: 15)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let (data, _) = try await URLSession.shared.data(from: url)
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
return data
|
return data
|
||||||
} catch {
|
} catch {
|
||||||
let error = error as NSError
|
let error = error as NSError
|
||||||
|
|
@ -252,8 +301,10 @@ class ScrapingViewModel: ObservableObject {
|
||||||
case -999:
|
case -999:
|
||||||
toastModel?.toastType = .info
|
toastModel?.toastType = .info
|
||||||
toastModel?.toastDescription = "Search cancelled"
|
toastModel?.toastDescription = "Search cancelled"
|
||||||
|
case -1001:
|
||||||
|
toastModel?.toastDescription = "Data request timed out. Trying fallback URLs if present."
|
||||||
default:
|
default:
|
||||||
toastModel?.toastDescription = "Error in fetching data \(error)"
|
toastModel?.toastDescription = "Error in fetching website data \(error)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print("Error in fetching data \(error)")
|
print("Error in fetching data \(error)")
|
||||||
|
|
@ -833,21 +884,49 @@ class ScrapingViewModel: ObservableObject {
|
||||||
func cleanApiCreds(api: SourceApi) {
|
func cleanApiCreds(api: SourceApi) {
|
||||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||||
|
|
||||||
var clientIdReset = false
|
let hasCredentials = api.clientId != nil || api.clientSecret != nil
|
||||||
var clientSecretReset = false
|
let clientIdReset: Bool
|
||||||
|
let clientSecretReset: Bool
|
||||||
|
|
||||||
|
var responseArray = ["Could not fetch API results"]
|
||||||
|
|
||||||
if let clientId = api.clientId, !clientId.dynamic {
|
if let clientId = api.clientId, !clientId.dynamic {
|
||||||
clientId.value = nil
|
clientId.value = nil
|
||||||
clientIdReset = true
|
clientIdReset = true
|
||||||
|
} else {
|
||||||
|
clientIdReset = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if let clientSecret = api.clientSecret, !clientSecret.dynamic {
|
if let clientSecret = api.clientSecret, !clientSecret.dynamic {
|
||||||
clientSecret.value = nil
|
clientSecret.value = nil
|
||||||
clientSecretReset = true
|
clientSecretReset = true
|
||||||
|
} else {
|
||||||
|
clientSecretReset = false
|
||||||
}
|
}
|
||||||
|
|
||||||
toastModel?.toastDescription =
|
if hasCredentials {
|
||||||
"Could not fetch results, your \(clientIdReset ? "client ID" : "") \(clientIdReset && clientSecretReset ? "and" : "") \(clientSecretReset ? "token" : "") was automatically reset. Make sure all credentials are correct in the source's settings!"
|
responseArray.append("your")
|
||||||
|
|
||||||
|
if clientIdReset {
|
||||||
|
responseArray.append("client ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientIdReset && clientSecretReset {
|
||||||
|
responseArray.append("and")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientSecretReset {
|
||||||
|
responseArray.append("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseArray.append("was automatically reset.")
|
||||||
|
|
||||||
|
if !(clientIdReset || clientSecretReset) {
|
||||||
|
responseArray.append("Make sure all credentials are correct in the source's settings!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toastModel?.toastDescription = responseArray.joined()
|
||||||
|
|
||||||
PersistenceController.shared.save(backgroundContext)
|
PersistenceController.shared.save(backgroundContext)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ public class SourceManager: ObservableObject {
|
||||||
newSource.version = sourceJson.version
|
newSource.version = sourceJson.version
|
||||||
newSource.dynamicBaseUrl = dynamicBaseUrl
|
newSource.dynamicBaseUrl = dynamicBaseUrl
|
||||||
newSource.baseUrl = sourceJson.baseUrl
|
newSource.baseUrl = sourceJson.baseUrl
|
||||||
|
newSource.fallbackUrls = dynamicBaseUrl ? nil : sourceJson.fallbackUrls
|
||||||
newSource.author = sourceJson.author ?? "Unknown"
|
newSource.author = sourceJson.author ?? "Unknown"
|
||||||
newSource.listId = sourceJson.listId
|
newSource.listId = sourceJson.listId
|
||||||
newSource.trackers = sourceJson.trackers
|
newSource.trackers = sourceJson.trackers
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue