Thumbnails for images and videos #87

Merged
Anna Sirota merged 28 commits from thumbnails into main 2024-04-25 17:50:58 +02:00
6 changed files with 39 additions and 15 deletions
Showing only changes of commit 028a3f0a37 - Show all commits

View File

@ -105,8 +105,7 @@ ABUSE_TYPE = Choices(
# thumbnails of existing images must exist in MEDIA_ROOT before # thumbnails of existing images must exist in MEDIA_ROOT before
# the code expecting the new dimensions can be deployed! # the code expecting the new dimensions can be deployed!
THUMBNAIL_SIZE_DEFAULT = (1920, 1080) THUMBNAIL_SIZE_DEFAULT = (1920, 1080)
THUMBNAIL_SIZE_M = (1280, 720)
THUMBNAIL_SIZE_S = (640, 360) THUMBNAIL_SIZE_S = (640, 360)
THUMBNAIL_SIZES = [THUMBNAIL_SIZE_DEFAULT, THUMBNAIL_SIZE_M, THUMBNAIL_SIZE_S] THUMBNAIL_SIZES = [THUMBNAIL_SIZE_DEFAULT, THUMBNAIL_SIZE_S]
THUMBNAIL_FORMAT = 'PNG' THUMBNAIL_FORMAT = 'PNG'
THUMBNAIL_QUALITY = 83 THUMBNAIL_QUALITY = 83

View File

@ -1,7 +1,8 @@
from pathlib import Path
import json import json
from django.contrib.admin.models import LogEntry, DELETION from django.contrib.admin.models import LogEntry, DELETION
from django.test import TestCase # , TransactionTestCase from django.test import TestCase, override_settings
from common.tests.factories.extensions import create_approved_version, create_version from common.tests.factories.extensions import create_approved_version, create_version
from common.tests.factories.files import FileFactory from common.tests.factories.files import FileFactory
@ -10,7 +11,10 @@ import extensions.models
import files.models import files.models
import reviewers.models import reviewers.models
TEST_MEDIA_DIR = Path(__file__).resolve().parent / 'media'
@override_settings(MEDIA_ROOT=TEST_MEDIA_DIR)
class DeleteTest(TestCase): class DeleteTest(TestCase):
fixtures = ['dev', 'licenses'] fixtures = ['dev', 'licenses']

View File

@ -21,7 +21,7 @@ def scan_selected_files(self, request, queryset):
def make_thumbnails(self, request, queryset): def make_thumbnails(self, request, queryset):
"""Make thumbnails for selected files.""" """Make thumbnails for selected files."""
for instance in queryset: for instance in queryset.filter(type__in=(File.TYPES.IMAGE, File.TYPE.VIDEO)):
files.tasks.make_thumbnails.task_function(file_id=instance.pk) files.tasks.make_thumbnails.task_function(file_id=instance.pk)
@ -44,6 +44,8 @@ class FileAdmin(admin.ModelAdmin):
css = {'all': ('files/admin/file.css',)} css = {'all': ('files/admin/file.css',)}
def thumbnails(self, obj): def thumbnails(self, obj):
if not obj or not (obj.is_image or obj.is_video):
return ''
try: try:
context = { context = {
'MEDIA_URL': settings.MEDIA_URL, 'MEDIA_URL': settings.MEDIA_URL,
@ -58,6 +60,14 @@ class FileAdmin(admin.ModelAdmin):
logger.exception('Failed to render thumbnails') logger.exception('Failed to render thumbnails')
raise raise
def get_form(self, request, obj=None, **kwargs):
# Only override if the obj exisits
if obj:
if obj.is_image or obj.is_video:
help_text = 'Additional information about the file, e.g. existing thumbnails.'
kwargs.update({'help_texts': {'metadata': help_text}})
return super().get_form(request, obj, **kwargs)
view_on_site = False view_on_site = False
save_on_top = True save_on_top = True
@ -79,8 +89,8 @@ class FileAdmin(admin.ModelAdmin):
'date_approved', 'date_approved',
'date_status_changed', 'date_status_changed',
'size_bytes', 'size_bytes',
'thumbnail',
'thumbnails', 'thumbnails',
'thumbnail',
'type', 'type',
'user', 'user',
'original_hash', 'original_hash',
@ -101,7 +111,7 @@ class FileAdmin(admin.ModelAdmin):
{ {
'fields': ( 'fields': (
'id', 'id',
('source', 'thumbnail', 'thumbnails'), ('source', 'thumbnails', 'thumbnail'),
('type', 'content_type', 'original_name'), ('type', 'content_type', 'original_name'),
'status', 'status',
) )

View File

@ -51,7 +51,7 @@ def thumbnail_upload_to(instance, filename):
class File(CreatedModifiedMixin, TrackChangesMixin, models.Model): class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
track_changes_to_fields = {'status', 'size_bytes', 'hash', 'thumbnail'} track_changes_to_fields = {'status', 'size_bytes', 'hash', 'thumbnail', 'metadata'}
TYPES = FILE_TYPE_CHOICES TYPES = FILE_TYPE_CHOICES
STATUSES = FILE_STATUS_CHOICES STATUSES = FILE_STATUS_CHOICES
@ -100,7 +100,7 @@ class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
default=dict, default=dict,
blank=True, blank=True,
# TODO add link to the manifest file user manual page. # TODO add link to the manifest file user manual page.
help_text=('Meta information that was parsed from the `manifest file.'), help_text=('Meta information that was parsed from the manifest file.'),
) )
objects = FileManager() objects = FileManager()

View File

@ -38,11 +38,10 @@ def make_thumbnails(file_id: int) -> None:
# TODO: For an image, source of the thumbnail is file.source itself # TODO: For an image, source of the thumbnail is file.source itself
if file.is_image: if file.is_image:
image_field = file.thumbnail thumbnail_field = file.thumbnail
if not image_field: if not thumbnail_field:
# Use the source image if max-size thumbnail is not yet available # Use the source image if max-size thumbnail is not yet available
image_field.name = file.source.name thumbnail_field.name = file.source.name
# base_path = files.utils.get_base_path(image_field.name)
thumbnails_paths = file.thumbnails thumbnails_paths = file.thumbnails
files.utils.make_thumbnails( files.utils.make_thumbnails(
abs_path, abs_path,
@ -52,8 +51,18 @@ def make_thumbnails(file_id: int) -> None:
optimize=True, optimize=True,
progressive=True, progressive=True,
) )
image_field.name = thumbnails_paths[THUMBNAIL_SIZE_DEFAULT] thumbnail_default_path = thumbnails_paths[THUMBNAIL_SIZE_DEFAULT]
file.save(update_fields={'thumbnail'}) # Reverse to make JSON-serialisable
thumbnail_metadata = {path: size for size, path in thumbnails_paths.items()}
update_fields = {}
if thumbnail_default_path != thumbnail_field.name:
thumbnail_field.name = thumbnail_default_path
update_fields.add('thumbnail')
if file.metadata.get('thumbnails') != thumbnail_metadata:
file.metadata.update({'thumbnails': thumbnail_metadata})
update_fields.add('metadata')
if update_fields:
file.save(update_fields=update_fields)
# TODO: For a video, source of the thumbnail is some frame fetched with ffpeg # TODO: For a video, source of the thumbnail is some frame fetched with ffpeg
elif file.is_video: elif file.is_video:
raise NotImplementedError raise NotImplementedError

View File

@ -42,9 +42,11 @@ class FileTest(TestCase):
'new_state': {'status': 'Approved'}, 'new_state': {'status': 'Approved'},
'object': '<File: test.zip (Approved)>', 'object': '<File: test.zip (Approved)>',
'old_state': { 'old_state': {
'status': 2,
'hash': 'foobar', 'hash': 'foobar',
'metadata': {},
'size_bytes': 7149, 'size_bytes': 7149,
'status': 2,
'thumbnail': '',
}, },
} }
}, },