mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-28 12:36:42 +00:00
Separate Movies, Series and Episodes into their own tables
This commit is contained in:
parent
0b5906538e
commit
0585fc1dfe
11 changed files with 344 additions and 203 deletions
|
|
@ -1042,12 +1042,12 @@ def xc_get_vod_categories(user):
|
|||
|
||||
def xc_get_vod_streams(request, user, category_id=None):
|
||||
"""Get VOD streams (movies) for XtreamCodes API"""
|
||||
from apps.vod.models import VOD
|
||||
from apps.vod.models import Movie
|
||||
|
||||
streams = []
|
||||
|
||||
# Build filters based on user access
|
||||
filters = {"type": "movie", "m3u_account__is_active": True}
|
||||
filters = {"m3u_account__is_active": True}
|
||||
|
||||
if user.user_level == 0:
|
||||
# For regular users, filter by accessible M3U accounts
|
||||
|
|
@ -1065,28 +1065,28 @@ def xc_get_vod_streams(request, user, category_id=None):
|
|||
if category_id:
|
||||
filters["category_id"] = category_id
|
||||
|
||||
vods = VOD.objects.filter(**filters).select_related('category', 'logo', 'm3u_account')
|
||||
movies = Movie.objects.filter(**filters).select_related('category', 'logo', 'm3u_account')
|
||||
|
||||
for vod in vods:
|
||||
for movie in movies:
|
||||
streams.append({
|
||||
"num": vod.id,
|
||||
"name": vod.name,
|
||||
"num": movie.id,
|
||||
"name": movie.name,
|
||||
"stream_type": "movie",
|
||||
"stream_id": vod.id,
|
||||
"stream_id": movie.id,
|
||||
"stream_icon": (
|
||||
None if not vod.logo
|
||||
None if not movie.logo
|
||||
else request.build_absolute_uri(
|
||||
reverse("api:channels:logo-cache", args=[vod.logo.id])
|
||||
reverse("api:channels:logo-cache", args=[movie.logo.id])
|
||||
)
|
||||
),
|
||||
"rating": vod.rating or "0",
|
||||
"rating_5based": float(vod.rating or 0) / 2 if vod.rating else 0,
|
||||
"rating": movie.rating or "0",
|
||||
"rating_5based": float(movie.rating or 0) / 2 if movie.rating else 0,
|
||||
"added": int(time.time()), # TODO: use actual created date
|
||||
"is_adult": 0,
|
||||
"category_id": str(vod.category.id) if vod.category else "0",
|
||||
"container_extension": vod.container_extension or "mp4",
|
||||
"category_id": str(movie.category.id) if movie.category else "0",
|
||||
"container_extension": movie.container_extension or "mp4",
|
||||
"custom_sid": None,
|
||||
"direct_source": vod.url,
|
||||
"direct_source": movie.url,
|
||||
})
|
||||
|
||||
return streams
|
||||
|
|
@ -1186,7 +1186,7 @@ def xc_get_series(request, user, category_id=None):
|
|||
|
||||
def xc_get_series_info(request, user, series_id):
|
||||
"""Get detailed series information including episodes"""
|
||||
from apps.vod.models import Series, VOD
|
||||
from apps.vod.models import Series, Episode
|
||||
|
||||
if not series_id:
|
||||
raise Http404()
|
||||
|
|
@ -1212,9 +1212,8 @@ def xc_get_series_info(request, user, series_id):
|
|||
raise Http404()
|
||||
|
||||
# Get episodes grouped by season
|
||||
episodes = VOD.objects.filter(
|
||||
series=serie,
|
||||
type="episode"
|
||||
episodes = Episode.objects.filter(
|
||||
series=serie
|
||||
).order_by('season_number', 'episode_number')
|
||||
|
||||
# Group episodes by season
|
||||
|
|
@ -1285,13 +1284,13 @@ def xc_get_series_info(request, user, series_id):
|
|||
|
||||
def xc_get_vod_info(request, user, vod_id):
|
||||
"""Get detailed VOD (movie) information"""
|
||||
from apps.vod.models import VOD
|
||||
from apps.vod.models import Movie
|
||||
|
||||
if not vod_id:
|
||||
raise Http404()
|
||||
|
||||
# Get VOD with user access filtering
|
||||
filters = {"id": vod_id, "type": "movie", "m3u_account__is_active": True}
|
||||
# Get Movie with user access filtering
|
||||
filters = {"id": vod_id, "m3u_account__is_active": True}
|
||||
|
||||
if user.user_level == 0:
|
||||
if user.channel_profiles.count() > 0:
|
||||
|
|
@ -1306,55 +1305,56 @@ def xc_get_vod_info(request, user, vod_id):
|
|||
raise Http404()
|
||||
|
||||
try:
|
||||
vod = VOD.objects.get(**filters)
|
||||
except VOD.DoesNotExist:
|
||||
movie = Movie.objects.get(**filters)
|
||||
except Movie.DoesNotExist:
|
||||
raise Http404()
|
||||
|
||||
info = {
|
||||
"info": {
|
||||
"tmdb_id": vod.tmdb_id or "",
|
||||
"name": vod.name,
|
||||
"o_name": vod.name,
|
||||
"tmdb_id": movie.tmdb_id or "",
|
||||
"name": movie.name,
|
||||
"o_name": movie.name,
|
||||
"cover_big": (
|
||||
None if not vod.logo
|
||||
None if not movie.logo
|
||||
else request.build_absolute_uri(
|
||||
reverse("api:channels:logo-cache", args=[vod.logo.id])
|
||||
reverse("api:channels:logo-cache", args=[movie.logo.id])
|
||||
)
|
||||
),
|
||||
"movie_image": (
|
||||
None if not vod.logo
|
||||
None if not movie.logo
|
||||
else request.build_absolute_uri(
|
||||
reverse("api:channels:logo-cache", args=[vod.logo.id])
|
||||
reverse("api:channels:logo-cache", args=[movie.logo.id])
|
||||
)
|
||||
),
|
||||
"releasedate": f"{vod.year}-01-01" if vod.year else "",
|
||||
"episode_run_time": (vod.duration or 0) * 60,
|
||||
"releasedate": f"{movie.year}-01-01" if movie.year else "",
|
||||
"episode_run_time": (movie.duration or 0) * 60,
|
||||
"youtube_trailer": "",
|
||||
"director": "",
|
||||
"actors": "",
|
||||
"cast": "",
|
||||
"description": vod.description or "",
|
||||
"plot": vod.description or "",
|
||||
"description": movie.description or "",
|
||||
"plot": movie.description or "",
|
||||
"age": "",
|
||||
"country": "",
|
||||
"genre": vod.genre or "",
|
||||
"genre": movie.genre or "",
|
||||
"backdrop_path": [],
|
||||
"duration_secs": (vod.duration or 0) * 60,
|
||||
"duration": f"{vod.duration or 0} min",
|
||||
"duration_secs": (movie.duration or 0) * 60,
|
||||
"duration": f"{movie.duration or 0} min",
|
||||
"video": {},
|
||||
"audio": {},
|
||||
"bitrate": 0,
|
||||
"rating": float(vod.rating or 0),
|
||||
"rating": float(movie.rating or 0),
|
||||
},
|
||||
"movie_data": {
|
||||
"stream_id": vod.id,
|
||||
"name": vod.name,
|
||||
"stream_id": movie.id,
|
||||
"name": movie.name,
|
||||
"added": int(time.time()),
|
||||
"category_id": str(vod.category.id) if vod.category else "0",
|
||||
"container_extension": vod.container_extension or "mp4",
|
||||
"category_id": str(movie.category.id) if movie.category else "0",
|
||||
"container_extension": movie.container_extension or "mp4",
|
||||
"custom_sid": "",
|
||||
"direct_source": vod.url,
|
||||
"direct_source": movie.url,
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
return info
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ from . import views
|
|||
app_name = 'vod_proxy'
|
||||
|
||||
urlpatterns = [
|
||||
path('stream/<uuid:vod_uuid>', views.stream_vod, name='stream_vod'),
|
||||
path('stream/<uuid:vod_uuid>/position', views.update_position, name='update_position'),
|
||||
# Movie streaming
|
||||
path('movie/<uuid:movie_uuid>', views.stream_movie, name='stream_movie'),
|
||||
path('movie/<uuid:movie_uuid>/position', views.update_movie_position, name='update_movie_position'),
|
||||
|
||||
# Episode streaming
|
||||
path('episode/<uuid:episode_uuid>', views.stream_episode, name='stream_episode'),
|
||||
path('episode/<uuid:episode_uuid>/position', views.update_episode_position, name='update_episode_position'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import requests
|
|||
from django.http import StreamingHttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.decorators import api_view
|
||||
|
||||
from apps.vod.models import VOD, VODConnection
|
||||
|
||||
from apps.vod.models import Movie, Episode, VODConnection
|
||||
from apps.m3u.models import M3UAccountProfile
|
||||
from dispatcharr.utils import network_access_allowed, get_client_ip
|
||||
from core.models import UserAgent, CoreSettings
|
||||
|
|
@ -18,25 +18,37 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
@csrf_exempt
|
||||
@api_view(["GET"])
|
||||
def stream_vod(request, vod_uuid):
|
||||
"""Stream VOD content with connection tracking and range support"""
|
||||
def stream_movie(request, movie_uuid):
|
||||
"""Stream movie content with connection tracking and range support"""
|
||||
return _stream_content(request, Movie, movie_uuid, "movie")
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(["GET"])
|
||||
def stream_episode(request, episode_uuid):
|
||||
"""Stream episode content with connection tracking and range support"""
|
||||
return _stream_content(request, Episode, episode_uuid, "episode")
|
||||
|
||||
|
||||
def _stream_content(request, model_class, content_uuid, content_type_name):
|
||||
"""Generic function to stream VOD content"""
|
||||
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
# Get VOD object
|
||||
vod = get_object_or_404(VOD, uuid=vod_uuid)
|
||||
# Get content object
|
||||
content = get_object_or_404(model_class, uuid=content_uuid)
|
||||
|
||||
# Generate client ID and get client info
|
||||
client_id = f"vod_client_{int(time.time() * 1000)}_{random.randint(1000, 9999)}"
|
||||
client_ip = get_client_ip(request)
|
||||
client_user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
|
||||
logger.info(f"[{client_id}] VOD stream request for: {vod.name}")
|
||||
logger.info(f"[{client_id}] VOD stream request for: {content.name}")
|
||||
|
||||
try:
|
||||
# Get available M3U profile for connection management
|
||||
m3u_account = vod.m3u_account
|
||||
m3u_account = content.m3u_account
|
||||
available_profile = None
|
||||
|
||||
for profile in m3u_account.profiles.filter(is_active=True):
|
||||
|
|
@ -51,9 +63,11 @@ def stream_vod(request, vod_uuid):
|
|||
status=503
|
||||
)
|
||||
|
||||
# Create connection tracking record
|
||||
# Create connection tracking record using generic foreign key
|
||||
content_type = ContentType.objects.get_for_model(content)
|
||||
connection = VODConnection.objects.create(
|
||||
vod=vod,
|
||||
content_type=content_type,
|
||||
object_id=content.id,
|
||||
m3u_profile=available_profile,
|
||||
client_id=client_id,
|
||||
client_ip=client_ip,
|
||||
|
|
@ -83,7 +97,7 @@ def stream_vod(request, vod_uuid):
|
|||
# Stream the VOD content
|
||||
try:
|
||||
response = requests.get(
|
||||
vod.url,
|
||||
content.url,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
timeout=(10, 60)
|
||||
|
|
@ -98,7 +112,7 @@ def stream_vod(request, vod_uuid):
|
|||
)
|
||||
|
||||
# Determine content type
|
||||
content_type = response.headers.get('Content-Type', 'video/mp4')
|
||||
content_type_header = response.headers.get('Content-Type', 'video/mp4')
|
||||
content_length = response.headers.get('Content-Length')
|
||||
content_range = response.headers.get('Content-Range')
|
||||
|
||||
|
|
@ -132,7 +146,7 @@ def stream_vod(request, vod_uuid):
|
|||
# Build response with appropriate headers
|
||||
streaming_response = StreamingHttpResponse(
|
||||
stream_generator(),
|
||||
content_type=content_type,
|
||||
content_type=content_type_header,
|
||||
status=response.status_code
|
||||
)
|
||||
|
||||
|
|
@ -147,7 +161,7 @@ def stream_vod(request, vod_uuid):
|
|||
streaming_response['Access-Control-Allow-Origin'] = '*'
|
||||
streaming_response['Cache-Control'] = 'no-cache'
|
||||
|
||||
logger.info(f"[{client_id}] Started streaming VOD: {vod.name}")
|
||||
logger.info(f"[{client_id}] Started streaming {content_type_name}: {content.name}")
|
||||
return streaming_response
|
||||
|
||||
except requests.RequestException as e:
|
||||
|
|
@ -168,8 +182,20 @@ def stream_vod(request, vod_uuid):
|
|||
|
||||
@csrf_exempt
|
||||
@api_view(["POST"])
|
||||
def update_position(request, vod_uuid):
|
||||
"""Update playback position for a VOD"""
|
||||
def update_movie_position(request, movie_uuid):
|
||||
"""Update playback position for a movie"""
|
||||
return _update_position(request, Movie, movie_uuid)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(["POST"])
|
||||
def update_episode_position(request, episode_uuid):
|
||||
"""Update playback position for an episode"""
|
||||
return _update_position(request, Episode, episode_uuid)
|
||||
|
||||
|
||||
def _update_position(request, model_class, content_uuid):
|
||||
"""Generic function to update playback position"""
|
||||
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
|
@ -181,8 +207,13 @@ def update_position(request, vod_uuid):
|
|||
return JsonResponse({"error": "Client ID required"}, status=400)
|
||||
|
||||
try:
|
||||
vod = get_object_or_404(VOD, uuid=vod_uuid)
|
||||
connection = VODConnection.objects.get(vod=vod, client_id=client_id)
|
||||
content = get_object_or_404(model_class, uuid=content_uuid)
|
||||
content_type = ContentType.objects.get_for_model(content)
|
||||
connection = VODConnection.objects.get(
|
||||
content_type=content_type,
|
||||
object_id=content.id,
|
||||
client_id=client_id
|
||||
)
|
||||
connection.update_activity(position=position)
|
||||
|
||||
return JsonResponse({"status": "success"})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from django.contrib import admin
|
||||
from .models import VOD, Series, VODCategory, VODConnection
|
||||
from .models import Series, VODCategory, VODConnection, Movie, Episode
|
||||
|
||||
|
||||
@admin.register(VODCategory)
|
||||
|
|
@ -17,23 +17,42 @@ class SeriesAdmin(admin.ModelAdmin):
|
|||
readonly_fields = ['uuid', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
@admin.register(VOD)
|
||||
class VODAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'type', 'series', 'season_number', 'episode_number', 'year', 'm3u_account']
|
||||
list_filter = ['type', 'm3u_account', 'category', 'year', 'created_at']
|
||||
@admin.register(Movie)
|
||||
class MovieAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'year', 'genre', 'duration', 'm3u_account', 'created_at']
|
||||
list_filter = ['m3u_account', 'category', 'year', 'created_at']
|
||||
search_fields = ['name', 'description', 'stream_id']
|
||||
readonly_fields = ['uuid', 'created_at', 'updated_at']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('series', 'm3u_account', 'category')
|
||||
return super().get_queryset(request).select_related('category', 'logo', 'm3u_account')
|
||||
|
||||
|
||||
@admin.register(Episode)
|
||||
class EpisodeAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'series', 'season_number', 'episode_number', 'duration', 'm3u_account', 'created_at']
|
||||
list_filter = ['m3u_account', 'series', 'season_number', 'created_at']
|
||||
search_fields = ['name', 'description', 'stream_id', 'series__name']
|
||||
readonly_fields = ['uuid', 'created_at', 'updated_at']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('series', 'm3u_account')
|
||||
|
||||
|
||||
@admin.register(VODConnection)
|
||||
class VODConnectionAdmin(admin.ModelAdmin):
|
||||
list_display = ['vod', 'client_ip', 'client_id', 'connected_at', 'last_activity', 'position_seconds']
|
||||
list_display = ['get_content_name', 'client_ip', 'client_id', 'connected_at', 'last_activity', 'position_seconds']
|
||||
list_filter = ['connected_at', 'last_activity']
|
||||
search_fields = ['client_ip', 'client_id', 'vod__name']
|
||||
search_fields = ['client_ip', 'client_id']
|
||||
readonly_fields = ['connected_at']
|
||||
|
||||
def get_content_name(self, obj):
|
||||
if obj.content_object:
|
||||
return obj.content_object.name
|
||||
elif obj.vod:
|
||||
return obj.vod.name
|
||||
return "Unknown"
|
||||
get_content_name.short_description = "Content"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('vod', 'm3u_profile')
|
||||
return super().get_queryset(request).select_related('content_object', 'm3u_profile')
|
||||
|
|
|
|||
|
|
@ -9,39 +9,38 @@ from apps.accounts.permissions import (
|
|||
Authenticated,
|
||||
permission_classes_by_action,
|
||||
)
|
||||
from .models import VOD, Series, VODCategory, VODConnection
|
||||
from .models import Series, VODCategory, VODConnection, Movie, Episode
|
||||
from .serializers import (
|
||||
VODSerializer,
|
||||
MovieSerializer,
|
||||
EpisodeSerializer,
|
||||
SeriesSerializer,
|
||||
VODCategorySerializer,
|
||||
VODConnectionSerializer
|
||||
)
|
||||
|
||||
|
||||
class VODFilter(django_filters.FilterSet):
|
||||
class MovieFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr="icontains")
|
||||
type = django_filters.ChoiceFilter(choices=VOD.TYPE_CHOICES)
|
||||
category = django_filters.CharFilter(field_name="category__name", lookup_expr="icontains")
|
||||
series = django_filters.NumberFilter(field_name="series__id")
|
||||
m3u_account = django_filters.NumberFilter(field_name="m3u_account__id")
|
||||
year = django_filters.NumberFilter()
|
||||
year_gte = django_filters.NumberFilter(field_name="year", lookup_expr="gte")
|
||||
year_lte = django_filters.NumberFilter(field_name="year", lookup_expr="lte")
|
||||
|
||||
class Meta:
|
||||
model = VOD
|
||||
fields = ['name', 'type', 'category', 'series', 'm3u_account', 'year']
|
||||
model = Movie
|
||||
fields = ['name', 'category', 'm3u_account', 'year']
|
||||
|
||||
|
||||
class VODViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for VOD content (Movies and Episodes)"""
|
||||
queryset = VOD.objects.all()
|
||||
serializer_class = VODSerializer
|
||||
class MovieViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for Movie content"""
|
||||
queryset = Movie.objects.all()
|
||||
serializer_class = MovieSerializer
|
||||
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_class = VODFilter
|
||||
filterset_class = MovieFilter
|
||||
search_fields = ['name', 'description', 'genre']
|
||||
ordering_fields = ['name', 'year', 'created_at', 'season_number', 'episode_number']
|
||||
ordering_fields = ['name', 'year', 'created_at']
|
||||
ordering = ['name']
|
||||
|
||||
def get_permissions(self):
|
||||
|
|
@ -51,37 +50,44 @@ class VODViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
return [Authenticated()]
|
||||
|
||||
def get_queryset(self):
|
||||
return VOD.objects.select_related(
|
||||
'series', 'category', 'logo', 'm3u_account'
|
||||
return Movie.objects.select_related(
|
||||
'category', 'logo', 'm3u_account'
|
||||
).filter(m3u_account__is_active=True)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def movies(self, request):
|
||||
"""Get only movie content"""
|
||||
movies = self.get_queryset().filter(type='movie')
|
||||
movies = self.filter_queryset(movies)
|
||||
|
||||
page = self.paginate_queryset(movies)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
class EpisodeFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr="icontains")
|
||||
series = django_filters.NumberFilter(field_name="series__id")
|
||||
m3u_account = django_filters.NumberFilter(field_name="m3u_account__id")
|
||||
season_number = django_filters.NumberFilter()
|
||||
episode_number = django_filters.NumberFilter()
|
||||
|
||||
serializer = self.get_serializer(movies, many=True)
|
||||
return Response(serializer.data)
|
||||
class Meta:
|
||||
model = Episode
|
||||
fields = ['name', 'series', 'm3u_account', 'season_number', 'episode_number']
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def episodes(self, request):
|
||||
"""Get only episode content"""
|
||||
episodes = self.get_queryset().filter(type='episode')
|
||||
episodes = self.filter_queryset(episodes)
|
||||
|
||||
page = self.paginate_queryset(episodes)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
class EpisodeViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for Episode content"""
|
||||
queryset = Episode.objects.all()
|
||||
serializer_class = EpisodeSerializer
|
||||
|
||||
serializer = self.get_serializer(episodes, many=True)
|
||||
return Response(serializer.data)
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_class = EpisodeFilter
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'season_number', 'episode_number', 'created_at']
|
||||
ordering = ['series__name', 'season_number', 'episode_number']
|
||||
|
||||
def get_permissions(self):
|
||||
try:
|
||||
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||
except KeyError:
|
||||
return [Authenticated()]
|
||||
|
||||
def get_queryset(self):
|
||||
return Episode.objects.select_related(
|
||||
'series', 'm3u_account'
|
||||
).filter(m3u_account__is_active=True)
|
||||
|
||||
|
||||
class SeriesViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
|
@ -113,10 +119,10 @@ class SeriesViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
|
||||
page = self.paginate_queryset(episodes)
|
||||
if page is not None:
|
||||
serializer = VODSerializer(page, many=True)
|
||||
serializer = EpisodeSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = VODSerializer(episodes, many=True)
|
||||
serializer = EpisodeSerializer(episodes, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
|
@ -162,4 +168,4 @@ class VODConnectionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
return [Authenticated()]
|
||||
|
||||
def get_queryset(self):
|
||||
return VODConnection.objects.select_related('vod', 'm3u_profile')
|
||||
return VODConnection.objects.select_related('m3u_profile')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-02 16:59
|
||||
# Generated by Django 5.2.4 on 2025-08-02 18:26
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
|
@ -10,11 +10,29 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dispatcharr_channels', '0023_stream_stream_stats_stream_stream_stats_updated_at'),
|
||||
('m3u', '0012_alter_m3uaccount_refresh_interval'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VODCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('category_type', models.CharField(choices=[('movie', 'Movie'), ('series', 'Series')], default='movie', help_text='Type of content this category contains', max_length=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('m3u_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vod_categories', to='m3u.m3uaccount')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'VOD Category',
|
||||
'verbose_name_plural': 'VOD Categories',
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('name', 'm3u_account', 'category_type')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Series',
|
||||
fields=[
|
||||
|
|
@ -33,32 +51,37 @@ class Migration(migrations.Migration):
|
|||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dispatcharr_channels.logo')),
|
||||
('m3u_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='series', to='m3u.m3uaccount')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vod.vodcategory')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Series',
|
||||
'verbose_name_plural': 'Series',
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('series_id', 'm3u_account')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VODCategory',
|
||||
name='VODConnection',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('category_type', models.CharField(choices=[('movie', 'Movie'), ('series', 'Series')], default='movie', help_text='Type of content this category contains', max_length=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('m3u_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vod_categories', to='m3u.m3uaccount')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('client_id', models.CharField(max_length=255)),
|
||||
('client_ip', models.GenericIPAddressField()),
|
||||
('user_agent', models.TextField(blank=True, null=True)),
|
||||
('connected_at', models.DateTimeField(auto_now_add=True)),
|
||||
('last_activity', models.DateTimeField(auto_now=True)),
|
||||
('bytes_sent', models.BigIntegerField(default=0)),
|
||||
('position_seconds', models.IntegerField(default=0, help_text='Current playback position')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('m3u_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vod_connections', to='m3u.m3uaccountprofile')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'VOD Category',
|
||||
'verbose_name_plural': 'VOD Categories',
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('name', 'm3u_account', 'category_type')},
|
||||
'verbose_name': 'VOD Connection',
|
||||
'verbose_name_plural': 'VOD Connections',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VOD',
|
||||
name='Episode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
|
|
@ -66,9 +89,7 @@ class Migration(migrations.Migration):
|
|||
('description', models.TextField(blank=True, null=True)),
|
||||
('year', models.IntegerField(blank=True, null=True)),
|
||||
('rating', models.CharField(blank=True, max_length=10, null=True)),
|
||||
('genre', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('duration', models.IntegerField(blank=True, help_text='Duration in minutes', null=True)),
|
||||
('type', models.CharField(choices=[('movie', 'Movie'), ('episode', 'Episode')], default='movie', max_length=10)),
|
||||
('season_number', models.IntegerField(blank=True, null=True)),
|
||||
('episode_number', models.IntegerField(blank=True, null=True)),
|
||||
('url', models.URLField(max_length=2048)),
|
||||
|
|
@ -79,45 +100,44 @@ class Migration(migrations.Migration):
|
|||
('custom_properties', models.JSONField(blank=True, help_text='JSON data for additional properties', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dispatcharr_channels.logo')),
|
||||
('m3u_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vods', to='m3u.m3uaccount')),
|
||||
('series', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='episodes', to='vod.series')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vod.vodcategory')),
|
||||
('m3u_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='episodes', to='m3u.m3uaccount')),
|
||||
('series', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='episodes', to='vod.series')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'VOD',
|
||||
'verbose_name_plural': 'VODs',
|
||||
'ordering': ['name', 'season_number', 'episode_number'],
|
||||
'verbose_name': 'Episode',
|
||||
'verbose_name_plural': 'Episodes',
|
||||
'ordering': ['series__name', 'season_number', 'episode_number'],
|
||||
'unique_together': {('stream_id', 'm3u_account')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='series',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vod.vodcategory'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='series',
|
||||
unique_together={('series_id', 'm3u_account')},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VODConnection',
|
||||
name='Movie',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('client_id', models.CharField(max_length=255)),
|
||||
('client_ip', models.GenericIPAddressField()),
|
||||
('user_agent', models.TextField(blank=True, null=True)),
|
||||
('connected_at', models.DateTimeField(auto_now_add=True)),
|
||||
('last_activity', models.DateTimeField(auto_now=True)),
|
||||
('bytes_sent', models.BigIntegerField(default=0)),
|
||||
('position_seconds', models.IntegerField(default=0, help_text='Current playback position')),
|
||||
('m3u_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vod_connections', to='m3u.m3uaccountprofile')),
|
||||
('vod', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections', to='vod.vod')),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('year', models.IntegerField(blank=True, null=True)),
|
||||
('rating', models.CharField(blank=True, max_length=10, null=True)),
|
||||
('genre', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('duration', models.IntegerField(blank=True, help_text='Duration in minutes', null=True)),
|
||||
('url', models.URLField(max_length=2048)),
|
||||
('stream_id', models.CharField(help_text='External stream ID from M3U provider', max_length=255)),
|
||||
('container_extension', models.CharField(blank=True, max_length=10, null=True)),
|
||||
('tmdb_id', models.CharField(blank=True, help_text='TMDB ID for metadata', max_length=50, null=True)),
|
||||
('imdb_id', models.CharField(blank=True, help_text='IMDB ID for metadata', max_length=50, null=True)),
|
||||
('custom_properties', models.JSONField(blank=True, help_text='JSON data for additional properties', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dispatcharr_channels.logo')),
|
||||
('m3u_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movies', to='m3u.m3uaccount')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vod.vodcategory')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'VOD Connection',
|
||||
'verbose_name_plural': 'VOD Connections',
|
||||
'unique_together': {('vod', 'client_id')},
|
||||
'verbose_name': 'Movie',
|
||||
'verbose_name_plural': 'Movies',
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('stream_id', 'm3u_account')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from apps.m3u.models import M3UAccount
|
||||
from apps.channels.models import Logo
|
||||
import uuid
|
||||
|
|
@ -7,16 +9,16 @@ import uuid
|
|||
|
||||
class VODCategory(models.Model):
|
||||
"""Categories for organizing VODs (e.g., Action, Comedy, Drama)"""
|
||||
|
||||
|
||||
CATEGORY_TYPE_CHOICES = [
|
||||
('movie', 'Movie'),
|
||||
('series', 'Series'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
category_type = models.CharField(
|
||||
max_length=10,
|
||||
choices=CATEGORY_TYPE_CHOICES,
|
||||
max_length=10,
|
||||
choices=CATEGORY_TYPE_CHOICES,
|
||||
default='movie',
|
||||
help_text="Type of content this category contains"
|
||||
)
|
||||
|
|
@ -72,13 +74,8 @@ class Series(models.Model):
|
|||
return f"{self.name} ({self.year or 'Unknown'})"
|
||||
|
||||
|
||||
class VOD(models.Model):
|
||||
"""Video on Demand content (Movies and Episodes)"""
|
||||
TYPE_CHOICES = [
|
||||
('movie', 'Movie'),
|
||||
('episode', 'Episode'),
|
||||
]
|
||||
|
||||
class Movie(models.Model):
|
||||
"""Movie content"""
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
|
@ -86,12 +83,6 @@ class VOD(models.Model):
|
|||
rating = models.CharField(max_length=10, blank=True, null=True)
|
||||
genre = models.CharField(max_length=255, blank=True, null=True)
|
||||
duration = models.IntegerField(blank=True, null=True, help_text="Duration in minutes")
|
||||
type = models.CharField(max_length=10, choices=TYPE_CHOICES, default='movie')
|
||||
|
||||
# Episode specific fields
|
||||
series = models.ForeignKey(Series, on_delete=models.CASCADE, null=True, blank=True, related_name='episodes')
|
||||
season_number = models.IntegerField(blank=True, null=True)
|
||||
episode_number = models.IntegerField(blank=True, null=True)
|
||||
|
||||
# Streaming information
|
||||
url = models.URLField(max_length=2048)
|
||||
|
|
@ -102,7 +93,7 @@ class VOD(models.Model):
|
|||
m3u_account = models.ForeignKey(
|
||||
M3UAccount,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='vods'
|
||||
related_name='movies'
|
||||
)
|
||||
stream_id = models.CharField(max_length=255, help_text="External stream ID from M3U provider")
|
||||
container_extension = models.CharField(max_length=10, blank=True, null=True)
|
||||
|
|
@ -118,25 +109,76 @@ class VOD(models.Model):
|
|||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "VOD"
|
||||
verbose_name_plural = "VODs"
|
||||
ordering = ['name', 'season_number', 'episode_number']
|
||||
verbose_name = "Movie"
|
||||
verbose_name_plural = "Movies"
|
||||
ordering = ['name']
|
||||
unique_together = ['stream_id', 'm3u_account']
|
||||
|
||||
def __str__(self):
|
||||
if self.type == 'episode' and self.series:
|
||||
season_ep = f"S{self.season_number:02d}E{self.episode_number:02d}" if self.season_number and self.episode_number else ""
|
||||
return f"{self.series.name} {season_ep} - {self.name}"
|
||||
return f"{self.name} ({self.year or 'Unknown'})"
|
||||
|
||||
def get_stream_url(self):
|
||||
"""Generate the proxied stream URL for this VOD"""
|
||||
return f"/proxy/vod/stream/{self.uuid}"
|
||||
"""Generate the proxied stream URL for this movie"""
|
||||
return f"/proxy/vod/movie/{self.uuid}"
|
||||
|
||||
|
||||
class Episode(models.Model):
|
||||
"""Episode content for TV series"""
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
year = models.IntegerField(blank=True, null=True)
|
||||
rating = models.CharField(max_length=10, blank=True, null=True)
|
||||
duration = models.IntegerField(blank=True, null=True, help_text="Duration in minutes")
|
||||
|
||||
# Episode specific fields
|
||||
series = models.ForeignKey(Series, on_delete=models.CASCADE, related_name='episodes')
|
||||
season_number = models.IntegerField(blank=True, null=True)
|
||||
episode_number = models.IntegerField(blank=True, null=True)
|
||||
|
||||
# Streaming information
|
||||
url = models.URLField(max_length=2048)
|
||||
|
||||
# M3U relationship
|
||||
m3u_account = models.ForeignKey(
|
||||
M3UAccount,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='episodes'
|
||||
)
|
||||
stream_id = models.CharField(max_length=255, help_text="External stream ID from M3U provider")
|
||||
container_extension = models.CharField(max_length=10, blank=True, null=True)
|
||||
|
||||
# Metadata IDs
|
||||
tmdb_id = models.CharField(max_length=50, blank=True, null=True, help_text="TMDB ID for metadata")
|
||||
imdb_id = models.CharField(max_length=50, blank=True, null=True, help_text="IMDB ID for metadata")
|
||||
|
||||
# Additional properties
|
||||
custom_properties = models.JSONField(blank=True, null=True, help_text="JSON data for additional properties")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Episode"
|
||||
verbose_name_plural = "Episodes"
|
||||
ordering = ['series__name', 'season_number', 'episode_number']
|
||||
unique_together = ['stream_id', 'm3u_account']
|
||||
|
||||
def __str__(self):
|
||||
season_ep = f"S{self.season_number:02d}E{self.episode_number:02d}" if self.season_number and self.episode_number else ""
|
||||
return f"{self.series.name} {season_ep} - {self.name}"
|
||||
|
||||
def get_stream_url(self):
|
||||
"""Generate the proxied stream URL for this episode"""
|
||||
return f"/proxy/vod/episode/{self.uuid}"
|
||||
|
||||
class VODConnection(models.Model):
|
||||
"""Track active VOD connections for connection limit management"""
|
||||
vod = models.ForeignKey(VOD, on_delete=models.CASCADE, related_name='connections')
|
||||
# Use generic foreign key to support both Movie and Episode
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
m3u_profile = models.ForeignKey(
|
||||
'm3u.M3UAccountProfile',
|
||||
on_delete=models.CASCADE,
|
||||
|
|
@ -153,10 +195,10 @@ class VODConnection(models.Model):
|
|||
class Meta:
|
||||
verbose_name = "VOD Connection"
|
||||
verbose_name_plural = "VOD Connections"
|
||||
unique_together = ['vod', 'client_id']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.vod.name} - {self.client_ip} ({self.client_id})"
|
||||
content_name = getattr(self.content_object, 'name', 'Unknown') if self.content_object else 'Unknown'
|
||||
return f"{content_name} - {self.client_ip} ({self.client_id})"
|
||||
|
||||
def update_activity(self, bytes_sent=0, position=0):
|
||||
"""Update connection activity"""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from rest_framework import serializers
|
||||
from .models import VOD, Series, VODCategory, VODConnection
|
||||
from .models import Series, VODCategory, VODConnection, Movie, Episode
|
||||
from apps.channels.serializers import LogoSerializer
|
||||
from apps.m3u.serializers import M3UAccountSerializer
|
||||
|
||||
|
|
@ -26,15 +26,28 @@ class SeriesSerializer(serializers.ModelSerializer):
|
|||
return obj.episodes.count()
|
||||
|
||||
|
||||
class VODSerializer(serializers.ModelSerializer):
|
||||
class MovieSerializer(serializers.ModelSerializer):
|
||||
logo = LogoSerializer(read_only=True)
|
||||
category = VODCategorySerializer(read_only=True)
|
||||
m3u_account = M3UAccountSerializer(read_only=True)
|
||||
stream_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Movie
|
||||
fields = '__all__'
|
||||
|
||||
def get_stream_url(self, obj):
|
||||
return obj.get_stream_url()
|
||||
|
||||
|
||||
class EpisodeSerializer(serializers.ModelSerializer):
|
||||
logo = LogoSerializer(read_only=True)
|
||||
series = SeriesSerializer(read_only=True)
|
||||
m3u_account = M3UAccountSerializer(read_only=True)
|
||||
stream_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = VOD
|
||||
model = Episode
|
||||
fields = '__all__'
|
||||
|
||||
def get_stream_url(self, obj):
|
||||
|
|
@ -42,8 +55,13 @@ class VODSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class VODConnectionSerializer(serializers.ModelSerializer):
|
||||
vod = VODSerializer(read_only=True)
|
||||
content_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = VODConnection
|
||||
fields = '__all__'
|
||||
|
||||
def get_content_name(self, obj):
|
||||
if obj.content_object:
|
||||
return getattr(obj.content_object, 'name', 'Unknown')
|
||||
return 'Unknown'
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import re
|
|||
from celery import shared_task
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from .models import VOD, Series, VODCategory, VODConnection
|
||||
from .models import Series, VODCategory, VODConnection, Movie, Episode
|
||||
from apps.m3u.models import M3UAccount
|
||||
from apps.channels.models import Logo
|
||||
|
||||
|
|
@ -161,11 +161,10 @@ def refresh_movies(account):
|
|||
# Extract year from title if not provided in API
|
||||
year = extract_year_from_data(movie_data, 'name')
|
||||
|
||||
vod_data = {
|
||||
movie_data_dict = {
|
||||
'name': movie_data['name'],
|
||||
'type': 'movie',
|
||||
'url': stream_url,
|
||||
'category_id': category,
|
||||
'category': category,
|
||||
'year': year,
|
||||
'rating': movie_data.get('rating'),
|
||||
'genre': movie_data.get('genre'),
|
||||
|
|
@ -176,10 +175,11 @@ def refresh_movies(account):
|
|||
'custom_properties': json.dumps(movie_data) if movie_data else None
|
||||
}
|
||||
|
||||
vod, created = VOD.objects.update_or_create(
|
||||
# Use new Movie model
|
||||
movie, created = Movie.objects.update_or_create(
|
||||
stream_id=movie_data['stream_id'],
|
||||
m3u_account=account,
|
||||
defaults=vod_data
|
||||
defaults=movie_data_dict
|
||||
)
|
||||
|
||||
# Handle logo
|
||||
|
|
@ -188,8 +188,8 @@ def refresh_movies(account):
|
|||
url=movie_data['stream_icon'],
|
||||
defaults={'name': movie_data['name']}
|
||||
)
|
||||
vod.logo = logo
|
||||
vod.save()
|
||||
movie.logo = logo
|
||||
movie.save()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing movie {movie_data.get('name', 'Unknown')}: {e}")
|
||||
|
|
@ -326,7 +326,6 @@ def refresh_series_episodes(account, series, series_id):
|
|||
|
||||
episode_dict = {
|
||||
'name': episode_data.get('title', f"Episode {episode_data.get('episode_num', '')}"),
|
||||
'type': 'episode',
|
||||
'series': series,
|
||||
'season_number': int(season_num) if season_num.isdigit() else None,
|
||||
'episode_number': episode_data.get('episode_num'),
|
||||
|
|
@ -341,7 +340,8 @@ def refresh_series_episodes(account, series, series_id):
|
|||
'custom_properties': json.dumps(episode_data) if episode_data else None
|
||||
}
|
||||
|
||||
VOD.objects.update_or_create(
|
||||
# Use new Episode model
|
||||
episode, created = Episode.objects.update_or_create(
|
||||
stream_id=episode_data['id'],
|
||||
m3u_account=account,
|
||||
defaults=episode_dict
|
||||
|
|
@ -354,7 +354,6 @@ def refresh_series_episodes(account, series, series_id):
|
|||
except Exception as e:
|
||||
logger.error(f"Error refreshing episodes for series {series_id}: {e}")
|
||||
|
||||
|
||||
@shared_task
|
||||
def cleanup_inactive_vod_connections():
|
||||
"""Clean up inactive VOD connections"""
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .api_views import VODViewSet, SeriesViewSet, VODCategoryViewSet, VODConnectionViewSet
|
||||
from .api_views import MovieViewSet, EpisodeViewSet, SeriesViewSet, VODCategoryViewSet, VODConnectionViewSet
|
||||
|
||||
app_name = 'vod'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'vods', VODViewSet)
|
||||
router.register(r'movies', MovieViewSet)
|
||||
router.register(r'episodes', EpisodeViewSet)
|
||||
router.register(r'series', SeriesViewSet)
|
||||
router.register(r'categories', VODCategoryViewSet)
|
||||
router.register(r'connections', VODConnectionViewSet)
|
||||
|
|
|
|||
|
|
@ -1703,7 +1703,7 @@ export default class API {
|
|||
static async getVODs(params = {}) {
|
||||
try {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
const response = await request(`${host}/api/vod/vods/?${searchParams.toString()}`);
|
||||
const response = await request(`${host}/api/vod/movies/?${searchParams.toString()}`);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to retrieve VODs', e);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue