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 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. :param path: The absolute path to the file.
:return: A list of tuples, each containing two strings, the file or directory name, and the media type. :param output_path: The absolute path to the intended output thumbnail file.
""" """
files = [] vidcap = cv2.VideoCapture(path)
for file in os.listdir(path): success, image = vidcap.read()
mediatype = get_all_mediatype(file, path) if success:
if mediatype == 'folder': img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
files.append((file, mediatype, os.path.join(path, file))) im_pil = Image.fromarray(img)
else:
files.append((file, mediatype))
return files
# 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: im_pil.save(output_path)
"""
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'

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,)))