diff --git a/frontend/pages/group/migrations.vue b/frontend/pages/group/migrations.vue index 89d41b05e89..1f8cdd94715 100644 --- a/frontend/pages/group/migrations.vue +++ b/frontend/pages/group/migrations.vue @@ -132,14 +132,14 @@ export default defineComponent({ text: i18n.tc("migration.plantoeat.title"), value: MIGRATIONS.plantoeat, }, - { - text: i18n.tc("migration.tandoor.title"), - value: MIGRATIONS.tandoor, - }, { text: i18n.tc("migration.recipekeeper.title"), value: MIGRATIONS.recipekeeper, }, + { + text: i18n.tc("migration.tandoor.title"), + value: MIGRATIONS.tandoor, + }, ]; const _content = { [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]: { text: i18n.tc("migration.tandoor.description-long"), 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) { diff --git a/mealie/services/migrations/_migration_base.py b/mealie/services/migrations/_migration_base.py index c8899dce5b8..e550e7a9579 100644 --- a/mealie/services/migrations/_migration_base.py +++ b/mealie/services/migrations/_migration_base.py @@ -74,6 +74,28 @@ def __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: raise NotImplementedError diff --git a/mealie/services/migrations/chowdown.py b/mealie/services/migrations/chowdown.py index 131e87a7c6d..9b8bd1de302 100644 --- a/mealie/services/migrations/chowdown.py +++ b/mealie/services/migrations/chowdown.py @@ -20,12 +20,24 @@ def __init__(self, **kwargs): 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: with tempfile.TemporaryDirectory() as tmpdir: with zipfile.ZipFile(self.archive) as zip_file: zip_file.extractall(tmpdir) - temp_path = Path(tmpdir) + temp_path = self.get_zip_base_path(Path(tmpdir)) chow_dir = next(temp_path.iterdir()) image_dir = temp_path.joinpath(chow_dir, "images") diff --git a/mealie/services/migrations/copymethat.py b/mealie/services/migrations/copymethat.py index d3b14f28cda..88515c1f5de 100644 --- a/mealie/services/migrations/copymethat.py +++ b/mealie/services/migrations/copymethat.py @@ -86,7 +86,7 @@ def _migrate(self) -> None: with zipfile.ZipFile(self.archive) as zip_file: zip_file.extractall(tmpdir) - source_dir = Path(tmpdir) + source_dir = self.get_zip_base_path(Path(tmpdir)) recipes_as_dicts: list[dict] = [] for recipes_data_file in source_dir.glob("*.html"): diff --git a/mealie/services/migrations/mealie_alpha.py b/mealie/services/migrations/mealie_alpha.py index 57fc374ccb6..c958c7dd62f 100644 --- a/mealie/services/migrations/mealie_alpha.py +++ b/mealie/services/migrations/mealie_alpha.py @@ -25,6 +25,18 @@ def __init__(self, **kwargs): 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: if recipe.get("categories", False): recipe["recipeCategory"] = recipe.get("categories") @@ -55,7 +67,7 @@ def _migrate(self) -> None: with zipfile.ZipFile(self.archive) as zip_file: zip_file.extractall(tmpdir) - temp_path = Path(tmpdir) + temp_path = self.get_zip_base_path(Path(tmpdir)) recipe_lookup: dict[str, Path] = {} recipes: list[Recipe] = [] diff --git a/mealie/services/migrations/nextcloud.py b/mealie/services/migrations/nextcloud.py index 90a4809f743..c8b897dbe44 100644 --- a/mealie/services/migrations/nextcloud.py +++ b/mealie/services/migrations/nextcloud.py @@ -57,6 +57,18 @@ def __init__(self, **kwargs): 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: # Unzip File into temp directory @@ -65,7 +77,8 @@ def _migrate(self) -> None: with zipfile.ZipFile(self.archive) as zip_file: 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))} all_recipes = [] diff --git a/mealie/services/migrations/recipekeeper.py b/mealie/services/migrations/recipekeeper.py index 8745b71b73e..ae463e644f9 100644 --- a/mealie/services/migrations/recipekeeper.py +++ b/mealie/services/migrations/recipekeeper.py @@ -11,6 +11,17 @@ 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): meta = {} for item in recipe.find_all(lambda x: x.has_attr("itemprop")): @@ -59,7 +70,7 @@ def __init__(self, **kwargs): key="recipeIngredient", 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="prepTime", alias="prepTime", func=parse_iso8601_duration), MigrationAlias(key="image", alias="photo0"), @@ -77,7 +88,7 @@ def _migrate(self) -> None: with zipfile.ZipFile(self.archive) as zip_file: zip_file.extractall(tmpdir) - source_dir = Path(tmpdir) / "recipekeeperhtml" + source_dir = self.get_zip_base_path(Path(tmpdir)) recipes_as_dicts: list[dict] = [] with open(source_dir / "recipes.html") as fp: diff --git a/mealie/services/migrations/tandoor.py b/mealie/services/migrations/tandoor.py index f94185ea4c9..7806e55ad84 100644 --- a/mealie/services/migrations/tandoor.py +++ b/mealie/services/migrations/tandoor.py @@ -109,7 +109,7 @@ def _migrate(self) -> None: with zipfile.ZipFile(self.archive) as zip_file: zip_file.extractall(tmpdir) - source_dir = Path(tmpdir) + source_dir = self.get_zip_base_path(Path(tmpdir)) recipes_as_dicts: list[dict] = [] for i, recipe_zip_file in enumerate(source_dir.glob("*.zip")): diff --git a/tests/data/migrations/recipekeeper.zip b/tests/data/migrations/recipekeeper.zip index 1136177f152..298d4d3fb95 100644 Binary files a/tests/data/migrations/recipekeeper.zip and b/tests/data/migrations/recipekeeper.zip differ