chore(api): rename schedule models fields

This commit is contained in:
jo 2022-07-17 22:27:57 +02:00 committed by Kyle Robbertze
parent 8ceb1419a0
commit 57046e2a9d
8 changed files with 224 additions and 334 deletions

View File

@ -2,8 +2,14 @@ from django.db import models
class Schedule(models.Model): class Schedule(models.Model):
starts = models.DateTimeField() starts_at = models.DateTimeField(db_column="starts")
ends = models.DateTimeField() ends_at = models.DateTimeField(db_column="ends")
instance = models.ForeignKey(
"schedule.ShowInstance",
on_delete=models.DO_NOTHING,
)
file = models.ForeignKey( file = models.ForeignKey(
"storage.File", "storage.File",
on_delete=models.DO_NOTHING, on_delete=models.DO_NOTHING,
@ -16,31 +22,60 @@ class Schedule(models.Model):
blank=True, blank=True,
null=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_in = models.TimeField(blank=True, null=True)
fade_out = models.TimeField(blank=True, null=True) fade_out = models.TimeField(blank=True, null=True)
cue_in = models.DurationField() cue_in = models.DurationField()
cue_out = 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) class PositionStatus(models.IntegerChoices):
playout_status = models.SmallIntegerField() FILLER = -1, "Filler" # Used to fill a show that already started
broadcasted = models.SmallIntegerField() 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 = 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): def get_owner(self):
return self.instance.get_owner() 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 Cue out 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. scheduled in. In that case, the cue out should be the end of the show.
This prevents the next show having overlapping items playing. This prevents the next show having overlapping items playing.
Cases: Cases:
- When the schedule ends before the end of the show instance, - 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 - When the schedule starts before the end of the show instance
and ends after the show instance ends, 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, - When the schedule starts after the end of the show instance,
return the stored cue_out even if the schedule WILL NOT BE PLAYED. 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: if (
return self.instance.ends - self.starts 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 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 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. 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, - When the schedule starts after the end of the show instance,
return the show instance ends. return the show instance ends.
""" """
if self.instance.ends < self.ends: if self.instance.ends_at < self.ends_at:
return self.instance.ends return self.instance.ends_at
return self.ends return self.ends_at
@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
class Meta: class Meta:
managed = False managed = False

View File

@ -7,29 +7,30 @@ class ScheduleSerializer(serializers.HyperlinkedModelSerializer):
file_id = serializers.IntegerField(source="file.id", read_only=True) file_id = serializers.IntegerField(source="file.id", read_only=True)
stream_id = serializers.IntegerField(source="stream.id", read_only=True) stream_id = serializers.IntegerField(source="stream.id", read_only=True)
instance_id = serializers.IntegerField(source="instance.id", read_only=True) instance_id = serializers.IntegerField(source="instance.id", read_only=True)
cue_out = serializers.DurationField(source="get_cueout", read_only=True) cue_out = serializers.DurationField(source="get_cue_out", read_only=True)
ends = serializers.DateTimeField(source="get_ends", read_only=True) ends_at = serializers.DateTimeField(source="get_ends_at", read_only=True)
class Meta: class Meta:
model = Schedule model = Schedule
fields = [ fields = [
"item_url", "item_url",
"id", "id",
"starts", "starts_at",
"ends", "ends_at",
"instance",
"instance_id",
"file", "file",
"file_id", "file_id",
"stream", "stream",
"stream_id", "stream_id",
"clip_length", "length",
"fade_in", "fade_in",
"fade_out", "fade_out",
"cue_in", "cue_in",
"cue_out", "cue_out",
"media_item_played",
"instance",
"instance_id",
"playout_status",
"broadcasted",
"position", "position",
"position_status",
"broadcasted",
"played",
"overbooked",
] ]

View File

@ -9,49 +9,49 @@ class TestSchedule(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.show_instance = ShowInstance( cls.show_instance = ShowInstance(
created=datetime(year=2021, month=10, day=1, hour=12), created_at=datetime(year=2021, month=10, day=1, hour=12),
starts=datetime(year=2021, month=10, day=2, hour=1), starts_at=datetime(year=2021, month=10, day=2, hour=1),
ends=datetime(year=2021, month=10, day=2, hour=2), ends_at=datetime(year=2021, month=10, day=2, hour=2),
) )
cls.length = timedelta(minutes=10) cls.length = timedelta(minutes=10)
cls.cue_in = timedelta(seconds=1) cls.cue_in = timedelta(seconds=1)
cls.cue_out = cls.length - timedelta(seconds=4) cls.cue_out = cls.length - timedelta(seconds=4)
def create_schedule(self, starts): def create_schedule(self, starts_at):
return Schedule( return Schedule(
starts=starts, starts_at=starts_at,
ends=starts + self.length, ends_at=starts_at + self.length,
cue_in=self.cue_in, cue_in=self.cue_in,
cue_out=self.cue_out, cue_out=self.cue_out,
instance=self.show_instance, instance=self.show_instance,
) )
def test_get_cueout(self): def test_get_cue_out(self):
# No overlapping schedule datetimes, normal usecase: # No overlapping schedule datetimes, normal usecase:
item1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30) item1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30)
item1 = self.create_schedule(item1_starts) item1 = self.create_schedule(item1_starts)
self.assertEqual(item1.get_cueout(), self.cue_out) self.assertEqual(item1.get_cue_out(), self.cue_out)
self.assertEqual(item1.get_ends(), item1_starts + self.length) self.assertEqual(item1.get_ends_at(), item1_starts + self.length)
# Mixed overlapping schedule datetimes (only ends is overlapping): # Mixed overlapping schedule datetimes (only ends is overlapping):
item_2_starts = datetime(year=2021, month=10, day=2, hour=1, minute=55) item_2_starts = datetime(year=2021, month=10, day=2, hour=1, minute=55)
item_2 = self.create_schedule(item_2_starts) item_2 = self.create_schedule(item_2_starts)
self.assertEqual(item_2.get_cueout(), timedelta(minutes=5)) self.assertEqual(item_2.get_cue_out(), timedelta(minutes=5))
self.assertEqual(item_2.get_ends(), self.show_instance.ends) self.assertEqual(item_2.get_ends_at(), self.show_instance.ends_at)
# Fully overlapping schedule datetimes (starts and ends are overlapping): # Fully overlapping schedule datetimes (starts and ends are overlapping):
item3_starts = datetime(year=2021, month=10, day=2, hour=2, minute=1) item3_starts = datetime(year=2021, month=10, day=2, hour=2, minute=1)
item3 = self.create_schedule(item3_starts) item3 = self.create_schedule(item3_starts)
self.assertEqual(item3.get_cueout(), self.cue_out) self.assertEqual(item3.get_cue_out(), self.cue_out)
self.assertEqual(item3.get_ends(), self.show_instance.ends) 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 # Starts before the schedule ends
item1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30) item1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30)
item1 = self.create_schedule(item1_starts) item1 = self.create_schedule(item1_starts)
self.assertTrue(item1.is_valid) self.assertFalse(item1.overbooked)
# Starts after the schedule ends # Starts after the schedule ends
item_2_starts = datetime(year=2021, month=10, day=2, hour=3) item_2_starts = datetime(year=2021, month=10, day=2, hour=3)
item_2 = self.create_schedule(item_2_starts) item_2 = self.create_schedule(item_2_starts)
self.assertFalse(item_2.is_valid) self.assertTrue(item_2.overbooked)

View File

@ -25,13 +25,13 @@ class TestScheduleViewSet(APITestCase):
) )
show = baker.make( show = baker.make(
"schedule.ShowInstance", "schedule.ShowInstance",
starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), starts_at=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
ends=datetime.now(tz=timezone.utc) + timedelta(minutes=5), ends_at=datetime.now(tz=timezone.utc) + timedelta(minutes=5),
) )
schedule_item = baker.make( schedule_item = baker.make(
"schedule.Schedule", "schedule.Schedule",
starts=datetime.now(tz=timezone.utc), starts_at=datetime.now(tz=timezone.utc),
ends=datetime.now(tz=timezone.utc) + file.length, ends_at=datetime.now(tz=timezone.utc) + file.length,
cue_out=file.cue_out, cue_out=file.cue_out,
instance=show, instance=show,
file=file, file=file,
@ -41,7 +41,7 @@ class TestScheduleViewSet(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
result = response.json() result = response.json()
self.assertEqual( 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) self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), file.cue_out)
@ -56,13 +56,13 @@ class TestScheduleViewSet(APITestCase):
) )
show = baker.make( show = baker.make(
"schedule.ShowInstance", "schedule.ShowInstance",
starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), starts_at=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
ends=datetime.now(tz=timezone.utc) + timedelta(seconds=20), ends_at=datetime.now(tz=timezone.utc) + timedelta(seconds=20),
) )
schedule_item = baker.make( schedule_item = baker.make(
"schedule.Schedule", "schedule.Schedule",
starts=datetime.now(tz=timezone.utc), starts_at=datetime.now(tz=timezone.utc),
ends=datetime.now(tz=timezone.utc) + file.length, ends_at=datetime.now(tz=timezone.utc) + file.length,
instance=show, instance=show,
file=file, file=file,
) )
@ -70,11 +70,11 @@ class TestScheduleViewSet(APITestCase):
response = self.client.get(self.path) response = self.client.get(self.path)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
result = response.json() result = response.json()
self.assertEqual(dateparse.parse_datetime(result[0]["ends"]), show.ends) self.assertEqual(dateparse.parse_datetime(result[0]["ends_at"]), show.ends_at)
expected = show.ends - schedule_item.starts expected = show.ends_at - schedule_item.starts_at
self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), expected) self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), expected)
self.assertNotEqual( 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): def test_schedule_item_invalid(self):
@ -88,33 +88,33 @@ class TestScheduleViewSet(APITestCase):
) )
show = baker.make( show = baker.make(
"schedule.ShowInstance", "schedule.ShowInstance",
starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), starts_at=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
ends=datetime.now(tz=timezone.utc) + timedelta(minutes=5), ends_at=datetime.now(tz=timezone.utc) + timedelta(minutes=5),
) )
schedule_item = baker.make( schedule_item = baker.make(
"schedule.Schedule", "schedule.Schedule",
starts=datetime.now(tz=timezone.utc), starts_at=datetime.now(tz=timezone.utc),
ends=datetime.now(tz=timezone.utc) + file.length, ends_at=datetime.now(tz=timezone.utc) + file.length,
cue_out=file.cue_out, cue_out=file.cue_out,
instance=show, instance=show,
file=file, file=file,
) )
invalid_schedule_item = baker.make( # pylint: disable=unused-variable invalid_schedule_item = baker.make( # pylint: disable=unused-variable
"schedule.Schedule", "schedule.Schedule",
starts=show.ends + timedelta(minutes=1), starts_at=show.ends_at + timedelta(minutes=1),
ends=show.ends + timedelta(minutes=1) + file.length, ends_at=show.ends_at + timedelta(minutes=1) + file.length,
cue_out=file.cue_out, cue_out=file.cue_out,
instance=show, instance=show,
file=file, file=file,
) )
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") 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) self.assertEqual(response.status_code, 200)
result = response.json() result = response.json()
# The invalid item should be filtered out and not returned # The invalid item should be filtered out and not returned
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assertEqual( 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) self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), file.cue_out)
@ -131,21 +131,21 @@ class TestScheduleViewSet(APITestCase):
show = baker.make( show = baker.make(
"schedule.ShowInstance", "schedule.ShowInstance",
starts=filter_point - timedelta(minutes=5), starts_at=filter_point - timedelta(minutes=5),
ends=filter_point + timedelta(minutes=5), ends_at=filter_point + timedelta(minutes=5),
) )
schedule_item = baker.make( schedule_item = baker.make(
"schedule.Schedule", "schedule.Schedule",
starts=filter_point, starts_at=filter_point,
ends=filter_point + file.length, ends_at=filter_point + file.length,
cue_out=file.cue_out, cue_out=file.cue_out,
instance=show, instance=show,
file=file, file=file,
) )
previous_item = baker.make( # pylint: disable=unused-variable previous_item = baker.make( # pylint: disable=unused-variable
"schedule.Schedule", "schedule.Schedule",
starts=filter_point - timedelta(minutes=5), starts_at=filter_point - timedelta(minutes=5),
ends=filter_point - timedelta(minutes=5) + file.length, ends_at=filter_point - timedelta(minutes=5) + file.length,
cue_out=file.cue_out, cue_out=file.cue_out,
instance=show, instance=show,
file=file, file=file,
@ -156,12 +156,13 @@ class TestScheduleViewSet(APITestCase):
) )
range_end = (filter_point + timedelta(minutes=1)).isoformat(timespec="seconds") range_end = (filter_point + timedelta(minutes=1)).isoformat(timespec="seconds")
response = self.client.get( 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) self.assertEqual(response.status_code, 200)
result = response.json() result = response.json()
# The previous_item should be filtered out and not returned # The previous_item should be filtered out and not returned
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assertEqual( self.assertEqual(
dateparse.parse_datetime(result[0]["starts"]), schedule_item.starts dateparse.parse_datetime(result[0]["starts_at"]), schedule_item.starts_at
) )

View File

@ -1,42 +1,33 @@
from django.db.models import F from django.db import models
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view from django_filters import rest_framework as filters
from rest_framework import viewsets from rest_framework import viewsets
from ..._constants import FILTER_NUMERICAL_LOOKUPS
from ..models import Schedule from ..models import Schedule
from ..serializers import ScheduleSerializer from ..serializers import ScheduleSerializer
@extend_schema_view( class ScheduleFilter(filters.FilterSet):
list=extend_schema( starts = filters.DateTimeFromToRangeFilter(field_name="starts_at")
parameters=[ ends = filters.DateTimeFromToRangeFilter(field_name="ends_at")
OpenApiParameter( position_status = filters.NumberFilter()
name="is_valid", broadcasted = filters.NumberFilter()
description="Filter on valid instances",
required=False, overbooked = filters.BooleanFilter(method="overbooked_filter")
type=bool,
), # 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): class ScheduleViewSet(viewsets.ModelViewSet):
queryset = Schedule.objects.all() queryset = Schedule.objects.all()
serializer_class = ScheduleSerializer serializer_class = ScheduleSerializer
filterset_fields = { filterset_class = ScheduleFilter
"starts": FILTER_NUMERICAL_LOOKUPS,
"ends": FILTER_NUMERICAL_LOOKUPS,
"playout_status": FILTER_NUMERICAL_LOOKUPS,
"broadcasted": FILTER_NUMERICAL_LOOKUPS,
}
model_permission_name = "schedule" 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"))

View File

@ -2540,134 +2540,33 @@ paths:
schema: schema:
type: integer type: integer
- in: query - in: query
name: broadcasted__gt name: ends_after
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
schema: schema:
type: string type: string
format: date-time format: date-time
- in: query - in: query
name: ends__gt name: ends_before
schema: schema:
type: string type: string
format: date-time format: date-time
- in: query - in: query
name: ends__gte name: overbooked
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
schema: schema:
type: boolean type: boolean
description: Filter on valid instances
- in: query - in: query
name: playout_status name: position_status
schema: schema:
type: integer type: integer
- in: query - in: query
name: playout_status__gt name: starts_after
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
schema: schema:
type: string type: string
format: date-time format: date-time
- in: query - in: query
name: starts__gt name: starts_before
schema: schema:
type: string type: string
format: date-time 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: tags:
- schedule - schedule
security: security:
@ -6553,13 +6452,19 @@ components:
id: id:
type: integer type: integer
readOnly: true readOnly: true
starts: starts_at:
type: string type: string
format: date-time format: date-time
ends: ends_at:
type: string type: string
format: date-time format: date-time
readOnly: true readOnly: true
instance:
type: string
format: uri
instance_id:
type: integer
readOnly: true
file: file:
type: string type: string
format: uri format: uri
@ -6574,7 +6479,7 @@ components:
stream_id: stream_id:
type: integer type: integer
readOnly: true readOnly: true
clip_length: length:
type: string type: string
nullable: true nullable: true
fade_in: fade_in:
@ -6590,27 +6495,25 @@ components:
cue_out: cue_out:
type: string type: string
readOnly: true 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: position:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 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: PatchedServiceRegister:
type: object type: object
properties: properties:
@ -7381,6 +7284,13 @@ components:
- item_url - item_url
- podcast - podcast
- published_at - published_at
PositionStatusEnum:
enum:
- -1
- 0
- 1
- 2
type: integer
Preference: Preference:
type: object type: object
properties: properties:
@ -7433,13 +7343,19 @@ components:
id: id:
type: integer type: integer
readOnly: true readOnly: true
starts: starts_at:
type: string type: string
format: date-time format: date-time
ends: ends_at:
type: string type: string
format: date-time format: date-time
readOnly: true readOnly: true
instance:
type: string
format: uri
instance_id:
type: integer
readOnly: true
file: file:
type: string type: string
format: uri format: uri
@ -7454,7 +7370,7 @@ components:
stream_id: stream_id:
type: integer type: integer
readOnly: true readOnly: true
clip_length: length:
type: string type: string
nullable: true nullable: true
fade_in: fade_in:
@ -7470,40 +7386,38 @@ components:
cue_out: cue_out:
type: string type: string
readOnly: true 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: position:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 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: required:
- broadcasted - broadcasted
- cue_in - cue_in
- cue_out - cue_out
- ends - ends_at
- file_id - file_id
- id - id
- instance - instance
- instance_id - instance_id
- item_url - item_url
- playout_status - overbooked
- position - position
- starts - starts_at
- stream_id - stream_id
ServiceRegister: ServiceRegister:
type: object type: object

View File

@ -27,16 +27,17 @@ def get_schedule(api_client: ApiClient):
schedule = api_client.services.schedule_url( schedule = api_client.services.schedule_url(
params={ params={
"ends__range": (f"{current_time_str}Z,{end_time_str}Z"), "ends_after": f"{current_time_str}Z",
"is_valid": True, "ends_before": f"{end_time_str}Z",
"playout_status__gt": 0, "overbooked": False,
"position_status__gt": 0,
} }
) )
events = {} events = {}
for item in schedule: for item in schedule:
item["starts"] = isoparse(item["starts"]) item["starts_at"] = isoparse(item["starts_at"])
item["ends"] = isoparse(item["ends"]) item["ends_at"] = isoparse(item["ends_at"])
show_instance = api_client.services.show_instance_url(id=item["instance_id"]) show_instance = api_client.services.show_instance_url(id=item["instance_id"])
show = api_client.services.show_url(id=show_instance["show_id"]) show = api_client.services.show_url(id=show_instance["show_id"])
@ -62,8 +63,8 @@ def generate_file_events(
""" """
events = {} events = {}
schedule_start_event_key = datetime_to_event_key(schedule["starts"]) schedule_start_event_key = datetime_to_event_key(schedule["starts_at"])
schedule_end_event_key = datetime_to_event_key(schedule["ends"]) schedule_end_event_key = datetime_to_event_key(schedule["ends_at"])
events[schedule_start_event_key] = { events[schedule_start_event_key] = {
"type": EventKind.FILE, "type": EventKind.FILE,
@ -102,15 +103,15 @@ def generate_webstream_events(
""" """
events = {} events = {}
schedule_start_event_key = datetime_to_event_key(schedule["starts"]) schedule_start_event_key = datetime_to_event_key(schedule["starts_at"])
schedule_end_event_key = datetime_to_event_key(schedule["ends"]) schedule_end_event_key = datetime_to_event_key(schedule["ends_at"])
events[schedule_start_event_key] = { events[schedule_start_event_key] = {
"type": EventKind.STREAM_BUFFER_START, "type": EventKind.STREAM_BUFFER_START,
"independent_event": True, "independent_event": True,
"row_id": schedule["id"], "row_id": schedule["id"],
"start": 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"] - timedelta(seconds=5)), "end": datetime_to_event_key(schedule["starts_at"] - timedelta(seconds=5)),
"uri": webstream["url"], "uri": webstream["url"],
"id": webstream["id"], "id": webstream["id"],
} }
@ -127,7 +128,8 @@ def generate_webstream_events(
"show_name": show["name"], "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] = { events[schedule_end_event_key] = {
"type": EventKind.STREAM_BUFFER_END, "type": EventKind.STREAM_BUFFER_END,
"independent_event": True, "independent_event": True,

View File

@ -3,92 +3,50 @@ from libretime_playout.schedule import get_schedule
class ApiClientServicesMock: class ApiClientServicesMock:
@staticmethod @staticmethod
def schedule_url(_post_data=None, params=None, **kwargs): def schedule_url(*args, **kwargs):
return [ return [
{ {
"item_url": "http://192.168.10.100:8081/api/v2/schedule/17/",
"id": 17, "id": 17,
"starts": "2022-03-04T15:30:00Z", "starts_at": "2022-03-04T15:30:00Z",
"ends": "2022-03-04T15:33:50.674340Z", "ends_at": "2022-03-04T15:33:50.674340Z",
"file": "http://192.168.10.100:8081/api/v2/files/1/", "file": "http://192.168.10.100:8081/api/v2/files/1/",
"file_id": 1, "file_id": 1,
"stream": None, "stream": None,
"clip_length": "00:03:50.674340",
"fade_in": "00:00:00.500000", "fade_in": "00:00:00.500000",
"fade_out": "00:00:00.500000", "fade_out": "00:00:00.500000",
"cue_in": "00:00:01.310660", "cue_in": "00:00:01.310660",
"cue_out": "00:03:51.985000", "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, "instance_id": 3,
"playout_status": 1,
"broadcasted": 0,
"position": 0,
}, },
{ {
"item_url": "http://192.168.10.100:8081/api/v2/schedule/18/",
"id": 18, "id": 18,
"starts": "2022-03-04T15:33:50.674340Z", "starts_at": "2022-03-04T15:33:50.674340Z",
"ends": "2022-03-04T16:03:50.674340Z", "ends_at": "2022-03-04T16:03:50.674340Z",
"file": None, "file": None,
"stream": "http://192.168.10.100:8081/api/v2/webstreams/1/", "stream": "http://192.168.10.100:8081/api/v2/webstreams/1/",
"stream_id": 1, "stream_id": 1,
"clip_length": "00:30:00",
"fade_in": "00:00:00.500000", "fade_in": "00:00:00.500000",
"fade_out": "00:00:00.500000", "fade_out": "00:00:00.500000",
"cue_in": "00:00:00", "cue_in": "00:00:00",
"cue_out": "00:30: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, "instance_id": 3,
"playout_status": 1,
"broadcasted": 0,
"position": 1,
}, },
] ]
@staticmethod @staticmethod
def show_instance_url(_post_data=None, params=None, **kwargs): def show_instance_url(*args, **kwargs):
return { 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, "show_id": 3,
"instance": None,
"file": None,
} }
@staticmethod @staticmethod
def show_url(_post_data=None, params=None, **kwargs): def show_url(*args, **kwargs):
return { return {
"item_url": "http://192.168.10.100:8081/api/v2/shows/3/",
"id": 3,
"name": "Test", "name": "Test",
"url": "",
"genre": "",
"description": "",
"color": "",
"background_color": "",
"linked": False,
"is_linkable": True,
"image_path": "",
"has_autoplaylist": False,
"autoplaylist_repeat": False,
"autoplaylist": None,
} }
@staticmethod @staticmethod
def file_url(_post_data=None, params=None, **kwargs): def file_url(*args, **kwargs):
return { return {
"id": 1, "id": 1,
"url": None, "url": None,
@ -100,19 +58,11 @@ class ApiClientServicesMock:
} }
@staticmethod @staticmethod
def webstream_url(_post_data=None, params=None, **kwargs): def webstream_url(*args, **kwargs):
return { return {
"item_url": "http://192.168.10.100:8081/api/v2/webstreams/1/",
"id": 1, "id": 1,
"name": "Test", "name": "Test",
"description": "",
"url": "http://some-other-radio:8800/main.ogg", "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",
} }