From 7e1be7b0280e411057be06d5c41dcd3365fd8dab Mon Sep 17 00:00:00 2001
From: Kyle Robbertze <kyle@paddatrapper.com>
Date: Tue, 10 Aug 2021 16:30:02 +0200
Subject: [PATCH] Truncate schedule items that run over the time of the
 containing show

Fixes: #1272
---
 api/libretimeapi/models/schedule.py  | 22 ++++++++
 api/libretimeapi/serializers.py      |  2 +
 api/libretimeapi/tests/test_views.py | 80 ++++++++++++++++++++++++++++
 3 files changed, 104 insertions(+)

diff --git a/api/libretimeapi/models/schedule.py b/api/libretimeapi/models/schedule.py
index ce87a06b1..314ef5ec2 100644
--- a/api/libretimeapi/models/schedule.py
+++ b/api/libretimeapi/models/schedule.py
@@ -22,6 +22,28 @@ class Schedule(models.Model):
     def get_owner(self):
         return self.instance.get_owner()
 
+    def get_cueout(self):
+        """
+        Returns a cueout that is based on the current show. 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. This prevents the next
+        show having overlapping items playing.
+        """
+        if self.instance.ends < self.ends:
+            return self.instance.ends - self.starts
+        return self.cue_out
+
+    def get_ends(self):
+        """
+        Returns an item end that is based on the current show. Ends 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. This prevents the
+        next show having overlapping items playing.
+        """
+        if self.instance.ends < self.ends:
+            return self.instance.ends
+        return self.ends
+
     class Meta:
         managed = False
         db_table = "cc_schedule"
diff --git a/api/libretimeapi/serializers.py b/api/libretimeapi/serializers.py
index 4ecd8e8db..200e3e2ce 100644
--- a/api/libretimeapi/serializers.py
+++ b/api/libretimeapi/serializers.py
@@ -128,6 +128,8 @@ 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)
 
     class Meta:
         model = Schedule
diff --git a/api/libretimeapi/tests/test_views.py b/api/libretimeapi/tests/test_views.py
index cb00ce222..a4693a14f 100644
--- a/api/libretimeapi/tests/test_views.py
+++ b/api/libretimeapi/tests/test_views.py
@@ -1,7 +1,9 @@
 import os
+from datetime import datetime, timedelta
 
 from django.conf import settings
 from django.contrib.auth.models import AnonymousUser
+from django.utils import dateparse
 from libretimeapi.views import FileViewSet
 from model_bakery import baker
 from rest_framework.test import APIRequestFactory, APITestCase
@@ -40,3 +42,81 @@ class TestFileViewSet(APITestCase):
         self.client.credentials(HTTP_AUTHORIZATION="Api-Key {}".format(self.token))
         response = self.client.get(path)
         self.assertEqual(response.status_code, 200)
+
+
+class TestScheduleViewSet(APITestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.path = "/api/v2/schedule/"
+        cls.token = settings.CONFIG.get("general", "api_key")
+
+    def test_schedule_item_full_length(self):
+        music_dir = baker.make(
+            "libretimeapi.MusicDir",
+            directory=os.path.join(os.path.dirname(__file__), "resources"),
+        )
+        f = baker.make(
+            "libretimeapi.File",
+            directory=music_dir,
+            mime="audio/mp3",
+            filepath="song.mp3",
+            length=timedelta(seconds=40.86),
+            cuein=timedelta(seconds=0),
+            cueout=timedelta(seconds=40.8131),
+        )
+        show = baker.make(
+            "libretimeapi.ShowInstance",
+            starts=datetime.now(tz=datetime.timezone.utc) - timedelta(minutes=5),
+            ends=datetime.now(tz=datetime.timezone.utc) + timedelta(minutes=5),
+        )
+        scheduleItem = baker.make(
+            "libretimeapi.Schedule",
+            starts=datetime.now(tz=datetime.timezone.utc),
+            ends=datetime.now(tz=datetime.timezone.utc) + f.length,
+            cue_out=f.cueout,
+            instance=show,
+            file=f,
+        )
+        self.client.credentials(HTTP_AUTHORIZATION="Api-Key {}".format(self.token))
+        response = self.client.get(self.path)
+        self.assertEqual(response.status_code, 200)
+        result = response.json()
+        self.assertEqual(dateparse.parse_datetime(result[0]["ends"]), scheduleItem.ends)
+        self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), f.cueout)
+
+    def test_schedule_item_trunc(self):
+        music_dir = baker.make(
+            "libretimeapi.MusicDir",
+            directory=os.path.join(os.path.dirname(__file__), "resources"),
+        )
+        f = baker.make(
+            "libretimeapi.File",
+            directory=music_dir,
+            mime="audio/mp3",
+            filepath="song.mp3",
+            length=timedelta(seconds=40.86),
+            cuein=timedelta(seconds=0),
+            cueout=timedelta(seconds=40.8131),
+        )
+        show = baker.make(
+            "libretimeapi.ShowInstance",
+            starts=datetime.now(tz=datetime.timezone.utc) - timedelta(minutes=5),
+            ends=datetime.now(tz=datetime.timezone.utc) + timedelta(seconds=20),
+        )
+        scheduleItem = baker.make(
+            "libretimeapi.Schedule",
+            starts=datetime.now(tz=datetime.timezone.utc),
+            ends=datetime.now(tz=datetime.timezone.utc) + f.length,
+            instance=show,
+            file=f,
+        )
+        self.client.credentials(HTTP_AUTHORIZATION="Api-Key {}".format(self.token))
+        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 - scheduleItem.starts
+        self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), expected)
+        self.assertNotEqual(
+            dateparse.parse_datetime(result[0]["ends"]), scheduleItem.ends
+        )