mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	Feature/image minify (#256)
* fix settings * app info cleanup * bottom-bar experiment * remove dup key * type hints * add dependency * updated image with query parameters * read image options * add image minification * add image minification step * alt image routes * add image minification * set mobile bar to top Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		| @@ -1,6 +1,6 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # {{ recipe.name }} | # {{ recipe.name }} | ||||||
| {{ recipe.description }} | {{ recipe.description }} | ||||||
|   | |||||||
| @@ -1,35 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <v-app> |   <v-app> | ||||||
|     <v-app-bar clipped-left dense app color="primary" dark class="d-print-none"> |     <TheAppBar /> | ||||||
|       <router-link v-if="!(isMobile && search)" to="/"> |  | ||||||
|         <v-btn icon> |  | ||||||
|           <v-icon size="40"> mdi-silverware-variant </v-icon> |  | ||||||
|         </v-btn> |  | ||||||
|       </router-link> |  | ||||||
|  |  | ||||||
|       <div v-if="!isMobile" btn class="pl-2"> |  | ||||||
|         <v-toolbar-title style="cursor: pointer" @click="$router.push('/')" |  | ||||||
|           >Mealie |  | ||||||
|         </v-toolbar-title> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <v-spacer></v-spacer> |  | ||||||
|       <v-expand-x-transition> |  | ||||||
|         <SearchBar |  | ||||||
|           ref="mainSearchBar" |  | ||||||
|           v-if="search" |  | ||||||
|           :show-results="true" |  | ||||||
|           @selected="navigateFromSearch" |  | ||||||
|           :max-width="isMobile ? '100%' : '450px'" |  | ||||||
|         /> |  | ||||||
|       </v-expand-x-transition> |  | ||||||
|       <v-btn icon @click="search = !search"> |  | ||||||
|         <v-icon>mdi-magnify</v-icon> |  | ||||||
|       </v-btn> |  | ||||||
|  |  | ||||||
|       <SiteMenu /> |  | ||||||
|       <LanguageMenu /> |  | ||||||
|     </v-app-bar> |  | ||||||
|     <v-main> |     <v-main> | ||||||
|       <v-banner v-if="demo" sticky |       <v-banner v-if="demo" sticky | ||||||
|         ><div class="text-center"> |         ><div class="text-center"> | ||||||
| @@ -47,10 +18,8 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import SiteMenu from "@/components/UI/SiteMenu"; | import TheAppBar from "@/components/UI/TheAppBar"; | ||||||
| import SearchBar from "@/components/UI/Search/SearchBar"; |  | ||||||
| import AddRecipeFab from "@/components/UI/AddRecipeFab"; | import AddRecipeFab from "@/components/UI/AddRecipeFab"; | ||||||
| import LanguageMenu from "@/components/UI/LanguageMenu"; |  | ||||||
| import Vuetify from "./plugins/vuetify"; | import Vuetify from "./plugins/vuetify"; | ||||||
| import { user } from "@/mixins/user"; | import { user } from "@/mixins/user"; | ||||||
|  |  | ||||||
| @@ -58,23 +27,13 @@ export default { | |||||||
|   name: "App", |   name: "App", | ||||||
|  |  | ||||||
|   components: { |   components: { | ||||||
|     SiteMenu, |     TheAppBar, | ||||||
|     AddRecipeFab, |     AddRecipeFab, | ||||||
|     SearchBar, |  | ||||||
|     LanguageMenu, |  | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   mixins: [user], |   mixins: [user], | ||||||
|  |  | ||||||
|   watch: { |  | ||||||
|     $route() { |  | ||||||
|       this.search = false; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   computed: { |   computed: { | ||||||
|     isMobile() { |  | ||||||
|       return this.$vuetify.breakpoint.name === "xs"; |  | ||||||
|     }, |  | ||||||
|     demo() { |     demo() { | ||||||
|       const appInfo = this.$store.getters.getAppInfo; |       const appInfo = this.$store.getters.getAppInfo; | ||||||
|       return appInfo.demoStatus; |       return appInfo.demoStatus; | ||||||
| @@ -102,9 +61,6 @@ export default { | |||||||
|     this.$store.dispatch("requestAppInfo"); |     this.$store.dispatch("requestAppInfo"); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   data: () => ({ |  | ||||||
|     search: false, |  | ||||||
|   }), |  | ||||||
|   methods: { |   methods: { | ||||||
|     // For Later! |     // For Later! | ||||||
|  |  | ||||||
| @@ -126,9 +82,6 @@ export default { | |||||||
|         this.darkModeSystemCheck(); |         this.darkModeSystemCheck(); | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     navigateFromSearch(slug) { |  | ||||||
|       this.$router.push(`/recipe/${slug}`); |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -5,27 +5,27 @@ import { store } from "@/store"; | |||||||
| const prefix = baseURL + "categories"; | const prefix = baseURL + "categories"; | ||||||
|  |  | ||||||
| const categoryURLs = { | const categoryURLs = { | ||||||
|   get_all: `${prefix}`, |   getAll: `${prefix}`, | ||||||
|   get_category: category => `${prefix}/${category}`, |   getCategory: category => `${prefix}/${category}`, | ||||||
|   delete_category: category => `${prefix}/${category}`, |   deleteCategory: category => `${prefix}/${category}`, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const categoryAPI = { | export const categoryAPI = { | ||||||
|   async getAll() { |   async getAll() { | ||||||
|     let response = await apiReq.get(categoryURLs.get_all); |     let response = await apiReq.get(categoryURLs.getAll); | ||||||
|     return response.data; |     return response.data; | ||||||
|   }, |   }, | ||||||
|   async create(name) { |   async create(name) { | ||||||
|     let response = await apiReq.post(categoryURLs.get_all, { name: name }); |     let response = await apiReq.post(categoryURLs.getAll, { name: name }); | ||||||
|     store.dispatch("requestCategories"); |     store.dispatch("requestCategories"); | ||||||
|     return response.data; |     return response.data; | ||||||
|   }, |   }, | ||||||
|   async getRecipesInCategory(category) { |   async getRecipesInCategory(category) { | ||||||
|     let response = await apiReq.get(categoryURLs.get_category(category)); |     let response = await apiReq.get(categoryURLs.getCategory(category)); | ||||||
|     return response.data; |     return response.data; | ||||||
|   }, |   }, | ||||||
|   async delete(category) { |   async delete(category) { | ||||||
|     let response = await apiReq.delete(categoryURLs.delete_category(category)); |     let response = await apiReq.delete(categoryURLs.deleteCategory(category)); | ||||||
|     store.dispatch("requestCategories"); |     store.dispatch("requestCategories"); | ||||||
|     return response.data; |     return response.data; | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -56,9 +56,7 @@ export const recipeAPI = { | |||||||
|     const fd = new FormData(); |     const fd = new FormData(); | ||||||
|     fd.append("image", fileObject); |     fd.append("image", fileObject); | ||||||
|     fd.append("extension", fileObject.name.split(".").pop()); |     fd.append("extension", fileObject.name.split(".").pop()); | ||||||
|  |  | ||||||
|     let response = apiReq.put(recipeURLs.updateImage(recipeSlug), fd); |     let response = apiReq.put(recipeURLs.updateImage(recipeSlug), fd); | ||||||
|  |  | ||||||
|     return response; |     return response; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
| @@ -87,4 +85,16 @@ export const recipeAPI = { | |||||||
|  |  | ||||||
|     return response.data; |     return response.data; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   recipeImage(recipeSlug) { | ||||||
|  |     return `/api/recipes/${recipeSlug}/image?image_type=original`; | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   recipeSmallImage(recipeSlug) { | ||||||
|  |     return `/api/recipes/${recipeSlug}/image?image_type=small`; | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   recipeTinyImage(recipeSlug) { | ||||||
|  |     return `/api/recipes/${recipeSlug}/image?image_type=tiny`; | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -28,8 +28,8 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import utils from "@/utils"; |  | ||||||
| import SearchDialog from "../UI/Search/SearchDialog"; | import SearchDialog from "../UI/Search/SearchDialog"; | ||||||
|  | import { api } from "@/api"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     SearchDialog, |     SearchDialog, | ||||||
| @@ -47,7 +47,7 @@ export default { | |||||||
|   methods: { |   methods: { | ||||||
|     getImage(slug) { |     getImage(slug) { | ||||||
|       if (slug) { |       if (slug) { | ||||||
|         return utils.getImageURL(slug); |         return api.recipes.recipeSmallImage(slug); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     setSlug(name, slug) { |     setSlug(name, slug) { | ||||||
|   | |||||||
| @@ -223,7 +223,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       return utils.getImageURL(image); |       return api.recipes.recipeSmallImage(image); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     formatDate(date) { |     formatDate(date) { | ||||||
|   | |||||||
| @@ -1,5 +1,10 @@ | |||||||
| <template> | <template> | ||||||
|   <v-card hover :to="`/recipe/${slug}`" max-height="125"> |   <v-card | ||||||
|  |     hover | ||||||
|  |     :to="`/recipe/${slug}`" | ||||||
|  |     max-height="125" | ||||||
|  |     @click="$emit('selected')" | ||||||
|  |   > | ||||||
|     <v-list-item> |     <v-list-item> | ||||||
|       <v-list-item-avatar rounded size="125" class="mt-0 ml-n4"> |       <v-list-item-avatar rounded size="125" class="mt-0 ml-n4"> | ||||||
|         <v-img :src="getImage(image)"> </v-img> |         <v-img :src="getImage(image)"> </v-img> | ||||||
| @@ -20,7 +25,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import utils from "@/utils"; | import { api } from "@/api"; | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|     name: String, |     name: String, | ||||||
| @@ -35,7 +40,7 @@ export default { | |||||||
|  |  | ||||||
|   methods: { |   methods: { | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       return utils.getImageURL(image); |       return api.recipes.recipeSmallImage(image); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import utils from "@/utils"; | import { api } from "@/api"; | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|     name: String, |     name: String, | ||||||
| @@ -57,7 +57,7 @@ export default { | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       return utils.getImageURL(image); |       return api.recipes.recipeSmallImage(image); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -162,6 +162,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import utils from "@/utils"; | import utils from "@/utils"; | ||||||
|  | import { api } from "@/api"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
| @@ -175,7 +176,7 @@ export default { | |||||||
|   methods: { |   methods: { | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       if (image) { |       if (image) { | ||||||
|         return utils.getImageURL(image) + "?rnd=" + this.imageKey; |         return api.recipes.recipeImage(image) + "?rnd=" + this.imageKey; | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     generateKey(item, index) { |     generateKey(item, index) { | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import Fuse from "fuse.js"; | import Fuse from "fuse.js"; | ||||||
| import utils from "@/utils"; | import { api } from "@/api"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
| @@ -151,7 +151,7 @@ export default { | |||||||
|       ); |       ); | ||||||
|     }, |     }, | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       return utils.getImageURL(image); |       return api.recipes.recipeTinyImage(image); | ||||||
|     }, |     }, | ||||||
|     selected(slug, name) { |     selected(slug, name) { | ||||||
|       this.$emit("selected", slug, name); |       this.$emit("selected", slug, name); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="text-center "> |   <div class="text-center "> | ||||||
|     <v-dialog v-model="dialog" class="search-dialog" width="600px" height="0"> |     <v-dialog v-model="dialog" width="600px" height="0" :fullscreen="isMobile"> | ||||||
|       <v-card> |       <v-card> | ||||||
|         <v-app-bar dark color="primary"> |         <v-app-bar dark color="primary"> | ||||||
|           <v-toolbar-title class="headline">Search a Recipe</v-toolbar-title> |           <v-toolbar-title class="headline">Search a Recipe</v-toolbar-title> | ||||||
| @@ -9,13 +9,27 @@ | |||||||
|           <SearchBar |           <SearchBar | ||||||
|             @results="updateResults" |             @results="updateResults" | ||||||
|             @selected="emitSelect" |             @selected="emitSelect" | ||||||
|             :show-results="true" |             :show-results="!isMobile" | ||||||
|             max-width="550px" |             max-width="550px" | ||||||
|             :dense="false" |             :dense="false" | ||||||
|             :nav-on-click="false" |             :nav-on-click="false" | ||||||
|             :reset-search="dialog" |             :reset-search="dialog" | ||||||
|             :solo="false" |             :solo="false" | ||||||
|           /> |           /> | ||||||
|  |           <div v-if="isMobile"> | ||||||
|  |             <div v-for="recipe in searchResults.slice(0, 7)" :key="recipe.name"> | ||||||
|  |               <MobileRecipeCard | ||||||
|  |                 class="ma-1 px-0" | ||||||
|  |                 :name="recipe.item.name" | ||||||
|  |                 :description="recipe.item.description" | ||||||
|  |                 :slug="recipe.item.slug" | ||||||
|  |                 :rating="recipe.item.rating" | ||||||
|  |                 :image="recipe.item.image" | ||||||
|  |                 :route="true" | ||||||
|  |                 @selected="dialog = false" | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|         </v-card-text> |         </v-card-text> | ||||||
|       </v-card> |       </v-card> | ||||||
|     </v-dialog> |     </v-dialog> | ||||||
| @@ -24,16 +38,32 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import SearchBar from "./SearchBar"; | import SearchBar from "./SearchBar"; | ||||||
|  | import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     SearchBar, |     SearchBar, | ||||||
|  |     MobileRecipeCard, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       searchResults: null, |       searchResults: [], | ||||||
|       dialog: false, |       dialog: false, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |   computed: { | ||||||
|  |     isMobile() { | ||||||
|  |       return this.$vuetify.breakpoint.name === "xs"; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     "$route.hash"(newHash, oldHash) { | ||||||
|  |       if (newHash === "#mobile-search") { | ||||||
|  |         this.dialog = true; | ||||||
|  |       } else if (oldHash === "#mobile-search") { | ||||||
|  |         this.dialog = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     updateResults(results) { |     updateResults(results) { | ||||||
|       this.searchResults = results; |       this.searchResults = results; | ||||||
| @@ -44,15 +74,22 @@ export default { | |||||||
|     }, |     }, | ||||||
|     open() { |     open() { | ||||||
|       this.dialog = true; |       this.dialog = true; | ||||||
|  |       this.$router.push("#mobile-search"); | ||||||
|  |     }, | ||||||
|  |     toggleDialog(open) { | ||||||
|  |       if (open) { | ||||||
|  |         this.$router.push("#mobile-search"); | ||||||
|  |       } else { | ||||||
|  |         this.$router.back(); // 😎 back button click | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scope> | <style scope> | ||||||
| .search-dialog { | .mobile-dialog { | ||||||
|   margin-top: 10%; |  | ||||||
|   align-items: flex-start; |   align-items: flex-start; | ||||||
|   justify-content: center; |   justify-content: flex-start; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
							
								
								
									
										114
									
								
								frontend/src/components/UI/TheAppBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								frontend/src/components/UI/TheAppBar.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <v-app-bar | ||||||
|  |       v-if="!isMobile" | ||||||
|  |       clipped-left | ||||||
|  |       dense | ||||||
|  |       app | ||||||
|  |       color="primary" | ||||||
|  |       dark | ||||||
|  |       class="d-print-none" | ||||||
|  |     > | ||||||
|  |       <router-link v-if="!(isMobile && search)" to="/"> | ||||||
|  |         <v-btn icon> | ||||||
|  |           <v-icon size="40"> mdi-silverware-variant </v-icon> | ||||||
|  |         </v-btn> | ||||||
|  |       </router-link> | ||||||
|  |  | ||||||
|  |       <div v-if="!isMobile" btn class="pl-2"> | ||||||
|  |         <v-toolbar-title style="cursor: pointer" @click="$router.push('/')" | ||||||
|  |           >Mealie | ||||||
|  |         </v-toolbar-title> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <v-spacer></v-spacer> | ||||||
|  |       <v-expand-x-transition> | ||||||
|  |         <SearchBar | ||||||
|  |           ref="mainSearchBar" | ||||||
|  |           v-if="search" | ||||||
|  |           :show-results="true" | ||||||
|  |           @selected="navigateFromSearch" | ||||||
|  |           :max-width="isMobile ? '100%' : '450px'" | ||||||
|  |         /> | ||||||
|  |       </v-expand-x-transition> | ||||||
|  |       <v-btn icon @click="search = !search"> | ||||||
|  |         <v-icon>mdi-magnify</v-icon> | ||||||
|  |       </v-btn> | ||||||
|  |  | ||||||
|  |       <SiteMenu /> | ||||||
|  |     </v-app-bar> | ||||||
|  |     <v-app-bar | ||||||
|  |       v-else | ||||||
|  |       bottom | ||||||
|  |       clipped-left | ||||||
|  |       dense | ||||||
|  |       app | ||||||
|  |       color="primary" | ||||||
|  |       dark | ||||||
|  |       class="d-print-none" | ||||||
|  |     > | ||||||
|  |       <router-link to="/"> | ||||||
|  |         <v-btn icon> | ||||||
|  |           <v-icon size="40"> mdi-silverware-variant </v-icon> | ||||||
|  |         </v-btn> | ||||||
|  |       </router-link> | ||||||
|  |  | ||||||
|  |       <div v-if="!isMobile" btn class="pl-2"> | ||||||
|  |         <v-toolbar-title style="cursor: pointer" @click="$router.push('/')" | ||||||
|  |           >Mealie | ||||||
|  |         </v-toolbar-title> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <v-spacer></v-spacer> | ||||||
|  |       <v-expand-x-transition> | ||||||
|  |         <SearchDialog ref="mainSearchDialog" /> | ||||||
|  |       </v-expand-x-transition> | ||||||
|  |       <v-btn icon @click="$refs.mainSearchDialog.open()"> | ||||||
|  |         <v-icon>mdi-magnify</v-icon> | ||||||
|  |       </v-btn> | ||||||
|  |  | ||||||
|  |       <SiteMenu /> | ||||||
|  |     </v-app-bar> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import SiteMenu from "@/components/UI/SiteMenu"; | ||||||
|  | import SearchBar from "@/components/UI/Search/SearchBar"; | ||||||
|  | import SearchDialog from "@/components/UI/Search/SearchDialog"; | ||||||
|  | import { user } from "@/mixins/user"; | ||||||
|  | export default { | ||||||
|  |   name: "AppBar", | ||||||
|  |  | ||||||
|  |   mixins: [user], | ||||||
|  |   components: { | ||||||
|  |     SiteMenu, | ||||||
|  |     SearchBar, | ||||||
|  |     SearchDialog, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       search: false, | ||||||
|  |       isMobile: false, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     $route() { | ||||||
|  |       this.search = false; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     // isMobile() { | ||||||
|  |     //   return this.$vuetify.breakpoint.name === "xs"; | ||||||
|  |     // }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     navigateFromSearch(slug) { | ||||||
|  |       this.$router.push(`/recipe/${slug}`); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | </style> | ||||||
							
								
								
									
										7
									
								
								frontend/src/mixins/utilMixins.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/mixins/utilMixins.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | export const utilMixins = { | ||||||
|  |   commputed: { | ||||||
|  |     isMobile() { | ||||||
|  |       return this.$vuetify.breakpoint.name === "xs"; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
| @@ -117,7 +117,7 @@ export default { | |||||||
|       return utils.getDateAsTextAlt(dateObject); |       return utils.getDateAsTextAlt(dateObject); | ||||||
|     }, |     }, | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       return utils.getImageURL(image); |       return api.recipes.recipeTinyImage(image); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     editPlan(id) { |     editPlan(id) { | ||||||
|   | |||||||
| @@ -52,7 +52,6 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import utils from "@/utils"; |  | ||||||
| export default { | export default { | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
| @@ -68,7 +67,7 @@ export default { | |||||||
|       else return 0; |       else return 0; | ||||||
|     }, |     }, | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       return utils.getImageURL(image); |       return api.recipes.recipeImage(image); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ | |||||||
|     <v-card v-else id="myRecipe"> |     <v-card v-else id="myRecipe"> | ||||||
|       <v-img |       <v-img | ||||||
|         height="400" |         height="400" | ||||||
|         :src="getImage(recipeDetails.image)" |         :src="getImage(recipeDetails.slug)" | ||||||
|         class="d-print-none" |         class="d-print-none" | ||||||
|         :key="imageKey" |         :key="imageKey" | ||||||
|       > |       > | ||||||
| @@ -71,7 +71,6 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import utils from "@/utils"; |  | ||||||
| import VJsoneditor from "v-jsoneditor"; | import VJsoneditor from "v-jsoneditor"; | ||||||
| import RecipeViewer from "@/components/Recipe/RecipeViewer"; | import RecipeViewer from "@/components/Recipe/RecipeViewer"; | ||||||
| import RecipeEditor from "@/components/Recipe/RecipeEditor"; | import RecipeEditor from "@/components/Recipe/RecipeEditor"; | ||||||
| @@ -160,7 +159,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       if (image) { |       if (image) { | ||||||
|         return utils.getImageURL(image) + "?rnd=" + this.imageKey; |         return api.recipes.recipeImage(image) + "&rnd=" + this.imageKey; | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     deleteRecipe() { |     deleteRecipe() { | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ const actions = { | |||||||
|  |  | ||||||
|   async refreshToken({ commit, getters }) { |   async refreshToken({ commit, getters }) { | ||||||
|     if (!getters.getIsLoggedIn) { |     if (!getters.getIsLoggedIn) { | ||||||
|       commit("setIsLoggedIn", false); // This is to be here... for some reasons?  ¯\_(ツ)_/¯ |       commit("setIsLoggedIn", false); // This has to be here... for some reasons?  ¯\_(ツ)_/¯ | ||||||
|       console.log("Not Logged In"); |       console.log("Not Logged In"); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ const monthsShort = [ | |||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   getImageURL(image) { |   getImageURL(image) { | ||||||
|     return `/api/recipes/${image}/image`; |     return `/api/recipes/${image}/image?image_type=small`; | ||||||
|   }, |   }, | ||||||
|   generateUniqueKey(item, index) { |   generateUniqueKey(item, index) { | ||||||
|     const uniqueKey = `${item}-${index}`; |     const uniqueKey = `${item}-${index}`; | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								makefile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								makefile
									
									
									
									
									
								
							| @@ -54,6 +54,7 @@ setup: ## Setup Development Instance | |||||||
|  |  | ||||||
| backend: ## Start Mealie Backend Development Server | backend: ## Start Mealie Backend Development Server | ||||||
| 	poetry run python mealie/db/init_db.py && \ | 	poetry run python mealie/db/init_db.py && \ | ||||||
|  | 	poetry run python mealie/services/image/minify.py && \ | ||||||
| 	poetry run python mealie/app.py | 	poetry run python mealie/app.py | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,19 +45,19 @@ LOGGER_FILE = DATA_DIR.joinpath("mealie.log") | |||||||
|  |  | ||||||
| class AppDirectories: | class AppDirectories: | ||||||
|     def __init__(self, cwd, data_dir) -> None: |     def __init__(self, cwd, data_dir) -> None: | ||||||
|         self.DATA_DIR = data_dir |         self.DATA_DIR: Path = data_dir | ||||||
|         self.WEB_PATH = cwd.joinpath("dist") |         self.WEB_PATH: Path = cwd.joinpath("dist") | ||||||
|         self.IMG_DIR = data_dir.joinpath("img") |         self.IMG_DIR: Path = data_dir.joinpath("img") | ||||||
|         self.BACKUP_DIR = data_dir.joinpath("backups") |         self.BACKUP_DIR: Path = data_dir.joinpath("backups") | ||||||
|         self.DEBUG_DIR = data_dir.joinpath("debug") |         self.DEBUG_DIR: Path = data_dir.joinpath("debug") | ||||||
|         self.MIGRATION_DIR = data_dir.joinpath("migration") |         self.MIGRATION_DIR: Path = data_dir.joinpath("migration") | ||||||
|         self.NEXTCLOUD_DIR = self.MIGRATION_DIR.joinpath("nextcloud") |         self.NEXTCLOUD_DIR: Path = self.MIGRATION_DIR.joinpath("nextcloud") | ||||||
|         self.CHOWDOWN_DIR = self.MIGRATION_DIR.joinpath("chowdown") |         self.CHOWDOWN_DIR: Path = self.MIGRATION_DIR.joinpath("chowdown") | ||||||
|         self.TEMPLATE_DIR = data_dir.joinpath("templates") |         self.TEMPLATE_DIR: Path = data_dir.joinpath("templates") | ||||||
|         self.USER_DIR = data_dir.joinpath("users") |         self.USER_DIR: Path = data_dir.joinpath("users") | ||||||
|         self.SQLITE_DIR = data_dir.joinpath("db") |         self.SQLITE_DIR: Path = data_dir.joinpath("db") | ||||||
|         self.RECIPE_DATA_DIR = data_dir.joinpath("recipes") |         self.RECIPE_DATA_DIR: Path = data_dir.joinpath("recipes") | ||||||
|         self.TEMP_DIR = data_dir.joinpath(".temp") |         self.TEMP_DIR: Path = data_dir.joinpath(".temp") | ||||||
|  |  | ||||||
|         self.ensure_directories() |         self.ensure_directories() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | from enum import Enum | ||||||
|  |  | ||||||
| from fastapi import APIRouter, Depends, File, Form, HTTPException | from fastapi import APIRouter, Depends, File, Form, HTTPException | ||||||
| from fastapi.responses import FileResponse | from fastapi.responses import FileResponse | ||||||
| from mealie.db.database import db | from mealie.db.database import db | ||||||
| @@ -5,7 +7,7 @@ from mealie.db.db_setup import generate_session | |||||||
| from mealie.routes.deps import get_current_user | from mealie.routes.deps import get_current_user | ||||||
| from mealie.schema.recipe import Recipe, RecipeURLIn | from mealie.schema.recipe import Recipe, RecipeURLIn | ||||||
| from mealie.schema.snackbar import SnackResponse | from mealie.schema.snackbar import SnackResponse | ||||||
| from mealie.services.image_services import read_image, write_image | from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, write_image | ||||||
| from mealie.services.scraper.scraper import create_from_url | from mealie.services.scraper.scraper import create_from_url | ||||||
| from sqlalchemy.orm.session import Session | from sqlalchemy.orm.session import Session | ||||||
|  |  | ||||||
| @@ -72,20 +74,35 @@ def delete_recipe( | |||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         db.recipes.delete(session, recipe_slug) |         db.recipes.delete(session, recipe_slug) | ||||||
|  |         delete_image(recipe_slug) | ||||||
|     except: |     except: | ||||||
|         raise HTTPException(status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")) |         raise HTTPException(status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")) | ||||||
|  |  | ||||||
|     return SnackResponse.error(f"Recipe {recipe_slug} Deleted") |     return SnackResponse.error(f"Recipe {recipe_slug} Deleted") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ImageType(str, Enum): | ||||||
|  |     original = "original" | ||||||
|  |     small = "small" | ||||||
|  |     tiny = "tiny" | ||||||
|  |  | ||||||
|  |  | ||||||
| @router.get("/{recipe_slug}/image") | @router.get("/{recipe_slug}/image") | ||||||
| async def get_recipe_img(recipe_slug: str): | async def get_recipe_img(recipe_slug: str, image_type: ImageType = ImageType.original): | ||||||
|     """ Takes in a recipe slug, returns the static image """ |     """ Takes in a recipe slug, returns the static image """ | ||||||
|     recipe_image = read_image(recipe_slug) |     if image_type == ImageType.original: | ||||||
|  |         which_image = IMG_OPTIONS.ORIGINAL_IMAGE | ||||||
|  |     elif image_type == ImageType.small: | ||||||
|  |         which_image = IMG_OPTIONS.MINIFIED_IMAGE | ||||||
|  |     elif image_type == ImageType.tiny: | ||||||
|  |         which_image = IMG_OPTIONS.TINY_IMAGE | ||||||
|  |  | ||||||
|  |     recipe_image = read_image(recipe_slug, image_type=which_image) | ||||||
|  |     print(recipe_image) | ||||||
|     if recipe_image: |     if recipe_image: | ||||||
|         return FileResponse(recipe_image) |         return FileResponse(recipe_image) | ||||||
|     else: |     else: | ||||||
|         return |         raise HTTPException(404, "file not found") | ||||||
|  |  | ||||||
|  |  | ||||||
| @router.put("/{recipe_slug}/image") | @router.put("/{recipe_slug}/image") | ||||||
|   | |||||||
| @@ -65,8 +65,7 @@ class ExportDatabase: | |||||||
|                     f.write(content) |                     f.write(content) | ||||||
|  |  | ||||||
|     def export_images(self): |     def export_images(self): | ||||||
|         for file in app_dirs.IMG_DIR.iterdir(): |         shutil.copytree(app_dirs.IMG_DIR, self.img_dir, dirs_exist_ok=True) | ||||||
|             shutil.copy(file, self.img_dir.joinpath(file.name)) |  | ||||||
|  |  | ||||||
|     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): | ||||||
|         items = [x.dict() for x in items] |         items = [x.dict() for x in items] | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ from mealie.schema.restore import CustomPageImport, GroupImport, RecipeImport, S | |||||||
| from mealie.schema.settings import CustomPageOut, SiteSettings | from mealie.schema.settings import CustomPageOut, SiteSettings | ||||||
| from mealie.schema.theme import SiteTheme | from mealie.schema.theme import SiteTheme | ||||||
| from mealie.schema.user import UpdateGroup, UserInDB | from mealie.schema.user import UpdateGroup, UserInDB | ||||||
|  | from mealie.services.image import minify | ||||||
| from pydantic.main import BaseModel | from pydantic.main import BaseModel | ||||||
| from sqlalchemy.orm.session import Session | from sqlalchemy.orm.session import Session | ||||||
|  |  | ||||||
| @@ -108,8 +109,14 @@ class ImportDatabase: | |||||||
|         image_dir = self.import_dir.joinpath("images") |         image_dir = self.import_dir.joinpath("images") | ||||||
|         for image in image_dir.iterdir(): |         for image in image_dir.iterdir(): | ||||||
|             if image.stem in successful_imports: |             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) |                     shutil.copy(image, app_dirs.IMG_DIR) | ||||||
|  |  | ||||||
|  |         minify.migrate_images() | ||||||
|  |  | ||||||
|     def import_themes(self): |     def import_themes(self): | ||||||
|         themes_file = self.import_dir.joinpath("themes", "themes.json") |         themes_file = self.import_dir.joinpath("themes", "themes.json") | ||||||
|         themes = ImportDatabase.read_models_file(themes_file, SiteTheme) |         themes = ImportDatabase.read_models_file(themes_file, SiteTheme) | ||||||
|   | |||||||
							
								
								
									
										101
									
								
								mealie/services/image/image.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								mealie/services/image/image.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | import shutil | ||||||
|  | from dataclasses import dataclass | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import Union | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  | from fastapi.logger import logger | ||||||
|  | from mealie.core.config import app_dirs | ||||||
|  | from mealie.services.image import minify | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class ImageOptions: | ||||||
|  |     ORIGINAL_IMAGE: str = "original*" | ||||||
|  |     MINIFIED_IMAGE: str = "min-original*" | ||||||
|  |     TINY_IMAGE: str = "tiny-original*" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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] | ||||||
|  |     """ | ||||||
|  |     print(image_type) | ||||||
|  |     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 write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name: | ||||||
|  |     try: | ||||||
|  |         delete_image(recipe_slug) | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     image_dir = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}")) | ||||||
|  |     image_dir.mkdir() | ||||||
|  |     extension = extension.replace(".", "") | ||||||
|  |     image_path = image_dir.joinpath(f"original.{extension}") | ||||||
|  |  | ||||||
|  |     if isinstance(file_data, bytes): | ||||||
|  |         with open(image_path, "ab") as f: | ||||||
|  |             f.write(file_data) | ||||||
|  |     else: | ||||||
|  |         with open(image_path, "ab") as f: | ||||||
|  |             shutil.copyfileobj(file_data, f) | ||||||
|  |  | ||||||
|  |     minify.migrate_images() | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |  | ||||||
|  |     if isinstance(image_url, list):  # Handles List Types | ||||||
|  |         image_url = image_url[0] | ||||||
|  |  | ||||||
|  |     if isinstance(image_url, dict):  # Handles Dictionary Types | ||||||
|  |         for key in image_url: | ||||||
|  |             if key == "url": | ||||||
|  |                 image_url = image_url.get("url") | ||||||
|  |  | ||||||
|  |     filename = slug + "." + image_url.split(".")[-1] | ||||||
|  |     filename = app_dirs.IMG_DIR.joinpath(filename) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         r = requests.get(image_url, stream=True) | ||||||
|  |     except: | ||||||
|  |         logger.exception("Fatal Image Request Exception") | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     if r.status_code == 200: | ||||||
|  |         r.raw.decode_content = True | ||||||
|  |  | ||||||
|  |         write_image(slug, r.raw, filename.suffix) | ||||||
|  |  | ||||||
|  |         filename.unlink() | ||||||
|  |  | ||||||
|  |         return filename | ||||||
|  |  | ||||||
|  |     return None | ||||||
							
								
								
									
										84
									
								
								mealie/services/image/minify.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								mealie/services/image/minify.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | from mealie.core.config import app_dirs | ||||||
|  | from PIL import Image, UnidentifiedImageError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def minify_image(image_file: Path, min_dest: Path, tiny_dest: Path): | ||||||
|  |     """Minifies an image in it's original file format. Quality is lost | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         my_path (Path): Source Files | ||||||
|  |         min_dest (Path): FULL Destination File Path | ||||||
|  |         tiny_dest (Path): FULL Destination File Path | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         img = Image.open(image_file) | ||||||
|  |         basewidth = 720 | ||||||
|  |         wpercent = basewidth / float(img.size[0]) | ||||||
|  |         hsize = int((float(img.size[1]) * float(wpercent))) | ||||||
|  |         img = img.resize((basewidth, hsize), Image.ANTIALIAS) | ||||||
|  |         img.save(min_dest, quality=70) | ||||||
|  |  | ||||||
|  |         tiny_image = crop_center(img) | ||||||
|  |         tiny_image.save(tiny_dest, quality=70) | ||||||
|  |  | ||||||
|  |     except UnidentifiedImageError: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def crop_center(pil_img, crop_width=300, crop_height=300): | ||||||
|  |     img_width, img_height = pil_img.size | ||||||
|  |     return pil_img.crop( | ||||||
|  |         ( | ||||||
|  |             (img_width - crop_width) // 2, | ||||||
|  |             (img_height - crop_height) // 2, | ||||||
|  |             (img_width + crop_width) // 2, | ||||||
|  |             (img_height + crop_height) // 2, | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def sizeof_fmt(size, decimal_places=2): | ||||||
|  |     for unit in ["B", "kB", "MB", "GB", "TB", "PB"]: | ||||||
|  |         if size < 1024.0 or unit == "PiB": | ||||||
|  |             break | ||||||
|  |         size /= 1024.0 | ||||||
|  |     return f"{size:.{decimal_places}f} {unit}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def move_all_images(): | ||||||
|  |     for image_file in app_dirs.IMG_DIR.iterdir(): | ||||||
|  |         if image_file.is_file(): | ||||||
|  |             if image_file.name == ".DS_Store": | ||||||
|  |                 continue | ||||||
|  |             new_folder = app_dirs.IMG_DIR.joinpath(image_file.stem) | ||||||
|  |             new_folder.mkdir(parents=True, exist_ok=True) | ||||||
|  |             image_file.rename(new_folder.joinpath(f"original{image_file.suffix}")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def migrate_images(): | ||||||
|  |     print("Checking for Images to Minify...") | ||||||
|  |  | ||||||
|  |     move_all_images() | ||||||
|  |  | ||||||
|  |     # Minify Loop | ||||||
|  |     for image in app_dirs.IMG_DIR.glob("*/original.*"): | ||||||
|  |         min_dest = image.parent.joinpath(f"min-original{image.suffix}") | ||||||
|  |         tiny_dest = image.parent.joinpath(f"tiny-original{image.suffix}") | ||||||
|  |  | ||||||
|  |         if min_dest.exists() and tiny_dest.exists(): | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |         minify_image(image, min_dest, tiny_dest) | ||||||
|  |  | ||||||
|  |         org_size = sizeof_fmt(image.stat().st_size) | ||||||
|  |         dest_size = sizeof_fmt(min_dest.stat().st_size) | ||||||
|  |         tiny_size = sizeof_fmt(tiny_dest.stat().st_size) | ||||||
|  |         print(f"{image.name} Minified: {org_size} -> {dest_size} -> {tiny_size}") | ||||||
|  |  | ||||||
|  |     print("Finished Minification Check") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     migrate_images() | ||||||
| @@ -1,63 +0,0 @@ | |||||||
| import shutil |  | ||||||
| from pathlib import Path |  | ||||||
|  |  | ||||||
| import requests |  | ||||||
| from fastapi.logger import logger |  | ||||||
| from mealie.core.config import app_dirs |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def read_image(recipe_slug: str) -> Path: |  | ||||||
|     if app_dirs.IMG_DIR.joinpath(recipe_slug).is_file(): |  | ||||||
|         return app_dirs.IMG_DIR.joinpath(recipe_slug) |  | ||||||
|  |  | ||||||
|     recipe_slug = recipe_slug.split(".")[0] |  | ||||||
|     for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"): |  | ||||||
|         return file |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name: |  | ||||||
|     delete_image(recipe_slug) |  | ||||||
|  |  | ||||||
|     image_path = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}.{extension}")) |  | ||||||
|     with open(image_path, "ab") as f: |  | ||||||
|         f.write(file_data) |  | ||||||
|  |  | ||||||
|     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 file.unlink() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def scrape_image(image_url: str, slug: str) -> Path: |  | ||||||
|     if isinstance(image_url, str):  # Handles String Types |  | ||||||
|         image_url = image_url |  | ||||||
|  |  | ||||||
|     if isinstance(image_url, list):  # Handles List Types |  | ||||||
|         image_url = image_url[0] |  | ||||||
|  |  | ||||||
|     if isinstance(image_url, dict):  # Handles Dictionary Types |  | ||||||
|         for key in image_url: |  | ||||||
|             if key == "url": |  | ||||||
|                 image_url = image_url.get("url") |  | ||||||
|  |  | ||||||
|     filename = slug + "." + image_url.split(".")[-1] |  | ||||||
|     filename = app_dirs.IMG_DIR.joinpath(filename) |  | ||||||
|  |  | ||||||
|     try: |  | ||||||
|         r = requests.get(image_url, stream=True) |  | ||||||
|     except: |  | ||||||
|         logger.exception("Fatal Image Request Exception") |  | ||||||
|         return None |  | ||||||
|  |  | ||||||
|     if r.status_code == 200: |  | ||||||
|         r.raw.decode_content = True |  | ||||||
|  |  | ||||||
|         with open(filename, "wb") as f: |  | ||||||
|             shutil.copyfileobj(r.raw, f) |  | ||||||
|  |  | ||||||
|         return filename |  | ||||||
|  |  | ||||||
|     return None |  | ||||||
| @@ -52,7 +52,7 @@ def get_todays_meal(session: Session, group: Union[int, GroupInDB]) -> Recipe: | |||||||
|     Returns: |     Returns: | ||||||
|         Recipe: Pydantic Recipe Object |         Recipe: Pydantic Recipe Object | ||||||
|     """ |     """ | ||||||
|     session = session if session else create_session() |     session = session or create_session() | ||||||
|  |  | ||||||
|     if isinstance(group, int): |     if isinstance(group, int): | ||||||
|         group: GroupInDB = db.groups.get(session, group) |         group: GroupInDB = db.groups.get(session, group) | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import requests | |||||||
| import scrape_schema_recipe | import scrape_schema_recipe | ||||||
| from mealie.core.config import app_dirs | from mealie.core.config import app_dirs | ||||||
| from fastapi.logger import logger | from fastapi.logger import logger | ||||||
| from mealie.services.image_services import scrape_image | from mealie.services.image.image import scrape_image | ||||||
| from mealie.schema.recipe import Recipe | from mealie.schema.recipe import Recipe | ||||||
| from mealie.services.scraper import open_graph | from mealie.services.scraper import open_graph | ||||||
| from mealie.services.scraper.cleaner import Cleaner | from mealie.services.scraper.cleaner import Cleaner | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										45
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @@ -606,6 +606,14 @@ category = "dev" | |||||||
| optional = false | optional = false | ||||||
| python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "pillow" | ||||||
|  | version = "8.2.0" | ||||||
|  | description = "Python Imaging Library (Fork)" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pluggy" | name = "pluggy" | ||||||
| version = "0.13.1" | version = "0.13.1" | ||||||
| @@ -1154,7 +1162,7 @@ python-versions = "*" | |||||||
| [metadata] | [metadata] | ||||||
| lock-version = "1.1" | lock-version = "1.1" | ||||||
| python-versions = "^3.9" | python-versions = "^3.9" | ||||||
| content-hash = "a6c10e179bc15efc30627c9793218bb944f43dce5e624a7bcabcc47545e661e8" | content-hash = "32bff6a472fd8564106e2cfa20161a47d271d89ed44a5f2f2483f419fe259c92" | ||||||
|  |  | ||||||
| [metadata.files] | [metadata.files] | ||||||
| aiofiles = [ | aiofiles = [ | ||||||
| @@ -1603,6 +1611,41 @@ pathspec = [ | |||||||
|     {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, |     {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, | ||||||
|     {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, |     {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, | ||||||
| ] | ] | ||||||
|  | pillow = [ | ||||||
|  |     {file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"}, | ||||||
|  |     {file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"}, | ||||||
|  |     {file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"}, | ||||||
|  |     {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"}, | ||||||
|  |     {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"}, | ||||||
|  |     {file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"}, | ||||||
|  |     {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, | ||||||
|  |     {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, | ||||||
|  |     {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, | ||||||
|  |     {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, | ||||||
|  | ] | ||||||
| pluggy = [ | pluggy = [ | ||||||
|     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, |     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, | ||||||
|     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, |     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ pytest-cov = "^2.11.0" | |||||||
| mkdocs-material = "^7.0.2" | mkdocs-material = "^7.0.2" | ||||||
| flake8 = "^3.9.0" | flake8 = "^3.9.0" | ||||||
| coverage = "^5.5" | coverage = "^5.5" | ||||||
|  | Pillow = "^8.2.0" | ||||||
|  |  | ||||||
| [build-system] | [build-system] | ||||||
| requires = ["poetry-core>=1.0.0"] | requires = ["poetry-core>=1.0.0"] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user