Initial commit - SoundWave v1.0

- Full PWA support with offline capabilities
- Comprehensive search across songs, playlists, and channels
- Offline playlist manager with download tracking
- Pre-built frontend for zero-build deployment
- Docker-based deployment with docker compose
- Material-UI dark theme interface
- YouTube audio download and management
- Multi-user authentication support
This commit is contained in:
Iulian 2025-12-16 23:43:07 +00:00
commit 51679d1943
254 changed files with 37281 additions and 0 deletions

View file

12
backend/channel/admin.py Normal file
View file

@ -0,0 +1,12 @@
"""Channel admin"""
from django.contrib import admin
from channel.models import Channel
@admin.register(Channel)
class ChannelAdmin(admin.ModelAdmin):
"""Channel admin"""
list_display = ('channel_name', 'subscribed', 'video_count', 'subscriber_count', 'last_refreshed')
list_filter = ('subscribed', 'last_refreshed')
search_fields = ('channel_name', 'channel_id')

View file

71
backend/channel/models.py Normal file
View file

@ -0,0 +1,71 @@
"""Channel models"""
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Channel(models.Model):
"""YouTube channel model"""
# User isolation
owner = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='channels',
help_text="User who owns this channel subscription"
)
youtube_account = models.ForeignKey(
'user.UserYouTubeAccount',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='channels',
help_text="YouTube account used to subscribe to this channel"
)
channel_id = models.CharField(max_length=50, db_index=True)
channel_name = models.CharField(max_length=200)
channel_description = models.TextField(blank=True)
channel_thumbnail = models.URLField(max_length=500, blank=True)
subscribed = models.BooleanField(default=True)
subscriber_count = models.IntegerField(default=0)
video_count = models.IntegerField(default=0)
last_refreshed = models.DateTimeField(auto_now=True)
created_date = models.DateTimeField(auto_now_add=True)
# Status tracking
active = models.BooleanField(default=True, help_text="Channel is active and available")
sync_status = models.CharField(
max_length=20,
choices=[
('pending', 'Pending'),
('syncing', 'Syncing'),
('success', 'Success'),
('failed', 'Failed'),
('stale', 'Stale'),
],
default='pending',
help_text="Current sync status"
)
error_message = models.TextField(blank=True, help_text="Last error message if sync failed")
downloaded_count = models.IntegerField(default=0, help_text="Downloaded videos count")
# Download settings per channel
auto_download = models.BooleanField(default=True, help_text="Auto-download new videos from this channel")
download_quality = models.CharField(
max_length=20,
default='auto',
choices=[('auto', 'Auto'), ('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('ultra', 'Ultra')]
)
class Meta:
ordering = ['channel_name']
unique_together = ('owner', 'channel_id') # Each user can subscribe once per channel
indexes = [
models.Index(fields=['owner', 'channel_id']),
models.Index(fields=['owner', 'subscribed']),
]
def __str__(self):
return f"{self.owner.username} - {self.channel_name}"

View file

@ -0,0 +1,54 @@
"""Channel serializers"""
from rest_framework import serializers
from channel.models import Channel
import re
class ChannelSubscribeSerializer(serializers.Serializer):
"""Channel subscription from URL"""
url = serializers.URLField(required=True, help_text="YouTube channel URL")
def validate_url(self, value):
"""Extract channel ID from URL"""
# Match various YouTube channel URL patterns
patterns = [
r'youtube\.com/channel/(UC[\w-]+)',
r'youtube\.com/@([\w-]+)',
r'youtube\.com/c/([\w-]+)',
r'youtube\.com/user/([\w-]+)',
]
for pattern in patterns:
match = re.search(pattern, value)
if match:
return match.group(1)
# If it's just a channel ID
if value.startswith('UC') and len(value) == 24:
return value
raise serializers.ValidationError("Invalid YouTube channel URL")
class ChannelSerializer(serializers.ModelSerializer):
"""Channel serializer"""
status_display = serializers.CharField(source='get_sync_status_display', read_only=True)
progress_percent = serializers.SerializerMethodField()
class Meta:
model = Channel
fields = '__all__'
read_only_fields = ['created_date', 'last_refreshed']
def get_progress_percent(self, obj):
"""Calculate download progress percentage"""
if obj.video_count == 0:
return 0
return int((obj.downloaded_count / obj.video_count) * 100)
class ChannelListSerializer(serializers.Serializer):
"""Channel list response"""
data = ChannelSerializer(many=True)
paginate = serializers.BooleanField(default=True)

9
backend/channel/urls.py Normal file
View file

@ -0,0 +1,9 @@
"""Channel URL patterns"""
from django.urls import path
from channel.views import ChannelListView, ChannelDetailView
urlpatterns = [
path('', ChannelListView.as_view(), name='channel-list'),
path('<str:channel_id>/', ChannelDetailView.as_view(), name='channel-detail'),
]

65
backend/channel/views.py Normal file
View file

@ -0,0 +1,65 @@
"""Channel API views"""
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.response import Response
from channel.models import Channel
from channel.serializers import ChannelSerializer
from common.views import ApiBaseView, AdminWriteOnly
class ChannelListView(ApiBaseView):
"""Channel list endpoint"""
permission_classes = [AdminWriteOnly]
def get(self, request):
"""Get channel list"""
channels = Channel.objects.filter(owner=request.user, subscribed=True)
serializer = ChannelSerializer(channels, many=True)
return Response({'data': serializer.data, 'paginate': True})
def post(self, request):
"""Subscribe to channel - TubeArchivist pattern with Celery task"""
from channel.serializers import ChannelSubscribeSerializer
# Check channel quota
if not request.user.can_add_channel:
return Response(
{'error': f'Channel limit reached. Maximum {request.user.max_channels} channels allowed.'},
status=status.HTTP_403_FORBIDDEN
)
# Validate URL
url_serializer = ChannelSubscribeSerializer(data=request.data)
url_serializer.is_valid(raise_exception=True)
channel_url = request.data['url']
# Trigger async Celery task (TubeArchivist pattern)
from task.tasks import subscribe_to_channel
task = subscribe_to_channel.delay(request.user.id, channel_url)
return Response(
{
'message': 'Channel subscription task started',
'task_id': str(task.id)
},
status=status.HTTP_202_ACCEPTED
)
class ChannelDetailView(ApiBaseView):
"""Channel detail endpoint"""
permission_classes = [AdminWriteOnly]
def get(self, request, channel_id):
"""Get channel details"""
channel = get_object_or_404(Channel, channel_id=channel_id, owner=request.user)
serializer = ChannelSerializer(channel)
return Response(serializer.data)
def delete(self, request, channel_id):
"""Unsubscribe from channel"""
channel = get_object_or_404(Channel, channel_id=channel_id, owner=request.user)
channel.subscribed = False
channel.save()
return Response(status=status.HTTP_204_NO_CONTENT)