From af380492941e64d84a3c806336c2fa067279f512 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 6 May 2026 03:02:39 +0200 Subject: [PATCH] =?UTF-8?q?add=20Previews=20folder=20with=20=E2=89=A41MB?= =?UTF-8?q?=20scaled=20previews,=20toggle=20in=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- Previews/.gitkeep | 0 Slideshow.py | 41 +++++++++++++++++++++++---- db.py | 18 ++++++++---- docker-compose.yml | 1 + downloader.py | 61 +++++++++++++++++++++++++++++++++++++--- requirements.txt | 1 + templates/slideshow.html | 52 +++++++++++++++++++++++----------- 8 files changed, 143 insertions(+), 33 deletions(-) create mode 100644 Previews/.gitkeep diff --git a/Dockerfile b/Dockerfile index dd2f3e0..56646e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,6 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . -VOLUME ["/app/Pictures", "/app/booru.db"] +VOLUME ["/app/Pictures", "/app/Previews", "/app/booru.db"] EXPOSE 5000 CMD ["python", "Slideshow.py"] diff --git a/Previews/.gitkeep b/Previews/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Slideshow.py b/Slideshow.py index 4656c90..0f3c90f 100644 --- a/Slideshow.py +++ b/Slideshow.py @@ -5,7 +5,7 @@ import sys import threading import time import uuid -from flask import Flask, render_template, request, redirect, url_for, Response +from flask import Flask, render_template, request, redirect, url_for, Response, send_from_directory from werkzeug.middleware.proxy_fix import ProxyFix from db import init_db, search_images @@ -18,23 +18,52 @@ init_db() downloads = {} +@app.route('/previews/') +def serve_preview(filename): + return send_from_directory('Previews', filename) + + @app.route('/') def slideshow(): raw_query = request.args.get('tags', '').strip() results = search_images(raw_query) - pictures_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Pictures') - image_urls = [f'pictures/{r["filename"]}' for r in results] - post_urls = [r['post_url'] for r in results] - tags_list = [r['tags'].split() for r in results] - file_sizes = [os.path.getsize(os.path.join(pictures_dir, r['filename'])) for r in results] + base_dir = os.path.dirname(os.path.abspath(__file__)) + pictures_dir = os.path.join(base_dir, 'Pictures') + previews_dir = os.path.join(base_dir, 'Previews') + + image_urls = [] + preview_urls = [] + post_urls = [] + tags_list = [] + file_sizes = [] + preview_sizes = [] + + for r in results: + fname = r['filename'] + pname = r['preview_filename'] + image_urls.append(f'pictures/{fname}') + post_urls.append(r['post_url']) + tags_list.append(r['tags'].split()) + file_sizes.append(os.path.getsize(os.path.join(pictures_dir, fname))) + + ppath = os.path.join(previews_dir, pname) if pname else '' + if pname and os.path.exists(ppath): + preview_urls.append(f'previews/{pname}') + preview_sizes.append(os.path.getsize(ppath)) + else: + preview_urls.append(f'pictures/{fname}') + preview_sizes.append(file_sizes[-1]) + active_tags = raw_query.split() if raw_query else [] job_id = request.args.get('job_id') return render_template( 'slideshow.html', images=image_urls, + preview_images=preview_urls, post_urls=post_urls, tags_list=tags_list, file_sizes=file_sizes, + preview_sizes=preview_sizes, active_tags=active_tags, tag_query=raw_query, job_id=job_id, diff --git a/db.py b/db.py index 32c120f..7253852 100644 --- a/db.py +++ b/db.py @@ -18,6 +18,7 @@ def init_db(): tags TEXT NOT NULL DEFAULT '', file_url TEXT NOT NULL, post_url TEXT NOT NULL DEFAULT '', + preview_filename TEXT NOT NULL DEFAULT '', downloaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )""") c.execute( @@ -28,6 +29,11 @@ def init_db(): c.execute("ALTER TABLE images ADD COLUMN post_url TEXT NOT NULL DEFAULT ''") except sqlite3.OperationalError: pass + # migrate existing DBs that predate the preview_filename column + try: + c.execute("ALTER TABLE images ADD COLUMN preview_filename TEXT NOT NULL DEFAULT ''") + except sqlite3.OperationalError: + pass def image_exists(site, post_id): @@ -40,11 +46,11 @@ def image_exists(site, post_id): ) -def insert_image(post_id, site, filename, tags, file_url, post_url): +def insert_image(post_id, site, filename, tags, file_url, post_url, preview_filename=''): with get_conn() as c: c.execute( - "INSERT OR IGNORE INTO images (post_id, site, filename, tags, file_url, post_url) VALUES (?,?,?,?,?,?)", - (post_id, site, filename, tags, file_url, post_url), + "INSERT OR IGNORE INTO images (post_id, site, filename, tags, file_url, post_url, preview_filename) VALUES (?,?,?,?,?,?,?)", + (post_id, site, filename, tags, file_url, post_url, preview_filename), ) @@ -53,13 +59,13 @@ def search_images(tag_query): with get_conn() as c: if not terms: rows = c.execute( - "SELECT filename, post_url, tags FROM images ORDER BY id" + "SELECT filename, preview_filename, post_url, tags FROM images ORDER BY id" ).fetchall() else: where = ' AND '.join(['tags LIKE ?'] * len(terms)) params = [f'%{t}%' for t in terms] rows = c.execute( - f"SELECT filename, post_url, tags FROM images WHERE {where} ORDER BY id", + f"SELECT filename, preview_filename, post_url, tags FROM images WHERE {where} ORDER BY id", params, ).fetchall() - return [{'filename': r[0], 'post_url': r[1], 'tags': r[2]} for r in rows] + return [{'filename': r[0], 'preview_filename': r[1], 'post_url': r[2], 'tags': r[3]} for r in rows] diff --git a/docker-compose.yml b/docker-compose.yml index 2ccab7c..40236e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: env_file: .env volumes: - ./Pictures:/app/Pictures + - ./Previews:/app/Previews - ./booru.db:/app/booru.db networks: - caddy diff --git a/downloader.py b/downloader.py index b3e43e4..2c26b36 100644 --- a/downloader.py +++ b/downloader.py @@ -1,4 +1,7 @@ +import io +import math import os +import shutil import sys import argparse import requests @@ -89,7 +92,53 @@ def fetch_all_posts(adapter, query, auth, limit): return posts[:limit] -def download_one(post, site_name, adapter, pictures_dir, session): +def make_preview(src_path, previews_dir, max_bytes=1_048_576): + from PIL import Image + + src_name = os.path.basename(src_path) + name_no_ext, _ext = os.path.splitext(src_name) + + if os.path.getsize(src_path) <= max_bytes: + dest_path = os.path.join(previews_dir, src_name) + shutil.copy2(src_path, dest_path) + return src_name + + try: + img = Image.open(src_path) + if img.mode not in ('RGB', 'L', 'RGBA'): + img = img.convert('RGB') + elif img.mode == 'RGBA': + bg = Image.new('RGB', img.size, (255, 255, 255)) + bg.paste(img, mask=img.split()[3]) + img = bg + except Exception: + dest_path = os.path.join(previews_dir, src_name) + shutil.copy2(src_path, dest_path) + return src_name + + file_size = os.path.getsize(src_path) + scale = math.sqrt(max_bytes / file_size) + w, h = img.size + img = img.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS) + + preview_name = name_no_ext + '.jpg' + dest_path = os.path.join(previews_dir, preview_name) + + quality = 85 + buf = io.BytesIO() + while quality >= 20: + buf = io.BytesIO() + img.save(buf, format='JPEG', quality=quality, optimize=True) + if buf.tell() <= max_bytes: + break + quality -= 10 + + with open(dest_path, 'wb') as f: + f.write(buf.getvalue()) + return preview_name + + +def download_one(post, site_name, adapter, pictures_dir, previews_dir, session): parsed = adapter['parse'](post) if not parsed: return 'skip:no_url' @@ -105,7 +154,8 @@ def download_one(post, site_name, adapter, pictures_dir, session): with open(dest, 'wb') as f: f.write(r.content) post_url = adapter['post_url_fmt'].format(post_id=post_id) - insert_image(post_id, site_name, filename, tags, file_url, post_url) + preview_filename = make_preview(dest, previews_dir) + insert_image(post_id, site_name, filename, tags, file_url, post_url, preview_filename) return f'ok:{filename}' @@ -137,8 +187,11 @@ def main(): if api_key and user_id: auth = {'api_key': api_key, 'user_id': user_id} - pictures_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Pictures') + base_dir = os.path.dirname(os.path.abspath(__file__)) + pictures_dir = os.path.join(base_dir, 'Pictures') + previews_dir = os.path.join(base_dir, 'Previews') os.makedirs(pictures_dir, exist_ok=True) + os.makedirs(previews_dir, exist_ok=True) piped = not sys.stdout.isatty() @@ -175,7 +228,7 @@ def main(): done = 0 with ThreadPoolExecutor(max_workers=adapter['threads']) as pool: futures = { - pool.submit(download_one, p, args.site, adapter, pictures_dir, session): p + pool.submit(download_one, p, args.site, adapter, pictures_dir, previews_dir, session): p for p in posts } for _ in tqdm(as_completed(futures), total=total, file=sys.stderr): diff --git a/requirements.txt b/requirements.txt index 8931d12..790b837 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ flask>=3.0 requests>=2.32 tqdm>=4.66 python-dotenv>=1.0 +Pillow>=10.0 diff --git a/templates/slideshow.html b/templates/slideshow.html index d7f12cc..c7b896d 100644 --- a/templates/slideshow.html +++ b/templates/slideshow.html @@ -29,6 +29,8 @@ #controls{margin-top:2vh} button{padding:.6rem 1.1rem;font-size:1rem;margin:.2rem;border:0;border-radius:.4rem;cursor:pointer} + #quality-toggle{background:#446;color:#ccf} + #quality-toggle.full{background:#464;color:#cfc} #counter{margin-top:1vh;font-size:1.1rem} @@ -84,7 +86,7 @@
- +
@@ -94,6 +96,7 @@ +
@@ -150,10 +153,18 @@ {% if images %}