diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2025-06-01 20:51:29 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2025-06-01 20:54:35 +0300 |
| commit | 1144b33e33bbf3a05e4e75df8a677c8aa82a3137 (patch) | |
| tree | 96bf5082386ccf92ecce51b1d19b6188074aee5d | |
| parent | 73378c104a0774c213e2ede7c557b14c40f8a12d (diff) | |
Add stuff
| -rw-r--r-- | .python-version | 2 | ||||
| -rw-r--r-- | Dockerfile | 10 | ||||
| -rw-r--r-- | app.py | 55 | ||||
| -rw-r--r-- | compose.yml | 13 | ||||
| -rw-r--r-- | pyproject.toml | 5 | ||||
| -rw-r--r-- | static/ig.svg | 27 | ||||
| -rw-r--r-- | static/style.css | 97 | ||||
| -rw-r--r-- | templates/gallery.html | 72 | ||||
| -rw-r--r-- | uv.lock | 29 |
9 files changed, 274 insertions, 36 deletions
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"] @@ -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/<filename>") 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 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint0_radial_87_7153)"/>
+<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint1_radial_87_7153)"/>
+<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint2_radial_87_7153)"/>
+<path d="M23 10.5C23 11.3284 22.3284 12 21.5 12C20.6716 12 20 11.3284 20 10.5C20 9.67157 20.6716 9 21.5 9C22.3284 9 23 9.67157 23 10.5Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16 21C18.7614 21 21 18.7614 21 16C21 13.2386 18.7614 11 16 11C13.2386 11 11 13.2386 11 16C11 18.7614 13.2386 21 16 21ZM16 19C17.6569 19 19 17.6569 19 16C19 14.3431 17.6569 13 16 13C14.3431 13 13 14.3431 13 16C13 17.6569 14.3431 19 16 19Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6 15.6C6 12.2397 6 10.5595 6.65396 9.27606C7.2292 8.14708 8.14708 7.2292 9.27606 6.65396C10.5595 6 12.2397 6 15.6 6H16.4C19.7603 6 21.4405 6 22.7239 6.65396C23.8529 7.2292 24.7708 8.14708 25.346 9.27606C26 10.5595 26 12.2397 26 15.6V16.4C26 19.7603 26 21.4405 25.346 22.7239C24.7708 23.8529 23.8529 24.7708 22.7239 25.346C21.4405 26 19.7603 26 16.4 26H15.6C12.2397 26 10.5595 26 9.27606 25.346C8.14708 24.7708 7.2292 23.8529 6.65396 22.7239C6 21.4405 6 19.7603 6 16.4V15.6ZM15.6 8H16.4C18.1132 8 19.2777 8.00156 20.1779 8.0751C21.0548 8.14674 21.5032 8.27659 21.816 8.43597C22.5686 8.81947 23.1805 9.43139 23.564 10.184C23.7234 10.4968 23.8533 10.9452 23.9249 11.8221C23.9984 12.7223 24 13.8868 24 15.6V16.4C24 18.1132 23.9984 19.2777 23.9249 20.1779C23.8533 21.0548 23.7234 21.5032 23.564 21.816C23.1805 22.5686 22.5686 23.1805 21.816 23.564C21.5032 23.7234 21.0548 23.8533 20.1779 23.9249C19.2777 23.9984 18.1132 24 16.4 24H15.6C13.8868 24 12.7223 23.9984 11.8221 23.9249C10.9452 23.8533 10.4968 23.7234 10.184 23.564C9.43139 23.1805 8.81947 22.5686 8.43597 21.816C8.27659 21.5032 8.14674 21.0548 8.0751 20.1779C8.00156 19.2777 8 18.1132 8 16.4V15.6C8 13.8868 8.00156 12.7223 8.0751 11.8221C8.14674 10.9452 8.27659 10.4968 8.43597 10.184C8.81947 9.43139 9.43139 8.81947 10.184 8.43597C10.4968 8.27659 10.9452 8.14674 11.8221 8.0751C12.7223 8.00156 13.8868 8 15.6 8Z" fill="white"/>
+<defs>
+<radialGradient id="paint0_radial_87_7153" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12 23) rotate(-55.3758) scale(25.5196)">
+<stop stop-color="#B13589"/>
+<stop offset="0.79309" stop-color="#C62F94"/>
+<stop offset="1" stop-color="#8A3AC8"/>
+</radialGradient>
+<radialGradient id="paint1_radial_87_7153" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11 31) rotate(-65.1363) scale(22.5942)">
+<stop stop-color="#E0E8B7"/>
+<stop offset="0.444662" stop-color="#FB8A2E"/>
+<stop offset="0.71474" stop-color="#E2425C"/>
+<stop offset="1" stop-color="#E2425C" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint2_radial_87_7153" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(0.500002 3) rotate(-8.1301) scale(38.8909 8.31836)">
+<stop offset="0.156701" stop-color="#406ADC"/>
+<stop offset="0.467799" stop-color="#6A45BE"/>
+<stop offset="1" stop-color="#6A45BE" stop-opacity="0"/>
+</radialGradient>
+</defs>
+</svg>
\ 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 @@ <!doctype html> <html> <head> - <title>Drive Photo Gallery</title> - <style> - img { - max-width: 200px; - margin: 10px; - display: block; - } - .gallery { - display: flex; - flex-wrap: wrap; - } - .gallery-item { - margin: 10px; - text-align: center; - } - </style> + <title>{{ title }}</title> + <link + rel="stylesheet" + href="{{ url_for('static', filename='style.css') }}" + /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> </head> <body> + <script> + function layout() { + const isMobile = + window.matchMedia("(max-width: 600px)").matches; + + document.querySelectorAll(".gallery-item").forEach((item) => { + const img = item.querySelector("img"); + if (!img) return; + + if (isMobile) { + // On mobile, reset any previously set width + item.style.width = ""; + return; + } + + const setWidth = () => { + item.style.width = img.offsetWidth + "px"; + }; + + if (img.complete) { + setWidth(); + } else { + img.addEventListener("load", setWidth, { once: true }); + } + }); + } + + document.addEventListener("DOMContentLoaded", layout); + window.addEventListener("resize", layout); + </script> + + <h1> + {{ title }} {% if ig_link %} + <a href="{{ ig_link }}" target="_blank" class="ig-link"> + <img + src="{{ url_for('static', filename='ig.svg') }}" + alt="Instagram" + width="30" + height="30" + /> + </a> + {% endif %} + </h1> + + <!-- --> {% for date in dates %} <h2>{{ date }}</h2> <div class="gallery"> @@ -31,6 +66,7 @@ <img src="{{ url_for('serve_thumbnail', filename=f.thumbnail.split('/')[-1]) }}" alt="{{ f.name }}" + loading="lazy" /> </a> {% if f.description %} @@ -39,6 +75,8 @@ </div> {% endfor %} </div> - {% endfor %} + {% endfor %} {% if dates|length == 0 %} + <p>No photos yet.</p> + {% endif %} </body> </html> @@ -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" @@ -300,6 +320,15 @@ wheels = [ ] [[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" source = { registry = "https://pypi.org/simple" } |
