summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--generator.py313
-rw-r--r--tagomatic.py277
2 files changed, 516 insertions, 74 deletions
diff --git a/generator.py b/generator.py
new file mode 100644
index 0000000..20c7c00
--- /dev/null
+++ b/generator.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+
+import subprocess
+import tempfile
+import hashlib
+import glob
+import shutil
+import os
+from os import path
+from urllib import parse
+import sqlite3
+
+import tqdm
+import jinja2
+
+MAIN_IDX_TEMPL = jinja2.Template('''
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>{{ title }}</title>
+ <link rel="stylesheet" type="text/css" href="style.css">
+</head>
+<body class="idx main">
+ <h1>{{ title }}</h1>
+ <h2>Books</h2>
+ <ul>
+ {% for book, idx, pages, cats in books %}
+ <li>
+ {{ book }}:
+ <a href="{{ idx }}">Index</a>,
+ <a href="{{ pages }}">Pages</a>{{ "," if cats }}
+ {% for cat, cat_fn in cats %}
+ <a href="{{ cat_fn }}">{{ cat }}</a>{{ "," if not loop.last }}
+ {% endfor %}
+ </li>
+ {% endfor %}
+ </ul>
+
+ <h2>Overall</h2>
+ <a href="pages.html">Overall Page List</a>
+
+ <h2>Entries</h2>
+ <ul class="entries">
+ {% for title, path in entries %}
+ <li><a href="{{ path }}">{{ title }}</a></li>
+ {% endfor %}
+ </ul>
+</body>
+</html>
+''')
+
+CAT_PAGE_LIST_TEMPL = jinja2.Template('''
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>{{ title }}: {{ cat }} in {{ book }}</title>
+ <link rel="stylesheet" type="text/css" href="style.css">
+</head>
+<body class="pglist book">
+ <h1>{{ cat }} in {{ book }}</h1>
+ <a href="../index.html">Home</a>
+ <ol class="pglist">
+ {% for pgnum, thumb, link in pages %}
+ <li value="{{ pgnum }}"><a href="{{ link }}"><img src="{{ thumb }}" alt="Page {{ pgnum }}"/></a></li>
+ {% endfor %}
+ </ol>
+</body>
+</html>
+''')
+
+BOOK_PAGE_LIST_TEMPL = jinja2.Template('''
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>{{ title }}: Page listing of {{ book }}</title>
+ <link rel="stylesheet" type="text/css" href="style.css">
+</head>
+<body class="pglist book">
+ <h1>Pages of {{ book }}</h1>
+ <a href="../index.html">Home</a>
+ <ol class="pglist">
+ {% for pgnum, thumb, _img, link, _hash in pages %}
+ <li value="{{ pgnum }}"><a href="{{ link }}"><img src="{{ thumb }}" alt="Page {{ pgnum }}"/></a></li>
+ {% endfor %}
+ </ol>
+</body>
+</html>
+''')
+
+MAIN_PAGE_LIST_TEMPL = jinja2.Template('''
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>{{ title }}</title>
+ <link rel="stylesheet" type="text/css" href="style.css">
+</head>
+<body class="pglist main">
+ <h1>{{ title }}: Overall Page Listing</h1>
+ <a href="../index.html">Home</a>
+ <h2>Books</h2>
+ <ul class="contents">
+ {% for book, pages in books %}
+ <li><a href="#pages-{{ parse.quote(book) }}">{{ book }}</a></li>
+ {% endfor %}
+ </ul>
+
+ <h2>Pages</h2>
+ {% for book, pages in books %}
+ <h1 id="pages-{{ parse.quote(book) }}">Pages of {{ book }}</h1>
+ <ol class="pglist">
+ {% for pgnum, thumb, link in pages %}
+ <li value="{{ pgnum }}"><a href="{{ link }}"><img src="{{ thumb }}" alt="Page {{ pgnum }}"/></a></li>
+ {% endfor %}
+ </ol>
+ {% endfor %}
+</body>
+</html>
+''')
+
+BOOK_IDX_TEMPL = jinja2.Template('''
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>{{ title }}: Index of {{ book }}</title>
+ <link rel="stylesheet" type="text/css" href="style.css">
+</head>
+<body class="idx book">
+ <h1>Index of {{ book }}</h1>
+ <a href="../index.html">Home</a>
+ <h2>Entries</h2>
+ <ul class="entries">
+ {% for title, path in entries %}
+ <li><a href="{{ path }}">{{ title }}</a></li>
+ {% endfor %}
+ </ul>
+</body>
+</html>
+''')
+
+PAGE_TEMPL = jinja2.Template('''
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>{{ title }}: Page {{ pgnum }} of {{ book }}</title>
+ <link rel="stylesheet" type="text/css" href="style.css">
+</head>
+<body class="page">
+
+ <h2>Page {{ pgnum }} of {{ book }}{{ " ("+cat+")" if cat }}</h2>
+ <a href="../../index.html">Home</a>, <a href="{{ page_list }}">Pages</a>, <a href="{{ index }}">Index</a>
+ {% for title in titles %}
+ <h4>{{ title }}</h4>
+ {% endfor %}
+
+ {% if has_prev %}
+ <a href="{{ prev_link }}" title="Previous page: {{ prev_num }}">Previous <img src="{{ prev_img }}" alt="Page {{ prev_num }} of {{ book }}"></a>
+ {% endif %}
+
+ <a href="{{ img }}"><img src="{{ img }}" alt="Page {{ pgnum }} of {{ book }}"></a>
+
+ {% if has_next %}
+ <a href="{{ next_link }}" title="Next page: {{ next_num }}">Next <img src="{{ next_img }}" alt="Page {{ next_num }} of {{ book }}"></a>
+ {% endif %}
+
+</body>
+''')
+
+imgname = lambda prefix, pgnum, orig_fn: f'ar-{prefix.lower()}-{pgnum:04}{path.splitext(orig_fn)[1].lower()}'
+imgpath = lambda prefix, pgnum, orig_fn: path.join('images', imgname(prefix, pgnum, orig_fn))
+thumbpath = lambda prefix, pgnum: path.join('thumbs', imgname(prefix, pgnum, '_.png'))
+
+if __name__ == '__main__':
+ import argparse
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-d', '--database', default='kochbuch.sqlite3', help='Metadata db path, default: kochbuch.sqlite3')
+ parser.add_argument('-t', '--title', default='Image Archive', help='Product title (for headings and file names)')
+ parser.add_argument('img_path', default='.', nargs='?', help='Base directory of image files')
+ parser.add_argument('out.zip', default='out', nargs='?', help='Output file')
+ args = parser.parse_args()
+
+ db = sqlite3.connect(args.database)
+ db.execute('PRAGMA foreign_keys = ON')
+
+ pics = {}
+ print('Building file index')
+ for pic in tqdm.tqdm(glob.glob(path.join(glob.escape(args.img_path), '**/*.jpg'), recursive=True)):
+ with open(pic, 'rb') as f:
+ hash = hashlib.sha3_256()
+ hash.update(f.read())
+ hash = hash.hexdigest()
+ pics[hash] = pic
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ ### FIXME debug
+ out = '/tmp/tagomatic'
+ #shutil.rmtree(out) FIXME DEBUG
+ #os.mkdir(out) FIXME DEBUG
+
+ print('Copying images')
+ # os.mkdir(path.join(out, 'images')) FIXME DEBUG
+ for hash, prefix, pgnum, orig_fn in db.execute('SELECT sha3, prefix, pgnum, filename FROM pics'):
+ dst = path.join(out, imgpath(prefix, pgnum, orig_fn))
+ # shutil.copy(pics[hash], dst) FIXME DEBUG
+ pics[hash] = dst
+
+ print('Generating Thumbnails')
+ thumb_path = path.join(out, 'thumbs')
+ # os.mkdir(thumb_path) FIXME DEBUG
+ # subprocess.check_call([ FIXME DEBUG
+ # 'mogrify', '-format', 'png', '-path', thumb_path, '-thumbnail', '100x100',
+ # *pics.values()
+ # ])
+
+ print('Writing indices')
+ books = [ book for book, in db.execute('SELECT DISTINCT book FROM pics ORDER BY book') ]
+ book_indices = []
+ book_pages = []
+ for book in books:
+ book_dir = book
+ # os.mkdir(path.join(out, book_dir)) FIXME DEBUG
+
+ entries = [ (value, f'pages/pg{pgnum}.html') for value, pgnum in db.execute(
+ '''SELECT value, pgnum FROM pic_tags
+ JOIN pics ON pics.id=pic_tags.pic
+ WHERE pic_tags.tag=(SELECT id FROM tags WHERE name="title")
+ AND book=?
+ AND value NOT NULL AND value != ""
+ ORDER BY value''', (book,)) ]
+ idx_fn = path.join(book_dir, 'index.html')
+ with open(path.join(out, idx_fn), 'w') as f:
+ f.write(BOOK_IDX_TEMPL.render(title=args.title, book=book, entries=entries))
+
+ results = db.execute('SELECT prefix, pgnum, filename, sha3 FROM pics WHERE book=? ORDER BY pgnum',
+ (book,)).fetchall()
+
+ tmp = [ (pgnum, thumbpath(prefix, pgnum), f'{book}/pages/pg{pgnum}.html')
+ for prefix, pgnum, fn, hash in results ]
+ book_pages.append((book, tmp))
+
+ pages = [ (pgnum,
+ f'../{thumbpath(prefix, pgnum)}', f'../{imgpath(prefix, pgnum, fn)}', f'pages/pg{pgnum}.html',
+ hash)
+ for prefix, pgnum, fn, hash in results ]
+
+ pages_fn = path.join(book_dir, 'pages.html')
+ with open(path.join(out, pages_fn), 'w') as f:
+ f.write(BOOK_PAGE_LIST_TEMPL.render(title=args.title, book=book, pages=pages))
+
+ cats = [ (cat, path.join(book_dir, f'pages-{cat}.html')) for cat, in db.execute(
+ 'SELECT DISTINCT category FROM pics WHERE book=? AND category NOT NULL', (book,)) ]
+ for cat, cat_fn in cats:
+ cat_pages = [ (pgnum, '../'+thumbpath(prefix, pgnum), f'pages/pg{pgnum}.html')
+ for prefix, pgnum, fn in
+ db.execute('SELECT prefix, pgnum, filename FROM pics WHERE book=? AND category=? ORDER BY pgnum',
+ (book, cat)) ]
+ with open(path.join(out, cat_fn), 'w') as f:
+ f.write(CAT_PAGE_LIST_TEMPL.render(title=args.title, book=book, cat=cat, pages=cat_pages))
+
+ book_indices.append((book, idx_fn, pages_fn, cats))
+
+ page_dir = path.join(book_dir, 'pages')
+ # os.mkdir(path.join(out, page_dir)) FIXME DEBUG
+ for prev, (pgnum, _thumb, img, link, hash), next in zip(
+ [None] + pages[:-1],
+ pages,
+ pages[1:] + [None]):
+
+ titles = [ title for title, in db.execute('''
+ SELECT value FROM pic_tags JOIN pics ON pics.id=pic_tags.pic
+ WHERE sha3=? AND value NOT NULL AND value!=""
+ ''', (hash,)) ]
+
+ cat, = db.execute('SELECT category FROM pics WHERE sha3=?', (hash,)).fetchone()
+
+ has_prev = prev is not None
+ if prev:
+ has_prev = True
+ prev_num, prev_img, _1, prev_link, _2 = prev
+ prev_link = f'pg{prev_num}.html'
+ else:
+ has_prev, prev_num, prev_img, prev_link = False, None, None, None
+
+ has_next = next is not None
+ if next:
+ has_next = True
+ next_num, next_img, _1, next_link, _2 = next
+ next_link = f'pg{next_num}.html'
+ else:
+ has_next, next_num, next_img, next_link = False, None, None, None
+
+ page_fn = path.join(page_dir, f'pg{pgnum}.html')
+ with open(path.join(out, page_fn), 'w') as f:
+ f.write(PAGE_TEMPL.render(
+ title=args.title, page_list='../pages.html', index='../index.html', img=f'../{img}',
+ titles=titles, pgnum=pgnum, book=book, cat=cat, has_prev=has_prev, prev_link=prev_link,
+ prev_num=prev_num, prev_img=f'../{prev_img}', has_next=has_next, next_link=next_link,
+ next_num=next_num, next_img=f'../{next_img}'))
+
+ entries = [ (value, f'{book}/pages/pg{pgnum}.html') for value, book, pgnum in db.execute(
+ '''SELECT value, book, pgnum FROM pic_tags
+ JOIN pics ON pics.id=pic_tags.pic
+ WHERE pic_tags.tag=(SELECT id FROM tags WHERE name="title")
+ AND value NOT NULL AND value != ""
+ ORDER BY value''') ]
+ with open(path.join(out, f'index.html'), 'w') as f:
+ f.write(MAIN_IDX_TEMPL.render(title=args.title, entries=entries, books=book_indices))
+
+ with open(path.join(out, f'pages.html'), 'w') as f:
+ f.write(MAIN_PAGE_LIST_TEMPL.render(title=args.title, books=book_pages, parse=parse))
+
+ print('Done.')
+
+
diff --git a/tagomatic.py b/tagomatic.py
index fce833a..b226355 100644
--- a/tagomatic.py
+++ b/tagomatic.py
@@ -13,8 +13,8 @@ import sqlite3
import tqdm
from PyQt5.QtWidgets import *
-from PyQt5.QtGui import QPixmap, QValidator
-from PyQt5.QtCore import QEvent, QItemSelectionModel, Qt
+from PyQt5.QtGui import QPixmap, QValidator, QTransform, QKeySequence
+from PyQt5.QtCore import Qt, QEvent, QItemSelectionModel, QCoreApplication, QSettings, QTimer
class TagValidator(QValidator):
def __init__(self, db):
@@ -22,11 +22,42 @@ class TagValidator(QValidator):
self.db = db
def validate(self, input, pos):
- if self.db.execute('SELECT id FROM tags WHERE name=?', (input,)).fetchone():
- return QValidator.Acceptable
- if self.db.execute('SELECT id FROM tags WHERE name LIKE ? + "%"', (input,)).fetchone():
- return QValidator.Intermediate
- return QValidator.Invalid
+ if self.db.execute('SELECT id FROM tags WHERE name=? LIMIT 1', (input,)).fetchone():
+ return QValidator.Acceptable, input, pos
+ return QValidator.Intermediate, input, pos
+
+class ZoomView(QGraphicsView):
+ def __init__(self, zoom=1.0, zoom_cb=lambda zoom: None):
+ super().__init__()
+ self.pixmap = None
+ self.zoom = zoom
+ self.zoom_cb = zoom_cb
+ self.setDragMode(QGraphicsView.ScrollHandDrag)
+ self.pixmap_item = QGraphicsPixmapItem()
+ self.scene = QGraphicsScene()
+ self.scene.addItem(self.pixmap_item)
+ self.setScene(self.scene)
+ self.setTransform(QTransform.fromScale(zoom, zoom))
+
+ def set_pic(self, le_path):
+ self.pixmap = QPixmap(le_path)
+ self.pixmap_item.setPixmap(self.pixmap)
+
+ def set_zoom(self, zoom):
+ self.zoom = zoom
+ self.setTransform(QTransform.fromScale(zoom, zoom))
+ self.zoom_cb(zoom)
+
+ def wheelEvent(self, evt):
+ if evt.modifiers() == Qt.ControlModifier:
+ self.set_zoom(max(1/16, min(4, self.zoom * (1.2**(evt.angleDelta().y() / 120)))))
+ elif evt.modifiers() == Qt.ShiftModifier:
+ QCoreApplication.sendEvent(self.horizontalScrollBar(), evt)
+ else:
+ if abs(evt.angleDelta().x()) > abs(evt.angleDelta().y()):
+ QCoreApplication.sendEvent(self.horizontalScrollBar(), evt)
+ else:
+ QCoreApplication.sendEvent(self.verticalScrollBar(), evt)
class App(QWidget):
def __init__(self, db, img_path):
@@ -37,42 +68,101 @@ class App(QWidget):
self.img_path = img_path
self.tag_validator = TagValidator(self.db)
self.tag_box = None
- self.pixmap = None
- self.zoom_levels = [ a*b for a in [0.01, 0.1, 1, 10, 100] for b in [0.25, 0.5, 1.0] ]
+ self.refresh_timer = QTimer(self)
+ self.refresh_timer.timeout.connect(self.refresh_pic)
+ self.refresh_timer.setSingleShot(True)
+ self.refresh_timer.setInterval(0)
+ self.tag_edits = {}
+ self.focused_tag_tid = None
+ self.settings = QSettings()
+
+ tags = [ tag for tag, in self.db.execute('SELECT name FROM tags') ]
+ self.new_completer = QCompleter(tags, self)
+ self.new_completer.setCompletionMode(QCompleter.InlineCompletion)
+ self.new_completer.setCaseSensitivity(Qt.CaseInsensitive)
pics = self.db.execute('SELECT filename, id FROM pics WHERE valid=1 ORDER BY filename').fetchall()
self.pid_for_fn = { k: v for k, v in pics }
self.fn_for_pid = { v: k for k, v in pics }
- _, self.current_pic = pics[0]
+ _, first_pid = pics[0]
+ self.current_pic = int(self.settings.value("current_picture", first_pid))
+ #self.shown = list(self.fn_for_pid.keys())
- pic_scroll = QScrollArea()
- self.pic_label = QLabel(self)
- pic_scroll.setWidget(self.pic_label)
- pic_scroll.setWidgetResizable(True)
- self.title_edit = QLineEdit()
+ zoom = float(self.settings.value('zoom', 1.0))
+ def save_zoom(zoom):
+ self.settings.setValue('zoom', zoom)
+ self.zoom_view = ZoomView(zoom, save_zoom)
self.pic_layout = QVBoxLayout()
- self.pic_layout.addWidget(pic_scroll)
+ self.pic_layout.addWidget(self.zoom_view)
self.refresh_pic()
self.pic_list = QListWidget()
self.pic_list.setSelectionMode(QAbstractItemView.SingleSelection)
- self.pic_list.itemActivated.connect(lambda item: self.set_pic(self.pid_for_fn[item.text()]))
+ show_item = lambda item: self.set_pic(self.pid_for_fn[item.text()])
+ self.pic_list.itemActivated.connect(show_item)
+ self.pic_list.itemClicked.connect(show_item)
for fn, _ in pics:
self.pic_list.addItem(fn)
- self.pic_list.setCurrentRow(0, QItemSelectionModel.SelectCurrent)
-
+ # Use row-based indexing since at this point there's no filters yet.
+ idx = next(i for i, (_fn, pid) in enumerate(pics) if pid == self.current_pic)
+ self.pic_list.setCurrentRow(idx, QItemSelectionModel.SelectCurrent)
+
+ nav_prev = QPushButton('Previous')
+ nav_prev.clicked.connect(self.prev)
+ nav_next = QPushButton('Next')
+ nav_next.clicked.connect(self.next)
+
+ shortcut1 = QShortcut(QKeySequence('Ctrl+N'), self)
+ shortcut1.activated.connect(self.next)
+ shortcut2 = QShortcut(QKeySequence('Ctrl+P'), self)
+ shortcut2.activated.connect(self.prev)
+ shortcut3 = QShortcut(QKeySequence(QKeySequence.MoveToNextPage), self)
+ shortcut3.activated.connect(self.next)
+ shortcut4 = QShortcut(QKeySequence(QKeySequence.MoveToPreviousPage), self)
+ shortcut4.activated.connect(self.prev)
+ shortcut4 = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Minus), self)
+ shortcut4.activated.connect(self.remove_current_tag)
+ #shortcut5 = QShortcut(QKeySequence(Qt.CTRL+ Qt.Key_Return), self)
+ #shortcut5.activated.connect(self.next)
+
+ nav_buttons = QHBoxLayout()
+ nav_buttons.addWidget(nav_prev)
+ nav_buttons.addWidget(nav_next)
+ list_layout = QVBoxLayout()
+ list_layout.addWidget(self.pic_list)
+ list_layout.addLayout(nav_buttons)
main_layout = QHBoxLayout()
- main_layout.addWidget(self.pic_list)
+ main_layout.addLayout(list_layout)
main_layout.addLayout(self.pic_layout)
self.setLayout(main_layout)
QApplication.instance().focusChanged.connect(self.focusChanged)
self.show()
+ def navigate(self, movement):
+ idx = self.pic_list.moveCursor(movement, Qt.NoModifier)
+ cmd = self.pic_list.selectionCommand(idx, None)
+ self.pic_list.selectionModel().setCurrentIndex(idx, cmd)
+ items = self.pic_list.selectedItems()
+ if len(items) != 1:
+ return
+ item, = items
+ self.set_pic(self.pid_for_fn[item.text()])
+
+ def prev(self):
+ self.navigate(QAbstractItemView.MovePrevious)
+
+ def next(self):
+ self.navigate(QAbstractItemView.MoveDown)
+
+ def schedule_refresh(self):
+ self.refresh_timer.start()
+
def set_pic(self, pid):
self.current_pic = pid
- self.refresh_pic()
+ self.settings.setValue("current_picture", pid)
+ self.schedule_refresh()
def refresh_list(self):
for i in range(self.pic_list.count()):
@@ -85,28 +175,33 @@ class App(QWidget):
def focusChanged(self, _old, new):
if new in self.tag_edits:
- print(f'Changed focus to tag {self.tag_edits[new]}')
- tid, name = self.tag_edits[new]
- self.focused_tag_name = name
-
- def remove_tag(self, tid):
- self.db.execute('DELETE FROM pic_tags WHERE id=?', (tid,))
+ tid, _lid = self.tag_edits[new]
+ self.focused_tag_tid = tid
+
+ def remove_current_tag(self):
+ obj = QApplication.instance().focusWidget()
+ if obj in self.tag_edits:
+ _tid, lid = self.tag_edits[obj]
+ self.remove_tag(lid)
+
+ def remove_tag(self, lid):
+ with self.db:
+ self.db.execute('DELETE FROM pic_tags WHERE id=?', (lid,))
self.refresh_pic()
- def update_tag(self, edit, tid):
- self.db.execute('UPDATE pic_tags SET value=? WHERE id=?', (edit.text(), tid))
+ def update_tag(self, edit, lid):
+ with self.db:
+ self.db.execute('UPDATE pic_tags SET value=? WHERE id=?', (edit.text(), lid))
def add_tag(self):
- self.db.execute('INESRT INTO pic_tags (pic, tag) VALUES (?, ?)', (self.current_pic,))
-
- def set_zoom(self, zoom):
- self.zoom = zoom
- self.pic_label.setPixmap(self.pixmap.scaled(
- self.pixmap.width()*zoom, self.pixmap.height()*zoom, Qt.KeepAspectRatio)
-
- def change_zoom(self, delta):
- idx = max(0, min(len(self.zoom_levels)-1, self.zoom_levels.index(self.zoom) + delta))
- self.set_zoom(self.zoom_levels[idx])
+ if not self.new_edit.hasAcceptableInput():
+ return
+ name = self.new_edit.text()
+ with self.db:
+ tid, = self.db.execute('SELECT id FROM tags WHERE name=?', (name,)).fetchone()
+ self.db.execute('INSERT INTO pic_tags (pic, tag) VALUES (?, ?)', (self.current_pic, tid))
+ self.focused_tag_tid = tid
+ self.refresh_pic()
def refresh_pic(self):
pid = self.current_pic
@@ -114,43 +209,65 @@ class App(QWidget):
self.img_path,
*self.db.execute('SELECT path FROM pics WHERE id=?', (pid,)).fetchone(),
self.fn_for_pid[pid])
- self.pixmap = QPixmap(le_path)
- print(path.isfile(le_path), le_path)
- self.set_zoom(self.zoom)
+ self.zoom_view.set_pic(le_path)
tag_layout = QGridLayout()
- tag_layout.addWidget(QLabel('Title'), 0, 1)
- tag_layout.addWidget(self.title_edit, 0, 2)
self.tag_edits = {}
- for i, (tid, name, value) in enumerate(self.db.execute('''SELECT pic_tags.id, tags.name, pic_tags.value FROM pic_tags
- JOIN tags ON pic_tags.tag = tags.id
+ tag_edit_for_tid = {}
+ for i, (lid, tid, tag_type, name, desc, value) in enumerate(self.db.execute(
+ '''SELECT pic_tags.id, tags.id, tags.type, name, description, value
+ FROM pic_tags JOIN tags ON pic_tags.tag = tags.id
WHERE pic_tags.pic = ?''',
(pid,)), start=1):
+ is_bool = tag_type == 'bool'
remove_btn = QPushButton('Remove')
- remove_btn.clicked.connect(lambda: self.remove_tag(tid))
- tag_edit = QLineEdit()
- self.tag_edits[tag_edit] = tid, name
- tag_edit.editingFinished.connect(lambda: self.update_tag(tag_edit, tid))
+ remove_btn.clicked.connect(lambda: self.remove_tag(lid))
tag_layout.addWidget(remove_btn, i, 0)
- tag_layout.addWidget(QLabel(f'{name}:'), i, 1)
- tag_layout.addWidget(tag_edit, i, 2)
+ tag_layout.addWidget(QLabel(f'{desc}:' if not is_bool else desc), i, 1)
+
+ if not is_bool:
+ tag_edit = QLineEdit()
+ self.tag_edits[tag_edit] = tid, lid
+ tag_edit_for_tid[tid] = tag_edit
+ tag_edit.setText(value)
+ tag_edit.editingFinished.connect(lambda: self.update_tag(tag_edit, lid))
+ tag_edit.returnPressed.connect(self.next)
+ tag_layout.addWidget(tag_edit, i, 2)
add_button = QPushButton('Add')
+ manage_button = QPushButton('Manage')
+ add_button.setEnabled(False)
add_button.clicked.connect(self.add_tag)
self.new_edit = QLineEdit()
- tag_layout.addWidget(QLabel('New:'), i+1, 0)
- tag_layout.addWidget(self.new_edit, i+1, 1)
- tag_layout.addWidget(add_button, i+1, 2)
+ self.new_edit.setCompleter(self.new_completer)
+ def edited(text):
+ ok = self.new_edit.hasAcceptableInput()
+ add_button.setEnabled(ok)
+ color = '#c4df9b' if ok else ('#fff79a' if text else '#ffffff')
+ self.new_edit.setStyleSheet(f'QLineEdit {{ background-color: {color} }}')
+ self.new_edit.textEdited.connect(edited)
+ new_layout = QHBoxLayout()
+ new_layout.addWidget(QLabel('New:'))
+ new_layout.addWidget(self.new_edit)
+ new_layout.addWidget(add_button)
+ new_layout.addWidget(manage_button)
self.new_edit.returnPressed.connect(self.add_tag)
self.new_edit.setValidator(self.tag_validator)
if self.tag_box:
self.pic_layout.removeWidget(self.tag_box)
+ self.tag_box.close()
self.tag_box = QGroupBox('Image tags')
- self.tag_box.setLayout(tag_layout)
+ self.tag_box.setAttribute(Qt.WA_DeleteOnClose, True)
+ box_layout = QVBoxLayout()
+ box_layout.addLayout(tag_layout)
+ box_layout.addLayout(new_layout)
+ self.tag_box.setLayout(box_layout)
self.pic_layout.addWidget(self.tag_box)
- self.update()
+
+ if self.focused_tag_tid in tag_edit_for_tid:
+ tag_edit_for_tid[self.focused_tag_tid].setFocus()
if __name__ == '__main__':
import argparse
@@ -163,7 +280,6 @@ if __name__ == '__main__':
db = sqlite3.connect(args.database)
db.execute('PRAGMA foreign_keys = ON')
- print(f'SQLite version {sqlite3.version}')
db_initialized = bool(db.execute('SELECT name FROM sqlite_master WHERE type="table" AND name="pics"').fetchone())
if not db_initialized:
@@ -177,6 +293,8 @@ if __name__ == '__main__':
cur.execute('''CREATE TABLE IF NOT EXISTS pics (
id INTEGER PRIMARY KEY,
filename TEXT,
+ category TEXT,
+ book TEXT,
prefix TEXT,
sha3 TEXT UNIQUE,
pgnum INTEGER,
@@ -196,19 +314,16 @@ if __name__ == '__main__':
value TEXT)''')
cur.execute('''INSERT INTO tags (type, name, description)
- VALUES ("str", "dir", "Scan Directory")
- ON CONFLICT DO NOTHING''')
- cur.execute('''INSERT INTO tags (type, name, description)
- VALUES ("str", "title", "Image Title")
+ VALUES ("str", "title", "Image Title"),
+ ("str", "comment", "Comment"),
+ ("bool", "check", "Double-check this image")
ON CONFLICT DO NOTHING''')
print('Enumerating pages')
piclist = glob.glob(path.join(glob.escape(args.img_path), '**/*.jpg'), recursive=True)
- print(' ', len(piclist), path.join(glob.escape(args.img_path), '**/*.jpg'))
print('Reading pages')
cur.execute('UPDATE pics SET valid=0')
- cur.execute('DELETE FROM pic_tags WHERE tag=(SELECT id FROM tags WHERE name="dir")')
for pic in tqdm.tqdm(piclist):
relpath = path.relpath(pic, args.img_path)
@@ -227,19 +342,30 @@ if __name__ == '__main__':
prefixes[prefix].append((int(series), int(imgnum), hash))
+ book, *rest = path.normpath(rel).split(os.sep)
+ if rest:
+ category, *rest = rest
+ if rest:
+ raise UserWarning(f'Unhandled directory component "{rest}"')
+ else:
+ category = None
+
# upsert hack
- cur.execute('UPDATE pics SET filename=?, path=?, prefix=?, valid=1 WHERE sha3=?',
- (fn, rel, prefix, hash))
+ cur.execute('UPDATE pics SET book=?, category=?, filename=?, path=?, prefix=?, valid=1 WHERE sha3=?',
+ (book, category, fn, rel, prefix, hash))
if cur.rowcount == 0:
- cur.execute('INSERT INTO pics (filename, path, prefix, sha3, valid) VALUES (?, ?, ?, ?, 1)',
- (fn, rel, prefix, hash))
+ cur.execute('INSERT INTO pics (book, category, filename, path, prefix, sha3, valid) VALUES (?, ?, ?, ?, ?, ?, 1)',
+ (book, category, fn, rel, prefix, hash))
+
+ title_tag_exists = bool(db.execute('''SELECT pic_tags.id FROM pic_tags
+ JOIN pics ON pics.id=pic_tags.pic, tags ON tags.id=pic_tags.tag
+ WHERE tags.name="title" AND pics.sha3=?''', (hash,)).fetchone())
- for tag in path.normpath(rel).split(os.sep):
- cur.execute('''INSERT INTO pic_tags (pic, tag, value) VALUES (
- (SELECT id FROM pics WHERE sha3=?),
- (SELECT id FROM tags WHERE name="dir"),
- ?)''',
- (hash, tag))
+ if not title_tag_exists:
+ db.execute('''INSERT INTO pic_tags (pic, tag) VALUES (
+ (SELECT id FROM pics WHERE sha3=?),
+ (SELECT id FROM tags WHERE name="title"))''',
+ (hash,))
print('Reindexing pages')
for entries in prefixes.values():
@@ -253,6 +379,9 @@ if __name__ == '__main__':
signal.signal(signal.SIGINT, signal.SIG_DFL)
+ QCoreApplication.setOrganizationName('jaseg')
+ QCoreApplication.setOrganizationDomain('jaseg.net')
+ QCoreApplication.setApplicationName('Tag-O-Matic')
app = QApplication(sys.argv[:1] + unparsed)
ex = App(db, args.img_path)
sys.exit(app.exec_())