diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9bcb32885..17bdd270c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -202,7 +202,6 @@ jobs: run: shell: bash env: - LIBRETIME_GENERAL_API_KEY: test_key LIBRETIME_DATABASE_HOST: postgres steps: diff --git a/api/Makefile b/api/Makefile index 6e1697d0c..9f3d07a76 100644 --- a/api/Makefile +++ b/api/Makefile @@ -13,4 +13,4 @@ clean: .clean test: $(VENV) source $(VENV)/bin/activate - LIBRETIME_DEBUG=True $(VENV)/bin/libretime-api test libretime_api + DJANGO_SETTINGS_MODULE=libretime_api.settings.testing libretime-api test libretime_api diff --git a/api/libretime_api/_constants.py b/api/libretime_api/_constants.py new file mode 100644 index 000000000..3d91ebbea --- /dev/null +++ b/api/libretime_api/_constants.py @@ -0,0 +1,8 @@ +FILTER_NUMERICAL_LOOKUPS = [ + "exact", + "gt", + "lt", + "gte", + "lte", + "range", +] diff --git a/api/libretime_api/_fixtures/__init__.py b/api/libretime_api/_fixtures/__init__.py new file mode 100644 index 000000000..f43ffd7f4 --- /dev/null +++ b/api/libretime_api/_fixtures/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +fixture_path = Path(__file__).resolve().parent + +AUDIO_FILENAME = "song.mp3" diff --git a/api/libretime_api/tests/resources/song.mp3 b/api/libretime_api/_fixtures/song.mp3 similarity index 100% rename from api/libretime_api/tests/resources/song.mp3 rename to api/libretime_api/_fixtures/song.mp3 diff --git a/api/libretime_api/apps.py b/api/libretime_api/apps.py deleted file mode 100644 index e933d8ca4..000000000 --- a/api/libretime_api/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig -from django.db.models.signals import pre_save - - -class LibreTimeAPIConfig(AppConfig): - name = "libretime_api" - verbose_name = "LibreTime API" - default_auto_field = "django.db.models.AutoField" diff --git a/api/libretime_api/asgi.py b/api/libretime_api/asgi.py new file mode 100644 index 000000000..dd084c66f --- /dev/null +++ b/api/libretime_api/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for libretime_api project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings.prod") + +application = get_asgi_application() diff --git a/api/libretime_api/tests/models/__init__.py b/api/libretime_api/core/__init__.py similarity index 100% rename from api/libretime_api/tests/models/__init__.py rename to api/libretime_api/core/__init__.py diff --git a/api/libretime_api/core/apps.py b/api/libretime_api/core/apps.py new file mode 100644 index 000000000..41373d854 --- /dev/null +++ b/api/libretime_api/core/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "libretime_api.core" + verbose_name = "LibreTime Core API" diff --git a/api/libretime_api/core/models/__init__.py b/api/libretime_api/core/models/__init__.py new file mode 100644 index 000000000..0071bc99a --- /dev/null +++ b/api/libretime_api/core/models/__init__.py @@ -0,0 +1,7 @@ +from .auth import LoginAttempt, Session, UserToken +from .country import Country +from .preference import Preference, StreamSetting +from .role import ADMIN, DJ, GUEST, PROGRAM_MANAGER, USER_TYPES +from .service import ServiceRegister +from .user import User, UserManager +from .worker import CeleryTask, ThirdPartyTrackReference diff --git a/api/libretime_api/core/models/auth.py b/api/libretime_api/core/models/auth.py new file mode 100644 index 000000000..d7faedeb9 --- /dev/null +++ b/api/libretime_api/core/models/auth.py @@ -0,0 +1,37 @@ +from django.db import models + + +class UserToken(models.Model): + user = models.ForeignKey("User", models.DO_NOTHING) + action = models.CharField(max_length=255) + token = models.CharField(unique=True, max_length=40) + created = models.DateTimeField() + + def get_owner(self): + return self.user + + class Meta: + managed = False + db_table = "cc_subjs_token" + + +class Session(models.Model): + sessid = models.CharField(primary_key=True, max_length=32) + userid = models.ForeignKey( + "User", models.DO_NOTHING, db_column="userid", blank=True, null=True + ) + login = models.CharField(max_length=255, blank=True, null=True) + ts = models.DateTimeField(blank=True, null=True) + + class Meta: + managed = False + db_table = "cc_sess" + + +class LoginAttempt(models.Model): + ip = models.CharField(primary_key=True, max_length=32) + attempts = models.IntegerField(blank=True, null=True) + + class Meta: + managed = False + db_table = "cc_login_attempts" diff --git a/api/libretime_api/models/countries.py b/api/libretime_api/core/models/country.py similarity index 100% rename from api/libretime_api/models/countries.py rename to api/libretime_api/core/models/country.py diff --git a/api/libretime_api/models/preferences.py b/api/libretime_api/core/models/preference.py similarity index 81% rename from api/libretime_api/models/preferences.py rename to api/libretime_api/core/models/preference.py index aca4e5a92..cc3737a4d 100644 --- a/api/libretime_api/models/preferences.py +++ b/api/libretime_api/core/models/preference.py @@ -14,14 +14,6 @@ class Preference(models.Model): unique_together = (("subjid", "keystr"),) -class MountName(models.Model): - mount_name = models.CharField(max_length=1024) - - class Meta: - managed = False - db_table = "cc_mount_name" - - class StreamSetting(models.Model): keyname = models.CharField(primary_key=True, max_length=64) value = models.CharField(max_length=255, blank=True, null=True) diff --git a/api/libretime_api/models/user_constants.py b/api/libretime_api/core/models/role.py similarity index 100% rename from api/libretime_api/models/user_constants.py rename to api/libretime_api/core/models/role.py diff --git a/api/libretime_api/models/services.py b/api/libretime_api/core/models/service.py similarity index 100% rename from api/libretime_api/models/services.py rename to api/libretime_api/core/models/service.py diff --git a/api/libretime_api/models/authentication.py b/api/libretime_api/core/models/user.py similarity index 73% rename from api/libretime_api/models/authentication.py rename to api/libretime_api/core/models/user.py index 387f53b40..30e1a2ef9 100644 --- a/api/libretime_api/models/authentication.py +++ b/api/libretime_api/core/models/user.py @@ -1,43 +1,37 @@ import hashlib -from django.contrib import auth -from django.contrib.auth.models import AbstractBaseUser, Permission -from django.core.exceptions import PermissionDenied +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Permission from django.db import models -from libretime_api.managers import UserManager -from libretime_api.permission_constants import GROUPS - -from .user_constants import ADMIN, USER_TYPES - - -class LoginAttempt(models.Model): - ip = models.CharField(primary_key=True, max_length=32) - attempts = models.IntegerField(blank=True, null=True) - - class Meta: - managed = False - db_table = "cc_login_attempts" - - -class Session(models.Model): - sessid = models.CharField(primary_key=True, max_length=32) - userid = models.ForeignKey( - "User", models.DO_NOTHING, db_column="userid", blank=True, null=True - ) - login = models.CharField(max_length=255, blank=True, null=True) - ts = models.DateTimeField(blank=True, null=True) - - class Meta: - managed = False - db_table = "cc_sess" - +from ...permission_constants import GROUPS +from .role import ADMIN, USER_TYPES USER_TYPE_CHOICES = () for item in USER_TYPES.items(): USER_TYPE_CHOICES = USER_TYPE_CHOICES + (item,) +class UserManager(BaseUserManager): + def create_user(self, username, type, email, first_name, last_name, password): + user = self.model( + username=username, + type=type, + email=email, + first_name=first_name, + last_name=last_name, + ) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, email, first_name, last_name, password): + user = self.create_user(username, "A", email, first_name, last_name, password) + return user + + def get_by_natural_key(self, username): + return self.get(username=username) + + class User(AbstractBaseUser): username = models.CharField(db_column="login", unique=True, max_length=255) password = models.CharField( @@ -132,17 +126,3 @@ class User(AbstractBaseUser): class Meta: managed = False db_table = "cc_subjs" - - -class UserToken(models.Model): - user = models.ForeignKey(User, models.DO_NOTHING) - action = models.CharField(max_length=255) - token = models.CharField(unique=True, max_length=40) - created = models.DateTimeField() - - def get_owner(self): - return self.user - - class Meta: - managed = False - db_table = "cc_subjs_token" diff --git a/api/libretime_api/models/celery.py b/api/libretime_api/core/models/worker.py similarity index 50% rename from api/libretime_api/models/celery.py rename to api/libretime_api/core/models/worker.py index 6a2b40881..4fcde6cfb 100644 --- a/api/libretime_api/models/celery.py +++ b/api/libretime_api/core/models/worker.py @@ -1,6 +1,18 @@ from django.db import models +class ThirdPartyTrackReference(models.Model): + service = models.CharField(max_length=256) + foreign_id = models.CharField(unique=True, max_length=256, blank=True, null=True) + file = models.ForeignKey("storage.File", models.DO_NOTHING, blank=True, null=True) + upload_time = models.DateTimeField(blank=True, null=True) + status = models.CharField(max_length=256, blank=True, null=True) + + class Meta: + managed = False + db_table = "third_party_track_references" + + class CeleryTask(models.Model): task_id = models.CharField(max_length=256) track_reference = models.ForeignKey( diff --git a/api/libretime_api/core/router.py b/api/libretime_api/core/router.py new file mode 100644 index 000000000..a8a1304b9 --- /dev/null +++ b/api/libretime_api/core/router.py @@ -0,0 +1,26 @@ +from rest_framework import routers + +from .views import ( + CeleryTaskViewSet, + CountryViewSet, + LoginAttemptViewSet, + PreferenceViewSet, + ServiceRegisterViewSet, + SessionViewSet, + StreamSettingViewSet, + ThirdPartyTrackReferenceViewSet, + UserTokenViewSet, + UserViewSet, +) + +router = routers.DefaultRouter() +router.register("countries", CountryViewSet) +router.register("login-attempts", LoginAttemptViewSet) +router.register("preferences", PreferenceViewSet) +router.register("service-registers", ServiceRegisterViewSet) +router.register("sessions", SessionViewSet) +router.register("stream-settings", StreamSettingViewSet) +router.register("users", UserViewSet) +router.register("user-tokens", UserTokenViewSet) +router.register("celery-tasks", CeleryTaskViewSet) +router.register("third-party-track-references", ThirdPartyTrackReferenceViewSet) diff --git a/api/libretime_api/core/serializers/__init__.py b/api/libretime_api/core/serializers/__init__.py new file mode 100644 index 000000000..31870bcf2 --- /dev/null +++ b/api/libretime_api/core/serializers/__init__.py @@ -0,0 +1,6 @@ +from .auth import LoginAttemptSerializer, SessionSerializer, UserTokenSerializer +from .country import CountrySerializer +from .preference import PreferenceSerializer, StreamSettingSerializer +from .service import ServiceRegisterSerializer +from .user import UserSerializer +from .worker import CeleryTaskSerializer, ThirdPartyTrackReferenceSerializer diff --git a/api/libretime_api/core/serializers/auth.py b/api/libretime_api/core/serializers/auth.py new file mode 100644 index 000000000..732bd5d5c --- /dev/null +++ b/api/libretime_api/core/serializers/auth.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from ..models import LoginAttempt, Session, UserToken + + +class UserTokenSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = UserToken + fields = "__all__" + + +class SessionSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Session + fields = "__all__" + + +class LoginAttemptSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = LoginAttempt + fields = "__all__" diff --git a/api/libretime_api/core/serializers/country.py b/api/libretime_api/core/serializers/country.py new file mode 100644 index 000000000..c29e05d72 --- /dev/null +++ b/api/libretime_api/core/serializers/country.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from ..models import Country + + +class CountrySerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Country + fields = "__all__" diff --git a/api/libretime_api/core/serializers/preference.py b/api/libretime_api/core/serializers/preference.py new file mode 100644 index 000000000..adf175eed --- /dev/null +++ b/api/libretime_api/core/serializers/preference.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from ..models import Preference, StreamSetting + + +class PreferenceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Preference + fields = "__all__" + + +class StreamSettingSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = StreamSetting + fields = "__all__" diff --git a/api/libretime_api/core/serializers/service.py b/api/libretime_api/core/serializers/service.py new file mode 100644 index 000000000..cd02e19f8 --- /dev/null +++ b/api/libretime_api/core/serializers/service.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from ..models import ServiceRegister + + +class ServiceRegisterSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = ServiceRegister + fields = "__all__" diff --git a/api/libretime_api/core/serializers/user.py b/api/libretime_api/core/serializers/user.py new file mode 100644 index 000000000..445c82431 --- /dev/null +++ b/api/libretime_api/core/serializers/user.py @@ -0,0 +1,20 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = get_user_model() + fields = [ + "item_url", + "username", + "type", + "first_name", + "last_name", + "lastfail", + "skype_contact", + "jabber_contact", + "email", + "cell_phone", + "login_attempts", + ] diff --git a/api/libretime_api/core/serializers/worker.py b/api/libretime_api/core/serializers/worker.py new file mode 100644 index 000000000..e365bb3d5 --- /dev/null +++ b/api/libretime_api/core/serializers/worker.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from ..models import CeleryTask, ThirdPartyTrackReference + + +class ThirdPartyTrackReferenceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = ThirdPartyTrackReference + fields = "__all__" + + +class CeleryTaskSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = CeleryTask + fields = "__all__" diff --git a/api/libretime_api/core/tests/__init__.py b/api/libretime_api/core/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/core/tests/models/__init__.py b/api/libretime_api/core/tests/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/tests/test_models.py b/api/libretime_api/core/tests/models/test_user.py similarity index 87% rename from api/libretime_api/tests/test_models.py rename to api/libretime_api/core/tests/models/test_user.py index 7371e1ff5..db96ee02d 100644 --- a/api/libretime_api/tests/test_models.py +++ b/api/libretime_api/core/tests/models/test_user.py @@ -1,10 +1,8 @@ from django.apps import apps -from django.contrib.auth.models import Group from rest_framework.test import APITestCase -from libretime_api.models import User -from libretime_api.models.user_constants import DJ, GUEST -from libretime_api.permission_constants import GROUPS +from ....permission_constants import GROUPS +from ...models import DJ, GUEST, User class TestUserManager(APITestCase): diff --git a/api/libretime_api/core/views/__init__.py b/api/libretime_api/core/views/__init__.py new file mode 100644 index 000000000..54cb28689 --- /dev/null +++ b/api/libretime_api/core/views/__init__.py @@ -0,0 +1,7 @@ +from .auth import LoginAttemptViewSet, SessionViewSet, UserTokenViewSet +from .country import CountryViewSet +from .preference import PreferenceViewSet, StreamSettingViewSet +from .service import ServiceRegisterViewSet +from .user import UserViewSet +from .version import version +from .worker import CeleryTaskViewSet, ThirdPartyTrackReferenceViewSet diff --git a/api/libretime_api/core/views/auth.py b/api/libretime_api/core/views/auth.py new file mode 100644 index 000000000..2f138f947 --- /dev/null +++ b/api/libretime_api/core/views/auth.py @@ -0,0 +1,22 @@ +from rest_framework import viewsets + +from ..models import LoginAttempt, Session, UserToken +from ..serializers import LoginAttemptSerializer, SessionSerializer, UserTokenSerializer + + +class UserTokenViewSet(viewsets.ModelViewSet): + queryset = UserToken.objects.all() + serializer_class = UserTokenSerializer + model_permission_name = "usertoken" + + +class SessionViewSet(viewsets.ModelViewSet): + queryset = Session.objects.all() + serializer_class = SessionSerializer + model_permission_name = "session" + + +class LoginAttemptViewSet(viewsets.ModelViewSet): + queryset = LoginAttempt.objects.all() + serializer_class = LoginAttemptSerializer + model_permission_name = "loginattempt" diff --git a/api/libretime_api/core/views/country.py b/api/libretime_api/core/views/country.py new file mode 100644 index 000000000..b28bdfbeb --- /dev/null +++ b/api/libretime_api/core/views/country.py @@ -0,0 +1,10 @@ +from rest_framework import viewsets + +from ..models import Country +from ..serializers import CountrySerializer + + +class CountryViewSet(viewsets.ModelViewSet): + queryset = Country.objects.all() + serializer_class = CountrySerializer + model_permission_name = "country" diff --git a/api/libretime_api/core/views/preference.py b/api/libretime_api/core/views/preference.py new file mode 100644 index 000000000..c1a3401d2 --- /dev/null +++ b/api/libretime_api/core/views/preference.py @@ -0,0 +1,16 @@ +from rest_framework import viewsets + +from ..models import Preference, StreamSetting +from ..serializers import PreferenceSerializer, StreamSettingSerializer + + +class PreferenceViewSet(viewsets.ModelViewSet): + queryset = Preference.objects.all() + serializer_class = PreferenceSerializer + model_permission_name = "preference" + + +class StreamSettingViewSet(viewsets.ModelViewSet): + queryset = StreamSetting.objects.all() + serializer_class = StreamSettingSerializer + model_permission_name = "streamsetting" diff --git a/api/libretime_api/core/views/service.py b/api/libretime_api/core/views/service.py new file mode 100644 index 000000000..ebbe4ee08 --- /dev/null +++ b/api/libretime_api/core/views/service.py @@ -0,0 +1,10 @@ +from rest_framework import viewsets + +from ..models import ServiceRegister +from ..serializers import ServiceRegisterSerializer + + +class ServiceRegisterViewSet(viewsets.ModelViewSet): + queryset = ServiceRegister.objects.all() + serializer_class = ServiceRegisterSerializer + model_permission_name = "serviceregister" diff --git a/api/libretime_api/core/views/user.py b/api/libretime_api/core/views/user.py new file mode 100644 index 000000000..8766f4c4a --- /dev/null +++ b/api/libretime_api/core/views/user.py @@ -0,0 +1,12 @@ +from django.contrib.auth import get_user_model +from rest_framework import viewsets + +from ...permissions import IsAdminOrOwnUser +from ..serializers import UserSerializer + + +class UserViewSet(viewsets.ModelViewSet): + queryset = get_user_model().objects.all() + serializer_class = UserSerializer + permission_classes = [IsAdminOrOwnUser] + model_permission_name = "user" diff --git a/api/libretime_api/core/views/version.py b/api/libretime_api/core/views/version.py new file mode 100644 index 000000000..ef05e401d --- /dev/null +++ b/api/libretime_api/core/views/version.py @@ -0,0 +1,10 @@ +from django.conf import settings +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + + +@api_view(["GET"]) +@permission_classes((AllowAny,)) +def version(request, *args, **kwargs): + return Response({"api_version": settings.API_VERSION}) diff --git a/api/libretime_api/core/views/worker.py b/api/libretime_api/core/views/worker.py new file mode 100644 index 000000000..1e3296f93 --- /dev/null +++ b/api/libretime_api/core/views/worker.py @@ -0,0 +1,16 @@ +from rest_framework import viewsets + +from ..models import CeleryTask, ThirdPartyTrackReference +from ..serializers import CeleryTaskSerializer, ThirdPartyTrackReferenceSerializer + + +class ThirdPartyTrackReferenceViewSet(viewsets.ModelViewSet): + queryset = ThirdPartyTrackReference.objects.all() + serializer_class = ThirdPartyTrackReferenceSerializer + model_permission_name = "thirdpartytrackreference" + + +class CeleryTaskViewSet(viewsets.ModelViewSet): + queryset = CeleryTask.objects.all() + serializer_class = CeleryTaskSerializer + model_permission_name = "celerytask" diff --git a/api/libretime_api/history/__init__.py b/api/libretime_api/history/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/history/apps.py b/api/libretime_api/history/apps.py new file mode 100644 index 000000000..eb53c7dda --- /dev/null +++ b/api/libretime_api/history/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class HistoryConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "libretime_api.history" + verbose_name = "LibreTime History API" diff --git a/api/libretime_api/history/models/__init__.py b/api/libretime_api/history/models/__init__.py new file mode 100644 index 000000000..5d33d30a1 --- /dev/null +++ b/api/libretime_api/history/models/__init__.py @@ -0,0 +1,8 @@ +from .listener import ListenerCount, MountName, Timestamp +from .live import LiveLog +from .played import ( + PlayoutHistory, + PlayoutHistoryMetadata, + PlayoutHistoryTemplate, + PlayoutHistoryTemplateField, +) diff --git a/api/libretime_api/history/models/listener.py b/api/libretime_api/history/models/listener.py new file mode 100644 index 000000000..038ff56e2 --- /dev/null +++ b/api/libretime_api/history/models/listener.py @@ -0,0 +1,27 @@ +from django.db import models + + +class MountName(models.Model): + mount_name = models.CharField(max_length=1024) + + class Meta: + managed = False + db_table = "cc_mount_name" + + +class Timestamp(models.Model): + timestamp = models.DateTimeField() + + class Meta: + managed = False + db_table = "cc_timestamp" + + +class ListenerCount(models.Model): + timestamp = models.ForeignKey("Timestamp", models.DO_NOTHING) + mount_name = models.ForeignKey("MountName", models.DO_NOTHING) + listener_count = models.IntegerField() + + class Meta: + managed = False + db_table = "cc_listener_count" diff --git a/api/libretime_api/history/models/live.py b/api/libretime_api/history/models/live.py new file mode 100644 index 000000000..7adf691ca --- /dev/null +++ b/api/libretime_api/history/models/live.py @@ -0,0 +1,11 @@ +from django.db import models + + +class LiveLog(models.Model): + state = models.CharField(max_length=32) + start_time = models.DateTimeField() + end_time = models.DateTimeField(blank=True, null=True) + + class Meta: + managed = False + db_table = "cc_live_log" diff --git a/api/libretime_api/models/playout.py b/api/libretime_api/history/models/played.py similarity index 52% rename from api/libretime_api/models/playout.py rename to api/libretime_api/history/models/played.py index 7659932da..2263dec49 100644 --- a/api/libretime_api/models/playout.py +++ b/api/libretime_api/history/models/played.py @@ -1,34 +1,12 @@ from django.db import models -from .files import File - - -class ListenerCount(models.Model): - timestamp = models.ForeignKey("Timestamp", models.DO_NOTHING) - mount_name = models.ForeignKey("MountName", models.DO_NOTHING) - listener_count = models.IntegerField() - - class Meta: - managed = False - db_table = "cc_listener_count" - - -class LiveLog(models.Model): - state = models.CharField(max_length=32) - start_time = models.DateTimeField() - end_time = models.DateTimeField(blank=True, null=True) - - class Meta: - managed = False - db_table = "cc_live_log" - class PlayoutHistory(models.Model): - file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True) + file = models.ForeignKey("storage.File", models.DO_NOTHING, blank=True, null=True) starts = models.DateTimeField() ends = models.DateTimeField(blank=True, null=True) instance = models.ForeignKey( - "ShowInstance", models.DO_NOTHING, blank=True, null=True + "schedule.ShowInstance", models.DO_NOTHING, blank=True, null=True ) class Meta: @@ -37,7 +15,7 @@ class PlayoutHistory(models.Model): class PlayoutHistoryMetadata(models.Model): - history = models.ForeignKey(PlayoutHistory, models.DO_NOTHING) + history = models.ForeignKey("PlayoutHistory", models.DO_NOTHING) key = models.CharField(max_length=128) value = models.CharField(max_length=128) @@ -56,7 +34,7 @@ class PlayoutHistoryTemplate(models.Model): class PlayoutHistoryTemplateField(models.Model): - template = models.ForeignKey(PlayoutHistoryTemplate, models.DO_NOTHING) + template = models.ForeignKey("PlayoutHistoryTemplate", models.DO_NOTHING) name = models.CharField(max_length=128) label = models.CharField(max_length=128) type = models.CharField(max_length=128) @@ -66,11 +44,3 @@ class PlayoutHistoryTemplateField(models.Model): class Meta: managed = False db_table = "cc_playout_history_template_field" - - -class Timestamp(models.Model): - timestamp = models.DateTimeField() - - class Meta: - managed = False - db_table = "cc_timestamp" diff --git a/api/libretime_api/history/router.py b/api/libretime_api/history/router.py new file mode 100644 index 000000000..23b2d45f8 --- /dev/null +++ b/api/libretime_api/history/router.py @@ -0,0 +1,22 @@ +from rest_framework import routers + +from .views import ( + ListenerCountViewSet, + LiveLogViewSet, + MountNameViewSet, + PlayoutHistoryMetadataViewSet, + PlayoutHistoryTemplateFieldViewSet, + PlayoutHistoryTemplateViewSet, + PlayoutHistoryViewSet, + TimestampViewSet, +) + +router = routers.DefaultRouter() +router.register("listener-counts", ListenerCountViewSet) +router.register("live-logs", LiveLogViewSet) +router.register("mount-names", MountNameViewSet) +router.register("playout-history", PlayoutHistoryViewSet) +router.register("playout-history-metadata", PlayoutHistoryMetadataViewSet) +router.register("playout-history-templates", PlayoutHistoryTemplateViewSet) +router.register("playout-history-template-fields", PlayoutHistoryTemplateFieldViewSet) +router.register("timestamps", TimestampViewSet) diff --git a/api/libretime_api/history/serializers/__init__.py b/api/libretime_api/history/serializers/__init__.py new file mode 100644 index 000000000..b6c9a40e5 --- /dev/null +++ b/api/libretime_api/history/serializers/__init__.py @@ -0,0 +1,8 @@ +from .listener import ListenerCountSerializer, MountNameSerializer, TimestampSerializer +from .live import LiveLogSerializer +from .played import ( + PlayoutHistoryMetadataSerializer, + PlayoutHistorySerializer, + PlayoutHistoryTemplateFieldSerializer, + PlayoutHistoryTemplateSerializer, +) diff --git a/api/libretime_api/history/serializers/listener.py b/api/libretime_api/history/serializers/listener.py new file mode 100644 index 000000000..cdd2f8a84 --- /dev/null +++ b/api/libretime_api/history/serializers/listener.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from ..models import ListenerCount, MountName, Timestamp + + +class MountNameSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = MountName + fields = "__all__" + + +class TimestampSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Timestamp + fields = "__all__" + + +class ListenerCountSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = ListenerCount + fields = "__all__" diff --git a/api/libretime_api/history/serializers/live.py b/api/libretime_api/history/serializers/live.py new file mode 100644 index 000000000..2668219e0 --- /dev/null +++ b/api/libretime_api/history/serializers/live.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from ..models import LiveLog + + +class LiveLogSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = LiveLog + fields = "__all__" diff --git a/api/libretime_api/history/serializers/played.py b/api/libretime_api/history/serializers/played.py new file mode 100644 index 000000000..66f771c48 --- /dev/null +++ b/api/libretime_api/history/serializers/played.py @@ -0,0 +1,32 @@ +from rest_framework import serializers + +from ..models import ( + PlayoutHistory, + PlayoutHistoryMetadata, + PlayoutHistoryTemplate, + PlayoutHistoryTemplateField, +) + + +class PlayoutHistorySerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = PlayoutHistory + fields = "__all__" + + +class PlayoutHistoryMetadataSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = PlayoutHistoryMetadata + fields = "__all__" + + +class PlayoutHistoryTemplateSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = PlayoutHistoryTemplate + fields = "__all__" + + +class PlayoutHistoryTemplateFieldSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = PlayoutHistoryTemplateField + fields = "__all__" diff --git a/api/libretime_api/history/tests/__init__.py b/api/libretime_api/history/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/history/views/__init__.py b/api/libretime_api/history/views/__init__.py new file mode 100644 index 000000000..8b2fc552c --- /dev/null +++ b/api/libretime_api/history/views/__init__.py @@ -0,0 +1,8 @@ +from .listener import ListenerCountViewSet, MountNameViewSet, TimestampViewSet +from .live import LiveLogViewSet +from .played import ( + PlayoutHistoryMetadataViewSet, + PlayoutHistoryTemplateFieldViewSet, + PlayoutHistoryTemplateViewSet, + PlayoutHistoryViewSet, +) diff --git a/api/libretime_api/history/views/listener.py b/api/libretime_api/history/views/listener.py new file mode 100644 index 000000000..8f33e4f5d --- /dev/null +++ b/api/libretime_api/history/views/listener.py @@ -0,0 +1,26 @@ +from rest_framework import viewsets + +from ..models import ListenerCount, MountName, Timestamp +from ..serializers import ( + ListenerCountSerializer, + MountNameSerializer, + TimestampSerializer, +) + + +class MountNameViewSet(viewsets.ModelViewSet): + queryset = MountName.objects.all() + serializer_class = MountNameSerializer + model_permission_name = "mountname" + + +class TimestampViewSet(viewsets.ModelViewSet): + queryset = Timestamp.objects.all() + serializer_class = TimestampSerializer + model_permission_name = "timestamp" + + +class ListenerCountViewSet(viewsets.ModelViewSet): + queryset = ListenerCount.objects.all() + serializer_class = ListenerCountSerializer + model_permission_name = "listenercount" diff --git a/api/libretime_api/history/views/live.py b/api/libretime_api/history/views/live.py new file mode 100644 index 000000000..069416034 --- /dev/null +++ b/api/libretime_api/history/views/live.py @@ -0,0 +1,10 @@ +from rest_framework import viewsets + +from ..models import LiveLog +from ..serializers import LiveLogSerializer + + +class LiveLogViewSet(viewsets.ModelViewSet): + queryset = LiveLog.objects.all() + serializer_class = LiveLogSerializer + model_permission_name = "livelog" diff --git a/api/libretime_api/history/views/played.py b/api/libretime_api/history/views/played.py new file mode 100644 index 000000000..15b97f74d --- /dev/null +++ b/api/libretime_api/history/views/played.py @@ -0,0 +1,38 @@ +from rest_framework import viewsets + +from ..models import ( + PlayoutHistory, + PlayoutHistoryMetadata, + PlayoutHistoryTemplate, + PlayoutHistoryTemplateField, +) +from ..serializers import ( + PlayoutHistoryMetadataSerializer, + PlayoutHistorySerializer, + PlayoutHistoryTemplateFieldSerializer, + PlayoutHistoryTemplateSerializer, +) + + +class PlayoutHistoryViewSet(viewsets.ModelViewSet): + queryset = PlayoutHistory.objects.all() + serializer_class = PlayoutHistorySerializer + model_permission_name = "playouthistory" + + +class PlayoutHistoryMetadataViewSet(viewsets.ModelViewSet): + queryset = PlayoutHistoryMetadata.objects.all() + serializer_class = PlayoutHistoryMetadataSerializer + model_permission_name = "playouthistorymetadata" + + +class PlayoutHistoryTemplateViewSet(viewsets.ModelViewSet): + queryset = PlayoutHistoryTemplate.objects.all() + serializer_class = PlayoutHistoryTemplateSerializer + model_permission_name = "playouthistorytemplate" + + +class PlayoutHistoryTemplateFieldViewSet(viewsets.ModelViewSet): + queryset = PlayoutHistoryTemplateField.objects.all() + serializer_class = PlayoutHistoryTemplateFieldSerializer + model_permission_name = "playouthistorytemplatefield" diff --git a/api/libretime_api/cli.py b/api/libretime_api/manage.py similarity index 89% rename from api/libretime_api/cli.py rename to api/libretime_api/manage.py index 7e94f7df0..63c2abeec 100755 --- a/api/libretime_api/cli.py +++ b/api/libretime_api/manage.py @@ -1,12 +1,12 @@ -#!/usr/bin/env python3 - +#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings") + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings.prod") os.environ.setdefault("LIBRETIME_CONFIG_FILEPATH", "/etc/airtime/airtime.conf") try: from django.core.management import execute_from_command_line diff --git a/api/libretime_api/managers.py b/api/libretime_api/managers.py deleted file mode 100644 index 5572229ec..000000000 --- a/api/libretime_api/managers.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.contrib.auth.models import BaseUserManager - - -class UserManager(BaseUserManager): - def create_user(self, username, type, email, first_name, last_name, password): - user = self.model( - username=username, - type=type, - email=email, - first_name=first_name, - last_name=last_name, - ) - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, username, email, first_name, last_name, password): - user = self.create_user(username, "A", email, first_name, last_name, password) - return user - - def get_by_natural_key(self, username): - return self.get(username=username) diff --git a/api/libretime_api/models/__init__.py b/api/libretime_api/models/__init__.py deleted file mode 100644 index a6c479d8b..000000000 --- a/api/libretime_api/models/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from .authentication import * -from .celery import * -from .countries import * -from .files import * -from .playlists import * -from .playout import * -from .podcasts import * -from .preferences import * -from .schedule import * -from .services import * -from .shows import * -from .smart_blocks import * -from .tracks import * -from .webstreams import * diff --git a/api/libretime_api/models/tracks.py b/api/libretime_api/models/tracks.py deleted file mode 100644 index 8467d0517..000000000 --- a/api/libretime_api/models/tracks.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.db import models - -from .files import File - - -class ThirdPartyTrackReference(models.Model): - service = models.CharField(max_length=256) - foreign_id = models.CharField(unique=True, max_length=256, blank=True, null=True) - file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True) - upload_time = models.DateTimeField(blank=True, null=True) - status = models.CharField(max_length=256, blank=True, null=True) - - class Meta: - managed = False - db_table = "third_party_track_references" - - -class TrackType(models.Model): - code = models.CharField(max_length=16, unique=True) - type_name = models.CharField(max_length=255, blank=True, null=True) - description = models.CharField(max_length=255, blank=True, null=True) - visibility = models.BooleanField(blank=True, default=True) - - class Meta: - managed = False - db_table = "cc_track_types" diff --git a/api/libretime_api/permission_constants.py b/api/libretime_api/permission_constants.py index eead5598f..8431a9e0d 100644 --- a/api/libretime_api/permission_constants.py +++ b/api/libretime_api/permission_constants.py @@ -1,10 +1,4 @@ -import logging - -from django.contrib.auth.models import Group, Permission - -from .models.user_constants import DJ, GUEST, PROGRAM_MANAGER, USER_TYPES - -logger = logging.getLogger(__name__) +from .core.models import DJ, GUEST, PROGRAM_MANAGER, USER_TYPES GUEST_PERMISSIONS = [ "view_schedule", @@ -24,6 +18,7 @@ GUEST_PERMISSIONS = [ "view_webstream", "view_apiroot", ] + DJ_PERMISSIONS = GUEST_PERMISSIONS + [ "add_file", "add_podcast", diff --git a/api/libretime_api/permissions.py b/api/libretime_api/permissions.py index 916a5eba7..539621fb5 100644 --- a/api/libretime_api/permissions.py +++ b/api/libretime_api/permissions.py @@ -1,7 +1,7 @@ from django.conf import settings from rest_framework.permissions import BasePermission -from .models.user_constants import DJ +from .core.models.role import DJ REQUEST_PERMISSION_TYPE_MAP = { "GET": "view", diff --git a/api/libretime_api/schedule/__init__.py b/api/libretime_api/schedule/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/schedule/apps.py b/api/libretime_api/schedule/apps.py new file mode 100644 index 000000000..db2601c91 --- /dev/null +++ b/api/libretime_api/schedule/apps.py @@ -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" diff --git a/api/libretime_api/schedule/models/__init__.py b/api/libretime_api/schedule/models/__init__.py new file mode 100644 index 000000000..f068ed279 --- /dev/null +++ b/api/libretime_api/schedule/models/__init__.py @@ -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 diff --git a/api/libretime_api/models/playlists.py b/api/libretime_api/schedule/models/playlist.py similarity index 74% rename from api/libretime_api/models/playlists.py rename to api/libretime_api/schedule/models/playlist.py index 98638a029..1454f2587 100644 --- a/api/libretime_api/models/playlists.py +++ b/api/libretime_api/schedule/models/playlist.py @@ -1,14 +1,11 @@ from django.db import models -from .files import File -from .smart_blocks import SmartBlock - 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("User", models.DO_NOTHING, 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) @@ -21,9 +18,9 @@ class Playlist(models.Model): class PlaylistContent(models.Model): - playlist = models.ForeignKey(Playlist, models.DO_NOTHING, blank=True, null=True) - file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True) - block = models.ForeignKey(SmartBlock, models.DO_NOTHING, blank=True, null=True) + 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) diff --git a/api/libretime_api/models/podcasts.py b/api/libretime_api/schedule/models/podcast.py similarity index 89% rename from api/libretime_api/models/podcasts.py rename to api/libretime_api/schedule/models/podcast.py index 0640b2c25..95d9ffacc 100644 --- a/api/libretime_api/models/podcasts.py +++ b/api/libretime_api/schedule/models/podcast.py @@ -1,22 +1,5 @@ from django.db import models -from .authentication import User -from .files import File - - -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" - class Podcast(models.Model): url = models.CharField(max_length=4096) @@ -33,7 +16,7 @@ class Podcast(models.Model): 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( - User, models.DO_NOTHING, db_column="owner", blank=True, null=True + "core.User", models.DO_NOTHING, db_column="owner", blank=True, null=True ) def get_owner(self): @@ -49,8 +32,8 @@ class Podcast(models.Model): class PodcastEpisode(models.Model): - file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True) - podcast = models.ForeignKey(Podcast, models.DO_NOTHING) + 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) @@ -76,7 +59,7 @@ class PodcastEpisode(models.Model): class StationPodcast(models.Model): - podcast = models.ForeignKey(Podcast, models.DO_NOTHING) + podcast = models.ForeignKey("Podcast", models.DO_NOTHING) def get_owner(self): return self.podcast.owner @@ -84,3 +67,17 @@ class StationPodcast(models.Model): 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" diff --git a/api/libretime_api/models/schedule.py b/api/libretime_api/schedule/models/schedule.py similarity index 96% rename from api/libretime_api/models/schedule.py rename to api/libretime_api/schedule/models/schedule.py index 56f68a70a..4dc8b823f 100644 --- a/api/libretime_api/models/schedule.py +++ b/api/libretime_api/schedule/models/schedule.py @@ -1,12 +1,10 @@ from django.db import models -from .files import File - class Schedule(models.Model): starts = models.DateTimeField() ends = models.DateTimeField() - file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True) + 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) diff --git a/api/libretime_api/models/shows.py b/api/libretime_api/schedule/models/show.py similarity index 85% rename from api/libretime_api/models/shows.py rename to api/libretime_api/schedule/models/show.py index de7abb3f3..0aa74a437 100644 --- a/api/libretime_api/models/shows.py +++ b/api/libretime_api/schedule/models/show.py @@ -1,8 +1,5 @@ from django.db import models -from .files import File -from .playlists import Playlist - class Show(models.Model): name = models.CharField(max_length=255) @@ -19,7 +16,9 @@ class Show(models.Model): 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 = models.ForeignKey( + "Playlist", models.DO_NOTHING, blank=True, null=True + ) autoplaylist_repeat = models.BooleanField() def get_owner(self): @@ -39,7 +38,7 @@ class ShowDays(models.Model): 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) + show = models.ForeignKey("Show", models.DO_NOTHING) record = models.SmallIntegerField(blank=True, null=True) def get_owner(self): @@ -51,8 +50,8 @@ class ShowDays(models.Model): class ShowHost(models.Model): - show = models.ForeignKey(Show, models.DO_NOTHING) - subjs = models.ForeignKey("User", models.DO_NOTHING) + show = models.ForeignKey("Show", models.DO_NOTHING) + subjs = models.ForeignKey("core.User", models.DO_NOTHING) class Meta: managed = False @@ -63,11 +62,11 @@ 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) + 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(File, 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) @@ -85,7 +84,7 @@ class ShowInstance(models.Model): class ShowRebroadcast(models.Model): day_offset = models.CharField(max_length=1024) start_time = models.TimeField() - show = models.ForeignKey(Show, models.DO_NOTHING) + show = models.ForeignKey("Show", models.DO_NOTHING) def get_owner(self): return show.get_owner() diff --git a/api/libretime_api/models/smart_blocks.py b/api/libretime_api/schedule/models/smart_block.py similarity index 88% rename from api/libretime_api/models/smart_blocks.py rename to api/libretime_api/schedule/models/smart_block.py index 003e0e364..f34b49154 100644 --- a/api/libretime_api/models/smart_blocks.py +++ b/api/libretime_api/schedule/models/smart_block.py @@ -5,7 +5,7 @@ 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("User", models.DO_NOTHING, 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) @@ -29,8 +29,8 @@ class SmartBlock(models.Model): class SmartBlockContent(models.Model): - block = models.ForeignKey(SmartBlock, models.DO_NOTHING, blank=True, null=True) - file = models.ForeignKey("File", models.DO_NOTHING, blank=True, null=True) + 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) @@ -63,7 +63,7 @@ class SmartBlockCriteria(models.Model): 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) + block = models.ForeignKey("SmartBlock", models.DO_NOTHING) def get_owner(self): return self.block.get_owner() diff --git a/api/libretime_api/models/webstreams.py b/api/libretime_api/schedule/models/webstream.py similarity index 92% rename from api/libretime_api/models/webstreams.py rename to api/libretime_api/schedule/models/webstream.py index 72f2022f7..82e9fa89c 100644 --- a/api/libretime_api/models/webstreams.py +++ b/api/libretime_api/schedule/models/webstream.py @@ -1,8 +1,6 @@ from django.contrib.auth import get_user_model from django.db import models -from .schedule import Schedule - class Webstream(models.Model): name = models.CharField(max_length=255) @@ -29,7 +27,7 @@ class Webstream(models.Model): class WebstreamMetadata(models.Model): - instance = models.ForeignKey(Schedule, models.DO_NOTHING) + instance = models.ForeignKey("Schedule", models.DO_NOTHING) start_time = models.DateTimeField() liquidsoap_data = models.CharField(max_length=1024) diff --git a/api/libretime_api/schedule/router.py b/api/libretime_api/schedule/router.py new file mode 100644 index 000000000..c3f61e0d6 --- /dev/null +++ b/api/libretime_api/schedule/router.py @@ -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) diff --git a/api/libretime_api/schedule/serializers/__init__.py b/api/libretime_api/schedule/serializers/__init__.py new file mode 100644 index 000000000..35c59aa4c --- /dev/null +++ b/api/libretime_api/schedule/serializers/__init__.py @@ -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 diff --git a/api/libretime_api/schedule/serializers/playlist.py b/api/libretime_api/schedule/serializers/playlist.py new file mode 100644 index 000000000..f8fd3ae4e --- /dev/null +++ b/api/libretime_api/schedule/serializers/playlist.py @@ -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__" diff --git a/api/libretime_api/schedule/serializers/podcast.py b/api/libretime_api/schedule/serializers/podcast.py new file mode 100644 index 000000000..8d9e7b85d --- /dev/null +++ b/api/libretime_api/schedule/serializers/podcast.py @@ -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__" diff --git a/api/libretime_api/schedule/serializers/schedule.py b/api/libretime_api/schedule/serializers/schedule.py new file mode 100644 index 000000000..ae0996230 --- /dev/null +++ b/api/libretime_api/schedule/serializers/schedule.py @@ -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", + ] diff --git a/api/libretime_api/schedule/serializers/show.py b/api/libretime_api/schedule/serializers/show.py new file mode 100644 index 000000000..84423b05e --- /dev/null +++ b/api/libretime_api/schedule/serializers/show.py @@ -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__" diff --git a/api/libretime_api/schedule/serializers/smart_block.py b/api/libretime_api/schedule/serializers/smart_block.py new file mode 100644 index 000000000..b91bc6523 --- /dev/null +++ b/api/libretime_api/schedule/serializers/smart_block.py @@ -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__" diff --git a/api/libretime_api/schedule/serializers/webstream.py b/api/libretime_api/schedule/serializers/webstream.py new file mode 100644 index 000000000..10907f72e --- /dev/null +++ b/api/libretime_api/schedule/serializers/webstream.py @@ -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__" diff --git a/api/libretime_api/schedule/tests/__init__.py b/api/libretime_api/schedule/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/schedule/tests/models/__init__.py b/api/libretime_api/schedule/tests/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/tests/models/test_schedule.py b/api/libretime_api/schedule/tests/models/test_schedule.py similarity index 97% rename from api/libretime_api/tests/models/test_schedule.py rename to api/libretime_api/schedule/tests/models/test_schedule.py index fbdfe7c78..eaa2ff502 100644 --- a/api/libretime_api/tests/models/test_schedule.py +++ b/api/libretime_api/schedule/tests/models/test_schedule.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from django.test import TestCase -from libretime_api.models import Schedule, ShowInstance +from ...models import Schedule, ShowInstance class TestSchedule(TestCase): diff --git a/api/libretime_api/schedule/tests/views/__init__.py b/api/libretime_api/schedule/tests/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/tests/test_views.py b/api/libretime_api/schedule/tests/views/test_schedule.py similarity index 67% rename from api/libretime_api/tests/test_views.py rename to api/libretime_api/schedule/tests/views/test_schedule.py index aa695dd38..9ece9fcd3 100644 --- a/api/libretime_api/tests/test_views.py +++ b/api/libretime_api/schedule/tests/views/test_schedule.py @@ -2,47 +2,11 @@ import os from datetime import datetime, timedelta, timezone from django.conf import settings -from django.contrib.auth.models import AnonymousUser from django.utils import dateparse from model_bakery import baker -from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework.test import APITestCase -from libretime_api.views import FileViewSet - - -class TestFileViewSet(APITestCase): - @classmethod - def setUpTestData(cls): - cls.path = "/api/v2/files/{id}/download/" - cls.token = settings.CONFIG.general.api_key - - def test_invalid(self): - path = self.path.format(id="a") - self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") - response = self.client.get(path) - self.assertEqual(response.status_code, 400) - - def test_does_not_exist(self): - path = self.path.format(id="1") - self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") - response = self.client.get(path) - self.assertEqual(response.status_code, 404) - - def test_exists(self): - music_dir = baker.make( - "libretime_api.MusicDir", - directory=os.path.join(os.path.dirname(__file__), "resources"), - ) - f = baker.make( - "libretime_api.File", - directory=music_dir, - mime="audio/mp3", - filepath="song.mp3", - ) - path = self.path.format(id=str(f.pk)) - self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") - response = self.client.get(path) - self.assertEqual(response.status_code, 200) +from ...._fixtures import AUDIO_FILENAME, fixture_path class TestScheduleViewSet(APITestCase): @@ -53,25 +17,25 @@ class TestScheduleViewSet(APITestCase): def test_schedule_item_full_length(self): music_dir = baker.make( - "libretime_api.MusicDir", - directory=os.path.join(os.path.dirname(__file__), "resources"), + "storage.MusicDir", + directory=str(fixture_path), ) f = baker.make( - "libretime_api.File", + "storage.File", directory=music_dir, mime="audio/mp3", - filepath="song.mp3", + filepath=AUDIO_FILENAME, length=timedelta(seconds=40.86), cuein=timedelta(seconds=0), cueout=timedelta(seconds=40.8131), ) show = baker.make( - "libretime_api.ShowInstance", + "schedule.ShowInstance", starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), ends=datetime.now(tz=timezone.utc) + timedelta(minutes=5), ) scheduleItem = baker.make( - "libretime_api.Schedule", + "schedule.Schedule", starts=datetime.now(tz=timezone.utc), ends=datetime.now(tz=timezone.utc) + f.length, cue_out=f.cueout, @@ -87,25 +51,25 @@ class TestScheduleViewSet(APITestCase): def test_schedule_item_trunc(self): music_dir = baker.make( - "libretime_api.MusicDir", - directory=os.path.join(os.path.dirname(__file__), "resources"), + "storage.MusicDir", + directory=str(fixture_path), ) f = baker.make( - "libretime_api.File", + "storage.File", directory=music_dir, mime="audio/mp3", - filepath="song.mp3", + filepath=AUDIO_FILENAME, length=timedelta(seconds=40.86), cuein=timedelta(seconds=0), cueout=timedelta(seconds=40.8131), ) show = baker.make( - "libretime_api.ShowInstance", + "schedule.ShowInstance", starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), ends=datetime.now(tz=timezone.utc) + timedelta(seconds=20), ) scheduleItem = baker.make( - "libretime_api.Schedule", + "schedule.Schedule", starts=datetime.now(tz=timezone.utc), ends=datetime.now(tz=timezone.utc) + f.length, instance=show, @@ -124,33 +88,33 @@ class TestScheduleViewSet(APITestCase): def test_schedule_item_invalid(self): music_dir = baker.make( - "libretime_api.MusicDir", - directory=os.path.join(os.path.dirname(__file__), "resources"), + "storage.MusicDir", + directory=str(fixture_path), ) f = baker.make( - "libretime_api.File", + "storage.File", directory=music_dir, mime="audio/mp3", - filepath="song.mp3", + filepath=AUDIO_FILENAME, length=timedelta(seconds=40.86), cuein=timedelta(seconds=0), cueout=timedelta(seconds=40.8131), ) show = baker.make( - "libretime_api.ShowInstance", + "schedule.ShowInstance", starts=datetime.now(tz=timezone.utc) - timedelta(minutes=5), ends=datetime.now(tz=timezone.utc) + timedelta(minutes=5), ) - scheduleItem = baker.make( - "libretime_api.Schedule", + 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, ) - invalidScheduleItem = baker.make( - "libretime_api.Schedule", + 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, @@ -163,19 +127,21 @@ class TestScheduleViewSet(APITestCase): 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"]), scheduleItem.ends) + 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( - "libretime_api.MusicDir", - directory=os.path.join(os.path.dirname(__file__), "resources"), + "storage.MusicDir", + directory=str(fixture_path), ) f = baker.make( - "libretime_api.File", + "storage.File", directory=music_dir, mime="audio/mp3", - filepath="song.mp3", + filepath=AUDIO_FILENAME, length=timedelta(seconds=40.86), cuein=timedelta(seconds=0), cueout=timedelta(seconds=40.8131), @@ -183,12 +149,12 @@ class TestScheduleViewSet(APITestCase): filter_point = datetime.now(tz=timezone.utc) show = baker.make( - "libretime_api.ShowInstance", + "schedule.ShowInstance", starts=filter_point - timedelta(minutes=5), ends=filter_point + timedelta(minutes=5), ) schedule_item = baker.make( - "libretime_api.Schedule", + "schedule.Schedule", starts=filter_point, ends=filter_point + f.length, cue_out=f.cueout, @@ -196,7 +162,7 @@ class TestScheduleViewSet(APITestCase): file=f, ) previous_item = baker.make( - "libretime_api.Schedule", + "schedule.Schedule", starts=filter_point - timedelta(minutes=5), ends=filter_point - timedelta(minutes=5) + f.length, cue_out=f.cueout, diff --git a/api/libretime_api/schedule/views/__init__.py b/api/libretime_api/schedule/views/__init__.py new file mode 100644 index 000000000..809888976 --- /dev/null +++ b/api/libretime_api/schedule/views/__init__.py @@ -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 diff --git a/api/libretime_api/schedule/views/playlist.py b/api/libretime_api/schedule/views/playlist.py new file mode 100644 index 000000000..10024220d --- /dev/null +++ b/api/libretime_api/schedule/views/playlist.py @@ -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" diff --git a/api/libretime_api/schedule/views/podcast.py b/api/libretime_api/schedule/views/podcast.py new file mode 100644 index 000000000..7c442fdf4 --- /dev/null +++ b/api/libretime_api/schedule/views/podcast.py @@ -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" diff --git a/api/libretime_api/schedule/views/schedule.py b/api/libretime_api/schedule/views/schedule.py new file mode 100644 index 000000000..a01a6e474 --- /dev/null +++ b/api/libretime_api/schedule/views/schedule.py @@ -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")) diff --git a/api/libretime_api/schedule/views/show.py b/api/libretime_api/schedule/views/show.py new file mode 100644 index 000000000..e8a93b18c --- /dev/null +++ b/api/libretime_api/schedule/views/show.py @@ -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" diff --git a/api/libretime_api/schedule/views/smart_block.py b/api/libretime_api/schedule/views/smart_block.py new file mode 100644 index 000000000..8e2a377aa --- /dev/null +++ b/api/libretime_api/schedule/views/smart_block.py @@ -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" diff --git a/api/libretime_api/schedule/views/webstream.py b/api/libretime_api/schedule/views/webstream.py new file mode 100644 index 000000000..5defd6de2 --- /dev/null +++ b/api/libretime_api/schedule/views/webstream.py @@ -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" diff --git a/api/libretime_api/serializers.py b/api/libretime_api/serializers.py deleted file mode 100644 index ab43acfbe..000000000 --- a/api/libretime_api/serializers.py +++ /dev/null @@ -1,316 +0,0 @@ -from django.contrib.auth import get_user_model -from rest_framework import serializers - -from .models import * - - -class UserSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = get_user_model() - fields = [ - "item_url", - "username", - "type", - "first_name", - "last_name", - "lastfail", - "skype_contact", - "jabber_contact", - "email", - "cell_phone", - "login_attempts", - ] - - -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__" - - -class CountrySerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = Country - fields = "__all__" - - -class FileSerializer(serializers.HyperlinkedModelSerializer): - id = serializers.IntegerField(read_only=True) - - class Meta: - model = File - fields = "__all__" - - -class ListenerCountSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ListenerCount - fields = "__all__" - - -class LiveLogSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = LiveLog - fields = "__all__" - - -class LoginAttemptSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = LoginAttempt - fields = "__all__" - - -class MountNameSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = MountName - fields = "__all__" - - -class MusicDirSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = MusicDir - fields = "__all__" - - -class PlaylistSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = Playlist - fields = "__all__" - - -class PlaylistContentSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = PlaylistContent - fields = "__all__" - - -class PlayoutHistorySerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = PlayoutHistory - fields = "__all__" - - -class PlayoutHistoryMetadataSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = PlayoutHistoryMetadata - fields = "__all__" - - -class PlayoutHistoryTemplateSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = PlayoutHistoryTemplate - fields = "__all__" - - -class PlayoutHistoryTemplateFieldSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = PlayoutHistoryTemplateField - fields = "__all__" - - -class PreferenceSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = Preference - fields = "__all__" - - -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", - ] - - -class ServiceRegisterSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ServiceRegister - fields = "__all__" - - -class SessionSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = Session - fields = "__all__" - - -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__" - - -class StreamSettingSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = StreamSetting - fields = "__all__" - - -class UserTokenSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = UserToken - fields = "__all__" - - -class TimestampSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = Timestamp - fields = "__all__" - - -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__" - - -class CeleryTaskSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = CeleryTask - fields = "__all__" - - -class CloudFileSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = CloudFile - fields = "__all__" - - -class ImportedPodcastSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ImportedPodcast - fields = "__all__" - - -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 ThirdPartyTrackReferenceSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ThirdPartyTrackReference - fields = "__all__" - - -class TrackTypeSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = TrackType - fields = "__all__" diff --git a/api/libretime_api/settings/README.md b/api/libretime_api/settings/README.md index 7f8b5d509..ab2ef5410 100644 --- a/api/libretime_api/settings/README.md +++ b/api/libretime_api/settings/README.md @@ -1,7 +1,11 @@ # Django settings +For more information on django settings, see https://docs.djangoproject.com/en/3.2/topics/settings/. +For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/. + The structure of the django settings module is the following: -- the `__init__.py` (`libretime_api.settings`) module is the django settings entrypoint. The module contains bindings between the user configuration and the django settings. **Advanced users** may edit this file to better integrate the LibreTime API in their setup. - the `_internal.py` module contains application settings for django. - the `_schema.py` module contains the schema for the user configuration parsing and validation. +- the `prod.py` (`libretime_api.settings.prod`) module is the django settings entrypoint. The module contains bindings between the user configuration and the django settings. **Advanced users** may edit this file to better integrate the LibreTime API in their setup. +- the `testing.py` (`libretime_api.settings.testing`) module is the testing django settings entrypoint. diff --git a/api/libretime_api/settings/__init__.py b/api/libretime_api/settings/__init__.py index 07813f8c3..e69de29bb 100644 --- a/api/libretime_api/settings/__init__.py +++ b/api/libretime_api/settings/__init__.py @@ -1,53 +0,0 @@ -# pylint: disable=unused-import -from os import getenv - -from ._internal import ( - AUTH_PASSWORD_VALIDATORS, - AUTH_USER_MODEL, - DEBUG, - INSTALLED_APPS, - MIDDLEWARE, - REST_FRAMEWORK, - ROOT_URLCONF, - STATIC_URL, - TEMPLATES, - TEST_RUNNER, - WSGI_APPLICATION, - setup_logger, -) -from ._schema import Config - -API_VERSION = "2.0.0" - -LIBRETIME_LOG_FILEPATH = getenv("LIBRETIME_LOG_FILEPATH") -LIBRETIME_CONFIG_FILEPATH = getenv("LIBRETIME_CONFIG_FILEPATH") - -CONFIG = Config(filepath=LIBRETIME_CONFIG_FILEPATH) - -SECRET_KEY = CONFIG.general.api_key -ALLOWED_HOSTS = ["*"] - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "HOST": CONFIG.database.host, - "PORT": CONFIG.database.port, - "NAME": CONFIG.database.name, - "USER": CONFIG.database.user, - "PASSWORD": CONFIG.database.password, - } -} - -LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" -USE_I18N = True -USE_L10N = True -USE_TZ = True - -LOGGING = setup_logger(LIBRETIME_LOG_FILEPATH) - -SPECTACULAR_SETTINGS = { - "TITLE": "LibreTime API", - "DESCRIPTION": "Radio Broadcast & Automation Platform", - "VERSION": API_VERSION, -} diff --git a/api/libretime_api/settings/_internal.py b/api/libretime_api/settings/_internal.py index 7222a7e2c..ba1d67eef 100644 --- a/api/libretime_api/settings/_internal.py +++ b/api/libretime_api/settings/_internal.py @@ -1,13 +1,18 @@ from os import getenv from typing import Optional +API_VERSION = "2.0.0" + # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = getenv("LIBRETIME_DEBUG") +DEBUG = getenv("LIBRETIME_DEBUG", "false").lower() == "true" # Application definition INSTALLED_APPS = [ - "libretime_api.apps.LibreTimeAPIConfig", + "libretime_api.core", + "libretime_api.history", + "libretime_api.storage", + "libretime_api.schedule", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", @@ -48,8 +53,19 @@ TEMPLATES = [ WSGI_APPLICATION = "libretime_api.wsgi.application" +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/api/v2/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + # Password validation -# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -66,39 +82,11 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] -# Rest Framework settings -# https://www.django-rest-framework.org/api-guide/settings/ - -renderer_classes = ["rest_framework.renderers.JSONRenderer"] -if DEBUG: - renderer_classes += ["rest_framework.renderers.BrowsableAPIRenderer"] - -REST_FRAMEWORK = { - "DEFAULT_RENDERER_CLASSES": renderer_classes, - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework.authentication.SessionAuthentication", - "rest_framework.authentication.BasicAuthentication", - ), - "DEFAULT_PERMISSION_CLASSES": [ - "libretime_api.permissions.IsSystemTokenOrUser", - ], - "DEFAULT_FILTER_BACKENDS": [ - "django_filters.rest_framework.DjangoFilterBackend", - ], - "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", - "URL_FIELD_NAME": "item_url", -} - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.0/howto/static-files/ - -STATIC_URL = "/api/v2/static/" - -AUTH_USER_MODEL = "libretime_api.User" - -TEST_RUNNER = "libretime_api.tests.runners.ManagedModelTestRunner" # Logging +# https://docs.djangoproject.com/en/3.2/topics/logging/#configuring-logging + + def setup_logger(log_filepath: Optional[str]): logging_handlers = { "console": { @@ -143,3 +131,41 @@ def setup_logger(log_filepath: Optional[str]): }, }, } + + +# Rest Framework +# https://www.django-rest-framework.org/api-guide/settings/ + +renderer_classes = ["rest_framework.renderers.JSONRenderer"] +if DEBUG: + renderer_classes += ["rest_framework.renderers.BrowsableAPIRenderer"] + +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": renderer_classes, + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", + ), + "DEFAULT_PERMISSION_CLASSES": [ + "libretime_api.permissions.IsSystemTokenOrUser", + ], + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + ], + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "URL_FIELD_NAME": "item_url", +} + +# Auth +# https://docs.djangoproject.com/en/3.2/topics/auth/customizing/#substituting-a-custom-user-model + +AUTH_USER_MODEL = "core.User" + +# Spectacular +# https://drf-spectacular.readthedocs.io/en/latest/settings.html + +SPECTACULAR_SETTINGS = { + "TITLE": "LibreTime API", + "DESCRIPTION": "Radio Broadcast & Automation Platform", + "VERSION": API_VERSION, +} diff --git a/api/libretime_api/settings/prod.py b/api/libretime_api/settings/prod.py new file mode 100644 index 000000000..cde7640e7 --- /dev/null +++ b/api/libretime_api/settings/prod.py @@ -0,0 +1,55 @@ +from os import getenv + +# pylint: disable=unused-import +from ._internal import ( + API_VERSION, + AUTH_PASSWORD_VALIDATORS, + AUTH_USER_MODEL, + DEBUG, + DEFAULT_AUTO_FIELD, + INSTALLED_APPS, + MIDDLEWARE, + REST_FRAMEWORK, + ROOT_URLCONF, + SPECTACULAR_SETTINGS, + STATIC_URL, + TEMPLATES, + WSGI_APPLICATION, + setup_logger, +) +from ._schema import Config + +LIBRETIME_LOG_FILEPATH = getenv("LIBRETIME_LOG_FILEPATH") +LIBRETIME_CONFIG_FILEPATH = getenv("LIBRETIME_CONFIG_FILEPATH") + +CONFIG = Config(filepath=LIBRETIME_CONFIG_FILEPATH) + +SECRET_KEY = CONFIG.general.api_key + +ALLOWED_HOSTS = ["*"] + +LOGGING = setup_logger(LIBRETIME_LOG_FILEPATH) + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "HOST": CONFIG.database.host, + "PORT": CONFIG.database.port, + "NAME": CONFIG.database.name, + "USER": CONFIG.database.user, + "PASSWORD": CONFIG.database.password, + } +} + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_L10N = True +USE_TZ = True diff --git a/api/libretime_api/settings/testing.py b/api/libretime_api/settings/testing.py new file mode 100644 index 000000000..6db452181 --- /dev/null +++ b/api/libretime_api/settings/testing.py @@ -0,0 +1,35 @@ +import os + +os.environ.setdefault("LIBRETIME_DEBUG", "true") +os.environ.setdefault("LIBRETIME_GENERAL_API_KEY", "testing") + +# pylint: disable=wrong-import-position,unused-import +from .prod import ( + ALLOWED_HOSTS, + API_VERSION, + AUTH_PASSWORD_VALIDATORS, + AUTH_USER_MODEL, + CONFIG, + DATABASES, + DEBUG, + DEFAULT_AUTO_FIELD, + INSTALLED_APPS, + LANGUAGE_CODE, + LOGGING, + MIDDLEWARE, + REST_FRAMEWORK, + ROOT_URLCONF, + SECRET_KEY, + STATIC_URL, + TEMPLATES, + TIME_ZONE, + USE_I18N, + USE_L10N, + USE_TZ, + WSGI_APPLICATION, +) + +# Testing +# https://docs.djangoproject.com/en/3.2/ref/settings/#test-runner + +TEST_RUNNER = "libretime_api.tests.runner.ManagedModelTestRunner" diff --git a/api/libretime_api/storage/__init__.py b/api/libretime_api/storage/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/storage/apps.py b/api/libretime_api/storage/apps.py new file mode 100644 index 000000000..17cbeb2aa --- /dev/null +++ b/api/libretime_api/storage/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class StorageConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "libretime_api.storage" + verbose_name = "LibreTime Storage API" diff --git a/api/libretime_api/storage/models/__init__.py b/api/libretime_api/storage/models/__init__.py new file mode 100644 index 000000000..6a0f0dbd6 --- /dev/null +++ b/api/libretime_api/storage/models/__init__.py @@ -0,0 +1,4 @@ +from .cloud_file import CloudFile +from .file import File +from .storage import MusicDir +from .track_type import TrackType diff --git a/api/libretime_api/storage/models/cloud_file.py b/api/libretime_api/storage/models/cloud_file.py new file mode 100644 index 000000000..084cb468b --- /dev/null +++ b/api/libretime_api/storage/models/cloud_file.py @@ -0,0 +1,13 @@ +from django.db import models + + +class CloudFile(models.Model): + storage_backend = models.CharField(max_length=512) + resource_id = models.TextField() + filename = models.ForeignKey( + "File", models.DO_NOTHING, blank=True, null=True, db_column="cc_file_id" + ) + + class Meta: + managed = False + db_table = "cloud_file" diff --git a/api/libretime_api/models/files.py b/api/libretime_api/storage/models/file.py similarity index 86% rename from api/libretime_api/models/files.py rename to api/libretime_api/storage/models/file.py index c340eaccd..ddbba8017 100644 --- a/api/libretime_api/models/files.py +++ b/api/libretime_api/storage/models/file.py @@ -12,7 +12,7 @@ class File(models.Model): import_status = models.IntegerField() currently_accessing = models.IntegerField(db_column="currentlyaccessing") edited_by = models.ForeignKey( - "User", + "core.User", models.DO_NOTHING, db_column="editedby", blank=True, @@ -70,7 +70,7 @@ class File(models.Model): replay_gain = models.DecimalField( max_digits=8, decimal_places=2, blank=True, null=True ) - owner = models.ForeignKey("User", models.DO_NOTHING, blank=True, null=True) + owner = models.ForeignKey("core.User", models.DO_NOTHING, blank=True, null=True) cuein = models.DurationField(blank=True, null=True) cueout = models.DurationField(blank=True, null=True) silan_check = models.BooleanField(blank=True, null=True) @@ -92,26 +92,3 @@ class File(models.Model): ("change_own_file", "Change the files where they are the owner"), ("delete_own_file", "Delete the files where they are the owner"), ] - - -class MusicDir(models.Model): - directory = models.TextField(unique=True, blank=True, null=True) - type = models.CharField(max_length=255, blank=True, null=True) - exists = models.BooleanField(blank=True, null=True) - watched = models.BooleanField(blank=True, null=True) - - class Meta: - managed = False - db_table = "cc_music_dirs" - - -class CloudFile(models.Model): - storage_backend = models.CharField(max_length=512) - resource_id = models.TextField() - filename = models.ForeignKey( - File, models.DO_NOTHING, blank=True, null=True, db_column="cc_file_id" - ) - - class Meta: - managed = False - db_table = "cloud_file" diff --git a/api/libretime_api/storage/models/storage.py b/api/libretime_api/storage/models/storage.py new file mode 100644 index 000000000..ea4fdc14e --- /dev/null +++ b/api/libretime_api/storage/models/storage.py @@ -0,0 +1,12 @@ +from django.db import models + + +class MusicDir(models.Model): + directory = models.TextField(unique=True, blank=True, null=True) + type = models.CharField(max_length=255, blank=True, null=True) + exists = models.BooleanField(blank=True, null=True) + watched = models.BooleanField(blank=True, null=True) + + class Meta: + managed = False + db_table = "cc_music_dirs" diff --git a/api/libretime_api/storage/models/track_type.py b/api/libretime_api/storage/models/track_type.py new file mode 100644 index 000000000..f42b8293c --- /dev/null +++ b/api/libretime_api/storage/models/track_type.py @@ -0,0 +1,12 @@ +from django.db import models + + +class TrackType(models.Model): + code = models.CharField(max_length=16, unique=True) + type_name = models.CharField(max_length=255, blank=True, null=True) + description = models.CharField(max_length=255, blank=True, null=True) + visibility = models.BooleanField(blank=True, default=True) + + class Meta: + managed = False + db_table = "cc_track_types" diff --git a/api/libretime_api/storage/router.py b/api/libretime_api/storage/router.py new file mode 100644 index 000000000..ee37a1f0b --- /dev/null +++ b/api/libretime_api/storage/router.py @@ -0,0 +1,9 @@ +from rest_framework import routers + +from .views import CloudFileViewSet, FileViewSet, MusicDirViewSet, TrackTypeViewSet + +router = routers.DefaultRouter() +router.register("files", FileViewSet) +router.register("music-dirs", MusicDirViewSet) +router.register("cloud-files", CloudFileViewSet) +router.register("track-types", TrackTypeViewSet) diff --git a/api/libretime_api/storage/serializers/__init__.py b/api/libretime_api/storage/serializers/__init__.py new file mode 100644 index 000000000..cd1b48f63 --- /dev/null +++ b/api/libretime_api/storage/serializers/__init__.py @@ -0,0 +1,4 @@ +from .cloud_file import CloudFileSerializer +from .file import FileSerializer +from .storage import MusicDirSerializer +from .track_type import TrackTypeSerializer diff --git a/api/libretime_api/storage/serializers/cloud_file.py b/api/libretime_api/storage/serializers/cloud_file.py new file mode 100644 index 000000000..1832e47e0 --- /dev/null +++ b/api/libretime_api/storage/serializers/cloud_file.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from ..models import CloudFile + + +class CloudFileSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = CloudFile + fields = "__all__" diff --git a/api/libretime_api/storage/serializers/file.py b/api/libretime_api/storage/serializers/file.py new file mode 100644 index 000000000..87a9020d8 --- /dev/null +++ b/api/libretime_api/storage/serializers/file.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from ..models import File + + +class FileSerializer(serializers.HyperlinkedModelSerializer): + id = serializers.IntegerField(read_only=True) + + class Meta: + model = File + fields = "__all__" diff --git a/api/libretime_api/storage/serializers/storage.py b/api/libretime_api/storage/serializers/storage.py new file mode 100644 index 000000000..211b844f0 --- /dev/null +++ b/api/libretime_api/storage/serializers/storage.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from ..models import MusicDir + + +class MusicDirSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = MusicDir + fields = "__all__" diff --git a/api/libretime_api/storage/serializers/track_type.py b/api/libretime_api/storage/serializers/track_type.py new file mode 100644 index 000000000..c87e382a9 --- /dev/null +++ b/api/libretime_api/storage/serializers/track_type.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from ..models import TrackType + + +class TrackTypeSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = TrackType + fields = "__all__" diff --git a/api/libretime_api/storage/tests/__init__.py b/api/libretime_api/storage/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/storage/tests/views/__init__.py b/api/libretime_api/storage/tests/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/libretime_api/storage/tests/views/test_file.py b/api/libretime_api/storage/tests/views/test_file.py new file mode 100644 index 000000000..533c07f03 --- /dev/null +++ b/api/libretime_api/storage/tests/views/test_file.py @@ -0,0 +1,42 @@ +import os + +from django.conf import settings +from model_bakery import baker +from rest_framework.test import APITestCase + +from ...._fixtures import AUDIO_FILENAME, fixture_path + + +class TestFileViewSet(APITestCase): + @classmethod + def setUpTestData(cls): + cls.path = "/api/v2/files/{id}/download/" + cls.token = settings.CONFIG.general.api_key + + def test_invalid(self): + path = self.path.format(id="a") + self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") + response = self.client.get(path) + self.assertEqual(response.status_code, 400) + + def test_does_not_exist(self): + path = self.path.format(id="1") + self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") + response = self.client.get(path) + self.assertEqual(response.status_code, 404) + + def test_exists(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, + ) + path = self.path.format(id=str(f.pk)) + self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}") + response = self.client.get(path) + self.assertEqual(response.status_code, 200) diff --git a/api/libretime_api/storage/views/__init__.py b/api/libretime_api/storage/views/__init__.py new file mode 100644 index 000000000..a489b503d --- /dev/null +++ b/api/libretime_api/storage/views/__init__.py @@ -0,0 +1,4 @@ +from .cloud_file import CloudFileViewSet +from .file import FileViewSet +from .storage import MusicDirViewSet +from .track_type import TrackTypeViewSet diff --git a/api/libretime_api/storage/views/cloud_file.py b/api/libretime_api/storage/views/cloud_file.py new file mode 100644 index 000000000..cf4a30d49 --- /dev/null +++ b/api/libretime_api/storage/views/cloud_file.py @@ -0,0 +1,10 @@ +from rest_framework import viewsets + +from ..models import CloudFile +from ..serializers import CloudFileSerializer + + +class CloudFileViewSet(viewsets.ModelViewSet): + queryset = CloudFile.objects.all() + serializer_class = CloudFileSerializer + model_permission_name = "cloudfile" diff --git a/api/libretime_api/storage/views/file.py b/api/libretime_api/storage/views/file.py new file mode 100644 index 000000000..702deacc9 --- /dev/null +++ b/api/libretime_api/storage/views/file.py @@ -0,0 +1,33 @@ +import os + +from django.http import FileResponse +from django.shortcuts import get_object_or_404 +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from ..models import File +from ..serializers import FileSerializer + + +class FileViewSet(viewsets.ModelViewSet): + queryset = File.objects.all() + serializer_class = FileSerializer + model_permission_name = "file" + + @action(detail=True, methods=["GET"]) + def download(self, request, pk=None): + if pk is None: + return Response("No file requested", status=status.HTTP_400_BAD_REQUEST) + try: + pk = int(pk) + except ValueError: + return Response( + "File ID should be an integer", status=status.HTTP_400_BAD_REQUEST + ) + + filename = get_object_or_404(File, pk=pk) + directory = filename.directory + path = os.path.join(directory.directory, filename.filepath) + response = FileResponse(open(path, "rb"), content_type=filename.mime) + return response diff --git a/api/libretime_api/storage/views/storage.py b/api/libretime_api/storage/views/storage.py new file mode 100644 index 000000000..3da80c953 --- /dev/null +++ b/api/libretime_api/storage/views/storage.py @@ -0,0 +1,10 @@ +from rest_framework import viewsets + +from ..models import MusicDir +from ..serializers import MusicDirSerializer + + +class MusicDirViewSet(viewsets.ModelViewSet): + queryset = MusicDir.objects.all() + serializer_class = MusicDirSerializer + model_permission_name = "musicdir" diff --git a/api/libretime_api/storage/views/track_type.py b/api/libretime_api/storage/views/track_type.py new file mode 100644 index 000000000..4464e1983 --- /dev/null +++ b/api/libretime_api/storage/views/track_type.py @@ -0,0 +1,10 @@ +from rest_framework import viewsets + +from ..models import TrackType +from ..serializers import TrackTypeSerializer + + +class TrackTypeViewSet(viewsets.ModelViewSet): + queryset = TrackType.objects.all() + serializer_class = TrackTypeSerializer + model_permission_name = "tracktype" diff --git a/api/libretime_api/tests/runners.py b/api/libretime_api/tests/runner.py similarity index 100% rename from api/libretime_api/tests/runners.py rename to api/libretime_api/tests/runner.py diff --git a/api/libretime_api/tests/test_permissions.py b/api/libretime_api/tests/test_permissions.py index 0735fe4ea..2b51e1cfd 100644 --- a/api/libretime_api/tests/test_permissions.py +++ b/api/libretime_api/tests/test_permissions.py @@ -6,13 +6,13 @@ from django.contrib.auth.models import AnonymousUser from model_bakery import baker from rest_framework.test import APIRequestFactory, APITestCase -from libretime_api.models.user_constants import ADMIN, DJ, GUEST, PROGRAM_MANAGER -from libretime_api.permission_constants import ( +from ..core.models import ADMIN, DJ, GUEST, PROGRAM_MANAGER +from ..permission_constants import ( DJ_PERMISSIONS, GUEST_PERMISSIONS, PROGRAM_MANAGER_PERMISSIONS, ) -from libretime_api.permissions import IsSystemTokenOrUser +from ..permissions import IsSystemTokenOrUser class TestIsSystemTokenOrUser(APITestCase): @@ -109,7 +109,7 @@ class TestPermissions(APITestCase): first_name="test", last_name="user", ) - f = baker.make("libretime_api.File", owner=user) + f = baker.make("storage.File", owner=user) model = f"files/{f.id}" path = self.path.format(model) self.client.login(username="test-dj", password="test") @@ -125,7 +125,7 @@ class TestPermissions(APITestCase): first_name="test", last_name="user", ) - f = baker.make("libretime_api.File") + f = baker.make("storage.File") model = f"files/{f.id}" path = self.path.format(model) self.client.login(username="test-dj", password="test") diff --git a/api/libretime_api/urls.py b/api/libretime_api/urls.py index d7290dc7a..f17815463 100644 --- a/api/libretime_api/urls.py +++ b/api/libretime_api/urls.py @@ -1,58 +1,39 @@ +""" +URL Configuration + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/http/urls/ +""" from django.urls import include, path from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from rest_framework import routers -from .views import * +from .core.router import router as core_router +from .core.views import version +from .history.router import router as history_router +from .schedule.router import router as schedule_router +from .storage.router import router as storage_router router = routers.DefaultRouter() -router.register("smart-blocks", SmartBlockViewSet) -router.register("smart-block-contents", SmartBlockContentViewSet) -router.register("smart-block-criteria", SmartBlockCriteriaViewSet) -router.register("countries", CountryViewSet) -router.register("files", FileViewSet) -router.register("listener-counts", ListenerCountViewSet) -router.register("live-logs", LiveLogViewSet) -router.register("login-attempts", LoginAttemptViewSet) -router.register("mount-names", MountNameViewSet) -router.register("music-dirs", MusicDirViewSet) -router.register("playlists", PlaylistViewSet) -router.register("playlist-contents", PlaylistContentViewSet) -router.register("playout-history", PlayoutHistoryViewSet) -router.register("playout-history-metadata", PlayoutHistoryMetadataViewSet) -router.register("playout-history-templates", PlayoutHistoryTemplateViewSet) -router.register("playout-history-template-fields", PlayoutHistoryTemplateFieldViewSet) -router.register("preferences", PreferenceViewSet) -router.register("schedule", ScheduleViewSet) -router.register("service-registers", ServiceRegisterViewSet) -router.register("sessions", SessionViewSet) -router.register("shows", ShowViewSet) -router.register("show-days", ShowDaysViewSet) -router.register("show-hosts", ShowHostViewSet) -router.register("show-instances", ShowInstanceViewSet) -router.register("show-rebroadcasts", ShowRebroadcastViewSet) -router.register("stream-settings", StreamSettingViewSet) -router.register("users", UserViewSet) -router.register("user-tokens", UserTokenViewSet) -router.register("timestamps", TimestampViewSet) -router.register("webstreams", WebstreamViewSet) -router.register("webstream-metadata", WebstreamMetadataViewSet) -router.register("celery-tasks", CeleryTaskViewSet) -router.register("cloud-files", CloudFileViewSet) -router.register("imported-podcasts", ImportedPodcastViewSet) -router.register("podcasts", PodcastViewSet) -router.register("podcast-episodes", PodcastEpisodeViewSet) -router.register("station-podcasts", StationPodcastViewSet) -router.register("third-party-track-references", ThirdPartyTrackReferenceViewSet) -router.register("track-types", TrackTypeViewSet) + +router.registry.extend(core_router.registry) +router.registry.extend(history_router.registry) +router.registry.extend(schedule_router.registry) +router.registry.extend(storage_router.registry) + urlpatterns = [ path("api/v2/", include(router.urls)), - path("api/v2/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/v2/version/", version), + path( + "api/v2/schema/", + SpectacularAPIView.as_view(), + name="schema", + ), path( "api/v2/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui", ), - path("api/v2/version/", version), path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), ] diff --git a/api/libretime_api/views.py b/api/libretime_api/views.py deleted file mode 100644 index aeb9d5e1a..000000000 --- a/api/libretime_api/views.py +++ /dev/null @@ -1,309 +0,0 @@ -import os - -from django.conf import settings -from django.db.models import F -from django.http import FileResponse -from django.shortcuts import get_object_or_404 -from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view -from rest_framework import fields, status, viewsets -from rest_framework.decorators import action, api_view, permission_classes -from rest_framework.permissions import AllowAny -from rest_framework.response import Response - -from .permissions import IsAdminOrOwnUser -from .serializers import * - -FILTER_NUMERICAL_LOOKUPS = [ - "exact", - "gt", - "lt", - "gte", - "lte", - "range", -] - - -class UserViewSet(viewsets.ModelViewSet): - queryset = get_user_model().objects.all() - serializer_class = UserSerializer - permission_classes = [IsAdminOrOwnUser] - model_permission_name = "user" - - -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" - - -class CountryViewSet(viewsets.ModelViewSet): - queryset = Country.objects.all() - serializer_class = CountrySerializer - model_permission_name = "country" - - -class FileViewSet(viewsets.ModelViewSet): - queryset = File.objects.all() - serializer_class = FileSerializer - model_permission_name = "file" - - @action(detail=True, methods=["GET"]) - def download(self, request, pk=None): - if pk is None: - return Response("No file requested", status=status.HTTP_400_BAD_REQUEST) - try: - pk = int(pk) - except ValueError: - return Response( - "File ID should be an integer", status=status.HTTP_400_BAD_REQUEST - ) - - filename = get_object_or_404(File, pk=pk) - directory = filename.directory - path = os.path.join(directory.directory, filename.filepath) - response = FileResponse(open(path, "rb"), content_type=filename.mime) - return response - - -class ListenerCountViewSet(viewsets.ModelViewSet): - queryset = ListenerCount.objects.all() - serializer_class = ListenerCountSerializer - model_permission_name = "listenercount" - - -class LiveLogViewSet(viewsets.ModelViewSet): - queryset = LiveLog.objects.all() - serializer_class = LiveLogSerializer - model_permission_name = "livelog" - - -class LoginAttemptViewSet(viewsets.ModelViewSet): - queryset = LoginAttempt.objects.all() - serializer_class = LoginAttemptSerializer - model_permission_name = "loginattempt" - - -class MountNameViewSet(viewsets.ModelViewSet): - queryset = MountName.objects.all() - serializer_class = MountNameSerializer - model_permission_name = "mountname" - - -class MusicDirViewSet(viewsets.ModelViewSet): - queryset = MusicDir.objects.all() - serializer_class = MusicDirSerializer - model_permission_name = "musicdir" - - -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" - - -class PlayoutHistoryViewSet(viewsets.ModelViewSet): - queryset = PlayoutHistory.objects.all() - serializer_class = PlayoutHistorySerializer - model_permission_name = "playouthistory" - - -class PlayoutHistoryMetadataViewSet(viewsets.ModelViewSet): - queryset = PlayoutHistoryMetadata.objects.all() - serializer_class = PlayoutHistoryMetadataSerializer - model_permission_name = "playouthistorymetadata" - - -class PlayoutHistoryTemplateViewSet(viewsets.ModelViewSet): - queryset = PlayoutHistoryTemplate.objects.all() - serializer_class = PlayoutHistoryTemplateSerializer - model_permission_name = "playouthistorytemplate" - - -class PlayoutHistoryTemplateFieldViewSet(viewsets.ModelViewSet): - queryset = PlayoutHistoryTemplateField.objects.all() - serializer_class = PlayoutHistoryTemplateFieldSerializer - model_permission_name = "playouthistorytemplatefield" - - -class PreferenceViewSet(viewsets.ModelViewSet): - queryset = Preference.objects.all() - serializer_class = PreferenceSerializer - model_permission_name = "preference" - - -@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")) - - -class ServiceRegisterViewSet(viewsets.ModelViewSet): - queryset = ServiceRegister.objects.all() - serializer_class = ServiceRegisterSerializer - model_permission_name = "serviceregister" - - -class SessionViewSet(viewsets.ModelViewSet): - queryset = Session.objects.all() - serializer_class = SessionSerializer - model_permission_name = "session" - - -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" - - -class StreamSettingViewSet(viewsets.ModelViewSet): - queryset = StreamSetting.objects.all() - serializer_class = StreamSettingSerializer - model_permission_name = "streamsetting" - - -class UserTokenViewSet(viewsets.ModelViewSet): - queryset = UserToken.objects.all() - serializer_class = UserTokenSerializer - model_permission_name = "usertoken" - - -class TimestampViewSet(viewsets.ModelViewSet): - queryset = Timestamp.objects.all() - serializer_class = TimestampSerializer - model_permission_name = "timestamp" - - -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" - - -class CeleryTaskViewSet(viewsets.ModelViewSet): - queryset = CeleryTask.objects.all() - serializer_class = CeleryTaskSerializer - model_permission_name = "celerytask" - - -class CloudFileViewSet(viewsets.ModelViewSet): - queryset = CloudFile.objects.all() - serializer_class = CloudFileSerializer - model_permission_name = "cloudfile" - - -class ImportedPodcastViewSet(viewsets.ModelViewSet): - queryset = ImportedPodcast.objects.all() - serializer_class = ImportedPodcastSerializer - model_permission_name = "importedpodcast" - - -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 ThirdPartyTrackReferenceViewSet(viewsets.ModelViewSet): - queryset = ThirdPartyTrackReference.objects.all() - serializer_class = ThirdPartyTrackReferenceSerializer - model_permission_name = "thirdpartytrackreference" - - -class TrackTypeViewSet(viewsets.ModelViewSet): - queryset = TrackType.objects.all() - serializer_class = TrackTypeSerializer - model_permission_name = "tracktype" - - -@api_view(["GET"]) -@permission_classes((AllowAny,)) -def version(request, *args, **kwargs): - return Response({"api_version": settings.API_VERSION}) diff --git a/api/libretime_api/wsgi.py b/api/libretime_api/wsgi.py index 0872d2d55..4f77d5525 100644 --- a/api/libretime_api/wsgi.py +++ b/api/libretime_api/wsgi.py @@ -1,16 +1,16 @@ """ -WSGI config for api project. +WSGI config for libretime_api project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings.prod") application = get_wsgi_application() diff --git a/api/setup.py b/api/setup.py index 77665173f..3dda09a84 100644 --- a/api/setup.py +++ b/api/setup.py @@ -25,7 +25,7 @@ setup( python_requires=">=3.6", entry_points={ "console_scripts": [ - "libretime-api=libretime_api.cli:main", + "libretime-api=libretime_api.manage:main", ] }, install_requires=[