feat(api): split api into multiple apps (#1626)

Fixes #1622

- split the api into 4 apps: core, history, schedule, storage
- exploded the settings into testing/prod
This commit is contained in:
Jonas L 2022-04-04 14:38:50 +02:00 committed by GitHub
parent 87d2da9d84
commit fce988aef1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
120 changed files with 1499 additions and 1078 deletions

View file

View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class ScheduleConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "libretime_api.schedule"
verbose_name = "LibreTime Schedule API"

View file

@ -0,0 +1,6 @@
from .playlist import Playlist, PlaylistContent
from .podcast import ImportedPodcast, Podcast, PodcastEpisode, StationPodcast
from .schedule import Schedule
from .show import Show, ShowDays, ShowHost, ShowInstance, ShowRebroadcast
from .smart_block import SmartBlock, SmartBlockContent, SmartBlockCriteria
from .webstream import Webstream, WebstreamMetadata

View file

@ -0,0 +1,39 @@
from django.db import models
class Playlist(models.Model):
name = models.CharField(max_length=255)
mtime = models.DateTimeField(blank=True, null=True)
utime = models.DateTimeField(blank=True, null=True)
creator = models.ForeignKey("core.User", models.DO_NOTHING, blank=True, null=True)
description = models.CharField(max_length=512, blank=True, null=True)
length = models.DurationField(blank=True, null=True)
def get_owner(self):
return self.creator
class Meta:
managed = False
db_table = "cc_playlist"
class PlaylistContent(models.Model):
playlist = models.ForeignKey("Playlist", models.DO_NOTHING, blank=True, null=True)
file = models.ForeignKey("storage.File", models.DO_NOTHING, blank=True, null=True)
block = models.ForeignKey("SmartBlock", models.DO_NOTHING, blank=True, null=True)
stream_id = models.IntegerField(blank=True, null=True)
type = models.SmallIntegerField()
position = models.IntegerField(blank=True, null=True)
trackoffset = models.FloatField()
cliplength = models.DurationField(blank=True, null=True)
cuein = models.DurationField(blank=True, null=True)
cueout = models.DurationField(blank=True, null=True)
fadein = models.TimeField(blank=True, null=True)
fadeout = models.TimeField(blank=True, null=True)
def get_owner(self):
return self.playlist.owner
class Meta:
managed = False
db_table = "cc_playlistcontents"

View file

@ -0,0 +1,83 @@
from django.db import models
class Podcast(models.Model):
url = models.CharField(max_length=4096)
title = models.CharField(max_length=4096)
creator = models.CharField(max_length=4096, blank=True, null=True)
description = models.CharField(max_length=4096, blank=True, null=True)
language = models.CharField(max_length=4096, blank=True, null=True)
copyright = models.CharField(max_length=4096, blank=True, null=True)
link = models.CharField(max_length=4096, blank=True, null=True)
itunes_author = models.CharField(max_length=4096, blank=True, null=True)
itunes_keywords = models.CharField(max_length=4096, blank=True, null=True)
itunes_summary = models.CharField(max_length=4096, blank=True, null=True)
itunes_subtitle = models.CharField(max_length=4096, blank=True, null=True)
itunes_category = models.CharField(max_length=4096, blank=True, null=True)
itunes_explicit = models.CharField(max_length=4096, blank=True, null=True)
owner = models.ForeignKey(
"core.User", models.DO_NOTHING, db_column="owner", blank=True, null=True
)
def get_owner(self):
return self.owner
class Meta:
managed = False
db_table = "podcast"
permissions = [
("change_own_podcast", "Change the podcasts where they are the owner"),
("delete_own_podcast", "Delete the podcasts where they are the owner"),
]
class PodcastEpisode(models.Model):
file = models.ForeignKey("storage.File", models.DO_NOTHING, blank=True, null=True)
podcast = models.ForeignKey("Podcast", models.DO_NOTHING)
publication_date = models.DateTimeField()
download_url = models.CharField(max_length=4096)
episode_guid = models.CharField(max_length=4096)
episode_title = models.CharField(max_length=4096)
episode_description = models.TextField()
def get_owner(self):
return self.podcast.owner
class Meta:
managed = False
db_table = "podcast_episodes"
permissions = [
(
"change_own_podcastepisode",
"Change the episodes of podcasts where they are the owner",
),
(
"delete_own_podcastepisode",
"Delete the episodes of podcasts where they are the owner",
),
]
class StationPodcast(models.Model):
podcast = models.ForeignKey("Podcast", models.DO_NOTHING)
def get_owner(self):
return self.podcast.owner
class Meta:
managed = False
db_table = "station_podcast"
class ImportedPodcast(models.Model):
auto_ingest = models.BooleanField()
auto_ingest_timestamp = models.DateTimeField(blank=True, null=True)
album_override = models.BooleanField()
podcast = models.ForeignKey("Podcast", models.DO_NOTHING)
def get_owner(self):
return self.podcast.owner
class Meta:
managed = False
db_table = "imported_podcast"

View file

@ -0,0 +1,83 @@
from django.db import models
class Schedule(models.Model):
starts = models.DateTimeField()
ends = models.DateTimeField()
file = models.ForeignKey("storage.File", models.DO_NOTHING, blank=True, null=True)
stream = models.ForeignKey("Webstream", models.DO_NOTHING, blank=True, null=True)
clip_length = models.DurationField(blank=True, null=True)
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("ShowInstance", models.DO_NOTHING)
playout_status = models.SmallIntegerField()
broadcasted = models.SmallIntegerField()
position = models.IntegerField()
def get_owner(self):
return self.instance.get_owner()
def get_cueout(self):
"""
Returns a scheduled item cueout 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.
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.
- When the schedule starts before the end of the show instance
and ends after the show instance ends,
return timedelta between schedule starts and show instance ends.
- 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
return self.cue_out
def get_ends(self):
"""
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.
This prevents the next show having overlapping items playing.
Cases:
- When the schedule ends before the end of the show instance,
return the scheduled item ends.
- When the schedule starts before the end of the show instance
and ends after the show instance ends,
return the show instance ends.
- 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
class Meta:
managed = False
db_table = "cc_schedule"
permissions = [
("change_own_schedule", "Change the content on their shows"),
("delete_own_schedule", "Delete the content on their shows"),
]

View file

@ -0,0 +1,94 @@
from django.db import models
class Show(models.Model):
name = models.CharField(max_length=255)
url = models.CharField(max_length=255, blank=True, null=True)
genre = models.CharField(max_length=255, blank=True, null=True)
description = models.CharField(max_length=8192, blank=True, null=True)
color = models.CharField(max_length=6, blank=True, null=True)
background_color = models.CharField(max_length=6, blank=True, null=True)
live_stream_using_airtime_auth = models.BooleanField(blank=True, null=True)
live_stream_using_custom_auth = models.BooleanField(blank=True, null=True)
live_stream_user = models.CharField(max_length=255, blank=True, null=True)
live_stream_pass = models.CharField(max_length=255, blank=True, null=True)
linked = models.BooleanField()
is_linkable = models.BooleanField()
image_path = models.CharField(max_length=255, blank=True, null=True)
has_autoplaylist = models.BooleanField()
autoplaylist = models.ForeignKey(
"Playlist", models.DO_NOTHING, blank=True, null=True
)
autoplaylist_repeat = models.BooleanField()
def get_owner(self):
return self.showhost_set.all()
class Meta:
managed = False
db_table = "cc_show"
class ShowDays(models.Model):
first_show = models.DateField()
last_show = models.DateField(blank=True, null=True)
start_time = models.TimeField()
timezone = models.CharField(max_length=1024)
duration = models.CharField(max_length=1024)
day = models.SmallIntegerField(blank=True, null=True)
repeat_type = models.SmallIntegerField()
next_pop_date = models.DateField(blank=True, null=True)
show = models.ForeignKey("Show", models.DO_NOTHING)
record = models.SmallIntegerField(blank=True, null=True)
def get_owner(self):
return self.show.get_owner()
class Meta:
managed = False
db_table = "cc_show_days"
class ShowHost(models.Model):
show = models.ForeignKey("Show", models.DO_NOTHING)
subjs = models.ForeignKey("core.User", models.DO_NOTHING)
class Meta:
managed = False
db_table = "cc_show_hosts"
class ShowInstance(models.Model):
description = models.CharField(max_length=8192, blank=True, null=True)
starts = models.DateTimeField()
ends = models.DateTimeField()
show = models.ForeignKey("Show", models.DO_NOTHING)
record = models.SmallIntegerField(blank=True, null=True)
rebroadcast = models.SmallIntegerField(blank=True, null=True)
instance = models.ForeignKey("self", models.DO_NOTHING, blank=True, null=True)
file = models.ForeignKey("storage.File", models.DO_NOTHING, blank=True, null=True)
time_filled = models.DurationField(blank=True, null=True)
created = models.DateTimeField()
last_scheduled = models.DateTimeField(blank=True, null=True)
modified_instance = models.BooleanField()
autoplaylist_built = models.BooleanField()
def get_owner(self):
return show.get_owner()
class Meta:
managed = False
db_table = "cc_show_instances"
class ShowRebroadcast(models.Model):
day_offset = models.CharField(max_length=1024)
start_time = models.TimeField()
show = models.ForeignKey("Show", models.DO_NOTHING)
def get_owner(self):
return show.get_owner()
class Meta:
managed = False
db_table = "cc_show_rebroadcast"

View file

@ -0,0 +1,83 @@
from django.db import models
class SmartBlock(models.Model):
name = models.CharField(max_length=255)
mtime = models.DateTimeField(blank=True, null=True)
utime = models.DateTimeField(blank=True, null=True)
creator = models.ForeignKey("core.User", models.DO_NOTHING, blank=True, null=True)
description = models.CharField(max_length=512, blank=True, null=True)
length = models.DurationField(blank=True, null=True)
type = models.CharField(max_length=7, blank=True, null=True)
def get_owner(self):
return self.creator
class Meta:
managed = False
db_table = "cc_block"
permissions = [
(
"change_own_smartblock",
"Change the smartblocks where they are the owner",
),
(
"delete_own_smartblock",
"Delete the smartblocks where they are the owner",
),
]
class SmartBlockContent(models.Model):
block = models.ForeignKey("SmartBlock", models.DO_NOTHING, blank=True, null=True)
file = models.ForeignKey("storage.File", models.DO_NOTHING, blank=True, null=True)
position = models.IntegerField(blank=True, null=True)
trackoffset = models.FloatField()
cliplength = models.DurationField(blank=True, null=True)
cuein = models.DurationField(blank=True, null=True)
cueout = models.DurationField(blank=True, null=True)
fadein = models.TimeField(blank=True, null=True)
fadeout = models.TimeField(blank=True, null=True)
def get_owner(self):
return self.block.get_owner()
class Meta:
managed = False
db_table = "cc_blockcontents"
permissions = [
(
"change_own_smartblockcontent",
"Change the content of smartblocks where they are the owner",
),
(
"delete_own_smartblockcontent",
"Delete the content of smartblocks where they are the owner",
),
]
class SmartBlockCriteria(models.Model):
criteria = models.CharField(max_length=32)
modifier = models.CharField(max_length=16)
value = models.CharField(max_length=512)
extra = models.CharField(max_length=512, blank=True, null=True)
criteriagroup = models.IntegerField(blank=True, null=True)
block = models.ForeignKey("SmartBlock", models.DO_NOTHING)
def get_owner(self):
return self.block.get_owner()
class Meta:
managed = False
db_table = "cc_blockcriteria"
permissions = [
(
"change_own_smartblockcriteria",
"Change the criteria of smartblocks where they are the owner",
),
(
"delete_own_smartblockcriteria",
"Delete the criteria of smartblocks where they are the owner",
),
]

View file

@ -0,0 +1,39 @@
from django.contrib.auth import get_user_model
from django.db import models
class Webstream(models.Model):
name = models.CharField(max_length=255)
description = models.CharField(max_length=255)
url = models.CharField(max_length=512)
length = models.DurationField()
creator_id = models.IntegerField()
mtime = models.DateTimeField()
utime = models.DateTimeField()
lptime = models.DateTimeField(blank=True, null=True)
mime = models.CharField(max_length=1024, blank=True, null=True)
def get_owner(self):
User = get_user_model()
return User.objects.get(pk=self.creator_id)
class Meta:
managed = False
db_table = "cc_webstream"
permissions = [
("change_own_webstream", "Change the webstreams where they are the owner"),
("delete_own_webstream", "Delete the webstreams where they are the owner"),
]
class WebstreamMetadata(models.Model):
instance = models.ForeignKey("Schedule", models.DO_NOTHING)
start_time = models.DateTimeField()
liquidsoap_data = models.CharField(max_length=1024)
def get_owner(self):
return self.instance.get_owner()
class Meta:
managed = False
db_table = "cc_webstream_metadata"

View file

@ -0,0 +1,40 @@
from rest_framework import routers
from .views import (
ImportedPodcastViewSet,
PlaylistContentViewSet,
PlaylistViewSet,
PodcastEpisodeViewSet,
PodcastViewSet,
ScheduleViewSet,
ShowDaysViewSet,
ShowHostViewSet,
ShowInstanceViewSet,
ShowRebroadcastViewSet,
ShowViewSet,
SmartBlockContentViewSet,
SmartBlockCriteriaViewSet,
SmartBlockViewSet,
StationPodcastViewSet,
WebstreamMetadataViewSet,
WebstreamViewSet,
)
router = routers.DefaultRouter()
router.register("playlist-contents", PlaylistContentViewSet)
router.register("playlists", PlaylistViewSet)
router.register("podcast-episodes", PodcastEpisodeViewSet)
router.register("podcasts", PodcastViewSet)
router.register("station-podcasts", StationPodcastViewSet)
router.register("imported-podcasts", ImportedPodcastViewSet)
router.register("schedule", ScheduleViewSet)
router.register("show-days", ShowDaysViewSet)
router.register("show-hosts", ShowHostViewSet)
router.register("show-instances", ShowInstanceViewSet)
router.register("show-rebroadcasts", ShowRebroadcastViewSet)
router.register("shows", ShowViewSet)
router.register("smart-block-contents", SmartBlockContentViewSet)
router.register("smart-block-criteria", SmartBlockCriteriaViewSet)
router.register("smart-blocks", SmartBlockViewSet)
router.register("webstream-metadata", WebstreamMetadataViewSet)
router.register("webstreams", WebstreamViewSet)

View file

@ -0,0 +1,21 @@
from .playlist import PlaylistContentSerializer, PlaylistSerializer
from .podcast import (
ImportedPodcastSerializer,
PodcastEpisodeSerializer,
PodcastSerializer,
StationPodcastSerializer,
)
from .schedule import ScheduleSerializer
from .show import (
ShowDaysSerializer,
ShowHostSerializer,
ShowInstanceSerializer,
ShowRebroadcastSerializer,
ShowSerializer,
)
from .smart_block import (
SmartBlockContentSerializer,
SmartBlockCriteriaSerializer,
SmartBlockSerializer,
)
from .webstream import WebstreamMetadataSerializer, WebstreamSerializer

View file

@ -0,0 +1,15 @@
from rest_framework import serializers
from ..models import Playlist, PlaylistContent
class PlaylistSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Playlist
fields = "__all__"
class PlaylistContentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PlaylistContent
fields = "__all__"

View file

@ -0,0 +1,27 @@
from rest_framework import serializers
from ..models import ImportedPodcast, Podcast, PodcastEpisode, StationPodcast
class PodcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Podcast
fields = "__all__"
class PodcastEpisodeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PodcastEpisode
fields = "__all__"
class StationPodcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = StationPodcast
fields = "__all__"
class ImportedPodcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ImportedPodcast
fields = "__all__"

View file

@ -0,0 +1,35 @@
from rest_framework import serializers
from ..models import Schedule
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
fields = [
"item_url",
"id",
"starts",
"ends",
"file",
"file_id",
"stream",
"stream_id",
"clip_length",
"fade_in",
"fade_out",
"cue_in",
"cue_out",
"media_item_played",
"instance",
"instance_id",
"playout_status",
"broadcasted",
"position",
]

View file

@ -0,0 +1,69 @@
from rest_framework import serializers
from ..models import Show, ShowDays, ShowHost, ShowInstance, ShowRebroadcast
class ShowSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Show
fields = [
"item_url",
"id",
"name",
"url",
"genre",
"description",
"color",
"background_color",
"linked",
"is_linkable",
"image_path",
"has_autoplaylist",
"autoplaylist_repeat",
"autoplaylist",
]
class ShowDaysSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ShowDays
fields = "__all__"
class ShowHostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ShowHost
fields = "__all__"
class ShowInstanceSerializer(serializers.HyperlinkedModelSerializer):
show_id = serializers.IntegerField(source="show.id", read_only=True)
file_id = serializers.IntegerField(source="file.id", read_only=True)
class Meta:
model = ShowInstance
fields = [
"item_url",
"id",
"description",
"starts",
"ends",
"record",
"rebroadcast",
"time_filled",
"created",
"last_scheduled",
"modified_instance",
"autoplaylist_built",
"show",
"show_id",
"instance",
"file",
"file_id",
]
class ShowRebroadcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ShowRebroadcast
fields = "__all__"

View file

@ -0,0 +1,21 @@
from rest_framework import serializers
from ..models import SmartBlock, SmartBlockContent, SmartBlockCriteria
class SmartBlockSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SmartBlock
fields = "__all__"
class SmartBlockContentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SmartBlockContent
fields = "__all__"
class SmartBlockCriteriaSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SmartBlockCriteria
fields = "__all__"

View file

@ -0,0 +1,17 @@
from rest_framework import serializers
from ..models import Webstream, WebstreamMetadata
class WebstreamSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.IntegerField(read_only=True)
class Meta:
model = Webstream
fields = "__all__"
class WebstreamMetadataSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = WebstreamMetadata
fields = "__all__"

View file

@ -0,0 +1,57 @@
from datetime import datetime, timedelta
from django.test import TestCase
from ...models import Schedule, ShowInstance
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),
)
cls.length = timedelta(minutes=10)
cls.cue_in = timedelta(seconds=1)
cls.cue_out = cls.length - timedelta(seconds=4)
def create_schedule(self, starts):
return Schedule(
starts=starts,
ends=starts + self.length,
cue_in=self.cue_in,
cue_out=self.cue_out,
instance=self.show_instance,
)
def test_get_cueout(self):
# No overlapping schedule datetimes, normal usecase:
s1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30)
s1 = self.create_schedule(s1_starts)
self.assertEqual(s1.get_cueout(), self.cue_out)
self.assertEqual(s1.get_ends(), s1_starts + self.length)
# Mixed overlapping schedule datetimes (only ends is overlapping):
s2_starts = datetime(year=2021, month=10, day=2, hour=1, minute=55)
s2 = self.create_schedule(s2_starts)
self.assertEqual(s2.get_cueout(), timedelta(minutes=5))
self.assertEqual(s2.get_ends(), self.show_instance.ends)
# Fully overlapping schedule datetimes (starts and ends are overlapping):
s3_starts = datetime(year=2021, month=10, day=2, hour=2, minute=1)
s3 = self.create_schedule(s3_starts)
self.assertEqual(s3.get_cueout(), self.cue_out)
self.assertEqual(s3.get_ends(), self.show_instance.ends)
def test_is_valid(self):
# Starts before the schedule ends
s1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30)
s1 = self.create_schedule(s1_starts)
self.assertTrue(s1.is_valid)
# Starts after the schedule ends
s2_starts = datetime(year=2021, month=10, day=2, hour=3)
s2 = self.create_schedule(s2_starts)
self.assertFalse(s2.is_valid)

View file

@ -0,0 +1,186 @@
import os
from datetime import datetime, timedelta, timezone
from django.conf import settings
from django.utils import dateparse
from model_bakery import baker
from rest_framework.test import APITestCase
from ...._fixtures import AUDIO_FILENAME, fixture_path
class TestScheduleViewSet(APITestCase):
@classmethod
def setUpTestData(cls):
cls.path = "/api/v2/schedule/"
cls.token = settings.CONFIG.general.api_key
def test_schedule_item_full_length(self):
music_dir = baker.make(
"storage.MusicDir",
directory=str(fixture_path),
)
f = baker.make(
"storage.File",
directory=music_dir,
mime="audio/mp3",
filepath=AUDIO_FILENAME,
length=timedelta(seconds=40.86),
cuein=timedelta(seconds=0),
cueout=timedelta(seconds=40.8131),
)
show = baker.make(
"schedule.ShowInstance",
starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
ends=datetime.now(tz=timezone.utc) + timedelta(minutes=5),
)
scheduleItem = baker.make(
"schedule.Schedule",
starts=datetime.now(tz=timezone.utc),
ends=datetime.now(tz=timezone.utc) + f.length,
cue_out=f.cueout,
instance=show,
file=f,
)
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {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(
"storage.MusicDir",
directory=str(fixture_path),
)
f = baker.make(
"storage.File",
directory=music_dir,
mime="audio/mp3",
filepath=AUDIO_FILENAME,
length=timedelta(seconds=40.86),
cuein=timedelta(seconds=0),
cueout=timedelta(seconds=40.8131),
)
show = baker.make(
"schedule.ShowInstance",
starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
ends=datetime.now(tz=timezone.utc) + timedelta(seconds=20),
)
scheduleItem = baker.make(
"schedule.Schedule",
starts=datetime.now(tz=timezone.utc),
ends=datetime.now(tz=timezone.utc) + f.length,
instance=show,
file=f,
)
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {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
)
def test_schedule_item_invalid(self):
music_dir = baker.make(
"storage.MusicDir",
directory=str(fixture_path),
)
f = baker.make(
"storage.File",
directory=music_dir,
mime="audio/mp3",
filepath=AUDIO_FILENAME,
length=timedelta(seconds=40.86),
cuein=timedelta(seconds=0),
cueout=timedelta(seconds=40.8131),
)
show = baker.make(
"schedule.ShowInstance",
starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
ends=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) + f.length,
cue_out=f.cueout,
instance=show,
file=f,
)
invalid_schedule_item = baker.make(
"schedule.Schedule",
starts=show.ends + timedelta(minutes=1),
ends=show.ends + timedelta(minutes=1) + f.length,
cue_out=f.cueout,
instance=show,
file=f,
)
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
response = self.client.get(self.path, {"is_valid": True})
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
)
self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), f.cueout)
def test_schedule_item_range(self):
music_dir = baker.make(
"storage.MusicDir",
directory=str(fixture_path),
)
f = baker.make(
"storage.File",
directory=music_dir,
mime="audio/mp3",
filepath=AUDIO_FILENAME,
length=timedelta(seconds=40.86),
cuein=timedelta(seconds=0),
cueout=timedelta(seconds=40.8131),
)
filter_point = datetime.now(tz=timezone.utc)
show = baker.make(
"schedule.ShowInstance",
starts=filter_point - timedelta(minutes=5),
ends=filter_point + timedelta(minutes=5),
)
schedule_item = baker.make(
"schedule.Schedule",
starts=filter_point,
ends=filter_point + f.length,
cue_out=f.cueout,
instance=show,
file=f,
)
previous_item = baker.make(
"schedule.Schedule",
starts=filter_point - timedelta(minutes=5),
ends=filter_point - timedelta(minutes=5) + f.length,
cue_out=f.cueout,
instance=show,
file=f,
)
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
range_start = (filter_point - timedelta(minutes=1)).isoformat(
timespec="seconds"
)
range_end = (filter_point + timedelta(minutes=1)).isoformat(timespec="seconds")
response = self.client.get(
self.path, {"starts__range": f"{range_start},{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
)

View file

@ -0,0 +1,21 @@
from .playlist import PlaylistContentViewSet, PlaylistViewSet
from .podcast import (
ImportedPodcastViewSet,
PodcastEpisodeViewSet,
PodcastViewSet,
StationPodcastViewSet,
)
from .schedule import ScheduleViewSet
from .show import (
ShowDaysViewSet,
ShowHostViewSet,
ShowInstanceViewSet,
ShowRebroadcastViewSet,
ShowViewSet,
)
from .smart_block import (
SmartBlockContentViewSet,
SmartBlockCriteriaViewSet,
SmartBlockViewSet,
)
from .webstream import WebstreamMetadataViewSet, WebstreamViewSet

View file

@ -0,0 +1,16 @@
from rest_framework import viewsets
from ..models import Playlist, PlaylistContent
from ..serializers import PlaylistContentSerializer, PlaylistSerializer
class PlaylistViewSet(viewsets.ModelViewSet):
queryset = Playlist.objects.all()
serializer_class = PlaylistSerializer
model_permission_name = "playlist"
class PlaylistContentViewSet(viewsets.ModelViewSet):
queryset = PlaylistContent.objects.all()
serializer_class = PlaylistContentSerializer
model_permission_name = "playlistcontent"

View file

@ -0,0 +1,33 @@
from rest_framework import viewsets
from ..models import ImportedPodcast, Podcast, PodcastEpisode, StationPodcast
from ..serializers import (
ImportedPodcastSerializer,
PodcastEpisodeSerializer,
PodcastSerializer,
StationPodcastSerializer,
)
class PodcastViewSet(viewsets.ModelViewSet):
queryset = Podcast.objects.all()
serializer_class = PodcastSerializer
model_permission_name = "podcast"
class PodcastEpisodeViewSet(viewsets.ModelViewSet):
queryset = PodcastEpisode.objects.all()
serializer_class = PodcastEpisodeSerializer
model_permission_name = "podcastepisode"
class StationPodcastViewSet(viewsets.ModelViewSet):
queryset = StationPodcast.objects.all()
serializer_class = StationPodcastSerializer
model_permission_name = "station"
class ImportedPodcastViewSet(viewsets.ModelViewSet):
queryset = ImportedPodcast.objects.all()
serializer_class = ImportedPodcastSerializer
model_permission_name = "importedpodcast"

View file

@ -0,0 +1,41 @@
from django.db.models import F
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
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 ScheduleViewSet(viewsets.ModelViewSet):
queryset = Schedule.objects.all()
serializer_class = ScheduleSerializer
filter_fields = {
"starts": FILTER_NUMERICAL_LOOKUPS,
"ends": FILTER_NUMERICAL_LOOKUPS,
"playout_status": FILTER_NUMERICAL_LOOKUPS,
"broadcasted": FILTER_NUMERICAL_LOOKUPS,
}
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"))
else:
return self.queryset.filter(starts__gte=F("instance__ends"))

View file

@ -0,0 +1,40 @@
from rest_framework import viewsets
from ..models import Show, ShowDays, ShowHost, ShowInstance, ShowRebroadcast
from ..serializers import (
ShowDaysSerializer,
ShowHostSerializer,
ShowInstanceSerializer,
ShowRebroadcastSerializer,
ShowSerializer,
)
class ShowViewSet(viewsets.ModelViewSet):
queryset = Show.objects.all()
serializer_class = ShowSerializer
model_permission_name = "show"
class ShowDaysViewSet(viewsets.ModelViewSet):
queryset = ShowDays.objects.all()
serializer_class = ShowDaysSerializer
model_permission_name = "showdays"
class ShowHostViewSet(viewsets.ModelViewSet):
queryset = ShowHost.objects.all()
serializer_class = ShowHostSerializer
model_permission_name = "showhost"
class ShowInstanceViewSet(viewsets.ModelViewSet):
queryset = ShowInstance.objects.all()
serializer_class = ShowInstanceSerializer
model_permission_name = "showinstance"
class ShowRebroadcastViewSet(viewsets.ModelViewSet):
queryset = ShowRebroadcast.objects.all()
serializer_class = ShowRebroadcastSerializer
model_permission_name = "showrebroadcast"

View file

@ -0,0 +1,26 @@
from rest_framework import viewsets
from ..models import SmartBlock, SmartBlockContent, SmartBlockCriteria
from ..serializers import (
SmartBlockContentSerializer,
SmartBlockCriteriaSerializer,
SmartBlockSerializer,
)
class SmartBlockViewSet(viewsets.ModelViewSet):
queryset = SmartBlock.objects.all()
serializer_class = SmartBlockSerializer
model_permission_name = "smartblock"
class SmartBlockContentViewSet(viewsets.ModelViewSet):
queryset = SmartBlockContent.objects.all()
serializer_class = SmartBlockContentSerializer
model_permission_name = "smartblockcontent"
class SmartBlockCriteriaViewSet(viewsets.ModelViewSet):
queryset = SmartBlockCriteria.objects.all()
serializer_class = SmartBlockCriteriaSerializer
model_permission_name = "smartblockcriteria"

View file

@ -0,0 +1,16 @@
from rest_framework import viewsets
from ..models import Webstream, WebstreamMetadata
from ..serializers import WebstreamMetadataSerializer, WebstreamSerializer
class WebstreamViewSet(viewsets.ModelViewSet):
queryset = Webstream.objects.all()
serializer_class = WebstreamSerializer
model_permission_name = "webstream"
class WebstreamMetadataViewSet(viewsets.ModelViewSet):
queryset = WebstreamMetadata.objects.all()
serializer_class = WebstreamMetadataSerializer
model_permission_name = "webstreametadata"