diff --git a/src/core/utils.py b/src/core/utils.py
index caf4a4af95c030f7c4ff6e8f2085f11bd62f412c..8332304b94ac9ac29c4c68009ea67e151b540b36 100644
--- a/src/core/utils.py
+++ b/src/core/utils.py
@@ -92,7 +92,7 @@ def str2bool(s, allow_null: bool = False):
 
     s = s.strip().lower()
 
-    if allow_null and s in ['-', 'none', 'null', 'unknown', '?']:
+    if allow_null and s in ['', '-', 'none', 'null', 'unknown', '?']:
         return None
 
     if s in ['true', 'on', '1', 't', 'y', 'yes', 'j', 'ja', '✓', '✔']:
diff --git a/src/plainui/jinja2/plainui/fahrplan.html b/src/plainui/jinja2/plainui/fahrplan.html
index 92754660c5686b62f2ca4e85ceedf488da91180b..dcc20ed7d551dfeefb6ceda34bb7408b129ec2ae 100644
--- a/src/plainui/jinja2/plainui/fahrplan.html
+++ b/src/plainui/jinja2/plainui/fahrplan.html
@@ -58,9 +58,10 @@
         {% if assembly %}<input type="hidden" name="assembly" value="{{assembly.slug}}">{% endif %}
         {% if track %}<input type="hidden" name="track" value="{{track.slug}}">{% endif %}
         {% if my_fahrplan %}<input type="hidden" name="my" value="y">{% endif %}
+        {% if is_recorded is not none %}<input type="hidden" name="rec" value="{{'y' if is_recorded else 'n'}}">{% endif %}
+
 
         <div class="d-flex gap-3 flex-column flex-md-row align-items-start hub-fahrplan__title mb-2">
-            <div class="me-md-4 fw-bold">{{ _("fahrplan.title") }}</div>
             <div>
                 <button
                     type="submit"
@@ -97,6 +98,10 @@
             {{ filter_button('d' ~ (n if n != day else ''), n == day, _("Day %(n)s", n=n + 1)) }}
           {%- endfor %}
 
+          <div class="hub-tag-divider"></div>
+
+          {{ filter_button("ry" if is_recorded is not true else "r", is_recorded is true, _("recorded only")) }}
+          {{ filter_button("rn" if is_recorded is not false else "r", is_recorded is false, _("not recorded only")) }}
         </div>
 
         <div class="hub-tags">
diff --git a/src/plainui/views/fahrplan.py b/src/plainui/views/fahrplan.py
index 377c28860052d543d4d8ecf0fcc4168a2d410034..990d6603a8a2623e0fe9aab0ec5991da3b846298 100644
--- a/src/plainui/views/fahrplan.py
+++ b/src/plainui/views/fahrplan.py
@@ -14,6 +14,7 @@ from core.models import (
     ConferenceTrack,
     Event,
 )
+from core.utils import str2bool
 
 from plainui.views.utils import ConferenceRequiredMixin, event_filter, organize_events_for_calendar, session_get_favorite_events
 
@@ -54,6 +55,7 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView):
         show_assembly_filters = self.request.GET.get('show_assembly_filters') == 'y'
         show_track_filters = self.request.GET.get('show_track_filters') == 'y'
         my_fahrplan = self.request.GET.get('my') == 'y'
+        is_recorded = str2bool(self.request.GET.get('rec', ''), allow_null=True)
 
         to_set = self.request.GET.get('set', None)
         if to_set:
@@ -79,6 +81,14 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView):
                 mode = to_set[1:]
             elif to_set[0] == 't':
                 track = to_set[1:]
+            elif to_set[0] == 'r':
+                match to_set[1:]:
+                    case '1' | 'y':
+                        is_recorded = True
+                    case '0' | 'n':
+                        is_recorded = False
+                    case _:
+                        is_recorded = None
 
         context['show_day_filters'] = show_day_filters
         context['show_assembly_filters'] = show_assembly_filters
@@ -130,6 +140,8 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView):
         track = tracks.get(slug=track) if track else None
         context['track'] = track
 
+        context['is_recorded'] = is_recorded
+
         events = event_filter(
             self.request.user,
             self.conf,
@@ -139,6 +151,7 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView):
             track=track,
             calendar_mode=mode == 'calendar',
             public_fahrplan=public_fahrplan,
+            is_recorded=is_recorded,
             **self.filter_opts,
         )
 
diff --git a/src/plainui/views/utils.py b/src/plainui/views/utils.py
index 5ada095ab5c7392d96499514d8e874e53bbbf069..5362bec081aa03bf728da1b3d41775573cbfbf16 100644
--- a/src/plainui/views/utils.py
+++ b/src/plainui/views/utils.py
@@ -185,6 +185,7 @@ def event_filter(
     upcoming=False,
     calendar_mode=True,
     public_fahrplan=None,
+    is_recorded: Optional[bool] = None,
 ):
     min_date, max_date = conf.start, conf.end
     if min_date is None or max_date is None:
@@ -215,9 +216,47 @@ def event_filter(
     if calendar_mode:
         events = events.filter(room__isnull=False)
     res = events.filter(schedule_duration__isnull=False, **filters).order_by('schedule_start', 'schedule_end')
+
+    # filter on the requested recording state
+    if is_recorded is True:
+        # don't include all events explicitly labeled "don't record"
+        res = res.exclude(recording=Event.Recording.NO)
+
+        res = res.filter(
+            # ... all events of unknown state in rooms with "record by default"
+            Q(recording=Event.Recording.UNKNOWN, room__isnull=False, room__recording_state__in=[Room.RecordingState.RECORD_BY_DEFAULT])
+            # ... all events with state "yes, record" which either ...
+            | (
+                Q(recording=Event.Recording.YES)
+                & (
+                    # don't have a room
+                    Q(room__isnull=True)
+                    # have a room which does allow recording
+                    | Q(room__isnull=False, room__recording_state__in=[Room.RecordingState.NOT_RECORDED_BY_DEFAULT, Room.RecordingState.RECORD_BY_DEFAULT])
+                )
+            )
+        )
+
+    elif is_recorded is False:
+        # kick all events which don't have a room and don't explicitly specify not to be recorded
+        res = res.exclude(room__isnull=True, recording__in=[Event.Recording.UNKNOWN, Event.Recording.YES])
+
+        # kick all events which ...
+        res = res.exclude(
+            # shall explicitly be recorded and ...
+            recording=Event.Recording.YES,
+            # have a room which permits recording (if an event may not be recorded because of this we can't filter it even though it requests to be recorded)
+            room__isnull=False,
+            room__recording_state__in=[Room.RecordingState.RECORD_BY_DEFAULT, Room.RecordingState.NOT_RECORDED_BY_DEFAULT],
+        )
+
+        # kick all events which default to their room's setting which would allow recording
+        res = res.exclude(recording=Event.Recording.UNKNOWN, room__isnull=False, room__recording_state__in=[Room.RecordingState.RECORD_BY_DEFAULT])
+
     res = res.annotate(track_name=F('track__name'))
     speakers = EventParticipant.objects.filter(is_public=True, role=EventParticipant.Role.SPEAKER).order_by('participant__username')
     speakers = speakers.annotate(speaker_name=F('participant__username'))
+
     return res.prefetch_related(
         Prefetch('participants', queryset=speakers, to_attr='speakers'),
         Prefetch('tags', to_attr='prefetched_tags', queryset=TagItem.objects.select_related('tag').filter(tag__is_public=True)),