From 12d2d4b15a00e393e437c171e33b71239cb9daeb Mon Sep 17 00:00:00 2001 From: jo Date: Wed, 24 Aug 2022 10:38:53 +0200 Subject: [PATCH] feat(api): add /info and /stream/* endpoints --- api-client/libretime_api_client/v2.py | 9 + api/libretime_api/core/models/preference.py | 71 ++++++ api/libretime_api/core/router.py | 8 +- .../core/serializers/__init__.py | 5 +- api/libretime_api/core/serializers/info.py | 7 +- .../core/serializers/preference.py | 8 +- api/libretime_api/core/serializers/stream.py | 17 ++ .../core/tests/models/test_preference.py | 31 +++ .../core/tests/views/test_info.py | 19 ++ .../core/tests/views/test_stream.py | 25 ++ api/libretime_api/core/views/__init__.py | 5 +- api/libretime_api/core/views/info.py | 18 +- api/libretime_api/core/views/preference.py | 10 +- api/libretime_api/core/views/stream.py | 44 ++++ api/schema.yml | 220 ++++++------------ 15 files changed, 324 insertions(+), 173 deletions(-) create mode 100644 api/libretime_api/core/serializers/stream.py create mode 100644 api/libretime_api/core/tests/views/test_info.py create mode 100644 api/libretime_api/core/tests/views/test_stream.py create mode 100644 api/libretime_api/core/views/stream.py diff --git a/api-client/libretime_api_client/v2.py b/api-client/libretime_api_client/v2.py index 7787a7934..07d65f923 100644 --- a/api-client/libretime_api_client/v2.py +++ b/api-client/libretime_api_client/v2.py @@ -8,6 +8,9 @@ class ApiClient(AbstractApiClient): super().__init__(base_url=base_url) self.session.headers.update({"Authorization": f"Api-Key {api_key}"}) + def get_info(self, **kwargs) -> Response: + return self._request("GET", "/api/v2/info", **kwargs) + def get_version(self, **kwargs) -> Response: return self._request("GET", "/api/v2/version", **kwargs) @@ -31,3 +34,9 @@ class ApiClient(AbstractApiClient): def download_file(self, item_id: int, **kwargs) -> Response: return self._request("GET", f"/api/v2/files/{item_id}/download", **kwargs) + + def get_stream_preferences(self, **kwargs) -> Response: + return self._request("GET", "/api/v2/stream/preferences", **kwargs) + + def get_stream_state(self, **kwargs) -> Response: + return self._request("GET", "/api/v2/stream/state", **kwargs) diff --git a/api/libretime_api/core/models/preference.py b/api/libretime_api/core/models/preference.py index 33bcf11aa..d535c3a17 100644 --- a/api/libretime_api/core/models/preference.py +++ b/api/libretime_api/core/models/preference.py @@ -1,4 +1,45 @@ +from enum import Enum + from django.db import models +from pydantic import BaseModel + + +class SitePreferences(BaseModel): + station_name: str + + +class MessageFormatKind(int, Enum): + ARTIST_TITLE = 0 + SHOW_ARTIST_TITLE = 1 + RADIO_SHOW = 2 + + +class StreamPreferences(BaseModel): + input_fade_transition: float + message_format: MessageFormatKind + message_offline: str + # input_auto_switch_off: bool + # input_auto_switch_on: bool + # input_main_user: str + # input_main_password: str + # replay_gain_enabled: bool + # replay_gain_offset: float + # track_fade_in: float + # track_fade_out: float + # track_fade_transition: float + + +class StreamState(BaseModel): + input_main_connected: bool + input_main_streaming: bool + input_show_connected: bool + input_show_streaming: bool + schedule_streaming: bool + + +class SitePreferenceManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter(user__isnull=True) class Preference(models.Model): @@ -22,6 +63,36 @@ class Preference(models.Model): db_column="valstr", ) + objects = models.Manager() + site = SitePreferenceManager() + + @classmethod + def get_site_preferences(cls) -> SitePreferences: + entries = dict(cls.site.values_list("key", "value")) + return SitePreferences( + station_name=entries.get("station_name") or "LibreTime", + ) + + @classmethod + def get_stream_preferences(cls) -> StreamPreferences: + entries = dict(cls.site.values_list("key", "value")) + return StreamPreferences( + input_fade_transition=float(entries.get("default_transition_fade") or 0.0), + message_format=int(entries.get("stream_label_format") or 0), + message_offline=entries.get("off_air_meta") or "Offline", + ) + + @classmethod + def get_stream_state(cls) -> StreamState: + entries = dict(cls.site.values_list("key", "value")) + return StreamState( + input_main_connected=entries.get("master_dj") == "true", + input_main_streaming=entries.get("master_dj_switch") == "on", + input_show_connected=entries.get("live_dj") == "true", + input_show_streaming=entries.get("live_dj_switch") == "on", + schedule_streaming=entries.get("scheduled_play_switch") == "on", + ) + class Meta: managed = False db_table = "cc_pref" diff --git a/api/libretime_api/core/router.py b/api/libretime_api/core/router.py index 3f2aebcd9..f1406117b 100644 --- a/api/libretime_api/core/router.py +++ b/api/libretime_api/core/router.py @@ -3,10 +3,12 @@ from rest_framework import routers from .views import ( CeleryTaskViewSet, + InfoView, LoginAttemptViewSet, PreferenceViewSet, ServiceRegisterViewSet, - StreamSettingViewSet, + StreamPreferencesView, + StreamStateView, ThirdPartyTrackReferenceViewSet, UserTokenViewSet, UserViewSet, @@ -17,7 +19,6 @@ router = routers.DefaultRouter(trailing_slash=False) router.register("login-attempts", LoginAttemptViewSet) router.register("preferences", PreferenceViewSet) router.register("service-registers", ServiceRegisterViewSet) -router.register("stream-settings", StreamSettingViewSet) router.register("users", UserViewSet) router.register("user-tokens", UserTokenViewSet) router.register("celery-tasks", CeleryTaskViewSet) @@ -25,5 +26,8 @@ router.register("third-party-track-references", ThirdPartyTrackReferenceViewSet) urls = [ *router.urls, + path("info", InfoView.as_view()), path("version", VersionView.as_view()), + path("stream/preferences", StreamPreferencesView.as_view()), + path("stream/state", StreamStateView.as_view()), ] diff --git a/api/libretime_api/core/serializers/__init__.py b/api/libretime_api/core/serializers/__init__.py index d3c89a503..c6f0fc2ad 100644 --- a/api/libretime_api/core/serializers/__init__.py +++ b/api/libretime_api/core/serializers/__init__.py @@ -1,6 +1,7 @@ from .auth import LoginAttemptSerializer, UserTokenSerializer -from .info import VersionSerializer -from .preference import PreferenceSerializer, StreamSettingSerializer +from .info import InfoSerializer, VersionSerializer +from .preference import PreferenceSerializer from .service import ServiceRegisterSerializer +from .stream import StreamPreferencesSerializer, StreamStateSerializer from .user import UserSerializer from .worker import CeleryTaskSerializer, ThirdPartyTrackReferenceSerializer diff --git a/api/libretime_api/core/serializers/info.py b/api/libretime_api/core/serializers/info.py index 7195b8c9d..ae063e877 100644 --- a/api/libretime_api/core/serializers/info.py +++ b/api/libretime_api/core/serializers/info.py @@ -3,4 +3,9 @@ from rest_framework import serializers # pylint: disable=abstract-method class VersionSerializer(serializers.Serializer): - api_version = serializers.CharField() + api_version = serializers.CharField(read_only=True) + + +# pylint: disable=abstract-method +class InfoSerializer(serializers.Serializer): + station_name = serializers.CharField(read_only=True) diff --git a/api/libretime_api/core/serializers/preference.py b/api/libretime_api/core/serializers/preference.py index 422a0bf23..3a7cad8ea 100644 --- a/api/libretime_api/core/serializers/preference.py +++ b/api/libretime_api/core/serializers/preference.py @@ -1,15 +1,9 @@ from rest_framework import serializers -from ..models import Preference, StreamSetting +from ..models import Preference class PreferenceSerializer(serializers.ModelSerializer): class Meta: model = Preference fields = "__all__" - - -class StreamSettingSerializer(serializers.ModelSerializer): - class Meta: - model = StreamSetting - fields = "__all__" diff --git a/api/libretime_api/core/serializers/stream.py b/api/libretime_api/core/serializers/stream.py new file mode 100644 index 000000000..c0c265f34 --- /dev/null +++ b/api/libretime_api/core/serializers/stream.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + + +# pylint: disable=abstract-method +class StreamPreferencesSerializer(serializers.Serializer): + input_fade_transition = serializers.FloatField(read_only=True) + message_format = serializers.IntegerField(read_only=True) + message_offline = serializers.CharField(read_only=True) + + +# pylint: disable=abstract-method +class StreamStateSerializer(serializers.Serializer): + input_main_connected = serializers.BooleanField(read_only=True) + input_main_streaming = serializers.BooleanField(read_only=True) + input_show_connected = serializers.BooleanField(read_only=True) + input_show_streaming = serializers.BooleanField(read_only=True) + schedule_streaming = serializers.BooleanField(read_only=True) diff --git a/api/libretime_api/core/tests/models/test_preference.py b/api/libretime_api/core/tests/models/test_preference.py index e69de29bb..1a8a38da3 100644 --- a/api/libretime_api/core/tests/models/test_preference.py +++ b/api/libretime_api/core/tests/models/test_preference.py @@ -0,0 +1,31 @@ +from libretime_api.core.models.preference import Preference + + +# pylint: disable=invalid-name,unused-argument +def test_preference_get_site_preferences(db): + result = Preference.get_site_preferences() + assert result.dict() == { + "station_name": "LibreTime", + } + + +# pylint: disable=invalid-name,unused-argument +def test_preference_get_stream_preferences(db): + result = Preference.get_stream_preferences() + assert result.dict() == { + "input_fade_transition": 0.0, + "message_format": 0, + "message_offline": "LibreTime - offline", + } + + +# pylint: disable=invalid-name,unused-argument +def test_preference_get_stream_state(db): + result = Preference.get_stream_state() + assert result.dict() == { + "input_main_connected": False, + "input_main_streaming": False, + "input_show_connected": False, + "input_show_streaming": False, + "schedule_streaming": True, + } diff --git a/api/libretime_api/core/tests/views/test_info.py b/api/libretime_api/core/tests/views/test_info.py new file mode 100644 index 000000000..a253ce325 --- /dev/null +++ b/api/libretime_api/core/tests/views/test_info.py @@ -0,0 +1,19 @@ +from rest_framework.test import APIClient + + +# pylint: disable=invalid-name,unused-argument +def test_version_get(db, api_client: APIClient): + response = api_client.get("/api/v2/version") + assert response.status_code == 200 + assert response.json() == { + "api_version": "2.0.0", + } + + +# pylint: disable=invalid-name,unused-argument +def test_info_get(db, api_client: APIClient): + response = api_client.get("/api/v2/info") + assert response.status_code == 200 + assert response.json() == { + "station_name": "LibreTime", + } diff --git a/api/libretime_api/core/tests/views/test_stream.py b/api/libretime_api/core/tests/views/test_stream.py new file mode 100644 index 000000000..7f9f9bfa0 --- /dev/null +++ b/api/libretime_api/core/tests/views/test_stream.py @@ -0,0 +1,25 @@ +from rest_framework.test import APIClient + + +# pylint: disable=invalid-name,unused-argument +def test_stream_preferences_get(db, api_client: APIClient): + response = api_client.get("/api/v2/stream/preferences") + assert response.status_code == 200 + assert response.json() == { + "input_fade_transition": 0.0, + "message_format": 0, + "message_offline": "LibreTime - offline", + } + + +# pylint: disable=invalid-name,unused-argument +def test_stream_state_get(db, api_client: APIClient): + response = api_client.get("/api/v2/stream/state") + assert response.status_code == 200 + assert response.json() == { + "input_main_connected": False, + "input_main_streaming": False, + "input_show_connected": False, + "input_show_streaming": False, + "schedule_streaming": True, + } diff --git a/api/libretime_api/core/views/__init__.py b/api/libretime_api/core/views/__init__.py index b2f7804be..cda9c3563 100644 --- a/api/libretime_api/core/views/__init__.py +++ b/api/libretime_api/core/views/__init__.py @@ -1,6 +1,7 @@ from .auth import LoginAttemptViewSet, UserTokenViewSet -from .info import VersionView -from .preference import PreferenceViewSet, StreamSettingViewSet +from .info import InfoView, VersionView +from .preference import PreferenceViewSet from .service import ServiceRegisterViewSet +from .stream import StreamPreferencesView, StreamStateView from .user import UserViewSet from .worker import CeleryTaskViewSet, ThirdPartyTrackReferenceViewSet diff --git a/api/libretime_api/core/views/info.py b/api/libretime_api/core/views/info.py index 121be34f5..6e188c036 100644 --- a/api/libretime_api/core/views/info.py +++ b/api/libretime_api/core/views/info.py @@ -3,7 +3,8 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView -from ..serializers import VersionSerializer +from ..models import Preference +from ..serializers import InfoSerializer, VersionSerializer class VersionView(APIView): @@ -12,3 +13,18 @@ class VersionView(APIView): def get(self, request): return Response({"api_version": settings.API_VERSION}) + + +class InfoView(APIView): + permission_classes = [AllowAny] + serializer_class = InfoSerializer + + def get(self, request): + data = Preference.get_site_preferences() + return Response( + data.dict( + include={ + "station_name", + } + ) + ) diff --git a/api/libretime_api/core/views/preference.py b/api/libretime_api/core/views/preference.py index c1a3401d2..1e05d1dd3 100644 --- a/api/libretime_api/core/views/preference.py +++ b/api/libretime_api/core/views/preference.py @@ -1,16 +1,10 @@ from rest_framework import viewsets -from ..models import Preference, StreamSetting -from ..serializers import PreferenceSerializer, StreamSettingSerializer +from ..models import Preference +from ..serializers import PreferenceSerializer 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/stream.py b/api/libretime_api/core/views/stream.py new file mode 100644 index 000000000..f25d5e577 --- /dev/null +++ b/api/libretime_api/core/views/stream.py @@ -0,0 +1,44 @@ +from rest_framework import views +from rest_framework.response import Response + +from ...permissions import IsSystemTokenOrUser +from ..models import Preference +from ..serializers import StreamPreferencesSerializer, StreamStateSerializer + + +class StreamPreferencesView(views.APIView): + permission_classes = [IsSystemTokenOrUser] + serializer_class = StreamPreferencesSerializer + model_permission_name = "streamsetting" + + def get(self, request): + data = Preference.get_stream_preferences() + return Response( + data.dict( + include={ + "input_fade_transition", + "message_format", + "message_offline", + } + ) + ) + + +class StreamStateView(views.APIView): + permission_classes = [IsSystemTokenOrUser] + serializer_class = StreamStateSerializer + model_permission_name = "streamsetting" + + def get(self, request): + data = Preference.get_stream_state() + return Response( + data.dict( + include={ + "input_main_connected", + "input_main_streaming", + "input_show_connected", + "input_show_streaming", + "schedule_streaming", + } + ) + ) diff --git a/api/schema.yml b/api/schema.yml index b665dec24..a271f77bd 100644 --- a/api/schema.yml +++ b/api/schema.yml @@ -467,6 +467,22 @@ paths: responses: "204": description: No response body + /api/v2/info: + get: + operationId: info_retrieve + tags: + - info + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Info" + description: "" /api/v2/libraries: get: operationId: libraries_list @@ -4326,11 +4342,11 @@ paths: responses: "204": description: No response body - /api/v2/stream-settings: + /api/v2/stream/preferences: get: - operationId: stream_settings_list + operationId: stream_preferences_retrieve tags: - - stream-settings + - stream security: - cookieAuth: [] - basicAuth: [] @@ -4339,48 +4355,13 @@ paths: content: application/json: schema: - type: array - items: - $ref: "#/components/schemas/StreamSetting" + $ref: "#/components/schemas/StreamPreferences" description: "" - post: - operationId: stream_settings_create - tags: - - stream-settings - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/StreamSetting" - application/x-www-form-urlencoded: - schema: - $ref: "#/components/schemas/StreamSetting" - multipart/form-data: - schema: - $ref: "#/components/schemas/StreamSetting" - required: true - security: - - cookieAuth: [] - - basicAuth: [] - responses: - "201": - content: - application/json: - schema: - $ref: "#/components/schemas/StreamSetting" - description: "" - /api/v2/stream-settings/{key}: + /api/v2/stream/state: get: - operationId: stream_settings_retrieve - parameters: - - in: path - name: key - schema: - type: string - description: A unique value identifying this stream setting. - required: true + operationId: stream_state_retrieve tags: - - stream-settings + - stream security: - cookieAuth: [] - basicAuth: [] @@ -4389,90 +4370,8 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/StreamSetting" + $ref: "#/components/schemas/StreamState" description: "" - put: - operationId: stream_settings_update - parameters: - - in: path - name: key - schema: - type: string - description: A unique value identifying this stream setting. - required: true - tags: - - stream-settings - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/StreamSetting" - application/x-www-form-urlencoded: - schema: - $ref: "#/components/schemas/StreamSetting" - multipart/form-data: - schema: - $ref: "#/components/schemas/StreamSetting" - required: true - security: - - cookieAuth: [] - - basicAuth: [] - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/StreamSetting" - description: "" - patch: - operationId: stream_settings_partial_update - parameters: - - in: path - name: key - schema: - type: string - description: A unique value identifying this stream setting. - required: true - tags: - - stream-settings - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/PatchedStreamSetting" - application/x-www-form-urlencoded: - schema: - $ref: "#/components/schemas/PatchedStreamSetting" - multipart/form-data: - schema: - $ref: "#/components/schemas/PatchedStreamSetting" - security: - - cookieAuth: [] - - basicAuth: [] - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/StreamSetting" - description: "" - delete: - operationId: stream_settings_destroy - parameters: - - in: path - name: key - schema: - type: string - description: A unique value identifying this stream setting. - required: true - tags: - - stream-settings - security: - - cookieAuth: [] - - basicAuth: [] - responses: - "204": - description: No response body /api/v2/third-party-track-references: get: operationId: third_party_track_references_list @@ -5684,6 +5583,14 @@ components: - id - override_album - podcast + Info: + type: object + properties: + station_name: + type: string + readOnly: true + required: + - station_name Library: type: object properties: @@ -6708,19 +6615,6 @@ components: readOnly: true podcast: type: integer - PatchedStreamSetting: - type: object - properties: - key: - type: string - maxLength: 64 - raw_value: - type: string - nullable: true - maxLength: 255 - type: - type: string - maxLength: 16 PatchedThirdPartyTrackReference: type: object properties: @@ -7579,22 +7473,47 @@ components: required: - id - podcast - StreamSetting: + StreamPreferences: type: object properties: - key: + input_fade_transition: + type: number + format: double + readOnly: true + message_format: + type: integer + readOnly: true + message_offline: type: string - maxLength: 64 - raw_value: - type: string - nullable: true - maxLength: 255 - type: - type: string - maxLength: 16 + readOnly: true required: - - key - - type + - input_fade_transition + - message_format + - message_offline + StreamState: + type: object + properties: + input_main_connected: + type: boolean + readOnly: true + input_main_streaming: + type: boolean + readOnly: true + input_show_connected: + type: boolean + readOnly: true + input_show_streaming: + type: boolean + readOnly: true + schedule_streaming: + type: boolean + readOnly: true + required: + - input_main_connected + - input_main_streaming + - input_show_connected + - input_show_streaming + - schedule_streaming ThirdPartyTrackReference: type: object properties: @@ -7714,6 +7633,7 @@ components: properties: api_version: type: string + readOnly: true required: - api_version Webstream: