mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-09 09:23:12 -05:00
feature/finish-recipe-assets (#384)
* add features to readme
* Copy markdown reference
* prop as whole recipe
* parameter as url instead of query
* add card styling to editor
* move images to /recipes/{slug}/images
* add image to breaking changes
* fix delete and import errors
* fix debug/about response
* logger updates
* dashboard ui
* add server side events
* unorganized routes
* default slot
* add backup viewer to dashboard
* format
* add dialog to backup imports
* initial event support
* delete assets when removed
Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -9,6 +9,7 @@ from mealie.core import root_logger
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import create_session
|
||||
from mealie.services.events import create_backup_event
|
||||
from pathvalidate import sanitize_filename
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
@@ -32,7 +33,7 @@ class ExportDatabase:
|
||||
export_tag = datetime.now().strftime("%Y-%b-%d")
|
||||
|
||||
self.main_dir = app_dirs.TEMP_DIR.joinpath(export_tag)
|
||||
self.img_dir = self.main_dir.joinpath("images")
|
||||
self.recipes = self.main_dir.joinpath("recipes")
|
||||
self.templates_dir = self.main_dir.joinpath("templates")
|
||||
|
||||
try:
|
||||
@@ -43,7 +44,7 @@ class ExportDatabase:
|
||||
|
||||
required_dirs = [
|
||||
self.main_dir,
|
||||
self.img_dir,
|
||||
self.recipes,
|
||||
self.templates_dir,
|
||||
]
|
||||
|
||||
@@ -67,10 +68,10 @@ class ExportDatabase:
|
||||
with open(out_file, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
def export_images(self):
|
||||
shutil.copytree(app_dirs.IMG_DIR, self.img_dir, dirs_exist_ok=True)
|
||||
def export_recipe_dirs(self):
|
||||
shutil.copytree(app_dirs.RECIPE_DATA_DIR, self.recipes, dirs_exist_ok=True)
|
||||
|
||||
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True):
|
||||
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True, slug_folder=False):
|
||||
items = [x.dict() for x in items]
|
||||
out_dir = self.main_dir.joinpath(folder_name)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -79,8 +80,10 @@ class ExportDatabase:
|
||||
ExportDatabase._write_json_file(items, out_dir.joinpath(f"{folder_name}.json"))
|
||||
else:
|
||||
for item in items:
|
||||
filename = sanitize_filename(f"{item.get('name')}.json")
|
||||
ExportDatabase._write_json_file(item, out_dir.joinpath(filename))
|
||||
final_dest = out_dir if not slug_folder else out_dir.joinpath(item.get("slug"))
|
||||
final_dest.mkdir(exist_ok=True)
|
||||
filename = sanitize_filename(f"{item.get('slug')}.json")
|
||||
ExportDatabase._write_json_file(item, final_dest.joinpath(filename))
|
||||
|
||||
@staticmethod
|
||||
def _write_json_file(data: Union[dict, list], out_file: Path):
|
||||
@@ -121,9 +124,9 @@ def backup_all(
|
||||
|
||||
if export_recipes:
|
||||
all_recipes = db.recipes.get_all(session)
|
||||
db_export.export_items(all_recipes, "recipes", export_list=False)
|
||||
db_export.export_recipe_dirs()
|
||||
db_export.export_items(all_recipes, "recipes", export_list=False, slug_folder=True)
|
||||
db_export.export_templates(all_recipes)
|
||||
db_export.export_images()
|
||||
|
||||
if export_settings:
|
||||
all_settings = db.settings.get_all(session)
|
||||
@@ -148,3 +151,5 @@ def auto_backup_job():
|
||||
session = create_session()
|
||||
backup_all(session=session, tag="Auto", templates=templates)
|
||||
logger.info("Auto Backup Called")
|
||||
create_backup_event("Automated Backup", "Automated backup created", session)
|
||||
session.close()
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Callable, List
|
||||
from typing import Callable
|
||||
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.db.database import db
|
||||
@@ -49,7 +49,7 @@ class ImportDatabase:
|
||||
def import_recipes(self):
|
||||
recipe_dir: Path = self.import_dir.joinpath("recipes")
|
||||
imports = []
|
||||
successful_imports = []
|
||||
successful_imports = {}
|
||||
|
||||
recipes = ImportDatabase.read_models_file(
|
||||
file_path=recipe_dir, model=Recipe, single_file=False, migrate=ImportDatabase._recipe_migration
|
||||
@@ -68,7 +68,7 @@ class ImportDatabase:
|
||||
)
|
||||
|
||||
if import_status.status:
|
||||
successful_imports.append(recipe.slug)
|
||||
successful_imports.update({recipe.slug: recipe})
|
||||
|
||||
imports.append(import_status)
|
||||
|
||||
@@ -105,15 +105,25 @@ class ImportDatabase:
|
||||
|
||||
return recipe_dict
|
||||
|
||||
def _import_images(self, successful_imports: List[str]):
|
||||
def _import_images(self, successful_imports: list[Recipe]):
|
||||
image_dir = self.import_dir.joinpath("images")
|
||||
for image in image_dir.iterdir():
|
||||
if image.stem in successful_imports:
|
||||
if image.is_dir():
|
||||
dest = app_dirs.IMG_DIR.joinpath(image.stem)
|
||||
shutil.copytree(image, dest, dirs_exist_ok=True)
|
||||
if image.is_file():
|
||||
shutil.copy(image, app_dirs.IMG_DIR)
|
||||
|
||||
if image_dir.exists(): # Migrate from before v0.5.0
|
||||
for image in image_dir.iterdir():
|
||||
item: Recipe = successful_imports.get(image.stem)
|
||||
|
||||
if item:
|
||||
dest_dir = item.image_dir
|
||||
|
||||
if image.is_dir():
|
||||
shutil.copytree(image, dest_dir, dirs_exist_ok=True)
|
||||
|
||||
if image.is_file():
|
||||
shutil.copy(image, dest_dir)
|
||||
|
||||
else:
|
||||
recipe_dir = self.import_dir.joinpath("recipes")
|
||||
shutil.copytree(recipe_dir, app_dirs.RECIPE_DATA_DIR, dirs_exist_ok=True)
|
||||
|
||||
minify.migrate_images()
|
||||
|
||||
@@ -227,7 +237,7 @@ class ImportDatabase:
|
||||
return [model(**g) for g in file_data]
|
||||
|
||||
all_models = []
|
||||
for file in file_path.glob("*.json"):
|
||||
for file in file_path.glob("**/*.json"):
|
||||
with open(file, "r") as f:
|
||||
file_data = json.loads(f.read())
|
||||
|
||||
|
||||
40
mealie/services/events.py
Normal file
40
mealie/services/events.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import create_session
|
||||
from mealie.schema.events import Event, EventCategory
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
||||
def save_event(title, text, category, session: Session):
|
||||
event = Event(title=title, text=text, category=category)
|
||||
session = session or create_session()
|
||||
db.events.create(session, event.dict())
|
||||
|
||||
|
||||
def create_general_event(title, text, session=None):
|
||||
category = EventCategory.general
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
||||
|
||||
def create_recipe_event(title, text, session=None):
|
||||
category = EventCategory.recipe
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
||||
|
||||
def create_backup_event(title, text, session=None):
|
||||
category = EventCategory.backup
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
||||
|
||||
def create_scheduled_event(title, text, session=None):
|
||||
category = EventCategory.scheduled
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
||||
|
||||
def create_migration_event(title, text, session=None):
|
||||
category = EventCategory.migration
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
||||
|
||||
def create_sign_up_event(title, text, session=None):
|
||||
category = EventCategory.sign_up
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
from mealie.core import root_logger
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.services.image import minify
|
||||
|
||||
logger = root_logger.get_logger()
|
||||
@@ -20,47 +20,11 @@ class ImageOptions:
|
||||
IMG_OPTIONS = ImageOptions()
|
||||
|
||||
|
||||
def read_image(recipe_slug: str, image_type: str = "original") -> Path:
|
||||
"""returns the path to the image file for the recipe base of image_type
|
||||
|
||||
Args:
|
||||
recipe_slug (str): Recipe Slug
|
||||
image_type (str, optional): Glob Style Matcher "original*" | "min-original* | "tiny-original*"
|
||||
|
||||
Returns:
|
||||
Path: [description]
|
||||
"""
|
||||
recipe_slug = recipe_slug.split(".")[0] # Incase of File Name
|
||||
recipe_image_dir = app_dirs.IMG_DIR.joinpath(recipe_slug)
|
||||
|
||||
for file in recipe_image_dir.glob(image_type):
|
||||
return file
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def rename_image(original_slug, new_slug) -> Path:
|
||||
current_path = app_dirs.IMG_DIR.joinpath(original_slug)
|
||||
new_path = app_dirs.IMG_DIR.joinpath(new_slug)
|
||||
|
||||
try:
|
||||
new_path = current_path.rename(new_path)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Image Directory {original_slug} Doesn't Exist")
|
||||
|
||||
return new_path
|
||||
|
||||
|
||||
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path:
|
||||
try:
|
||||
delete_image(recipe_slug)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
image_dir = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}"))
|
||||
image_dir.mkdir(exist_ok=True, parents=True)
|
||||
image_dir = Recipe(slug=recipe_slug).image_dir
|
||||
extension = extension.replace(".", "")
|
||||
image_path = image_dir.joinpath(f"original.{extension}")
|
||||
image_path.unlink(missing_ok=True)
|
||||
|
||||
if isinstance(file_data, Path):
|
||||
shutil.copy2(file_data, image_path)
|
||||
@@ -77,12 +41,6 @@ def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path:
|
||||
return image_path
|
||||
|
||||
|
||||
def delete_image(recipe_slug: str) -> str:
|
||||
recipe_slug = recipe_slug.split(".")[0]
|
||||
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
|
||||
return shutil.rmtree(file)
|
||||
|
||||
|
||||
def scrape_image(image_url: str, slug: str) -> Path:
|
||||
if isinstance(image_url, str): # Handles String Types
|
||||
image_url = image_url
|
||||
@@ -96,7 +54,7 @@ def scrape_image(image_url: str, slug: str) -> Path:
|
||||
image_url = image_url.get("url")
|
||||
|
||||
filename = slug + "." + image_url.split(".")[-1]
|
||||
filename = app_dirs.IMG_DIR.joinpath(filename)
|
||||
filename = Recipe(slug=slug).image_dir.joinpath(filename)
|
||||
|
||||
try:
|
||||
r = requests.get(image_url, stream=True)
|
||||
|
||||
@@ -4,10 +4,8 @@ from pathlib import Path
|
||||
|
||||
from mealie.core import root_logger
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import create_session
|
||||
from mealie.schema.recipe import Recipe
|
||||
from PIL import Image
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
logger = root_logger.get_logger()
|
||||
|
||||
@@ -20,11 +18,7 @@ class ImageSizes:
|
||||
|
||||
|
||||
def get_image_sizes(org_img: Path, min_img: Path, tiny_img: Path) -> ImageSizes:
|
||||
return ImageSizes(
|
||||
org=sizeof_fmt(org_img),
|
||||
min=sizeof_fmt(min_img),
|
||||
tiny=sizeof_fmt(tiny_img),
|
||||
)
|
||||
return ImageSizes(org=sizeof_fmt(org_img), min=sizeof_fmt(min_img), tiny=sizeof_fmt(tiny_img))
|
||||
|
||||
|
||||
def minify_image(image_file: Path) -> ImageSizes:
|
||||
@@ -110,28 +104,9 @@ def move_all_images():
|
||||
if new_file.is_file():
|
||||
new_file.unlink()
|
||||
image_file.rename(new_file)
|
||||
|
||||
|
||||
def validate_slugs_in_database(session: Session = None):
|
||||
def check_image_path(image_name: str, slug_path: str) -> bool:
|
||||
existing_path: Path = app_dirs.IMG_DIR.joinpath(image_name)
|
||||
slug_path: Path = app_dirs.IMG_DIR.joinpath(slug_path)
|
||||
|
||||
if existing_path.is_dir():
|
||||
slug_path.rename(existing_path)
|
||||
else:
|
||||
logger.info("No Image Found")
|
||||
|
||||
session = session or create_session()
|
||||
all_recipes = db.recipes.get_all(session)
|
||||
|
||||
slugs_and_images = [(x.slug, x.image) for x in all_recipes]
|
||||
|
||||
for slug, image in slugs_and_images:
|
||||
image_slug = image.split(".")[0] # Remove Extension
|
||||
if slug != image_slug:
|
||||
logger.info(f"{slug}, Doesn't Match '{image_slug}'")
|
||||
check_image_path(image, slug)
|
||||
if image_file.is_dir():
|
||||
slug = image_file.name
|
||||
image_file.rename(Recipe(slug=slug).image_dir)
|
||||
|
||||
|
||||
def migrate_images():
|
||||
@@ -139,7 +114,7 @@ def migrate_images():
|
||||
|
||||
move_all_images()
|
||||
|
||||
for image in app_dirs.IMG_DIR.glob("*/original.*"):
|
||||
for image in app_dirs.RECIPE_DATA_DIR.glob("**/original.*"):
|
||||
|
||||
minify_image(image)
|
||||
|
||||
@@ -148,4 +123,3 @@ def migrate_images():
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_images()
|
||||
validate_slugs_in_database()
|
||||
|
||||
0
mealie/services/recipe/__init__.py
Normal file
0
mealie/services/recipe/__init__.py
Normal file
34
mealie/services/recipe/media.py
Normal file
34
mealie/services/recipe/media.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from pathlib import Path
|
||||
from shutil import copytree, rmtree
|
||||
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.recipe import Recipe
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def check_assets(original_slug, recipe: Recipe) -> None:
|
||||
if original_slug != recipe.slug:
|
||||
current_dir = app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
|
||||
|
||||
try:
|
||||
copytree(current_dir, recipe.directory, dirs_exist_ok=True)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Recipe Directory not Found: {original_slug}")
|
||||
logger.info(f"Renaming Recipe Directory: {original_slug} -> {recipe.slug}")
|
||||
|
||||
all_asset_files = [x.file_name for x in recipe.assets]
|
||||
for file in recipe.asset_dir.iterdir():
|
||||
file: Path
|
||||
if file.is_dir():
|
||||
continue
|
||||
if file.name not in all_asset_files:
|
||||
file.unlink()
|
||||
|
||||
|
||||
def delete_assets(recipe_slug):
|
||||
recipe_dir = Recipe(slug=recipe_slug).directory
|
||||
rmtree(recipe_dir, ignore_errors=True)
|
||||
logger.info(f"Recipe Directory Removed: {recipe_slug}")
|
||||
Reference in New Issue
Block a user