├── .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 |
12 |
13 |
14 |
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 |
19 |
20 | {% if successful %}
21 | Video successfully imported to Mealie
22 | {% endif %}
23 |
24 |
25 |
--------------------------------------------------------------------------------