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" : {
"localizations" : {
@@ -396,6 +399,9 @@
}
}
}
},
"Connection error" : {
},
"Cookbook Client" : {
"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." : {
},
"Please check the entered URL." : {
},
"Please check your credentials and internet connection." : {
"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" : {
"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." : {
"localizations" : {

View File

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

View File

@@ -9,6 +9,12 @@ import Foundation
import SwiftUI
protocol UserAlert: Error {
var localizedTitle: LocalizedStringKey { get }
var localizedDescription: LocalizedStringKey { get }
var alertButtons: [AlertButton] { get }
}
enum AlertButton: LocalizedStringKey, Identifiable {
var id: Self {
return self
@@ -19,7 +25,7 @@ enum AlertButton: LocalizedStringKey, Identifiable {
enum AlertType: Error {
enum RecipeCreationError: UserAlert {
case NO_TITLE,
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 {
@ObservedObject var viewModel: MainViewModel
@State var recipe: RecipeDetail = RecipeDetail()
@State var recipe: RecipeDetail = RecipeDetail() {
didSet {
prepareView()
}
}
@Binding var isPresented: Bool
@State var uploadNew: Bool = true
@State private var presentAlert = false
@State private var alertType: AlertType = .GENERIC
@State private var alertType: UserAlert = RecipeCreationError.GENERIC
@State private var alertAction: () -> () = {}
@StateObject private var prepDuration: Duration = Duration()
@@ -46,7 +50,7 @@ struct RecipeEditView: View {
Menu {
Button {
print("Delete recipe.")
alertType = .CONFIRM_DELETE
alertType = RecipeCreationError.CONFIRM_DELETE
alertAction = deleteRecipe
presentAlert = true
} label: {
@@ -87,8 +91,14 @@ struct RecipeEditView: View {
.onSubmit {
Task {
do {
if let recipe = try await RecipeScraper().scrape(url: importURL) {
self.recipe = recipe
let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: importURL)
if let scrapedRecipe = scrapedRecipe {
self.recipe = scrapedRecipe
}
if let error = error {
self.alertType = error
self.alertAction = {}
self.presentAlert = true
}
} catch {
print("Error")
@@ -168,19 +178,7 @@ struct RecipeEditView: View {
self.keywordSuggestions = await viewModel.getKeywords()
}
.onAppear {
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)
}
prepareView()
}
.alert(alertType.localizedTitle, isPresented: $presentAlert) {
ForEach(alertType.alertButtons) { buttonType in
@@ -214,7 +212,7 @@ struct RecipeEditView: View {
func recipeValid() -> Bool {
// Check if the recipe has a name
if recipe.name.replacingOccurrences(of: " ", with: "") == "" {
alertType = .NO_TITLE
alertType = RecipeCreationError.NO_TITLE
alertAction = {}
presentAlert = true
return false
@@ -229,7 +227,7 @@ struct RecipeEditView: View {
.replacingOccurrences(of: " ", with: "")
.lowercased()
{
alertType = .DUPLICATE
alertType = RecipeCreationError.DUPLICATE
alertAction = {}
presentAlert = true
return false
@@ -316,6 +314,22 @@ struct RecipeEditView: View {
}
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)
}
}
}