diff --git a/apps/m3u/api_urls.py b/apps/m3u/api_urls.py index c676b333..04528f94 100644 --- a/apps/m3u/api_urls.py +++ b/apps/m3u/api_urls.py @@ -1,6 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .api_views import M3UAccountViewSet, M3UFilterViewSet, ServerGroupViewSet, RefreshM3UAPIView, RefreshSingleM3UAPIView, UserAgentViewSet +from .api_views import M3UAccountViewSet, M3UFilterViewSet, ServerGroupViewSet, RefreshM3UAPIView, RefreshSingleM3UAPIView, UserAgentViewSet, M3UAccountProfileViewSet app_name = 'm3u' @@ -8,10 +8,13 @@ router = DefaultRouter() router.register(r'accounts', M3UAccountViewSet, basename='m3u-account') router.register(r'filters', M3UFilterViewSet, basename='m3u-filter') router.register(r'server-groups', ServerGroupViewSet, basename='server-group') +router.register(r'profiles', M3UAccountViewSet, basename='m3u-account-profiles') urlpatterns = [ path('refresh/', RefreshM3UAPIView.as_view(), name='m3u_refresh'), path('refresh//', RefreshSingleM3UAPIView.as_view(), name='m3u_refresh_single'), + path('accounts//profiles/', M3UAccountProfileViewSet.as_view({'get': 'list', 'post': 'create'})), + path('accounts//profiles//', M3UAccountProfileViewSet.as_view({'put': 'update', 'delete': 'destroy'})), ] urlpatterns += router.urls diff --git a/apps/m3u/api_views.py b/apps/m3u/api_views.py index 2f6c6d9b..35d558db 100644 --- a/apps/m3u/api_views.py +++ b/apps/m3u/api_views.py @@ -9,7 +9,7 @@ from django.http import JsonResponse from django.core.cache import cache # Import all models, including UserAgent. -from .models import M3UAccount, M3UFilter, ServerGroup +from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile from core.models import UserAgent from core.serializers import UserAgentSerializer # Import all serializers, including the UserAgentSerializer. @@ -17,6 +17,7 @@ from .serializers import ( M3UAccountSerializer, M3UFilterSerializer, ServerGroupSerializer, + M3UAccountProfileSerializer, ) from .tasks import refresh_single_m3u_account, refresh_m3u_accounts @@ -68,3 +69,16 @@ class UserAgentViewSet(viewsets.ModelViewSet): serializer_class = UserAgentSerializer permission_classes = [IsAuthenticated] +class M3UAccountProfileViewSet(viewsets.ModelViewSet): + queryset = M3UAccountProfile.objects.all() + serializer_class = M3UAccountProfileSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + account_id = self.kwargs['account_id'] + return M3UAccountProfile.objects.filter(account_id=account_id) + + def perform_create(self, serializer): + account_id = self.kwargs['account_id'] + account = M3UAccount.objects.get(id=account_id) + serializer.save(account=account) diff --git a/apps/m3u/migrations/0001_initial.py b/apps/m3u/migrations/0001_initial.py new file mode 100644 index 00000000..92a79a54 --- /dev/null +++ b/apps/m3u/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# Generated by Django 5.1.6 on 2025-02-28 01:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ServerGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Unique name for this server group.', max_length=100, unique=True)), + ], + ), + migrations.CreateModel( + name='M3UAccount', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Unique name for this M3U account', max_length=255, unique=True)), + ('server_url', models.URLField(blank=True, help_text='The base URL of the M3U server (optional if a file is uploaded)', null=True)), + ('uploaded_file', models.FileField(blank=True, null=True, upload_to='m3u_uploads/')), + ('max_streams', models.PositiveIntegerField(default=0, help_text='Maximum number of concurrent streams (0 for unlimited)')), + ('is_active', models.BooleanField(default=True, help_text='Set to false to deactivate this M3U account')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Time when this account was created')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Time when this account was last updated')), + ('user_agent', models.ForeignKey(blank=True, help_text='The User-Agent associated with this M3U account.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m3u_accounts', to='core.useragent')), + ('server_group', models.ForeignKey(blank=True, help_text='The server group this M3U account belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m3u_accounts', to='m3u.servergroup')), + ], + ), + migrations.CreateModel( + name='M3UFilter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('filter_type', models.CharField(choices=[('group', 'Group Title'), ('name', 'Stream Name')], default='group', help_text='Filter based on either group title or stream name.', max_length=50)), + ('regex_pattern', models.CharField(help_text='A regex pattern to match streams or groups.', max_length=200)), + ('exclude', models.BooleanField(default=True, help_text='If True, matching items are excluded; if False, only matches are included.')), + ('m3u_account', models.ForeignKey(help_text='The M3U account this filter is applied to.', on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='m3u.m3uaccount')), + ], + ), + migrations.CreateModel( + name='M3UAccountProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Name for the M3U account profile', max_length=255)), + ('max_streams', models.PositiveIntegerField(default=0, help_text='Maximum number of concurrent streams (0 for unlimited)')), + ('search_pattern', models.CharField(max_length=255)), + ('replace_pattern', models.CharField(max_length=255)), + ('m3u_account_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='m3u.m3uaccount')), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('m3u_account_id', 'name'), name='unique_account_name')], + }, + ), + ] diff --git a/apps/m3u/models.py b/apps/m3u/models.py index 2d9a2d24..28c1b39b 100644 --- a/apps/m3u/models.py +++ b/apps/m3u/models.py @@ -134,7 +134,7 @@ class M3UFilter(models.Model): # If no include filters exist, assume all non-excluded streams are valid if not any(not f.exclude for f in filters): return streams.exclude(id__in=[s.id for s in excluded_streams]) - + return streams.filter(id__in=[s.id for s in included_streams]) @@ -148,3 +148,27 @@ class ServerGroup(models.Model): def __str__(self): return self.name + +class M3UAccountProfile(models.Model): + name = models.CharField( + max_length=255, + help_text="Name for the M3U account profile" + ) + m3u_account_id = models.ForeignKey(M3UAccount, + on_delete=models.CASCADE + ) + max_streams = models.PositiveIntegerField( + default=0, + help_text="Maximum number of concurrent streams (0 for unlimited)" + ) + search_pattern = models.CharField( + max_length=255, + ) + replace_pattern = models.CharField( + max_length=255, + ) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['m3u_account_id', 'name'], name='unique_account_name') + ] diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py index 8f20a329..e15d8499 100644 --- a/apps/m3u/serializers.py +++ b/apps/m3u/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import M3UAccount, M3UFilter, ServerGroup +from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile from core.models import UserAgent class M3UFilterSerializer(serializers.ModelSerializer): @@ -9,6 +9,12 @@ class M3UFilterSerializer(serializers.ModelSerializer): model = M3UFilter fields = ['id', 'filter_type', 'regex_pattern', 'exclude'] +class M3UAccountProfileSerializer(serializers.ModelSerializer): + """Serializer for M3U Account Profiles""" + + class Meta: + model = M3UAccountProfile + fields = ['id', 'name', 'm3u_account_id', 'search_pattern', 'replace_pattern'] class M3UAccountSerializer(serializers.ModelSerializer): """Serializer for M3U Account""" @@ -18,20 +24,18 @@ class M3UAccountSerializer(serializers.ModelSerializer): queryset=UserAgent.objects.all(), required=True ) + profiles = M3UAccountProfileSerializer(many=True, read_only=True) class Meta: model = M3UAccount fields = [ 'id', 'name', 'server_url', 'uploaded_file', 'server_group', - 'max_streams', 'is_active', 'created_at', 'updated_at', 'filters', 'user_agent' + 'max_streams', 'is_active', 'created_at', 'updated_at', 'filters', 'user_agent', 'profiles' ] - class ServerGroupSerializer(serializers.ModelSerializer): """Serializer for Server Group""" class Meta: model = ServerGroup fields = ['id', 'name'] - -