Merge pull request #3 from Xevion/thumbnails

Thumbnails
This commit is contained in:
Xevion
2020-10-31 23:08:46 -05:00
committed by GitHub
10 changed files with 305 additions and 55 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Repository specific # Repository specific
.idea/** .idea/**
viewer/static/thumbnails/**
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

View File

@@ -1,42 +1,28 @@
import os
from typing import List, Tuple
def extra_listdir(path: str) -> List[Tuple[str, str]]:
""" """
Helper function used for identifying file media type for every file in a given directory, extending os.listdir helpers.py
:param path: The path to the directory. Contains helper functions used as refactored shortcuts or in order to separate code for readability.
:return: A list of tuples, each containing two strings, the file or directory name, and the media type.
""" """
files = [] import cv2
for file in os.listdir(path): from PIL import Image
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
def get_all_mediatype(head: str, tail: str) -> str: def generate_thumbnail(path: str, output_path: str) -> None:
""" """
A extra media type function supporting directories on top of files. Helper function which completes the process of generating thumbnails for both pictures and videos.
:param head: The head of the path, usually the directory name or filename at the very end. :param path: The absolute path to the file.
:param tail: The rest of the path, everything that comes before the head. :param output_path: The absolute path to the intended output thumbnail file.
:return: A media type in string form.
""" """
if os.path.isfile(os.path.join(tail, head)): vidcap = cv2.VideoCapture(path)
return get_file_mediatype(head) success, image = vidcap.read()
return "folder" 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_file_mediatype(mimetype: str) -> str: im_pil.save(output_path)
"""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'

View File

@@ -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'),
),
]

View File

@@ -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')),
],
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -1,6 +1,16 @@
import mimetypes
import os
import uuid import uuid
import jsonfield
from django.db import models 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): class ServedDirectory(models.Model):
@@ -19,6 +29,111 @@ class ServedDirectory(models.Model):
regex_pattern = models.CharField('RegEx Matching Pattern', max_length=100, default='') regex_pattern = models.CharField('RegEx Matching Pattern', max_length=100, default='')
regex = models.BooleanField('Directory RegEx Option', default=False) regex = models.BooleanField('Directory RegEx Option', default=False)
match_filename = models.BooleanField('RegEx Matches Against Filename', default=True) 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): def __str__(self):
return self.path 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

View File

@@ -1,5 +1,59 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block head %}
{{ block.super }}
<style>
.media {
width: 100%;
{#height: 3em;#}
}
.media .media-filename {
text-decoration: underline;
text-decoration-color: #3273dc;
}
</style>
{% endblock head %}
{% block content %} {% block content %}
<div class="card">
<div class="card-header">
<p class="card-header-title">
{{ directory.path }}
<span class="pl-1" style="font-weight: 400; font-style: italic; font-size: 70%;">
{{ files|length }} files
</span>
<span class="icon align-self">
<a href="{% url 'refresh' directory.id %}">
<i class="fas fa-sync"></i>
</a>
</span>
</p>
</div>
<div class="card-content">
<div class="content">
{% for directory in directories %}
<div>
<span class="icon">
<i class="fas fa-folder"></i>
</span>
{{ directory }}
</div>
{% endfor %}
{% for file in files %}
<div class="media">
{% load static %}
<img class="px-2" loading="lazy" src="{% static "/thumbnails/"|add:file.thumbnail %}">
<span class="media-filename">
<a href="{% url 'file' directory.id file.filename %}">
{{ file.filename }}
</a>
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="panel"> <div class="panel">
<div class="panel-heading"> <div class="panel-heading">
{{ directory.path }} {{ directory.path }}
@@ -15,16 +69,15 @@
{% for file in files %} {% for file in files %}
<div class="panel-block"> <div class="panel-block">
<span class="panel-icon pr-4"> <span class="panel-icon pr-4">
<i class="fas fa-{{ file.1 }} fa-lg" aria-hidden="true"></i> {# <i class="fas fa-{{ file }} fa-lg" aria-hidden="true"></i>#}
</span> </span>
{% if file.1 == 'folder' %} {% if file.1 == 'folder' %}
<a href="{% url 'add' %}?path={{ file.2 }}"> <a href="{% url 'add' %}?path={{ file.fullpath }}">
{{ file.0 }} {{ file.filename }}
</a> </a>
{% else %} {% else %}
<a href="{% url 'file' directory.id file.0 %}"> <a href="{% url 'file' directory.id file.filename %}">
{{ file.0 }} {{ file.filename }}
{{ file.0 }}
</a> </a>
{% endif %} {% endif %}
</div> </div>
@@ -33,6 +86,5 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg=="
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
{% load static %}
<script src="{% static "hover.js" %}"></script> <script src="{% static "hover.js" %}"></script>
{% endblock content %} {% endblock content %}

View File

@@ -7,5 +7,8 @@ urlpatterns = [
path('add/', views.add, name='add'), path('add/', views.add, name='add'),
path('add/submit', views.submit_new, name='add_submit'), path('add/submit', views.submit_new, name='add_submit'),
path('<uuid:directory_id>/', views.browse, name='browse'), path('<uuid:directory_id>/', views.browse, name='browse'),
path('<uuid:directory_id>/<str:file>/', views.file, name='file') path('<uuid:directory_id>/refresh', views.refresh, name='refresh'),
path('<uuid:directory_id>/<str:file>/', views.file, name='file'),
path('<uuid:directory_id>/<str:file>/generate', views.generate_thumb, name='generate_thumb'),
] ]

View File

@@ -4,8 +4,7 @@ from django.http import FileResponse, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.urls import reverse from django.urls import reverse
from viewer.helpers import extra_listdir from viewer.models import ServedDirectory, File
from viewer.models import ServedDirectory
def index(request): def index(request):
@@ -17,29 +16,29 @@ def index(request):
def browse(request, directory_id): def browse(request, directory_id):
dir = get_object_or_404(ServedDirectory, id=directory_id) directory = get_object_or_404(ServedDirectory, id=directory_id)
if os.path.isdir(dir.path): if os.path.isdir(directory.path):
context = { context = {
'title': f'Browse - {os.path.dirname(dir.path)}', 'title': f'Browse - {os.path.dirname(directory.path)}',
'files': extra_listdir(dir.path), 'files': directory.files.all(),
'directory': dir 'directory': directory
} }
return render(request, 'browse.html', context) return render(request, 'browse.html', context)
else: else:
context = { context = {
'title': 'Invalid Directory', 'title': 'Invalid Directory',
'message': 'The path this server directory points to {}.'.format( 'message': 'The path this server directory points to {}.'.format(
'exists, but is not a directory' if os.path.exists(dir.path) else 'does not exist' 'exists, but is not a directory' if os.path.exists(directory.path) else 'does not exist'
) )
} }
return render(request, 'message.html', context, status=500) return render(request, 'message.html', context, status=500)
def file(request, directory_id, file): def file(request, directory_id, file):
dir = get_object_or_404(ServedDirectory, id=directory_id) directory = get_object_or_404(ServedDirectory, id=directory_id)
if os.path.isdir(dir.path): if os.path.isdir(directory.path):
path = os.path.join(dir.path, file) path = os.path.join(directory.path, file)
if os.path.exists(path): if os.path.exists(path):
return FileResponse(open(path, 'rb')) return FileResponse(open(path, 'rb'))
else: else:
@@ -51,7 +50,7 @@ def file(request, directory_id, file):
context = { context = {
'title': 'Invalid Directory', 'title': 'Invalid Directory',
'message': 'The path this server directory points to {}.'.format( 'message': 'The path this server directory points to {}.'.format(
'exists, but is not a directory' if os.path.exists(dir.path) else 'does not exist' 'exists, but is not a directory' if os.path.exists(directory.path) else 'does not exist'
) )
} }
return render(request, 'message.html', context, status=500) return render(request, 'message.html', context, status=500)
@@ -64,6 +63,13 @@ def add(request):
return render(request, 'add.html', context) return render(request, 'add.html', context)
def refresh(request, directory_id):
"""A simple API view for refreshing a directory. May schedule new thumbnail generation."""
directory = get_object_or_404(ServedDirectory, id=directory_id)
directory.refresh()
return HttpResponseRedirect(reverse('browse', args=(directory.id,)))
def submit_new(request): def submit_new(request):
try: try:
s = ServedDirectory( s = ServedDirectory(
@@ -87,3 +93,11 @@ def submit_new(request):
'message': 'The directory you specified was not a valid directory, either it doesn\'t ' 'message': 'The directory you specified was not a valid directory, either it doesn\'t '
'exist or it isn\'t a directory.'}) 'exist or it isn\'t a directory.'})
return HttpResponseRedirect(reverse('browse', args=(s.id,))) return HttpResponseRedirect(reverse('browse', args=(s.id,)))
def generate_thumb(request, directory_id, file: str):
"""View for regenerating a thumbnail for a specific file."""
directory = get_object_or_404(ServedDirectory, id=directory_id)
file = directory.files.filter(filename=file).first()
file.generate_thumbnail()
return HttpResponseRedirect(reverse('browse', args=(directory.id,)))