diff --git a/program/services.py b/program/services.py
index 038f3089355ef1c27a12053364a1d317225fa423..df3a45f27591081083974005778f41fec15a44a4 100644
--- a/program/services.py
+++ b/program/services.py
@@ -19,7 +19,8 @@
 
 import copy
 from datetime import datetime, time, timedelta
-from typing import TypedDict
+from itertools import pairwise
+from typing import Literal, TypedDict
 
 from dateutil.relativedelta import relativedelta
 from dateutil.rrule import rrule
@@ -30,7 +31,15 @@ from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q, QuerySet
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
-from program.models import Note, RRule, Schedule, ScheduleConflictError, Show, TimeSlot
+from program.models import (
+    Note,
+    RadioSettings,
+    RRule,
+    Schedule,
+    ScheduleConflictError,
+    Show,
+    TimeSlot,
+)
 from program.serializers import ScheduleSerializer, TimeSlotSerializer
 from program.utils import parse_date, parse_datetime, parse_time
 
@@ -86,6 +95,36 @@ class ScheduleCreateUpdateData(TypedDict):
     solutions: dict[str, str]
 
 
+class ScheduleEntry(TypedDict):
+    end: str
+    is_virtual: bool
+    show_id: int
+    start: str
+    title: str
+
+
+class TimeslotEntry(TypedDict):
+    end: str
+    id: int
+    is_virtual: Literal[False]
+    playlist_id: int | None
+    repetition_of_id: int | None
+    schedule_default_playlist_id: int | None
+    schedule_id: int
+    show_default_playlist_id: int | None
+    show_id: int
+    start: str
+    title: str
+
+
+class VirtualTimeslotEntry(TypedDict):
+    end: str
+    is_virtual: Literal[True]
+    show_id: int
+    start: str
+    title: str
+
+
 def create_timeslot(start: str, end: str, schedule: Schedule) -> TimeSlot:
     """Creates and returns an unsaved timeslot with the given `start`, `end` and `schedule`."""
 
@@ -740,14 +779,93 @@ def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts:
     return conflicts
 
 
-def get_timerange_timeslots(start: datetime, end: datetime) -> QuerySet[TimeSlot]:
-    """Gets a queryset of timeslots between the given `start` and `end` datetime."""
+def make_schedule_entry(*, timeslot_entry: TimeslotEntry) -> ScheduleEntry:
+    """returns a schedule entry for the given timeslot entry."""
+
+    return {
+        "end": timeslot_entry["end"],
+        "show_id": timeslot_entry["show_id"],
+        "is_virtual": timeslot_entry["is_virtual"],
+        "start": timeslot_entry["start"],
+        "title": timeslot_entry["title"],
+    }
+
+
+def make_timeslot_entry(*, timeslot: TimeSlot) -> TimeslotEntry:
+    """returns a timeslot entry for the given timeslot."""
+
+    schedule = timeslot.schedule
+    show = timeslot.schedule.show
+
+    return {
+        "end": timeslot.end.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "id": timeslot.id,
+        "is_virtual": False,
+        "playlist_id": timeslot.playlist_id,
+        # 'timeslot.repetition_of` is a foreign key that can be null
+        "repetition_of_id": timeslot.repetition_of.id if timeslot.repetition_of else None,
+        "schedule_default_playlist_id": schedule.default_playlist_id,
+        "schedule_id": schedule.id,
+        "show_default_playlist_id": show.default_playlist_id,
+        "show_id": show.id,
+        "start": timeslot.start.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "title": f"{show.name} {_('REP')}" if schedule.is_repetition else show.name,
+    }
+
+
+def make_virtual_timeslot_entry(*, gap_start: datetime, gap_end: datetime) -> VirtualTimeslotEntry:
+    """returns a virtual timeslot entry to fill the gap in between `gap_start` and `gap_end`."""
+
+    return {
+        "end": gap_end.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "is_virtual": True,
+        "show_id": RadioSettings.objects.first().fallback_show.id,
+        "start": gap_start.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "title": RadioSettings.objects.first().fallback_default_pool,
+    }
+
+
+def get_timerange_timeslot_entries(
+    timerange_start: datetime, timerange_end: datetime, include_virtual: bool = False
+) -> list[TimeslotEntry | VirtualTimeslotEntry]:
+    """Gets list of timeslot entries between the given `timerange_start` and `timerange_end`.
+
+    Include virtual timeslots if requested."""
+
+    timeslots = TimeSlot.objects.filter(
+        # start before `timerange_start` and end after `timerange_start`
+        Q(start__lt=timerange_start, end__gt=timerange_start)
+        # start after/at `timerange_start`, end before/at `timerange_end`
+        | Q(start__gte=timerange_start, end__lte=timerange_end)
+        # start before `timerange_end`, end after/at `timerange_end`
+        | Q(start__lt=timerange_end, end__gte=timerange_end)
+    ).select_related("schedule")
+
+    if not include_virtual:
+        return [make_timeslot_entry(timeslot=timeslot) for timeslot in timeslots]
+
+    timeslot_entries = []
+    # gap before the first timeslot
+    first_timeslot = timeslots.first()
+    if first_timeslot.start > timerange_start:
+        timeslot_entries.append(
+            make_virtual_timeslot_entry(gap_start=timerange_start, gap_end=first_timeslot.start)
+        )
 
-    return TimeSlot.objects.filter(
-        # start before `start` and end after `start`
-        Q(start__lt=start, end__gt=start)
-        # start after/at `start`, end before/at `end`
-        | Q(start__gte=start, end__lte=end)
-        # start before `end`, end after/at `end`
-        | Q(start__lt=end, end__gte=end)
-    )
+    for index, (current, upcoming) in enumerate(pairwise(timeslots)):
+        timeslot_entries.append(make_timeslot_entry(timeslot=current))
+
+        # gap between the timeslots
+        if current.end != upcoming.start:
+            timeslot_entries.append(
+                make_virtual_timeslot_entry(gap_start=current.end, gap_end=upcoming.start)
+            )
+
+    # gap after the last timeslot
+    last_timeslot = timeslots.last()
+    if last_timeslot.end < timerange_end:
+        timeslot_entries.append(
+            make_virtual_timeslot_entry(gap_start=last_timeslot.end, gap_end=timerange_end)
+        )
+
+    return timeslot_entries
diff --git a/program/views.py b/program/views.py
index b70e0d78c41c7103bdcb2ca424ba5b0bc5ff606e..ecfa6794e3e5ef0e7998e4b4d724330fa56a8a2a 100644
--- a/program/views.py
+++ b/program/views.py
@@ -20,7 +20,6 @@
 
 import logging
 from datetime import date, datetime, time, timedelta
-from itertools import pairwise
 from textwrap import dedent
 
 from django_filters.rest_framework import DjangoFilterBackend
@@ -43,7 +42,6 @@ from django.db import IntegrityError
 from django.http import HttpResponseRedirect, JsonResponse
 from django.shortcuts import get_object_or_404
 from django.utils import timezone
-from django.utils.translation import gettext as _
 from program import filters
 from program.models import (
     Category,
@@ -89,59 +87,16 @@ from program.serializers import (
     TypeSerializer,
     UserSerializer,
 )
-from program.services import get_timerange_timeslots, resolve_conflicts
+from program.services import (
+    get_timerange_timeslot_entries,
+    make_schedule_entry,
+    resolve_conflicts,
+)
 from program.utils import get_values, parse_date
 
 logger = logging.getLogger(__name__)
 
 
-def timeslot_entry(*, timeslot: TimeSlot) -> dict:
-    """return a timeslot entry as a dict"""
-
-    schedule = timeslot.schedule
-    show = timeslot.schedule.show
-    playlist_id = timeslot.playlist_id
-
-    title = show_name = f"{show.name} {_('REP')}" if schedule.is_repetition else show.name
-    # we start and end as timezone naive datetime objects
-    start = timezone.make_naive(timeslot.start).strftime("%Y-%m-%dT%H:%M:%S")
-    end = timezone.make_naive(timeslot.end).strftime("%Y-%m-%dT%H:%M:%S")
-
-    return {
-        "end": end,
-        "id": timeslot.id,
-        "playlistId": playlist_id,
-        # `Timeslot.repetition_of` is a foreign key that can be null
-        "repetitionOfId": timeslot.repetition_of.id if timeslot.repetition_of else None,
-        "scheduleDefaultPlaylistId": schedule.default_playlist_id,
-        "scheduleId": schedule.id,
-        "showCategories": ", ".join(show.category.values_list("name", flat=True)),
-        "showDefaultPlaylistId": show.default_playlist_id,
-        # `Show.funding_category` is a foreign key can be null
-        "showFundingCategory": show.funding_category.name if show.funding_category_id else "",
-        "showHosts": ", ".join(show.hosts.values_list("name", flat=True)),
-        "showId": show.id,
-        "showLanguages": ", ".join(show.language.values_list("name", flat=True)),
-        "showMusicFocus": ", ".join(show.music_focus.values_list("name", flat=True)),
-        "showName": show_name,
-        "showTopics": ", ".join(show.topic.values_list("name", flat=True)),
-        # `Show.type` is a foreign key that can be null
-        "showType": show.type.name if show.type_id else "",
-        "start": start,
-        "title": title,
-    }
-
-
-def gap_entry(*, gap_start: datetime, gap_end: datetime) -> dict:
-    """return a virtual timeslot to fill the gap in between `gap_start` and `gap_end` as a dict"""
-
-    return {
-        "end": gap_end.strftime("%Y-%m-%dT%H:%M:%S"),
-        "start": gap_start.strftime("%Y-%m-%dT%H:%M:%S"),
-        "virtual": True,
-    }
-
-
 @extend_schema_view(
     list=extend_schema(
         summary="List schedule for a specific date.",
@@ -168,18 +123,12 @@ class APIDayScheduleViewSet(
 
         end = start + timedelta(hours=24)
 
-        timeslots = get_timerange_timeslots(start, end).select_related("schedule")
-        schedule = []
-
-        for ts in timeslots:
-            entry = {
-                "start": ts.start.strftime("%Y-%m-%d_%H:%M:%S"),
-                "end": ts.end.strftime("%Y-%m-%d_%H:%M:%S"),
-                "title": ts.schedule.show.name,
-                "id": ts.schedule.show.id,
-            }
+        include_virtual = request.GET.get("include_virtual") == "true"
 
-            schedule.append(entry)
+        schedule = [
+            make_schedule_entry(timeslot_entry=timeslot_entry)
+            for timeslot_entry in get_timerange_timeslot_entries(start, end, include_virtual)
+        ]
 
         return JsonResponse(schedule, safe=False)
 
@@ -229,33 +178,9 @@ class APIPlayoutViewSet(
 
         include_virtual = request.GET.get("include_virtual") == "true"
 
-        timeslots = get_timerange_timeslots(schedule_start, schedule_end).select_related(
-            "schedule"
-        )
-
-        schedule = []
-
-        first_timeslot = timeslots.first()
+        playout = get_timerange_timeslot_entries(schedule_start, schedule_end, include_virtual)
 
-        if include_virtual and first_timeslot.start > schedule_start:
-            schedule.append(gap_entry(gap_start=schedule_start, gap_end=first_timeslot.start))
-
-        for current, upcoming in pairwise(timeslots):
-            schedule.append(timeslot_entry(timeslot=current))
-
-            if include_virtual and current.end != upcoming.start:
-                schedule.append(gap_entry(gap_start=current.end, gap_end=upcoming.start))
-
-        last_timeslot = timeslots.last()
-
-        # we need to append the last timeslot to the schedule to complete it
-        if last_timeslot:
-            schedule.append(timeslot_entry(timeslot=last_timeslot))
-
-        if include_virtual and last_timeslot.end < schedule_end:
-            schedule.append(gap_entry(gap_start=last_timeslot.end, gap_end=schedule_end))
-
-        return JsonResponse(schedule, safe=False)
+        return JsonResponse(playout, safe=False)
 
 
 @extend_schema_view(