from django.db import models from django.utils.translation import gettext_lazy as _ from django.conf import settings import datetime from filer.fields.image import FilerImageField from utils import generate_slug_for_model from utils.mixins import LowercaseSlugMixin from django.core.validators import FileExtensionValidator from django.core.exceptions import ValidationError class VideoCategory(LowercaseSlugMixin, models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug')) status = models.BooleanField(default=True, verbose_name=_('status')) order = models.PositiveIntegerField(default=0, verbose_name=_('order')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): return self.title class Meta: verbose_name = _('Video Category') verbose_name_plural = _('Video Categories') ordering = ['order'] class VideoCollection(LowercaseSlugMixin, models.Model): class DisplayPosition(models.TextChoices): PINNED = 'pinned', _('Pinned') MIDDLE = 'middle', _('Middle Section') title = models.CharField(max_length=255) slug = models.SlugField(max_length=255, unique=True, allow_unicode=True) summary = models.CharField(max_length=512, null=True, blank=True, help_text=_('could be null')) thumbnail = models.ImageField(upload_to='video/collection/', null=True, blank=True, help_text=_('image allowed')) order = models.IntegerField(default=0, verbose_name=_('order')) status = models.BooleanField(default=True, verbose_name=_('status')) display_position = models.CharField( max_length=20, choices=DisplayPosition.choices, default=DisplayPosition.PINNED, verbose_name=_('Display Position') ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): return f'Collection #{self.id}/{self.title}' class Meta: verbose_name = _('Video Collection') verbose_name_plural = _('Video Collections') class PinnedVideoCollection(VideoCollection): class Meta: proxy = True verbose_name = _('Pinned Video Collection') verbose_name_plural = _('Pinned Video Collections') class MiddleVideoCollection(VideoCollection): class Meta: proxy = True verbose_name = _('Middle Section Video Collection') verbose_name_plural = _('Middle Section Video Collections') class Video(LowercaseSlugMixin, models.Model): slug_source_field = 'title' class VedioTypeChoices(models.TextChoices): YOUTUBE_LINK = 'youtube_link', 'Youtube Link' VIDEO_FILE = 'video_file', 'Video File' title = models.CharField(max_length=255, null=True) slug = models.SlugField(allow_unicode=True, unique=True) thumbnail = models.ImageField(upload_to='video/thumbnails/', null=True, blank=True, help_text=_('image allowed')) description = models.TextField(null=True) video_type = models.CharField(max_length=255, choices=VedioTypeChoices.choices) video_file = models.FileField(upload_to='video/videos/', null=True, blank=True ,validators=[ FileExtensionValidator( allowed_extensions=['mp4', 'mov', 'avi', 'mkv', 'webm'] ) ]) video_url = models.CharField(max_length=655, null=True, blank=True) video_time = models.TimeField() view_count = models.PositiveBigIntegerField(default=0, verbose_name=_('view count')) status = models.BooleanField(default=True, verbose_name=_('status')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): return self.title @property def share_link(self): if self.slug: return f"{settings.DOVODI_DOMAIN}/videos/{self.slug}" return None def increment_view_count(self): """Increment the view count for this video""" self.view_count += 1 self.save(update_fields=['view_count']) return self.view_count def clean(self): super().clean() if self.video_type == self.VedioTypeChoices.YOUTUBE_LINK and not self.video_url: raise ValidationError({ 'video_url': _('This field is required when video type is Link.') }) if self.video_type == self.VedioTypeChoices.VIDEO_FILE and not self.video_file: raise ValidationError({ 'video_file': _('This field is required when video type is File.') }) def save(self, *args, **kwargs): is_new = self.pk is None file_changed = False if not is_new: try: old_instance = Video.objects.get(pk=self.pk) if old_instance.video_file != self.video_file: file_changed = True except Video.DoesNotExist: pass else: file_changed = True if not self.video_time: self.video_time = datetime.time(0, 0, 0) super().save(*args, **kwargs) if (file_changed or self.video_time == datetime.time(0, 0, 0)) and self.video_file and self.video_type == self.VedioTypeChoices.VIDEO_FILE: from utils.media import get_media_duration_time try: duration_time = get_media_duration_time(self.video_file.path) if duration_time: self.video_time = duration_time Video.objects.filter(pk=self.pk).update(video_time=duration_time) # Update playlists that contain this video for appearance in self.playlist_appearances.all(): playlist = appearance.playlist playlist.total_time = playlist.calculate_total_time() playlist.save(update_fields=['total_time']) except Exception as e: pass class VideoPlaylist(LowercaseSlugMixin, models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) slug = models.SlugField(allow_unicode=True, unique=True, null=True, blank=True, verbose_name=_('slug')) slogan = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('slogan')) description = models.TextField(null=True, blank=True, verbose_name=_('description')) thumbnail = models.ImageField(upload_to='video/playlist/thumbnails/', null=True, blank=True, verbose_name=_('thumbnail')) categories = models.ManyToManyField( VideoCategory, related_name='playlists', verbose_name=_('categories'), blank=True, ) collections = models.ManyToManyField( VideoCollection, through='VideoPlaylistInCollection', related_name='related_playlists', verbose_name=_('collections'), blank=True ) order = models.PositiveIntegerField(default=0, verbose_name=_('order')) status = models.BooleanField(default=True, verbose_name=_('status')) view_count = models.PositiveBigIntegerField(default=0, verbose_name=_('view count')) total_time = models.DurationField(null=True, blank=True, verbose_name=_('total time')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): return self.title def increment_view_count(self): """Increment the view count for this playlist""" self.view_count += 1 self.save(update_fields=['view_count']) return self.view_count def calculate_total_time(self): """Calculate total duration of all videos in this playlist""" from datetime import timedelta total_seconds = 0 for item in self.playlist_items.select_related('video'): video_time = item.video.video_time total_seconds += video_time.hour * 3600 + video_time.minute * 60 + video_time.second return timedelta(seconds=total_seconds) class Meta: verbose_name = _('Video Playlist') verbose_name_plural = _('Video Playlists') ordering = ['order', '-created_at'] class VideoPlaylistInCollection(models.Model): collection = models.ForeignKey( VideoCollection, on_delete=models.CASCADE, related_name='collection_playlists', verbose_name=_('collection') ) playlist = models.ForeignKey( VideoPlaylist, on_delete=models.CASCADE, related_name='playlist_collections', verbose_name=_('playlist') ) order = models.PositiveIntegerField(default=0, verbose_name=_('order')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): return f"{self.collection.title} - {self.playlist.title}" class Meta: verbose_name = _('Video Playlist in Collection') verbose_name_plural = _('Video Playlists in Collections') ordering = ['order'] unique_together = ['collection', 'playlist'] class PlaylistItem(models.Model): playlist = models.ForeignKey( VideoPlaylist, on_delete=models.CASCADE, related_name='playlist_items', verbose_name=_('playlist') ) video = models.ForeignKey( Video, on_delete=models.CASCADE, related_name='playlist_appearances', verbose_name=_('video') ) priority = models.PositiveIntegerField(default=0, verbose_name=_('priority')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): return f"{self.playlist.title} - {self.video.title} (Priority: {self.priority})" class Meta: verbose_name = _('Playlist Item') verbose_name_plural = _('Playlist Items') ordering = ['priority'] unique_together = ['playlist', 'video']