Fix: Include backend/audio Django app in repository
This commit is contained in:
parent
d04e726373
commit
644cfab298
37 changed files with 6632 additions and 4 deletions
556
backend/audio/tasks_artwork.py
Normal file
556
backend/audio/tasks_artwork.py
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
"""Celery tasks for artwork and metadata management"""
|
||||
from celery import shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import Q
|
||||
import os
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
from audio.models import Audio, Channel
|
||||
from audio.models_artwork import Artwork, MusicMetadata, ArtistInfo
|
||||
from audio.lastfm_client import LastFMClient
|
||||
from audio.fanart_client import FanartClient
|
||||
from audio.id3_service import ID3TagService
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def fetch_metadata_for_audio(self, audio_id: int):
|
||||
"""
|
||||
Fetch metadata for audio from Last.fm
|
||||
|
||||
Args:
|
||||
audio_id: Audio ID
|
||||
"""
|
||||
try:
|
||||
audio = Audio.objects.get(id=audio_id)
|
||||
client = LastFMClient()
|
||||
|
||||
# Extract artist and title from audio
|
||||
artist = audio.channel.channel_name if audio.channel else 'Unknown Artist'
|
||||
title = audio.audio_title
|
||||
|
||||
# Search Last.fm
|
||||
track_info = client.search_track(artist, title)
|
||||
|
||||
if not track_info:
|
||||
logger.warning(f"No track info found on Last.fm for: {artist} - {title}")
|
||||
return
|
||||
|
||||
# Create or update metadata
|
||||
metadata, created = MusicMetadata.objects.get_or_create(audio=audio)
|
||||
|
||||
# Update metadata fields
|
||||
if 'album' in track_info:
|
||||
metadata.album_name = track_info['album']
|
||||
if 'tags' in track_info and track_info['tags']:
|
||||
metadata.genre = track_info['tags'][0] if track_info['tags'] else None
|
||||
metadata.tags = track_info['tags']
|
||||
|
||||
metadata.lastfm_url = track_info.get('url', '')
|
||||
metadata.lastfm_mbid = track_info.get('mbid', '')
|
||||
metadata.play_count = track_info.get('playcount', 0)
|
||||
metadata.listeners = track_info.get('listeners', 0)
|
||||
|
||||
metadata.save()
|
||||
|
||||
logger.info(f"Updated metadata for audio {audio_id}")
|
||||
|
||||
# Also fetch artwork
|
||||
fetch_artwork_for_audio.delay(audio_id)
|
||||
|
||||
except Audio.DoesNotExist:
|
||||
logger.error(f"Audio {audio_id} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching metadata for audio {audio_id}: {e}")
|
||||
raise self.retry(exc=e, countdown=300)
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def fetch_artwork_for_audio(self, audio_id: int):
|
||||
"""
|
||||
Fetch artwork for audio from Last.fm and Fanart.tv
|
||||
|
||||
Args:
|
||||
audio_id: Audio ID
|
||||
"""
|
||||
try:
|
||||
audio = Audio.objects.get(id=audio_id)
|
||||
|
||||
# Try Last.fm first
|
||||
lastfm_client = LastFMClient()
|
||||
artist = audio.channel.channel_name if audio.channel else 'Unknown Artist'
|
||||
title = audio.audio_title
|
||||
|
||||
track_info = lastfm_client.search_track(artist, title)
|
||||
|
||||
if track_info and 'images' in track_info:
|
||||
# Save album cover from Last.fm
|
||||
for img in track_info['images']:
|
||||
if img['size'] in ['large', 'extralarge', 'mega']:
|
||||
# Check if artwork already exists
|
||||
if not Artwork.objects.filter(
|
||||
audio=audio,
|
||||
source='lastfm',
|
||||
artwork_type='audio_cover'
|
||||
).exists():
|
||||
artwork = Artwork.objects.create(
|
||||
audio=audio,
|
||||
artwork_type='audio_cover',
|
||||
source='lastfm',
|
||||
url=img['url'],
|
||||
priority=20
|
||||
)
|
||||
|
||||
# Download and save locally
|
||||
download_artwork.delay(artwork.id)
|
||||
|
||||
logger.info(f"Created Last.fm artwork for audio {audio_id}")
|
||||
break
|
||||
|
||||
# Try Fanart.tv if we have MusicBrainz ID
|
||||
try:
|
||||
metadata = MusicMetadata.objects.get(audio=audio)
|
||||
if metadata.lastfm_mbid:
|
||||
fanart_client = FanartClient()
|
||||
artist_images = fanart_client.get_artist_images(metadata.lastfm_mbid)
|
||||
|
||||
if artist_images:
|
||||
# Save artist thumbnail
|
||||
if artist_images['thumbnails']:
|
||||
img = artist_images['thumbnails'][0]
|
||||
if not Artwork.objects.filter(
|
||||
audio=audio,
|
||||
source='fanart',
|
||||
artwork_type='audio_cover'
|
||||
).exists():
|
||||
artwork = Artwork.objects.create(
|
||||
audio=audio,
|
||||
artwork_type='audio_cover',
|
||||
source='fanart',
|
||||
url=img['url'],
|
||||
priority=30
|
||||
)
|
||||
download_artwork.delay(artwork.id)
|
||||
logger.info(f"Created Fanart.tv artwork for audio {audio_id}")
|
||||
except MusicMetadata.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Use YouTube thumbnail as fallback
|
||||
if audio.thumb_url and not Artwork.objects.filter(audio=audio, source='youtube').exists():
|
||||
artwork = Artwork.objects.create(
|
||||
audio=audio,
|
||||
artwork_type='audio_thumbnail',
|
||||
source='youtube',
|
||||
url=audio.thumb_url,
|
||||
priority=10
|
||||
)
|
||||
download_artwork.delay(artwork.id)
|
||||
logger.info(f"Created YouTube thumbnail artwork for audio {audio_id}")
|
||||
|
||||
except Audio.DoesNotExist:
|
||||
logger.error(f"Audio {audio_id} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching artwork for audio {audio_id}: {e}")
|
||||
raise self.retry(exc=e, countdown=300)
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def fetch_artist_info(self, channel_id: int):
|
||||
"""
|
||||
Fetch artist information from Last.fm
|
||||
|
||||
Args:
|
||||
channel_id: Channel ID
|
||||
"""
|
||||
try:
|
||||
channel = Channel.objects.get(id=channel_id)
|
||||
client = LastFMClient()
|
||||
|
||||
artist_name = channel.channel_name
|
||||
artist_info = client.get_artist_info(artist_name)
|
||||
|
||||
if not artist_info:
|
||||
logger.warning(f"No artist info found on Last.fm for: {artist_name}")
|
||||
return
|
||||
|
||||
# Create or update artist info
|
||||
info, created = ArtistInfo.objects.get_or_create(channel=channel)
|
||||
|
||||
info.bio = artist_info.get('bio', '')
|
||||
info.bio_summary = artist_info.get('bio_summary', '')
|
||||
info.lastfm_url = artist_info.get('url', '')
|
||||
info.lastfm_mbid = artist_info.get('mbid', '')
|
||||
info.lastfm_listeners = artist_info.get('listeners', 0)
|
||||
info.lastfm_playcount = artist_info.get('playcount', 0)
|
||||
info.tags = artist_info.get('tags', [])
|
||||
info.similar_artists = artist_info.get('similar_artists', [])
|
||||
|
||||
info.save()
|
||||
|
||||
logger.info(f"Updated artist info for channel {channel_id}")
|
||||
|
||||
# Also fetch artist artwork
|
||||
fetch_artist_artwork.delay(channel_id)
|
||||
|
||||
except Channel.DoesNotExist:
|
||||
logger.error(f"Channel {channel_id} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching artist info for channel {channel_id}: {e}")
|
||||
raise self.retry(exc=e, countdown=300)
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def fetch_artist_artwork(self, channel_id: int):
|
||||
"""
|
||||
Fetch artist artwork from Last.fm and Fanart.tv
|
||||
|
||||
Args:
|
||||
channel_id: Channel ID
|
||||
"""
|
||||
try:
|
||||
channel = Channel.objects.get(id=channel_id)
|
||||
|
||||
# Try Last.fm first
|
||||
lastfm_client = LastFMClient()
|
||||
artist_name = channel.channel_name
|
||||
artist_info = lastfm_client.get_artist_info(artist_name)
|
||||
|
||||
if artist_info and 'images' in artist_info:
|
||||
# Save artist image from Last.fm
|
||||
for img in artist_info['images']:
|
||||
if img['size'] in ['large', 'extralarge', 'mega']:
|
||||
if not Artwork.objects.filter(
|
||||
channel=channel,
|
||||
source='lastfm',
|
||||
artwork_type='artist_image'
|
||||
).exists():
|
||||
artwork = Artwork.objects.create(
|
||||
channel=channel,
|
||||
artwork_type='artist_image',
|
||||
source='lastfm',
|
||||
url=img['url'],
|
||||
priority=20
|
||||
)
|
||||
download_artwork.delay(artwork.id)
|
||||
logger.info(f"Created Last.fm artist image for channel {channel_id}")
|
||||
break
|
||||
|
||||
# Try Fanart.tv if we have MusicBrainz ID
|
||||
try:
|
||||
info = ArtistInfo.objects.get(channel=channel)
|
||||
if info.lastfm_mbid:
|
||||
fanart_client = FanartClient()
|
||||
artist_images = fanart_client.get_artist_images(info.lastfm_mbid)
|
||||
|
||||
if artist_images:
|
||||
# Save artist thumbnail
|
||||
if artist_images['thumbnails'] and not Artwork.objects.filter(
|
||||
channel=channel,
|
||||
source='fanart',
|
||||
artwork_type='artist_image'
|
||||
).exists():
|
||||
img = artist_images['thumbnails'][0]
|
||||
artwork = Artwork.objects.create(
|
||||
channel=channel,
|
||||
artwork_type='artist_image',
|
||||
source='fanart',
|
||||
url=img['url'],
|
||||
priority=30
|
||||
)
|
||||
download_artwork.delay(artwork.id)
|
||||
|
||||
# Save artist banner
|
||||
if artist_images['banners'] and not Artwork.objects.filter(
|
||||
channel=channel,
|
||||
source='fanart',
|
||||
artwork_type='artist_banner'
|
||||
).exists():
|
||||
img = artist_images['banners'][0]
|
||||
artwork = Artwork.objects.create(
|
||||
channel=channel,
|
||||
artwork_type='artist_banner',
|
||||
source='fanart',
|
||||
url=img['url'],
|
||||
priority=30
|
||||
)
|
||||
download_artwork.delay(artwork.id)
|
||||
|
||||
# Save artist logo
|
||||
if (artist_images['logos_hd'] or artist_images['logos']) and not Artwork.objects.filter(
|
||||
channel=channel,
|
||||
source='fanart',
|
||||
artwork_type='artist_logo'
|
||||
).exists():
|
||||
img = artist_images['logos_hd'][0] if artist_images['logos_hd'] else artist_images['logos'][0]
|
||||
artwork = Artwork.objects.create(
|
||||
channel=channel,
|
||||
artwork_type='artist_logo',
|
||||
source='fanart',
|
||||
url=img['url'],
|
||||
priority=30
|
||||
)
|
||||
download_artwork.delay(artwork.id)
|
||||
|
||||
logger.info(f"Created Fanart.tv artwork for channel {channel_id}")
|
||||
except ArtistInfo.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Use YouTube thumbnail as fallback
|
||||
if channel.channel_thumb_url and not Artwork.objects.filter(channel=channel, source='youtube').exists():
|
||||
artwork = Artwork.objects.create(
|
||||
channel=channel,
|
||||
artwork_type='artist_image',
|
||||
source='youtube',
|
||||
url=channel.channel_thumb_url,
|
||||
priority=10
|
||||
)
|
||||
download_artwork.delay(artwork.id)
|
||||
logger.info(f"Created YouTube thumbnail for channel {channel_id}")
|
||||
|
||||
except Channel.DoesNotExist:
|
||||
logger.error(f"Channel {channel_id} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching artist artwork for channel {channel_id}: {e}")
|
||||
raise self.retry(exc=e, countdown=300)
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def download_artwork(self, artwork_id: int):
|
||||
"""
|
||||
Download artwork from URL and save locally
|
||||
|
||||
Args:
|
||||
artwork_id: Artwork ID
|
||||
"""
|
||||
try:
|
||||
artwork = Artwork.objects.get(id=artwork_id)
|
||||
|
||||
if not artwork.url:
|
||||
logger.warning(f"No URL for artwork {artwork_id}")
|
||||
return
|
||||
|
||||
# Download image
|
||||
response = requests.get(artwork.url, timeout=30, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
# Get file extension from content type
|
||||
content_type = response.headers.get('content-type', '')
|
||||
if 'jpeg' in content_type or 'jpg' in content_type:
|
||||
ext = 'jpg'
|
||||
elif 'png' in content_type:
|
||||
ext = 'png'
|
||||
elif 'webp' in content_type:
|
||||
ext = 'webp'
|
||||
else:
|
||||
ext = 'jpg' # Default
|
||||
|
||||
# Generate filename
|
||||
if artwork.audio:
|
||||
filename = f"audio_{artwork.audio.id}_{artwork.artwork_type}_{artwork.source}.{ext}"
|
||||
elif artwork.channel:
|
||||
filename = f"channel_{artwork.channel.id}_{artwork.artwork_type}_{artwork.source}.{ext}"
|
||||
else:
|
||||
filename = f"artwork_{artwork.id}.{ext}"
|
||||
|
||||
# Save to media directory
|
||||
from django.conf import settings
|
||||
artwork_dir = Path(settings.MEDIA_ROOT) / 'artwork'
|
||||
artwork_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
filepath = artwork_dir / filename
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
# Update artwork record
|
||||
artwork.local_path = str(filepath.relative_to(settings.MEDIA_ROOT))
|
||||
artwork.save()
|
||||
|
||||
logger.info(f"Downloaded artwork {artwork_id} to {filepath}")
|
||||
|
||||
# If it's audio cover, embed in file
|
||||
if artwork.audio and artwork.artwork_type in ['audio_cover', 'audio_thumbnail']:
|
||||
embed_artwork_in_audio.delay(artwork.audio.id, artwork_id)
|
||||
|
||||
except Artwork.DoesNotExist:
|
||||
logger.error(f"Artwork {artwork_id} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading artwork {artwork_id}: {e}")
|
||||
raise self.retry(exc=e, countdown=300)
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def embed_artwork_in_audio(self, audio_id: int, artwork_id: int = None):
|
||||
"""
|
||||
Embed artwork in audio file using ID3 tags
|
||||
|
||||
Args:
|
||||
audio_id: Audio ID
|
||||
artwork_id: Optional specific artwork ID to embed (uses best if not provided)
|
||||
"""
|
||||
try:
|
||||
audio = Audio.objects.get(id=audio_id)
|
||||
|
||||
if not audio.media_url:
|
||||
logger.warning(f"No media file for audio {audio_id}")
|
||||
return
|
||||
|
||||
# Get artwork
|
||||
if artwork_id:
|
||||
artwork = Artwork.objects.get(id=artwork_id)
|
||||
else:
|
||||
# Get best artwork (highest priority)
|
||||
artwork = Artwork.objects.filter(
|
||||
audio=audio,
|
||||
local_path__isnull=False
|
||||
).order_by('-priority', '-id').first()
|
||||
|
||||
if not artwork or not artwork.local_path:
|
||||
logger.warning(f"No local artwork found for audio {audio_id}")
|
||||
return
|
||||
|
||||
# Read image data
|
||||
from django.conf import settings
|
||||
image_path = Path(settings.MEDIA_ROOT) / artwork.local_path
|
||||
|
||||
if not image_path.exists():
|
||||
logger.error(f"Artwork file not found: {image_path}")
|
||||
return
|
||||
|
||||
with open(image_path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
|
||||
# Determine MIME type
|
||||
if image_path.suffix.lower() in ['.jpg', '.jpeg']:
|
||||
mime_type = 'image/jpeg'
|
||||
elif image_path.suffix.lower() == '.png':
|
||||
mime_type = 'image/png'
|
||||
else:
|
||||
mime_type = 'image/jpeg'
|
||||
|
||||
# Embed in audio file
|
||||
service = ID3TagService()
|
||||
audio_path = Path(settings.MEDIA_ROOT) / audio.media_url
|
||||
|
||||
if audio_path.exists():
|
||||
success = service.embed_cover_art(str(audio_path), image_data, mime_type)
|
||||
if success:
|
||||
logger.info(f"Embedded artwork in audio {audio_id}")
|
||||
else:
|
||||
logger.error(f"Failed to embed artwork in audio {audio_id}")
|
||||
else:
|
||||
logger.error(f"Audio file not found: {audio_path}")
|
||||
|
||||
except Audio.DoesNotExist:
|
||||
logger.error(f"Audio {audio_id} not found")
|
||||
except Artwork.DoesNotExist:
|
||||
logger.error(f"Artwork {artwork_id} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error embedding artwork for audio {audio_id}: {e}")
|
||||
|
||||
|
||||
@shared_task
|
||||
def auto_fetch_artwork_batch(limit: int = 50):
|
||||
"""
|
||||
Auto-fetch artwork for audio without artwork
|
||||
|
||||
Args:
|
||||
limit: Maximum number of audio to process
|
||||
"""
|
||||
# Find audio without artwork
|
||||
audio_without_artwork = Audio.objects.filter(
|
||||
~Q(artwork__isnull=False)
|
||||
)[:limit]
|
||||
|
||||
count = 0
|
||||
for audio in audio_without_artwork:
|
||||
fetch_metadata_for_audio.delay(audio.id)
|
||||
count += 1
|
||||
|
||||
logger.info(f"Queued artwork fetch for {count} audio tracks")
|
||||
|
||||
|
||||
@shared_task
|
||||
def auto_fetch_artist_info_batch(limit: int = 20):
|
||||
"""
|
||||
Auto-fetch artist info for channels without info
|
||||
|
||||
Args:
|
||||
limit: Maximum number of channels to process
|
||||
"""
|
||||
# Find channels without artist info
|
||||
channels_without_info = Channel.objects.filter(
|
||||
~Q(artistinfo__isnull=False)
|
||||
)[:limit]
|
||||
|
||||
count = 0
|
||||
for channel in channels_without_info:
|
||||
fetch_artist_info.delay(channel.id)
|
||||
count += 1
|
||||
|
||||
logger.info(f"Queued artist info fetch for {count} channels")
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_id3_tags_from_metadata(audio_id: int):
|
||||
"""
|
||||
Update ID3 tags in audio file from metadata
|
||||
|
||||
Args:
|
||||
audio_id: Audio ID
|
||||
"""
|
||||
try:
|
||||
audio = Audio.objects.get(id=audio_id)
|
||||
|
||||
if not audio.media_url:
|
||||
logger.warning(f"No media file for audio {audio_id}")
|
||||
return
|
||||
|
||||
from django.conf import settings
|
||||
audio_path = Path(settings.MEDIA_ROOT) / audio.media_url
|
||||
|
||||
if not audio_path.exists():
|
||||
logger.error(f"Audio file not found: {audio_path}")
|
||||
return
|
||||
|
||||
# Prepare tags
|
||||
tags = {
|
||||
'title': audio.audio_title,
|
||||
'artist': audio.channel.channel_name if audio.channel else 'Unknown Artist',
|
||||
}
|
||||
|
||||
# Add metadata if available
|
||||
try:
|
||||
metadata = MusicMetadata.objects.get(audio=audio)
|
||||
if metadata.album_name:
|
||||
tags['album'] = metadata.album_name
|
||||
if metadata.album_artist:
|
||||
tags['album_artist'] = metadata.album_artist
|
||||
if metadata.release_year:
|
||||
tags['year'] = str(metadata.release_year)
|
||||
if metadata.genre:
|
||||
tags['genre'] = metadata.genre
|
||||
if metadata.track_number:
|
||||
tags['track_number'] = metadata.track_number
|
||||
if metadata.disc_number:
|
||||
tags['disc_number'] = metadata.disc_number
|
||||
except MusicMetadata.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Write tags
|
||||
service = ID3TagService()
|
||||
success = service.write_tags(str(audio_path), tags)
|
||||
|
||||
if success:
|
||||
logger.info(f"Updated ID3 tags for audio {audio_id}")
|
||||
else:
|
||||
logger.error(f"Failed to update ID3 tags for audio {audio_id}")
|
||||
|
||||
except Audio.DoesNotExist:
|
||||
logger.error(f"Audio {audio_id} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating ID3 tags for audio {audio_id}: {e}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue