mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	fix: Recipe Keeper Errors and Other Safari Issues (#3712)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
		| @@ -132,14 +132,14 @@ export default defineComponent({ | |||||||
|         text: i18n.tc("migration.plantoeat.title"), |         text: i18n.tc("migration.plantoeat.title"), | ||||||
|         value: MIGRATIONS.plantoeat, |         value: MIGRATIONS.plantoeat, | ||||||
|       }, |       }, | ||||||
|       { |  | ||||||
|         text: i18n.tc("migration.tandoor.title"), |  | ||||||
|         value: MIGRATIONS.tandoor, |  | ||||||
|       }, |  | ||||||
|       { |       { | ||||||
|         text: i18n.tc("migration.recipekeeper.title"), |         text: i18n.tc("migration.recipekeeper.title"), | ||||||
|         value: MIGRATIONS.recipekeeper, |         value: MIGRATIONS.recipekeeper, | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         text: i18n.tc("migration.tandoor.title"), | ||||||
|  |         value: MIGRATIONS.tandoor, | ||||||
|  |       }, | ||||||
|     ]; |     ]; | ||||||
|     const _content = { |     const _content = { | ||||||
|       [MIGRATIONS.mealie]: { |       [MIGRATIONS.mealie]: { | ||||||
| @@ -312,6 +312,26 @@ export default defineComponent({ | |||||||
|           } |           } | ||||||
|         ], |         ], | ||||||
|       }, |       }, | ||||||
|  |       [MIGRATIONS.recipekeeper]: { | ||||||
|  |         text: i18n.tc("migration.recipekeeper.description-long"), | ||||||
|  |         acceptedFileType: ".zip", | ||||||
|  |         tree: [ | ||||||
|  |           { | ||||||
|  |             id: 1, | ||||||
|  |             icon: $globals.icons.zip, | ||||||
|  |             name: "recipekeeperhtml.zip", | ||||||
|  |             children: [ | ||||||
|  |                   { id: 2, name: "recipes.html", icon: $globals.icons.codeJson }, | ||||||
|  |                   { id: 3, name: "images", icon: $globals.icons.folderOutline, | ||||||
|  |                     children: [ | ||||||
|  |                     { id: 4, name: "image1.jpg", icon: $globals.icons.fileImage }, | ||||||
|  |                     { id: 5, name: "image2.jpg", icon: $globals.icons.fileImage }, | ||||||
|  |                     ] | ||||||
|  |                    }, | ||||||
|  |             ], | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|       [MIGRATIONS.tandoor]: { |       [MIGRATIONS.tandoor]: { | ||||||
|         text: i18n.tc("migration.tandoor.description-long"), |         text: i18n.tc("migration.tandoor.description-long"), | ||||||
|         acceptedFileType: ".zip", |         acceptedFileType: ".zip", | ||||||
| @@ -352,26 +372,6 @@ export default defineComponent({ | |||||||
|           } |           } | ||||||
|         ], |         ], | ||||||
|       }, |       }, | ||||||
|       [MIGRATIONS.recipekeeper]: { |  | ||||||
|         text: i18n.tc("migration.recipekeeper.description-long"), |  | ||||||
|         acceptedFileType: ".zip", |  | ||||||
|         tree: [ |  | ||||||
|           { |  | ||||||
|             id: 1, |  | ||||||
|             icon: $globals.icons.zip, |  | ||||||
|             name: "recipekeeperhtml.zip", |  | ||||||
|             children: [ |  | ||||||
|                   { id: 2, name: "recipes.html", icon: $globals.icons.codeJson }, |  | ||||||
|                   { id: 3, name: "images", icon: $globals.icons.folderOutline, |  | ||||||
|                     children: [ |  | ||||||
|                     { id: 4, name: "image1.jpeg", icon: $globals.icons.fileImage }, |  | ||||||
|                     { id: 5, name: "image2.jpeg", icon: $globals.icons.fileImage }, |  | ||||||
|                     ] |  | ||||||
|                    }, |  | ||||||
|             ], |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|       }, |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     function setFileObject(fileObject: File) { |     function setFileObject(fileObject: File) { | ||||||
|   | |||||||
| @@ -74,6 +74,28 @@ class BaseMigrator(BaseService): | |||||||
|  |  | ||||||
|         super().__init__() |         super().__init__() | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_zip_base_path(cls, path: Path) -> Path: | ||||||
|  |         # Safari mangles our ZIP structure and adds a "__MACOSX" directory at the root along with | ||||||
|  |         # an arbitrarily-named directory containing the actual contents. So, if we find a dunder directory | ||||||
|  |         # at the root (i.e. __MACOSX) we traverse down the first non-dunder directory and assume this is the base. | ||||||
|  |         # We assume migration exports never contain a directory that starts with "__". | ||||||
|  |         normal_dirs: list[Path] = [] | ||||||
|  |         dunder_dirs: list[Path] = [] | ||||||
|  |         for dir in path.iterdir(): | ||||||
|  |             if not dir.is_dir(): | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             if dir.name.startswith("__"): | ||||||
|  |                 dunder_dirs.append(dir) | ||||||
|  |             else: | ||||||
|  |                 normal_dirs.append(dir) | ||||||
|  |  | ||||||
|  |         if len(normal_dirs) == 1 and len(dunder_dirs) == 1: | ||||||
|  |             return normal_dirs[0] | ||||||
|  |         else: | ||||||
|  |             return path | ||||||
|  |  | ||||||
|     def _migrate(self) -> None: |     def _migrate(self) -> None: | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,12 +20,24 @@ class ChowdownMigrator(BaseMigrator): | |||||||
|             MigrationAlias(key="tags", alias="tags", func=split_by_comma), |             MigrationAlias(key="tags", alias="tags", func=split_by_comma), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_zip_base_path(cls, path: Path) -> Path: | ||||||
|  |         potential_path = super().get_zip_base_path(path) | ||||||
|  |         if path == potential_path: | ||||||
|  |             return path | ||||||
|  |  | ||||||
|  |         # make sure we didn't accidentally open a recipe dir | ||||||
|  |         if (potential_path / "recipe.json").exists(): | ||||||
|  |             return path | ||||||
|  |         else: | ||||||
|  |             return potential_path | ||||||
|  |  | ||||||
|     def _migrate(self) -> None: |     def _migrate(self) -> None: | ||||||
|         with tempfile.TemporaryDirectory() as tmpdir: |         with tempfile.TemporaryDirectory() as tmpdir: | ||||||
|             with zipfile.ZipFile(self.archive) as zip_file: |             with zipfile.ZipFile(self.archive) as zip_file: | ||||||
|                 zip_file.extractall(tmpdir) |                 zip_file.extractall(tmpdir) | ||||||
|  |  | ||||||
|             temp_path = Path(tmpdir) |             temp_path = self.get_zip_base_path(Path(tmpdir)) | ||||||
|  |  | ||||||
|             chow_dir = next(temp_path.iterdir()) |             chow_dir = next(temp_path.iterdir()) | ||||||
|             image_dir = temp_path.joinpath(chow_dir, "images") |             image_dir = temp_path.joinpath(chow_dir, "images") | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ class CopyMeThatMigrator(BaseMigrator): | |||||||
|             with zipfile.ZipFile(self.archive) as zip_file: |             with zipfile.ZipFile(self.archive) as zip_file: | ||||||
|                 zip_file.extractall(tmpdir) |                 zip_file.extractall(tmpdir) | ||||||
|  |  | ||||||
|             source_dir = Path(tmpdir) |             source_dir = self.get_zip_base_path(Path(tmpdir)) | ||||||
|  |  | ||||||
|             recipes_as_dicts: list[dict] = [] |             recipes_as_dicts: list[dict] = [] | ||||||
|             for recipes_data_file in source_dir.glob("*.html"): |             for recipes_data_file in source_dir.glob("*.html"): | ||||||
|   | |||||||
| @@ -25,6 +25,18 @@ class MealieAlphaMigrator(BaseMigrator): | |||||||
|             MigrationAlias(key="tags", alias="tags", func=split_by_comma), |             MigrationAlias(key="tags", alias="tags", func=split_by_comma), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_zip_base_path(cls, path: Path) -> Path: | ||||||
|  |         potential_path = super().get_zip_base_path(path) | ||||||
|  |         if path == potential_path: | ||||||
|  |             return path | ||||||
|  |  | ||||||
|  |         # make sure we didn't accidentally open the "recipes" dir | ||||||
|  |         if potential_path.name == "recipes": | ||||||
|  |             return path | ||||||
|  |         else: | ||||||
|  |             return potential_path | ||||||
|  |  | ||||||
|     def _convert_to_new_schema(self, recipe: dict) -> Recipe: |     def _convert_to_new_schema(self, recipe: dict) -> Recipe: | ||||||
|         if recipe.get("categories", False): |         if recipe.get("categories", False): | ||||||
|             recipe["recipeCategory"] = recipe.get("categories") |             recipe["recipeCategory"] = recipe.get("categories") | ||||||
| @@ -55,7 +67,7 @@ class MealieAlphaMigrator(BaseMigrator): | |||||||
|             with zipfile.ZipFile(self.archive) as zip_file: |             with zipfile.ZipFile(self.archive) as zip_file: | ||||||
|                 zip_file.extractall(tmpdir) |                 zip_file.extractall(tmpdir) | ||||||
|  |  | ||||||
|             temp_path = Path(tmpdir) |             temp_path = self.get_zip_base_path(Path(tmpdir)) | ||||||
|             recipe_lookup: dict[str, Path] = {} |             recipe_lookup: dict[str, Path] = {} | ||||||
|  |  | ||||||
|             recipes: list[Recipe] = [] |             recipes: list[Recipe] = [] | ||||||
|   | |||||||
| @@ -57,6 +57,18 @@ class NextcloudMigrator(BaseMigrator): | |||||||
|             MigrationAlias(key="performTime", alias="cookTime", func=parse_iso8601_duration), |             MigrationAlias(key="performTime", alias="cookTime", func=parse_iso8601_duration), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_zip_base_path(cls, path: Path) -> Path: | ||||||
|  |         potential_path = super().get_zip_base_path(path) | ||||||
|  |         if path == potential_path: | ||||||
|  |             return path | ||||||
|  |  | ||||||
|  |         # make sure we didn't accidentally open a recipe dir | ||||||
|  |         if (potential_path / "recipe.json").exists(): | ||||||
|  |             return path | ||||||
|  |         else: | ||||||
|  |             return potential_path | ||||||
|  |  | ||||||
|     def _migrate(self) -> None: |     def _migrate(self) -> None: | ||||||
|         # Unzip File into temp directory |         # Unzip File into temp directory | ||||||
|  |  | ||||||
| @@ -65,7 +77,8 @@ class NextcloudMigrator(BaseMigrator): | |||||||
|             with zipfile.ZipFile(self.archive) as zip_file: |             with zipfile.ZipFile(self.archive) as zip_file: | ||||||
|                 zip_file.extractall(tmpdir) |                 zip_file.extractall(tmpdir) | ||||||
|  |  | ||||||
|             potential_recipe_dirs = glob_walker(Path(tmpdir), glob_str="**/[!.]*.json", return_parent=True) |             base_dir = self.get_zip_base_path(Path(tmpdir)) | ||||||
|  |             potential_recipe_dirs = glob_walker(base_dir, glob_str="**/[!.]*.json", return_parent=True) | ||||||
|             nextcloud_dirs = {y.slug: y for x in potential_recipe_dirs if (y := NextcloudDir.from_dir(x))} |             nextcloud_dirs = {y.slug: y for x in potential_recipe_dirs if (y := NextcloudDir.from_dir(x))} | ||||||
|  |  | ||||||
|             all_recipes = [] |             all_recipes = [] | ||||||
|   | |||||||
| @@ -11,6 +11,17 @@ from .utils.migration_alias import MigrationAlias | |||||||
| from .utils.migration_helpers import import_image, parse_iso8601_duration | from .utils.migration_helpers import import_image, parse_iso8601_duration | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def clean_instructions(instructions: list[str]) -> list[str]: | ||||||
|  |     try: | ||||||
|  |         for i, instruction in enumerate(instructions): | ||||||
|  |             if instruction.startswith(f"{i + 1}. "): | ||||||
|  |                 instructions[i] = instruction.removeprefix(f"{i + 1}. ") | ||||||
|  |  | ||||||
|  |         return instructions | ||||||
|  |     except Exception: | ||||||
|  |         return instructions | ||||||
|  |  | ||||||
|  |  | ||||||
| def parse_recipe_div(recipe, image_path): | def parse_recipe_div(recipe, image_path): | ||||||
|     meta = {} |     meta = {} | ||||||
|     for item in recipe.find_all(lambda x: x.has_attr("itemprop")): |     for item in recipe.find_all(lambda x: x.has_attr("itemprop")): | ||||||
| @@ -59,7 +70,7 @@ class RecipeKeeperMigrator(BaseMigrator): | |||||||
|                 key="recipeIngredient", |                 key="recipeIngredient", | ||||||
|                 alias="recipeIngredients", |                 alias="recipeIngredients", | ||||||
|             ), |             ), | ||||||
|             MigrationAlias(key="recipeInstructions", alias="recipeDirections"), |             MigrationAlias(key="recipeInstructions", alias="recipeDirections", func=clean_instructions), | ||||||
|             MigrationAlias(key="performTime", alias="cookTime", func=parse_iso8601_duration), |             MigrationAlias(key="performTime", alias="cookTime", func=parse_iso8601_duration), | ||||||
|             MigrationAlias(key="prepTime", alias="prepTime", func=parse_iso8601_duration), |             MigrationAlias(key="prepTime", alias="prepTime", func=parse_iso8601_duration), | ||||||
|             MigrationAlias(key="image", alias="photo0"), |             MigrationAlias(key="image", alias="photo0"), | ||||||
| @@ -77,7 +88,7 @@ class RecipeKeeperMigrator(BaseMigrator): | |||||||
|             with zipfile.ZipFile(self.archive) as zip_file: |             with zipfile.ZipFile(self.archive) as zip_file: | ||||||
|                 zip_file.extractall(tmpdir) |                 zip_file.extractall(tmpdir) | ||||||
|  |  | ||||||
|             source_dir = Path(tmpdir) / "recipekeeperhtml" |             source_dir = self.get_zip_base_path(Path(tmpdir)) | ||||||
|  |  | ||||||
|             recipes_as_dicts: list[dict] = [] |             recipes_as_dicts: list[dict] = [] | ||||||
|             with open(source_dir / "recipes.html") as fp: |             with open(source_dir / "recipes.html") as fp: | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ class TandoorMigrator(BaseMigrator): | |||||||
|             with zipfile.ZipFile(self.archive) as zip_file: |             with zipfile.ZipFile(self.archive) as zip_file: | ||||||
|                 zip_file.extractall(tmpdir) |                 zip_file.extractall(tmpdir) | ||||||
|  |  | ||||||
|             source_dir = Path(tmpdir) |             source_dir = self.get_zip_base_path(Path(tmpdir)) | ||||||
|  |  | ||||||
|             recipes_as_dicts: list[dict] = [] |             recipes_as_dicts: list[dict] = [] | ||||||
|             for i, recipe_zip_file in enumerate(source_dir.glob("*.zip")): |             for i, recipe_zip_file in enumerate(source_dir.glob("*.zip")): | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user