diff options
author | jaseg <git@jaseg.net> | 2019-11-18 13:41:20 +0100 |
---|---|---|
committer | jaseg <git@jaseg.net> | 2019-11-18 13:41:20 +0100 |
commit | be3e6aca67951c9bcc138f867190c7aaf8894a60 (patch) | |
tree | cf302a00421c99027fdc70f525abda3869197395 | |
parent | 5b6396fbcd1cba7089e480ea45489ca6626448c1 (diff) | |
download | kochbuch-be3e6aca67951c9bcc138f867190c7aaf8894a60.tar.gz kochbuch-be3e6aca67951c9bcc138f867190c7aaf8894a60.tar.bz2 kochbuch-be3e6aca67951c9bcc138f867190c7aaf8894a60.zip |
Generator basically working
-rw-r--r-- | generator.py | 313 | ||||
-rw-r--r-- | tagomatic.py | 277 |
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_()) |