Coverage for api/serializers/audio_serializers.py: 97%
61 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 06:14 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 06:14 +0000
1from rest_framework import serializers
3from elasticsearch_dsl.response import Hit
5from api.constants.field_order import field_position_map
6from api.constants.field_values import AUDIO_CATEGORIES, LENGTHS
7from api.models import Audio, AudioReport, AudioSet
8from api.serializers.fields import EnumCharField, SchemableHyperlinkedIdentityField
9from api.serializers.media_serializers import (
10 MediaReportRequestSerializer,
11 MediaSearchRequestSerializer,
12 MediaSerializer,
13 get_hyperlinks_serializer,
14)
17#######################
18# Request serializers #
19#######################
22class AudioSearchRequestSerializer(MediaSearchRequestSerializer):
23 """Parse and validate search query string parameters."""
25 field_names = [
26 *MediaSearchRequestSerializer.field_names,
27 "category",
28 "length",
29 ]
30 """
31 Keep the fields names in sync with the actual fields below as this list is
32 used to generate Swagger documentation.
33 """
35 category = EnumCharField(
36 plural="categories",
37 enum_class=AUDIO_CATEGORIES,
38 required=False,
39 )
40 length = EnumCharField(
41 plural="lengths",
42 enum_class=LENGTHS,
43 required=False,
44 )
45 peaks = serializers.BooleanField(
46 help_text="Whether to include the waveform peaks or not",
47 required=False,
48 default=False,
49 )
52class AudioReportRequestSerializer(MediaReportRequestSerializer):
53 identifier = serializers.SlugRelatedField(
54 slug_field="identifier",
55 queryset=Audio.objects.all(),
56 source="media_obj",
57 )
59 class Meta(MediaReportRequestSerializer.Meta):
60 model = AudioReport
63########################
64# Response serializers #
65########################
68class AudioSetSerializer(serializers.ModelSerializer):
69 """An audio set, rendered as a part of the ``AudioSerializer`` output."""
71 class Meta:
72 model = AudioSet
73 fields = [
74 "title",
75 "foreign_landing_url",
76 "creator",
77 "creator_url",
78 "url",
79 "filesize",
80 "filetype",
81 ]
84AudioHyperlinksSerializer = get_hyperlinks_serializer("audio")
87class AudioAltFileSerializer(serializers.Serializer):
88 """
89 A set of alternative files for a single audio object,
90 rendered as a part of the ``AudioSerializer`` output.
91 """
93 class Meta:
94 fields = [
95 "url",
96 "bit_rate",
97 "filesize",
98 "filetype",
99 "sample_rate",
100 ]
102 url = serializers.URLField(
103 help_text="URL of the alternative file.",
104 )
105 bit_rate = serializers.IntegerField(
106 help_text="Bit rate of the alternative file.",
107 min_value=0,
108 required=False,
109 )
110 filesize = serializers.IntegerField(
111 help_text="Size of the alternative file in bytes.",
112 min_value=0,
113 required=False,
114 )
115 filetype = serializers.CharField(
116 help_text="File type of the alternative file.",
117 )
118 sample_rate = serializers.IntegerField(
119 help_text="Sample rate of the alternative file.",
120 required=False,
121 min_value=0,
122 )
125class AudioSerializer(AudioHyperlinksSerializer, MediaSerializer):
126 """A single audio file. Used in search results."""
128 class Meta:
129 model = Audio
130 fields = sorted( # keep this list ordered logically
131 [
132 *MediaSerializer.Meta.fields,
133 *AudioHyperlinksSerializer.field_names,
134 "genres",
135 "alt_files",
136 "audio_set",
137 "duration",
138 "bit_rate",
139 "sample_rate",
140 "waveform", # hyperlink to the endpoint that generates the waveform
141 "peaks", # waveform peaks, if they have already been generated
142 ],
143 key=lambda val: field_position_map.get(val, 999),
144 )
145 """
146 Keep the fields names in sync with the actual fields below as this list is
147 used to generate Swagger documentation.
148 """
150 audio_set = AudioSetSerializer(
151 allow_null=True,
152 help_text="Reference to set of which this track is a part.",
153 read_only=True,
154 )
156 alt_files = AudioAltFileSerializer(
157 allow_null=True,
158 help_text="JSON describing alternative files for this audio.",
159 read_only=True,
160 many=True,
161 )
163 waveform = SchemableHyperlinkedIdentityField(
164 read_only=True,
165 view_name="audio-waveform",
166 lookup_field="identifier",
167 help_text="A direct link to the waveform peaks.",
168 )
170 # Add-on data
171 peaks = serializers.SerializerMethodField(
172 help_text="The list of peaks used to generate the waveform for the audio."
173 )
175 def __init__(self, *args, **kwargs):
176 # Includes the peaks only if requested via the `peaks` query param
177 if not kwargs.get("context", {}).get("validated_data", {}).get("peaks"):
178 del self.fields["peaks"]
179 super().__init__(*args, **kwargs)
181 def get_peaks(self, obj) -> list[int]:
182 audio_addon = self.context.get("addons", {}).get(obj.identifier)
183 if audio_addon:
184 return audio_addon.waveform_peaks
186 def to_representation(self, instance):
187 # Get the original representation
188 output = super().to_representation(instance)
189 audio = instance
191 if isinstance(instance, Hit): 191 ↛ 193line 191 didn't jump to line 193 because the condition on line 191 was never true
192 # TODO: Remove this DB query when updating ES index
193 audio = Audio.objects.get(identifier=instance.identifier)
195 if isinstance(audio, Audio) and not audio.thumbnail:
196 output["thumbnail"] = None
198 return output
201##########################
202# Additional serializers #
203##########################
206class AudioWaveformSerializer(serializers.Serializer):
207 len = serializers.SerializerMethodField()
208 points = serializers.ListField(
209 child=serializers.FloatField(min_value=0, max_value=1)
210 )
212 @staticmethod
213 def get_len(obj) -> int:
214 return len(obj.get("points", []))