mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-04 21:15:22 +02:00
Feature/improve error message on scrape (#476)
* add better feedback on failed scrape * fix json download link * add better recipe parser * dump deps * fix force open on mobile * formatting * rewrite scraper to use new library * fix failing tests * bookmarklet support * bookmarklet instructions * recipes changelog Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
3702331630
commit
a78fbea711
22 changed files with 658 additions and 15582 deletions
15465
frontend/package-lock.json
generated
15465
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -22,7 +22,7 @@
|
|||
"vue-i18n": "^8.24.1",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.4.6",
|
||||
"vuetify": "^2.5.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ const recipeURLs = {
|
|||
allRecipesByCategory: prefix + "category",
|
||||
create: prefix + "create",
|
||||
createByURL: prefix + "create-url",
|
||||
testParseURL: prefix + "test-scrape-url",
|
||||
recipe: slug => prefix + slug,
|
||||
update: slug => prefix + slug,
|
||||
delete: slug => prefix + slug,
|
||||
|
@ -29,11 +30,8 @@ export const recipeAPI = {
|
|||
* @returns {string} Recipe Slug
|
||||
*/
|
||||
async createByURL(recipeURL) {
|
||||
const response = await apiReq.post(
|
||||
recipeURLs.createByURL,
|
||||
{ url: recipeURL },
|
||||
() => i18n.t("recipe.recipe-creation-failed"),
|
||||
() => i18n.t("recipe.recipe-created")
|
||||
const response = await apiReq.post(recipeURLs.createByURL, { url: recipeURL }, false, () =>
|
||||
i18n.t("recipe.recipe-created")
|
||||
);
|
||||
|
||||
store.dispatch("requestRecentRecipes");
|
||||
|
@ -186,4 +184,9 @@ export const recipeAPI = {
|
|||
const response = await apiReq.delete(API_ROUTES.recipesSlugCommentsId(slug, id));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async testScrapeURL(url) {
|
||||
const response = await apiReq.post(recipeURLs.testParseURL, { url: url });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
<v-dialog v-model="addRecipe" width="650" @click:outside="reset">
|
||||
<v-card :loading="processing">
|
||||
<v-app-bar dark color="primary mb-2">
|
||||
<v-icon large left v-if="!processing">
|
||||
mdi-link
|
||||
</v-icon>
|
||||
<v-icon large left v-if="!processing"> mdi-link </v-icon>
|
||||
<v-progress-circular v-else indeterminate color="white" large class="mr-2"> </v-progress-circular>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
|
@ -28,19 +26,58 @@
|
|||
persistent-hint
|
||||
></v-text-field>
|
||||
|
||||
<v-alert v-if="error" color="red" outlined type="success">
|
||||
{{ $t("new-recipe.error-message") }}
|
||||
</v-alert>
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" color="error" class="mt-6 white--text">
|
||||
<v-card-title class="ma-0 pa-0">
|
||||
<v-icon left color="white" x-large> mdi-robot </v-icon>
|
||||
{{ $t("new-recipe.error-title") }}
|
||||
</v-card-title>
|
||||
<v-divider class="my-3 mx-2"></v-divider>
|
||||
|
||||
<p>
|
||||
{{ $t("new-recipe.error-details") }}
|
||||
</p>
|
||||
<div class="d-flex row justify-space-around my-3 force-white">
|
||||
<a
|
||||
class="dark"
|
||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
Google ld+json Info
|
||||
</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
||||
GitHub Issues
|
||||
</a>
|
||||
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
||||
Recipe Markup Specification
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn
|
||||
white
|
||||
outlined
|
||||
:to="{ path: '/recipes/debugger', query: { test_url: recipeURL } }"
|
||||
@click="addRecipe = false"
|
||||
>
|
||||
<v-icon> mdi-external-link </v-icon>
|
||||
View Scraped Data
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="reset">
|
||||
<v-icon left> mdi-close </v-icon>
|
||||
{{ $t("general.close") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" text type="submit" :loading="processing">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" type="submit" :loading="processing">
|
||||
<v-icon left> {{ $globals.icons.create }} </v-icon>
|
||||
{{ $t("general.submit") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
@ -65,7 +102,6 @@
|
|||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
absolute: {
|
||||
|
@ -77,14 +113,32 @@ export default {
|
|||
error: false,
|
||||
fab: false,
|
||||
addRecipe: false,
|
||||
recipeURL: "",
|
||||
processing: false,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.$route.query.recipe_import_url) {
|
||||
this.addRecipe = true;
|
||||
this.createRecipe();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
recipeURL: {
|
||||
set(recipe_import_url) {
|
||||
this.$router.replace({ query: { ...this.$route.query, recipe_import_url } });
|
||||
},
|
||||
get() {
|
||||
return this.$route.query.recipe_import_url || "";
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async createRecipe() {
|
||||
if (this.$refs.urlForm.validate()) {
|
||||
this.error = false;
|
||||
if (this.$refs.urlForm === undefined || this.$refs.urlForm.validate()) {
|
||||
this.processing = true;
|
||||
const response = await api.recipes.createByURL(this.recipeURL);
|
||||
this.processing = false;
|
||||
|
@ -106,11 +160,20 @@ export default {
|
|||
this.processing = false;
|
||||
},
|
||||
isValidWebUrl(url) {
|
||||
let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
||||
let regEx =
|
||||
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
||||
return regEx.test(url) ? true : "Must be a Valid URL";
|
||||
},
|
||||
|
||||
bookmark() {
|
||||
return `javascript:(function()%7Bvar url %3D document.URL %3B%0Avar mealie %3D "http%3A%2F%2Flocalhost%3A8080%2F%23"%0Avar dest %3D mealie %2B "%2F%3Frecipe_import_url%3D" %2B url%0Awindow.open(dest%2C '_blank')%7D)()%3B`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style>
|
||||
.force-white > a {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
|
||||
<v-list-item dense v-if="isLoggedIn" :to="`/user/${user.id}/favorites`">
|
||||
<v-list-item-icon>
|
||||
<v-icon>
|
||||
mdi-heart
|
||||
</v-icon>
|
||||
<v-icon> mdi-heart </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> Favorites </v-list-item-title>
|
||||
|
@ -30,17 +28,13 @@
|
|||
<v-list nav dense class="fixedBottom" v-if="!isMain">
|
||||
<v-list-item href="https://github.com/sponsors/hay-kot" target="_target">
|
||||
<v-list-item-icon>
|
||||
<v-icon color="pink">
|
||||
mdi-heart
|
||||
</v-icon>
|
||||
<v-icon color="pink"> mdi-heart </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title> {{ $t("about.support") }} </v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item to="/admin/about">
|
||||
<v-list-item-icon class="mr-3 pt-1">
|
||||
<v-icon :color="newVersionAvailable ? 'red--text' : ''">
|
||||
mdi-information
|
||||
</v-icon>
|
||||
<v-icon :color="newVersionAvailable ? 'red--text' : ''"> mdi-information </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
|
@ -86,7 +80,8 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
this.getVersion();
|
||||
this.resetView();
|
||||
|
||||
this.showSidebar = !this.isMobile;
|
||||
},
|
||||
watch: {
|
||||
user() {
|
||||
|
@ -98,7 +93,6 @@ export default {
|
|||
isMain() {
|
||||
const testVal = this.$route.path.split("/");
|
||||
if (testVal[1] === "recipe") this.closeSidebar();
|
||||
else this.resetView();
|
||||
|
||||
return !(testVal[1] === "admin");
|
||||
},
|
||||
|
@ -135,7 +129,7 @@ export default {
|
|||
const pages = this.$store.getters.getCustomPages;
|
||||
if (pages.length > 0) {
|
||||
pages.sort((a, b) => a.position - b.position);
|
||||
return pages.map(x => ({
|
||||
return pages.map((x) => ({
|
||||
title: x.name,
|
||||
to: `/pages/${x.slug}`,
|
||||
icon: this.$globals.icons.pages,
|
||||
|
@ -217,9 +211,7 @@ export default {
|
|||
resetImage() {
|
||||
this.hideImage == false;
|
||||
},
|
||||
resetView() {
|
||||
this.showSidebar = !this.isMobile;
|
||||
},
|
||||
|
||||
toggleSidebar() {
|
||||
this.showSidebar = !this.showSidebar;
|
||||
},
|
||||
|
|
|
@ -179,7 +179,8 @@
|
|||
},
|
||||
"new-recipe": {
|
||||
"bulk-add": "Bulk Add",
|
||||
"error-message": "Looks like there was an error parsing the URL. Check the log and debug/last_recipe.json to see what went wrong.",
|
||||
"error-title": "Looks Like We Couldn't Find Anything",
|
||||
"error-details": "Only websites containing ld+json or microdata can be imported imported by Mealie. Most major recipe websites support this data structure. If your site cannot be imported but there is json data in the log, please submit a github issue with the URL and data.",
|
||||
"from-url": "Import a Recipe",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list",
|
||||
"recipe-url": "Recipe URL",
|
||||
|
@ -251,7 +252,6 @@
|
|||
"total-time": "Total Time",
|
||||
"unable-to-delete-recipe": "Unable to Delete Recipe",
|
||||
"view-recipe": "View Recipe"
|
||||
|
||||
},
|
||||
"search": {
|
||||
"and": "and",
|
||||
|
|
62
frontend/src/pages/Recipe/ScraperDebugger.vue
Normal file
62
frontend/src/pages/Recipe/ScraperDebugger.vue
Normal file
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-text-field v-model="testUrl" outlined single-line label="Recipe Url"> </v-text-field>
|
||||
<div class="d-flex">
|
||||
<v-btn class="mt-0 ml-auto" color="info" @click="getTestData">
|
||||
<v-icon left> mdi-test-tube </v-icon>
|
||||
Test Scrape
|
||||
</v-btn>
|
||||
</div>
|
||||
<VJsoneditor class="mt-2" v-model="recipeJson" height="1500px" :options="jsonEditorOptions" />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
components: {
|
||||
VJsoneditor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
jsonEditorOptions: {
|
||||
mode: "code",
|
||||
search: false,
|
||||
mainMenuBar: false,
|
||||
},
|
||||
recipeJson: {},
|
||||
defaultMessage: { details: "site failed to return valid schema" },
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.query.test_url) {
|
||||
this.getTestData();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
testUrl: {
|
||||
set(test_url) {
|
||||
this.$router.replace({ query: { ...this.$route.query, test_url } });
|
||||
},
|
||||
get() {
|
||||
return this.$route.query.test_url || "";
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getTestData() {
|
||||
const response = await api.recipes.testScrapeURL(this.testUrl).catch(() => {
|
||||
this.recipeJson = this.defaultMessage;
|
||||
});
|
||||
|
||||
if (response.length < 1) {
|
||||
this.recipeJson = this.defaultMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
this.recipeJson = response;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -63,7 +63,7 @@ import RecipeViewer from "@/components/Recipe/RecipeViewer";
|
|||
import PrintView from "@/components/Recipe/PrintView";
|
||||
import RecipeEditor from "@/components/Recipe/RecipeEditor";
|
||||
import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue";
|
||||
import EditorButtonRow from "@/components/Recipe/EditorButtonRow";
|
||||
import EditorButtonRow from "@/components/Recipe/EditorButtonRow.vue";
|
||||
import NoRecipe from "@/components/Fallbacks/NoRecipe";
|
||||
import { user } from "@/mixins/user";
|
||||
import { router } from "@/routes";
|
||||
|
@ -133,7 +133,7 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
$route: function() {
|
||||
$route: function () {
|
||||
this.getRecipeDetails();
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const ViewRecipe = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipe/ViewRecipe");
|
||||
const NewRecipe = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipe/NewRecipe");
|
||||
const ScraperDebugger = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipe/ScraperDebugger");
|
||||
const CustomPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CustomPage");
|
||||
const AllRecipes = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/AllRecipes");
|
||||
const CategoryTagPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CategoryTagPage");
|
||||
|
@ -9,6 +10,7 @@ import { api } from "@/api";
|
|||
export const recipeRoutes = [
|
||||
// Recipes
|
||||
{ path: "/recipes/all", component: AllRecipes },
|
||||
{ path: "/recipes/debugger", component: ScraperDebugger },
|
||||
{ path: "/user/:id/favorites", component: Favorites },
|
||||
{ path: "/recipes/tag/:tag", component: CategoryTagPage },
|
||||
{ path: "/recipes/tag", component: CategoryTagPage },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue