Demo: https://img.amiya.eu.org/ (no upload access)
A small self-hosted photo gallery (Flask + static HTML) with:
- Grid gallery + lightbox viewer
- Admin “Manage” page for upload, pin/unpin, description edits, and delete
- Automatic WebP + AVIF conversion on upload
- Lazy-generated WebP thumbnails (~50KB target)
- Optional downloads as AVIF or JPG
api/main.py— Flask app + JSON APIsapi/page/index.html— gallery UI (/)api/page/manage.html— admin UI (/manage)api/requirements.txt— Python dependenciesphoto/— stored images + metadataphoto/YYYY/MM/— images stored as*.webpand (if available)*.avifphoto/thumb/— generated thumbnails (*.webp)photo/download/— cached JPG conversions (*.jpg)photo/description/— per-image descriptions (<id>.txt)photo/.meta.json— pinned state + captured datetimes
Note: this repo’s .gitignore ignores photo/ by default.
- Python 3.x
- macOS/Linux/Windows supported (uses Pillow for image processing)
cd api
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python main.pyrun in Server mode:
pip install -r api/requirements.txt && pip install waitress && waitress-serve --host=0.0.0.0 --port=5088 --threads=16 --connection-limit=300 --channel-timeout=120 api.main:appThen open:
- Gallery:
http://localhost:5088/ - Admin:
http://localhost:5088/manage(redirects to login)
The admin page uses a simple session cookie.
Environment variables:
MIO_GALLERY_PASSWORD— manage password (default:Admin123)MIO_GALLERY_SECRET— Flask session secret (default:dev-secret-change-me)
Example:
export MIO_GALLERY_PASSWORD='change-me'
export MIO_GALLERY_SECRET='a-long-random-secret'
python api/main.pyOn upload, files are:
- Temporarily written under
photo/and validated (type + size) - Converted to
WebPand (when supported)AVIF - Saved under
photo/YYYY/MM/using an id likeYYYYMMDD_HHMMSS_<hash> - Datetime saved to
photo/.meta.json(EXIF preferred; upload time fallback)
Pages:
GET /— gallery UIGET /manage— manage UI (requires admin session)GET|POST /manage-login— login form
APIs:
GET /api/healthGET /api/images?start_date=YYYY-MM-DD&end_date=YYYY-MM-DDPOST /api/upload—multipart/form-datawithimageorimagesPUT /api/images/<id>/pin— JSON{ "pinned": true|false }(or toggle if omitted)GET|PUT /api/images/<id>/description— JSON{ "description": "..." }DELETE /api/images/<id>GET /api/images/<id>/download?format=avif|jpgGET /api/images/<path:filename>— serves files fromphoto/GET /api/thumb/<id>.webp— thumbnail (generated on demand)
