From 57046e2a9d5988881926dade9a480495e0e27f2d Mon Sep 17 00:00:00 2001 From: jo Date: Sun, 17 Jul 2022 22:27:57 +0200 Subject: [PATCH] chore(api): rename schedule models fields --- api/libretime_api/schedule/models/schedule.py | 85 ++++--- .../schedule/serializers/schedule.py | 21 +- .../schedule/tests/models/test_schedule.py | 32 +-- .../schedule/tests/views/test_schedule.py | 57 ++--- api/libretime_api/schedule/views/schedule.py | 55 ++--- api/schema.yml | 214 ++++++------------ playout/libretime_playout/schedule.py | 26 ++- playout/tests/schedule_test.py | 68 +----- 8 files changed, 224 insertions(+), 334 deletions(-) diff --git a/api/libretime_api/schedule/models/schedule.py b/api/libretime_api/schedule/models/schedule.py index f7b6880dc..62d41b36c 100644 --- a/api/libretime_api/schedule/models/schedule.py +++ b/api/libretime_api/schedule/models/schedule.py @@ -2,8 +2,14 @@ from django.db import models class Schedule(models.Model): - starts = models.DateTimeField() - ends = models.DateTimeField() + starts_at = models.DateTimeField(db_column="starts") + ends_at = models.DateTimeField(db_column="ends") + + instance = models.ForeignKey( + "schedule.ShowInstance", + on_delete=models.DO_NOTHING, + ) + file = models.ForeignKey( "storage.File", on_delete=models.DO_NOTHING, @@ -16,31 +22,60 @@ class Schedule(models.Model): blank=True, null=True, ) - clip_length = models.DurationField(blank=True, null=True) + + length = models.DurationField(blank=True, null=True, db_column="clip_length") fade_in = models.TimeField(blank=True, null=True) fade_out = models.TimeField(blank=True, null=True) cue_in = models.DurationField() cue_out = models.DurationField() - media_item_played = models.BooleanField(blank=True, null=True) - instance = models.ForeignKey("schedule.ShowInstance", on_delete=models.DO_NOTHING) - playout_status = models.SmallIntegerField() - broadcasted = models.SmallIntegerField() + + class PositionStatus(models.IntegerChoices): + FILLER = -1, "Filler" # Used to fill a show that already started + OUTSIDE = 0, "Outside" # Is outside the show time frame + INSIDE = 1, "Inside" # Is inside the show time frame + BOUNDARY = 2, "Boundary" # Is at the boundary of the show time frame + position = models.IntegerField() + position_status = models.SmallIntegerField( + choices=PositionStatus.choices, + default=PositionStatus.INSIDE, + db_column="playout_status", + ) + + # Broadcasted is set to 1 when a live source is not + # on. Used for the playout history. + broadcasted = models.SmallIntegerField() + played = models.BooleanField( + blank=True, + null=True, + db_column="media_item_played", + ) + + @property + def overbooked(self): + """ + A schedule item is overbooked if it starts after the end of the show + instance it is in. + + Related to self.position_status + """ + return self.starts_at >= self.instance.ends_at def get_owner(self): return self.instance.get_owner() - def get_cueout(self): + def get_cue_out(self): """ - Returns a scheduled item cueout that is based on the current show instance. + Returns a scheduled item cue out that is based on the current show + instance. - Cueout of a specific item can potentially overrun the show that it is - scheduled in. In that case, the cueout should be the end of the show. + Cue out of a specific item can potentially overrun the show that it is + scheduled in. In that case, the cue out should be the end of the show. This prevents the next show having overlapping items playing. Cases: - When the schedule ends before the end of the show instance, - return the stored cueout. + return the stored cue out. - When the schedule starts before the end of the show instance and ends after the show instance ends, @@ -49,13 +84,17 @@ class Schedule(models.Model): - When the schedule starts after the end of the show instance, return the stored cue_out even if the schedule WILL NOT BE PLAYED. """ - if self.starts < self.instance.ends and self.instance.ends < self.ends: - return self.instance.ends - self.starts + if ( + self.starts_at < self.instance.ends_at + and self.instance.ends_at < self.ends_at + ): + return self.instance.ends_at - self.starts_at return self.cue_out - def get_ends(self): + def get_ends_at(self): """ - Returns a scheduled item ends that is based on the current show instance. + Returns a scheduled item ends that is based on the current show + instance. End of a specific item can potentially overrun the show that it is scheduled in. In that case, the end should be the end of the show. @@ -72,17 +111,9 @@ class Schedule(models.Model): - When the schedule starts after the end of the show instance, return the show instance ends. """ - if self.instance.ends < self.ends: - return self.instance.ends - return self.ends - - @property - def is_valid(self): - """ - A schedule item is valid if it starts before the end of the show instance - it is in - """ - return self.starts < self.instance.ends + if self.instance.ends_at < self.ends_at: + return self.instance.ends_at + return self.ends_at class Meta: managed = False diff --git a/api/libretime_api/schedule/serializers/schedule.py b/api/libretime_api/schedule/serializers/schedule.py index ae0996230..967424c0c 100644 --- a/api/libretime_api/schedule/serializers/schedule.py +++ b/api/libretime_api/schedule/serializers/schedule.py @@ -7,29 +7,30 @@ class ScheduleSerializer(serializers.HyperlinkedModelSerializer): file_id = serializers.IntegerField(source="file.id", read_only=True) stream_id = serializers.IntegerField(source="stream.id", read_only=True) instance_id = serializers.IntegerField(source="instance.id", read_only=True) - cue_out = serializers.DurationField(source="get_cueout", read_only=True) - ends = serializers.DateTimeField(source="get_ends", read_only=True) + cue_out = serializers.DurationField(source="get_cue_out", read_only=True) + ends_at = serializers.DateTimeField(source="get_ends_at", read_only=True) class Meta: model = Schedule fields = [ "item_url", "id", - "starts", - "ends", + "starts_at", + "ends_at", + "instance", + "instance_id", "file", "file_id", "stream", "stream_id", - "clip_length", + "length", "fade_in", "fade_out", "cue_in", "cue_out", - "media_item_played", - "instance", - "instance_id", - "playout_status", - "broadcasted", "position", + "position_status", + "broadcasted", + "played", + "overbooked", ] diff --git a/api/libretime_api/schedule/tests/models/test_schedule.py b/api/libretime_api/schedule/tests/models/test_schedule.py index 4430a2232..51bba7aa9 100644 --- a/api/libretime_api/schedule/tests/models/test_schedule.py +++ b/api/libretime_api/schedule/tests/models/test_schedule.py @@ -9,49 +9,49 @@ class TestSchedule(TestCase): @classmethod def setUpTestData(cls): cls.show_instance = ShowInstance( - created=datetime(year=2021, month=10, day=1, hour=12), - starts=datetime(year=2021, month=10, day=2, hour=1), - ends=datetime(year=2021, month=10, day=2, hour=2), + created_at=datetime(year=2021, month=10, day=1, hour=12), + starts_at=datetime(year=2021, month=10, day=2, hour=1), + ends_at=datetime(year=2021, month=10, day=2, hour=2), ) cls.length = timedelta(minutes=10) cls.cue_in = timedelta(seconds=1) cls.cue_out = cls.length - timedelta(seconds=4) - def create_schedule(self, starts): + def create_schedule(self, starts_at): return Schedule( - starts=starts, - ends=starts + self.length, + starts_at=starts_at, + ends_at=starts_at + self.length, cue_in=self.cue_in, cue_out=self.cue_out, instance=self.show_instance, ) - def test_get_cueout(self): + def test_get_cue_out(self): # No overlapping schedule datetimes, normal usecase: item1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30) item1 = self.create_schedule(item1_starts) - self.assertEqual(item1.get_cueout(), self.cue_out) - self.assertEqual(item1.get_ends(), item1_starts + self.length) + self.assertEqual(item1.get_cue_out(), self.cue_out) + self.assertEqual(item1.get_ends_at(), item1_starts + self.length) # Mixed overlapping schedule datetimes (only ends is overlapping): item_2_starts = datetime(year=2021, month=10, day=2, hour=1, minute=55) item_2 = self.create_schedule(item_2_starts) - self.assertEqual(item_2.get_cueout(), timedelta(minutes=5)) - self.assertEqual(item_2.get_ends(), self.show_instance.ends) + self.assertEqual(item_2.get_cue_out(), timedelta(minutes=5)) + self.assertEqual(item_2.get_ends_at(), self.show_instance.ends_at) # Fully overlapping schedule datetimes (starts and ends are overlapping): item3_starts = datetime(year=2021, month=10, day=2, hour=2, minute=1) item3 = self.create_schedule(item3_starts) - self.assertEqual(item3.get_cueout(), self.cue_out) - self.assertEqual(item3.get_ends(), self.show_instance.ends) + self.assertEqual(item3.get_cue_out(), self.cue_out) + self.assertEqual(item3.get_ends_at(), self.show_instance.ends_at) - def test_is_valid(self): + def test_overbooked(self): # Starts before the schedule ends item1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30) item1 = self.create_schedule(item1_starts) - self.assertTrue(item1.is_valid) + self.assertFalse(item1.overbooked) # Starts after the schedule ends item_2_starts = datetime(year=2021, month=10, day=2, hour=3) item_2 = self.create_schedule(item_2_starts) - self.assertFalse(item_2.is_valid) + self.assertTrue(item_2.overbooked) diff --git a/api/libretime_api/schedule/tests/views/test_schedule.py b/api/libretime_api/schedule/tests/views/test_schedule.py index 559b47d6c..3522491a2 100644 --- a/api/libretime_api/schedule/tests/views/test_schedule.py +++ b/api/libretime_api/schedule/tests/views/test_schedule.py @@ -25,13 +25,13 @@ class TestScheduleViewSet(APITestCase): ) show = baker.make( "schedule.ShowInstance", - starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), - ends=datetime.now(tz=timezone.utc) + timedelta(minutes=5), + starts_at=datetime.now(tz=timezone.utc) - timedelta(minutes=5), + ends_at=datetime.now(tz=timezone.utc) + timedelta(minutes=5), ) schedule_item = baker.make( "schedule.Schedule", - starts=datetime.now(tz=timezone.utc), - ends=datetime.now(tz=timezone.utc) + file.length, + starts_at=datetime.now(tz=timezone.utc), + ends_at=datetime.now(tz=timezone.utc) + file.length, cue_out=file.cue_out, instance=show, file=file, @@ -41,7 +41,7 @@ class TestScheduleViewSet(APITestCase): self.assertEqual(response.status_code, 200) result = response.json() self.assertEqual( - dateparse.parse_datetime(result[0]["ends"]), schedule_item.ends + dateparse.parse_datetime(result[0]["ends_at"]), schedule_item.ends_at ) self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), file.cue_out) @@ -56,13 +56,13 @@ class TestScheduleViewSet(APITestCase): ) show = baker.make( "schedule.ShowInstance", - starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), - ends=datetime.now(tz=timezone.utc) + timedelta(seconds=20), + starts_at=datetime.now(tz=timezone.utc) - timedelta(minutes=5), + ends_at=datetime.now(tz=timezone.utc) + timedelta(seconds=20), ) schedule_item = baker.make( "schedule.Schedule", - starts=datetime.now(tz=timezone.utc), - ends=datetime.now(tz=timezone.utc) + file.length, + starts_at=datetime.now(tz=timezone.utc), + ends_at=datetime.now(tz=timezone.utc) + file.length, instance=show, file=file, ) @@ -70,11 +70,11 @@ class TestScheduleViewSet(APITestCase): response = self.client.get(self.path) self.assertEqual(response.status_code, 200) result = response.json() - self.assertEqual(dateparse.parse_datetime(result[0]["ends"]), show.ends) - expected = show.ends - schedule_item.starts + self.assertEqual(dateparse.parse_datetime(result[0]["ends_at"]), show.ends_at) + expected = show.ends_at - schedule_item.starts_at self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), expected) self.assertNotEqual( - dateparse.parse_datetime(result[0]["ends"]), schedule_item.ends + dateparse.parse_datetime(result[0]["ends_at"]), schedule_item.ends_at ) def test_schedule_item_invalid(self): @@ -88,33 +88,33 @@ class TestScheduleViewSet(APITestCase): ) show = baker.make( "schedule.ShowInstance", - starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), - ends=datetime.now(tz=timezone.utc) + timedelta(minutes=5), + starts_at=datetime.now(tz=timezone.utc) - timedelta(minutes=5), + ends_at=datetime.now(tz=timezone.utc) + timedelta(minutes=5), ) schedule_item = baker.make( "schedule.Schedule", - starts=datetime.now(tz=timezone.utc), - ends=datetime.now(tz=timezone.utc) + file.length, + starts_at=datetime.now(tz=timezone.utc), + ends_at=datetime.now(tz=timezone.utc) + file.length, cue_out=file.cue_out, instance=show, file=file, ) invalid_schedule_item = baker.make( # pylint: disable=unused-variable "schedule.Schedule", - starts=show.ends + timedelta(minutes=1), - ends=show.ends + timedelta(minutes=1) + file.length, + starts_at=show.ends_at + timedelta(minutes=1), + ends_at=show.ends_at + timedelta(minutes=1) + file.length, cue_out=file.cue_out, instance=show, file=file, ) self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") - response = self.client.get(self.path, {"is_valid": True}) + response = self.client.get(self.path, {"overbooked": False}) self.assertEqual(response.status_code, 200) result = response.json() # The invalid item should be filtered out and not returned self.assertEqual(len(result), 1) self.assertEqual( - dateparse.parse_datetime(result[0]["ends"]), schedule_item.ends + dateparse.parse_datetime(result[0]["ends_at"]), schedule_item.ends_at ) self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), file.cue_out) @@ -131,21 +131,21 @@ class TestScheduleViewSet(APITestCase): show = baker.make( "schedule.ShowInstance", - starts=filter_point - timedelta(minutes=5), - ends=filter_point + timedelta(minutes=5), + starts_at=filter_point - timedelta(minutes=5), + ends_at=filter_point + timedelta(minutes=5), ) schedule_item = baker.make( "schedule.Schedule", - starts=filter_point, - ends=filter_point + file.length, + starts_at=filter_point, + ends_at=filter_point + file.length, cue_out=file.cue_out, instance=show, file=file, ) previous_item = baker.make( # pylint: disable=unused-variable "schedule.Schedule", - starts=filter_point - timedelta(minutes=5), - ends=filter_point - timedelta(minutes=5) + file.length, + starts_at=filter_point - timedelta(minutes=5), + ends_at=filter_point - timedelta(minutes=5) + file.length, cue_out=file.cue_out, instance=show, file=file, @@ -156,12 +156,13 @@ class TestScheduleViewSet(APITestCase): ) range_end = (filter_point + timedelta(minutes=1)).isoformat(timespec="seconds") response = self.client.get( - self.path, {"starts__range": f"{range_start},{range_end}"} + self.path, + {"starts_after": range_start, "starts_before": range_end}, ) self.assertEqual(response.status_code, 200) result = response.json() # The previous_item should be filtered out and not returned self.assertEqual(len(result), 1) self.assertEqual( - dateparse.parse_datetime(result[0]["starts"]), schedule_item.starts + dateparse.parse_datetime(result[0]["starts_at"]), schedule_item.starts_at ) diff --git a/api/libretime_api/schedule/views/schedule.py b/api/libretime_api/schedule/views/schedule.py index 020fd9780..e933424b5 100644 --- a/api/libretime_api/schedule/views/schedule.py +++ b/api/libretime_api/schedule/views/schedule.py @@ -1,42 +1,33 @@ -from django.db.models import F -from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view +from django.db import models +from django_filters import rest_framework as filters from rest_framework import viewsets -from ..._constants import FILTER_NUMERICAL_LOOKUPS from ..models import Schedule from ..serializers import ScheduleSerializer -@extend_schema_view( - list=extend_schema( - parameters=[ - OpenApiParameter( - name="is_valid", - description="Filter on valid instances", - required=False, - type=bool, - ), - ] - ) -) +class ScheduleFilter(filters.FilterSet): + starts = filters.DateTimeFromToRangeFilter(field_name="starts_at") + ends = filters.DateTimeFromToRangeFilter(field_name="ends_at") + position_status = filters.NumberFilter() + broadcasted = filters.NumberFilter() + + overbooked = filters.BooleanFilter(method="overbooked_filter") + + # pylint: disable=unused-argument + def overbooked_filter(self, queryset, name, value): + # TODO: deduplicate code using the overbooked property + if value: + return queryset.filter(starts_at__gte=models.F("instance__ends_at")) + return queryset.filter(starts_at__lt=models.F("instance__ends_at")) + + class Meta: + model = Schedule + fields = [] # type: ignore + + class ScheduleViewSet(viewsets.ModelViewSet): queryset = Schedule.objects.all() serializer_class = ScheduleSerializer - filterset_fields = { - "starts": FILTER_NUMERICAL_LOOKUPS, - "ends": FILTER_NUMERICAL_LOOKUPS, - "playout_status": FILTER_NUMERICAL_LOOKUPS, - "broadcasted": FILTER_NUMERICAL_LOOKUPS, - } + filterset_class = ScheduleFilter model_permission_name = "schedule" - - def get_queryset(self): - filter_valid = self.request.query_params.get("is_valid") - if filter_valid is None: - return self.queryset.all() - - filter_valid = filter_valid.strip().lower() in ("true", "yes", "1") - if filter_valid: - return self.queryset.filter(starts__lt=F("instance__ends")) - - return self.queryset.filter(starts__gte=F("instance__ends")) diff --git a/api/schema.yml b/api/schema.yml index d74d40767..63bbcf85d 100644 --- a/api/schema.yml +++ b/api/schema.yml @@ -2540,134 +2540,33 @@ paths: schema: type: integer - in: query - name: broadcasted__gt - schema: - type: integer - - in: query - name: broadcasted__gte - schema: - type: integer - - in: query - name: broadcasted__lt - schema: - type: integer - - in: query - name: broadcasted__lte - schema: - type: integer - - in: query - name: broadcasted__range - schema: - type: array - items: - type: integer - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: ends + name: ends_after schema: type: string format: date-time - in: query - name: ends__gt + name: ends_before schema: type: string format: date-time - in: query - name: ends__gte - schema: - type: string - format: date-time - - in: query - name: ends__lt - schema: - type: string - format: date-time - - in: query - name: ends__lte - schema: - type: string - format: date-time - - in: query - name: ends__range - schema: - type: array - items: - type: string - format: date-time - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: is_valid + name: overbooked schema: type: boolean - description: Filter on valid instances - in: query - name: playout_status + name: position_status schema: type: integer - in: query - name: playout_status__gt - schema: - type: integer - - in: query - name: playout_status__gte - schema: - type: integer - - in: query - name: playout_status__lt - schema: - type: integer - - in: query - name: playout_status__lte - schema: - type: integer - - in: query - name: playout_status__range - schema: - type: array - items: - type: integer - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: starts + name: starts_after schema: type: string format: date-time - in: query - name: starts__gt + name: starts_before schema: type: string format: date-time - - in: query - name: starts__gte - schema: - type: string - format: date-time - - in: query - name: starts__lt - schema: - type: string - format: date-time - - in: query - name: starts__lte - schema: - type: string - format: date-time - - in: query - name: starts__range - schema: - type: array - items: - type: string - format: date-time - description: Multiple values may be separated by commas. - explode: false - style: form tags: - schedule security: @@ -6553,13 +6452,19 @@ components: id: type: integer readOnly: true - starts: + starts_at: type: string format: date-time - ends: + ends_at: type: string format: date-time readOnly: true + instance: + type: string + format: uri + instance_id: + type: integer + readOnly: true file: type: string format: uri @@ -6574,7 +6479,7 @@ components: stream_id: type: integer readOnly: true - clip_length: + length: type: string nullable: true fade_in: @@ -6590,27 +6495,25 @@ components: cue_out: type: string readOnly: true - media_item_played: - type: boolean - nullable: true - instance: - type: string - format: uri - instance_id: - type: integer - readOnly: true - playout_status: - type: integer - maximum: 32767 - minimum: -32768 - broadcasted: - type: integer - maximum: 32767 - minimum: -32768 position: type: integer maximum: 2147483647 minimum: -2147483648 + position_status: + allOf: + - $ref: "#/components/schemas/PositionStatusEnum" + minimum: -32768 + maximum: 32767 + broadcasted: + type: integer + maximum: 32767 + minimum: -32768 + played: + type: boolean + nullable: true + overbooked: + type: string + readOnly: true PatchedServiceRegister: type: object properties: @@ -7381,6 +7284,13 @@ components: - item_url - podcast - published_at + PositionStatusEnum: + enum: + - -1 + - 0 + - 1 + - 2 + type: integer Preference: type: object properties: @@ -7433,13 +7343,19 @@ components: id: type: integer readOnly: true - starts: + starts_at: type: string format: date-time - ends: + ends_at: type: string format: date-time readOnly: true + instance: + type: string + format: uri + instance_id: + type: integer + readOnly: true file: type: string format: uri @@ -7454,7 +7370,7 @@ components: stream_id: type: integer readOnly: true - clip_length: + length: type: string nullable: true fade_in: @@ -7470,40 +7386,38 @@ components: cue_out: type: string readOnly: true - media_item_played: - type: boolean - nullable: true - instance: - type: string - format: uri - instance_id: - type: integer - readOnly: true - playout_status: - type: integer - maximum: 32767 - minimum: -32768 - broadcasted: - type: integer - maximum: 32767 - minimum: -32768 position: type: integer maximum: 2147483647 minimum: -2147483648 + position_status: + allOf: + - $ref: "#/components/schemas/PositionStatusEnum" + minimum: -32768 + maximum: 32767 + broadcasted: + type: integer + maximum: 32767 + minimum: -32768 + played: + type: boolean + nullable: true + overbooked: + type: string + readOnly: true required: - broadcasted - cue_in - cue_out - - ends + - ends_at - file_id - id - instance - instance_id - item_url - - playout_status + - overbooked - position - - starts + - starts_at - stream_id ServiceRegister: type: object diff --git a/playout/libretime_playout/schedule.py b/playout/libretime_playout/schedule.py index 83670e8b9..c1dcf6094 100644 --- a/playout/libretime_playout/schedule.py +++ b/playout/libretime_playout/schedule.py @@ -27,16 +27,17 @@ def get_schedule(api_client: ApiClient): schedule = api_client.services.schedule_url( params={ - "ends__range": (f"{current_time_str}Z,{end_time_str}Z"), - "is_valid": True, - "playout_status__gt": 0, + "ends_after": f"{current_time_str}Z", + "ends_before": f"{end_time_str}Z", + "overbooked": False, + "position_status__gt": 0, } ) events = {} for item in schedule: - item["starts"] = isoparse(item["starts"]) - item["ends"] = isoparse(item["ends"]) + item["starts_at"] = isoparse(item["starts_at"]) + item["ends_at"] = isoparse(item["ends_at"]) show_instance = api_client.services.show_instance_url(id=item["instance_id"]) show = api_client.services.show_url(id=show_instance["show_id"]) @@ -62,8 +63,8 @@ def generate_file_events( """ events = {} - schedule_start_event_key = datetime_to_event_key(schedule["starts"]) - schedule_end_event_key = datetime_to_event_key(schedule["ends"]) + schedule_start_event_key = datetime_to_event_key(schedule["starts_at"]) + schedule_end_event_key = datetime_to_event_key(schedule["ends_at"]) events[schedule_start_event_key] = { "type": EventKind.FILE, @@ -102,15 +103,15 @@ def generate_webstream_events( """ events = {} - schedule_start_event_key = datetime_to_event_key(schedule["starts"]) - schedule_end_event_key = datetime_to_event_key(schedule["ends"]) + schedule_start_event_key = datetime_to_event_key(schedule["starts_at"]) + schedule_end_event_key = datetime_to_event_key(schedule["ends_at"]) events[schedule_start_event_key] = { "type": EventKind.STREAM_BUFFER_START, "independent_event": True, "row_id": schedule["id"], - "start": datetime_to_event_key(schedule["starts"] - timedelta(seconds=5)), - "end": datetime_to_event_key(schedule["starts"] - timedelta(seconds=5)), + "start": datetime_to_event_key(schedule["starts_at"] - timedelta(seconds=5)), + "end": datetime_to_event_key(schedule["starts_at"] - timedelta(seconds=5)), "uri": webstream["url"], "id": webstream["id"], } @@ -127,7 +128,8 @@ def generate_webstream_events( "show_name": show["name"], } - # NOTE: stream_*_end were previously triggerered 1 second before the schedule end. + # NOTE: stream_*_end were previously triggered 1 second before + # the schedule end. events[schedule_end_event_key] = { "type": EventKind.STREAM_BUFFER_END, "independent_event": True, diff --git a/playout/tests/schedule_test.py b/playout/tests/schedule_test.py index bea84c6ad..c27a85b2d 100644 --- a/playout/tests/schedule_test.py +++ b/playout/tests/schedule_test.py @@ -3,92 +3,50 @@ from libretime_playout.schedule import get_schedule class ApiClientServicesMock: @staticmethod - def schedule_url(_post_data=None, params=None, **kwargs): + def schedule_url(*args, **kwargs): return [ { - "item_url": "http://192.168.10.100:8081/api/v2/schedule/17/", "id": 17, - "starts": "2022-03-04T15:30:00Z", - "ends": "2022-03-04T15:33:50.674340Z", + "starts_at": "2022-03-04T15:30:00Z", + "ends_at": "2022-03-04T15:33:50.674340Z", "file": "http://192.168.10.100:8081/api/v2/files/1/", "file_id": 1, "stream": None, - "clip_length": "00:03:50.674340", "fade_in": "00:00:00.500000", "fade_out": "00:00:00.500000", "cue_in": "00:00:01.310660", "cue_out": "00:03:51.985000", - "media_item_played": False, - "instance": "http://192.168.10.100:8081/api/v2/show-instances/3/", "instance_id": 3, - "playout_status": 1, - "broadcasted": 0, - "position": 0, }, { - "item_url": "http://192.168.10.100:8081/api/v2/schedule/18/", "id": 18, - "starts": "2022-03-04T15:33:50.674340Z", - "ends": "2022-03-04T16:03:50.674340Z", + "starts_at": "2022-03-04T15:33:50.674340Z", + "ends_at": "2022-03-04T16:03:50.674340Z", "file": None, "stream": "http://192.168.10.100:8081/api/v2/webstreams/1/", "stream_id": 1, - "clip_length": "00:30:00", "fade_in": "00:00:00.500000", "fade_out": "00:00:00.500000", "cue_in": "00:00:00", "cue_out": "00:30:00", - "media_item_played": False, - "instance": "http://192.168.10.100:8081/api/v2/show-instances/3/", "instance_id": 3, - "playout_status": 1, - "broadcasted": 0, - "position": 1, }, ] @staticmethod - def show_instance_url(_post_data=None, params=None, **kwargs): + def show_instance_url(*args, **kwargs): return { - "item_url": "http://192.168.10.100:8081/api/v2/show-instances/3/", - "id": 3, - "description": "", - "starts": "2022-03-04T15:30:00Z", - "ends": "2022-03-04T16:30:00Z", - "record": 0, - "rebroadcast": 0, - "time_filled": "00:33:50.674340", - "created": "2022-03-04T15:05:36Z", - "last_scheduled": "2022-03-04T15:05:46Z", - "modified_instance": False, - "autoplaylist_built": False, - "show": "http://192.168.10.100:8081/api/v2/shows/3/", "show_id": 3, - "instance": None, - "file": None, } @staticmethod - def show_url(_post_data=None, params=None, **kwargs): + def show_url(*args, **kwargs): return { - "item_url": "http://192.168.10.100:8081/api/v2/shows/3/", - "id": 3, "name": "Test", - "url": "", - "genre": "", - "description": "", - "color": "", - "background_color": "", - "linked": False, - "is_linkable": True, - "image_path": "", - "has_autoplaylist": False, - "autoplaylist_repeat": False, - "autoplaylist": None, } @staticmethod - def file_url(_post_data=None, params=None, **kwargs): + def file_url(*args, **kwargs): return { "id": 1, "url": None, @@ -100,19 +58,11 @@ class ApiClientServicesMock: } @staticmethod - def webstream_url(_post_data=None, params=None, **kwargs): + def webstream_url(*args, **kwargs): return { - "item_url": "http://192.168.10.100:8081/api/v2/webstreams/1/", "id": 1, "name": "Test", - "description": "", "url": "http://some-other-radio:8800/main.ogg", - "length": "00:30:00", - "creator_id": 1, - "mtime": "2022-03-04T13:11:20Z", - "utime": "2022-03-04T13:11:20Z", - "lptime": None, - "mime": "application/ogg", }