diff --git a/calendar_utils/models.py b/calendar_utils/models.py
index 0b19b54e3cade6fa455d48f92b84a9cdd4d5c317..6314deddeeb45ee4acf51b69ac2a96cf4e6a1641 100644
--- a/calendar_utils/models.py
+++ b/calendar_utils/models.py
@@ -93,9 +93,7 @@ class CalendarMixin(models.Model):
         calendar_format_events = []
 
         for event in (
-            self.calendar.past_events
-            if self.calendar.past_events is not None
-            else []
+            self.calendar.past_events if self.calendar.past_events is not None else []
         ) + (
             self.calendar.future_events
             if self.calendar.future_events is not None
diff --git a/district/migrations/0114_merge_20230502_2140.py b/district/migrations/0114_merge_20230502_2140.py
index d565ee04ea3d58ebc76b40458f8e939b1cf1c95d..22f934ed80e907d0306a0eb96506124d6de0f9d9 100644
--- a/district/migrations/0114_merge_20230502_2140.py
+++ b/district/migrations/0114_merge_20230502_2140.py
@@ -4,11 +4,9 @@ from django.db import migrations
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('district', '0110_remove_districtpersonpage_ical_calendar_url_and_more'),
-        ('district', '0113_merge_20230502_1854'),
+        ("district", "0110_remove_districtpersonpage_ical_calendar_url_and_more"),
+        ("district", "0113_merge_20230502_1854"),
     ]
 
-    operations = [
-    ]
+    operations = []
diff --git a/instagram_utils/models.py b/instagram_utils/models.py
index ec9ccdd15a26881f8e850c9a3d8dabae926b9a8e..f66474f8cd58e2f33b2336f3e1dccc5c907c91cd 100644
--- a/instagram_utils/models.py
+++ b/instagram_utils/models.py
@@ -1,4 +1,6 @@
 import datetime
+import mimetypes
+import uuid
 
 from django.db import models
 
@@ -7,6 +9,27 @@ def get_current_datetime() -> datetime.datetime:
     return datetime.datetime.now(tz=datetime.timezone.utc)
 
 
+def get_instagram_image_path(instance, filename) -> str:
+    mimetypes_instance = mimetypes.MimeTypes()
+    guessed_type = mimetypes_instance.guess_type(filename, strict=False)[0]
+
+    extension = ""
+
+    if guessed_type is not None:
+        for mapper in mimetypes_instance.types_map_inv:
+            if guessed_type not in mapper:
+                continue
+
+            extension = mapper[guessed_type]
+
+            if isinstance(extension, list):
+                extension = extension[0]
+
+            break
+
+    return f"instagram/{uuid.uuid4()}{extension}"
+
+
 class InstagramPost(models.Model):
     """
     Model representing an Instgram post obtained from its API through the
@@ -38,7 +61,7 @@ class InstagramPost(models.Model):
     )
     image = models.ImageField(
         verbose_name="Obrázek",
-        upload_to="instagram",
+        upload_to=get_instagram_image_path,
     )
     url = models.URLField(
         verbose_name="Odkaz",
diff --git a/instagram_utils/services.py b/instagram_utils/services.py
index b2857519a709c86f66c6579ffb6af896683dc50b..77101c81d6630455d31ea8c84e0929f9eea725c8 100644
--- a/instagram_utils/services.py
+++ b/instagram_utils/services.py
@@ -3,6 +3,7 @@ import io
 import logging
 import os
 
+import instaloader
 import requests
 from django.core.files import File
 
@@ -22,24 +23,21 @@ class InstagramDownloadService:
         self.app_id = app_id
         self.app_secret = app_secret
 
-    def get_user_info_list(self) -> list[str]:
+    def get_usernames(self) -> list[str]:
         access_block = MainHomePage.objects.first().instagram_access
 
-        homepage_access_list = [
-            (block["value"]["name"], block["value"]["access_token"])
-            for block in access_block.raw_data
-        ]
+        username_list = [block["value"]["username"] for block in access_block.raw_data]
 
-        people_access_list = []
+        for person_page in MainPersonPage.objects.all():
+            if (
+                person_page.instagram_username is None
+                or person_page.instagram_username in username_list
+            ):
+                continue
 
-        for people_page in MainPersonPage.objects.all():
-            people_access_list += [
-                (block["value"]["name"], block["value"]["access_token"])
-                for block in people_page.instagram_access.raw_data
-            ]
+            username_list.append(person_page.instagram_username)
 
-        # Remove duplicates
-        return list({*people_access_list, *homepage_access_list})
+        return username_list
 
     def download_remote_image(self, image_url) -> (str, File):
         try:
@@ -51,81 +49,43 @@ class InstagramDownloadService:
 
         return os.path.basename(image_url), File(io.BytesIO(response.content))
 
-    def get_user_data(self, access_token: str) -> dict:
-        user_data = requests.get(
-            f"https://graph.instagram.com/v16.0/me?access_token={access_token}"
-            "&fields=id,username"
-        )
-        user_data.raise_for_status()
-
-        return user_data.json()
-
-    def get_recent_media(self, user_data: dict, access_token: str) -> list[dict]:
-        recent_media = requests.get(
-            f"https://graph.instagram.com/v16.0/{user_data['id']}/media?access_token="
-            f"{access_token}&fields=id,timestamp,caption,media_type,permalink,"
-            "media_url,thumbnail_url"
-        )
-
-        if not recent_media.ok:
-            logger.warning(
-                "Error getting media for user %s: %s",
-                user_data["id"],
-                recent_media.status_code,
-            )
-
-            return []
-
-        logger.debug("Parsing Instagram feed: %s", recent_media)
+    def parse_media_for_user(self, username: str) -> None:
+        loader = instaloader.Instaloader()
 
-        return recent_media.json()["data"]
+        profile = instaloader.Profile.from_username(loader.context, username)
 
-    def parse_media_for_user(self, name: str, access_token: str) -> None:
-        user_data = self.get_user_data(access_token)
-        recent_media_json = self.get_recent_media(user_data, access_token)
+        for remote_post in profile.get_posts():
+            # Don't recreate existing posts
 
-        if len(recent_media_json) == 0:
-            return
-
-        posts = []
-
-        for media_data in recent_media_json:
-            # Don't recreate existing posts'
-            if InstagramPost.objects.filter(remote_id=media_data["id"]).exists():
+            if InstagramPost.objects.filter(remote_id=remote_post.shortcode).exists():
                 logging.info(
-                    "Skipping Instagram post ID %s, already exists", media_data["id"]
+                    "Skipping Instagram post ID %s, already exists",
+                    remote_post.shortcode,
                 )
 
                 continue
 
-            post = InstagramPost(
-                remote_id=media_data["id"],
-                author_name=name,
-                author_username=user_data["username"],
-                timestamp=datetime.datetime.strptime(
-                    media_data["timestamp"],
-                    "%Y-%m-%dT%H:%M:%S%z",
-                ),
-                caption=media_data["caption"],
-                url=media_data["permalink"],
+            local_post_instance = InstagramPost(
+                remote_id=remote_post.shortcode,
+                author_name=profile.full_name,
+                author_username=profile.username,
+                timestamp=remote_post.date_local,
+                caption=remote_post.caption,
+                url=f"https://instagram.com/p/{remote_post.shortcode}",
             )
 
-            post.image.save(
-                *self.download_remote_image(media_data["media_url"]),
+            local_post_instance.image.save(
+                *self.download_remote_image(remote_post.url),
                 False,  # Don't save yet
             )
 
-            post.save()
+            local_post_instance.save()
 
             logger.info(
                 "Saved Instagram post ID %s",
-                post.remote_id,
+                remote_post.mediaid,
             )
 
     def perform_update(self) -> None:
-        user_info_list = self.get_user_info_list()
-
-        media_list = []
-
-        for user_info in user_info_list:
-            self.parse_media_for_user(*user_info)
+        for username in self.get_usernames():
+            self.parse_media_for_user(username)
diff --git a/main/blocks.py b/main/blocks.py
index de53c4579b3e9c66c7e77e617860eeca51d5e402..5e2ef662c6787982b55d237b69f5a8f0578ac464 100644
--- a/main/blocks.py
+++ b/main/blocks.py
@@ -361,19 +361,12 @@ class CardLinkWithHeadlineBlock(CardLinkWithHeadlineBlockMixin):
 
 
 class InstagramAccessBlock(StructBlock):
-    name = CharBlock(label="Zobrazované jméno")
     username = CharBlock(
-        label="Username", help_text="Např. pirati.cz, bez @ na začátku!"
+        label="Uživatelské jméno", help_text="Např. pirati.cz, bez @ na začátku!"
     )
-    access_token = CharBlock(label="Přístupový token")
 
     class Meta:
         label = "Synchronizace s Instagramem"
-        help_text = (
-            "Informace lze získat přihlášením požadovaným Instagramovým "
-            "účtem na tools.pirati.cz/instagram . Token je třeba kvůli "
-            "podmínkám Instagramu každých 60 dní obnovit."
-        )
 
 
 class InstagramPostsBlock(StructBlock):
diff --git a/main/migrations/0057_remove_mainpersonpage_instagram_access_and_more.py b/main/migrations/0057_remove_mainpersonpage_instagram_access_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..93f05364421a26c26517fd47088022a6de58b918
--- /dev/null
+++ b/main/migrations/0057_remove_mainpersonpage_instagram_access_and_more.py
@@ -0,0 +1,53 @@
+# Generated by Django 4.1.10 on 2023-07-08 06:23
+
+import wagtail.blocks
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("main", "0056_remove_mainpersonpage_ical_calendar_url_and_more"),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name="mainpersonpage",
+            name="instagram_access",
+        ),
+        migrations.AddField(
+            model_name="mainpersonpage",
+            name="instagram_username",
+            field=models.CharField(
+                blank=True,
+                max_length=64,
+                null=True,
+                verbose_name="Uživatelské jméno na Instagramu",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="mainhomepage",
+            name="instagram_access",
+            field=wagtail.fields.StreamField(
+                [
+                    (
+                        "instagram_access",
+                        wagtail.blocks.StructBlock(
+                            [
+                                (
+                                    "username",
+                                    wagtail.blocks.CharBlock(
+                                        help_text="Např. pirati.cz, bez @ na začátku!",
+                                        label="Uživatelské jméno",
+                                    ),
+                                )
+                            ]
+                        ),
+                    )
+                ],
+                blank=True,
+                use_json_field=True,
+                verbose_name="Uživatelská jména synchronizovaných Instagram účtů",
+            ),
+        ),
+    ]
diff --git a/main/models.py b/main/models.py
index 74fa5fda89aba30df59439860da1a97cb978d82a..f1f728aefb40d01950a70e220688a862f651b8b6 100644
--- a/main/models.py
+++ b/main/models.py
@@ -141,7 +141,7 @@ class MainHomePage(
 
     instagram_access = StreamField(
         [("instagram_access", blocks.InstagramAccessBlock())],
-        verbose_name="Uživatelská jména a přístupové tokeny pro synchronizované Instagram účty",
+        verbose_name="Uživatelská jména synchronizovaných Instagram účtů",
         blank=True,
         max_num=64,
         use_json_field=True,
@@ -764,13 +764,8 @@ class MainPersonPage(
     perex = models.TextField()
     text = RichTextField()
 
-    instagram_access = StreamField(
-        [
-            ("instagram_access", blocks.InstagramAccessBlock()),
-        ],
-        verbose_name="Synchronizace s Instagramem",
-        blank=True,
-        use_json_field=True,
+    instagram_username = models.CharField(
+        "Uživatelské jméno na Instagramu", max_length=64, blank=True, null=True
     )
 
     social_links = StreamField(
@@ -807,11 +802,11 @@ class MainPersonPage(
         FieldPanel("after_name"),
         FieldPanel("position"),
         FieldPanel("perex"),
-        FieldPanel("instagram_access"),
         FieldPanel("text"),
         FieldPanel("email"),
         FieldPanel("phone"),
         FieldPanel("calendar_url"),
+        FieldPanel("instagram_username"),
         FieldPanel("social_links"),
         FieldPanel("people"),
     ]
@@ -819,12 +814,10 @@ class MainPersonPage(
     def get_context(self, request) -> dict:
         context = super().get_context(request)
 
-        if len(self.instagram_access.raw_data) != 0:
+        if self.instagram_username:
             context["instagram_post_list"] = (
                 InstagramPost.objects.filter(
-                    author_username=self.instagram_access.raw_data[0]["value"][
-                        "username"
-                    ]
+                    author_username=self.instagram_username
                 ).order_by("-timestamp")
             )[:20]
 
diff --git a/requirements/base.in b/requirements/base.in
index 0ccf7f6ba0f300761b9bb27feb3e902570b9c4a7..1025394b07ab0b9ad3eee6eb0a09d7a96f2265b6 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -1,6 +1,7 @@
-wagtail
+wagtail<5.0  # For now
 wagtail-metadata
 wagtail-trash
+django<4.2  # Wagtail compatibility
 django-environ<0.10.0
 django-extensions
 django-redis
@@ -14,6 +15,7 @@ opencv-python
 requests
 icalevents
 ics
+instaloader
 arrow
 sentry-sdk
 Markdown
diff --git a/requirements/base.txt b/requirements/base.txt
index a8f9c8a7761b73828256c642c43a0a8098763c42..298b3fc84a069514f88043a6854a0491c3d88878 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -12,13 +12,13 @@ arrow==1.2.3
     # via
     #   -r base.in
     #   ics
-asgiref==3.6.0
+asgiref==3.7.2
     # via django
 asttokens==2.2.1
     # via stack-data
 async-timeout==4.0.2
     # via redis
-attrs==22.2.0
+attrs==23.1.0
     # via
     #   cattrs
     #   ics
@@ -29,17 +29,17 @@ beautifulsoup4==4.11.2
     # via
     #   -r base.in
     #   wagtail
-billiard==3.6.4.0
+billiard==4.1.0
     # via celery
 bleach==6.0.0
     # via -r base.in
 brotli==1.0.9
     # via fonttools
-cattrs==22.2.0
+cattrs==23.1.2
     # via requests-cache
-celery==5.2.7
+celery==5.3.1
     # via -r base.in
-certifi==2022.12.7
+certifi==2023.5.7
     # via
     #   requests
     #   sentry-sdk
@@ -47,9 +47,9 @@ cffi==1.15.1
     # via
     #   cryptography
     #   weasyprint
-charset-normalizer==3.1.0
+charset-normalizer==3.2.0
     # via requests
-click==8.1.3
+click==8.1.4
     # via
     #   celery
     #   click-didyoumean
@@ -59,9 +59,9 @@ click-didyoumean==0.3.0
     # via celery
 click-plugins==1.1.1
     # via celery
-click-repl==0.2.0
+click-repl==0.3.0
     # via celery
-cryptography==40.0.1
+cryptography==41.0.1
     # via
     #   josepy
     #   mozilla-django-oidc
@@ -72,8 +72,9 @@ datetime==4.9
     # via icalevents
 decorator==5.1.1
     # via ipython
-django==4.1.8
+django==4.1.10
     # via
+    #   -r base.in
     #   django-extensions
     #   django-filter
     #   django-modelcluster
@@ -89,7 +90,7 @@ django==4.1.8
     #   wagtail
 django-environ==0.9.0
     # via -r base.in
-django-extensions==3.2.1
+django-extensions==3.2.3
     # via -r base.in
 django-filter==22.1
     # via wagtail
@@ -99,15 +100,15 @@ django-permissionedforms==0.1
     # via wagtail
 django-ranged-response==0.2.0
     # via django-simple-captcha
-django-redis==5.2.0
+django-redis==5.3.0
     # via -r base.in
 django-settings-export==1.2.1
     # via -r base.in
-django-simple-captcha==0.5.17
+django-simple-captcha==0.5.18
     # via -r base.in
 django-taggit==3.1.0
     # via wagtail
-django-treebeard==4.6.1
+django-treebeard==4.7
     # via wagtail
 django-widget-tweaks==1.4.12
     # via -r base.in
@@ -119,9 +120,9 @@ et-xmlfile==1.1.0
     # via openpyxl
 executing==1.2.0
     # via stack-data
-fastjsonschema==2.16.3
+fastjsonschema==2.17.1
     # via -r base.in
-fonttools[woff]==4.39.3
+fonttools[woff]==4.40.0
     # via weasyprint
 html5lib==1.1
     # via
@@ -137,13 +138,15 @@ ics==0.7.2
     # via -r base.in
 idna==3.4
     # via requests
-ipython==8.12.0
+instaloader==4.9.6
+    # via -r base.in
+ipython==8.14.0
     # via -r base.in
 jedi==0.18.2
     # via ipython
 josepy==1.13.0
     # via mozilla-django-oidc
-kombu==5.2.4
+kombu==5.3.1
     # via celery
 l18n==2021.3
     # via wagtail
@@ -153,13 +156,13 @@ matplotlib-inline==0.1.6
     # via ipython
 mozilla-django-oidc==2.0.0
     # via pirates
-numpy==1.24.2
+numpy==1.25.0
     # via opencv-python
 oauthlib==3.2.2
     # via
     #   requests-oauthlib
     #   tweepy
-opencv-python==4.7.0.72
+opencv-python==4.8.0.74
     # via -r base.in
 openpyxl==3.1.2
     # via wagtail
@@ -176,9 +179,9 @@ pillow==9.5.0
     #   weasyprint
 pirates==0.6.0
     # via -r base.in
-platformdirs==3.2.0
+platformdirs==3.8.1
     # via requests-cache
-prompt-toolkit==3.0.38
+prompt-toolkit==3.0.39
     # via
     #   click-repl
     #   ipython
@@ -190,13 +193,13 @@ pure-eval==0.2.2
     # via stack-data
 pycparser==2.21
     # via cffi
-pydyf==0.6.0
+pydyf==0.7.0
     # via weasyprint
-pygments==2.15.0
+pygments==2.15.1
     # via ipython
-pyopenssl==23.1.1
+pyopenssl==23.2.0
     # via josepy
-pyparsing==3.0.9
+pyparsing==3.1.0
     # via httplib2
 pypdf2==3.0.1
     # via -r base.in
@@ -205,12 +208,12 @@ pyphen==0.14.0
 python-dateutil==2.8.2
     # via
     #   arrow
+    #   celery
     #   icalendar
     #   icalevents
     #   ics
 pytz==2021.3
     # via
-    #   celery
     #   datetime
     #   django-modelcluster
     #   djangorestframework
@@ -219,41 +222,41 @@ pytz==2021.3
     #   l18n
 pyyaml==6.0
     # via -r base.in
-redis==4.5.4
+redis==4.6.0
     # via django-redis
-requests==2.28.2
+requests==2.31.0
     # via
     #   -r base.in
+    #   instaloader
     #   mozilla-django-oidc
     #   requests-cache
     #   requests-oauthlib
     #   tweepy
     #   wagtail
-requests-cache==1.0.1
+requests-cache==1.1.0
     # via -r base.in
 requests-oauthlib==1.3.1
     # via tweepy
-sentry-sdk==1.19.1
+sentry-sdk==1.27.1
     # via -r base.in
 six==1.16.0
     # via
     #   asttokens
     #   bleach
-    #   click-repl
     #   html5lib
     #   ics
     #   l18n
     #   python-dateutil
     #   url-normalize
-soupsieve==2.4
+soupsieve==2.4.1
     # via beautifulsoup4
-sqlparse==0.4.3
+sqlparse==0.4.4
     # via django
 stack-data==0.6.2
     # via ipython
 tatsu==5.8.3
     # via ics
-telepath==0.3
+telepath==0.3.1
     # via wagtail
 tinycss2==1.2.1
     # via
@@ -263,11 +266,13 @@ traitlets==5.9.0
     # via
     #   ipython
     #   matplotlib-inline
-tweepy==4.13.0
+tweepy==4.14.0
     # via -r base.in
+tzdata==2023.3
+    # via celery
 url-normalize==1.4.3
     # via requests-cache
-urllib3==1.26.15
+urllib3==2.0.3
     # via
     #   requests
     #   requests-cache
@@ -277,18 +282,18 @@ vine==5.0.0
     #   amqp
     #   celery
     #   kombu
-wagtail==4.2.2
+wagtail==4.2.4
     # via
     #   -r base.in
     #   wagtail-metadata
     #   wagtail-trash
 wagtail-metadata==4.0.3
     # via -r base.in
-wagtail-trash==1.0.0
+wagtail-trash==1.0.1
     # via -r base.in
 wcwidth==0.2.6
     # via prompt-toolkit
-weasyprint==58.1
+weasyprint==59.0
     # via -r base.in
 webencodings==0.5.1
     # via
@@ -296,7 +301,7 @@ webencodings==0.5.1
     #   cssselect2
     #   html5lib
     #   tinycss2
-whitenoise==6.4.0
+whitenoise==6.5.0
     # via -r base.in
 willow==1.4.1
     # via wagtail
diff --git a/requirements/dev.txt b/requirements/dev.txt
index d27107e71711573cc795a9c93681722e672fa071..fe21dda0f381ccee5ee6af18b08b586ff6805c0d 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1,24 +1,22 @@
 #
-# This file is autogenerated by pip-compile with Python 3.10
+# This file is autogenerated by pip-compile with Python 3.11
 # by the following command:
 #
 #    pip-compile dev.in
 #
-asgiref==3.6.0
+asgiref==3.7.2
     # via django
-coverage[toml]==7.2.3
+coverage[toml]==7.2.7
     # via pytest-cov
-django==4.1.8
+django==4.1.10
     # via
     #   -r dev.in
     #   django-debug-toolbar
-django-debug-toolbar==4.0.0
+django-debug-toolbar==4.1.0
     # via -r dev.in
-exceptiongroup==1.1.1
-    # via pytest
 factory-boy==3.2.1
     # via pytest-factoryboy
-faker==18.4.0
+faker==18.13.0
     # via factory-boy
 fastdiff==0.3.0
     # via snapshottest
@@ -32,9 +30,9 @@ packaging==23.1
     # via
     #   pytest
     #   pytest-sugar
-pluggy==1.0.0
+pluggy==1.2.0
     # via pytest
-pytest==7.3.0
+pytest==7.4.0
     # via
     #   -r dev.in
     #   pytest-cov
@@ -43,7 +41,7 @@ pytest==7.3.0
     #   pytest-freezegun
     #   pytest-mock
     #   pytest-sugar
-pytest-cov==4.0.0
+pytest-cov==4.1.0
     # via -r dev.in
 pytest-django==4.5.2
     # via -r dev.in
@@ -51,7 +49,7 @@ pytest-factoryboy==2.5.1
     # via -r dev.in
 pytest-freezegun==0.4.2
     # via -r dev.in
-pytest-mock==3.10.0
+pytest-mock==3.11.1
     # via -r dev.in
 pytest-sugar==0.9.7
     # via -r dev.in
@@ -65,19 +63,15 @@ six==1.16.0
     #   snapshottest
 snapshottest==0.6.0
     # via -r dev.in
-sqlparse==0.4.3
+sqlparse==0.4.4
     # via
     #   django
     #   django-debug-toolbar
-termcolor==2.2.0
+termcolor==2.3.0
     # via
     #   pytest-sugar
     #   snapshottest
-tomli==2.0.1
-    # via
-    #   coverage
-    #   pytest
-typing-extensions==4.5.0
+typing-extensions==4.7.1
     # via pytest-factoryboy
 wasmer==1.1.0
     # via fastdiff
diff --git a/requirements/production.txt b/requirements/production.txt
index 4f1ff713e8bdd33fadf6ca9bece8acbfc4575046..992947ab06bdebf5c6edc9fe1cbc51c9b1b34600 100644
--- a/requirements/production.txt
+++ b/requirements/production.txt
@@ -1,5 +1,5 @@
 #
-# This file is autogenerated by pip-compile with Python 3.10
+# This file is autogenerated by pip-compile with Python 3.11
 # by the following command:
 #
 #    pip-compile production.in