Improvec scraper error handling, Improved scraper

This commit is contained in:
Vicnet
2023-11-10 16:57:41 +01:00
parent 2ba0aaf636
commit 1598d24b00
4 changed files with 101 additions and 33 deletions

View File

@@ -308,6 +308,9 @@
} }
} }
} }
},
"Bad URL" : {
}, },
"Cancel" : { "Cancel" : {
"localizations" : { "localizations" : {
@@ -396,6 +399,9 @@
} }
} }
} }
},
"Connection error" : {
}, },
"Cookbook Client" : { "Cookbook Client" : {
"localizations" : { "localizations" : {
@@ -1326,9 +1332,15 @@
} }
} }
} }
},
"Parsing error" : {
}, },
"Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings." : { "Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings." : {
},
"Please check the entered URL." : {
}, },
"Please check your credentials and internet connection." : { "Please check your credentials and internet connection." : {
"localizations" : { "localizations" : {
@@ -1703,6 +1715,9 @@
} }
} }
} }
},
"This website might not be currently supported. If this appears incorrect, you can use the support options in the app settings to raise awareness about this issue." : {
}, },
"Title" : { "Title" : {
"localizations" : { "localizations" : {
@@ -1813,6 +1828,9 @@
} }
} }
} }
},
"Unable to load website content. Please check your internet connection." : {
}, },
"Unable to upload your recipe. Please check your internet connection." : { "Unable to upload your recipe. Please check your internet connection." : {
"localizations" : { "localizations" : {

View File

@@ -7,25 +7,28 @@
import Foundation import Foundation
import SwiftSoup import SwiftSoup
import SwiftUI
class RecipeScraper { class RecipeScraper {
func scrape(url: String) async throws -> RecipeDetail? { func scrape(url: String) async throws -> (RecipeDetail?, RecipeImportError?) {
var contents: String? = nil var contents: String? = nil
if let url = URL(string: url) { if let url = URL(string: url) {
do { do {
contents = try String(contentsOf: url) contents = try String(contentsOf: url)
} catch { } catch {
print("ERROR: Could not load url content.") print("ERROR: Could not load url content.")
return (nil, .CHECK_CONNECTION)
} }
} else { } else {
print("ERROR: Bad url.") print("ERROR: Bad url.")
return nil return (nil, .BAD_URL)
} }
guard let html = contents else { guard let html = contents else {
print("ERROR: no contents") print("ERROR: no contents")
return nil return (nil, .WEBSITE_NOT_SUPPORTED)
} }
let doc = try SwiftSoup.parse(html) let doc = try SwiftSoup.parse(html)
@@ -34,11 +37,13 @@ class RecipeScraper {
for attr in elem.getAttributes()!.asList() { for attr in elem.getAttributes()!.asList() {
if attr.getValue() == "application/ld+json" { if attr.getValue() == "application/ld+json" {
guard let dict = toDict(elem) else { continue } guard let dict = toDict(elem) else { continue }
return getRecipe(fromDict: dict) if let recipe = getRecipe(fromDict: dict) {
return (recipe, nil)
} }
} }
} }
return nil }
return (nil, .WEBSITE_NOT_SUPPORTED)
} }
@@ -46,7 +51,6 @@ class RecipeScraper {
var recipeDict: [String: Any]? = nil var recipeDict: [String: Any]? = nil
do { do {
let jsonString = try elem.html() let jsonString = try elem.html()
//print(json)
let json = try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!, options: .fragmentsAllowed) let json = try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!, options: .fragmentsAllowed)
if let recipe = json as? [String : Any] { if let recipe = json as? [String : Any] {
recipeDict = recipe recipeDict = recipe
@@ -78,7 +82,7 @@ class RecipeScraper {
var recipeDetail = RecipeDetail() var recipeDetail = RecipeDetail()
recipeDetail.name = recipe["name"] as? String ?? "New Recipe" recipeDetail.name = recipe["name"] as? String ?? "New Recipe"
recipeDetail.recipeCategory = recipe["recipeCategory"] as? String ?? "" recipeDetail.recipeCategory = recipe["recipeCategory"] as? String ?? ""
recipeDetail.keywords = recipe["keywords"] as? String ?? "" recipeDetail.keywords = joinedStringForKey("keywords", dict: recipe)
recipeDetail.description = recipe["description"] as? String ?? "" recipeDetail.description = recipe["description"] as? String ?? ""
recipeDetail.dateCreated = recipe["dateCreated"] as? String ?? "" recipeDetail.dateCreated = recipe["dateCreated"] as? String ?? ""
recipeDetail.dateModified = recipe["dateModified"] as? String ?? "" recipeDetail.dateModified = recipe["dateModified"] as? String ?? ""
@@ -90,14 +94,16 @@ class RecipeScraper {
recipeDetail.recipeInstructions = stringArrayForKey("recipeInstructions", dict: recipe) recipeDetail.recipeInstructions = stringArrayForKey("recipeInstructions", dict: recipe)
recipeDetail.recipeYield = recipe["recipeYield"] as? Int ?? 0 recipeDetail.recipeYield = recipe["recipeYield"] as? Int ?? 0
recipeDetail.recipeIngredient = recipe["recipeIngredient"] as? [String] ?? [] recipeDetail.recipeIngredient = recipe["recipeIngredient"] as? [String] ?? []
recipeDetail.tool = recipe["tool"] as? [String] ?? [] recipeDetail.tool = stringArrayForKey("tool", dict: recipe)
recipeDetail.nutrition = recipe["nutrition"] as? [String:String] ?? [:] recipeDetail.nutrition = recipe["nutrition"] as? [String:String] ?? [:]
print(recipeDetail)
return recipeDetail return recipeDetail
} }
private func stringArrayForKey(_ key: String, dict: Dictionary<String, Any>) -> [String] { private func stringArrayForKey(_ key: String, dict: Dictionary<String, Any>) -> [String] {
if let value = dict[key] as? [String] { if let text = dict[key] as? String {
return [text]
} else if let value = dict[key] as? [String] {
return value return value
} else if let orderedList = dict[key] as? [Any] { } else if let orderedList = dict[key] as? [Any] {
var entries: [String] = [] var entries: [String] = []
@@ -107,8 +113,6 @@ class RecipeScraper {
entries.append(text) entries.append(text)
} }
return entries return entries
} else if let text = dict[key] as? String {
return [text]
} }
return [] return []
} }

View File

@@ -9,6 +9,12 @@ import Foundation
import SwiftUI import SwiftUI
protocol UserAlert: Error {
var localizedTitle: LocalizedStringKey { get }
var localizedDescription: LocalizedStringKey { get }
var alertButtons: [AlertButton] { get }
}
enum AlertButton: LocalizedStringKey, Identifiable { enum AlertButton: LocalizedStringKey, Identifiable {
var id: Self { var id: Self {
return self return self
@@ -19,7 +25,7 @@ enum AlertButton: LocalizedStringKey, Identifiable {
enum AlertType: Error { enum RecipeCreationError: UserAlert {
case NO_TITLE, case NO_TITLE,
DUPLICATE, DUPLICATE,
@@ -77,3 +83,29 @@ enum AlertType: Error {
} }
} }
enum RecipeImportError: UserAlert {
case BAD_URL,
CHECK_CONNECTION,
WEBSITE_NOT_SUPPORTED
var localizedDescription: LocalizedStringKey {
switch self {
case .BAD_URL: return "Please check the entered URL."
case .CHECK_CONNECTION: return "Unable to load website content. Please check your internet connection."
case .WEBSITE_NOT_SUPPORTED: return "This website might not be currently supported. If this appears incorrect, you can use the support options in the app settings to raise awareness about this issue."
}
}
var localizedTitle: LocalizedStringKey {
switch self {
case .BAD_URL: return "Bad URL"
case .CHECK_CONNECTION: return "Connection error"
case .WEBSITE_NOT_SUPPORTED: return "Parsing error"
}
}
var alertButtons: [AlertButton] {
return [.OK]
}
}

View File

@@ -13,12 +13,16 @@ import PhotosUI
struct RecipeEditView: View { struct RecipeEditView: View {
@ObservedObject var viewModel: MainViewModel @ObservedObject var viewModel: MainViewModel
@State var recipe: RecipeDetail = RecipeDetail() @State var recipe: RecipeDetail = RecipeDetail() {
didSet {
prepareView()
}
}
@Binding var isPresented: Bool @Binding var isPresented: Bool
@State var uploadNew: Bool = true @State var uploadNew: Bool = true
@State private var presentAlert = false @State private var presentAlert = false
@State private var alertType: AlertType = .GENERIC @State private var alertType: UserAlert = RecipeCreationError.GENERIC
@State private var alertAction: () -> () = {} @State private var alertAction: () -> () = {}
@StateObject private var prepDuration: Duration = Duration() @StateObject private var prepDuration: Duration = Duration()
@@ -46,7 +50,7 @@ struct RecipeEditView: View {
Menu { Menu {
Button { Button {
print("Delete recipe.") print("Delete recipe.")
alertType = .CONFIRM_DELETE alertType = RecipeCreationError.CONFIRM_DELETE
alertAction = deleteRecipe alertAction = deleteRecipe
presentAlert = true presentAlert = true
} label: { } label: {
@@ -87,8 +91,14 @@ struct RecipeEditView: View {
.onSubmit { .onSubmit {
Task { Task {
do { do {
if let recipe = try await RecipeScraper().scrape(url: importURL) { let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: importURL)
self.recipe = recipe if let scrapedRecipe = scrapedRecipe {
self.recipe = scrapedRecipe
}
if let error = error {
self.alertType = error
self.alertAction = {}
self.presentAlert = true
} }
} catch { } catch {
print("Error") print("Error")
@@ -168,19 +178,7 @@ struct RecipeEditView: View {
self.keywordSuggestions = await viewModel.getKeywords() self.keywordSuggestions = await viewModel.getKeywords()
} }
.onAppear { .onAppear {
if uploadNew { return } prepareView()
if let prepTime = recipe.prepTime {
prepDuration.initFromPT(prepTime)
}
if let cookTime = recipe.cookTime {
cookDuration.initFromPT(cookTime)
}
if let totalTime = recipe.totalTime {
totalDuration.initFromPT(totalTime)
}
for keyword in self.recipe.keywords.components(separatedBy: ",") {
keywords.append(keyword)
}
} }
.alert(alertType.localizedTitle, isPresented: $presentAlert) { .alert(alertType.localizedTitle, isPresented: $presentAlert) {
ForEach(alertType.alertButtons) { buttonType in ForEach(alertType.alertButtons) { buttonType in
@@ -214,7 +212,7 @@ struct RecipeEditView: View {
func recipeValid() -> Bool { func recipeValid() -> Bool {
// Check if the recipe has a name // Check if the recipe has a name
if recipe.name.replacingOccurrences(of: " ", with: "") == "" { if recipe.name.replacingOccurrences(of: " ", with: "") == "" {
alertType = .NO_TITLE alertType = RecipeCreationError.NO_TITLE
alertAction = {} alertAction = {}
presentAlert = true presentAlert = true
return false return false
@@ -229,7 +227,7 @@ struct RecipeEditView: View {
.replacingOccurrences(of: " ", with: "") .replacingOccurrences(of: " ", with: "")
.lowercased() .lowercased()
{ {
alertType = .DUPLICATE alertType = RecipeCreationError.DUPLICATE
alertAction = {} alertAction = {}
presentAlert = true presentAlert = true
return false return false
@@ -316,6 +314,22 @@ struct RecipeEditView: View {
} }
self.isPresented = false self.isPresented = false
} }
func prepareView() {
if uploadNew { return }
if let prepTime = recipe.prepTime {
prepDuration.initFromPT(prepTime)
}
if let cookTime = recipe.cookTime {
cookDuration.initFromPT(cookTime)
}
if let totalTime = recipe.totalTime {
totalDuration.initFromPT(totalTime)
}
for keyword in self.recipe.keywords.components(separatedBy: ",") {
keywords.append(keyword)
}
}
} }