diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index 9977308..338d783 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -32,8 +32,10 @@ A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */; }; A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; }; A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; }; + A74D33BE2AF82AAE00D06555 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = A74D33BD2AF82AAE00D06555 /* SwiftSoup */; }; A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */; }; A76B8A712AE002AE00096CEC /* AlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A702AE002AE00096CEC /* AlertHandler.swift */; }; + A781E7612AF822D000452F6F /* RecipeScraper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A781E7602AF822CF00452F6F /* RecipeScraper.swift */; }; A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; }; A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; }; A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */; }; @@ -86,8 +88,10 @@ A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = ""; }; A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; + A74D33BF2AF82CB500D06555 /* Scraper.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Scraper.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedLanguage.swift; sourceTree = ""; }; A76B8A702AE002AE00096CEC /* AlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertHandler.swift; sourceTree = ""; }; + A781E7602AF822CF00452F6F /* RecipeScraper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeScraper.swift; sourceTree = ""; }; A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = ""; }; A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = ""; }; @@ -98,6 +102,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A74D33BE2AF82AAE00D06555 /* SwiftSoup in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -149,6 +154,7 @@ A70171BA2AB4980100064C43 /* Views */, A70171B72AB2445700064C43 /* ViewModels */, A70171B22AB211F000064C43 /* Network */, + A781E75F2AF8228100452F6F /* RecipeImport */, A703226B2ABAF60D00D7C4ED /* Extensions */, A7AEAE632AD5521400135378 /* Localizable.xcstrings */, A70171852AA8E71F00064C43 /* Assets.xcassets */, @@ -246,6 +252,15 @@ path = Screenshots; sourceTree = ""; }; + A781E75F2AF8228100452F6F /* RecipeImport */ = { + isa = PBXGroup; + children = ( + A781E7602AF822CF00452F6F /* RecipeScraper.swift */, + A74D33BF2AF82CB500D06555 /* Scraper.playground */, + ); + path = RecipeImport; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -262,6 +277,9 @@ dependencies = ( ); name = "Nextcloud Cookbook iOS Client"; + packageProductDependencies = ( + A74D33BD2AF82AAE00D06555 /* SwiftSoup */, + ); productName = "Nextcloud Cookbook iOS Client"; productReference = A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */; productType = "com.apple.product-type.application"; @@ -337,6 +355,9 @@ fr, ); mainGroup = A70171752AA8E71900064C43; + packageReferences = ( + A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */, + ); productRefGroup = A701717F2AA8E71900064C43 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -400,6 +421,7 @@ A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */, A703226D2ABAF90D00D7C4ED /* APIController.swift in Sources */, A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */, + A781E7612AF822D000452F6F /* RecipeScraper.swift in Sources */, A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */, A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */, A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */, @@ -579,7 +601,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -620,7 +642,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -761,6 +783,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.6.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A74D33BD2AF82AAE00D06555 /* SwiftSoup */ = { + isa = XCSwiftPackageProductDependency; + package = A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = A70171762AA8E71900064C43 /* Project object */; } diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..6a1c025 --- /dev/null +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6", + "version" : "2.6.1" + } + } + ], + "version" : 2 +} diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/vincentmeilinger.xcuserdatad/xcschemes/xcschememanagement.plist b/Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/vincentmeilinger.xcuserdatad/xcschemes/xcschememanagement.plist index caa45f8..e2b77cd 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/vincentmeilinger.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/vincentmeilinger.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,10 +4,52 @@ SchemeUserState + MyPlayground (Playground) 1.xcscheme + + isShown + + orderHint + 2 + + MyPlayground (Playground) 2.xcscheme + + isShown + + orderHint + 3 + + MyPlayground (Playground).xcscheme + + isShown + + orderHint + 0 + Nextcloud Cookbook iOS Client.xcscheme_^#shared#^_ orderHint - 0 + 1 + + Scraper (Playground) 1.xcscheme + + isShown + + orderHint + 5 + + Scraper (Playground) 2.xcscheme + + isShown + + orderHint + 6 + + Scraper (Playground).xcscheme + + isShown + + orderHint + 4 diff --git a/Nextcloud Cookbook iOS Client/RecipeImport/RecipeScraper.swift b/Nextcloud Cookbook iOS Client/RecipeImport/RecipeScraper.swift new file mode 100644 index 0000000..4955630 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/RecipeImport/RecipeScraper.swift @@ -0,0 +1,67 @@ +// +// RecipeScraper.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 05.11.23. +// + +import Foundation +import SwiftSoup + +class RecipeScraper { + func scrape(url: String) -> RecipeDetail? { + var contents: String? = nil + if let url = URL(string: url) { + do { + contents = try String(contentsOf: url) + } catch { + print("ERROR: Could not load url content.") + } + + } else { + print("ERROR: Bad url.") + } + + guard let html = contents else { + print("ERROR: no contents") + exit(1) + } + + let doc: Document = try SwiftSoup.parse(html) + let elements: Elements = try doc.select("script") + for elem in elements.array() { + for attr in elem.getAttributes()!.asList() { + if attr.getValue() == "application/ld+json" { + toDict(elem) + } + } + } + } + + + private func toDict(_ elem: Element) -> [String: Any] { + 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] { + return recipe + } else if let recipe = (json as! [Any])[0] as? [String : Any] { + return recipe + } + } catch { + print("COULD NOT DECODE") + } + } + + private func getRecipe(fromDict recipe: Dictionary) { + if recipe["@type"] as? String ?? "" == "Recipe" { + print(recipe["name"] ?? "No name") + print(recipe["recipeIngredient"] ?? "No ingredients") + print(recipe["recipeInstruction"] ?? "No instruction") + } else if (recipe["@type"] as? [String] ?? []).contains("Recipe") { + print(recipe["name"] ?? "No name") + } + } + +} diff --git a/Nextcloud Cookbook iOS Client/RecipeImport/Scraper.playground/Contents.swift b/Nextcloud Cookbook iOS Client/RecipeImport/Scraper.playground/Contents.swift new file mode 100644 index 0000000..2ca041c --- /dev/null +++ b/Nextcloud Cookbook iOS Client/RecipeImport/Scraper.playground/Contents.swift @@ -0,0 +1,63 @@ +import SwiftSoup +import Foundation + + + +//let url = "https://www.chefkoch.de/rezepte/1385981243676608/Knusprige-Entenbrust.html" +let url = "https://www.allrecipes.com/recipe/234620/mascarpone-mashed-potatoes/" +var contents: String? = nil +if let url = URL(string: url) { + do { + contents = try String(contentsOf: url) + //print(contents) + } catch { + print("ERROR: Could not load url content.") + } + +} else { + print("ERROR: Bad url.") +} + +guard let html = contents else { + print("ERROR: no contents") + exit(1) +} + +let doc: Document = try SwiftSoup.parse(html) +let elements: Elements = try doc.select("script") +for elem in elements.array() { + for attr in elem.getAttributes()!.asList() { + //print(attr.getValue()) + if attr.getValue() == "application/ld+json" { + + 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] { + print("1") + getRecipe(fromDict: recipe) + } else if let recipe = (json as! [Any])[0] as? [String : Any] { + print("2") + getRecipe(fromDict: recipe) + } + + + } catch { + print("COULD NOT DECODE") + } + } + } +} + + +func getRecipe(fromDict recipe: Dictionary) { + + if recipe["@type"] as? String ?? "" == "Recipe" { + print(recipe["name"] ?? "No name") + print(recipe["recipeIngredient"] ?? "No ingredients") + print(recipe["recipeInstruction"] ?? "No instruction") + } else if (recipe["@type"] as? [String] ?? []).contains("Recipe") { + print(recipe["name"] ?? "No name") + } +} diff --git a/Nextcloud Cookbook iOS Client/RecipeImport/Scraper.playground/contents.xcplayground b/Nextcloud Cookbook iOS Client/RecipeImport/Scraper.playground/contents.xcplayground new file mode 100644 index 0000000..cf026f2 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/RecipeImport/Scraper.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file