feat: Video Trimmer and more

This commit is contained in:
Markos Gogoulos
2025-06-11 14:48:30 +03:00
committed by GitHub
parent d34fc328bf
commit b28c2d8271
124 changed files with 15696 additions and 586 deletions

View File

@@ -1,3 +1,4 @@
import json
from datetime import datetime, timedelta
from allauth.socialaccount.models import SocialApp
@@ -7,9 +8,10 @@ from django.contrib.auth.decorators import login_required
from django.contrib.postgres.search import SearchQuery
from django.core.mail import EmailMessage
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from drf_yasg import openapi as openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
@@ -36,14 +38,22 @@ from cms.version import VERSION
from identity_providers.models import LoginOption
from users.models import User
from .forms import ContactForm, EditSubtitleForm, MediaForm, SubtitleForm
from . import helpers
from .forms import (
ContactForm,
EditSubtitleForm,
MediaMetadataForm,
MediaPublishForm,
SubtitleForm,
)
from .frontend_translations import translate_string
from .helpers import clean_query, get_alphanumeric_only, produce_ffmpeg_commands
from .methods import (
check_comment_for_mention,
create_video_trim_request,
get_user_or_session,
handle_video_chapters,
is_mediacms_editor,
is_mediacms_manager,
list_tasks,
notify_user_on_comment,
show_recommended_media,
@@ -60,6 +70,7 @@ from .models import (
PlaylistMedia,
Subtitle,
Tag,
VideoTrimRequest,
)
from .serializers import (
CategorySerializer,
@@ -73,7 +84,7 @@ from .serializers import (
TagSerializer,
)
from .stop_words import STOP_WORDS
from .tasks import save_user_action
from .tasks import save_user_action, video_trim_task
VALID_USER_ACTIONS = [action for action, name in USER_MEDIA_ACTIONS]
@@ -103,7 +114,7 @@ def add_subtitle(request):
if not media:
return HttpResponseRedirect("/")
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
if request.method == "POST":
@@ -138,7 +149,7 @@ def edit_subtitle(request):
if not subtitle:
return HttpResponseRedirect("/")
if not (request.user == subtitle.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
if not (request.user == subtitle.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
context = {"subtitle": subtitle, "action": action}
@@ -233,6 +244,43 @@ def history(request):
return render(request, "cms/history.html", context)
@csrf_exempt
@login_required
def video_chapters(request, friendly_token):
# this is not ready...
return False
if not request.method == "POST":
return HttpResponseRedirect("/")
media = Media.objects.filter(friendly_token=friendly_token).first()
if not media:
return HttpResponseRedirect("/")
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
try:
data = json.loads(request.body)["chapters"]
chapters = []
for _, chapter_data in enumerate(data):
start_time = chapter_data.get('start')
title = chapter_data.get('title')
if start_time and title:
chapters.append(
{
'start': start_time,
'title': title,
}
)
except Exception as e: # noqa
return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with start and title'}, status=400)
ret = handle_video_chapters(media, chapters)
return JsonResponse(ret, safe=False)
@login_required
def edit_media(request):
"""Edit a media view"""
@@ -245,10 +293,10 @@ def edit_media(request):
if not media:
return HttpResponseRedirect("/")
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
if request.method == "POST":
form = MediaForm(request.user, request.POST, request.FILES, instance=media)
form = MediaMetadataForm(request.user, request.POST, request.FILES, instance=media)
if form.is_valid():
media = form.save()
for tag in media.tags.all():
@@ -267,11 +315,145 @@ def edit_media(request):
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
return HttpResponseRedirect(media.get_absolute_url())
else:
form = MediaForm(request.user, instance=media)
form = MediaMetadataForm(request.user, instance=media)
return render(
request,
"cms/edit_media.html",
{"form": form, "add_subtitle_url": media.add_subtitle_url},
{"form": form, "media_object": media, "add_subtitle_url": media.add_subtitle_url},
)
@login_required
def publish_media(request):
"""Publish media"""
friendly_token = request.GET.get("m", "").strip()
if not friendly_token:
return HttpResponseRedirect("/")
media = Media.objects.filter(friendly_token=friendly_token).first()
if not media:
return HttpResponseRedirect("/")
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
if request.method == "POST":
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media)
if form.is_valid():
media = form.save()
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
return HttpResponseRedirect(media.get_absolute_url())
else:
form = MediaPublishForm(request.user, instance=media)
return render(
request,
"cms/publish_media.html",
{"form": form, "media_object": media, "add_subtitle_url": media.add_subtitle_url},
)
@login_required
def edit_chapters(request):
"""Edit chapters"""
# not implemented yet
return False
friendly_token = request.GET.get("m", "").strip()
if not friendly_token:
return HttpResponseRedirect("/")
media = Media.objects.filter(friendly_token=friendly_token).first()
if not media:
return HttpResponseRedirect("/")
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
return render(
request,
"cms/edit_chapters.html",
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": helpers.url_from_path(media.media_file.path), "media_id": media.friendly_token},
)
@csrf_exempt
@login_required
def trim_video(request, friendly_token):
if not settings.ALLOW_VIDEO_TRIMMER:
return JsonResponse({"success": False, "error": "Video trimming is not allowed"}, status=400)
if not request.method == "POST":
return HttpResponseRedirect("/")
media = Media.objects.filter(friendly_token=friendly_token).first()
if not media:
return HttpResponseRedirect("/")
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
existing_requests = VideoTrimRequest.objects.filter(media=media, status__in=["initial", "running"]).exists()
if existing_requests:
return JsonResponse({"success": False, "error": "A trim request is already in progress for this video"}, status=400)
try:
data = json.loads(request.body)
video_trim_request = create_video_trim_request(media, data)
video_trim_task.delay(video_trim_request.id)
ret = {"success": True, "request_id": video_trim_request.id}
return JsonResponse(ret, safe=False, status=200)
except Exception as e: # noqa
ret = {"success": False, "error": "Incorrect request data"}
return JsonResponse(ret, safe=False, status=400)
@login_required
def edit_video(request):
"""Edit video"""
friendly_token = request.GET.get("m", "").strip()
if not friendly_token:
return HttpResponseRedirect("/")
media = Media.objects.filter(friendly_token=friendly_token).first()
if not media:
return HttpResponseRedirect("/")
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
if not media.media_type == "video":
messages.add_message(request, messages.INFO, "Media is not video")
return HttpResponseRedirect(media.get_absolute_url())
if not settings.ALLOW_VIDEO_TRIMMER:
messages.add_message(request, messages.INFO, "Video Trimmer is not enabled")
return HttpResponseRedirect(media.get_absolute_url())
# Check if there's a running trim request
running_trim_request = VideoTrimRequest.objects.filter(media=media, status__in=["initial", "running"]).exists()
if running_trim_request:
messages.add_message(request, messages.INFO, "Video trim request is already running")
return HttpResponseRedirect(media.get_absolute_url())
media_file_path = media.trim_video_url
if not media_file_path:
messages.add_message(request, messages.INFO, "Media processing has not finished yet")
return HttpResponseRedirect(media.get_absolute_url())
if media.encoding_status in ["pending", "running"]:
video_msg = "Media encoding hasn't finished yet. Attempting to show the original video file"
messages.add_message(request, messages.INFO, video_msg)
return render(
request,
"cms/edit_video.html",
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": media_file_path},
)
@@ -428,10 +610,22 @@ def view_media(request):
context["CAN_DELETE_COMMENTS"] = False
if request.user.is_authenticated:
if (media.user.id == request.user.id) or is_mediacms_editor(request.user) or is_mediacms_manager(request.user):
if media.user.id == request.user.id or is_mediacms_editor(request.user):
context["CAN_DELETE_MEDIA"] = True
context["CAN_EDIT_MEDIA"] = True
context["CAN_DELETE_COMMENTS"] = True
# in case media is video and is processing (eg the case a video was just uploaded)
# attempt to show it (rather than showing a blank video player)
if media.media_type == 'video':
video_msg = None
if media.encoding_status == "pending":
video_msg = "Media encoding hasn't started yet. Attempting to show the original video file"
if media.encoding_status == "running":
video_msg = "Media encoding is under processing. Attempting to show the original video file"
if video_msg:
messages.add_message(request, messages.INFO, video_msg)
return render(request, "cms/media.html", context)
@@ -621,7 +815,7 @@ class MediaDetail(APIView):
if isinstance(media, Response):
return media
if not (is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
if not is_mediacms_editor(request.user):
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
action = request.data.get("type")
@@ -738,7 +932,7 @@ class MediaActions(APIView):
def get(self, request, friendly_token, format=None):
# show date and reason for each time media was reported
media = self.get_object(friendly_token)
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
if not (request.user == media.user or is_mediacms_editor(request.user)):
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
if isinstance(media, Response):