Browse Source

youtube stream link generation added for flutter

shokhmgar
Mohsen Taba 1 month ago
parent
commit
7e0e0bc550
  1. 20
      apps/video/models.py
  2. 42
      apps/video/serializers.py
  3. 19
      apps/video/serializers_dovodi.py
  4. 2
      apps/video/urls.py
  5. 47
      apps/video/views.py
  6. 2
      requirements.txt
  7. 94
      utils/youtube.py

20
apps/video/models.py

@ -107,6 +107,26 @@ class Video(LowercaseSlugMixin, models.Model):
return f"{settings.DOVODI_DOMAIN}/videos/{self.slug}"
return None
@property
def stream_url(self):
"""
Get direct playable stream URL for video.
For YouTube link types, extracts direct media URL via yt-dlp (cached).
For Video file types, returns video file URL.
"""
if self.video_type == self.VedioTypeChoices.YOUTUBE_LINK:
if self.video_url:
from utils.youtube import get_youtube_stream_url
extracted = get_youtube_stream_url(self.video_url)
return extracted or self.video_url
return None
elif self.video_type == self.VedioTypeChoices.VIDEO_FILE:
if self.video_file:
return self.video_file.url
return None
return self.video_url
def increment_view_count(self):
"""Increment the view count for this video"""
self.view_count += 1

42
apps/video/serializers.py

@ -19,6 +19,7 @@ class VideoCategoryListSerializer(serializers.ModelSerializer):
class VideoListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
video_file = serializers.SerializerMethodField()
stream_url = serializers.SerializerMethodField()
share_link = serializers.CharField(read_only=True)
video_time = serializers.SerializerMethodField()
total_time_formatted = serializers.SerializerMethodField()
@ -26,7 +27,7 @@ class VideoListSerializer(serializers.ModelSerializer):
class Meta:
model = Video
fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'video_type',
'video_file', 'video_url', 'video_time', 'view_count', 'created_at', 'share_link', 'total_time_formatted']
'video_file', 'video_url', 'stream_url', 'video_time', 'view_count', 'created_at', 'share_link', 'total_time_formatted']
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
@ -43,10 +44,27 @@ class VideoListSerializer(serializers.ModelSerializer):
return obj.video_file.url
return None
def get_stream_url(self, obj):
if obj.video_type == Video.VedioTypeChoices.YOUTUBE_LINK:
if obj.video_url:
from utils.youtube import get_youtube_stream_url
extracted = get_youtube_stream_url(obj.video_url)
return extracted or obj.video_url
return None
elif obj.video_type == Video.VedioTypeChoices.VIDEO_FILE:
if obj.video_file:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.video_file.url)
return obj.video_file.url
return None
return obj.video_url
def get_video_time(self, obj):
return format_media_time(obj.video_time)
class VideoPlaylistListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
video_time = serializers.SerializerMethodField()
@ -158,21 +176,39 @@ class VideoDetailSerializer(serializers.ModelSerializer):
is_in_playlist = serializers.SerializerMethodField()
playlist_videos = serializers.SerializerMethodField()
share_link = serializers.CharField(read_only=True)
stream_url = serializers.SerializerMethodField()
video_time = serializers.SerializerMethodField()
total_time_formatted = serializers.SerializerMethodField()
class Meta:
model = Video
fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'video_type',
'video_file', 'video_url', 'video_time', 'view_count',
'video_file', 'video_url', 'stream_url', 'video_time', 'view_count',
'categories', 'created_at', 'user_rate', 'average_rate', 'bookmark',
'is_in_playlist', 'playlist_videos', 'share_link', 'video_time', 'total_time_formatted']
def get_stream_url(self, obj):
if obj.video_type == Video.VedioTypeChoices.YOUTUBE_LINK:
if obj.video_url:
from utils.youtube import get_youtube_stream_url
extracted = get_youtube_stream_url(obj.video_url)
return extracted or obj.video_url
return None
elif obj.video_type == Video.VedioTypeChoices.VIDEO_FILE:
if obj.video_file:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.video_file.url)
return obj.video_file.url
return None
return obj.video_url
def get_video_time(self, obj):
return format_media_time(obj.video_time)
def get_total_time_formatted(self, obj):
return format_media_time(obj.video_time)
return format_media_time(obj.video_time)
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))

19
apps/video/serializers_dovodi.py

@ -47,6 +47,7 @@ class DovodiVideoItemSerializer(serializers.ModelSerializer):
slug = serializers.CharField(required=False, allow_blank=True)
thumbnail = AbsoluteImageField(required=False, allow_null=True)
video_file = AbsoluteFileField(required=False, allow_null=True)
stream_url = serializers.SerializerMethodField(read_only=True)
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
remove_video_file = serializers.BooleanField(write_only=True, required=False, default=False)
playlist = serializers.IntegerField(required=False, allow_null=True, write_only=True)
@ -63,6 +64,7 @@ class DovodiVideoItemSerializer(serializers.ModelSerializer):
"video_type",
"video_file",
"video_url",
"stream_url",
"video_time",
"status",
"view_count",
@ -75,10 +77,27 @@ class DovodiVideoItemSerializer(serializers.ModelSerializer):
]
read_only_fields = ["id", "view_count", "playlist_id", "created_at", "updated_at"]
def get_stream_url(self, obj):
if obj.video_type == Video.VedioTypeChoices.YOUTUBE_LINK:
if obj.video_url:
from utils.youtube import get_youtube_stream_url
extracted = get_youtube_stream_url(obj.video_url)
return extracted or obj.video_url
return None
elif obj.video_type == Video.VedioTypeChoices.VIDEO_FILE:
if obj.video_file:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.video_file.url)
return obj.video_file.url
return None
return obj.video_url
def get_playlist_id(self, obj):
appearance = obj.playlist_appearances.first()
return appearance.playlist_id if appearance else None
def validate(self, attrs):
video_type = attrs.get("video_type", getattr(self.instance, "video_type", None))
video_url = attrs.get("video_url", getattr(self.instance, "video_url", None))

2
apps/video/urls.py

@ -33,4 +33,6 @@ urlpatterns = [
# Keep old video endpoints for backward compatibility if needed
path('list/', VideoListAPIView.as_view(), name='video-list'),
re_path(r'detail/(?P<slug>[\w-]+)/$', VideoDetailAPIView.as_view(), name='video-detail'),
path('resolve-youtube/', ResolveYouTubeStreamURLAPIView.as_view(), name='resolve-youtube'),
]

47
apps/video/views.py

@ -500,3 +500,50 @@ class DovodiVideoItemViewSet(ModelViewSet):
)
return queryset
class ResolveYouTubeStreamURLAPIView(generics.GenericAPIView):
"""
API view to extract direct stream URL for a YouTube URL using yt-dlp.
"""
permission_classes = (IsAuthenticated,)
authentication_classes = [TokenAuthentication]
@swagger_auto_schema(
operation_description="Extract direct playable stream URL from YouTube URL",
tags=["Dobodbi - Video"],
manual_parameters=[
openapi.Parameter(
name='url',
in_=openapi.IN_QUERY,
description='YouTube video URL',
type=openapi.TYPE_STRING,
required=True
),
],
responses={
200: openapi.Response(
description="Resolved direct stream URL",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'youtube_url': openapi.Schema(type=openapi.TYPE_STRING),
'stream_url': openapi.Schema(type=openapi.TYPE_STRING),
}
)
),
400: "Bad Request"
}
)
def get(self, request, *args, **kwargs):
youtube_url = request.query_params.get('url')
if not youtube_url:
return Response({'error': 'URL parameter is required.'}, status=status.HTTP_400_BAD_REQUEST)
from utils.youtube import get_youtube_stream_url
stream_url = get_youtube_stream_url(youtube_url)
return Response({
'youtube_url': youtube_url,
'stream_url': stream_url or youtube_url
}, status=status.HTTP_200_OK)

2
requirements.txt

@ -134,6 +134,8 @@ google-auth==2.6.0
pyjwt
cryptography>=41.0.0
django-celery-beat==2.5.0
yt-dlp>=2024.3.10
https://yaghoubi:e07059e0ac6be3b0032ded5f65f03363fbd3811f@git.habibapp.com/NewHorizon/django-limitless-dashboard.git/archive/master.zip
https://yaghoubi:e07059e0ac6be3b0032ded5f65f03363fbd3811f@git.habibapp.com/NewHorizon/ajax-datatable.git/archive/master.zip

94
utils/youtube.py

@ -0,0 +1,94 @@
import hashlib
import logging
import re
from typing import Optional
from django.core.cache import cache
logger = logging.getLogger(__name__)
# Cache duration for YouTube stream URLs (3 hours = 10,800 seconds)
YOUTUBE_STREAM_CACHE_TTL = 3 * 3600
def normalize_youtube_url(url: str) -> str:
"""
Extract YouTube 11-character video ID and return clean canonical URL.
Strips tracking parameters (like ?si=...) and converts short/embed links to standard watch URL.
"""
if not url:
return url
match = re.search(r'(?:v=|\/|embed\/|v\/|^)([0-9A-Za-z_-]{11})(?:[\?&/]|$)', url)
if match:
video_id = match.group(1)
return f"https://www.youtube.com/watch?v={video_id}"
return url
def _get_cache_key(youtube_url: str) -> str:
url_hash = hashlib.md5(youtube_url.strip().encode('utf-8')).hexdigest()
return f"yt_stream_url_{url_hash}"
def get_youtube_stream_url(youtube_url: str) -> Optional[str]:
"""
Extract direct playable stream URL from a YouTube video URL using yt-dlp.
Caches the extracted URL in Django cache for 3 hours to minimize calls to YouTube.
Returns direct stream URL string if successful, or None if extraction fails.
"""
if not youtube_url or not isinstance(youtube_url, str):
return None
youtube_url = youtube_url.strip()
if not youtube_url:
return None
cache_key = _get_cache_key(youtube_url)
cached_url = cache.get(cache_key)
if cached_url:
return cached_url
clean_url = normalize_youtube_url(youtube_url)
try:
import yt_dlp
ydl_opts = {
'format': 'best[ext=mp4]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
'quiet': True,
'no_warnings': True,
'skip_download': True,
'nocheckcertificate': True,
'extractor_args': {
'youtube': {
'player_client': ['android', 'ios', 'web', 'mweb'],
}
}
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(clean_url, download=False)
if not info:
return None
if 'entries' in info:
info = info['entries'][0]
stream_url = info.get('url')
if stream_url:
cache.set(cache_key, stream_url, YOUTUBE_STREAM_CACHE_TTL)
return stream_url
except Exception as e:
logger.error(f"Failed to extract YouTube stream URL for '{youtube_url}': {e}")
return None
def clear_youtube_stream_cache(youtube_url: str) -> None:
"""
Clear cached stream URL for a given YouTube URL.
"""
if youtube_url:
cache_key = _get_cache_key(youtube_url)
cache.delete(cache_key)
Loading…
Cancel
Save