├── .github └── workflows │ └── docker-publish.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── compose.example.yaml ├── helpers ├── instadownloader.py ├── instaloader_login_helper.py └── mealie_api.py ├── main.py ├── requirements.txt └── templates └── index.html /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | release: 8 | types: [ published ] 9 | 10 | jobs: 11 | build-and-publish: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Log in to Docker Hub 19 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 20 | 21 | - name: Build Docker image 22 | run: docker build -t ${{ secrets.DOCKER_USERNAME }}/instagramtomealie:latest . 23 | 24 | - name: Tag Docker image 25 | run: | 26 | if [ "${{ github.event_name }}" = "release" ]; then 27 | TAG=${{ github.event.release.tag_name }} 28 | else 29 | TAG="latest" 30 | fi 31 | docker tag ${{ secrets.DOCKER_USERNAME }}/instagramtomealie:latest ${{ secrets.DOCKER_USERNAME }}/instagramtomealie:${TAG} 32 | 33 | - name: Push Docker image 34 | run: | 35 | if [ "${{ github.event_name }}" = "release" ]; then 36 | docker push ${{ secrets.DOCKER_USERNAME }}/instagramtomealie:${{ github.event.release.tag_name }} 37 | fi 38 | docker push ${{ secrets.DOCKER_USERNAME }}/instagramtomealie:latest 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Custom 2 | docker-compose.yaml 3 | compose.yaml 4 | session-file 5 | 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | .idea/ 169 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 2 | LABEL authors="JoTec2002" 3 | 4 | WORKDIR /app 5 | 6 | COPY requirements.txt /app/ 7 | 8 | # Install required packages 9 | RUN pip install -r requirements.txt 10 | 11 | # Copy application code 12 | COPY main.py /app/ 13 | COPY templates /app/templates 14 | COPY helpers /app/helpers 15 | 16 | # Expose the Flask port 17 | EXPOSE 9001 18 | 19 | # Run the application 20 | CMD ["python", "-u", "main.py"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jonas Graubner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InstagramToMealie 2 | 3 | A simple little converter, that imports an instagram URL into mealie 4 | 5 |

6 | 7 |

8 |

INSTAGRAM TO MEALIE

9 | 10 |

11 | license 12 | last-commit 13 | repo-top-language 14 | repo-language-count 15 |

16 |
17 | 18 | ## Table of Contents 19 | 20 | - [Overview](#overview) 21 | - [Getting Started](#getting-started) 22 | - [Prerequisites](#prerequisites) 23 | - [Installation](#installation) 24 | - [Usage](#usage) 25 | - [Configuration](#configuration) 26 | - [Contributing](#contributing) 27 | - [License](#license) 28 | - [Acknowledgments](#acknowledgments) 29 | 30 | --- 31 | 32 | ## Overview 33 | 34 | With InstagramToMealie, you can simply input an Instagram post URL. The project seamlessly integrates with the Mealie API to 35 | create a new recipe with an associated image or video assets. 36 | 37 | --- 38 | 39 | ## Getting Started 40 | 41 | ### Prerequisites 42 | 43 | 1. Make sure you have OpenAI / Ollama configured in Mealie by navigating to `/admin/debug/openai` on your Mealie instance. This project doesn't integrate directly with OpenAI / Ollama, but needs it to be configured in Mealie to work properly. I personally got the best results with `qwen2.5:7b` as the Ollama Model. 44 | 2. Generate a Mealie API Key (`/user/profile/api-tokens`). [Mealie Docs](https://docs.mealie.io/documentation/getting-started/api-usage/) 45 | 3. Generate a Instagram Session File (!thats the most tricky step). A [helper script](https://raw.githubusercontent.com/JoTec2002/InstagramToMealie/refs/heads/main/helpers/instaloader_login_helper.py) is provided in this repo! It's just 46 | copied from the [Instaloader Docs](https://instaloader.github.io/troubleshooting.html). 47 | 1. Download the script: [https://raw.githubusercontent.com/JoTec2002/InstagramToMealie/refs/heads/main/helpers/instaloader_login_helper.py](https://raw.githubusercontent.com/JoTec2002/InstagramToMealie/refs/heads/main/helpers/instaloader_login_helper.py) 48 | 2. Login to Instagram in Firefox 49 | 3. Execute the snippet: `python ./instaloader_login_helper.py` 50 | 4. Copy the file that was generated by the script to a known location. This file will later be mounted to the Docker container. It can be generated on a different system and than copied to the target system. 51 | 52 | ### Installation 53 | 54 | Install InstagramToMealie using one of the following methods: 55 | 56 | **Build from source:** 57 |
58 | 59 | 1. Clone the InstagramToMealie repository: 60 | 61 | ```sh 62 | ❯ git clone https://github.com/JoTec2002/InstagramToMealie 63 | ``` 64 | 65 | 2. Navigate to the project directory: 66 | 67 | ```sh 68 | ❯ cd InstagramToMealie 69 | ``` 70 | 71 | 3. Install the project dependencies: 72 | 73 | ```sh 74 | ❯ pip install -r requirements.txt 75 | ``` 76 | 77 | 4. Start the server: 78 | 79 | ```sh 80 | ❯ python -u main.py 81 | ``` 82 | 83 |
84 | 85 | **Use the provided Docker image at [jotec2002/instagramtomealie](https://hub.docker.com/repository/docker/jotec2002/instagramtomealie/general)** 86 | 87 | Deploy it via Docker Compose alongside your Mealie installation 88 | 89 | Example `compose.yaml` file using a session file to authenticate: 90 | 91 | ```yaml 92 | services: 93 | mealie: 94 | image: ghcr.io/mealie-recipes/mealie:v2.1.0 95 | container_name: mealie 96 | #Look up in the Mealie Docs for how to use Mealie 97 | InstagramToMealie: 98 | image: jotec2002/instagramtomealie 99 | ports: 100 | - 9001:9001 101 | environment: 102 | INSTA_USER: "instagram username" 103 | MEALIE_API_KEY: "MEALIE API KEY" 104 | MEALIE_URL: "YOU LOCAL MEALIE INSTALLATION" # e.g http://mealie:9000 105 | MEALIE_OPENAI_REQUEST_TIMEOUT: 60 # Optional, default: 60 106 | volumes: 107 | - ./session-file:/app/session-file # The Instagram session file you created in the Prerequisites 108 | depends_on: 109 | mealie: 110 | condition: service_healthy 111 | ``` 112 | 113 | Example `compose.yaml` file using a username & password environment variables to authenticate: 114 | 115 | > [!IMPORTANT] 116 | > **Two-factor authentication (TFA/TOTP) needs to be disabled on the account in order for this method to work.** 117 | > You will probably need multiple attempts to get this to work. Log in on other systems / IPs in parallel to not trip the Instagram bot detection. 118 | > This is not the recommended way to set up InstagramToMealie. 119 | 120 | ```yaml 121 | services: 122 | mealie: 123 | image: ghcr.io/mealie-recipes/mealie:v2.1.0 124 | container_name: mealie 125 | #Look up in the Mealie Docs for how to use Mealie 126 | InstagramToMealie: 127 | image: jotec2002/instagramtomealie 128 | ports: 129 | - 9001:9001 130 | environment: 131 | INSTA_USER: "instagram username" 132 | INSTA_PWD: "Cleartext Instagram password" 133 | MEALIE_API_KEY: "MEALIE API KEY" 134 | MEALIE_URL: "YOU LOCAL MEALIE INSTALLATION" # e.g http://mealie:9000 135 | MEALIE_OPENAI_REQUEST_TIMEOUT: 60 # Optional, default: 60 136 | MEALIE_USE_INSTAGRAM_TAGS: true 137 | ``` 138 | 139 | **Building the Docker image yourself** 140 | 141 | Configure just like when using the provided Docker image but replace with the following in `compose.yaml`: 142 | 143 | ```diff 144 | services: 145 | mealie: 146 | image: ghcr.io/mealie-recipes/mealie:v2.1.0 147 | container_name: mealie 148 | #Look up in the Mealie Docs for how to use Mealie 149 | InstagramToMealie: 150 | + build: 151 | + context: . 152 | + dockerfile: Dockerfile 153 | + image: instagramtomealie:latest 154 | ports: 155 | - 9001:9001 156 | ``` 157 | 158 | ### Usage 159 | 160 | 1. Open in Webbrowser (e.g. `http://instagramtomealie.my-server.com`) and just import the Instagram URL into the textfield 161 | 2. Call from an automation (e.g. IOS shortcut) the url `http://instagramtomealie.my-server.com?url=` 162 | 163 | ### Configuration 164 | 165 | ```env 166 | MEALIE_URL: # Full URL of your Mealie instance (e.g http://mealie:9000, http://192.168.1.2:9000, http://my-mealie.com), required. 167 | MEALIE_API_KEY: # API key used to authenticate with the Mealie REST API, required. 168 | MEALIE_OPENAI_REQUEST_TIMEOUT: 60 # The timeout in seconds for OpenAI / Ollama requests, optional, default 60. 169 | MEALIE_USE_INSTAGRAM_TAGS: true # Embeds tags provided on the Instagram post as tags in Mealie, optional, default true. 170 | INSTA_USER: # Instagram username (e.g mob_kitchen), required. 171 | INSTA_PWD: # Instagram password in plaintext, optional (if using a session file). 172 | INSTA_TOTP_SECRET: # Secret key used for 2FA authentication, optional, not recommended. 173 | HTTP_PORT: # Port to use for the Flask HTTP server, optional, default 9001 174 | ``` 175 | 176 | --- 177 | 178 | ## Contributing 179 | 180 | - **💬 [Join the Discussions](https://github.com/JoTec2002/InstagramToMealie/discussions)**: Share your insights, provide 181 | feedback, or ask questions. 182 | - **🐛 [Report Issues](https://github.com/JoTec2002/InstagramToMealie/issues)**: Submit bugs found or log feature 183 | requests for the `InstagramToMealie` project. 184 | - **💡 [Submit Pull Requests](https://github.com/JoTec2002/InstagramToMealie/blob/main/CONTRIBUTING.md)**: Review open 185 | PRs, and submit your own PRs. 186 | 187 |
188 | Contributor Graph 189 |
190 |

191 | 192 | 193 | 194 |

195 |
196 | 197 | --- 198 | 199 | ## License 200 | 201 | This project is protected under the MIT License. For more details, 202 | refer to the [LICENSE](https://choosealicense.com/licenses/) file. 203 | 204 | --- 205 | 206 | ## Acknowledgments 207 | 208 | - [Mealie](https://github.com/mealie-recipes/mealie/) 209 | - [Instadownloader](https://github.com/instaloader/instaloader) 210 | 211 | --- 212 | -------------------------------------------------------------------------------- /compose.example.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | InstagramToMealie: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | image: instagramtomealie:latest 7 | ports: 8 | - 9001:9001 9 | environment: 10 | INSTA_USER: "instagram username" 11 | MEALIE_API_KEY: "MEALIE API KEY" 12 | MEALIE_URL: "YOU LOCAL MEALIE INSTALLATION" 13 | MEALIE_OPENAI_REQUEST_TIMEOUT: 360 14 | volumes: 15 | - ./session-file:/app/session-file 16 | -------------------------------------------------------------------------------- /helpers/instadownloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import instaloader 4 | import pyotp 5 | from instaloader import Post, TwoFactorAuthRequiredException, BadCredentialsException 6 | 7 | 8 | class InstaDownloader: 9 | def __init__(self): 10 | self.loader = instaloader.Instaloader(download_comments=False, 11 | download_geotags=False, 12 | save_metadata=False, 13 | dirname_pattern="downloads/{target}", ) 14 | try: 15 | user = os.environ.get('INSTA_USER') 16 | if os.path.isfile("./session-file"): 17 | self.loader.load_session_from_file(user, "./session-file") 18 | else: 19 | self.loader.login(os.environ.get("INSTA_USER"), os.environ.get("INSTA_PWD")) 20 | except TwoFactorAuthRequiredException: # Probably not going to work https://github.com/instaloader/instaloader/issues/1217 21 | print(os.environ.get("INSTA_TOTP_SECRET")) 22 | totp = pyotp.TOTP(os.environ.get("INSTA_TOTP_SECRET")) 23 | print(totp.now()) 24 | try: 25 | self.loader.two_factor_login(totp.now()) 26 | except BadCredentialsException: 27 | self.loader.two_factor_login(totp.now()) 28 | 29 | print(self.loader.test_login()) 30 | 31 | def download_instagram_post(self, url) -> Post | None: 32 | # Validate and extract shortcode from the URL 33 | match = re.search(r'(https?://)?(www\.)?instagram\.com/(p|reel|tv)/([A-Za-z0-9_-]+)', url) 34 | if not match: 35 | print(f"Received invalid Instagram URL ({url}). Please make sure it is a post, reel, or IGTV URL.") 36 | return None 37 | 38 | shortcode = match.group(4) # Extract the shortcode from the URL 39 | 40 | try: 41 | # Load and download the post using the shortcode 42 | post = instaloader.Post.from_shortcode(self.loader.context, shortcode) 43 | self.loader.download_post(post, target=post.shortcode) 44 | print(f"Downloaded post: {url}") 45 | return post 46 | except Exception as e: 47 | print(f"Error downloading post: {e}") 48 | -------------------------------------------------------------------------------- /helpers/instaloader_login_helper.py: -------------------------------------------------------------------------------- 1 | # Copied from https://instaloader.github.io/troubleshooting.html 2 | 3 | from argparse import ArgumentParser 4 | from glob import glob 5 | from os.path import expanduser 6 | from platform import system 7 | from sqlite3 import OperationalError, connect 8 | 9 | try: 10 | from instaloader import ConnectionException, Instaloader 11 | except ModuleNotFoundError: 12 | raise SystemExit("Instaloader not found.\n pip install [--user] instaloader") 13 | 14 | 15 | def get_cookiefile(): 16 | default_cookiefile = { 17 | "Windows": "~/AppData/Roaming/Mozilla/Firefox/Profiles/*/cookies.sqlite", 18 | "Darwin": "~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite", 19 | }.get(system(), "~/.mozilla/firefox/*/cookies.sqlite") 20 | cookiefiles = glob(expanduser(default_cookiefile)) 21 | if not cookiefiles: 22 | raise SystemExit("No Firefox cookies.sqlite file found. Use -c COOKIEFILE.") 23 | return cookiefiles[0] 24 | 25 | 26 | def import_session(cookiefile, sessionfile): 27 | print("Using cookies from {}.".format(cookiefile)) 28 | conn = connect(f"file:{cookiefile}?immutable=1", uri=True) 29 | try: 30 | cookie_data = conn.execute( 31 | "SELECT name, value FROM moz_cookies WHERE baseDomain='instagram.com'" 32 | ) 33 | except OperationalError: 34 | cookie_data = conn.execute( 35 | "SELECT name, value FROM moz_cookies WHERE host LIKE '%instagram.com'" 36 | ) 37 | instaloader = Instaloader(max_connection_attempts=1) 38 | instaloader.context._session.cookies.update(cookie_data) 39 | username = instaloader.test_login() 40 | if not username: 41 | raise SystemExit("Not logged in. Are you logged in successfully in Firefox?") 42 | print("Imported session cookie for {}.".format(username)) 43 | instaloader.context.username = username 44 | instaloader.save_session_to_file(sessionfile) 45 | 46 | 47 | if __name__ == "__main__": 48 | p = ArgumentParser() 49 | p.add_argument("-c", "--cookiefile") 50 | p.add_argument("-f", "--sessionfile") 51 | args = p.parse_args() 52 | try: 53 | import_session(args.cookiefile or get_cookiefile(), args.sessionfile) 54 | except (ConnectionException, OperationalError) as e: 55 | raise SystemExit("Cookie import failed: {}".format(e)) 56 | -------------------------------------------------------------------------------- /helpers/mealie_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | 5 | 6 | class MealieAPI: 7 | def __init__(self, url, key): 8 | self.MEALIE_URL = url 9 | self.API_KEY = key 10 | self.HEADERS = { 11 | "Authorization": f"Bearer {self.API_KEY}", 12 | } 13 | self.get_user_self() # Test if connection to Mealie is valid 14 | 15 | def get_user_self(self) -> bool: 16 | # Check connection and authentication data 17 | print(f"\nChecking connection and validating auth data...") 18 | 19 | response = requests.get(f"{self.MEALIE_URL}/api/users/self", headers=self.HEADERS) 20 | 21 | if response.status_code == 200: 22 | print(f"\nConnection established! Auth data validated! - Status Code: {response.status_code}") 23 | return True 24 | else: 25 | print( 26 | f"\nError while connecting to your Mealie API! - Status Code: {response.status_code}, Response: {response.text}") 27 | return False 28 | 29 | def __get_recipe(self, recipe_id) -> dict: 30 | response = requests.get(f"{self.MEALIE_URL}/api/recipes/{recipe_id}", headers=self.HEADERS) 31 | if response.status_code == 200: 32 | return response.json() 33 | else: 34 | raise Exception( 35 | f"Error while getting recipe from API! - Status Code: {response.status_code} - Response: {response.text}") 36 | 37 | def __put_recipe(self, recipe_id, data) -> str: 38 | response = requests.put(f"{self.MEALIE_URL}/api/recipes/{recipe_id}", headers=self.HEADERS, json=data) 39 | if response.status_code == 200: 40 | return response.json() 41 | else: 42 | raise Exception( 43 | f"Error while getting recipe from API! - Status Code: {response.status_code} - Response: {response.text}") 44 | 45 | def create_recipe_from_html(self, html_content) -> str: 46 | include_tags = True 47 | if "MEALIE_USE_INSTAGRAM_TAGS" in os.environ: 48 | if os.environ.get("MEALIE_USE_INSTAGRAM_TAGS").lower() == "false": 49 | include_tags = False 50 | 51 | recipe_data = { 52 | "includeTags": include_tags, 53 | "data": html_content 54 | } 55 | 56 | response = requests.post( 57 | f"{self.MEALIE_URL}/api/recipes/create/html-or-json", 58 | json=recipe_data, 59 | headers=self.HEADERS, 60 | timeout=int(os.environ.get("MEALIE_OPENAI_REQUEST_TIMEOUT") or 60) 61 | ) 62 | 63 | if response.status_code == 201: 64 | recipe = response.json() 65 | print(f"Created recipe with ID: {recipe}") 66 | else: 67 | raise Exception( 68 | f"Error while getting Recipe from API! - Status Code: {response.status_code} - Response: {response.text}") 69 | 70 | return recipe 71 | 72 | def update_recipe_orig_url(self, recipe_id, orig_url) -> str: 73 | recipe = self.__get_recipe(recipe_id) 74 | recipe.update({"orgURL": orig_url}) 75 | return self.__put_recipe(recipe_id, recipe) 76 | 77 | def upload_recipe_image(self, recipe_slug, image_url) -> str: 78 | files = { 79 | 'image': open(image_url, 'rb') 80 | } 81 | data = { 82 | 'extension': image_url.split('.')[-1] 83 | } 84 | response = requests.put(f"{self.MEALIE_URL}/api/recipes/{recipe_slug}/image", files=files, data=data, 85 | headers=self.HEADERS) 86 | 87 | if response.status_code == 200: 88 | print(f"Added cover image") 89 | return response.json() 90 | else: 91 | raise Exception( 92 | f"Error while uploading Image to API! - Status Code: {response.status_code} - Response: {response.text}") 93 | 94 | def upload_recipe_asset(self, recipe_slug, recipe_asset) -> str: 95 | files = { 96 | 'file': open(recipe_asset, 'rb') 97 | } 98 | data = { 99 | 'extension': recipe_asset.split('.')[-1], 100 | 'icon': "mdi-file-image", 101 | 'name': recipe_slug + "_video" 102 | } 103 | response = requests.post(f"{self.MEALIE_URL}/api/recipes/{recipe_slug}/assets", files=files, data=data, 104 | headers=self.HEADERS) 105 | 106 | if response.status_code == 200: 107 | print(f"Added video asset") 108 | return response.json() 109 | else: 110 | raise Exception( 111 | f"Error while uploading Video Asset to API! - Status Code: {response.status_code} - Response: {response.text}") 112 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from helpers.instadownloader import InstaDownloader 5 | from helpers.mealie_api import MealieAPI 6 | 7 | from flask import Flask, request, render_template 8 | 9 | if "MEALIE_URL" in os.environ: 10 | print(f"Got Mealie URL: {os.environ.get("MEALIE_URL")} from environment") 11 | else: 12 | print("Failed to get Mealie URL from environment, make sure MEALIE_URL is set.") 13 | exit(1) 14 | 15 | if "MEALIE_API_KEY" in os.environ: 16 | print("Got Mealie API key from environment") 17 | else: 18 | print("Failed to get Mealie API key from environment, make sure MEALIE_API_KEY is set.") 19 | exit(1) 20 | 21 | if "INSTA_USER" in os.environ: 22 | print(f"Got Instagram username: {os.environ.get("INSTA_USER")} from environment") 23 | else: 24 | print("Failed to get Instagram username from environment, make sure INSTA_USER is set.") 25 | exit(1) 26 | 27 | if os.path.isfile("./session-file"): 28 | print("Using the session file at: ./session-file") 29 | else: 30 | if "INSTA_PWD" in os.environ: 31 | if "INSTA_TOTP_SECRET" in os.environ: 32 | print("Got Instagram password and TOTP secret from environment. Trying to login without session file but failure is possible. Authenticating via session file is recommended.") 33 | else: 34 | print("Instagram password is set but no TOTP secret was found. Set INSTA_TOTP_SECRET if using 2FA, contuining with regular login without 2FA...") 35 | else: 36 | print("Failed to get a session file or Instagram password. Provide a valid session file or set INSTA_PWD in environment") 37 | exit(1) 38 | 39 | if "MEALIE_OPENAI_REQUEST_TIMEOUT" in os.environ: 40 | print(f"Got OpenAI timeout: {os.environ.get("MEALIE_OPENAI_REQUEST_TIMEOUT")}s from environment") 41 | else: 42 | print("Failed to get OpenAI timeout from environment. Using the default of 60s, if other timeout is desired make sure MEALIE_OPENAI_REQUEST_TIMEOUT is set.") 43 | 44 | mealie_api = MealieAPI(os.environ.get("MEALIE_URL"), os.environ.get("MEALIE_API_KEY")) 45 | downloader = InstaDownloader() 46 | print("Started succesfully") 47 | 48 | app = Flask(__name__) 49 | 50 | 51 | def execute_download(url): 52 | post = downloader.download_instagram_post(url) 53 | filepath = "downloads/" + post.shortcode + "/" 54 | 55 | try: 56 | recipe_slug = mealie_api.create_recipe_from_html(post.caption) 57 | 58 | mealie_api.update_recipe_orig_url(recipe_slug, url) 59 | image_file = filepath + post.date.strftime(format="%Y-%m-%d_%H-%M-%S_UTC") + ".jpg" 60 | mealie_api.upload_recipe_image(recipe_slug, image_file) 61 | if post.is_video: 62 | video_file = filepath + post.date.strftime(format="%Y-%m-%d_%H-%M-%S_UTC") + ".mp4" 63 | mealie_api.upload_recipe_asset(recipe_slug, video_file) 64 | 65 | shutil.rmtree(filepath) 66 | 67 | return render_template("index.html", successful="true") 68 | 69 | except Exception as e: 70 | shutil.rmtree(filepath) 71 | return repr(e) 72 | 73 | 74 | @app.route("/", methods=["GET", "POST"]) 75 | def index(): 76 | if request.method == "POST": 77 | url = request.form.get("url") 78 | return execute_download(url) 79 | 80 | elif request.args.get('url') is not None and request.args.get('url') != "": 81 | url = request.args.get('url') 82 | return execute_download(url) 83 | 84 | return render_template("index.html") 85 | 86 | 87 | if __name__ == "__main__": 88 | from waitress import serve 89 | 90 | http_port = os.environ.get("HTTP_PORT") or 9001 91 | 92 | serve(app, host="0.0.0.0", port=http_port) 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask~=3.1.0 2 | instaloader~=4.14 3 | pyotp~=2.9.0 4 | waitress~=3.0.2 5 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Instagram To Mealie Importer 8 | 9 | 10 |

Download Instagram Post/Reel

11 | {% if error %} 12 |

{{ error }}

13 | {% endif %} 14 |
15 | 16 | 17 | 18 |
19 | 20 | {% if successful %} 21 |

Video successfully imported to Mealie

22 | {% endif %} 23 | 24 | 25 | --------------------------------------------------------------------------------