‹ Back homeTechnology

From an iOS-inspired photo gallery on Ubuntu to Henry Media Library: features, failure points, technical choices, and open-source notices.

Oct 19, 2026·6 min read·Reads …

Contents
  1. I only wanted a better-looking gallery
  2. What it could do
  3. iOS-style grid
  4. Full-screen viewer
  5. Folders as albums
  6. Upload, delete, and album management
  7. Video seeking
  8. A zero-dependency Python backend
  9. The details that consumed the time
  10. hidden versus class-based state
  11. Chinese file names
  12. Large videos
  13. 1Panel Compose paths
  14. Why I abandoned it
  15. It was still a remote folder browser
  16. The server dependency was heavy
  17. Large-library performance
  18. It had no real library
  19. What it gave the successor
  20. Current state
  21. Open-source and third-party notices
  22. Python
  23. Pillow
  24. FFmpeg
  25. Nginx / OpenResty
  26. 1Panel
  27. Docker
  28. Apple and iOS
  29. Project

Project

  • Name: iOS-style photo and video gallery
  • Status: no longer maintained
  • Started: August 2026
  • Environment: Ubuntu, Docker, systemd, Nginx / OpenResty
  • Stack: Python standard library, vanilla JavaScript, CSS, Pillow, FFmpeg
  • Nature: personal web-gallery experiment; source is currently private
  • Article: Henry

Before Henry Media Library, I built a more direct thing: an iOS-style photo and video gallery running on an Ubuntu server.

The goal was to point it at media folders, open a browser, and browse in a way that felt like iPhone Photos: grids, albums, a full-screen viewer, video playback, upload, delete, search, and a little frosted glass.

It eventually ran and implemented most of that. It never became the media tool I used every day. In hindsight it was a prototype and one of the real inspirations for Henry Media Library.

The problem was finding files. Photos and videos were spread across directories. Finder was slow, bare directory listings were unpleasant, and importing everything into Photos conflicted with my desire to preserve my folder structure.

The constraints were specific:

  • Run on my own Ubuntu server.
  • Display photos and videos.
  • Use an iOS-like grid, albums, and full-screen viewer.
  • Avoid Node.js, a database, or a complex backend.
  • Keep files in ordinary folders.
  • Support upload and delete in the browser.

The project added a browsing layer to a filesystem. It did not manage a library. That boundary made it quick to build and also explained why it eventually stopped.

What it could do

iOS-style grid

The home screen grouped media by date, mixed photos and video thumbnails, showed video duration, supported filters, and searched file names. It had frosted navigation, light and dark themes, and custom SVG icons rather than Apple assets.

Full-screen viewer

The viewer supported previous/next navigation, arrow keys, wheel and pinch zoom, double-click zoom, video playback, download, and delete.

That interaction made me realize that a gallery feels native not because of frosted glass, but because scrolling, zooming, switching, and feedback remain continuous.

Folders as albums

Each subfolder of media became an album. There was no import step and no separate album database. I kept this idea for Henry Media Library: the filesystem is a legitimate structure.

Upload, delete, and album management

The browser supported click and drag upload, progress overlays, collision-safe renaming, Chinese file names, deletion from the viewer, and album creation or renaming. Uploads were parsed as streaming multipart data and written to disk, including a round-trip check with a video over 50 MB.

Video seeking

The backend implemented HTTP Range responses, and the Nginx proxy disabled buffering where necessary so the browser could seek without loading the whole video.

A zero-dependency Python backend

python3 server.py was enough to start it. Pillow and FFmpeg were optional enhancements for thumbnails and video posters.

The details that consumed the time

hidden versus class-based state

The viewer used the HTML hidden attribute while JavaScript controlled visibility through classes. Clicking an image looked like nothing happened because two display models contradicted each other. A UI bug was really a state-ownership bug.

Chinese file names

Multipart headers were decoded with the wrong assumptions. The fix was to preserve raw bytes and decode the filename as UTF-8.

Large videos

The first parser loaded the entire request into memory. Streaming multipart writes made real video uploads practical.

1Panel Compose paths

build: . was resolved relative to 1Panel’s own Compose directory, producing a missing Dockerfile. The solution was to select the repository’s Compose file explicitly or use an absolute build context.

Why I abandoned it

It was still a remote folder browser

It could not index EXIF, locations, devices, duplicates, ratings, or long-term media metadata. It also had no incremental client-server synchronization.

The server dependency was heavy

Viewing a photo required a live server, running container, working network, and valid certificate. Henry Media Library became local-first because of this friction.

Large-library performance

Thousands of files were manageable. Tens of thousands of photos and videos exposed slow scans, large thumbnail caches, browser memory pressure, video-format differences, and slow mobile loading.

It had no real library

Folders are a good starting point but not a complete data model. Without indexes, metadata, and search, the project stayed at “can browse”.

The successor changed the architecture:

  • Browser for the interface.
  • IndexedDB for local indexes.
  • Python for file and media processing.
  • SwiftUI for native macOS interaction.
  • Files kept ordinary while browsing state no longer depended entirely on the server.

Continue with: Henry Media Library.

What it gave the successor

  • Filesystem structure remained valuable.
  • Browser interaction can feel native.
  • Reliable upload and deletion matter more than the happy path.
  • A feature-rich prototype is not a long-term product.

The prototype answered “how do I browse files on a server?” It did not answer “how do I manage my entire media life?”

Current state

The project is no longer maintained. Its deployment no longer serves as my primary photo entry point. The source and documentation remain as a record that a Python-standard-library server and vanilla JavaScript can produce a convincing prototype, but without an index and a local-first strategy it cannot become a real media library.


Open-source and third-party notices

Python

  • python.org
  • License: PSF-2.0
  • Use: HTTP server, directory scanning, Range responses, upload parsing.

Pillow

FFmpeg

  • ffmpeg.org
  • License: LGPL-2.1-or-later, depending on build options; some binaries may include GPL components
  • Use: optional video poster extraction. No FFmpeg source was modified.

Nginx / OpenResty

  • nginx.org
  • License: BSD-2-Clause
  • Use: reverse proxy, HTTPS, and video Range forwarding.

OpenResty, when supplied through 1Panel, remains under its own BSD-style license.

1Panel

Docker

  • docker.com
  • License: Apache-2.0
  • Use: building and running the gallery container.

Apple and iOS

The project referenced the information architecture of iPhone Photos but used no Apple icons, SF Symbols, or proprietary assets. iOS, iPhone, and Apple are trademarks of their owners. This was an unofficial personal experiment with no affiliation or endorsement.

Project

The gallery was written by Henry. Its README once identified it as MIT licensed, but the source is not currently published as an open-source repository. This article only records its history and includes no real server address, password, or API credential.

The real lesson was that the distance between “can browse” and “can manage for years” is not a prettier UI. It is an entire data model.

H
Henry

Notes on technology, design, and the slower observations of everyday life.

About the author
DISCUSSION

Comments 0

Leave a thought or add to the conversation. Comments appear after moderation.

Loading comments…