diff --git a/.gitignore b/.gitignore index 43660d2..9159069 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Repository specific .idea/** +viewer/static/thumbnails/** # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/viewer/helpers.py b/viewer/helpers.py index 101871b..b37ee7b 100644 --- a/viewer/helpers.py +++ b/viewer/helpers.py @@ -1,42 +1,28 @@ -import os -from typing import List, Tuple +""" +helpers.py + +Contains helper functions used as refactored shortcuts or in order to separate code for readability. +""" +import cv2 +from PIL import Image -def extra_listdir(path: str) -> List[Tuple[str, str]]: +def generate_thumbnail(path: str, output_path: str) -> None: """ - Helper function used for identifying file media type for every file in a given directory, extending os.listdir + Helper function which completes the process of generating thumbnails for both pictures and videos. - :param path: The path to the directory. - :return: A list of tuples, each containing two strings, the file or directory name, and the media type. + :param path: The absolute path to the file. + :param output_path: The absolute path to the intended output thumbnail file. """ - files = [] - for file in os.listdir(path): - mediatype = get_all_mediatype(file, path) - if mediatype == 'folder': - files.append((file, mediatype, os.path.join(path, file))) - else: - files.append((file, mediatype)) - return files + vidcap = cv2.VideoCapture(path) + success, image = vidcap.read() + if success: + img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + im_pil = Image.fromarray(img) + # Resize, crop, thumbnail + im_pil.thumbnail((300, 300)) + # im_pil.crop((0, 0, 200, 66)) + # im_pil.resize((200, 66)) -def get_all_mediatype(head: str, tail: str) -> str: - """ - A extra media type function supporting directories on top of files. - - :param head: The head of the path, usually the directory name or filename at the very end. - :param tail: The rest of the path, everything that comes before the head. - :return: A media type in string form. - """ - if os.path.isfile(os.path.join(tail, head)): - return get_file_mediatype(head) - return "folder" - - -def get_file_mediatype(mimetype: str) -> str: - """Simple media type categorization based on the given mimetype""" - if mimetype is not None: - if mimetype.startswith('image'): - return 'image' - elif mimetype.startswith('video'): - return 'video' - return 'file' + im_pil.save(output_path) diff --git a/viewer/migrations/0003_auto_20201031_2007.py b/viewer/migrations/0003_auto_20201031_2007.py new file mode 100644 index 0000000..ba6b560 --- /dev/null +++ b/viewer/migrations/0003_auto_20201031_2007.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.2 on 2020-11-01 01:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0002_auto_20201031_0526'), + ] + + operations = [ + migrations.AlterField( + model_name='serveddirectory', + name='regex_pattern', + field=models.CharField(default='', max_length=100, verbose_name='RegEx Matching Pattern'), + ), + ] diff --git a/viewer/migrations/0004_file.py b/viewer/migrations/0004_file.py new file mode 100644 index 0000000..0999c99 --- /dev/null +++ b/viewer/migrations/0004_file.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.2 on 2020-11-01 01:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0003_auto_20201031_2007'), + ] + + operations = [ + migrations.CreateModel( + name='File', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('path', models.CharField(max_length=300, verbose_name='Full Filepath')), + ('filename', models.CharField(max_length=160, verbose_name='Filename')), + ('mediatype', models.CharField(max_length=30, verbose_name='Mediatype')), + ('directory', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='viewer.serveddirectory')), + ], + ), + ] diff --git a/viewer/migrations/0005_serveddirectory_known_subdirectories.py b/viewer/migrations/0005_serveddirectory_known_subdirectories.py new file mode 100644 index 0000000..c321fd9 --- /dev/null +++ b/viewer/migrations/0005_serveddirectory_known_subdirectories.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.2 on 2020-11-01 01:46 + +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0004_file'), + ] + + operations = [ + migrations.AddField( + model_name='serveddirectory', + name='known_subdirectories', + field=jsonfield.fields.JSONField(default=[], verbose_name='Tracked Subdirectories JSON'), + ), + ] diff --git a/viewer/migrations/0006_file_thumbnail.py b/viewer/migrations/0006_file_thumbnail.py new file mode 100644 index 0000000..1d7f0bb --- /dev/null +++ b/viewer/migrations/0006_file_thumbnail.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.2 on 2020-11-01 03:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0005_serveddirectory_known_subdirectories'), + ] + + operations = [ + migrations.AddField( + model_name='file', + name='thumbnail', + field=models.CharField(default=None, max_length=160, null=True, verbose_name='Thumbnail Filename'), + ), + ] diff --git a/viewer/models.py b/viewer/models.py index 9259d89..b167910 100644 --- a/viewer/models.py +++ b/viewer/models.py @@ -1,6 +1,16 @@ +import mimetypes +import os import uuid +import jsonfield from django.db import models +from django.urls import reverse +from easy_thumbnails.alias import aliases + +from viewer import helpers + +if not aliases.get('small'): + aliases.set('small', {'size': (150, 80), 'crop': True}) class ServedDirectory(models.Model): @@ -19,6 +29,111 @@ class ServedDirectory(models.Model): regex_pattern = models.CharField('RegEx Matching Pattern', max_length=100, default='') regex = models.BooleanField('Directory RegEx Option', default=False) match_filename = models.BooleanField('RegEx Matches Against Filename', default=True) + known_subdirectories = jsonfield.JSONField('Tracked Subdirectories JSON', default=[]) + + def refresh(self): + """Refresh the directory listing to see if any new files have appeared and add them to the list.""" + # TODO: Implement separate recursive file matching implementation + # TODO: Implement RegEx filtering step + directories = [] + files = os.listdir(self.path) + for i, file in enumerate(files): + print(f'{i} / {len(files)}') + file_path = os.path.join(self.path, file) + + if os.path.isfile(file_path): + # Check if the file has been entered before + entry: File + entry = self.files.filter(filename__exact=file).first() + if entry is None: + # create the file entry + entry = File.create(full_path=file_path, parent=self) + entry.save() + else: + if entry.thumbnail is None: + entry.generate_thumbnail() + else: + # directory found, remember it + directories.append(file_path) + + # Dump subdirectories found + self.known_subdirectories = directories def __str__(self): return self.path + + +class File(models.Model): + path = models.CharField('Full Filepath', max_length=300) + filename = models.CharField('Filename', max_length=160) + mediatype = models.CharField('Mediatype', max_length=30) + directory = models.ForeignKey(ServedDirectory, on_delete=models.CASCADE, related_name='files') + thumbnail = models.CharField('Thumbnail Filename', max_length=160, null=True, default=None) + + @classmethod + def create(cls, full_path: str, parent: ServedDirectory) -> 'File': + """Simple shortcut for creating a File database entry with just the path.""" + return File( + path=full_path, + filename=os.path.basename(full_path), + mediatype=File.get_mediatype(full_path), + directory=parent + ) + + def get_url(self, directory: ServedDirectory) -> str: + """Retrieve the direct URL for a given file.""" + return reverse('file', args=(directory.id, self.filename)) + + def delete_thumbnail(self) -> None: + if self.thumbnail: + try: + os.remove(os.path.join(self.thumbs_dir, self.thumbnail)) + self.thumbnail = None + self.save() + except FileNotFoundError: + pass + + @property + def thumbnail_url(self): + if self.thumbnail: + return f'/thumbnails/{self.thumbnail}' + return '' + + @property + def thumbs_dir(self): + return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'thumbnails') + + def generate_thumbnail(self) -> None: + # TODO: Add django-background-task scheduling + + self.delete_thumbnail() + + thumb_file = f'{uuid.uuid4()}.jpeg' + self.thumbnail = thumb_file + + # Generate thumbnail + try: + helpers.generate_thumbnail(self.path, os.path.join(self.thumbs_dir, self.thumbnail)) + except Exception: + print(f'Could not thumbnail: {self.filename}') + self.delete_thumbnail() + + self.save() + + @staticmethod + def get_mediatype(path) -> str: + """Simple media type categorization based on the given path.""" + if os.path.exists(path): + if os.path.isdir(path): + return 'folder' + mimetype = mimetypes.guess_type(path)[0] + if mimetype is not None: + if mimetype.startswith('image'): + return 'image' + elif mimetype.startswith('video'): + return 'video' + return 'file' + return 'unknown' + + def __str__(self) -> str: + return self.filename diff --git a/viewer/templates/browse.html b/viewer/templates/browse.html index 5cbecf4..296a96f 100644 --- a/viewer/templates/browse.html +++ b/viewer/templates/browse.html @@ -1,5 +1,59 @@ {% extends 'base.html' %} +{% block head %} + {{ block.super }} + +{% endblock head %} {% block content %} +
+ {{ directory.path }} + + {{ files|length }} files + + + + + + +
+