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
.idea/**
viewer/static/thumbnails/**
# Byte-compiled / optimized / DLL files
__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.
: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)

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

View File

@@ -1,5 +1,59 @@
{% 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 %}
<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-heading">
{{ directory.path }}
@@ -15,16 +69,15 @@
{% for file in files %}
<div class="panel-block">
<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>
{% if file.1 == 'folder' %}
<a href="{% url 'add' %}?path={{ file.2 }}">
{{ file.0 }}
<a href="{% url 'add' %}?path={{ file.fullpath }}">
{{ file.filename }}
</a>
{% else %}
<a href="{% url 'file' directory.id file.0 %}">
{{ file.0 }}
{{ file.0 }}
<a href="{% url 'file' directory.id file.filename %}">
{{ file.filename }}
</a>
{% endif %}
</div>
@@ -33,6 +86,5 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg=="
crossorigin="anonymous"></script>
{% load static %}
<script src="{% static "hover.js" %}"></script>
{% endblock content %}

View File

@@ -7,5 +7,8 @@ urlpatterns = [
path('add/', views.add, name='add'),
path('add/submit', views.submit_new, name='add_submit'),
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.urls import reverse
from viewer.helpers import extra_listdir
from viewer.models import ServedDirectory
from viewer.models import ServedDirectory, File
def index(request):
@@ -17,29 +16,29 @@ def index(request):
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 = {
'title': f'Browse - {os.path.dirname(dir.path)}',
'files': extra_listdir(dir.path),
'directory': dir
'title': f'Browse - {os.path.dirname(directory.path)}',
'files': directory.files.all(),
'directory': directory
}
return render(request, 'browse.html', context)
else:
context = {
'title': 'Invalid Directory',
'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)
def file(request, directory_id, file):
dir = get_object_or_404(ServedDirectory, id=directory_id)
if os.path.isdir(dir.path):
path = os.path.join(dir.path, file)
directory = get_object_or_404(ServedDirectory, id=directory_id)
if os.path.isdir(directory.path):
path = os.path.join(directory.path, file)
if os.path.exists(path):
return FileResponse(open(path, 'rb'))
else:
@@ -51,7 +50,7 @@ def file(request, directory_id, file):
context = {
'title': 'Invalid Directory',
'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)
@@ -64,6 +63,13 @@ def add(request):
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):
try:
s = ServedDirectory(
@@ -87,3 +93,11 @@ def submit_new(request):
'message': 'The directory you specified was not a valid directory, either it doesn\'t '
'exist or it isn\'t a directory.'})
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,)))