mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-20 05:36:03 -05:00
feat: bulk actions API
This commit is contained in:
@@ -68,14 +68,18 @@ class MediaMetadataForm(forms.ModelForm):
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_enctype = "multipart/form-data"
|
||||
self.helper.form_show_errors = False
|
||||
self.helper.layout = Layout(
|
||||
|
||||
layout_fields = [
|
||||
CustomField('title'),
|
||||
CustomField('new_tags'),
|
||||
CustomField('add_date'),
|
||||
CustomField('description'),
|
||||
CustomField('uploaded_poster'),
|
||||
CustomField('enable_comments'),
|
||||
)
|
||||
]
|
||||
if self.instance.media_type != "image":
|
||||
layout_fields.append(CustomField('uploaded_poster'))
|
||||
|
||||
self.helper.layout = Layout(*layout_fields)
|
||||
|
||||
if self.instance.media_type == "video":
|
||||
self.helper.layout.append(CustomField('thumbnail_time'))
|
||||
|
||||
@@ -567,3 +567,42 @@ def handle_video_chapters(media, chapters):
|
||||
video_chapter = models.VideoChapterData.objects.create(media=media, data=chapters)
|
||||
|
||||
return media.chapter_data
|
||||
|
||||
|
||||
def change_media_owner(media_id, new_user):
|
||||
"""Change the owner of a media
|
||||
|
||||
Args:
|
||||
media_id: ID of the media to change owner
|
||||
new_user: New user object to set as owner
|
||||
|
||||
Returns:
|
||||
Media object or None if media not found
|
||||
"""
|
||||
media = models.Media.objects.filter(id=media_id).first()
|
||||
if not media:
|
||||
return None
|
||||
|
||||
# Change the owner
|
||||
media.user = new_user
|
||||
media.save(update_fields=["user"])
|
||||
|
||||
# Update any related permissions
|
||||
media_permissions = models.MediaPermission.objects.filter(media=media)
|
||||
for permission in media_permissions:
|
||||
permission.owner_user = new_user
|
||||
permission.save(update_fields=["owner_user"])
|
||||
|
||||
return media
|
||||
|
||||
|
||||
def copy_media(media_id):
|
||||
"""Create a copy of a media
|
||||
|
||||
Args:
|
||||
media_id: ID of the media to copy
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
pass
|
||||
|
||||
29
files/migrations/0011_mediapermission.py
Normal file
29
files/migrations/0011_mediapermission.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.6 on 2025-07-08 19:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('files', '0010_alter_encodeprofile_resolution'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MediaPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('permission', models.CharField(choices=[('viewer', 'Viewer'), ('editor', 'Editor'), ('owner', 'Owner')], max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='files.media')),
|
||||
('owner_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='granted_permissions', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'media')},
|
||||
},
|
||||
),
|
||||
]
|
||||
25
files/models/__init__.py
Normal file
25
files/models/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Import all models for backward compatibility
|
||||
from .category import Category, Tag # noqa: F401
|
||||
from .comment import Comment # noqa: F401
|
||||
from .encoding import EncodeProfile, Encoding # noqa: F401
|
||||
from .license import License # noqa: F401
|
||||
from .media import Media, MediaPermission # noqa: F401
|
||||
from .playlist import Playlist, PlaylistMedia # noqa: F401
|
||||
from .rating import Rating, RatingCategory # noqa: F401
|
||||
from .subtitle import Language, Subtitle # noqa: F401
|
||||
from .utils import CODECS # noqa: F401
|
||||
from .utils import ENCODE_EXTENSIONS # noqa: F401
|
||||
from .utils import ENCODE_EXTENSIONS_KEYS # noqa: F401
|
||||
from .utils import ENCODE_RESOLUTIONS # noqa: F401
|
||||
from .utils import ENCODE_RESOLUTIONS_KEYS # noqa: F401
|
||||
from .utils import MEDIA_ENCODING_STATUS # noqa: F401
|
||||
from .utils import MEDIA_STATES # noqa: F401
|
||||
from .utils import MEDIA_TYPES_SUPPORTED # noqa: F401
|
||||
from .utils import category_thumb_path # noqa: F401
|
||||
from .utils import encoding_media_file_path # noqa: F401
|
||||
from .utils import generate_uid # noqa: F401
|
||||
from .utils import original_media_file_path # noqa: F401
|
||||
from .utils import original_thumbnail_file_path # noqa: F401
|
||||
from .utils import subtitles_file_path # noqa: F401
|
||||
from .utils import validate_rating # noqa: F401
|
||||
from .video_data import VideoChapterData, VideoTrimRequest # noqa: F401
|
||||
156
files/models/category.py
Normal file
156
files/models/category.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFit
|
||||
|
||||
from .. import helpers
|
||||
from .utils import category_thumb_path, generate_uid
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""A Category base model"""
|
||||
|
||||
uid = models.CharField(unique=True, max_length=36, default=generate_uid)
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
is_global = models.BooleanField(default=False, help_text="global categories or user specific")
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
thumbnail = ProcessedImageField(
|
||||
upload_to=category_thumb_path,
|
||||
processors=[ResizeToFit(width=344, height=None)],
|
||||
format="JPEG",
|
||||
options={"quality": 85},
|
||||
blank=True,
|
||||
)
|
||||
|
||||
listings_thumbnail = models.CharField(max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings")
|
||||
|
||||
is_rbac_category = models.BooleanField(default=False, db_index=True, help_text='If access to Category is controlled by role based membership of Groups')
|
||||
|
||||
identity_provider = models.ForeignKey(
|
||||
'socialaccount.SocialApp',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='categories',
|
||||
help_text='If category is related with a specific Identity Provider',
|
||||
verbose_name='IDP Config Name',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
verbose_name_plural = "Categories"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?c={self.title}"
|
||||
|
||||
def update_category_media(self):
|
||||
"""Set media_count"""
|
||||
|
||||
# Always set number of Category the total number of media
|
||||
# Depending on how RBAC is set and Permissions etc it is
|
||||
# possible that users won't see all media in a Category
|
||||
# but it's worth to handle this on the UI level
|
||||
# (eg through a message that says that you see only files you have permissions to see)
|
||||
|
||||
self.media_count = Media.objects.filter(category=self).count()
|
||||
self.save(update_fields=["media_count"])
|
||||
|
||||
# OLD logic
|
||||
# if getattr(settings, 'USE_RBAC', False) and self.is_rbac_category:
|
||||
# self.media_count = Media.objects.filter(category=self).count()
|
||||
# else:
|
||||
# self.media_count = Media.objects.filter(listable=True, category=self).count()
|
||||
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
"""Return thumbnail for category
|
||||
prioritize processed value of listings_thumbnail
|
||||
then thumbnail
|
||||
"""
|
||||
|
||||
if self.thumbnail:
|
||||
return helpers.url_from_path(self.thumbnail.path)
|
||||
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
|
||||
if Media.objects.filter(category=self, state="public").exists():
|
||||
media = Media.objects.filter(category=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
super(Category, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
"""A Tag model"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True, db_index=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
listings_thumbnail = models.CharField(
|
||||
max_length=400,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Thumbnail to show on listings",
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?t={self.title}"
|
||||
|
||||
def update_tag_media(self):
|
||||
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.title = helpers.get_alphanumeric_only(self.title)
|
||||
self.title = self.title[:99]
|
||||
super(Tag, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
media = Media.objects.filter(tags=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Import Media to avoid circular imports
|
||||
from .media import Media # noqa
|
||||
46
files/models/comment.py
Normal file
46
files/models/comment.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
|
||||
class Comment(MPTTModel):
|
||||
"""Comments model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, db_index=True, related_name="comments")
|
||||
|
||||
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
|
||||
|
||||
text = models.TextField(help_text="text")
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True)
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ["add_date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"On {self.media.title} by {self.user.username}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["text"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
|
||||
if self.text:
|
||||
self.text = self.text[: settings.MAX_CHARS_FOR_COMMENT]
|
||||
|
||||
super(Comment, self).save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('get_media')}?m={self.media.friendly_token}"
|
||||
|
||||
@property
|
||||
def media_url(self):
|
||||
return self.get_absolute_url()
|
||||
303
files/models/encoding.py
Normal file
303
files/models/encoding.py
Normal file
@@ -0,0 +1,303 @@
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
|
||||
from .. import helpers
|
||||
from .utils import (
|
||||
CODECS,
|
||||
ENCODE_EXTENSIONS,
|
||||
ENCODE_RESOLUTIONS,
|
||||
MEDIA_ENCODING_STATUS,
|
||||
encoding_media_file_path,
|
||||
)
|
||||
|
||||
|
||||
class EncodeProfile(models.Model):
|
||||
"""Encode Profile model
|
||||
keeps information for each profile
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=90)
|
||||
|
||||
extension = models.CharField(max_length=10, choices=ENCODE_EXTENSIONS)
|
||||
|
||||
resolution = models.IntegerField(choices=ENCODE_RESOLUTIONS, blank=True, null=True)
|
||||
|
||||
codec = models.CharField(max_length=10, choices=CODECS, blank=True, null=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ["resolution"]
|
||||
|
||||
|
||||
class Encoding(models.Model):
|
||||
"""Encoding Media Instances"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
commands = models.TextField(blank=True, help_text="commands run")
|
||||
|
||||
chunk = models.BooleanField(default=False, db_index=True, help_text="is chunk?")
|
||||
|
||||
chunk_file_path = models.CharField(max_length=400, blank=True)
|
||||
|
||||
chunks_info = models.TextField(blank=True)
|
||||
|
||||
logs = models.TextField(blank=True)
|
||||
|
||||
md5sum = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="encodings")
|
||||
|
||||
media_file = models.FileField("encoding file", upload_to=encoding_media_file_path, blank=True, max_length=500)
|
||||
|
||||
profile = models.ForeignKey(EncodeProfile, on_delete=models.CASCADE)
|
||||
|
||||
progress = models.PositiveSmallIntegerField(default=0)
|
||||
|
||||
update_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
retries = models.IntegerField(default=0)
|
||||
|
||||
size = models.CharField(max_length=20, blank=True)
|
||||
|
||||
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending")
|
||||
|
||||
temp_file = models.CharField(max_length=400, blank=True)
|
||||
|
||||
task_id = models.CharField(max_length=100, blank=True)
|
||||
|
||||
total_run_time = models.IntegerField(default=0)
|
||||
|
||||
worker = models.CharField(max_length=100, blank=True)
|
||||
|
||||
@property
|
||||
def media_encoding_url(self):
|
||||
if self.media_file:
|
||||
return helpers.url_from_path(self.media_file.path)
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_chunk_url(self):
|
||||
if self.chunk_file_path:
|
||||
return helpers.url_from_path(self.chunk_file_path)
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
self.size = helpers.show_file_size(size)
|
||||
if self.chunk_file_path and not self.md5sum:
|
||||
cmd = ["md5sum", self.chunk_file_path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
md5sum = stdout.strip().split()[0]
|
||||
self.md5sum = md5sum
|
||||
|
||||
super(Encoding, self).save(*args, **kwargs)
|
||||
|
||||
def update_size_without_save(self):
|
||||
"""Update the size of an encoding without saving to avoid calling signals"""
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
size = helpers.show_file_size(size)
|
||||
Encoding.objects.filter(pk=self.pk).update(size=size)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_progress(self, progress, commit=True):
|
||||
if isinstance(progress, int):
|
||||
if 0 <= progress <= 100:
|
||||
self.progress = progress
|
||||
# save object with filter update
|
||||
# to avoid calling signals
|
||||
Encoding.objects.filter(pk=self.pk).update(progress=progress)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.profile.name}-{self.media.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
||||
|
||||
|
||||
@receiver(post_save, sender=Encoding)
|
||||
def encoding_file_save(sender, instance, created, **kwargs):
|
||||
"""Performs actions on encoding file delete
|
||||
For example, if encoding is a chunk file, with encoding_status success,
|
||||
perform a check if this is the final chunk file of a media, then
|
||||
concatenate chunks, create final encoding file and delete chunks
|
||||
"""
|
||||
|
||||
if instance.chunk and instance.status == "success":
|
||||
# a chunk got completed
|
||||
|
||||
# check if all chunks are OK
|
||||
# then concatenate to new Encoding - and remove chunks
|
||||
# this should run only once!
|
||||
if instance.media_file:
|
||||
try:
|
||||
orig_chunks = json.loads(instance.chunks_info).keys()
|
||||
except BaseException:
|
||||
instance.delete()
|
||||
return False
|
||||
|
||||
chunks = Encoding.objects.filter(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
chunk=True,
|
||||
).order_by("add_date")
|
||||
|
||||
complete = True
|
||||
|
||||
# perform validation, make sure everything is there
|
||||
for chunk in orig_chunks:
|
||||
if not chunks.filter(chunk_file_path=chunk):
|
||||
complete = False
|
||||
break
|
||||
|
||||
for chunk in chunks:
|
||||
if not (chunk.media_file and chunk.media_file.path):
|
||||
complete = False
|
||||
break
|
||||
|
||||
if complete:
|
||||
# concatenate chunks and create final encoding file
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
||||
tf = helpers.create_temp_file(suffix=f".{instance.profile.extension}", dir=temp_dir)
|
||||
with open(seg_file, "w") as ff:
|
||||
for f in chunks_paths:
|
||||
ff.write(f"file {f}\n")
|
||||
cmd = [
|
||||
settings.FFMPEG_COMMAND,
|
||||
"-y",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
seg_file,
|
||||
"-c",
|
||||
"copy",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"faststart",
|
||||
tf,
|
||||
]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
encoding = Encoding(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
status="success",
|
||||
progress=100,
|
||||
)
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{stdout}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
with open(tf, "rb") as f:
|
||||
myfile = File(f)
|
||||
output_name = f"{helpers.get_file_name(instance.media.media_file.path)}.{instance.profile.extension}"
|
||||
encoding.media_file.save(content=myfile, name=output_name)
|
||||
|
||||
# encoding is saved, deleting chunks
|
||||
# and any other encoding that might exist
|
||||
# first perform one last validation
|
||||
# to avoid that this is run twice
|
||||
if (
|
||||
len(orig_chunks)
|
||||
== Encoding.objects.filter( # noqa
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
).count()
|
||||
):
|
||||
# if two chunks are finished at the same time, this
|
||||
# will be changed
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
who.delete()
|
||||
else:
|
||||
encoding.delete()
|
||||
if not Encoding.objects.filter(chunks_info=instance.chunks_info):
|
||||
# TODO: in case of remote workers, files should be deleted
|
||||
# example
|
||||
# for worker in workers:
|
||||
# for chunk in json.loads(instance.chunks_info).keys():
|
||||
# remove_media_file.delay(media_file=chunk)
|
||||
for chunk in json.loads(instance.chunks_info).keys():
|
||||
helpers.rm_file(chunk)
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
elif instance.chunk and instance.status == "fail":
|
||||
encoding = Encoding(media=instance.media, profile=instance.profile, status="fail", progress=100)
|
||||
|
||||
chunks = Encoding.objects.filter(media=instance.media, chunks_info=instance.chunks_info, chunk=True).order_by("add_date")
|
||||
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
|
||||
who.delete()
|
||||
# TODO: merge with above if, do not repeat code
|
||||
else:
|
||||
if instance.status in ["fail", "success"]:
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
|
||||
if ("running" in encodings) or ("pending" in encodings):
|
||||
return
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Encoding)
|
||||
def encoding_file_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
when corresponding `Encoding` object is deleted.
|
||||
"""
|
||||
|
||||
if instance.media_file:
|
||||
helpers.rm_file(instance.media_file.path)
|
||||
if not instance.chunk:
|
||||
instance.media.post_encode_actions(encoding=instance, action="delete")
|
||||
# delete local chunks, and remote chunks + media file. Only when the
|
||||
# last encoding of a media is complete
|
||||
11
files/models/license.py
Normal file
11
files/models/license.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
"""A Base license model to be used in Media"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -3,15 +3,12 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
import m3u8
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVectorField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db.models import Func, Value
|
||||
@@ -19,108 +16,25 @@ from django.db.models.signals import m2m_changed, post_delete, post_save, pre_de
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.html import strip_tags
|
||||
from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFit
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from . import helpers
|
||||
from .stop_words import STOP_WORDS
|
||||
from .. import helpers
|
||||
from ..stop_words import STOP_WORDS
|
||||
from .encoding import EncodeProfile, Encoding
|
||||
from .utils import (
|
||||
ENCODE_RESOLUTIONS_KEYS,
|
||||
MEDIA_ENCODING_STATUS,
|
||||
MEDIA_STATES,
|
||||
MEDIA_TYPES_SUPPORTED,
|
||||
original_media_file_path,
|
||||
original_thumbnail_file_path,
|
||||
)
|
||||
from .video_data import VideoTrimRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RE_TIMECODE = re.compile(r"(\d+:\d+:\d+.\d+)")
|
||||
|
||||
# this is used by Media and Encoding models
|
||||
# reflects media encoding status for objects
|
||||
MEDIA_ENCODING_STATUS = (
|
||||
("pending", "Pending"),
|
||||
("running", "Running"),
|
||||
("fail", "Fail"),
|
||||
("success", "Success"),
|
||||
)
|
||||
|
||||
# the media state of a Media object
|
||||
# this is set by default according to the portal workflow
|
||||
MEDIA_STATES = (
|
||||
("private", "Private"),
|
||||
("public", "Public"),
|
||||
("unlisted", "Unlisted"),
|
||||
)
|
||||
|
||||
# each uploaded Media gets a media_type hint
|
||||
# by helpers.get_file_type
|
||||
|
||||
MEDIA_TYPES_SUPPORTED = (
|
||||
("video", "Video"),
|
||||
("image", "Image"),
|
||||
("pdf", "Pdf"),
|
||||
("audio", "Audio"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS = (
|
||||
("mp4", "mp4"),
|
||||
("webm", "webm"),
|
||||
("gif", "gif"),
|
||||
)
|
||||
|
||||
ENCODE_RESOLUTIONS = (
|
||||
(2160, "2160"),
|
||||
(1440, "1440"),
|
||||
(1080, "1080"),
|
||||
(720, "720"),
|
||||
(480, "480"),
|
||||
(360, "360"),
|
||||
(240, "240"),
|
||||
(144, "144"),
|
||||
)
|
||||
|
||||
CODECS = (
|
||||
("h265", "h265"),
|
||||
("h264", "h264"),
|
||||
("vp9", "vp9"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS_KEYS = [extension for extension, name in ENCODE_EXTENSIONS]
|
||||
ENCODE_RESOLUTIONS_KEYS = [resolution for resolution, name in ENCODE_RESOLUTIONS]
|
||||
|
||||
|
||||
def generate_uid():
|
||||
return get_random_string(length=16)
|
||||
|
||||
|
||||
def original_media_file_path(instance, filename):
|
||||
"""Helper function to place original media file"""
|
||||
file_name = f"{instance.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"user/{instance.user.username}/{file_name}"
|
||||
|
||||
|
||||
def encoding_media_file_path(instance, filename):
|
||||
"""Helper function to place encoded media file"""
|
||||
|
||||
file_name = f"{instance.media.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_ENCODING_DIR + f"{instance.profile.id}/{instance.media.user.username}/{file_name}"
|
||||
|
||||
|
||||
def original_thumbnail_file_path(instance, filename):
|
||||
"""Helper function to place original media thumbnail file"""
|
||||
|
||||
return settings.THUMBNAIL_UPLOAD_DIR + f"user/{instance.user.username}/{filename}"
|
||||
|
||||
|
||||
def subtitles_file_path(instance, filename):
|
||||
"""Helper function to place subtitle file"""
|
||||
|
||||
return settings.SUBTITLES_UPLOAD_DIR + f"user/{instance.media.user.username}/{filename}"
|
||||
|
||||
|
||||
def category_thumb_path(instance, filename):
|
||||
"""Helper function to place category thumbnail file"""
|
||||
|
||||
file_name = f"{instance.uid}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"categories/{file_name}"
|
||||
|
||||
|
||||
class Media(models.Model):
|
||||
"""The most important model for MediaCMS"""
|
||||
@@ -138,7 +52,6 @@ class Media(models.Model):
|
||||
null=True,
|
||||
help_text="Media can exist in one or no Channels",
|
||||
)
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
dislikes = models.IntegerField(default=0)
|
||||
@@ -566,7 +479,7 @@ class Media(models.Model):
|
||||
To be used on the video player
|
||||
"""
|
||||
|
||||
from . import tasks
|
||||
from .. import tasks
|
||||
|
||||
tasks.produce_sprite_from_video.delay(self.friendly_token)
|
||||
return True
|
||||
@@ -582,7 +495,7 @@ class Media(models.Model):
|
||||
profiles = EncodeProfile.objects.filter(active=True)
|
||||
profiles = list(profiles)
|
||||
|
||||
from . import tasks
|
||||
from .. import tasks
|
||||
|
||||
# attempt to break media file in chunks
|
||||
if self.duration > settings.CHUNKIZE_VIDEO_DURATION and chunkize:
|
||||
@@ -638,7 +551,7 @@ class Media(models.Model):
|
||||
self.save(update_fields=["encoding_status", "listable", "preview_file_path"])
|
||||
|
||||
if encoding and encoding.status == "success" and encoding.profile.codec == "h264" and action == "add" and not encoding.chunk:
|
||||
from . import tasks
|
||||
from .. import tasks
|
||||
|
||||
tasks.create_hls.delay(self.friendly_token)
|
||||
|
||||
@@ -993,585 +906,26 @@ class Media(models.Model):
|
||||
return data
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
"""A Base license model to be used in Media"""
|
||||
class MediaPermission(models.Model):
|
||||
"""Model to store user permissions for media"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""A Category base model"""
|
||||
|
||||
uid = models.CharField(unique=True, max_length=36, default=generate_uid)
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
is_global = models.BooleanField(default=False, help_text="global categories or user specific")
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
thumbnail = ProcessedImageField(
|
||||
upload_to=category_thumb_path,
|
||||
processors=[ResizeToFit(width=344, height=None)],
|
||||
format="JPEG",
|
||||
options={"quality": 85},
|
||||
blank=True,
|
||||
PERMISSION_CHOICES = (
|
||||
("viewer", "Viewer"),
|
||||
("editor", "Editor"),
|
||||
("owner", "Owner"),
|
||||
)
|
||||
|
||||
listings_thumbnail = models.CharField(max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings")
|
||||
|
||||
is_rbac_category = models.BooleanField(default=False, db_index=True, help_text='If access to Category is controlled by role based membership of Groups')
|
||||
|
||||
identity_provider = models.ForeignKey(
|
||||
'socialaccount.SocialApp',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='categories',
|
||||
help_text='If category is related with a specific Identity Provider',
|
||||
verbose_name='IDP Config Name',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
owner_user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='granted_permissions')
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='permissions')
|
||||
permission = models.CharField(max_length=20, choices=PERMISSION_CHOICES)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
verbose_name_plural = "Categories"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?c={self.title}"
|
||||
|
||||
def update_category_media(self):
|
||||
"""Set media_count"""
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False) and self.is_rbac_category:
|
||||
self.media_count = Media.objects.filter(category=self).count()
|
||||
else:
|
||||
self.media_count = Media.objects.filter(listable=True, category=self).count()
|
||||
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
"""Return thumbnail for category
|
||||
prioritize processed value of listings_thumbnail
|
||||
then thumbnail
|
||||
"""
|
||||
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
if self.thumbnail:
|
||||
return helpers.url_from_path(self.thumbnail.path)
|
||||
|
||||
media = Media.objects.filter(category=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
super(Category, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
"""A Tag model"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True, db_index=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
listings_thumbnail = models.CharField(
|
||||
max_length=400,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Thumbnail to show on listings",
|
||||
db_index=True,
|
||||
)
|
||||
unique_together = ('user', 'media')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?t={self.title}"
|
||||
|
||||
def update_tag_media(self):
|
||||
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.title = helpers.get_alphanumeric_only(self.title)
|
||||
self.title = self.title[:99]
|
||||
super(Tag, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
media = Media.objects.filter(tags=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class EncodeProfile(models.Model):
|
||||
"""Encode Profile model
|
||||
keeps information for each profile
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=90)
|
||||
|
||||
extension = models.CharField(max_length=10, choices=ENCODE_EXTENSIONS)
|
||||
|
||||
resolution = models.IntegerField(choices=ENCODE_RESOLUTIONS, blank=True, null=True)
|
||||
|
||||
codec = models.CharField(max_length=10, choices=CODECS, blank=True, null=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ["resolution"]
|
||||
|
||||
|
||||
class Encoding(models.Model):
|
||||
"""Encoding Media Instances"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
commands = models.TextField(blank=True, help_text="commands run")
|
||||
|
||||
chunk = models.BooleanField(default=False, db_index=True, help_text="is chunk?")
|
||||
|
||||
chunk_file_path = models.CharField(max_length=400, blank=True)
|
||||
|
||||
chunks_info = models.TextField(blank=True)
|
||||
|
||||
logs = models.TextField(blank=True)
|
||||
|
||||
md5sum = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="encodings")
|
||||
|
||||
media_file = models.FileField("encoding file", upload_to=encoding_media_file_path, blank=True, max_length=500)
|
||||
|
||||
profile = models.ForeignKey(EncodeProfile, on_delete=models.CASCADE)
|
||||
|
||||
progress = models.PositiveSmallIntegerField(default=0)
|
||||
|
||||
update_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
retries = models.IntegerField(default=0)
|
||||
|
||||
size = models.CharField(max_length=20, blank=True)
|
||||
|
||||
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending")
|
||||
|
||||
temp_file = models.CharField(max_length=400, blank=True)
|
||||
|
||||
task_id = models.CharField(max_length=100, blank=True)
|
||||
|
||||
total_run_time = models.IntegerField(default=0)
|
||||
|
||||
worker = models.CharField(max_length=100, blank=True)
|
||||
|
||||
@property
|
||||
def media_encoding_url(self):
|
||||
if self.media_file:
|
||||
return helpers.url_from_path(self.media_file.path)
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_chunk_url(self):
|
||||
if self.chunk_file_path:
|
||||
return helpers.url_from_path(self.chunk_file_path)
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
self.size = helpers.show_file_size(size)
|
||||
if self.chunk_file_path and not self.md5sum:
|
||||
cmd = ["md5sum", self.chunk_file_path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
md5sum = stdout.strip().split()[0]
|
||||
self.md5sum = md5sum
|
||||
|
||||
super(Encoding, self).save(*args, **kwargs)
|
||||
|
||||
def update_size_without_save(self):
|
||||
"""Update the size of an encoding without saving to avoid calling signals"""
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
size = helpers.show_file_size(size)
|
||||
Encoding.objects.filter(pk=self.pk).update(size=size)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_progress(self, progress, commit=True):
|
||||
if isinstance(progress, int):
|
||||
if 0 <= progress <= 100:
|
||||
self.progress = progress
|
||||
# save object with filter update
|
||||
# to avoid calling signals
|
||||
Encoding.objects.filter(pk=self.pk).update(progress=progress)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.profile.name}-{self.media.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
"""Language model
|
||||
to be used with Subtitles
|
||||
"""
|
||||
|
||||
code = models.CharField(max_length=12, help_text="language code")
|
||||
|
||||
title = models.CharField(max_length=100, help_text="language code")
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code}-{self.title}"
|
||||
|
||||
|
||||
class Subtitle(models.Model):
|
||||
"""Subtitles model"""
|
||||
|
||||
language = models.ForeignKey(Language, on_delete=models.CASCADE)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="subtitles")
|
||||
|
||||
subtitle_file = models.FileField(
|
||||
"Subtitle/CC file",
|
||||
help_text="File has to be WebVTT format",
|
||||
upload_to=subtitles_file_path,
|
||||
max_length=500,
|
||||
)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ["language__title"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.media.title}-{self.language.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('edit_subtitle')}?id={self.id}"
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def convert_to_srt(self):
|
||||
input_path = self.subtitle_file.path
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
||||
pysub = settings.PYSUBS_COMMAND
|
||||
|
||||
cmd = [pysub, input_path, "--to", "vtt", "-o", tmpdirname]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
list_of_files = os.listdir(tmpdirname)
|
||||
if list_of_files:
|
||||
subtitles_file = os.path.join(tmpdirname, list_of_files[0])
|
||||
cmd = ["cp", subtitles_file, input_path]
|
||||
stdout = helpers.run_command(cmd) # noqa
|
||||
else:
|
||||
raise Exception("Could not convert to srt")
|
||||
return True
|
||||
|
||||
|
||||
class RatingCategory(models.Model):
|
||||
"""Rating Category
|
||||
Facilitate user ratings.
|
||||
One or more rating categories per Category can exist
|
||||
will be shown to the media if they are enabled
|
||||
"""
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
title = models.CharField(max_length=200, unique=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Rating Categories"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
|
||||
def validate_rating(value):
|
||||
if -1 >= value or value > 5:
|
||||
raise ValidationError("score has to be between 0 and 5")
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
"""User Rating"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="ratings")
|
||||
|
||||
rating_category = models.ForeignKey(RatingCategory, on_delete=models.CASCADE)
|
||||
|
||||
score = models.IntegerField(validators=[validate_rating])
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Ratings"
|
||||
indexes = [
|
||||
models.Index(fields=["user", "media"]),
|
||||
]
|
||||
unique_together = ("user", "media", "rating_category")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}, rate for {self.media.title} for category {self.rating_category.title}"
|
||||
|
||||
|
||||
class Playlist(models.Model):
|
||||
"""Playlists model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
friendly_token = models.CharField(blank=True, max_length=12, db_index=True)
|
||||
|
||||
media = models.ManyToManyField(Media, through="playlistmedia", blank=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def media_count(self):
|
||||
return self.media.filter(listable=True).count()
|
||||
|
||||
def get_absolute_url(self, api=False):
|
||||
if api:
|
||||
return reverse("api_get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
else:
|
||||
return reverse("get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
return self.get_absolute_url(api=True)
|
||||
|
||||
def user_thumbnail_url(self):
|
||||
if self.user.logo:
|
||||
return helpers.url_from_path(self.user.logo.path)
|
||||
return None
|
||||
|
||||
def set_ordering(self, media, ordering):
|
||||
if media not in self.media.all():
|
||||
return False
|
||||
pm = PlaylistMedia.objects.filter(playlist=self, media=media).first()
|
||||
if pm and isinstance(ordering, int) and 0 < ordering:
|
||||
pm.ordering = ordering
|
||||
pm.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
self.title = self.title[:99]
|
||||
|
||||
if not self.friendly_token:
|
||||
while True:
|
||||
friendly_token = helpers.produce_friendly_token()
|
||||
if not Playlist.objects.filter(friendly_token=friendly_token):
|
||||
self.friendly_token = friendly_token
|
||||
break
|
||||
super(Playlist, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
pm = self.playlistmedia_set.filter(media__listable=True).first()
|
||||
if pm and pm.media.thumbnail:
|
||||
return helpers.url_from_path(pm.media.thumbnail.path)
|
||||
return None
|
||||
|
||||
|
||||
class PlaylistMedia(models.Model):
|
||||
"""Helper model to store playlist specific media"""
|
||||
|
||||
action_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE)
|
||||
|
||||
playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
|
||||
|
||||
ordering = models.IntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
ordering = ["ordering", "-action_date"]
|
||||
|
||||
|
||||
class Comment(MPTTModel):
|
||||
"""Comments model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, db_index=True, related_name="comments")
|
||||
|
||||
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
|
||||
|
||||
text = models.TextField(help_text="text")
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True)
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ["add_date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"On {self.media.title} by {self.user.username}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["text"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
|
||||
if self.text:
|
||||
self.text = self.text[: settings.MAX_CHARS_FOR_COMMENT]
|
||||
|
||||
super(Comment, self).save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('get_media')}?m={self.media.friendly_token}"
|
||||
|
||||
@property
|
||||
def media_url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
|
||||
class VideoChapterData(models.Model):
|
||||
data = models.JSONField(null=False, blank=False, help_text="Chapter data")
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='chapters')
|
||||
|
||||
class Meta:
|
||||
unique_together = ['media']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from . import tasks
|
||||
|
||||
is_new = self.pk is None
|
||||
if is_new or (not is_new and self._check_data_changed()):
|
||||
super().save(*args, **kwargs)
|
||||
tasks.produce_video_chapters.delay(self.pk)
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _check_data_changed(self):
|
||||
if self.pk:
|
||||
old_instance = VideoChapterData.objects.get(pk=self.pk)
|
||||
return old_instance.data != self.data
|
||||
return False
|
||||
|
||||
@property
|
||||
def chapter_data(self):
|
||||
# ensure response is consistent
|
||||
data = []
|
||||
for item in self.data:
|
||||
if item.get("start") and item.get("title"):
|
||||
thumbnail = item.get("thumbnail")
|
||||
if thumbnail:
|
||||
thumbnail = helpers.url_from_path(thumbnail)
|
||||
else:
|
||||
thumbnail = "static/images/chapter_default.jpg"
|
||||
data.append(
|
||||
{
|
||||
"start": item.get("start"),
|
||||
"title": item.get("title"),
|
||||
"thumbnail": thumbnail,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class VideoTrimRequest(models.Model):
|
||||
"""Model to handle video trimming requests"""
|
||||
|
||||
VIDEO_TRIM_STATUS = (
|
||||
("initial", "Initial"),
|
||||
("running", "Running"),
|
||||
("success", "Success"),
|
||||
("fail", "Fail"),
|
||||
)
|
||||
|
||||
VIDEO_ACTION_CHOICES = (
|
||||
("replace", "Replace Original"),
|
||||
("save_new", "Save as New"),
|
||||
("create_segments", "Create Segments"),
|
||||
)
|
||||
|
||||
TRIM_STYLE_CHOICES = (
|
||||
("no_encoding", "No Encoding"),
|
||||
("precise", "Precise"),
|
||||
)
|
||||
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='trim_requests')
|
||||
status = models.CharField(max_length=20, choices=VIDEO_TRIM_STATUS, default="initial")
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
video_action = models.CharField(max_length=20, choices=VIDEO_ACTION_CHOICES)
|
||||
media_trim_style = models.CharField(max_length=20, choices=TRIM_STYLE_CHOICES, default="no_encoding")
|
||||
timestamps = models.JSONField(null=False, blank=False, help_text="Timestamps for trimming")
|
||||
|
||||
def __str__(self):
|
||||
return f"Trim request for {self.media.title} ({self.status})"
|
||||
return f"{self.user.username} - {self.media.title} ({self.permission})"
|
||||
|
||||
|
||||
@receiver(post_save, sender=Media)
|
||||
@@ -1585,7 +939,7 @@ def media_save(sender, instance, created, **kwargs):
|
||||
return False
|
||||
|
||||
if created:
|
||||
from .methods import notify_users
|
||||
from ..methods import notify_users
|
||||
|
||||
instance.media_init()
|
||||
notify_users(friendly_token=instance.friendly_token, action="media_added")
|
||||
@@ -1616,11 +970,6 @@ def media_file_pre_delete(sender, instance, **kwargs):
|
||||
tag.update_tag_media()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=VideoChapterData)
|
||||
def videochapterdata_delete(sender, instance, **kwargs):
|
||||
helpers.rm_dir(instance.media.video_chapters_folder)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Media)
|
||||
def media_file_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
@@ -1661,166 +1010,3 @@ def media_m2m(sender, instance, **kwargs):
|
||||
if instance.tags.all():
|
||||
for tag in instance.tags.all():
|
||||
tag.update_tag_media()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Encoding)
|
||||
def encoding_file_save(sender, instance, created, **kwargs):
|
||||
"""Performs actions on encoding file delete
|
||||
For example, if encoding is a chunk file, with encoding_status success,
|
||||
perform a check if this is the final chunk file of a media, then
|
||||
concatenate chunks, create final encoding file and delete chunks
|
||||
"""
|
||||
|
||||
if instance.chunk and instance.status == "success":
|
||||
# a chunk got completed
|
||||
|
||||
# check if all chunks are OK
|
||||
# then concatenate to new Encoding - and remove chunks
|
||||
# this should run only once!
|
||||
if instance.media_file:
|
||||
try:
|
||||
orig_chunks = json.loads(instance.chunks_info).keys()
|
||||
except BaseException:
|
||||
instance.delete()
|
||||
return False
|
||||
|
||||
chunks = Encoding.objects.filter(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
chunk=True,
|
||||
).order_by("add_date")
|
||||
|
||||
complete = True
|
||||
|
||||
# perform validation, make sure everything is there
|
||||
for chunk in orig_chunks:
|
||||
if not chunks.filter(chunk_file_path=chunk):
|
||||
complete = False
|
||||
break
|
||||
|
||||
for chunk in chunks:
|
||||
if not (chunk.media_file and chunk.media_file.path):
|
||||
complete = False
|
||||
break
|
||||
|
||||
if complete:
|
||||
# concatenate chunks and create final encoding file
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
||||
tf = helpers.create_temp_file(suffix=f".{instance.profile.extension}", dir=temp_dir)
|
||||
with open(seg_file, "w") as ff:
|
||||
for f in chunks_paths:
|
||||
ff.write(f"file {f}\n")
|
||||
cmd = [
|
||||
settings.FFMPEG_COMMAND,
|
||||
"-y",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
seg_file,
|
||||
"-c",
|
||||
"copy",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"faststart",
|
||||
tf,
|
||||
]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
encoding = Encoding(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
status="success",
|
||||
progress=100,
|
||||
)
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{stdout}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
with open(tf, "rb") as f:
|
||||
myfile = File(f)
|
||||
output_name = f"{helpers.get_file_name(instance.media.media_file.path)}.{instance.profile.extension}"
|
||||
encoding.media_file.save(content=myfile, name=output_name)
|
||||
|
||||
# encoding is saved, deleting chunks
|
||||
# and any other encoding that might exist
|
||||
# first perform one last validation
|
||||
# to avoid that this is run twice
|
||||
if (
|
||||
len(orig_chunks)
|
||||
== Encoding.objects.filter( # noqa
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
).count()
|
||||
):
|
||||
# if two chunks are finished at the same time, this
|
||||
# will be changed
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
who.delete()
|
||||
else:
|
||||
encoding.delete()
|
||||
if not Encoding.objects.filter(chunks_info=instance.chunks_info):
|
||||
# TODO: in case of remote workers, files should be deleted
|
||||
# example
|
||||
# for worker in workers:
|
||||
# for chunk in json.loads(instance.chunks_info).keys():
|
||||
# remove_media_file.delay(media_file=chunk)
|
||||
for chunk in json.loads(instance.chunks_info).keys():
|
||||
helpers.rm_file(chunk)
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
elif instance.chunk and instance.status == "fail":
|
||||
encoding = Encoding(media=instance.media, profile=instance.profile, status="fail", progress=100)
|
||||
|
||||
chunks = Encoding.objects.filter(media=instance.media, chunks_info=instance.chunks_info, chunk=True).order_by("add_date")
|
||||
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
|
||||
who.delete()
|
||||
# TODO: merge with above if, do not repeat code
|
||||
else:
|
||||
if instance.status in ["fail", "success"]:
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
|
||||
if ("running" in encodings) or ("pending" in encodings):
|
||||
return
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Encoding)
|
||||
def encoding_file_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
when corresponding `Encoding` object is deleted.
|
||||
"""
|
||||
|
||||
if instance.media_file:
|
||||
helpers.rm_file(instance.media_file.path)
|
||||
if not instance.chunk:
|
||||
instance.media.post_encode_actions(encoding=instance, action="delete")
|
||||
# delete local chunks, and remote chunks + media file. Only when the
|
||||
# last encoding of a media is complete
|
||||
97
files/models/playlist.py
Normal file
97
files/models/playlist.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from .. import helpers
|
||||
|
||||
|
||||
class Playlist(models.Model):
|
||||
"""Playlists model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
friendly_token = models.CharField(blank=True, max_length=12, db_index=True)
|
||||
|
||||
media = models.ManyToManyField("Media", through="playlistmedia", blank=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def media_count(self):
|
||||
return self.media.filter(listable=True).count()
|
||||
|
||||
def get_absolute_url(self, api=False):
|
||||
if api:
|
||||
return reverse("api_get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
else:
|
||||
return reverse("get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
return self.get_absolute_url(api=True)
|
||||
|
||||
def user_thumbnail_url(self):
|
||||
if self.user.logo:
|
||||
return helpers.url_from_path(self.user.logo.path)
|
||||
return None
|
||||
|
||||
def set_ordering(self, media, ordering):
|
||||
if media not in self.media.all():
|
||||
return False
|
||||
pm = PlaylistMedia.objects.filter(playlist=self, media=media).first()
|
||||
if pm and isinstance(ordering, int) and 0 < ordering:
|
||||
pm.ordering = ordering
|
||||
pm.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
self.title = self.title[:99]
|
||||
|
||||
if not self.friendly_token:
|
||||
while True:
|
||||
friendly_token = helpers.produce_friendly_token()
|
||||
if not Playlist.objects.filter(friendly_token=friendly_token):
|
||||
self.friendly_token = friendly_token
|
||||
break
|
||||
super(Playlist, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
pm = self.playlistmedia_set.filter(media__listable=True).first()
|
||||
if pm and pm.media.thumbnail:
|
||||
return helpers.url_from_path(pm.media.thumbnail.path)
|
||||
return None
|
||||
|
||||
|
||||
class PlaylistMedia(models.Model):
|
||||
"""Helper model to store playlist specific media"""
|
||||
|
||||
action_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE)
|
||||
|
||||
playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
|
||||
|
||||
ordering = models.IntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
ordering = ["ordering", "-action_date"]
|
||||
47
files/models/rating.py
Normal file
47
files/models/rating.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.db import models
|
||||
|
||||
from .utils import validate_rating
|
||||
|
||||
|
||||
class RatingCategory(models.Model):
|
||||
"""Rating Category
|
||||
Facilitate user ratings.
|
||||
One or more rating categories per Category can exist
|
||||
will be shown to the media if they are enabled
|
||||
"""
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
title = models.CharField(max_length=200, unique=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Rating Categories"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
"""User Rating"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="ratings")
|
||||
|
||||
rating_category = models.ForeignKey(RatingCategory, on_delete=models.CASCADE)
|
||||
|
||||
score = models.IntegerField(validators=[validate_rating])
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Ratings"
|
||||
indexes = [
|
||||
models.Index(fields=["user", "media"]),
|
||||
]
|
||||
unique_together = ("user", "media", "rating_category")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}, rate for {self.media.title} for category {self.rating_category.title}"
|
||||
72
files/models/subtitle.py
Normal file
72
files/models/subtitle.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from .. import helpers
|
||||
from .utils import subtitles_file_path
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
"""Language model
|
||||
to be used with Subtitles
|
||||
"""
|
||||
|
||||
code = models.CharField(max_length=12, help_text="language code")
|
||||
|
||||
title = models.CharField(max_length=100, help_text="language code")
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code}-{self.title}"
|
||||
|
||||
|
||||
class Subtitle(models.Model):
|
||||
"""Subtitles model"""
|
||||
|
||||
language = models.ForeignKey(Language, on_delete=models.CASCADE)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="subtitles")
|
||||
|
||||
subtitle_file = models.FileField(
|
||||
"Subtitle/CC file",
|
||||
help_text="File has to be WebVTT format",
|
||||
upload_to=subtitles_file_path,
|
||||
max_length=500,
|
||||
)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ["language__title"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.media.title}-{self.language.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('edit_subtitle')}?id={self.id}"
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def convert_to_srt(self):
|
||||
input_path = self.subtitle_file.path
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
||||
pysub = settings.PYSUBS_COMMAND
|
||||
|
||||
cmd = [pysub, input_path, "--to", "vtt", "-o", tmpdirname]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
list_of_files = os.listdir(tmpdirname)
|
||||
if list_of_files:
|
||||
subtitles_file = os.path.join(tmpdirname, list_of_files[0])
|
||||
cmd = ["cp", subtitles_file, input_path]
|
||||
stdout = helpers.run_command(cmd) # noqa
|
||||
else:
|
||||
raise Exception("Could not convert to srt")
|
||||
return True
|
||||
99
files/models/utils.py
Normal file
99
files/models/utils.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
from .. import helpers
|
||||
|
||||
# this is used by Media and Encoding models
|
||||
# reflects media encoding status for objects
|
||||
MEDIA_ENCODING_STATUS = (
|
||||
("pending", "Pending"),
|
||||
("running", "Running"),
|
||||
("fail", "Fail"),
|
||||
("success", "Success"),
|
||||
)
|
||||
|
||||
# the media state of a Media object
|
||||
# this is set by default according to the portal workflow
|
||||
MEDIA_STATES = (
|
||||
("private", "Private"),
|
||||
("public", "Public"),
|
||||
("unlisted", "Unlisted"),
|
||||
)
|
||||
|
||||
# each uploaded Media gets a media_type hint
|
||||
# by helpers.get_file_type
|
||||
|
||||
MEDIA_TYPES_SUPPORTED = (
|
||||
("video", "Video"),
|
||||
("image", "Image"),
|
||||
("pdf", "Pdf"),
|
||||
("audio", "Audio"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS = (
|
||||
("mp4", "mp4"),
|
||||
("webm", "webm"),
|
||||
("gif", "gif"),
|
||||
)
|
||||
|
||||
ENCODE_RESOLUTIONS = (
|
||||
(2160, "2160"),
|
||||
(1440, "1440"),
|
||||
(1080, "1080"),
|
||||
(720, "720"),
|
||||
(480, "480"),
|
||||
(360, "360"),
|
||||
(240, "240"),
|
||||
(144, "144"),
|
||||
)
|
||||
|
||||
CODECS = (
|
||||
("h265", "h265"),
|
||||
("h264", "h264"),
|
||||
("vp9", "vp9"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS_KEYS = [extension for extension, name in ENCODE_EXTENSIONS]
|
||||
ENCODE_RESOLUTIONS_KEYS = [resolution for resolution, name in ENCODE_RESOLUTIONS]
|
||||
|
||||
|
||||
def generate_uid():
|
||||
return get_random_string(length=16)
|
||||
|
||||
|
||||
def original_media_file_path(instance, filename):
|
||||
"""Helper function to place original media file"""
|
||||
file_name = f"{instance.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"user/{instance.user.username}/{file_name}"
|
||||
|
||||
|
||||
def encoding_media_file_path(instance, filename):
|
||||
"""Helper function to place encoded media file"""
|
||||
|
||||
file_name = f"{instance.media.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_ENCODING_DIR + f"{instance.profile.id}/{instance.media.user.username}/{file_name}"
|
||||
|
||||
|
||||
def original_thumbnail_file_path(instance, filename):
|
||||
"""Helper function to place original media thumbnail file"""
|
||||
|
||||
return settings.THUMBNAIL_UPLOAD_DIR + f"user/{instance.user.username}/{filename}"
|
||||
|
||||
|
||||
def subtitles_file_path(instance, filename):
|
||||
"""Helper function to place subtitle file"""
|
||||
|
||||
return settings.SUBTITLES_UPLOAD_DIR + f"user/{instance.media.user.username}/{filename}"
|
||||
|
||||
|
||||
def category_thumb_path(instance, filename):
|
||||
"""Helper function to place category thumbnail file"""
|
||||
|
||||
file_name = f"{instance.uid}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"categories/{file_name}"
|
||||
|
||||
|
||||
def validate_rating(value):
|
||||
if -1 >= value or value > 5:
|
||||
raise ValidationError("score has to be between 0 and 5")
|
||||
86
files/models/video_data.py
Normal file
86
files/models/video_data.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .. import helpers
|
||||
|
||||
|
||||
class VideoChapterData(models.Model):
|
||||
data = models.JSONField(null=False, blank=False, help_text="Chapter data")
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='chapters')
|
||||
|
||||
class Meta:
|
||||
unique_together = ['media']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from .. import tasks
|
||||
|
||||
is_new = self.pk is None
|
||||
if is_new or (not is_new and self._check_data_changed()):
|
||||
super().save(*args, **kwargs)
|
||||
tasks.produce_video_chapters.delay(self.pk)
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _check_data_changed(self):
|
||||
if self.pk:
|
||||
old_instance = VideoChapterData.objects.get(pk=self.pk)
|
||||
return old_instance.data != self.data
|
||||
return False
|
||||
|
||||
@property
|
||||
def chapter_data(self):
|
||||
# ensure response is consistent
|
||||
data = []
|
||||
for item in self.data:
|
||||
if item.get("start") and item.get("title"):
|
||||
thumbnail = item.get("thumbnail")
|
||||
if thumbnail:
|
||||
thumbnail = helpers.url_from_path(thumbnail)
|
||||
else:
|
||||
thumbnail = "static/images/chapter_default.jpg"
|
||||
data.append(
|
||||
{
|
||||
"start": item.get("start"),
|
||||
"title": item.get("title"),
|
||||
"thumbnail": thumbnail,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class VideoTrimRequest(models.Model):
|
||||
"""Model to handle video trimming requests"""
|
||||
|
||||
VIDEO_TRIM_STATUS = (
|
||||
("initial", "Initial"),
|
||||
("running", "Running"),
|
||||
("success", "Success"),
|
||||
("fail", "Fail"),
|
||||
)
|
||||
|
||||
VIDEO_ACTION_CHOICES = (
|
||||
("replace", "Replace Original"),
|
||||
("save_new", "Save as New"),
|
||||
("create_segments", "Create Segments"),
|
||||
)
|
||||
|
||||
TRIM_STYLE_CHOICES = (
|
||||
("no_encoding", "No Encoding"),
|
||||
("precise", "Precise"),
|
||||
)
|
||||
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='trim_requests')
|
||||
status = models.CharField(max_length=20, choices=VIDEO_TRIM_STATUS, default="initial")
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
video_action = models.CharField(max_length=20, choices=VIDEO_ACTION_CHOICES)
|
||||
media_trim_style = models.CharField(max_length=20, choices=TRIM_STYLE_CHOICES, default="no_encoding")
|
||||
timestamps = models.JSONField(null=False, blank=False, help_text="Timestamps for trimming")
|
||||
|
||||
def __str__(self):
|
||||
return f"Trim request for {self.media.title} ({self.status})"
|
||||
|
||||
|
||||
@receiver(post_delete, sender=VideoChapterData)
|
||||
def videochapterdata_delete(sender, instance, **kwargs):
|
||||
helpers.rm_dir(instance.media.video_chapters_folder)
|
||||
@@ -820,7 +820,7 @@ def update_listings_thumbnails():
|
||||
# Categories
|
||||
used_media = []
|
||||
saved = 0
|
||||
qs = Category.objects.filter().order_by("-media_count")
|
||||
qs = Category.objects.filter()
|
||||
for object in qs:
|
||||
media = Media.objects.exclude(friendly_token__in=used_media).filter(category=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||
if media:
|
||||
@@ -833,7 +833,7 @@ def update_listings_thumbnails():
|
||||
# Tags
|
||||
used_media = []
|
||||
saved = 0
|
||||
qs = Tag.objects.filter().order_by("-media_count")
|
||||
qs = Tag.objects.filter()
|
||||
for object in qs:
|
||||
media = Media.objects.exclude(friendly_token__in=used_media).filter(tags=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||
if media:
|
||||
|
||||
@@ -48,6 +48,8 @@ urlpatterns = [
|
||||
re_path(r"^view", views.view_media, name="get_media"),
|
||||
re_path(r"^upload", views.upload_media, name="upload_media"),
|
||||
# API VIEWS
|
||||
re_path(r"^api/v1/media/user/bulk_actions$", views.MediaBulkUserActions.as_view()),
|
||||
re_path(r"^api/v1/media/user/bulk_actions/$", views.MediaBulkUserActions.as_view()),
|
||||
re_path(r"^api/v1/media$", views.MediaList.as_view()),
|
||||
re_path(r"^api/v1/media/$", views.MediaList.as_view()),
|
||||
re_path(
|
||||
|
||||
1744
files/views.py
1744
files/views.py
File diff suppressed because it is too large
Load Diff
43
files/views/__init__.py
Normal file
43
files/views/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Import all views for backward compatibility
|
||||
from .auth import custom_login_view, saml_metadata # noqa: F401
|
||||
from .categories import CategoryList, TagList # noqa: F401
|
||||
from .comments import CommentDetail, CommentList # noqa: F401
|
||||
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
|
||||
from .media import MediaActions # noqa: F401
|
||||
from .media import MediaBulkUserActions # noqa: F401
|
||||
from .media import MediaDetail # noqa: F401
|
||||
from .media import MediaList # noqa: F401
|
||||
from .media import MediaSearch # noqa: F401
|
||||
from .pages import about # noqa: F401
|
||||
from .pages import add_subtitle # noqa: F401
|
||||
from .pages import categories # noqa: F401
|
||||
from .pages import contact # noqa: F401
|
||||
from .pages import edit_chapters # noqa: F401
|
||||
from .pages import edit_media # noqa: F401
|
||||
from .pages import edit_subtitle # noqa: F401
|
||||
from .pages import edit_video # noqa: F401
|
||||
from .pages import embed_media # noqa: F401
|
||||
from .pages import featured_media # noqa: F401
|
||||
from .pages import history # noqa: F401
|
||||
from .pages import index # noqa: F401
|
||||
from .pages import latest_media # noqa: F401
|
||||
from .pages import liked_media # noqa: F401
|
||||
from .pages import manage_comments # noqa: F401
|
||||
from .pages import manage_media # noqa: F401
|
||||
from .pages import manage_users # noqa: F401
|
||||
from .pages import members # noqa: F401
|
||||
from .pages import publish_media # noqa: F401
|
||||
from .pages import recommended_media # noqa: F401
|
||||
from .pages import search # noqa: F401
|
||||
from .pages import setlanguage # noqa: F401
|
||||
from .pages import sitemap # noqa: F401
|
||||
from .pages import tags # noqa: F401
|
||||
from .pages import tos # noqa: F401
|
||||
from .pages import trim_video # noqa: F401
|
||||
from .pages import upload_media # noqa: F401
|
||||
from .pages import video_chapters # noqa: F401
|
||||
from .pages import view_media # noqa: F401
|
||||
from .pages import view_playlist # noqa: F401
|
||||
from .playlists import PlaylistDetail, PlaylistList # noqa: F401
|
||||
from .tasks import TaskDetail, TasksList # noqa: F401
|
||||
from .user import UserActions # noqa: F401
|
||||
42
files/views/auth.py
Normal file
42
files/views/auth.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from identity_providers.models import LoginOption
|
||||
|
||||
|
||||
def saml_metadata(request):
|
||||
if not (hasattr(settings, "USE_SAML") and settings.USE_SAML):
|
||||
raise Http404
|
||||
|
||||
xml_parts = ['<?xml version="1.0"?>']
|
||||
saml_social_apps = SocialApp.objects.filter(provider='saml')
|
||||
entity_id = f"{settings.FRONTEND_HOST}/saml/metadata/"
|
||||
xml_parts.append(f'<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Name="{entity_id}">') # noqa
|
||||
xml_parts.append(f' <md:EntityDescriptor entityID="{entity_id}">') # noqa
|
||||
xml_parts.append(' <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">') # noqa
|
||||
|
||||
# Add multiple AssertionConsumerService elements with different indices
|
||||
for index, app in enumerate(saml_social_apps, start=1):
|
||||
xml_parts.append(
|
||||
f' <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ' # noqa
|
||||
f'Location="{settings.FRONTEND_HOST}/accounts/saml/{app.client_id}/acs/" index="{index}"/>' # noqa
|
||||
)
|
||||
|
||||
xml_parts.append(' </md:SPSSODescriptor>') # noqa
|
||||
xml_parts.append(' </md:EntityDescriptor>') # noqa
|
||||
xml_parts.append('</md:EntitiesDescriptor>') # noqa
|
||||
metadata_xml = '\n'.join(xml_parts)
|
||||
return HttpResponse(metadata_xml, content_type='application/xml')
|
||||
|
||||
|
||||
def custom_login_view(request):
|
||||
if not (hasattr(settings, "USE_IDENTITY_PROVIDERS") and settings.USE_IDENTITY_PROVIDERS):
|
||||
return redirect(reverse('login_system'))
|
||||
|
||||
login_options = []
|
||||
for option in LoginOption.objects.filter(active=True):
|
||||
login_options.append({'url': option.url, 'title': option.title})
|
||||
return render(request, 'account/custom_login_selector.html', {'login_options': login_options})
|
||||
66
files/views/categories.py
Normal file
66
files/views/categories.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from django.conf import settings
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..methods import is_mediacms_editor
|
||||
from ..models import Category, Tag
|
||||
from ..serializers import CategorySerializer, TagSerializer
|
||||
|
||||
|
||||
class CategoryList(APIView):
|
||||
"""List categories"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Categories'],
|
||||
operation_summary='Lists Categories',
|
||||
operation_description='Lists all categories',
|
||||
responses={
|
||||
200: openapi.Response('response description', CategorySerializer),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
base_filters = {}
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
base_filters = {"is_rbac_category": False}
|
||||
|
||||
base_queryset = Category.objects.prefetch_related("user")
|
||||
categories = base_queryset.filter(**base_filters)
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
if getattr(settings, 'USE_RBAC', False) and request.user.is_authenticated:
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
categories = categories.union(rbac_categories)
|
||||
|
||||
categories = categories.order_by("title")
|
||||
|
||||
serializer = CategorySerializer(categories, many=True, context={"request": request})
|
||||
ret = serializer.data
|
||||
return Response(ret)
|
||||
|
||||
|
||||
class TagList(APIView):
|
||||
"""List tags"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
|
||||
],
|
||||
tags=['Tags'],
|
||||
operation_summary='Lists Tags',
|
||||
operation_description='Paginated listing of all tags',
|
||||
responses={
|
||||
200: openapi.Response('response description', TagSerializer),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
tags = Tag.objects.filter().order_by("-media_count")
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(tags, request)
|
||||
serializer = TagSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
159
files/views/comments.py
Normal file
159
files/views/comments.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.permissions import IsAuthorizedToAdd, IsAuthorizedToAddComment
|
||||
from users.models import User
|
||||
|
||||
from ..methods import (
|
||||
check_comment_for_mention,
|
||||
is_mediacms_editor,
|
||||
notify_user_on_comment,
|
||||
)
|
||||
from ..models import Comment, Media
|
||||
from ..serializers import CommentSerializer
|
||||
|
||||
|
||||
class CommentList(APIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
|
||||
openapi.Parameter(name='author', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='username'),
|
||||
],
|
||||
tags=['Comments'],
|
||||
operation_summary='Lists Comments',
|
||||
operation_description='Paginated listing of all comments',
|
||||
responses={
|
||||
200: openapi.Response('response description', CommentSerializer(many=True)),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
comments = Comment.objects.filter(media__state="public").order_by("-add_date")
|
||||
comments = comments.prefetch_related("user")
|
||||
comments = comments.prefetch_related("media")
|
||||
params = self.request.query_params
|
||||
if "author" in params:
|
||||
author_param = params["author"].strip()
|
||||
user_queryset = User.objects.all()
|
||||
user = get_object_or_404(user_queryset, username=author_param)
|
||||
comments = comments.filter(user=user)
|
||||
|
||||
page = paginator.paginate_queryset(comments, request)
|
||||
|
||||
serializer = CommentSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
|
||||
class CommentDetail(APIView):
|
||||
"""Comments related views
|
||||
Listings of comments for a media (GET)
|
||||
Create comment (POST)
|
||||
Delete comment (DELETE)
|
||||
"""
|
||||
|
||||
permission_classes = (IsAuthorizedToAddComment,)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
def get_object(self, friendly_token):
|
||||
try:
|
||||
media = Media.objects.select_related("user").get(friendly_token=friendly_token)
|
||||
self.check_object_permissions(self.request, media)
|
||||
if media.state == "private" and self.request.user != media.user:
|
||||
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return media
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "media file does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Media'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def get(self, request, friendly_token):
|
||||
# list comments for a media
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
comments = media.comments.filter().prefetch_related("user")
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(comments, request)
|
||||
serializer = CommentSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Media'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def delete(self, request, friendly_token, uid=None):
|
||||
"""Delete a comment
|
||||
Administrators, MediaCMS editors and managers,
|
||||
media owner, and comment owners, can delete a comment
|
||||
"""
|
||||
if uid:
|
||||
try:
|
||||
comment = Comment.objects.get(uid=uid)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "comment does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if (comment.user == self.request.user) or comment.media.user == self.request.user or is_mediacms_editor(self.request.user):
|
||||
comment.delete()
|
||||
else:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Media'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def post(self, request, friendly_token):
|
||||
"""Create a comment"""
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not media.enable_comments:
|
||||
return Response(
|
||||
{"detail": "comments not allowed here"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CommentSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user, media=media)
|
||||
if request.user != media.user:
|
||||
notify_user_on_comment(friendly_token=media.friendly_token)
|
||||
# here forward the comment to check if a user was mentioned
|
||||
if settings.ALLOW_MENTION_IN_COMMENTS:
|
||||
check_comment_for_mention(friendly_token=media.friendly_token, comment_text=serializer.data['text'])
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
179
files/views/encoding.py
Normal file
179
files/views/encoding.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from django.conf import settings
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..helpers import produce_ffmpeg_commands
|
||||
from ..models import EncodeProfile, Encoding
|
||||
from ..serializers import EncodeProfileSerializer
|
||||
|
||||
|
||||
class EncodingDetail(APIView):
|
||||
"""Experimental. This View is used by remote workers
|
||||
Needs heavy testing and documentation.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(auto_schema=None)
|
||||
def post(self, request, encoding_id):
|
||||
ret = {}
|
||||
force = request.data.get("force", False)
|
||||
task_id = request.data.get("task_id", False)
|
||||
action = request.data.get("action", "")
|
||||
chunk = request.data.get("chunk", False)
|
||||
chunk_file_path = request.data.get("chunk_file_path", "")
|
||||
|
||||
encoding_status = request.data.get("status", "")
|
||||
progress = request.data.get("progress", "")
|
||||
commands = request.data.get("commands", "")
|
||||
logs = request.data.get("logs", "")
|
||||
retries = request.data.get("retries", "")
|
||||
worker = request.data.get("worker", "")
|
||||
temp_file = request.data.get("temp_file", "")
|
||||
total_run_time = request.data.get("total_run_time", "")
|
||||
if action == "start":
|
||||
try:
|
||||
encoding = Encoding.objects.get(id=encoding_id)
|
||||
media = encoding.media
|
||||
profile = encoding.profile
|
||||
except BaseException:
|
||||
Encoding.objects.filter(id=encoding_id).delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# TODO: break chunk True/False logic here
|
||||
if (
|
||||
Encoding.objects.filter(
|
||||
media=media,
|
||||
profile=profile,
|
||||
chunk=chunk,
|
||||
chunk_file_path=chunk_file_path,
|
||||
).count()
|
||||
> 1 # noqa
|
||||
and force is False # noqa
|
||||
):
|
||||
Encoding.objects.filter(id=encoding_id).delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
Encoding.objects.filter(
|
||||
media=media,
|
||||
profile=profile,
|
||||
chunk=chunk,
|
||||
chunk_file_path=chunk_file_path,
|
||||
).exclude(id=encoding.id).delete()
|
||||
|
||||
encoding.status = "running"
|
||||
if task_id:
|
||||
encoding.task_id = task_id
|
||||
|
||||
encoding.save()
|
||||
if chunk:
|
||||
original_media_path = chunk_file_path
|
||||
original_media_md5sum = encoding.md5sum
|
||||
original_media_url = settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
|
||||
else:
|
||||
original_media_path = media.media_file.path
|
||||
original_media_md5sum = media.md5sum
|
||||
original_media_url = settings.SSL_FRONTEND_HOST + media.original_media_url
|
||||
|
||||
ret["original_media_url"] = original_media_url
|
||||
ret["original_media_path"] = original_media_path
|
||||
ret["original_media_md5sum"] = original_media_md5sum
|
||||
|
||||
# generating the commands here, and will replace these with temporary
|
||||
# files created on the remote server
|
||||
tf = "TEMP_FILE_REPLACE"
|
||||
tfpass = "TEMP_FPASS_FILE_REPLACE"
|
||||
ffmpeg_commands = produce_ffmpeg_commands(
|
||||
original_media_path,
|
||||
media.media_info,
|
||||
resolution=profile.resolution,
|
||||
codec=profile.codec,
|
||||
output_filename=tf,
|
||||
pass_file=tfpass,
|
||||
chunk=chunk,
|
||||
)
|
||||
if not ffmpeg_commands:
|
||||
encoding.delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ret["duration"] = media.duration
|
||||
ret["ffmpeg_commands"] = ffmpeg_commands
|
||||
ret["profile_extension"] = profile.extension
|
||||
return Response(ret, status=status.HTTP_201_CREATED)
|
||||
elif action == "update_fields":
|
||||
try:
|
||||
encoding = Encoding.objects.get(id=encoding_id)
|
||||
except BaseException:
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
to_update = ["size", "update_date"]
|
||||
if encoding_status:
|
||||
encoding.status = encoding_status
|
||||
to_update.append("status")
|
||||
if progress:
|
||||
encoding.progress = progress
|
||||
to_update.append("progress")
|
||||
if logs:
|
||||
encoding.logs = logs
|
||||
to_update.append("logs")
|
||||
if commands:
|
||||
encoding.commands = commands
|
||||
to_update.append("commands")
|
||||
if task_id:
|
||||
encoding.task_id = task_id
|
||||
to_update.append("task_id")
|
||||
if total_run_time:
|
||||
encoding.total_run_time = total_run_time
|
||||
to_update.append("total_run_time")
|
||||
if worker:
|
||||
encoding.worker = worker
|
||||
to_update.append("worker")
|
||||
if temp_file:
|
||||
encoding.temp_file = temp_file
|
||||
to_update.append("temp_file")
|
||||
|
||||
if retries:
|
||||
encoding.retries = retries
|
||||
to_update.append("retries")
|
||||
|
||||
try:
|
||||
encoding.save(update_fields=to_update)
|
||||
except BaseException:
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"status": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@swagger_auto_schema(auto_schema=None)
|
||||
def put(self, request, encoding_id, format=None):
|
||||
encoding_file = request.data["file"]
|
||||
encoding = Encoding.objects.filter(id=encoding_id).first()
|
||||
if not encoding:
|
||||
return Response(
|
||||
{"detail": "encoding does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
encoding.media_file = encoding_file
|
||||
encoding.save()
|
||||
return Response({"detail": "ok"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class EncodeProfileList(APIView):
|
||||
"""List encode profiles"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Encoding Profiles'],
|
||||
operation_summary='List Encoding Profiles',
|
||||
operation_description='Lists all encoding profiles for videos',
|
||||
responses={200: EncodeProfileSerializer(many=True)},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
profiles = EncodeProfile.objects.all()
|
||||
serializer = EncodeProfileSerializer(profiles, many=True, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
763
files/views/media.py
Normal file
763
files/views/media.py
Normal file
@@ -0,0 +1,763 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.search import SearchQuery
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from actions.models import MediaAction
|
||||
from cms.custom_pagination import FastPaginationWithoutCount
|
||||
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor
|
||||
from users.models import User
|
||||
|
||||
from .. import helpers
|
||||
from ..methods import (
|
||||
change_media_owner,
|
||||
copy_media,
|
||||
get_user_or_session,
|
||||
is_mediacms_editor,
|
||||
show_recommended_media,
|
||||
show_related_media,
|
||||
update_user_ratings,
|
||||
)
|
||||
from ..models import EncodeProfile, Media, MediaPermission, Playlist, PlaylistMedia
|
||||
from ..serializers import MediaSearchSerializer, MediaSerializer, SingleMediaSerializer
|
||||
from ..stop_words import STOP_WORDS
|
||||
from ..tasks import save_user_action
|
||||
|
||||
|
||||
class MediaList(APIView):
|
||||
"""Media listings views"""
|
||||
|
||||
permission_classes = (IsAuthorizedToAdd,)
|
||||
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
|
||||
openapi.Parameter(name='author', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='username'),
|
||||
openapi.Parameter(name='show', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='show', enum=['recommended', 'featured', 'latest']),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='List Media',
|
||||
operation_description='Lists all media',
|
||||
responses={200: MediaSerializer(many=True)},
|
||||
)
|
||||
def _get_media_queryset(self, request, user=None):
|
||||
base_filters = Q(listable=True)
|
||||
if user:
|
||||
base_filters &= Q(user=user)
|
||||
|
||||
base_queryset = Media.objects.prefetch_related("user")
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return base_queryset.filter(base_filters).order_by("-add_date")
|
||||
|
||||
# Build OR conditions for authenticated users
|
||||
conditions = base_filters # Start with listable media
|
||||
|
||||
# Add user permissions
|
||||
permission_filter = {'user': request.user}
|
||||
if user:
|
||||
permission_filter['owner_user'] = user
|
||||
|
||||
if MediaPermission.objects.filter(**permission_filter).exists():
|
||||
perm_conditions = Q(permissions__user=request.user)
|
||||
if user:
|
||||
perm_conditions &= Q(user=user)
|
||||
conditions |= perm_conditions
|
||||
|
||||
# Add RBAC conditions
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
rbac_conditions = Q(category__in=rbac_categories)
|
||||
if user:
|
||||
rbac_conditions &= Q(user=user)
|
||||
conditions |= rbac_conditions
|
||||
|
||||
return base_queryset.filter(conditions).distinct().order_by("-add_date")[:1000]
|
||||
|
||||
def get(self, request, format=None):
|
||||
# Show media
|
||||
# authenticated users can see:
|
||||
|
||||
# All listable media (public access)
|
||||
# Non-listable media they have RBAC access to
|
||||
# Non-listable media they have direct permissions for
|
||||
|
||||
params = self.request.query_params
|
||||
show_param = params.get("show", "")
|
||||
|
||||
author_param = params.get("author", "").strip()
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
|
||||
if show_param == "recommended":
|
||||
pagination_class = FastPaginationWithoutCount
|
||||
media = show_recommended_media(request, limit=50)
|
||||
elif show_param == "featured":
|
||||
media = Media.objects.filter(listable=True, featured=True).prefetch_related("user").order_by("-add_date")
|
||||
elif show_param == "shared_by_me":
|
||||
if not self.request.user.is_authenticated:
|
||||
media = Media.objects.none()
|
||||
else:
|
||||
media = Media.objects.filter(permissions__owner_user=self.request.user).prefetch_related("user")
|
||||
elif show_param == "shared_with_me":
|
||||
if not self.request.user.is_authenticated:
|
||||
media = Media.objects.none()
|
||||
else:
|
||||
base_queryset = Media.objects.prefetch_related("user")
|
||||
user_media_filters = {'permissions__user': request.user}
|
||||
media = base_queryset.filter(**user_media_filters)
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
rbac_filters = {'category__in': rbac_categories}
|
||||
|
||||
rbac_media = base_queryset.filter(**rbac_filters)
|
||||
media = media.union(rbac_media)
|
||||
media = media.order_by("-add_date")[:1000] # limit to 1000 results
|
||||
elif author_param:
|
||||
user_queryset = User.objects.all()
|
||||
user = get_object_or_404(user_queryset, username=author_param)
|
||||
if self.request.user == user:
|
||||
media = Media.objects.filter(user=user).prefetch_related("user").order_by("-add_date")
|
||||
else:
|
||||
media = self._get_media_queryset(request, user)
|
||||
else:
|
||||
media = self._get_media_queryset(request)
|
||||
|
||||
paginator = pagination_class()
|
||||
|
||||
page = paginator.paginate_queryset(media, request)
|
||||
|
||||
serializer = MediaSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name="media_file", in_=openapi.IN_FORM, type=openapi.TYPE_FILE, required=True, description="media_file"),
|
||||
openapi.Parameter(name="description", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="description"),
|
||||
openapi.Parameter(name="title", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="title"),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Add new Media',
|
||||
operation_description='Adds a new media, for authenticated users',
|
||||
responses={201: openapi.Response('response description', MediaSerializer), 401: 'bad request'},
|
||||
)
|
||||
def post(self, request, format=None):
|
||||
# Add new media
|
||||
serializer = MediaSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
media_file = request.data["media_file"]
|
||||
serializer.save(user=request.user, media_file=media_file)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MediaBulkUserActions(APIView):
|
||||
"""Bulk actions on media items"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
parser_classes = (JSONParser,)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='media_ids', in_=openapi.IN_FORM, type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_STRING), required=True, description="List of media IDs"),
|
||||
openapi.Parameter(
|
||||
name='action',
|
||||
in_=openapi.IN_FORM,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
description="Action to perform",
|
||||
enum=[
|
||||
"enable_comments",
|
||||
"disable_comments",
|
||||
"delete_media",
|
||||
"enable_download",
|
||||
"disable_download",
|
||||
"add_to_playlist",
|
||||
"remove_from_playlist",
|
||||
"set_state",
|
||||
"change_owner",
|
||||
"copy_media",
|
||||
],
|
||||
),
|
||||
openapi.Parameter(
|
||||
name='playlist_ids',
|
||||
in_=openapi.IN_FORM,
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
||||
required=False,
|
||||
description="List of playlist IDs (required for add_to_playlist and remove_from_playlist actions)",
|
||||
),
|
||||
openapi.Parameter(
|
||||
name='state', in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="State to set (required for set_state action)", enum=["private", "public", "unlisted"]
|
||||
),
|
||||
openapi.Parameter(name='owner', in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="New owner username (required for change_owner action)"),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Perform bulk actions on media',
|
||||
operation_description='Perform various bulk actions on multiple media items at once',
|
||||
responses={
|
||||
200: openapi.Response('Action performed successfully'),
|
||||
400: 'Bad request',
|
||||
401: 'Not authenticated',
|
||||
},
|
||||
)
|
||||
def post(self, request, format=None):
|
||||
# Check if user is authenticated
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"detail": "Authentication required"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Get required parameters
|
||||
media_ids = request.data.get('media_ids', [])
|
||||
action = request.data.get('action')
|
||||
|
||||
# Validate required parameters
|
||||
if not media_ids:
|
||||
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not action:
|
||||
return Response({"detail": "action is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get media objects owned by the user
|
||||
media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)
|
||||
|
||||
if not media:
|
||||
return Response({"detail": "No matching media found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Process based on action
|
||||
if action == "enable_comments":
|
||||
media.update(enable_comments=True)
|
||||
return Response({"detail": f"Comments enabled for {media.count()} media items"})
|
||||
|
||||
elif action == "disable_comments":
|
||||
media.update(enable_comments=False)
|
||||
return Response({"detail": f"Comments disabled for {media.count()} media items"})
|
||||
|
||||
elif action == "delete_media":
|
||||
count = media.count()
|
||||
media.delete()
|
||||
return Response({"detail": f"{count} media items deleted"})
|
||||
|
||||
elif action == "enable_download":
|
||||
media.update(allow_download=True)
|
||||
return Response({"detail": f"Download enabled for {media.count()} media items"})
|
||||
|
||||
elif action == "disable_download":
|
||||
media.update(allow_download=False)
|
||||
return Response({"detail": f"Download disabled for {media.count()} media items"})
|
||||
|
||||
elif action == "add_to_playlist":
|
||||
playlist_ids = request.data.get('playlist_ids', [])
|
||||
if not playlist_ids:
|
||||
return Response({"detail": "playlist_ids is required for add_to_playlist action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
playlists = Playlist.objects.filter(user=request.user, id__in=playlist_ids)
|
||||
if not playlists:
|
||||
return Response({"detail": "No matching playlists found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
added_count = 0
|
||||
for playlist in playlists:
|
||||
for m in media:
|
||||
media_in_playlist = PlaylistMedia.objects.filter(playlist=playlist).count()
|
||||
if media_in_playlist < settings.MAX_MEDIA_PER_PLAYLIST:
|
||||
obj, created = PlaylistMedia.objects.get_or_create(
|
||||
playlist=playlist,
|
||||
media=m,
|
||||
ordering=media_in_playlist + 1,
|
||||
)
|
||||
if created:
|
||||
added_count += 1
|
||||
|
||||
return Response({"detail": f"Added {added_count} media items to {playlists.count()} playlists"})
|
||||
|
||||
elif action == "remove_from_playlist":
|
||||
playlist_ids = request.data.get('playlist_ids', [])
|
||||
if not playlist_ids:
|
||||
return Response({"detail": "playlist_ids is required for remove_from_playlist action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
playlists = Playlist.objects.filter(user=request.user, id__in=playlist_ids)
|
||||
if not playlists:
|
||||
return Response({"detail": "No matching playlists found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
removed_count = 0
|
||||
for playlist in playlists:
|
||||
removed = PlaylistMedia.objects.filter(playlist=playlist, media__in=media).delete()[0]
|
||||
removed_count += removed
|
||||
|
||||
return Response({"detail": f"Removed {removed_count} media items from {playlists.count()} playlists"})
|
||||
|
||||
elif action == "set_state":
|
||||
state = request.data.get('state')
|
||||
if not state:
|
||||
return Response({"detail": "state is required for set_state action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
valid_states = ["private", "public", "unlisted"]
|
||||
if state not in valid_states:
|
||||
return Response({"detail": f"state must be one of {valid_states}"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Check if user can set public state
|
||||
if not is_mediacms_editor(request.user) and settings.PORTAL_WORKFLOW != "public":
|
||||
if state == "public":
|
||||
return Response({"detail": "You are not allowed to set media to public state"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Update media state
|
||||
for m in media:
|
||||
m.state = state
|
||||
if m.state == "public" and m.encoding_status == "success" and m.is_reviewed is True:
|
||||
m.listable = True
|
||||
else:
|
||||
m.listable = False
|
||||
|
||||
m.save(update_fields=["state", "listable"])
|
||||
|
||||
return Response({"detail": f"State updated to {state} for {media.count()} media items"})
|
||||
|
||||
elif action == "change_owner":
|
||||
owner = request.data.get('owner')
|
||||
if not owner:
|
||||
return Response({"detail": "owner is required for change_owner action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
new_user = User.objects.filter(username=owner).first()
|
||||
if not new_user:
|
||||
return Response({"detail": "User not found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
changed_count = 0
|
||||
for m in media:
|
||||
result = change_media_owner(m.id, new_user)
|
||||
if result:
|
||||
changed_count += 1
|
||||
|
||||
return Response({"detail": f"Owner changed for {changed_count} media items"})
|
||||
|
||||
elif action == "copy_media":
|
||||
for m in media:
|
||||
copy_media(m.id)
|
||||
|
||||
return Response({"detail": f"{media.count()} media items copied"})
|
||||
|
||||
else:
|
||||
return Response({"detail": f"Unknown action: {action}"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MediaDetail(APIView):
|
||||
"""
|
||||
Retrieve, update or delete a media instance.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrEditor)
|
||||
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
def get_object(self, friendly_token):
|
||||
try:
|
||||
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
|
||||
|
||||
# this need be explicitly called, and will call
|
||||
# has_object_permission() after has_permission has succeeded
|
||||
self.check_object_permissions(self.request, media)
|
||||
if media.state == "private":
|
||||
if self.request.user.has_member_access_to_media(media) or is_mediacms_editor(self.request.user):
|
||||
pass
|
||||
else:
|
||||
return Response(
|
||||
{"detail": "media is private"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
return media
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "media file does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Get information for Media',
|
||||
operation_description='Get information for a media',
|
||||
responses={200: SingleMediaSerializer(), 400: 'bad request'},
|
||||
)
|
||||
def get(self, request, friendly_token, format=None):
|
||||
# Get media details
|
||||
# password = request.GET.get("password")
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
serializer = SingleMediaSerializer(media, context={"request": request})
|
||||
if media.state == "private":
|
||||
related_media = []
|
||||
else:
|
||||
related_media = show_related_media(media, request=request, limit=100)
|
||||
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
|
||||
related_media = related_media_serializer.data
|
||||
ret = serializer.data
|
||||
|
||||
# update rattings info with user specific ratings
|
||||
# eg user has already rated for this media
|
||||
# this only affects user rating and only if enabled
|
||||
if settings.ALLOW_RATINGS and ret.get("ratings_info") and not request.user.is_anonymous:
|
||||
ret["ratings_info"] = update_user_ratings(request.user, media, ret.get("ratings_info"))
|
||||
|
||||
ret["related_media"] = related_media
|
||||
return Response(ret)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
|
||||
openapi.Parameter(name='type', type=openapi.TYPE_STRING, in_=openapi.IN_FORM, description='action to perform', enum=['encode', 'review']),
|
||||
openapi.Parameter(
|
||||
name='encoding_profiles',
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_STRING),
|
||||
in_=openapi.IN_FORM,
|
||||
description='if action to perform is encode, need to specify list of ids of encoding profiles',
|
||||
),
|
||||
openapi.Parameter(name='result', type=openapi.TYPE_BOOLEAN, in_=openapi.IN_FORM, description='if action is review, this is the result (True for reviewed, False for not reviewed)'),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Run action on Media',
|
||||
operation_description='Actions for a media, for MediaCMS editors and managers',
|
||||
responses={201: 'action created', 400: 'bad request'},
|
||||
operation_id='media_manager_actions',
|
||||
)
|
||||
def post(self, request, friendly_token, format=None):
|
||||
"""superuser actions
|
||||
Available only to MediaCMS editors and managers
|
||||
|
||||
Action is a POST variable, review and encode are implemented
|
||||
"""
|
||||
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
action = request.data.get("type")
|
||||
profiles_list = request.data.get("encoding_profiles")
|
||||
result = request.data.get("result", True)
|
||||
if action == "encode":
|
||||
# Create encoding tasks for specific profiles
|
||||
valid_profiles = []
|
||||
if profiles_list:
|
||||
if isinstance(profiles_list, list):
|
||||
for p in profiles_list:
|
||||
p = EncodeProfile.objects.filter(id=p).first()
|
||||
if p:
|
||||
valid_profiles.append(p)
|
||||
elif isinstance(profiles_list, str):
|
||||
try:
|
||||
p = EncodeProfile.objects.filter(id=int(profiles_list)).first()
|
||||
valid_profiles.append(p)
|
||||
except ValueError:
|
||||
return Response(
|
||||
{"detail": "encoding_profiles must be int or list of ints of valid encode profiles"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
media.encode(profiles=valid_profiles)
|
||||
return Response({"detail": "media will be encoded"}, status=status.HTTP_201_CREATED)
|
||||
elif action == "review":
|
||||
if result:
|
||||
media.is_reviewed = True
|
||||
elif result is False:
|
||||
media.is_reviewed = False
|
||||
media.save(update_fields=["is_reviewed"])
|
||||
return Response({"detail": "media reviewed set"}, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
{"detail": "not valid action or no action specified"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name="description", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="description"),
|
||||
openapi.Parameter(name="title", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="title"),
|
||||
openapi.Parameter(name="media_file", in_=openapi.IN_FORM, type=openapi.TYPE_FILE, required=False, description="media_file"),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Update Media',
|
||||
operation_description='Update a Media, for Media uploader',
|
||||
responses={201: openapi.Response('response description', MediaSerializer), 401: 'bad request'},
|
||||
)
|
||||
def put(self, request, friendly_token, format=None):
|
||||
# Update a media object
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not (request.user.has_contributor_access_to_media(media) or is_mediacms_editor(request.user)):
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
serializer = MediaSerializer(media, data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user)
|
||||
# no need to update the media file itself, only the metadata
|
||||
# if request.data.get('media_file'):
|
||||
# media_file = request.data["media_file"]
|
||||
# serializer.save(user=request.user, media_file=media_file)
|
||||
# else:
|
||||
# serializer.save(user=request.user)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Delete Media',
|
||||
operation_description='Delete a Media, for MediaCMS editors and managers',
|
||||
responses={
|
||||
204: 'no content',
|
||||
},
|
||||
)
|
||||
def delete(self, request, friendly_token, format=None):
|
||||
# Delete a media object
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
media.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class MediaActions(APIView):
|
||||
"""
|
||||
Retrieve, update or delete a media action instance.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
parser_classes = (JSONParser,)
|
||||
|
||||
def get_object(self, friendly_token):
|
||||
try:
|
||||
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
|
||||
if media.state == "private" and self.request.user != media.user:
|
||||
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return media
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "media file does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Media'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
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)):
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
ret = {}
|
||||
reported = MediaAction.objects.filter(media=media, action="report")
|
||||
ret["reported"] = []
|
||||
for rep in reported:
|
||||
item = {"reported_date": rep.action_date, "reason": rep.extra_info}
|
||||
ret["reported"].append(item)
|
||||
|
||||
return Response(ret, status=status.HTTP_200_OK)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Media'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def post(self, request, friendly_token, format=None):
|
||||
# perform like/dislike/report actions
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
action = request.data.get("type")
|
||||
extra = request.data.get("extra_info")
|
||||
if request.user.is_anonymous:
|
||||
# there is a list of allowed actions for
|
||||
# anonymous users, specified in settings
|
||||
if action not in settings.ALLOW_ANONYMOUS_ACTIONS:
|
||||
return Response(
|
||||
{"detail": "action allowed on logged in users only"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if action:
|
||||
user_or_session = get_user_or_session(request)
|
||||
save_user_action.delay(
|
||||
user_or_session,
|
||||
friendly_token=media.friendly_token,
|
||||
action=action,
|
||||
extra_info=extra,
|
||||
)
|
||||
|
||||
return Response({"detail": "action received"}, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response({"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Media'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def delete(self, request, friendly_token, format=None):
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
action = request.data.get("type")
|
||||
if action:
|
||||
if action == "report": # delete reported actions
|
||||
MediaAction.objects.filter(media=media, action="report").delete()
|
||||
media.reported_times = 0
|
||||
media.save(update_fields=["reported_times"])
|
||||
return Response(
|
||||
{"detail": "reset reported times counter"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
else:
|
||||
return Response({"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MediaSearch(APIView):
|
||||
"""
|
||||
Retrieve results for search
|
||||
Only GET is implemented here
|
||||
"""
|
||||
|
||||
parser_classes = (JSONParser,)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Search'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
params = self.request.query_params
|
||||
query = params.get("q", "").strip().lower()
|
||||
category = params.get("c", "").strip()
|
||||
tag = params.get("t", "").strip()
|
||||
|
||||
ordering = params.get("ordering", "").strip()
|
||||
sort_by = params.get("sort_by", "").strip()
|
||||
media_type = params.get("media_type", "").strip()
|
||||
|
||||
author = params.get("author", "").strip()
|
||||
upload_date = params.get('upload_date', '').strip()
|
||||
|
||||
sort_by_options = ["title", "add_date", "edit_date", "views", "likes"]
|
||||
if sort_by not in sort_by_options:
|
||||
sort_by = "add_date"
|
||||
if ordering == "asc":
|
||||
ordering = ""
|
||||
else:
|
||||
ordering = "-"
|
||||
|
||||
if media_type not in ["video", "image", "audio", "pdf"]:
|
||||
media_type = None
|
||||
|
||||
if not (query or category or tag):
|
||||
ret = {}
|
||||
return Response(ret, status=status.HTTP_200_OK)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
basic_query = Q(listable=True) | Q(permissions__user=request.user)
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
basic_query |= Q(category__in=rbac_categories)
|
||||
|
||||
else:
|
||||
basic_query = Q(listable=True)
|
||||
|
||||
media = Media.objects.filter(basic_query).distinct()
|
||||
|
||||
if query:
|
||||
# move this processing to a prepare_query function
|
||||
query = helpers.clean_query(query)
|
||||
q_parts = [q_part.rstrip("y") for q_part in query.split() if q_part not in STOP_WORDS]
|
||||
if q_parts:
|
||||
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
|
||||
for part in q_parts[1:]:
|
||||
query &= SearchQuery(part + ":*", search_type="raw")
|
||||
else:
|
||||
query = None
|
||||
if query:
|
||||
media = media.filter(search=query)
|
||||
|
||||
if tag:
|
||||
media = media.filter(tags__title=tag)
|
||||
|
||||
if category:
|
||||
media = media.filter(category__title__contains=category)
|
||||
|
||||
if media_type:
|
||||
media = media.filter(media_type=media_type)
|
||||
|
||||
if author:
|
||||
media = media.filter(user__username=author)
|
||||
|
||||
if upload_date:
|
||||
gte = None
|
||||
if upload_date == 'today':
|
||||
gte = datetime.now().date()
|
||||
if upload_date == 'this_week':
|
||||
gte = datetime.now() - timedelta(days=7)
|
||||
if upload_date == 'this_month':
|
||||
year = datetime.now().date().year
|
||||
month = datetime.now().date().month
|
||||
gte = datetime(year, month, 1)
|
||||
if upload_date == 'this_year':
|
||||
year = datetime.now().date().year
|
||||
gte = datetime(year, 1, 1)
|
||||
if gte:
|
||||
media = media.filter(add_date__gte=gte)
|
||||
|
||||
media = media.order_by(f"{ordering}{sort_by}")
|
||||
|
||||
if self.request.query_params.get("show", "").strip() == "titles":
|
||||
media = media.values("title")[:40]
|
||||
return Response(media, status=status.HTTP_200_OK)
|
||||
else:
|
||||
media = media.prefetch_related("user")[:1000] # limit to 1000 results
|
||||
|
||||
if category or tag:
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
else:
|
||||
# pagination_class = FastPaginationWithoutCount
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(media, request)
|
||||
serializer = MediaSearchSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
593
files/views/pages.py
Normal file
593
files/views/pages.py
Normal file
@@ -0,0 +1,593 @@
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.mail import EmailMessage
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from cms.permissions import user_allowed_to_upload
|
||||
from cms.version import VERSION
|
||||
from users.models import User
|
||||
|
||||
from .. import helpers
|
||||
from ..forms import (
|
||||
ContactForm,
|
||||
EditSubtitleForm,
|
||||
MediaMetadataForm,
|
||||
MediaPublishForm,
|
||||
SubtitleForm,
|
||||
)
|
||||
from ..frontend_translations import translate_string
|
||||
from ..helpers import get_alphanumeric_only
|
||||
from ..methods import (
|
||||
create_video_trim_request,
|
||||
get_user_or_session,
|
||||
handle_video_chapters,
|
||||
is_mediacms_editor,
|
||||
)
|
||||
from ..models import Category, Media, Playlist, Subtitle, Tag, VideoTrimRequest
|
||||
from ..tasks import save_user_action, video_trim_task
|
||||
|
||||
|
||||
def about(request):
|
||||
"""About view"""
|
||||
|
||||
context = {"VERSION": VERSION}
|
||||
return render(request, "cms/about.html", context)
|
||||
|
||||
|
||||
def setlanguage(request):
|
||||
"""Set Language view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/set_language.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_subtitle(request):
|
||||
"""Add subtitle view"""
|
||||
|
||||
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 = SubtitleForm(media, request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
subtitle = form.save()
|
||||
new_subtitle = Subtitle.objects.filter(id=subtitle.id).first()
|
||||
try:
|
||||
new_subtitle.convert_to_srt()
|
||||
messages.add_message(request, messages.INFO, "Subtitle was added!")
|
||||
return HttpResponseRedirect(subtitle.media.get_absolute_url())
|
||||
except: # noqa: E722
|
||||
new_subtitle.delete()
|
||||
error_msg = "Invalid subtitle format. Use SubRip (.srt) or WebVTT (.vtt) files."
|
||||
form.add_error("subtitle_file", error_msg)
|
||||
|
||||
else:
|
||||
form = SubtitleForm(media_item=media)
|
||||
subtitles = media.subtitles.all()
|
||||
context = {"media": media, "form": form, "subtitles": subtitles}
|
||||
return render(request, "cms/add_subtitle.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_subtitle(request):
|
||||
subtitle_id = request.GET.get("id", "").strip()
|
||||
action = request.GET.get("action", "").strip()
|
||||
if not subtitle_id:
|
||||
return HttpResponseRedirect("/")
|
||||
subtitle = Subtitle.objects.filter(id=subtitle_id).first()
|
||||
|
||||
if not subtitle:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user == subtitle.user or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {"subtitle": subtitle, "action": action}
|
||||
|
||||
if action == "download":
|
||||
response = HttpResponse(subtitle.subtitle_file.read(), content_type="text/vtt")
|
||||
filename = subtitle.subtitle_file.name.split("/")[-1]
|
||||
|
||||
if not filename.endswith(".vtt"):
|
||||
filename = f"{filename}.vtt"
|
||||
|
||||
response["Content-Disposition"] = f"attachment; filename={filename}" # noqa
|
||||
|
||||
return response
|
||||
|
||||
if request.method == "GET":
|
||||
form = EditSubtitleForm(subtitle)
|
||||
context["form"] = form
|
||||
elif request.method == "POST":
|
||||
confirm = request.GET.get("confirm", "").strip()
|
||||
if confirm == "true":
|
||||
messages.add_message(request, messages.INFO, "Subtitle was deleted")
|
||||
redirect_url = subtitle.media.get_absolute_url()
|
||||
subtitle.delete()
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
form = EditSubtitleForm(subtitle, request.POST)
|
||||
subtitle_text = form.data["subtitle"]
|
||||
with open(subtitle.subtitle_file.path, "w") as ff:
|
||||
ff.write(subtitle_text)
|
||||
|
||||
messages.add_message(request, messages.INFO, "Subtitle was edited")
|
||||
return HttpResponseRedirect(subtitle.media.get_absolute_url())
|
||||
return render(request, "cms/edit_subtitle.html", context)
|
||||
|
||||
|
||||
def categories(request):
|
||||
"""List categories view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/categories.html", context)
|
||||
|
||||
|
||||
def contact(request):
|
||||
"""Contact view"""
|
||||
|
||||
context = {}
|
||||
if request.method == "GET":
|
||||
form = ContactForm(request.user)
|
||||
context["form"] = form
|
||||
|
||||
else:
|
||||
form = ContactForm(request.user, request.POST)
|
||||
if form.is_valid():
|
||||
if request.user.is_authenticated:
|
||||
from_email = request.user.email
|
||||
name = request.user.name
|
||||
else:
|
||||
from_email = request.POST.get("from_email")
|
||||
name = request.POST.get("name")
|
||||
message = request.POST.get("message")
|
||||
|
||||
title = f"[{settings.PORTAL_NAME}] - Contact form message received"
|
||||
|
||||
msg = """
|
||||
You have received a message through the contact form\n
|
||||
Sender name: %s
|
||||
Sender email: %s\n
|
||||
\n %s
|
||||
""" % (
|
||||
name,
|
||||
from_email,
|
||||
message,
|
||||
)
|
||||
email = EmailMessage(
|
||||
title,
|
||||
msg,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
settings.ADMIN_EMAIL_LIST,
|
||||
reply_to=[from_email],
|
||||
)
|
||||
email.send(fail_silently=True)
|
||||
success_msg = "Message was sent! Thanks for contacting"
|
||||
context["success_msg"] = success_msg
|
||||
|
||||
return render(request, "cms/contact.html", context)
|
||||
|
||||
|
||||
def history(request):
|
||||
"""Show personal history view"""
|
||||
|
||||
context = {}
|
||||
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"""
|
||||
|
||||
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.has_contributor_access_to_media(media) or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
if request.method == "POST":
|
||||
form = MediaMetadataForm(request.user, request.POST, request.FILES, instance=media)
|
||||
if form.is_valid():
|
||||
media = form.save()
|
||||
for tag in media.tags.all():
|
||||
media.tags.remove(tag)
|
||||
if form.cleaned_data.get("new_tags"):
|
||||
for tag in form.cleaned_data.get("new_tags").split(","):
|
||||
tag = get_alphanumeric_only(tag)
|
||||
tag = tag[:99]
|
||||
if tag:
|
||||
try:
|
||||
tag = Tag.objects.get(title=tag)
|
||||
except Tag.DoesNotExist:
|
||||
tag = Tag.objects.create(title=tag, user=request.user)
|
||||
if tag not in media.tags.all():
|
||||
media.tags.add(tag)
|
||||
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
else:
|
||||
form = MediaMetadataForm(request.user, instance=media)
|
||||
return render(
|
||||
request,
|
||||
"cms/edit_media.html",
|
||||
{"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.has_contributor_access_to_media(media) 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},
|
||||
)
|
||||
|
||||
|
||||
def embed_media(request):
|
||||
"""Embed media view"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
if not friendly_token:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
media = Media.objects.values("title").filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {}
|
||||
context["media"] = friendly_token
|
||||
return render(request, "cms/embed.html", context)
|
||||
|
||||
|
||||
def featured_media(request):
|
||||
"""List featured media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/featured-media.html", context)
|
||||
|
||||
|
||||
def index(request):
|
||||
"""Index view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/index.html", context)
|
||||
|
||||
|
||||
def latest_media(request):
|
||||
"""List latest media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/latest-media.html", context)
|
||||
|
||||
|
||||
def liked_media(request):
|
||||
"""List user's liked media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/liked_media.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_users(request):
|
||||
"""List users management view"""
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/manage_users.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_media(request):
|
||||
"""List media management view"""
|
||||
if not is_mediacms_editor(request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
categories = Category.objects.all().order_by('title').values_list('title', flat=True)
|
||||
context = {'categories': list(categories)}
|
||||
return render(request, "cms/manage_media.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_comments(request):
|
||||
"""List comments management view"""
|
||||
if not is_mediacms_editor(request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/manage_comments.html", context)
|
||||
|
||||
|
||||
def members(request):
|
||||
"""List members view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/members.html", context)
|
||||
|
||||
|
||||
def recommended_media(request):
|
||||
"""List recommended media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/recommended-media.html", context)
|
||||
|
||||
|
||||
def search(request):
|
||||
"""Search view"""
|
||||
|
||||
context = {}
|
||||
RSS_URL = f"/rss{request.environ.get('REQUEST_URI')}"
|
||||
context["RSS_URL"] = RSS_URL
|
||||
return render(request, "cms/search.html", context)
|
||||
|
||||
|
||||
def sitemap(request):
|
||||
"""Sitemap"""
|
||||
|
||||
context = {}
|
||||
context["media"] = list(Media.objects.filter(listable=True).order_by("-add_date"))
|
||||
context["playlists"] = list(Playlist.objects.filter().order_by("-add_date"))
|
||||
context["users"] = list(User.objects.filter())
|
||||
return render(request, "sitemap.xml", context, content_type="application/xml")
|
||||
|
||||
|
||||
def tags(request):
|
||||
"""List tags view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/tags.html", context)
|
||||
|
||||
|
||||
def tos(request):
|
||||
"""Terms of service view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/tos.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def upload_media(request):
|
||||
"""Upload media view"""
|
||||
|
||||
from allauth.account.forms import LoginForm
|
||||
|
||||
form = LoginForm()
|
||||
context = {}
|
||||
context["form"] = form
|
||||
context["can_add"] = user_allowed_to_upload(request)
|
||||
can_upload_exp = settings.CANNOT_ADD_MEDIA_MESSAGE
|
||||
context["can_upload_exp"] = can_upload_exp
|
||||
|
||||
return render(request, "cms/add-media.html", context)
|
||||
|
||||
|
||||
def view_media(request):
|
||||
"""View media view"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
context = {}
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
if not media:
|
||||
context["media"] = None
|
||||
return render(request, "cms/media.html", context)
|
||||
|
||||
user_or_session = get_user_or_session(request)
|
||||
save_user_action.delay(user_or_session, friendly_token=friendly_token, action="watch")
|
||||
context = {}
|
||||
context["media"] = friendly_token
|
||||
context["media_object"] = media
|
||||
|
||||
context["CAN_DELETE_MEDIA"] = False
|
||||
context["CAN_EDIT_MEDIA"] = False
|
||||
context["CAN_DELETE_COMMENTS"] = False
|
||||
|
||||
if request.user.is_authenticated:
|
||||
if request.user.has_contributor_access_to_media(media) 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)
|
||||
|
||||
|
||||
def view_playlist(request, friendly_token):
|
||||
"""View playlist view"""
|
||||
|
||||
try:
|
||||
playlist = Playlist.objects.get(friendly_token=friendly_token)
|
||||
except BaseException:
|
||||
playlist = None
|
||||
|
||||
context = {}
|
||||
context["playlist"] = playlist
|
||||
return render(request, "cms/playlist.html", context)
|
||||
195
files/views/playlists.py
Normal file
195
files/views/playlists.py
Normal file
@@ -0,0 +1,195 @@
|
||||
from django.conf import settings
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor
|
||||
|
||||
from ..models import Media, Playlist, PlaylistMedia
|
||||
from ..serializers import MediaSerializer, PlaylistDetailSerializer, PlaylistSerializer
|
||||
|
||||
|
||||
class PlaylistList(APIView):
|
||||
"""Playlists listings and creation views"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Playlists'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
responses={
|
||||
200: openapi.Response('response description', PlaylistSerializer(many=True)),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
playlists = Playlist.objects.filter().prefetch_related("user")
|
||||
|
||||
if "author" in self.request.query_params:
|
||||
author = self.request.query_params["author"].strip()
|
||||
playlists = playlists.filter(user__username=author)
|
||||
|
||||
page = paginator.paginate_queryset(playlists, request)
|
||||
|
||||
serializer = PlaylistSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Playlists'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def post(self, request, format=None):
|
||||
serializer = PlaylistSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class PlaylistDetail(APIView):
|
||||
"""Playlist related views"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrEditor)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
def get_playlist(self, friendly_token):
|
||||
try:
|
||||
playlist = Playlist.objects.get(friendly_token=friendly_token)
|
||||
self.check_object_permissions(self.request, playlist)
|
||||
return playlist
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "Playlist does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Playlists'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def get(self, request, friendly_token, format=None):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
|
||||
serializer = PlaylistDetailSerializer(playlist, context={"request": request})
|
||||
|
||||
playlist_media = PlaylistMedia.objects.filter(playlist=playlist, media__state="public").prefetch_related("media__user")
|
||||
|
||||
playlist_media = [c.media for c in playlist_media]
|
||||
|
||||
playlist_media_serializer = MediaSerializer(playlist_media, many=True, context={"request": request})
|
||||
ret = serializer.data
|
||||
ret["playlist_media"] = playlist_media_serializer.data
|
||||
|
||||
return Response(ret)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Playlists'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def post(self, request, friendly_token, format=None):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
serializer = PlaylistDetailSerializer(playlist, data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Playlists'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def put(self, request, friendly_token, format=None):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
action = request.data.get("type")
|
||||
media_friendly_token = request.data.get("media_friendly_token")
|
||||
ordering = 0
|
||||
if request.data.get("ordering"):
|
||||
try:
|
||||
ordering = int(request.data.get("ordering"))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if action in ["add", "remove", "ordering"]:
|
||||
media = Media.objects.filter(friendly_token=media_friendly_token).first()
|
||||
if media:
|
||||
if action == "add":
|
||||
media_in_playlist = PlaylistMedia.objects.filter(playlist=playlist).count()
|
||||
if media_in_playlist >= settings.MAX_MEDIA_PER_PLAYLIST:
|
||||
return Response(
|
||||
{"detail": "max number of media for a Playlist reached"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
obj, created = PlaylistMedia.objects.get_or_create(
|
||||
playlist=playlist,
|
||||
media=media,
|
||||
ordering=media_in_playlist + 1,
|
||||
)
|
||||
obj.save()
|
||||
return Response(
|
||||
{"detail": "media added to Playlist"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
elif action == "remove":
|
||||
PlaylistMedia.objects.filter(playlist=playlist, media=media).delete()
|
||||
return Response(
|
||||
{"detail": "media removed from Playlist"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
elif action == "ordering":
|
||||
if ordering:
|
||||
playlist.set_ordering(media, ordering)
|
||||
return Response(
|
||||
{"detail": "new ordering set"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
else:
|
||||
return Response({"detail": "media is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"detail": "invalid or not specified action"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Playlists'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
)
|
||||
def delete(self, request, friendly_token, format=None):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
|
||||
playlist.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
30
files/views/tasks.py
Normal file
30
files/views/tasks.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..methods import list_tasks
|
||||
|
||||
|
||||
class TasksList(APIView):
|
||||
"""List tasks"""
|
||||
|
||||
swagger_schema = None
|
||||
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
ret = list_tasks()
|
||||
return Response(ret)
|
||||
|
||||
|
||||
class TaskDetail(APIView):
|
||||
"""Cancel a task"""
|
||||
|
||||
swagger_schema = None
|
||||
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def delete(self, request, uid, format=None):
|
||||
# This is not imported!
|
||||
# revoke(uid, terminate=True)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
45
files/views/user.py
Normal file
45
files/views/user.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from actions.models import USER_MEDIA_ACTIONS
|
||||
|
||||
from ..models import Media
|
||||
from ..serializers import MediaSerializer
|
||||
|
||||
VALID_USER_ACTIONS = [action for action, name in USER_MEDIA_ACTIONS]
|
||||
|
||||
|
||||
class UserActions(APIView):
|
||||
parser_classes = (JSONParser,)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='action', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='action', required=True, enum=VALID_USER_ACTIONS),
|
||||
],
|
||||
tags=['Users'],
|
||||
operation_summary='List user actions',
|
||||
operation_description='Lists user actions',
|
||||
)
|
||||
def get(self, request, action):
|
||||
media = []
|
||||
if action in VALID_USER_ACTIONS:
|
||||
if request.user.is_authenticated:
|
||||
media = Media.objects.select_related("user").filter(mediaactions__user=request.user, mediaactions__action=action).order_by("-mediaactions__action_date")
|
||||
elif request.session.session_key:
|
||||
media = (
|
||||
Media.objects.select_related("user")
|
||||
.filter(
|
||||
mediaactions__session_key=request.session.session_key,
|
||||
mediaactions__action=action,
|
||||
)
|
||||
.order_by("-mediaactions__action_date")
|
||||
)
|
||||
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(media, request)
|
||||
serializer = MediaSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
Reference in New Issue
Block a user