173 lines
4.9 KiB
Python
173 lines
4.9 KiB
Python
import re
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
|
|
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_PLAIN, render_content
|
|
|
|
VIDEO_SOURCE_YOUTUBE = "youtube"
|
|
VIDEO_SOURCE_UPLOAD = "upload"
|
|
|
|
VIDEO_SOURCE_CHOICES = [
|
|
(VIDEO_SOURCE_YOUTUBE, "YouTube / external link"),
|
|
(VIDEO_SOURCE_UPLOAD, "Uploaded file"),
|
|
]
|
|
|
|
VIDEO_SIZE_SMALL = "sm"
|
|
VIDEO_SIZE_MEDIUM = "md"
|
|
VIDEO_SIZE_LARGE = "lg"
|
|
VIDEO_SIZE_FULL = "full"
|
|
|
|
VIDEO_SIZE_CHOICES = [
|
|
(VIDEO_SIZE_SMALL, "Small (480px)"),
|
|
(VIDEO_SIZE_MEDIUM, "Medium (720px)"),
|
|
(VIDEO_SIZE_LARGE, "Large (960px)"),
|
|
(VIDEO_SIZE_FULL, "Full width"),
|
|
]
|
|
|
|
VIDEO_ASPECT_16_9 = "16/9"
|
|
VIDEO_ASPECT_4_3 = "4/3"
|
|
VIDEO_ASPECT_1_1 = "1/1"
|
|
|
|
VIDEO_ASPECT_CHOICES = [
|
|
(VIDEO_ASPECT_16_9, "16:9 (widescreen)"),
|
|
(VIDEO_ASPECT_4_3, "4:3 (standard)"),
|
|
(VIDEO_ASPECT_1_1, "1:1 (square)"),
|
|
]
|
|
|
|
VIDEO_UPLOAD_EXTENSIONS = frozenset({".mp4", ".webm", ".ogg", ".mov"})
|
|
|
|
VIDEO_ADMIN_FIELDS = (
|
|
"video_source",
|
|
"video_url",
|
|
"video_file",
|
|
"video_poster",
|
|
"video_size",
|
|
"video_aspect_ratio",
|
|
"video_styled_background",
|
|
)
|
|
|
|
VIDEO_ADMIN_FIELDSET = (
|
|
"Video",
|
|
{
|
|
"fields": VIDEO_ADMIN_FIELDS + ("video_preview",),
|
|
"description": (
|
|
"Choose YouTube link or uploaded file. Set display size and aspect ratio "
|
|
"to control how the preview appears on the site."
|
|
),
|
|
},
|
|
)
|
|
|
|
YOUTUBE_ID_PATTERNS = (
|
|
re.compile(r"(?:youtube\.com/watch\?(?:[^&]+&)*v=|youtube\.com/embed/|youtube\.com/shorts/|youtu\.be/)([\w-]{11})"),
|
|
re.compile(r"^([\w-]{11})$"),
|
|
)
|
|
|
|
|
|
def parse_youtube_video_id(url):
|
|
if not url:
|
|
return ""
|
|
value = url.strip()
|
|
for pattern in YOUTUBE_ID_PATTERNS:
|
|
match = pattern.search(value)
|
|
if match:
|
|
return match.group(1)
|
|
return ""
|
|
|
|
|
|
def youtube_embed_url(url):
|
|
video_id = parse_youtube_video_id(url)
|
|
if not video_id:
|
|
return ""
|
|
return f"https://www.youtube-nocookie.com/embed/{video_id}"
|
|
|
|
|
|
class VideoBlockMixin(models.Model):
|
|
video_source = models.CharField(
|
|
max_length=20,
|
|
choices=VIDEO_SOURCE_CHOICES,
|
|
default=VIDEO_SOURCE_YOUTUBE,
|
|
blank=True,
|
|
)
|
|
video_url = models.CharField(
|
|
max_length=500,
|
|
blank=True,
|
|
help_text="YouTube watch, embed, or youtu.be link.",
|
|
)
|
|
video_file = models.FileField(
|
|
upload_to="videos/",
|
|
blank=True,
|
|
help_text="MP4, WebM, OGG, or MOV file.",
|
|
)
|
|
video_poster = models.ImageField(
|
|
upload_to="videos/posters/",
|
|
blank=True,
|
|
null=True,
|
|
help_text="Optional thumbnail shown before an uploaded video plays.",
|
|
)
|
|
video_size = models.CharField(
|
|
max_length=10,
|
|
choices=VIDEO_SIZE_CHOICES,
|
|
default=VIDEO_SIZE_MEDIUM,
|
|
)
|
|
video_aspect_ratio = models.CharField(
|
|
max_length=10,
|
|
choices=VIDEO_ASPECT_CHOICES,
|
|
default=VIDEO_ASPECT_16_9,
|
|
)
|
|
video_styled_background = models.BooleanField(
|
|
default=False,
|
|
help_text="Glass panel with ambient glow (similar to the Downloads section).",
|
|
)
|
|
description_format = models.CharField(
|
|
max_length=20,
|
|
choices=CONTENT_FORMAT_CHOICES,
|
|
default=FORMAT_PLAIN,
|
|
)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
@property
|
|
def youtube_embed_url(self):
|
|
return youtube_embed_url(self.video_url)
|
|
|
|
@property
|
|
def has_video(self):
|
|
if self.video_source == VIDEO_SOURCE_UPLOAD:
|
|
return bool(self.video_file)
|
|
return bool(self.youtube_embed_url)
|
|
|
|
@property
|
|
def video_size_class(self):
|
|
return f"video-block--{self.video_size or VIDEO_SIZE_MEDIUM}"
|
|
|
|
@property
|
|
def video_aspect_class(self):
|
|
ratio = (self.video_aspect_ratio or VIDEO_ASPECT_16_9).replace("/", "-")
|
|
return f"video-block--ratio-{ratio}"
|
|
|
|
@property
|
|
def rendered_description(self):
|
|
description = getattr(self, "description", "") or ""
|
|
return render_content(description, self.description_format)
|
|
|
|
def clean_video_fields(self, require=False):
|
|
if not require and not self.video_url and not self.video_file:
|
|
return
|
|
if self.video_source == VIDEO_SOURCE_YOUTUBE:
|
|
if not self.video_url.strip():
|
|
raise ValidationError({"video_url": "Enter a YouTube link."})
|
|
if not self.youtube_embed_url:
|
|
raise ValidationError({"video_url": "Enter a valid YouTube link."})
|
|
elif self.video_source == VIDEO_SOURCE_UPLOAD:
|
|
if not self.video_file:
|
|
raise ValidationError({"video_file": "Upload a video file."})
|
|
extension = self.video_file.name.rsplit(".", 1)[-1].lower() if self.video_file.name else ""
|
|
if f".{extension}" not in VIDEO_UPLOAD_EXTENSIONS:
|
|
raise ValidationError(
|
|
{
|
|
"video_file": "Unsupported format. Use MP4, WebM, OGG, or MOV.",
|
|
}
|
|
)
|