import re
from collections.abc import Iterable
from cms.models import CMSPlugin
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext, gettext_lazy as _
from djangocms_attributes_field.fields import AttributesField
from filer.fields.file import FilerFileField
from filer.fields.image import FilerImageField
# mp4, are required for full browser support
ALLOWED_EXTENSIONS = getattr(
settings,
'DJANGOCMS_VIDEO_ALLOWED_EXTENSIONS',
['mp4', 'webm', 'ogv'],
)
class AbstractVideoPlayer(CMSPlugin):
"""
Abstract configuration model for video player.
"""
label = models.CharField(
verbose_name=_('Label'),
blank=True,
max_length=255,
)
poster = FilerImageField(
verbose_name=_('Poster'),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='+',
)
width = models.IntegerField(
verbose_name=_('Width'),
blank=True,
null=True,
help_text='Leave it blank to make a video player of the default width of video source'
)
height = models.IntegerField(
verbose_name=_('Height'),
blank=True,
null=True,
help_text='Leave it blank to make a video player of the default width of video source'
)
controls = models.BooleanField(
verbose_name=_('Show controls'),
default=True,
)
autoplay = models.BooleanField(
verbose_name=_('Autoplay'),
default=False)
loop = models.BooleanField(
verbose_name=_('Loop'),
default=False)
other_attributes = AttributesField(
verbose_name=_('Other attributes'),
blank=True,
)
class Meta:
abstract = True
# Add an app namespace to related_name to avoid field name clashes
# with any other plugins that have a field with the same name as the
# lowercase of the class name of this model.
# https://github.com/divio/django-cms/issues/5030
cmsplugin_ptr = models.OneToOneField(
CMSPlugin,
related_name='%(app_label)s_%(class)s',
parent_link=True,
on_delete=models.CASCADE,
)
def __str__(self):
return self.label or str(self.pk)
def copy_relations(self, old_instance):
# Because we have a ForeignKey, it's required to copy over
# the reference from the instance to the new plugin.
self.poster = old_instance.poster
def _get_attributes_str_to_html(self, attributes: Iterable[str]) -> str:
"""
Return string with attributes to add to HTML tag e.g.:
width="500" autoplay mute
"""
return ' '.join(
[f"{attribute}" if isinstance(value, bool) else f"{attribute}={value}"
for attribute, value in self.__dict__.items() if attribute in attributes and value]
)
def _get_attributes_str_to_url(self, attributes: Iterable[str]) -> str:
"""
Return string with attributes to add to URL e.g.:
width=500&autoplay=0&mute=0
"""
return '&'.join(
[f"{attribute}={int(value)}" for attribute, value in self.__dict__.items()
if attribute in attributes and value]
)
@property
def attributes_str_to_html(self) -> str:
"""
Return height and width attributes to put them to html tag. Looks like:
height="{value}" width="{value}" etc
"""
attributes_to_print = ('width', 'height', 'controls', 'autoplay', 'loop')
return self._get_attributes_str_to_html(attributes_to_print)
@property
def attributes_str_to_url(self) -> str:
"""
Return height and width attributes to put them to url. Looks like:
controls={value}&width={value} etc.
"""
attributes_to_print = ('controls', 'autoplay', 'loop')
return self._get_attributes_str_to_url(attributes_to_print)
class SourceFileVideoPlayer(AbstractVideoPlayer):
"""
Configuration model for video player to play video from file on local disk.
"""
source_file = FilerFileField(
verbose_name=_('Source'),
blank=False,
null=True,
on_delete=models.SET_NULL,
related_name='+',
)
text_title = models.CharField(
verbose_name=_('Title'),
blank=True,
max_length=255,
)
text_description = models.TextField(
verbose_name=_('Description'),
blank=True,
)
attributes = AttributesField(
verbose_name=_('Attributes'),
blank=True,
)
muted = models.BooleanField(
verbose_name=_('Mute'),
default=False)
def __str__(self):
res = self.label or str(self.pk)
if not self.source_file:
res += gettext(' <file is missing>')
return res
def clean(self):
if self.source_file and self.source_file.extension not in ALLOWED_EXTENSIONS:
raise ValidationError(
gettext('Incorrect file type: {extension}.').format(extension=self.source_file.extension)
)
def get_short_description(self):
return str(self)
def copy_relations(self, old_instance):
# Because we have a ForeignKey, it's required to copy over
# the reference from the instance to the new plugin.
self.source_file = old_instance.source_file
@property
def attributes_str_to_html(self) -> str:
"""
Overloaded.
Add mute attribute to base-class function.
"""
attributes_to_print = ('muted',)
return super().attributes_str_to_html + ' ' + self._get_attributes_str_to_html(attributes_to_print)
# Video hosts services constants
YOUTUBE = 1
VIMEO = 2
OTHERS = 3
class HostingVideoPlayer(AbstractVideoPlayer):
"""
Configuration model for video player to play video from video hosting services.
"""
VIDEO_HOSTING_SERVICES = (
(YOUTUBE, 'YouTube'),
(VIMEO, 'Vimeo'),
(OTHERS, _('Others'))
)
video_hosting_service = models.IntegerField(
verbose_name=_('Video hosting service'),
choices=VIDEO_HOSTING_SERVICES,
default=OTHERS,
)
video_url = models.URLField(
verbose_name=_('Embed link'),
max_length=255,
help_text=_('Use this field to embed videos from external services '
'such as YouTube, Vimeo or others.'),
)
@property
def size_attributes_str_to_html(self) -> str:
"""
Return height and width attributes to put them to html tag. Looks like:
height="{value}" width="{value}"
"""
attributes_to_print = ('height', 'width')
return self._get_attributes_str_to_html(attributes_to_print)
def clean(self):
"""
Validation URLs. Function checks if URL belongs to selected video host service.
"""
if self.video_hosting_service == VIMEO:
if not re.search(r'(^|[/.])vimeo.com/', self.video_url):
raise ValidationError(_('URL does not belong to Vimeo'))
if not self.controls:
raise ValidationError(_('Vimeo does not support hiding controls.'))
elif self.video_hosting_service == YOUTUBE:
if not re.search(r'(^|[/.])youtu.be/', self.video_url) and not re.search(r'(^|[/.])youtube.com/',
self.video_url):
raise ValidationError(_('URL does not belong to YouTube'))
class VideoTrack(CMSPlugin):
"""
Renders the HTML <track> element inside <video>.
"""
KIND_CHOICES = [
('subtitles', _('Subtitles')),
('captions', _('Captions')),
('descriptions', _('Descriptions')),
('chapters', _('Chapters')),
]
kind = models.CharField(
verbose_name=_('Kind'),
choices=KIND_CHOICES,
max_length=255,
)
src = FilerFileField(
verbose_name=_('Source file'),
blank=False,
null=True,
on_delete=models.SET_NULL,
related_name='+',
)
srclang = models.CharField(
verbose_name=_('Source language'),
blank=True,
max_length=255,
help_text=_('Examples: "en" or "de" etc.'),
)
label = models.CharField(
verbose_name=_('Label'),
blank=True,
max_length=255,
)
attributes = AttributesField(
verbose_name=_('Attributes'),
blank=True,
)
def __str__(self):
label = self.kind
if self.srclang:
label += f' {self.srclang}'
return label