mirror of
https://github.com/Xevion/simple-viewer.git
synced 2025-12-06 01:16:24 -06:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Repository specific
|
||||
.idea/**
|
||||
viewer/static/thumbnails/**
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -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)
|
||||
|
||||
18
viewer/migrations/0003_auto_20201031_2007.py
Normal file
18
viewer/migrations/0003_auto_20201031_2007.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
24
viewer/migrations/0004_file.py
Normal file
24
viewer/migrations/0004_file.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
18
viewer/migrations/0006_file_thumbnail.py
Normal file
18
viewer/migrations/0006_file_thumbnail.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
115
viewer/models.py
115
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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
]
|
||||
|
||||
@@ -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,)))
|
||||
|
||||
Reference in New Issue
Block a user