Merge pull request #60 from hay-kot/nextcloud-migration
Nextcloud migration
This commit is contained in:
commit
85f9235a17
26 changed files with 983 additions and 131 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -12,6 +12,7 @@ mealie/temp/api.html
|
|||
mealie/data/backups/*
|
||||
mealie/data/debug/*
|
||||
mealie/data/img/*
|
||||
mealie/data/migration/*
|
||||
!mealie/dist/*
|
||||
|
||||
#Exception to keep folders
|
||||
|
@ -19,6 +20,7 @@ mealie/data/img/*
|
|||
!mealie/data/backups/.gitkeep
|
||||
!mealie/data/backups/dev_sample_data*
|
||||
!mealie/data/debug/.gitkeep
|
||||
!mealie/data/migration/.gitkeep
|
||||
!mealie/data/img/.gitkeep
|
||||
|
||||
.DS_Store
|
||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -11,6 +11,7 @@
|
|||
"python.discoverTest": true,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"cSpell.enableFiletypes": [
|
||||
"!javascript",
|
||||
"!python"
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
|
|
1
dev/non-working-links.txt
Normal file
1
dev/non-working-links.txt
Normal file
|
@ -0,0 +1 @@
|
|||
http://www.cookingforkeeps.com/2013/02/05/blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2/
|
|
@ -6,7 +6,7 @@ Recipes extras are a key feature of the Mealie API. They allow you to create cus
|
|||
|
||||
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
|
||||
|
||||
![api-extras-gif](/gifs/api-extras.gif)
|
||||
![api-extras-gif](../gifs/api-extras.gif)
|
||||
|
||||
|
||||
## Examples
|
||||
|
|
|
@ -6,8 +6,8 @@ A quality update with major props to [zackbcom](https://github.com/zackbcom) for
|
|||
- Fixed empty backup failure without markdown template
|
||||
- Fixed opacity issues with marked steps - [mtoohey31](https://github.com/mtoohey31)
|
||||
- Fixed hot-reloading development environment - [grssmnn](https://github.com/grssmnn)
|
||||
- Fixed recipe not saving without image
|
||||
- Fixed parsing error on image property null
|
||||
- Fixed recipe not saving without image - Issue #7 + Issue #54
|
||||
- Fixed parsing error on image property null - Issue #43
|
||||
|
||||
### General Improvements
|
||||
- Added Confirmation component to deleting recipes - [zackbcom](https://github.com/zackbcom)
|
||||
|
@ -20,7 +20,7 @@ A quality update with major props to [zackbcom](https://github.com/zackbcom) for
|
|||
- Users can now add custom json key/value pairs to all recipes via the editor for access in 3rd part applications. For example users can add a "message" field in the extras that can be accessed on API calls to play a message over google home.
|
||||
- Improved image rendering (nearly x2 speed)
|
||||
- Improved documentation + API Documentation
|
||||
- Improved recipe parsing
|
||||
- Improved recipe parsing - Issue #51
|
||||
- User feedback on backup importing
|
||||
|
||||
## v0.0.1 - Pre-release Patch
|
||||
|
|
|
@ -5,7 +5,12 @@ import { store } from "../store/store";
|
|||
const migrationBase = baseURL + "migration/";
|
||||
|
||||
const migrationURLs = {
|
||||
upload: migrationBase + "upload/",
|
||||
delete: (file) => `${migrationBase}${file}/delete/`,
|
||||
chowdownURL: migrationBase + "chowdown/repo/",
|
||||
nextcloudAvaiable: migrationBase + "nextcloud/available/",
|
||||
nextcloudImport: (selection) =>
|
||||
`${migrationBase}nextcloud/${selection}/import/`,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -15,4 +20,24 @@ export default {
|
|||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
async getNextcloudImports() {
|
||||
let response = await apiReq.get(migrationURLs.nextcloudAvaiable);
|
||||
return response.data;
|
||||
},
|
||||
async importNextcloud(selected) {
|
||||
let response = await apiReq.post(migrationURLs.nextcloudImport(selected));
|
||||
return response.data;
|
||||
},
|
||||
async uploadFile(form_data) {
|
||||
let response = await apiReq.post(migrationURLs.upload, form_data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
async delete(file_folder_name) {
|
||||
let response = await apiReq.delete(migrationURLs.delete(file_folder_name));
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
73
frontend/src/components/Settings/Migration/ChowdownCard.vue
Normal file
73
frontend/src/components/Settings/Migration/ChowdownCard.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<v-card-text>
|
||||
<p>
|
||||
Currently Chowdown via public Repo URL is the only supported type of
|
||||
migration
|
||||
</p>
|
||||
<v-form ref="form">
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="5" sm="5">
|
||||
<v-text-field
|
||||
v-model="repo"
|
||||
label="Chowdown Repo URL"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="importRepo"> Migrate </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-alert v-if="failedRecipes[1]" outlined dense type="error">
|
||||
<h4>Failed Recipes</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedRecipes" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
<v-alert v-if="failedImages[1]" outlined dense type="error">
|
||||
<h4>Failed Images</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedImages" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../../api";
|
||||
// import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
// import TimePicker from "./Webhooks/TimePicker";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processRan: false,
|
||||
failedImages: [],
|
||||
failedRecipes: [],
|
||||
repo: "",
|
||||
rules: {
|
||||
required: (v) => !!v || "Selection Required",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async importRepo() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.$emit("loading");
|
||||
let response = await api.migrations.migrateChowdown(this.repo);
|
||||
this.failedImages = response.failedImages;
|
||||
this.failedRecipes = response.failedRecipes;
|
||||
this.$emit("finished");
|
||||
this.processRan = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
103
frontend/src/components/Settings/Migration/NextcloudCard.vue
Normal file
103
frontend/src/components/Settings/Migration/NextcloudCard.vue
Normal file
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<v-card-text>
|
||||
<p>
|
||||
You can import recipes from either a zip file or a directory located in
|
||||
the /app/data/migraiton/ folder. Please review the documentation to ensure
|
||||
your directory structure matches what is expected
|
||||
</p>
|
||||
<v-form ref="form">
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="5" sm="12">
|
||||
<v-select
|
||||
:items="availableImports"
|
||||
v-model="selectedImport"
|
||||
label="Nextcloud Data"
|
||||
:rules="[rules.required]"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" sm="12">
|
||||
<v-btn text color="info" @click="importRecipes"> Migrate </v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="1" sm="12">
|
||||
<v-btn text color="error" @click="deleteImportValidation">
|
||||
Delete
|
||||
</v-btn>
|
||||
<Confirmation
|
||||
title="Delete Data"
|
||||
message="Are you sure you want to delete this migration data?"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
v-on:confirm="deleteImport()"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="5" sm="12">
|
||||
<UploadMigrationButton @uploaded="getAvaiableImports" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<SuccessFailureAlert
|
||||
success-header="Successfully Imported from Nextcloud"
|
||||
:success="successfulImports"
|
||||
failed-header="Failed Imports"
|
||||
:failed="failedImports"
|
||||
/>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../../api";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import UploadMigrationButton from "./UploadMigrationButton";
|
||||
import Confirmation from "../../UI/Confirmation";
|
||||
export default {
|
||||
components: {
|
||||
SuccessFailureAlert,
|
||||
UploadMigrationButton,
|
||||
Confirmation,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
successfulImports: [],
|
||||
failedImports: [],
|
||||
availableImports: [],
|
||||
selectedImport: null,
|
||||
rules: {
|
||||
required: (v) => !!v || "Selection Required",
|
||||
},
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.getAvaiableImports();
|
||||
},
|
||||
methods: {
|
||||
async getAvaiableImports() {
|
||||
this.availableImports = await api.migrations.getNextcloudImports();
|
||||
},
|
||||
async importRecipes() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.$emit("loading");
|
||||
let data = await api.migrations.importNextcloud(this.selectedImport);
|
||||
|
||||
this.successfulImports = data.successful;
|
||||
this.failedImports = data.failed;
|
||||
this.$emit("finished");
|
||||
}
|
||||
},
|
||||
deleteImportValidation() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.$refs.deleteThemeConfirm.open();
|
||||
}
|
||||
},
|
||||
async deleteImport() {
|
||||
await api.migrations.delete(this.selectedImport);
|
||||
this.getAvaiableImports();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<v-form ref="file">
|
||||
<v-file-input
|
||||
:loading="loading"
|
||||
label="Upload an Archive"
|
||||
v-model="file"
|
||||
accept=".zip"
|
||||
@change="upload"
|
||||
:prepend-icon="icon"
|
||||
class="file-icon"
|
||||
>
|
||||
</v-file-input>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../../api";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
loading: false,
|
||||
icon: "mdi-paperclip",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async upload() {
|
||||
if (this.file != null) {
|
||||
this.loading = true;
|
||||
let formData = new FormData();
|
||||
formData.append("archive", this.file);
|
||||
|
||||
await api.migrations.uploadFile(formData);
|
||||
|
||||
this.loading = false;
|
||||
this.$emit("uploaded");
|
||||
this.file = null;
|
||||
this.icon = "mdi-check";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.file-icon {
|
||||
transition-duration: 5s;
|
||||
}
|
||||
</style>
|
|
@ -2,64 +2,42 @@
|
|||
<v-card :loading="loading">
|
||||
<v-card-title class="headline"> Recipe Migration </v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<p>
|
||||
Currently Chowdown via public Repo URL is the only supported type of
|
||||
migration
|
||||
</p>
|
||||
<v-form>
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="5" sm="5">
|
||||
<v-text-field v-model="repo" label="Chowdown Repo URL">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="importRepo"> Migrate </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-alert v-if="failedRecipes[1]" outlined dense type="error">
|
||||
<h4>Failed Recipes</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedRecipes" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
<v-alert v-if="failedImages[1]" outlined dense type="error">
|
||||
<h4>Failed Images</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedImages" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-tabs v-model="tab">
|
||||
<v-tab>Chowdown</v-tab>
|
||||
<v-tab>Nextcloud Recipes</v-tab>
|
||||
|
||||
<v-tab-item>
|
||||
<ChowdownCard @loading="loading = true" @finished="finished" />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<NextcloudCard @loading="loading = true" @finished="finished" />
|
||||
</v-tab-item>
|
||||
</v-tabs>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import api from "../../../api";
|
||||
import ChowdownCard from "./ChowdownCard";
|
||||
import NextcloudCard from "./NextcloudCard";
|
||||
// import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
// import TimePicker from "./Webhooks/TimePicker";
|
||||
export default {
|
||||
components: {
|
||||
ChowdownCard,
|
||||
NextcloudCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processRan: false,
|
||||
tab: null,
|
||||
loading: false,
|
||||
failedImages: [],
|
||||
failedRecipes: [],
|
||||
repo: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async importRepo() {
|
||||
this.loading = true;
|
||||
let response = await api.migrations.migrateChowdown(this.repo);
|
||||
this.failedImages = response.failedImages;
|
||||
this.failedRecipes = response.failedRecipes;
|
||||
finished() {
|
||||
this.loading = false;
|
||||
this.processRan = true;
|
||||
this.$store.dispatch("requestRecentRecipes");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -29,7 +29,6 @@ app = FastAPI(
|
|||
redoc_url=redoc_url,
|
||||
)
|
||||
|
||||
|
||||
# Mount Vue Frontend only in production
|
||||
if PRODUCTION:
|
||||
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
||||
|
|
|
@ -1,94 +1,91 @@
|
|||
{
|
||||
"@context": "https://schema.org/",
|
||||
"@context": "http://schema.org/",
|
||||
"@type": "Recipe",
|
||||
"name": "Pressure Cooker Chicken Tortilla Soup",
|
||||
"description": "",
|
||||
"name": "Jalape\u00f1o Popper Dip",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Kitschen Cat"
|
||||
"name": "Michelle"
|
||||
},
|
||||
"image": null,
|
||||
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/",
|
||||
"description": "Jalapeno Popper Dip is creamy, cheesy and has just the perfect amount of kick. Great appetizer for your next party or watching the big game!",
|
||||
"datePublished": "2016-02-22 00:01:37+00:00",
|
||||
"image": "jalapeno-popper-dip.jpg",
|
||||
"recipeYield": [
|
||||
"10",
|
||||
"10 to 12 servings"
|
||||
],
|
||||
"prepTime": "0:15:00",
|
||||
"cookTime": "0:30:00",
|
||||
"totalTime": "0:45:00",
|
||||
"recipeIngredient": [
|
||||
"2 Large Chicken Breasts",
|
||||
"12 oz your favorite salsa",
|
||||
"6 Cups Chicken Broth",
|
||||
"1 onion, chopped",
|
||||
"1 red bell pepper, diced",
|
||||
"2 teaspoons cumin",
|
||||
"1 tablespoon chili powder",
|
||||
"2 teaspoons salt",
|
||||
"1/2 teaspoon black pepper",
|
||||
"1/8 teaspoon cayenne pepper",
|
||||
"4 ounces tomato paste",
|
||||
"1 15oz can black beans, drained and rinsed",
|
||||
"2 cups frozen corn",
|
||||
"limes, sour cream or greek yogurt, cilantro, green onion, avocado, tortilla chips"
|
||||
"16 ounces cream cheese (at room temperature)",
|
||||
"1 cup mayonnaise",
|
||||
"8 pieces of bacon (cooked and chopped)",
|
||||
"6 jalape\u00f1os (seeded and minced (if you can't get fresh, substitute a 4-ounce can diced jalape\u00f1o peppers, drained))",
|
||||
"2 cloves garlic (minced)",
|
||||
"\u00bd teaspoon cumin",
|
||||
"6 ounces cheddar cheese (shredded (about 1\u00bd cups))",
|
||||
"1 cup panko breadcrumbs",
|
||||
"1 cup grated Parmesan cheese",
|
||||
"4 tablespoons unsalted butter, melted"
|
||||
],
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "In pressure cooking pot, add chicken, salsa, chicken broth, onion, bell pepper, cumin, chili powder, salt, black pepper, cayenne pepper, and tomato paste. Stir together.",
|
||||
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-1"
|
||||
"text": "Preheat oven to 375 degrees F.",
|
||||
"name": "Preheat oven to 375 degrees F.",
|
||||
"url": "https://www.browneyedbaker.com/jalapeno-popper-dip/#wprm-recipe-44993-step-0-0"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Lock lid and set to high pressure for 10 minutes.",
|
||||
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-2"
|
||||
"text": "Combine the cream cheese, mayonnaise, bacon, jalapenos, garlic, cumin and cheddar cheese in a mixing bowl. Transfer the mixture into 2-quart baking dish.",
|
||||
"name": "Combine the cream cheese, mayonnaise, bacon, jalapenos, garlic, cumin and cheddar cheese in a mixing bowl. Transfer the mixture into 2-quart baking dish.",
|
||||
"url": "https://www.browneyedbaker.com/jalapeno-popper-dip/#wprm-recipe-44993-step-0-1"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "When time is up, allow pressure to naturally release for 10 minutes and then use a quick release to get all the remaining pressure out.",
|
||||
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-3"
|
||||
"text": "Combine the panko breadcrumbs, Parmesan cheese and melted butter in a small bowl, tossing with a fork until the mixture is evenly moistened. Sprinkle evenly over the cream cheese mixture.",
|
||||
"name": "Combine the panko breadcrumbs, Parmesan cheese and melted butter in a small bowl, tossing with a fork until the mixture is evenly moistened. Sprinkle evenly over the cream cheese mixture.",
|
||||
"url": "https://www.browneyedbaker.com/jalapeno-popper-dip/#wprm-recipe-44993-step-0-2"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Remove lid and shred chicken using two forks.",
|
||||
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-4"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Set pressure cooker to “simmer” setting and add black beans and corn. Stir until corn is heated through.",
|
||||
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-5"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Ladle into bowls and top with a squeeze of lime juice, a dollop of sour cream or greek yogurt, a few sprigs of cilantro, chopped green onion, chopped avocado, and crushed tortilla chips.",
|
||||
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-6"
|
||||
"text": "Bake in the preheated oven for 25 to 30 minutes, until the top is golden brown and the dip is bubbling. Let rest for 5 minutes before serving. Serve with your favorite tortilla chips, crackers, vegetables, etc.",
|
||||
"name": "Bake in the preheated oven for 25 to 30 minutes, until the top is golden brown and the dip is bubbling. Let rest for 5 minutes before serving. Serve with your favorite tortilla chips, crackers, vegetables, etc.",
|
||||
"url": "https://www.browneyedbaker.com/jalapeno-popper-dip/#wprm-recipe-44993-step-0-3"
|
||||
}
|
||||
],
|
||||
"prepTime": "0:10:00",
|
||||
"cookTime": "0:10:00",
|
||||
"totalTime": "0:20:00",
|
||||
"recipeYield": "8",
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"reviewCount": "1",
|
||||
"ratingValue": "5"
|
||||
"ratingValue": "4.34",
|
||||
"ratingCount": "15"
|
||||
},
|
||||
"review": [
|
||||
{
|
||||
"@type": "Review",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"ratingValue": "5"
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Alison"
|
||||
},
|
||||
"datePublished": "2017-05-08",
|
||||
"reviewBody": "Simple and delicious, even my kids loved it!"
|
||||
}
|
||||
"recipeCategory": [
|
||||
"Appetizer"
|
||||
],
|
||||
"datePublished": "2017-01-18",
|
||||
"@id": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#recipe",
|
||||
"isPartOf": {
|
||||
"@id": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#webpage"
|
||||
"recipeCuisine": [
|
||||
"American"
|
||||
],
|
||||
"keywords": "cheese dip, game day food, party food",
|
||||
"nutrition": {
|
||||
"@type": "NutritionInformation",
|
||||
"calories": "560 kcal",
|
||||
"carbohydrateContent": "7 g",
|
||||
"proteinContent": "14 g",
|
||||
"fatContent": "52 g",
|
||||
"saturatedFatContent": "21 g",
|
||||
"cholesterolContent": "109 mg",
|
||||
"sodiumContent": "707 mg",
|
||||
"sugarContent": "2 g",
|
||||
"servingSize": "1 serving"
|
||||
},
|
||||
"mainEntityOfPage": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#webpage",
|
||||
"slug": "pressure-cooker-chicken-tortilla-soup",
|
||||
"orgURL": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/",
|
||||
"@id": "https://www.browneyedbaker.com/jalapeno-popper-dip/#recipe",
|
||||
"isPartOf": {
|
||||
"@id": "https://www.browneyedbaker.com/jalapeno-popper-dip/#article"
|
||||
},
|
||||
"mainEntityOfPage": "https://www.browneyedbaker.com/jalapeno-popper-dip/#webpage",
|
||||
"url": "https://www.browneyedbaker.com/jalapeno-popper-dip/",
|
||||
"slug": "jalapeno-popper-dip",
|
||||
"orgURL": "http://www.browneyedbaker.com/2011/08/03/jalapeno-popper-dip/",
|
||||
"categories": [],
|
||||
"tags": [],
|
||||
"dateAdded": null,
|
||||
|
|
0
mealie/data/migration/.gitkeep
Normal file
0
mealie/data/migration/.gitkeep
Normal file
|
@ -1,12 +1,16 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from models.backup_models import BackupJob
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, File, HTTPException, UploadFile
|
||||
from models.migration_models import ChowdownURL
|
||||
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
|
||||
from services.migrations.nextcloud import migrate as nextcloud_migrate
|
||||
from settings import MIGRATION_DIR
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Chowdown
|
||||
@router.post("/api/migration/chowdown/repo/", tags=["Migration"])
|
||||
async def import_chowdown_recipes(repo: ChowdownURL):
|
||||
""" Import Chowsdown Recipes from Repo URL """
|
||||
|
@ -23,3 +27,54 @@ async def import_chowdown_recipes(repo: ChowdownURL):
|
|||
"Unable to Migrate Recipes. See Log for Details"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Nextcloud
|
||||
@router.get("/api/migration/nextcloud/available/", tags=["Migration"])
|
||||
async def get_avaiable_nextcloud_imports():
|
||||
""" Returns a list of avaiable directories that can be imported into Mealie """
|
||||
available = []
|
||||
for dir in MIGRATION_DIR.iterdir():
|
||||
if dir.is_dir():
|
||||
available.append(dir.stem)
|
||||
elif dir.suffix == ".zip":
|
||||
available.append(dir.name)
|
||||
|
||||
return available
|
||||
|
||||
|
||||
@router.post("/api/migration/nextcloud/{selection}/import/", tags=["Migration"])
|
||||
async def import_nextcloud_directory(selection: str):
|
||||
""" Imports all the recipes in a given directory """
|
||||
|
||||
return nextcloud_migrate(selection)
|
||||
|
||||
|
||||
@router.delete("/api/migration/{file_folder_name}/delete/", tags=["Migration"])
|
||||
async def delete_migration_data(file_folder_name: str):
|
||||
""" Removes migration data from the file system """
|
||||
|
||||
remove_path = MIGRATION_DIR.joinpath(file_folder_name)
|
||||
|
||||
if remove_path.is_file():
|
||||
remove_path.unlink()
|
||||
elif remove_path.is_dir():
|
||||
shutil.rmtree(remove_path)
|
||||
else:
|
||||
SnackResponse.error("File/Folder not found.")
|
||||
|
||||
return SnackResponse.info(f"Migration Data Remove: {remove_path.absolute()}")
|
||||
|
||||
|
||||
@router.post("/api/migration/upload/", tags=["Migration"])
|
||||
async def upload_nextcloud_zipfile(archive: UploadFile = File(...)):
|
||||
""" Upload a .zip File to later be imported into Mealie """
|
||||
dest = MIGRATION_DIR.joinpath(archive.filename)
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
shutil.copyfileobj(archive.file, buffer)
|
||||
|
||||
if dest.is_file:
|
||||
return SnackResponse.success("Migration data uploaded")
|
||||
else:
|
||||
return SnackResponse.error("Failure uploading file")
|
||||
|
|
0
mealie/scratch.py
Normal file
0
mealie/scratch.py
Normal file
|
@ -52,7 +52,7 @@ def read_chowdown_file(recipe_file: Path) -> Recipe:
|
|||
elif x == 1:
|
||||
recipe_description = str(item)
|
||||
|
||||
except yaml.YAMLError as exc:
|
||||
except yaml.YAMLError:
|
||||
return
|
||||
|
||||
reformat_data = {
|
||||
|
|
89
mealie/services/migrations/nextcloud.py
Normal file
89
mealie/services/migrations/nextcloud.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import json
|
||||
import logging
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from services.recipe_services import IMG_DIR, Recipe
|
||||
from services.scrape_services import normalize_data, process_recipe_data
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
TEMP_DIR = CWD.parent.parent.joinpath("data", "temp")
|
||||
MIGRTAION_DIR = CWD.parent.parent.joinpath("data", "migration")
|
||||
|
||||
|
||||
def process_selection(selection: Path) -> Path:
|
||||
if selection.is_dir():
|
||||
return selection
|
||||
elif selection.suffix == ".zip":
|
||||
with zipfile.ZipFile(selection, "r") as zip_ref:
|
||||
nextcloud_dir = TEMP_DIR.joinpath("nextcloud")
|
||||
nextcloud_dir.mkdir(exist_ok=False, parents=True)
|
||||
zip_ref.extractall(nextcloud_dir)
|
||||
return nextcloud_dir
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def import_recipes(recipe_dir: Path) -> Recipe:
|
||||
image = False
|
||||
for file in recipe_dir.glob("full.*"):
|
||||
image = file
|
||||
|
||||
recipe_file = recipe_dir.joinpath("recipe.json")
|
||||
|
||||
with open(recipe_file, "r") as f:
|
||||
recipe_dict = json.loads(f.read())
|
||||
|
||||
recipe_dict = process_recipe_data(recipe_dict)
|
||||
recipe_data = normalize_data(recipe_dict)
|
||||
|
||||
image_name = None
|
||||
if image:
|
||||
image_name = recipe_data["slug"] + image.suffix
|
||||
recipe_data["image"] = image_name
|
||||
else:
|
||||
recipe_data["image"] = "none"
|
||||
|
||||
recipe = Recipe(**recipe_data)
|
||||
|
||||
if image:
|
||||
shutil.copy(image, IMG_DIR.joinpath(image_name))
|
||||
|
||||
return recipe
|
||||
|
||||
|
||||
def prep():
|
||||
try:
|
||||
shutil.rmtree(TEMP_DIR)
|
||||
except:
|
||||
pass
|
||||
TEMP_DIR.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
|
||||
def cleanup():
|
||||
shutil.rmtree(TEMP_DIR)
|
||||
|
||||
|
||||
def migrate(selection: str):
|
||||
prep()
|
||||
MIGRTAION_DIR.mkdir(exist_ok=True)
|
||||
selection = MIGRTAION_DIR.joinpath(selection)
|
||||
|
||||
nextcloud_dir = process_selection(selection)
|
||||
|
||||
successful_imports = []
|
||||
failed_imports = []
|
||||
for dir in nextcloud_dir.iterdir():
|
||||
if dir.is_dir():
|
||||
try:
|
||||
recipe = import_recipes(dir)
|
||||
recipe.save_to_db()
|
||||
successful_imports.append(recipe.name)
|
||||
except:
|
||||
logging.error(f"Failed Nextcloud Import: {dir.name}")
|
||||
failed_imports.append(dir.name)
|
||||
|
||||
cleanup()
|
||||
|
||||
return {"successful": successful_imports, "failed": failed_imports}
|
|
@ -1,7 +1,6 @@
|
|||
from typing import List
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from scrape_schema_recipe import scrape_url
|
||||
from slugify import slugify
|
||||
|
@ -18,7 +17,7 @@ def normalize_image_url(image) -> str:
|
|||
if type(image) == list:
|
||||
return image[0]
|
||||
elif type(image) == dict:
|
||||
return image['url']
|
||||
return image["url"]
|
||||
elif type(image) == str:
|
||||
return image
|
||||
else:
|
||||
|
@ -28,7 +27,9 @@ def normalize_image_url(image) -> str:
|
|||
def normalize_instructions(instructions) -> List[dict]:
|
||||
# One long string split by (possibly multiple) new lines
|
||||
if type(instructions) == str:
|
||||
return [{"text": line.strip()} for line in filter(None, instructions.splitlines())]
|
||||
return [
|
||||
{"text": line.strip()} for line in filter(None, instructions.splitlines())
|
||||
]
|
||||
|
||||
# Plain strings in a list
|
||||
elif type(instructions) == list and type(instructions[0]) == str:
|
||||
|
@ -36,7 +37,11 @@ def normalize_instructions(instructions) -> List[dict]:
|
|||
|
||||
# Dictionaries (let's assume it's a HowToStep) in a list
|
||||
elif type(instructions) == list and type(instructions[0]) == dict:
|
||||
return [{"text": step['text'].strip()} for step in instructions if step['@type'] == 'HowToStep']
|
||||
return [
|
||||
{"text": step["text"].strip()}
|
||||
for step in instructions
|
||||
if step["@type"] == "HowToStep"
|
||||
]
|
||||
|
||||
else:
|
||||
raise Exception(f"Unrecognised instruction format: {instructions}")
|
||||
|
@ -51,7 +56,9 @@ def normalize_yield(yld) -> str:
|
|||
|
||||
def normalize_data(recipe_data: dict) -> dict:
|
||||
recipe_data["recipeYield"] = normalize_yield(recipe_data.get("recipeYield"))
|
||||
recipe_data["recipeInstructions"] = normalize_instructions(recipe_data["recipeInstructions"])
|
||||
recipe_data["recipeInstructions"] = normalize_instructions(
|
||||
recipe_data["recipeInstructions"]
|
||||
)
|
||||
return recipe_data
|
||||
|
||||
|
||||
|
@ -67,13 +74,7 @@ def create_from_url(url: str) -> dict:
|
|||
return recipe.save_to_db()
|
||||
|
||||
|
||||
def process_recipe_url(url: str) -> dict:
|
||||
new_recipe: dict = scrape_url(url, python_objects=True)[0]
|
||||
logger.info(f"Recipe Scraped From Web: {new_recipe}")
|
||||
|
||||
if not new_recipe:
|
||||
return "fail" # TODO: Return Better Error Here
|
||||
|
||||
def process_recipe_data(new_recipe: dict, url=None) -> dict:
|
||||
slug = slugify(new_recipe["name"])
|
||||
mealie_tags = {
|
||||
"slug": slug,
|
||||
|
@ -87,8 +88,22 @@ def process_recipe_url(url: str) -> dict:
|
|||
|
||||
new_recipe.update(mealie_tags)
|
||||
|
||||
return new_recipe
|
||||
|
||||
|
||||
def process_recipe_url(url: str) -> dict:
|
||||
new_recipe: dict = scrape_url(url, python_objects=True)[0]
|
||||
logger.info(f"Recipe Scraped From Web: {new_recipe}")
|
||||
|
||||
if not new_recipe:
|
||||
return "fail" # TODO: Return Better Error Here
|
||||
|
||||
new_recipe = process_recipe_data(new_recipe, url)
|
||||
|
||||
try:
|
||||
img_path = scrape_image(normalize_image_url(new_recipe.get("image")), slug)
|
||||
img_path = scrape_image(
|
||||
normalize_image_url(new_recipe.get("image")), new_recipe.get("slug")
|
||||
)
|
||||
new_recipe["image"] = img_path.name
|
||||
except:
|
||||
new_recipe["image"] = None
|
||||
|
|
|
@ -3,7 +3,18 @@ from pathlib import Path
|
|||
|
||||
import dotenv
|
||||
|
||||
# Helpful Globas
|
||||
CWD = Path(__file__).parent
|
||||
DATA_DIR = CWD.joinpath("data")
|
||||
IMG_DIR = DATA_DIR.joinpath("img")
|
||||
BACKUP_DIR = DATA_DIR.joinpath("backups")
|
||||
DEBUG_DIR = DATA_DIR.joinpath("debug")
|
||||
MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||
TEMP_DIR = DATA_DIR.joinpath("temp")
|
||||
|
||||
|
||||
# Env Variables
|
||||
ENV = CWD.joinpath(".env")
|
||||
dotenv.load_dotenv(ENV)
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"@context": "http:\/\/schema.org",
|
||||
"@type": "Recipe",
|
||||
"name": "Air Fryer Shrimp",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Anna"
|
||||
},
|
||||
"description": "These Air Fryer Shrimp are plump, juicy and perfectly seasoned! This healthy dish is ready in just 8 minutes and requires pantry staples to make it.",
|
||||
"datePublished": "2020-07-13T16:48:25+00:00",
|
||||
"image": "https:\/\/www.crunchycreamysweet.com\/wp-content\/uploads\/2020\/07\/air-fryer-shrimp-A-480x270.jpg",
|
||||
"recipeYield": 4,
|
||||
"prepTime": "PT0H15M",
|
||||
"cookTime": "PT0H8M",
|
||||
"totalTime": "PT0H23M",
|
||||
"recipeIngredient": [
|
||||
"1 lb shrimp",
|
||||
"2 teaspoons olive oil",
|
||||
"\u00bd teaspoon garlic powder",
|
||||
"\u00bc teaspoon paprika",
|
||||
"\u00bd teaspoon Italian seasoning",
|
||||
"\u00bd teaspoon salt",
|
||||
"\u00bc teaspoon black pepper"
|
||||
],
|
||||
"recipeInstructions": [
|
||||
"Cleaning the shrimp by removing shells and veins. Run under tap water, then pat dry with paper towel.",
|
||||
"Mix oil with seasoning in a small bowl.",
|
||||
"Brush shrimp with seasoning mixture on both sides.",
|
||||
"Arrange shrimp in air fryer basket or rack, in a single layer.",
|
||||
"Cook at 400 degrees F for 8 minutes (no need to turn them).",
|
||||
"Serve."
|
||||
],
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "5",
|
||||
"ratingCount": "4"
|
||||
},
|
||||
"recipeCategory": "Main Course",
|
||||
"recipeCuisine": [
|
||||
"American"
|
||||
],
|
||||
"keywords": "air fryer shrimp",
|
||||
"nutrition": {
|
||||
"@type": "NutritionInformation",
|
||||
"calories": "134 kcal",
|
||||
"carbohydrateContent": "1 g",
|
||||
"proteinContent": "23 g",
|
||||
"fatContent": "4 g",
|
||||
"saturatedFatContent": "1 g",
|
||||
"cholesterolContent": "286 mg",
|
||||
"sodiumContent": "1172 mg",
|
||||
"fiberContent": "1 g",
|
||||
"sugarContent": "1 g",
|
||||
"servingSize": "1 serving"
|
||||
},
|
||||
"@id": "https:\/\/www.crunchycreamysweet.com\/air-fryer-shrimp\/#recipe",
|
||||
"isPartOf": {
|
||||
"@id": "https:\/\/www.crunchycreamysweet.com\/air-fryer-shrimp\/#article"
|
||||
},
|
||||
"mainEntityOfPage": "https:\/\/www.crunchycreamysweet.com\/air-fryer-shrimp\/#webpage",
|
||||
"tool": [],
|
||||
"url": "https:\/\/www.crunchycreamysweet.com\/air-fryer-shrimp\/"
|
||||
}
|
BIN
mealie/test/data/nextcloud_recipes/Air Fryer Shrimp/thumb.jpg
Normal file
BIN
mealie/test/data/nextcloud_recipes/Air Fryer Shrimp/thumb.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
|
@ -0,0 +1,259 @@
|
|||
{
|
||||
"@context": "http:\/\/schema.org",
|
||||
"@type": "Recipe",
|
||||
"mainEntityOfPage": "https:\/\/www.allrecipes.com\/recipe\/8975\/chicken-parmigiana\/",
|
||||
"name": "Chicken Parmigiana",
|
||||
"image": "https:\/\/imagesvc.meredithcorp.io\/v3\/mm\/image?url=https%3A%2F%2Fimages.media-allrecipes.com%2Fuserphotos%2F10037.jpg",
|
||||
"datePublished": "1999-04-27T12:40:19.000Z",
|
||||
"description": "This is a very nice dinner for two. Serve it with your favorite pasta and tossed greens.",
|
||||
"prepTime": "PT0H30M",
|
||||
"cookTime": "PT1H0M",
|
||||
"totalTime": "PT1H30M",
|
||||
"recipeYield": 2,
|
||||
"recipeIngredient": [
|
||||
"1 egg, beaten",
|
||||
"2 ounces dry bread crumbs",
|
||||
"2 skinless, boneless chicken breast halves",
|
||||
"\u00be (16 ounce) jar spaghetti sauce",
|
||||
"2 ounces shredded mozzarella cheese",
|
||||
"\u00bc cup grated Parmesan cheese"
|
||||
],
|
||||
"recipeInstructions": [
|
||||
"Preheat oven to 350 degrees F (175 degrees C). Lightly grease a medium baking sheet.\n",
|
||||
"Pour egg into a small shallow bowl. Place bread crumbs in a separate shallow bowl. Dip chicken into egg, then into the bread crumbs. Place coated chicken on the prepared baking sheet and bake in the preheated oven for 40 minutes, or until no longer pink and juices run clear.\n",
|
||||
"Pour 1\/2 of the spaghetti sauce into a 7x11 inch baking dish. Place chicken over sauce, and cover with remaining sauce. Sprinkle mozzarella and Parmesan cheeses on top and return to the preheated oven for 20 minutes.\n"
|
||||
],
|
||||
"recipeCategory": "World Cuisine Recipes",
|
||||
"recipeCuisine": [],
|
||||
"author": [
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "Candy"
|
||||
}
|
||||
],
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": 4.580034423407917,
|
||||
"ratingCount": 1743,
|
||||
"itemReviewed": "Chicken Parmigiana",
|
||||
"bestRating": "5",
|
||||
"worstRating": "1"
|
||||
},
|
||||
"nutrition": {
|
||||
"@type": "NutritionInformation",
|
||||
"calories": "528.3 calories",
|
||||
"carbohydrateContent": "44.9 g",
|
||||
"cholesterolContent": "184.1 mg",
|
||||
"fatContent": "18.3 g",
|
||||
"fiberContent": "5.6 g",
|
||||
"proteinContent": "43.5 g",
|
||||
"saturatedFatContent": "7.6 g",
|
||||
"servingSize": null,
|
||||
"sodiumContent": "1309.5 mg",
|
||||
"sugarContent": "17.2 g",
|
||||
"transFatContent": null,
|
||||
"unsaturatedFatContent": null
|
||||
},
|
||||
"review": [
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2004-02-10T10:18:54.927Z",
|
||||
"reviewBody": "This is a DELICIOUS basic recipe. I have been doing a similar one for years. I also, prefer adding a few more spices TO THE BREAD CRUMBS,like basil, oregano, garlic powder, salt, fresh cracked pepper and onion powder, and a few TBSP of the parmensan cheese;not only ON IT later. For some reason these spices (added separately) are good, but we don't like with an pre-mix of \"Italian\"spice. It seems to taste a little \"soapy\". Not sure which spice does that to it.? Some suggested to \"double dip\" in bread crumbs;if you do, you should really LIKE a heavy battering. It was too thick for our tastes(esp. since you bake in the sauce; to me,the bottom gets a little mushy, and it just adds extra fat and calories). I also use a cookie cooling \"RACK\" SET ON TOP of a baking sheet, to bake the chicken on instead of just on the cookie sheet pan. It comes out much crisper; letting air get to the BOTTOM of the chicken,also. Also,I wait to spoon the SECOND 1\/2 of the sauce UNTIL SERVING, the chicken will stay crisper,(even with the cheese on top). Obviously, we like the chicken on the crisp side (but we don't want to deep fry).\r\nFor company, put the chicken (with just the cheese baked on top) ON TOP of a small mound of spaghetti and sauce,or any pasta; It makes for a delicious looking presentation. A side salad with some sort of CREAMY dressing seems to compliment the red sauce, and completes the meal wonderfully. We get cravings for this one about 2c a month!",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 4
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "somethingdifferentagain?!",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/342976\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2004-01-23T16:37:02.013Z",
|
||||
"reviewBody": "This was an extremely easy, very tasty recipe. As many others suggested, I only put sauce on the bottom of the chicken and then spooned a little over the top when serving. I think the recipe could be improved, though, by (1) pounding the chicken to a uniform thickness and (2) by spicing up the bread crumbs. I used Italian bread crumbs but next time will sprinkle pepper on the chicken before dredging through the crumbs, and I also plan to add more Italian seasoning and maybe a little parmesan to the crumbs. Both these steps, in my opinion, would take this from a really good recipe to an excellent dish!",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 4
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "JBAGNALL",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/642772\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2005-11-19T20:22:40.53Z",
|
||||
"reviewBody": "I BRINED my chicken in 4 cups water , 1\/2 cup kosher salt (1\/4 table salt) \u00bd cup sugar for 30 minutes. No need to brine if you are using quick frozen chicken that has been enhanced. Kosher chicken is prebrined. Brining=juicy chicken. Took brined chicken, cut off thin edges, pounded out & shook chicken w\/flour (preflouring allows bread crumbs to stick) in a Ziploc-letting floured chicken sit for 5 min. I heated 6 TBS vegetable oil till it shimmered & then added 2 TBS butter to my pan, reserving half of this mixture for my second batch. Bread crumb mixture: I use \u00bd cup seasoned bread crumbs(same as 2 ounces), \u00bd cup grated parmesan( double what recipe calls for), 1tsp. Mrs. Dash Garlic and Herb, \u00bd tsp. garlic powder, \u00bd tsp, onion powder, \u00bd tsp. Italian seasoning & a pinch of pepper. Took pre-floured chicken, coated it with egg mixture, then dipped in bread crumbs & FRIED the chicken to a medium golden brown. Shook some parmesan on them right away when done frying to absorb any oil. Side-by side I plated plain spaghetti noodles & cutlets, w\/2 TBSP sauce on cutlet & desired amount of sauce on pasta, covered in cheese & baked each individual plate till cheese melted, serving them straight out of the oven. \r\nThe reviews on this were probably the best I have ever gotten, I used to work in an Italian Restaurant owned by NY Italians & have picked up some techniques. My Fettuccine Alfredo used to be my husband favorite dish, after last night he told me he has a new favorite. \r\n",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 5
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "KC MARTEL",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/526291\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2003-10-22T15:32:26.607Z",
|
||||
"reviewBody": "After several Chicken Parm recipes THIS is THE ONE:-) I've finally found one that we all love! It's simple and it's darned good:-) I will definately make this recipe again and again; thanks so much:-)",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 5
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "STARCHILD1166",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/736533\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2003-11-14T16:55:26.39Z",
|
||||
"reviewBody": "This chicken was so easy to make and turned out excellent! Used Best Marinara Sauce Yet (found here as well)instead of regular spaghetti sauce. This added even more flavor.",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 5
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Alison",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/516223\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2003-01-23T04:38:19.873Z",
|
||||
"reviewBody": "I REALLY liked this recipe. I made my own spaghetti sauce and used parmesan reggiano. I also skipped dipping the breasts in egg as I thought it was unnecessary and it was. Cooking temp. and time are accurate. Even my fussy fiance liked this. I'll definitely make this again.",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 4
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "CSANDST1",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/115553\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2003-08-05T20:26:00.81Z",
|
||||
"reviewBody": "Wow! This was really tasty and simple. Something quick to make when you can't spend too much time figuring out what's for dinner. Also great on a toasted roll\/hero as a sandwich. I varied the recipe a little by adding some parmesan cheese (big cheese lover that I am!), garlic powder, onion powder and some salt into the bread crumbs and then mixing it up before breading the chicken with it. Also added a little salt to the beaten egg to make sure the chicken wouldn't end up bland, but that's just my preference. In response to the one reviewer who wanted thicker breading, what I did was double dip the chicken - coat first with the bread crumbs, then dip into the beaten egg and re-coat with breadcrumbs before actually baking (this would require some more breadcrumbs and probably another egg). Excellent recipe! =]",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 5
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "LIZCHAO74",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/511187\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2003-07-23T07:53:37.18Z",
|
||||
"reviewBody": "Wonderful chicken recipe! I have made this several times. One night we were craving it and I didn't have any bottled spaghetti sauce. I poured a 14 ounce can of tomato sauce in a microwave bowl added 2t Italian Seasoning and 1t of garlic powder cooked on high for 6 minutes and ended up with a rich thick sauce for the chicken.",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 4
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "MAGGIE MCGUIRE",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/392086\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2008-06-10T21:54:38.893Z",
|
||||
"reviewBody": "This is gonna be one of those it\u2019s a good recipe when you completely change it reviews. I did originally follow the recipe and the chicken tasted like it had been in breaded in cardboard. It just was not appetizing. However there is a great breaded chicken recipe on this site, garlic chicken. Made this simple and easy and oh so TASTY. I got great reviews. Here is what I did. Took \u00bc cup olive oil with 3 cloves garlic crushed and heated in microwave for 30 sec. Then coated the chicken in the oil and dipped in a mixture of \u00bd Italian seasoned bread crumbs and \u00bd parmesan cheese (double coat if u like thick breading). Cooked in oven at 325 for 20min (on a foil covered cookie sheet to make clean up easy). Set them in a casserole dish on top of about \u00bd a jar of spaghetti sauce for 3 chicken breast. Covered the breast with slices of mozzarella cheese and baked for another 20-25 minutes. Top with parmesan cheese. This turned out really really yummy and smells sooo good while it\u2019s cooking. ",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 4
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "ANGEL.9",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/218599\/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Review",
|
||||
"datePublished": "2006-02-02T19:05:24.607Z",
|
||||
"reviewBody": "Check out \"Tomato Chicken Parmesan\" on this site for a truly fabulous chicken parm recipe. Every time I make that one people say its the best chicken parm they every had. No matter what kind you make though always pound your chicken breasts it will help immensely keeping the chicken tender and moist.",
|
||||
"reviewRating": {
|
||||
"@type": "Rating",
|
||||
"worstRating": "1",
|
||||
"bestRating": "5",
|
||||
"ratingValue": 3
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "MomSavedbyGrace",
|
||||
"image": null,
|
||||
"sameAs": "https:\/\/www.allrecipes.com\/cook\/1366670\/"
|
||||
}
|
||||
}
|
||||
],
|
||||
"video": {
|
||||
"@context": "http:\/\/schema.org",
|
||||
"@type": "VideoObject",
|
||||
"name": "Chicken Parmigiana",
|
||||
"description": "Make this quick and easy version of chicken Parmigiana.",
|
||||
"uploadDate": "2012-05-23T22:01:40.476Z",
|
||||
"duration": "PT2M18.43S",
|
||||
"thumbnailUrl": "https:\/\/imagesvc.meredithcorp.io\/v3\/mm\/image?url=https%3A%2F%2Fcf-images.us-east-1.prod.boltdns.net%2Fv1%2Fstatic%2F1033249144001%2F15c9e37d-979a-4c2c-a35d-fc3f436b0047%2F6b7f7749-9989-4707-971e-8578e60c0670%2F160x90%2Fmatch%2Fimage.jpg",
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Allrecipes",
|
||||
"url": "https:\/\/www.allrecipes.com",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https:\/\/www.allrecipes.com\/img\/logo.png",
|
||||
"width": 209,
|
||||
"height": 60
|
||||
},
|
||||
"sameAs": [
|
||||
"https:\/\/www.facebook.com\/allrecipes",
|
||||
"https:\/\/twitter.com\/Allrecipes",
|
||||
"https:\/\/www.pinterest.com\/allrecipes\/",
|
||||
"https:\/\/www.instagram.com\/allrecipes\/"
|
||||
]
|
||||
},
|
||||
"embedUrl": "https:\/\/players.brightcove.net\/1033249144001\/default_default\/index.html?videoId=1653498713001"
|
||||
},
|
||||
"keywords": "",
|
||||
"tool": [],
|
||||
"url": "https:\/\/www.allrecipes.com\/recipe\/8975\/chicken-parmigiana\/"
|
||||
}
|
BIN
mealie/test/data/nextcloud_recipes/Chicken Parmigiana/thumb.jpg
Normal file
BIN
mealie/test/data/nextcloud_recipes/Chicken Parmigiana/thumb.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"@context": "http:\/\/schema.org",
|
||||
"@type": "Recipe",
|
||||
"name": "Skillet Shepherd's Pie",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Joanna Cismaru"
|
||||
},
|
||||
"description": "This Skillet Shepherd's Pie recipe, also known as cottage pie, is loaded with flavorful beef and veggies, topped with fluffy and creamy mashed potatoes, then baked to perfection!",
|
||||
"datePublished": "2019-03-16T20:15:47+00:00",
|
||||
"image": "https:\/\/www.jocooks.com\/wp-content\/uploads\/2016\/12\/skillet-shepherds-pie-1-2-480x270.jpg",
|
||||
"video": {
|
||||
"name": "Skillet Shepherd's Pie",
|
||||
"description": "This skillet shepherd\u2019s pie is loaded with flavorful beef and veggies then topped with fluffy and creamy mashed potatoes, then baked to perfection!",
|
||||
"thumbnailUrl": "https:\/\/content.jwplatform.com\/thumbs\/HGr48vds-720.jpg",
|
||||
"contentUrl": "https:\/\/content.jwplatform.com\/videos\/HGr48vds.mp4",
|
||||
"uploadDate": "2018-03-08T16:13:05.000Z",
|
||||
"@type": "VideoObject"
|
||||
},
|
||||
"recipeYield": 1,
|
||||
"prepTime": "PT0H15M",
|
||||
"cookTime": "PT1H10M",
|
||||
"totalTime": "PT1H25M",
|
||||
"recipeIngredient": [
|
||||
"1 tbsp olive oil",
|
||||
"1 1\/4 lb ground beef (lean)",
|
||||
"1\/2 tsp salt (or to taste)",
|
||||
"1\/2 tsp pepper (or to taste)",
|
||||
"1 large onion (chopped)",
|
||||
"1 clove garlic (minced)",
|
||||
"1\/2 tsp red pepper flakes",
|
||||
"2 tbsp Worcestershire sauce",
|
||||
"1.9 oz onion soup mix (I used Knorr, 55g pkg)",
|
||||
"1 cup beef broth (low sodium)",
|
||||
"2 cups frozen veggies (I used mix of peas, carrots, green beans and corn)",
|
||||
"6 large potatoes (peeled and cut into cubes)",
|
||||
"4 tbsp butter (softened)",
|
||||
"2\/3 cup milk",
|
||||
"1\/4 cup Parmesan cheese",
|
||||
"1\/2 tsp salt (or to taste)",
|
||||
"1\/2 tsp white pepper (or to taste)",
|
||||
"1 tbsp parsley (fresh, for garnish)"
|
||||
],
|
||||
"recipeInstructions": [
|
||||
"Boil the potatoes: Start by first cooking the potatoes in boiling water for about 15 minutes or until fork tender. While the potatoes are cooking, you can prepare the meat mixture.",
|
||||
"Prepare the meat mixture: Heat the oil in a large skillet over medium heat. Add the ground beef to the skillet, season it with the salt and pepper and cook it for abut 5 minutes or until it's no longer pink, breaking it up as you go along.",
|
||||
"Add the onion and garlic and cook for 3 more minutes until the onion softens and becomes translucent. Add the pepper flakes, Worcestershire sauce, onion soup mix, beef broth and stir. Stir in the frozen veggies and cook for a couple more minutes. Set aside.",
|
||||
"Preheat the oven 350 F degrees.",
|
||||
"Prepare the mashed potatoes: Drain the potatoes then add them to a large bowl. Add in the butter and using a potato masher, mash until smooth. Add the milk, Parmesan cheese, salt pepper and mash a bit a more until smooth.",
|
||||
"Finish assembling the shepherd's pie: Spread the potatoes over the meat and smooth with a spoon. Take a fork and rough up the top a bit and garnish with a bit of parsley.",
|
||||
"Bake: Place the skillet on a baking sheet, then place it in the oven and bake for 40 minutes until golden brown on top.",
|
||||
"Garnish with more parsley and pepper and serve warm."
|
||||
],
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.48",
|
||||
"ratingCount": "505"
|
||||
},
|
||||
"recipeCategory": "Main Course",
|
||||
"recipeCuisine": [
|
||||
"American"
|
||||
],
|
||||
"keywords": "cottage pie,shepherd's pie,skillet shepherd's pie",
|
||||
"nutrition": {
|
||||
"@type": "NutritionInformation",
|
||||
"calories": "252 kcal",
|
||||
"carbohydrateContent": "14 g",
|
||||
"proteinContent": "19 g",
|
||||
"fatContent": "12 g",
|
||||
"saturatedFatContent": "6 g",
|
||||
"cholesterolContent": "63 mg",
|
||||
"sodiumContent": "1165 mg",
|
||||
"fiberContent": "2 g",
|
||||
"sugarContent": "2 g",
|
||||
"servingSize": "1 serving"
|
||||
},
|
||||
"@id": "https:\/\/www.jocooks.com\/recipes\/skillet-shepherds-pie\/#recipe",
|
||||
"isPartOf": {
|
||||
"@id": "https:\/\/www.jocooks.com\/recipes\/skillet-shepherds-pie\/#article"
|
||||
},
|
||||
"mainEntityOfPage": "https:\/\/www.jocooks.com\/recipes\/skillet-shepherds-pie\/#webpage",
|
||||
"url": "https:\/\/www.jocooks.com\/recipes\/skillet-shepherds-pie\/",
|
||||
"id": "4485",
|
||||
"dateCreated": "0",
|
||||
"dateModified": "1607461134",
|
||||
"printImage": "false",
|
||||
"imageUrl": "\/nextcloud\/index.php\/apps\/cookbook\/recipes\/4485\/image?size=full",
|
||||
"tool": []
|
||||
}
|
BIN
mealie/test/data/nextcloud_recipes/nextcloud.zip
Normal file
BIN
mealie/test/data/nextcloud_recipes/nextcloud.zip
Normal file
Binary file not shown.
43
mealie/test/test_nextcloud.py
Normal file
43
mealie/test/test_nextcloud.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from services.image_services import IMG_DIR
|
||||
from services.migrations.nextcloud import (
|
||||
cleanup,
|
||||
import_recipes,
|
||||
prep,
|
||||
process_selection,
|
||||
)
|
||||
from services.recipe_services import Recipe
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
NEXTCLOUD_DIR = CWD.joinpath("data", "nextcloud_recipes")
|
||||
TEMP_NEXTCLOUD = CWD.parent.joinpath("data", "temp", "nextcloud")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_name,final_path",
|
||||
[("nextcloud.zip", TEMP_NEXTCLOUD)],
|
||||
)
|
||||
def test_zip_extraction(file_name: str, final_path: Path):
|
||||
prep()
|
||||
zip = NEXTCLOUD_DIR.joinpath(file_name)
|
||||
dir = process_selection(zip)
|
||||
|
||||
assert dir == final_path
|
||||
cleanup()
|
||||
assert dir.exists() == False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recipe_dir",
|
||||
[
|
||||
NEXTCLOUD_DIR.joinpath("Air Fryer Shrimp"),
|
||||
NEXTCLOUD_DIR.joinpath("Chicken Parmigiana"),
|
||||
NEXTCLOUD_DIR.joinpath("Skillet Shepherd's Pie"),
|
||||
],
|
||||
)
|
||||
def test_nextcloud_migration(recipe_dir: Path):
|
||||
recipe = import_recipes(recipe_dir)
|
||||
assert type(recipe) == Recipe
|
||||
IMG_DIR.joinpath(recipe.image).unlink(missing_ok=True)
|
Loading…
Reference in a new issue