checkpoint

This commit is contained in:
voussoir 2016-10-29 18:46:23 -07:00
parent 198900c990
commit 25a4b69cd8
5 changed files with 185 additions and 33 deletions

View file

@ -33,6 +33,7 @@ MOTD_STRINGS = [
#'Buckle up, it\'s time to:', #'Buckle up, it\'s time to:',
] ]
THUMBDIR = phototagger.DEFAULT_THUMBDIR
ERROR_INVALID_ACTION = 'Invalid action' ERROR_INVALID_ACTION = 'Invalid action'
ERROR_NO_TAG_GIVEN = 'No tag name supplied' ERROR_NO_TAG_GIVEN = 'No tag name supplied'
ERROR_TAG_TOO_SHORT = 'Not enough valid chars' ERROR_TAG_TOO_SHORT = 'Not enough valid chars'
@ -317,7 +318,7 @@ def get_album_html(albumid):
response = flask.render_template( response = flask.render_template(
'album.html', 'album.html',
album=album, album=album,
child_albums=album['sub_albums'], child_albums=[jsonify_album(P_album(x)) for x in album['sub_albums']],
photos=album['photos'], photos=album['photos'],
) )
return response return response
@ -392,7 +393,8 @@ def get_photo_json(photoid):
def get_search_core(): def get_search_core():
print(request.args) print(request.args)
# EXTENSION # FILENAME & EXTENSION
filename_terms = request.args.get('filename', None)
extension_string = request.args.get('extension', None) extension_string = request.args.get('extension', None)
extension_not_string = request.args.get('extension_not', None) extension_not_string = request.args.get('extension_not', None)
mimetype_string = request.args.get('mimetype', None) mimetype_string = request.args.get('mimetype', None)
@ -464,6 +466,7 @@ def get_search_core():
'created': created, 'created': created,
'extension': extension_list, 'extension': extension_list,
'extension_not': extension_not_list, 'extension_not': extension_not_list,
'filename': filename_terms,
'has_tags': has_tags, 'has_tags': has_tags,
'mimetype': mimetype_list, 'mimetype': mimetype_list,
'tag_musts': tag_musts, 'tag_musts': tag_musts,

View file

@ -2,6 +2,8 @@
# py -i etiquette_easy.py # py -i etiquette_easy.py
import phototagger import phototagger
import os
import sys
P = phototagger.PhotoDB() P = phototagger.PhotoDB()
import traceback import traceback

View file

@ -61,6 +61,7 @@ SQL_ALBUM_COLUMNS = [
'id', 'id',
'title', 'title',
'description', 'description',
'associated_directory'
] ]
SQL_PHOTO_COLUMNS = [ SQL_PHOTO_COLUMNS = [
'id', 'id',
@ -112,11 +113,12 @@ PRAGMA cache_size = 10000;
CREATE TABLE IF NOT EXISTS albums( CREATE TABLE IF NOT EXISTS albums(
id TEXT, id TEXT,
title TEXT, title TEXT,
description TEXT description TEXT,
associated_directory TEXT COLLATE NOCASE
); );
CREATE TABLE IF NOT EXISTS photos( CREATE TABLE IF NOT EXISTS photos(
id TEXT, id TEXT,
filepath TEXT, filepath TEXT COLLATE NOCASE,
extension TEXT, extension TEXT,
width INT, width INT,
height INT, height INT,
@ -160,7 +162,7 @@ CREATE INDEX IF NOT EXISTS index_albumrel_photoid on album_photo_rel(photoid);
-- Photo -- Photo
CREATE INDEX IF NOT EXISTS index_photo_id on photos(id); CREATE INDEX IF NOT EXISTS index_photo_id on photos(id);
CREATE INDEX IF NOT EXISTS index_photo_path on photos(filepath); CREATE INDEX IF NOT EXISTS index_photo_path on photos(filepath COLLATE NOCASE);
CREATE INDEX IF NOT EXISTS index_photo_created on photos(created); CREATE INDEX IF NOT EXISTS index_photo_created on photos(created);
CREATE INDEX IF NOT EXISTS index_photo_extension on photos(extension); CREATE INDEX IF NOT EXISTS index_photo_extension on photos(extension);
@ -213,6 +215,10 @@ def _helper_extension(ext):
ext = set(ext) ext = set(ext)
return ext return ext
def _helper_filenamefilter(subject, terms):
basename = subject.lower()
return all(term in basename for term in terms)
def _helper_minmax(key, value, minimums, maximums): def _helper_minmax(key, value, minimums, maximums):
''' '''
When searching, this function dissects a hyphenated range string When searching, this function dissects a hyphenated range string
@ -686,10 +692,27 @@ class PDBAlbumMixin:
def get_album(self, id): def get_album(self, id):
return self.get_thing_by_id('album', id) return self.get_thing_by_id('album', id)
def get_album_by_path(self, filepath):
'''
Return the album with the `associated_directory` of this value, NOT case-sensitive.
'''
filepath = os.path.abspath(filepath)
self.cur.execute('SELECT * FROM albums WHERE associated_directory == ?', [filepath])
f = self.cur.fetchone()
if f is None:
raise NoSuchAlbum(filepath)
return self.get_album(f[SQL_ALBUM['id']])
def get_albums(self): def get_albums(self):
yield from self.get_things(thing_type='album') yield from self.get_things(thing_type='album')
def new_album(self, title=None, description=None, photos=None, commit=True): def new_album(self,
associated_directory=None,
commit=True,
description=None,
photos=None,
title=None,
):
''' '''
Create a new album. Photos can be added now or later. Create a new album. Photos can be added now or later.
''' '''
@ -697,6 +720,9 @@ class PDBAlbumMixin:
albumid = self.generate_id('tags') albumid = self.generate_id('tags')
title = title or '' title = title or ''
description = description or '' description = description or ''
if associated_directory is not None:
associated_directory = os.path.abspath(associated_directory)
if not isinstance(title, str): if not isinstance(title, str):
raise TypeError('Title must be string, not %s' % type(title)) raise TypeError('Title must be string, not %s' % type(title))
@ -707,8 +733,9 @@ class PDBAlbumMixin:
data[SQL_ALBUM['id']] = albumid data[SQL_ALBUM['id']] = albumid
data[SQL_ALBUM['title']] = title data[SQL_ALBUM['title']] = title
data[SQL_ALBUM['description']] = description data[SQL_ALBUM['description']] = description
data[SQL_ALBUM['associated_directory']] = associated_directory
self.cur.execute('INSERT INTO albums VALUES(?, ?, ?)', data) self.cur.execute('INSERT INTO albums VALUES(?, ?, ?, ?)', data)
album = Album(self, data) album = Album(self, data)
if photos: if photos:
for photo in photos: for photo in photos:
@ -716,6 +743,7 @@ class PDBAlbumMixin:
album.add_photo(photo, commit=False) album.add_photo(photo, commit=False)
if commit: if commit:
log.debug('Committing - new Album')
self.commit() self.commit()
return album return album
@ -851,6 +879,7 @@ class PDBPhotoMixin:
created=None, created=None,
extension=None, extension=None,
extension_not=None, extension_not=None,
filename=None,
has_tags=None, has_tags=None,
mimetype=None, mimetype=None,
tag_musts=None, tag_musts=None,
@ -878,6 +907,10 @@ class PDBPhotoMixin:
extension_not: extension_not:
A string or list of strings of unacceptable file extensions. A string or list of strings of unacceptable file extensions.
filename:
A string or list of strings which will be split into words. The file's basename
must include every word, NOT case-sensitive.
has_tags: has_tags:
If True, require that the Photo has >=1 tag. If True, require that the Photo has >=1 tag.
If False, require that the Photo has no tags. If False, require that the Photo has no tags.
@ -934,6 +967,11 @@ class PDBPhotoMixin:
extension_not = _helper_extension(extension_not) extension_not = _helper_extension(extension_not)
mimetype = _helper_extension(mimetype) mimetype = _helper_extension(mimetype)
if filename is not None:
if not isinstance(filename, str):
filename = ' '.join(filename)
filename = set(term.lower() for term in filename.strip().split(' '))
if (tag_musts or tag_mays or tag_forbids) and tag_expression: if (tag_musts or tag_mays or tag_forbids) and tag_expression:
raise XORException('Expression filter cannot be used with musts, mays, forbids') raise XORException('Expression filter cannot be used with musts, mays, forbids')
@ -984,6 +1022,11 @@ class PDBPhotoMixin:
continue continue
if mimetype and photo.mimetype() not in mimetype: if mimetype and photo.mimetype() not in mimetype:
#print('Failed mimetype')
continue
if filename and not _helper_filenamefilter(subject=photo.basename, terms=filename):
#print('Failed filename')
continue continue
if any(not fetch[SQL_PHOTO[key]] or fetch[SQL_PHOTO[key]] > value for (key, value) in maximums.items()): if any(not fetch[SQL_PHOTO[key]] or fetch[SQL_PHOTO[key]] > value for (key, value) in maximums.items()):
@ -1173,7 +1216,9 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
def digest_directory(self, directory, exclude_directories=None, exclude_filenames=None, commit=True): def digest_directory(self, directory, exclude_directories=None, exclude_filenames=None, commit=True):
''' '''
Create an album, and add the directory's contents to it. Create an album, and add the directory's contents to it recursively.
If a Photo object already exists for a file, it will be added to the correct album.
''' '''
if not os.path.isdir(directory): if not os.path.isdir(directory):
raise ValueError('Not a directory: %s' % directory) raise ValueError('Not a directory: %s' % directory)
@ -1189,22 +1234,41 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
] ]
directory = spinal.str_to_fp(directory) directory = spinal.str_to_fp(directory)
directory.correct_case()
generator = spinal.walk_generator( generator = spinal.walk_generator(
directory, directory,
exclude_directories=exclude_directories, exclude_directories=exclude_directories,
exclude_filenames=exclude_filenames, exclude_filenames=exclude_filenames,
yield_style='nested', yield_style='nested',
) )
album = self.new_album(title=directory.basename, commit=False) try:
album = self.get_album_by_path(directory.absolute_path)
except NoSuchAlbum:
album = self.new_album(
associated_directory=directory.absolute_path,
commit=False,
title=directory.basename,
)
albums = {directory.absolute_path: album} albums = {directory.absolute_path: album}
for (current_location, directories, files) in generator: for (current_location, directories, files) in generator:
current_album = albums.get(current_location.absolute_path, None) current_album = albums.get(current_location.absolute_path, None)
if current_album is None: if current_album is None:
current_album = self.new_album(title=current_location.basename, commit=False) try:
current_album = self.get_album_by_path(current_location.absolute_path)
except NoSuchAlbum:
current_album = self.new_album(
associated_directory=current_location.absolute_path,
commit=False,
title=current_location.basename,
)
print('Created %s' % current_album.title) print('Created %s' % current_album.title)
albums[current_location.absolute_path] = current_album albums[current_location.absolute_path] = current_album
parent = albums[current_location.parent.absolute_path] parent = albums[current_location.parent.absolute_path]
try:
parent.add(current_album, commit=False) parent.add(current_album, commit=False)
except GroupExists:
pass
#print('Added to %s' % parent.title) #print('Added to %s' % parent.title)
for filepath in files: for filepath in files:
try: try:
@ -1561,6 +1625,7 @@ class Album(ObjectBase, GroupableMixin):
self.photodb = photodb self.photodb = photodb
self.id = row_tuple[SQL_ALBUM['id']] self.id = row_tuple[SQL_ALBUM['id']]
self.title = row_tuple[SQL_ALBUM['title']] self.title = row_tuple[SQL_ALBUM['title']]
self.name = 'Album %s' % self.id
self.description = row_tuple[SQL_ALBUM['description']] self.description = row_tuple[SQL_ALBUM['description']]
self.group_getter = self.photodb.get_album self.group_getter = self.photodb.get_album
@ -1736,14 +1801,7 @@ class Photo(ObjectBase):
special: special:
For videos, you can provide a `timestamp` to take the thumbnail from. For videos, you can provide a `timestamp` to take the thumbnail from.
''' '''
chunked_id = chunk_sequence(self.id, 3) hopeful_filepath = self.make_thumbnail_filepath()
basename = chunked_id[-1]
folder = chunked_id[:-1]
folder = os.sep.join(folder)
folder = os.path.join(self.photodb.thumbnail_folder, folder)
if folder:
os.makedirs(folder, exist_ok=True)
hopeful_filepath = os.path.join(folder, basename) + '.jpg'
return_filepath = None return_filepath = None
mime = self.mimetype() mime = self.mimetype()
@ -1828,6 +1886,17 @@ class Photo(ObjectBase):
return False return False
def make_thumbnail_filepath(self):
chunked_id = chunk_sequence(self.id, 3)
basename = chunked_id[-1]
folder = chunked_id[:-1]
folder = os.sep.join(folder)
folder = os.path.join(self.photodb.thumbnail_folder, folder)
if folder:
os.makedirs(folder, exist_ok=True)
hopeful_filepath = os.path.join(folder, basename) + '.jpg'
return hopeful_filepath
def mimetype(self): def mimetype(self):
return get_mimetype(self.real_filepath) return get_mimetype(self.real_filepath)

View file

@ -6,6 +6,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css"> <link rel="stylesheet" href="/static/common.css">
<script src="/static/common.js"></script> <script src="/static/common.js"></script>
{% set filename = photo["id"] + "." + photo["extension"] %}
{% set link = "/file/" + filename %}
{% set mimetype=photo["mimetype"] %}
</head> </head>
<style> <style>
#content_body #content_body
@ -46,19 +49,26 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
max-height: 100%;
max-width: 100%;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.photo_object a .photo_object a
{ {
max-height: 100%; height: 100%;
max-width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
#photo_img_holder
{
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-repeat: no-repeat;
}
.photo_object img .photo_object img
{ {
max-height: 100%; max-height: 100%;
@ -100,12 +110,12 @@
<h4>File info</h4> <h4>File info</h4>
<ul id="metadata"> <ul id="metadata">
{% if photo["width"] %} {% if photo["width"] %}
<li>{{photo["width"]}}x{{photo["height"]}} px</li> <li>Dimensions: {{photo["width"]}}x{{photo["height"]}} px</li>
<li>{{photo["ratio"]}} aspect ratio</li> <li>Aspect ratio: {{photo["ratio"]}}</li>
<li>{{photo["bytestring"]}}</li> <li>Size: {{photo["bytestring"]}}</li>
{% endif %} {% endif %}
<li>{{photo["duration"]}}</li>
{% if photo["duration"] %} {% if photo["duration"] %}
<li>Duration: {{photo["duration"]}}</li>
{% endif %} {% endif %}
<li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1">Download as {{photo["id"]}}.{{photo["extension"]}}</a></li> <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1">Download as {{photo["id"]}}.{{photo["extension"]}}</a></li>
<li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1&original_filename=1">Download as "{{photo["filename"]}}"</a></li> <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1&original_filename=1">Download as "{{photo["filename"]}}"</a></li>
@ -126,13 +136,11 @@
</div> </div>
<div id="right"> <div id="right">
<!-- THE PHOTO ITSELF -->
<div class="photo_object"> <div class="photo_object">
{% set filename = photo["id"] + "." + photo["extension"] %}
{% set link = "/file/" + filename %}
{% set mimetype=photo["mimetype"] %}
{% if mimetype == "image" %} {% if mimetype == "image" %}
<!-- <a target="_blank" href="{{link}}"><img src="{{link}}"></a> --> <div id="photo_img_holder"><img id="photo_img" src="{{link}}" onclick="toggle_hoverzoom()" onload="this.style.opacity=0.99"></div>
<img src="{{link}}"> <!-- <img src="{{link}}"> -->
{% elif mimetype == "video" %} {% elif mimetype == "video" %}
<video src="{{link}}" controls preload=none {%if photo["has_thumbnail"]%}poster="/thumbnail/{{photo["id"]}}.jpg"{%endif%}></video> <video src="{{link}}" controls preload=none {%if photo["has_thumbnail"]%}poster="/thumbnail/{{photo["id"]}}.jpg"{%endif%}></video>
{% elif mimetype == "audio" %} {% elif mimetype == "audio" %}
@ -152,6 +160,72 @@ var add_tag_button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area'); var message_area = document.getElementById('message_area');
add_tag_box.onkeydown = function(){entry_with_history_hook(add_tag_box, add_tag_button)}; add_tag_box.onkeydown = function(){entry_with_history_hook(add_tag_box, add_tag_button)};
function enable_hoverzoom()
{
console.log("enable");
div = document.getElementById("photo_img_holder");
img = document.getElementById("photo_img");
if (img.naturalWidth < div.offsetWidth && img.naturalHeight < div.offsetHeight)
{
return;
}
img.style.opacity = 0;
img.style.display = "none";
div.style.cursor = "zoom-out";
div.style.backgroundImage = "url('{{link}}')";
div.onmousemove = move_hoverzoom;
setTimeout(function(){div.onclick = disable_hoverzoom;}, 100);
return true;
}
function disable_hoverzoom()
{
console.log("disable");
div = document.getElementById("photo_img_holder");
img = document.getElementById("photo_img");
img.style.opacity = 100;
div.style.cursor = "";
img.style.display="";
div.style.backgroundImage = "none";
div.onmousemove = null;
div.onclick = null;
}
function toggle_hoverzoom()
{
img = document.getElementById("photo_img");
if (img.style.opacity === "0")
{
disable_hoverzoom();
}
else
{
enable_hoverzoom();
}
}
function move_hoverzoom(event)
{
div = document.getElementById("photo_img_holder");
img = document.getElementById("photo_img");
var x;
var y;
if (img.naturalWidth < div.offsetWidth)
{
x = (img.naturalWidth - div.offsetWidth) / 2;
}
else
{
x = (img.naturalWidth - div.offsetWidth) * (event.offsetX / div.offsetWidth);
}
if (img.naturalHeight < div.offsetHeight)
{
y = (img.naturalHeight - div.offsetHeight) / 2;
}
else
{
y = (img.naturalHeight - div.offsetHeight) * (event.offsetY / div.offsetHeight);
}
//console.log(x);
div.style.backgroundPosition=(-x)+"px "+(-y)+"px";
}
function receive_callback(response) function receive_callback(response)
{ {
var tagname = response["tagname"]; var tagname = response["tagname"];

View file

@ -189,6 +189,10 @@ form
<br> <br>
<span>Other filters</span> <span>Other filters</span>
<input type="text" class="basic_param"
value="{%if search_kwargs['filename']%}{{search_kwargs['filename']}}{%endif%}"
name="filename" placeholder="Filename">
<input type="text" class="basic_param" <input type="text" class="basic_param"
value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}" value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}"
name="mimetype" placeholder="Mimetype(s)"> name="mimetype" placeholder="Mimetype(s)">