Files
NibasaViewer/viewer/views.py

428 lines
12 KiB
Python

# Standard library imports.
from pathlib import Path
from urllib.parse import urlencode
from functools import cmp_to_key
# Django imports.
from django.http import HttpResponseNotFound
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
# Project imports.
from .utils import make_thumbnail, is_image_file
###########################################################################################
# Constants. #
###########################################################################################
SORT_OPTIONS = [
("abc", "Alphabetical A-Z"),
("cba", "Alphabetical Z-A"),
("old", "Creation date old to new"),
("new", "Creation date new to old"),
("recent", "Modification date most recent"),
("tnecer", "Modification date least recent"),
]
SORT_LABELS = dict(SORT_OPTIONS)
###########################################################################################
# Helper functions. #
###########################################################################################
def normalize_sort(sort_value):
return sort_value if sort_value in SORT_LABELS else "abc"
def normalize_theme(theme_value):
return theme_value if theme_value in ("dark", "light") else "dark"
def get_creation_timestamp(path_obj):
try:
stat_data = path_obj.stat()
return getattr(stat_data, "st_birthtime", stat_data.st_ctime)
except OSError:
return 0
def get_modification_timestamp(path_obj):
try:
return path_obj.stat().st_mtime
except OSError:
return 0
def build_query(search_text, sort_key, theme):
query = {"sort": sort_key, "theme": theme}
if search_text != "":
query["search"] = search_text
return query
def append_query(url, query_dict):
if len(query_dict) == 0:
return url
return url + "?" + urlencode(query_dict)
def gallery_url(path_obj=None, is_dir=False, query_dict=None):
if query_dict is None:
query_dict = {}
if path_obj is None:
base_url = "/gallery/"
else:
path_text = str(path_obj).replace("\\", "/")
base_url = "/gallery/" + path_text
if is_dir and not base_url.endswith("/"):
base_url += "/"
return append_query(base_url, query_dict)
def do_recursive_search(start_path, query):
"""
Gets all images and sub-directories inside the start_path whose name matches the given query,
and then joins the results recursively by iterating in each sub-directory.
"""
try:
entries = [entry for entry in start_path.iterdir() if not entry.is_symlink()]
except OSError:
return [], []
all_subdirs = sorted(
[entry for entry in entries if entry.is_dir()],
key=lambda item: (item.name.lower(), str(item).lower()),
)
subdirs = [entry for entry in all_subdirs if query.lower() in entry.name.lower()]
images = sorted(
[
entry
for entry in entries
if entry.is_file()
and is_image_file(entry)
and query.lower() in entry.name.lower()
],
key=lambda item: (item.name.lower(), str(item).lower()),
)
for subdir in all_subdirs:
rec_subdirs, rec_images = do_recursive_search(subdir, query.lower())
subdirs.extend(rec_subdirs)
images.extend(rec_images)
return subdirs, images
def sort_images(images, sort_key):
def compare(img_a, img_b):
name_a = img_a.name.lower()
name_b = img_b.name.lower()
rel_a = str(img_a.relative_to(settings.GALLERY_ROOT)).lower()
rel_b = str(img_b.relative_to(settings.GALLERY_ROOT)).lower()
if sort_key == "abc":
if name_a < name_b:
return -1
if name_a > name_b:
return 1
elif sort_key == "cba":
if name_a > name_b:
return -1
if name_a < name_b:
return 1
elif sort_key == "old":
created_a = get_creation_timestamp(img_a)
created_b = get_creation_timestamp(img_b)
if created_a < created_b:
return -1
if created_a > created_b:
return 1
elif sort_key == "new":
created_a = get_creation_timestamp(img_a)
created_b = get_creation_timestamp(img_b)
if created_a > created_b:
return -1
if created_a < created_b:
return 1
elif sort_key == "recent":
modified_a = get_modification_timestamp(img_a)
modified_b = get_modification_timestamp(img_b)
if modified_a > modified_b:
return -1
if modified_a < modified_b:
return 1
elif sort_key == "tnecer":
modified_a = get_modification_timestamp(img_a)
modified_b = get_modification_timestamp(img_b)
if modified_a < modified_b:
return -1
if modified_a > modified_b:
return 1
if name_a < name_b:
return -1
if name_a > name_b:
return 1
if rel_a < rel_b:
return -1
if rel_a > rel_b:
return 1
return 0
return sorted(images, key=cmp_to_key(compare))
def build_breadcrumbs(path_text, query_dict):
breadcrumbs = [{"label": "Gallery", "path": gallery_url(None, True, query_dict)}]
if path_text == "":
return breadcrumbs
segments = Path(path_text).parts
current = Path("")
for segment in segments:
current = current.joinpath(segment)
breadcrumbs.append(
{"label": segment, "path": gallery_url(current, True, query_dict)}
)
return breadcrumbs
def build_sort_options(request, search_text, sort_key, theme):
options = []
for option_key, label in SORT_OPTIONS:
query = {"sort": option_key, "theme": theme}
if search_text != "":
query["search"] = search_text
options.append(
{
"key": option_key,
"label": label,
"url": append_query(request.path, query),
"is_active": option_key == sort_key,
}
)
return options
def get_first_image_thumbnail_url(subdir):
try:
images = sorted(
[
entry
for entry in subdir.iterdir()
if entry.is_file() and not entry.is_symlink() and is_image_file(entry)
],
key=lambda item: (
item.name.lower(),
str(item.relative_to(settings.GALLERY_ROOT)).lower(),
),
)
except OSError:
return None
if len(images) == 0:
return None
first_image = images[0]
try:
make_thumbnail(first_image)
rel_path = first_image.relative_to(settings.GALLERY_ROOT)
thumb_path = settings.THUMBNAILS_ROOT.joinpath(rel_path)
if thumb_path.exists():
return "/thumbs/" + str(rel_path).replace("\\", "/")
except Exception:
pass
return None
###########################################################################################
# View functions. #
###########################################################################################
@login_required
def index(request):
return redirect("gallery_view_root")
def _render_directory(request, path_text, full_path):
"""
Renders the gallery view related to directories, be it the contents of an actual directory
in the file system, or logical gallery directories like search result pages.
"""
search_text = request.GET.get("search", "").strip()
sort_key = normalize_sort(request.GET.get("sort", "abc"))
theme = normalize_theme(request.GET.get("theme", "dark"))
query_state = build_query(search_text, sort_key, theme)
try:
current_entries = [
entry for entry in full_path.iterdir() if not entry.is_symlink()
]
except OSError:
return HttpResponseNotFound("Not found")
current_subdirs = sorted(
[entry for entry in current_entries if entry.is_dir()],
key=lambda item: (
item.name.lower(),
str(item.relative_to(settings.GALLERY_ROOT)).lower(),
),
)
if search_text == "":
images = [
entry
for entry in current_entries
if entry.is_file() and is_image_file(entry)
]
else:
_, images = do_recursive_search(full_path, search_text)
images = sort_images(images, sort_key)
image_data = []
for image in images:
rel_path = image.relative_to(settings.GALLERY_ROOT)
image_url = gallery_url(rel_path, False, query_state)
thumbnail = None
try:
make_thumbnail(image)
thumb_path = settings.THUMBNAILS_ROOT.joinpath(rel_path)
if thumb_path.exists():
thumbnail = "/thumbs/" + str(rel_path).replace("\\", "/")
except Exception:
pass
image_data.append(
{"path": image_url, "name": image.name, "thumbnail": thumbnail}
)
subdir_data = []
for subdir in current_subdirs:
rel_path = subdir.relative_to(settings.GALLERY_ROOT)
subdir_data.append(
{
"path": gallery_url(rel_path, True, query_state),
"name": subdir.name,
"thumbnail": get_first_image_thumbnail_url(subdir),
}
)
next_theme = "light" if theme == "dark" else "dark"
theme_query = {"sort": sort_key, "theme": next_theme}
if search_text != "":
theme_query["search"] = search_text
search_action_query = {"sort": sort_key, "theme": theme}
context = {
"path": path_text,
"search_text": search_text,
"theme": theme,
"sort_key": sort_key,
"sort_label": SORT_LABELS[sort_key],
"sort_options": build_sort_options(request, search_text, sort_key, theme),
"breadcrumbs": build_breadcrumbs(path_text, query_state),
"images": image_data,
"subdirs": subdir_data,
"theme_toggle_url": append_query(request.path, theme_query),
"search_action_url": append_query(request.path, search_action_query),
}
return render(request, "gallery_view.html", context)
def _render_image(request, path_text, full_path):
"""
Renders the view corresponding to an image file.
"""
image = Path("/imgs/").joinpath(path_text)
img_dir = full_path.parent
try:
siblings = [
entry.name
for entry in img_dir.iterdir()
if entry.is_file() and not entry.is_symlink() and is_image_file(entry)
]
except OSError:
return HttpResponseNotFound("Not found")
images = sorted(siblings, key=lambda item: item.lower())
if image.name not in images:
return HttpResponseNotFound("Not found")
index = images.index(image.name)
previous = index - 1 if index > 0 else None
following = index + 1 if index < len(images) - 1 else None
context = {
"image_path": image,
"prev": images[previous] if previous is not None else None,
"next": images[following] if following is not None else None,
}
return render(request, "image_view.html", context)
@login_required
def gallery_view(request, path=None):
"""
Shows a list of subdirectories and image files inside the given path.
The path should be inside the GALLERY_ROOT path, otherwise a 404 error will be thrown.
"""
root = settings.GALLERY_ROOT.resolve()
try:
candidate = root.joinpath(path).resolve() if path is not None else root
candidate.relative_to(root)
except Exception:
return HttpResponseNotFound("Not found")
if not candidate.exists():
return HttpResponseNotFound("Not found")
path_text = path if path is not None else ""
if candidate.is_dir():
return _render_directory(request, path_text, candidate)
return _render_image(request, path_text, candidate)