Improvec scraper error handling, Improved scraper
This commit is contained in:
@@ -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" : {
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user