187 lines
6.2 KiB
Python
187 lines
6.2 KiB
Python
|
|
"""Serializers for local audio files"""
|
||
|
|
|
||
|
|
from rest_framework import serializers
|
||
|
|
from audio.models_local import LocalAudio, LocalAudioPlaylist, LocalAudioPlaylistItem
|
||
|
|
|
||
|
|
|
||
|
|
class LocalAudioSerializer(serializers.ModelSerializer):
|
||
|
|
"""Serializer for local audio files"""
|
||
|
|
file_size_mb = serializers.FloatField(read_only=True)
|
||
|
|
duration_formatted = serializers.CharField(read_only=True)
|
||
|
|
file_url = serializers.SerializerMethodField()
|
||
|
|
cover_art_url = serializers.SerializerMethodField()
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = LocalAudio
|
||
|
|
fields = [
|
||
|
|
'id',
|
||
|
|
'title',
|
||
|
|
'artist',
|
||
|
|
'album',
|
||
|
|
'year',
|
||
|
|
'genre',
|
||
|
|
'track_number',
|
||
|
|
'file',
|
||
|
|
'file_url',
|
||
|
|
'file_size',
|
||
|
|
'file_size_mb',
|
||
|
|
'duration',
|
||
|
|
'duration_formatted',
|
||
|
|
'audio_format',
|
||
|
|
'bitrate',
|
||
|
|
'sample_rate',
|
||
|
|
'channels',
|
||
|
|
'cover_art',
|
||
|
|
'cover_art_url',
|
||
|
|
'original_filename',
|
||
|
|
'uploaded_date',
|
||
|
|
'modified_date',
|
||
|
|
'play_count',
|
||
|
|
'last_played',
|
||
|
|
'tags',
|
||
|
|
'notes',
|
||
|
|
'is_favorite',
|
||
|
|
]
|
||
|
|
read_only_fields = [
|
||
|
|
'id',
|
||
|
|
'file_size',
|
||
|
|
'duration',
|
||
|
|
'audio_format',
|
||
|
|
'bitrate',
|
||
|
|
'sample_rate',
|
||
|
|
'channels',
|
||
|
|
'original_filename',
|
||
|
|
'uploaded_date',
|
||
|
|
'modified_date',
|
||
|
|
'play_count',
|
||
|
|
'last_played',
|
||
|
|
]
|
||
|
|
|
||
|
|
def get_file_url(self, obj):
|
||
|
|
"""Get full URL for audio file"""
|
||
|
|
request = self.context.get('request')
|
||
|
|
if obj.file and request:
|
||
|
|
return request.build_absolute_uri(obj.file.url)
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_cover_art_url(self, obj):
|
||
|
|
"""Get full URL for cover art"""
|
||
|
|
request = self.context.get('request')
|
||
|
|
if obj.cover_art and request:
|
||
|
|
return request.build_absolute_uri(obj.cover_art.url)
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
class LocalAudioUploadSerializer(serializers.ModelSerializer):
|
||
|
|
"""Serializer for uploading local audio files"""
|
||
|
|
# Make title optional - will be extracted from ID3 tags if not provided
|
||
|
|
title = serializers.CharField(required=False, allow_blank=True, max_length=500)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = LocalAudio
|
||
|
|
fields = [
|
||
|
|
'file',
|
||
|
|
'title',
|
||
|
|
'artist',
|
||
|
|
'album',
|
||
|
|
'year',
|
||
|
|
'genre',
|
||
|
|
'track_number',
|
||
|
|
'cover_art',
|
||
|
|
'tags',
|
||
|
|
'notes',
|
||
|
|
]
|
||
|
|
|
||
|
|
def create(self, validated_data):
|
||
|
|
"""Extract metadata and create local audio"""
|
||
|
|
file = validated_data['file']
|
||
|
|
|
||
|
|
# Set original filename
|
||
|
|
validated_data['original_filename'] = file.name
|
||
|
|
validated_data['file_size'] = file.size
|
||
|
|
|
||
|
|
# Extract audio format from filename
|
||
|
|
file_extension = file.name.split('.')[-1].lower()
|
||
|
|
validated_data['audio_format'] = file_extension
|
||
|
|
|
||
|
|
# Try to extract metadata using mutagen
|
||
|
|
try:
|
||
|
|
from mutagen import File as MutagenFile
|
||
|
|
audio_file = MutagenFile(file)
|
||
|
|
|
||
|
|
if audio_file:
|
||
|
|
# Get duration
|
||
|
|
if hasattr(audio_file.info, 'length'):
|
||
|
|
validated_data['duration'] = int(audio_file.info.length)
|
||
|
|
|
||
|
|
# Get bitrate
|
||
|
|
if hasattr(audio_file.info, 'bitrate'):
|
||
|
|
validated_data['bitrate'] = int(audio_file.info.bitrate / 1000) # Convert to kbps
|
||
|
|
|
||
|
|
# Get sample rate
|
||
|
|
if hasattr(audio_file.info, 'sample_rate'):
|
||
|
|
validated_data['sample_rate'] = audio_file.info.sample_rate
|
||
|
|
|
||
|
|
# Get channels
|
||
|
|
if hasattr(audio_file.info, 'channels'):
|
||
|
|
validated_data['channels'] = audio_file.info.channels
|
||
|
|
|
||
|
|
# Extract ID3 tags if not provided
|
||
|
|
if not validated_data.get('title'):
|
||
|
|
validated_data['title'] = audio_file.get('TIT2', [file.name])[0] if hasattr(audio_file, 'get') else file.name
|
||
|
|
|
||
|
|
if not validated_data.get('artist'):
|
||
|
|
validated_data['artist'] = audio_file.get('TPE1', [''])[0] if hasattr(audio_file, 'get') else ''
|
||
|
|
|
||
|
|
if not validated_data.get('album'):
|
||
|
|
validated_data['album'] = audio_file.get('TALB', [''])[0] if hasattr(audio_file, 'get') else ''
|
||
|
|
except Exception as e:
|
||
|
|
# If metadata extraction fails, use filename as title
|
||
|
|
if not validated_data.get('title'):
|
||
|
|
validated_data['title'] = file.name
|
||
|
|
|
||
|
|
return super().create(validated_data)
|
||
|
|
|
||
|
|
|
||
|
|
class LocalAudioPlaylistItemSerializer(serializers.ModelSerializer):
|
||
|
|
"""Serializer for playlist items"""
|
||
|
|
audio_data = LocalAudioSerializer(source='audio', read_only=True)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = LocalAudioPlaylistItem
|
||
|
|
fields = ['id', 'audio', 'audio_data', 'position', 'added_date']
|
||
|
|
read_only_fields = ['id', 'added_date']
|
||
|
|
|
||
|
|
|
||
|
|
class LocalAudioPlaylistSerializer(serializers.ModelSerializer):
|
||
|
|
"""Serializer for local audio playlists"""
|
||
|
|
items = LocalAudioPlaylistItemSerializer(many=True, read_only=True)
|
||
|
|
items_count = serializers.SerializerMethodField()
|
||
|
|
cover_image_url = serializers.SerializerMethodField()
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = LocalAudioPlaylist
|
||
|
|
fields = [
|
||
|
|
'id',
|
||
|
|
'title',
|
||
|
|
'description',
|
||
|
|
'cover_image',
|
||
|
|
'cover_image_url',
|
||
|
|
'created_date',
|
||
|
|
'modified_date',
|
||
|
|
'items',
|
||
|
|
'items_count',
|
||
|
|
]
|
||
|
|
read_only_fields = ['id', 'created_date', 'modified_date']
|
||
|
|
|
||
|
|
def get_items_count(self, obj):
|
||
|
|
"""Get number of items in playlist"""
|
||
|
|
return obj.items.count()
|
||
|
|
|
||
|
|
def get_cover_image_url(self, obj):
|
||
|
|
"""Get full URL for cover image"""
|
||
|
|
request = self.context.get('request')
|
||
|
|
if obj.cover_image and request:
|
||
|
|
return request.build_absolute_uri(obj.cover_image.url)
|
||
|
|
return None
|