From 1144b33e33bbf3a05e4e75df8a677c8aa82a3137 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Sun, 1 Jun 2025 20:51:29 +0300 Subject: Add stuff --- .python-version | 2 +- Dockerfile | 10 ++++++ app.py | 55 ++++++++++++++++++---------- compose.yml | 13 +++++++ pyproject.toml | 5 +++ static/ig.svg | 27 ++++++++++++++ static/style.css | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ templates/gallery.html | 72 ++++++++++++++++++++++++++++--------- uv.lock | 29 +++++++++++++++ 9 files changed, 274 insertions(+), 36 deletions(-) create mode 100644 Dockerfile create mode 100644 compose.yml create mode 100644 static/ig.svg create mode 100644 static/style.css diff --git a/.python-version b/.python-version index bd28b9c..e4fba21 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9 +3.12 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a27cb9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine +RUN apk add --no-cache python3 uv + +WORKDIR /app +COPY static /app/static +COPY templates /app/templates +COPY app.py pyproject.toml .python-version uv.lock /app/ +RUN uv sync + +CMD ["uv", "run", "gunicorn", "--bind", "0.0.0.0:8000", "app:app", "-w", "1"] diff --git a/app.py b/app.py index bad1d0b..fe67838 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,7 @@ load_dotenv() import threading import time -from flask import Flask, render_template, send_from_directory +from flask import Flask, render_template, make_response, send_file import requests from datetime import datetime from collections import defaultdict @@ -19,8 +19,10 @@ GDRIVE_FOLDER = os.environ["GDRIVE_FOLDER"] GAPI_KEY = os.environ["GAPI_KEY"] PORT = int(os.environ.get("PORT", 5000)) THUMBNAIL_DIR = "thumbnails" -DB_FILE = "files.db" +DB_FILE = "data/sqlite.db" ISO_DATE_REGEX = re.compile(r"^\d{4}-\d{2}-\d{2}") +TITLE = os.environ.get("TITLE", "Gallery") +IG_LINK = os.environ.get("IG_LINK", None) # Setup os.makedirs(THUMBNAIL_DIR, exist_ok=True) @@ -28,6 +30,9 @@ app = Flask(__name__) # Initialize DB def init_db(): + # Create dir if not exists + os.makedirs(os.path.dirname(DB_FILE), exist_ok=True) + with sqlite3.connect(DB_FILE) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS files ( @@ -62,7 +67,7 @@ def download_and_create_thumbnail(file_id): response = requests.get(url) response.raise_for_status() img = Image.open(BytesIO(response.content)) - img.thumbnail((200, 200)) + img.thumbnail((400, 400)) path = os.path.join(THUMBNAIL_DIR, f"{file_id}.jpg") img.save(path, format="JPEG") return path @@ -82,16 +87,28 @@ def save_files_periodically(): date_match = ISO_DATE_REGEX.match(f["name"]) date = date_match.group(0) if date_match else f["createdTime"][:10] - cur.execute("SELECT 1 FROM files WHERE id = ?", (f["id"],)) - exists = cur.fetchone() + cur.execute("SELECT description FROM files WHERE id = ?", (f["id"],)) + row = cur.fetchone() - if not exists: + if row is None: + # File not in DB — insert new with thumbnail thumb_path = download_and_create_thumbnail(f["id"]) cur.execute(""" - INSERT OR REPLACE INTO files (id, name, description, date, thumbnail) + INSERT INTO files (id, name, description, date, thumbnail) VALUES (?, ?, ?, ?, ?) """, (f["id"], f["name"], f.get("description", ""), date, thumb_path)) - conn.commit() + else: + # File exists — update description if changed + existing_description = row[0] or "" + new_description = f.get("description", "") + if existing_description != new_description: + cur.execute(""" + UPDATE files + SET name = ?, description = ?, date = ? + WHERE id = ? + """, (f["name"], new_description, date, f["id"])) + + conn.commit() # Delete removed files cur.execute("SELECT id, thumbnail FROM files") @@ -125,18 +142,20 @@ def gallery(): }) sorted_dates = sorted(grouped.keys(), reverse=True) - return render_template("gallery.html", grouped=grouped, dates=sorted_dates) + return render_template("gallery.html", + grouped=grouped, dates=sorted_dates, title=TITLE, ig_link=IG_LINK) -# Route: Serve thumbnail @app.route("/thumbnails/") def serve_thumbnail(filename): - return send_from_directory(THUMBNAIL_DIR, filename) + path = os.path.join(THUMBNAIL_DIR, filename) + if not os.path.exists(path): + return "Not Found", 404 + + response = make_response(send_file(path)) + response.headers["Cache-Control"] = "public, max-age=86400, stale-while-revalidate=604800" + return response -# Startup -if __name__ == "__main__": - init_db() - thread = threading.Thread(target=save_files_periodically, daemon=True) - thread.start() +init_db() - print(f"Starting server at http://localhost:{PORT}") - app.run(debug=True, port=PORT) +thread = threading.Thread(target=save_files_periodically, daemon=True) +thread.start() diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..7480e9b --- /dev/null +++ b/compose.yml @@ -0,0 +1,13 @@ +services: + web: + build: . + ports: + - "8000:8000" + environment: + PYTHONUNBUFFERED: 1 + GDRIVE_FOLDER: ${GDRIVE_FOLDER} + GAPI_KEY: ${GAPI_KEY} + TITLE: ${TITLE} + volumes: + - ./data:/app/data + - ./thumbnails:/app/thumbnails diff --git a/pyproject.toml b/pyproject.toml index 94ab690..085910c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,8 @@ dependencies = [ "python-dotenv>=1.1.0", "requests>=2.32.3", ] + +[dependency-groups] +dev = [ + "gunicorn>=23.0.0", +] diff --git a/static/ig.svg b/static/ig.svg new file mode 100644 index 0000000..c61b111 --- /dev/null +++ b/static/ig.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..5ab2f7e --- /dev/null +++ b/static/style.css @@ -0,0 +1,97 @@ +/* Base layout and typography */ +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: #121212; + color: #e0e0e0; + margin: 0; + padding: 20px; +} + +h1 { + font-size: 48px; + margin-bottom: 30px; +} + +h2 { + font-size: 1.5rem; + font-weight: 500; + color: #ffffff; + border-bottom: 1px solid #333; + margin-top: 40px; + padding-bottom: 8px; +} + +/* Flexible, wrap-aligned image gallery */ +.gallery { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: flex-start; +} + +/* Gallery item constrains to image width */ +.gallery-item { + display: inline-block; + margin: 0; + box-sizing: border-box; + overflow: hidden; + vertical-align: top; +} + +/* Image: fixed height, natural width */ +.gallery-item img { + display: block; + height: 200px; + width: auto; + max-width: 100vw; + object-fit: cover; + transition: transform 0.2s ease-in-out; +} + +/* Description: match image width */ +.gallery-item p { + margin: 8px 0 0; + font-size: 0.85rem; + color: #aaaaaa; + line-height: 1.4; + max-width: 100%; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + display: block; +} + +/* Hover effect */ +.gallery-item img:hover { + transform: scale(1.03); +} + +/* Mobile: full width per row */ +@media (max-width: 600px) { + body { + padding: 12px; + } + + h2 { + font-size: 1.2rem; + } + + .gallery { + flex-direction: column; + gap: 12px; + } + + .gallery-item { + width: 100%; + } + + .gallery-item img { + width: 100%; + height: auto; + //max-height: 300px; + } + + .gallery-item p { + padding: 0 8px 8px; + } +} diff --git a/templates/gallery.html b/templates/gallery.html index 2950d8e..70f5d25 100644 --- a/templates/gallery.html +++ b/templates/gallery.html @@ -1,24 +1,59 @@ - Drive Photo Gallery - + {{ title }} + + + + +

+ {{ title }} {% if ig_link %} + + Instagram + + {% endif %} +

+ + {% for date in dates %}

{{ date }}

{% endfor %} - {% endfor %} + {% endfor %} {% if dates|length == 0 %} +

No photos yet.

+ {% endif %} diff --git a/uv.lock b/uv.lock index f653ba6..6d1cf48 100644 --- a/uv.lock +++ b/uv.lock @@ -180,6 +180,11 @@ dependencies = [ { name = "requests" }, ] +[package.dev-dependencies] +dev = [ + { name = "gunicorn" }, +] + [package.metadata] requires-dist = [ { name = "apscheduler", specifier = ">=3.11.0" }, @@ -189,6 +194,21 @@ requires-dist = [ { name = "requests", specifier = ">=2.32.3" }, ] +[package.metadata.requires-dev] +dev = [{ name = "gunicorn", specifier = ">=23.0.0" }] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -299,6 +319,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + [[package]] name = "pillow" version = "11.2.1" -- cgit v1.3