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

1from rest_framework import serializers 

2 

3from elasticsearch_dsl.response import Hit 

4 

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) 

15 

16 

17####################### 

18# Request serializers # 

19####################### 

20 

21 

22class AudioSearchRequestSerializer(MediaSearchRequestSerializer): 

23 """Parse and validate search query string parameters.""" 

24 

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 """ 

34 

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 ) 

50 

51 

52class AudioReportRequestSerializer(MediaReportRequestSerializer): 

53 identifier = serializers.SlugRelatedField( 

54 slug_field="identifier", 

55 queryset=Audio.objects.all(), 

56 source="media_obj", 

57 ) 

58 

59 class Meta(MediaReportRequestSerializer.Meta): 

60 model = AudioReport 

61 

62 

63######################## 

64# Response serializers # 

65######################## 

66 

67 

68class AudioSetSerializer(serializers.ModelSerializer): 

69 """An audio set, rendered as a part of the ``AudioSerializer`` output.""" 

70 

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 ] 

82 

83 

84AudioHyperlinksSerializer = get_hyperlinks_serializer("audio") 

85 

86 

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 """ 

92 

93 class Meta: 

94 fields = [ 

95 "url", 

96 "bit_rate", 

97 "filesize", 

98 "filetype", 

99 "sample_rate", 

100 ] 

101 

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 ) 

123 

124 

125class AudioSerializer(AudioHyperlinksSerializer, MediaSerializer): 

126 """A single audio file. Used in search results.""" 

127 

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 """ 

149 

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 ) 

155 

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 ) 

162 

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 ) 

169 

170 # Add-on data 

171 peaks = serializers.SerializerMethodField( 

172 help_text="The list of peaks used to generate the waveform for the audio." 

173 ) 

174 

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) 

180 

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 

185 

186 def to_representation(self, instance): 

187 # Get the original representation 

188 output = super().to_representation(instance) 

189 audio = instance 

190 

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) 

194 

195 if isinstance(audio, Audio) and not audio.thumbnail: 

196 output["thumbnail"] = None 

197 

198 return output 

199 

200 

201########################## 

202# Additional serializers # 

203########################## 

204 

205 

206class AudioWaveformSerializer(serializers.Serializer): 

207 len = serializers.SerializerMethodField() 

208 points = serializers.ListField( 

209 child=serializers.FloatField(min_value=0, max_value=1) 

210 ) 

211 

212 @staticmethod 

213 def get_len(obj) -> int: 

214 return len(obj.get("points", []))