chore(api): rename schedule models fields
This commit is contained in:
parent
8ceb1419a0
commit
57046e2a9d
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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"))
|
||||
|
|
214
api/schema.yml
214
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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue