diff --git a/apps/m3u/admin.py b/apps/m3u/admin.py
index 5ddce119..dd5986eb 100644
--- a/apps/m3u/admin.py
+++ b/apps/m3u/admin.py
@@ -18,6 +18,7 @@ class M3UAccountAdmin(admin.ModelAdmin):
"server_url",
"server_group",
"max_streams",
+ "priority",
"is_active",
"user_agent_display",
"uploaded_file_link",
diff --git a/apps/m3u/migrations/0016_m3uaccount_priority.py b/apps/m3u/migrations/0016_m3uaccount_priority.py
new file mode 100644
index 00000000..55e0e95b
--- /dev/null
+++ b/apps/m3u/migrations/0016_m3uaccount_priority.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2.4 on 2025-08-20 22:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('m3u', '0015_alter_m3ufilter_options_m3ufilter_custom_properties'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='m3uaccount',
+ name='priority',
+ field=models.PositiveIntegerField(default=0, help_text='Priority for VOD provider selection (higher numbers = higher priority). Used when multiple providers offer the same content.'),
+ ),
+ ]
diff --git a/apps/m3u/models.py b/apps/m3u/models.py
index 2a7846c6..cfcc3646 100644
--- a/apps/m3u/models.py
+++ b/apps/m3u/models.py
@@ -94,6 +94,10 @@ class M3UAccount(models.Model):
default=7,
help_text="Number of days after which a stream will be removed if not seen in the M3U source.",
)
+ priority = models.PositiveIntegerField(
+ default=0,
+ help_text="Priority for VOD provider selection (higher numbers = higher priority). Used when multiple providers offer the same content.",
+ )
def __str__(self):
return self.name
diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py
index b963db7b..1a62080b 100644
--- a/apps/m3u/serializers.py
+++ b/apps/m3u/serializers.py
@@ -1,5 +1,5 @@
from core.utils import validate_flexible_url
-from rest_framework import serializers
+from rest_framework import serializers, status
from rest_framework.response import Response
from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile
from core.models import UserAgent
@@ -113,6 +113,7 @@ class M3UAccountSerializer(serializers.ModelSerializer):
"username",
"password",
"stale_stream_days",
+ "priority",
"status",
"last_message",
"enable_vod",
diff --git a/apps/output/views.py b/apps/output/views.py
index bb54d3c3..20d3c206 100644
--- a/apps/output/views.py
+++ b/apps/output/views.py
@@ -1077,10 +1077,10 @@ def xc_get_vod_streams(request, user, category_id=None):
movies = Movie.objects.filter(**filters).select_related('logo').distinct()
for movie in movies:
- # Get the first relation for this movie (for metadata like container_extension)
+ # Get the highest priority relation for this movie (for metadata like container_extension)
relation = movie.m3u_relations.filter(
m3u_account__is_active=True
- ).first()
+ ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first()
if relation:
relation_custom = relation.custom_properties or {}
@@ -1282,9 +1282,11 @@ def xc_get_series_info(request, user, series_id):
if season_num not in seasons:
seasons[season_num] = []
- # Try to get the first related M3UEpisodeRelation for this episode (for video/audio/bitrate)
+ # Try to get the highest priority related M3UEpisodeRelation for this episode (for video/audio/bitrate)
from apps.vod.models import M3UEpisodeRelation
- first_relation = M3UEpisodeRelation.objects.filter(episode=episode).order_by('id').first()
+ first_relation = M3UEpisodeRelation.objects.filter(
+ episode=episode
+ ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first()
video = audio = bitrate = None
if first_relation and first_relation.custom_properties:
info = first_relation.custom_properties.get('info')
diff --git a/apps/proxy/vod_proxy/views.py b/apps/proxy/vod_proxy/views.py
index 8a5abd88..00ab72ea 100644
--- a/apps/proxy/vod_proxy/views.py
+++ b/apps/proxy/vod_proxy/views.py
@@ -345,16 +345,28 @@ class VODStreamView(View):
content_obj = get_object_or_404(Movie, uuid=content_id)
logger.info(f"[CONTENT-FOUND] Movie: {content_obj.name} (ID: {content_obj.id})")
- # Get the first active relation
- relation = content_obj.m3u_relations.filter(m3u_account__is_active=True).first()
+ # Get the highest priority active relation
+ relation = content_obj.m3u_relations.filter(
+ m3u_account__is_active=True
+ ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first()
+
+ if relation:
+ logger.info(f"[PROVIDER-SELECTED] Using provider: {relation.m3u_account.name} (priority: {relation.m3u_account.priority})")
+
return content_obj, relation
elif content_type == 'episode':
content_obj = get_object_or_404(Episode, uuid=content_id)
logger.info(f"[CONTENT-FOUND] Episode: {content_obj.name} (ID: {content_obj.id}, Series: {content_obj.series.name})")
- # Get the first active relation
- relation = content_obj.m3u_relations.filter(m3u_account__is_active=True).first()
+ # Get the highest priority active relation
+ relation = content_obj.m3u_relations.filter(
+ m3u_account__is_active=True
+ ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first()
+
+ if relation:
+ logger.info(f"[PROVIDER-SELECTED] Using provider: {relation.m3u_account.name} (priority: {relation.m3u_account.priority})")
+
return content_obj, relation
elif content_type == 'series':
@@ -367,7 +379,14 @@ class VODStreamView(View):
return None, None
logger.info(f"[CONTENT-FOUND] First episode: {episode.name} (ID: {episode.id})")
- relation = episode.m3u_relations.filter(m3u_account__is_active=True).first()
+ # Get the highest priority active relation
+ relation = episode.m3u_relations.filter(
+ m3u_account__is_active=True
+ ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first()
+
+ if relation:
+ logger.info(f"[PROVIDER-SELECTED] Using provider: {relation.m3u_account.name} (priority: {relation.m3u_account.priority})")
+
return episode, relation
else:
logger.error(f"[CONTENT-ERROR] Invalid content type: {content_type}")
diff --git a/apps/vod/api_views.py b/apps/vod/api_views.py
index d93dc6cf..df686c9c 100644
--- a/apps/vod/api_views.py
+++ b/apps/vod/api_views.py
@@ -108,11 +108,11 @@ class MovieViewSet(viewsets.ReadOnlyModelViewSet):
"""Get detailed movie information from the original provider, throttled to 24h."""
movie = self.get_object()
- # Get the first active relation
+ # Get the highest priority active relation
relation = M3UMovieRelation.objects.filter(
movie=movie,
m3u_account__is_active=True
- ).select_related('m3u_account').first()
+ ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first()
if not relation:
return Response(
@@ -314,11 +314,11 @@ class SeriesViewSet(viewsets.ReadOnlyModelViewSet):
series = self.get_object()
logger.debug(f"Retrieved series: {series.name} (ID: {series.id})")
- # Get the first active relation
+ # Get the highest priority active relation
relation = M3USeriesRelation.objects.filter(
series=series,
m3u_account__is_active=True
- ).select_related('m3u_account').first()
+ ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first()
if not relation:
return Response(
diff --git a/frontend/src/components/forms/M3U.jsx b/frontend/src/components/forms/M3U.jsx
index a317a7fe..ff25ed92 100644
--- a/frontend/src/components/forms/M3U.jsx
+++ b/frontend/src/components/forms/M3U.jsx
@@ -64,6 +64,7 @@ const M3U = ({
username: '',
password: '',
stale_stream_days: 7,
+ priority: 0,
enable_vod: false,
},
@@ -93,6 +94,9 @@ const M3U = ({
m3uAccount.stale_stream_days !== null
? m3uAccount.stale_stream_days
: 7,
+ priority: m3uAccount.priority !== undefined && m3uAccount.priority !== null
+ ? m3uAccount.priority
+ : 0,
enable_vod: m3uAccount.enable_vod || false,
});
@@ -366,6 +370,15 @@ const M3U = ({
{...form.getInputProps('stale_stream_days')}
/>
+
+