Contents
Project information
- Project: Henry Media Library
- Author: Henry
- Version at the time of writing: Web
v160/ macOS2.22.0- Built with: native ES Modules, IndexedDB, a Python standard-library backend, and SwiftUI
- Status: a personal, local-first media management tool; source code is not currently public
- Article by: Henry
My drives hold tens of thousands of photos and hundreds, sometimes thousands, of videos.
Wedding shoots, commercial work, project material, and everyday records are spread across dozens of folders. After a while, finding one picture becomes a surprisingly clumsy exercise: remember which drive, guess which directory, then try your luck in Finder.
Apple Photos is not really designed for that workflow. It wants to import everything into its own library, build indexes, synchronize, analyze, and keep suggesting cloud services. QuickTime and IINA play videos well, but neither manages tens of thousands of assets for me.
What I actually wanted was simple:
- Keep all data on my own devices or server.
- Search, filter, and browse in a web page.
- Put pictures and videos in the same interface.
- Browse from a phone without uploading the originals to someone else’s cloud.
- Keep my files as ordinary files, rather than locking them inside a proprietary library.
That became Henry Media Library.
1. It Is Not a Cloud Photo Service
The first principle is local-first.
Indexes, thumbnails, tags, groups, and settings live in IndexedDB and LocalStorage. In local mode there are no external requests, uploads, or CDN dependencies. The interface can work offline.
But it is not merely a browser demo.
Browsers can read files, generate thumbnails, play videos, search, and virtualize lists. They cannot freely delete original files, reveal them in Finder, or rename arbitrary files on disk. A thin Python backend handles those boundaries:
- Reveal an original file.
- Delete or rename it.
- Export files to a selected directory.
- Provide uploads and HTTP Range streaming in remote mode.
Everything that can stay in the front end stays there. The backend does only the work that actually needs disk access.
That produces two forms of the same tool:
- Run
python3 server.pylocally and use a browser as the interface. - Deploy it on a personal server and access it from a phone, tablet, or another computer.
There is no account system or subscription. The remote server is my own machine; the “cloud” is simply an entrance to it over the network.
2. Why I Did Not Use React
There is no build step, npm workflow, or bundler.
The front end consists of more than 30 native ES Modules, roughly 8,600 lines of JavaScript. The served files are the finished application. Change a line and refresh.
This was not a romantic commitment to vanilla JavaScript. It was a practical decision for a personal tool that I intend to maintain for years, rather than a product built by a large team.
The advantages are straightforward:
- I do not want to repair a dependency tree before opening the project two years from now.
- Running the page should not require Node.
- The relationship between state, views, and storage remains visible.
- Debugging has fewer layers.
The cost is equally real: routing, state updates, event coordination, and component rendering become my responsibility.
The structure looks roughly like this:
app.js Navigation, toolbar, filters, and shortcuts
store.js Reactive state, event bus, and search pipeline
db.js IndexedDB wrapper
scanner.js Incremental scanning and metadata scheduling
thumbnails.js Thumbnail workers and video frame extraction
views.js Four views and virtual scrolling
viewer.js Image viewer
player.js Video player
manage.js Rename, delete, trash, and tags
advanced.js Duplicate detection, maps, and password lock
remote.js Remote-mode client
It is not particularly fashionable, but it is understandable. For something I use every day, that matters more.
3. With Thousands of Files, Rendering Is Only the Beginning
The earliest version rendered every media card at once. A few thousand images were barely manageable; larger libraries started dropping frames.
The browser was not simply incapable of displaying pictures. I was creating too many DOM nodes simultaneously.
The view layer eventually moved to virtual scrolling:
- Calculate the visible range from the scroll position.
- Render that range with a small buffer on either side.
- Preserve the scrollbar with a correctly sized spacer.
- Recalculate when filters or views change instead of rebuilding everything.
Virtual scrolling now activates above 500 items. A five-figure index does not produce a five-figure collection of DOM cards, and generating thumbnails no longer has to block the first screen.
Thumbnail work follows the same principle.
Image processing and video-frame tasks are scheduled through workers where appropriate, leaving the main thread to coordinate and render. Video covers use a <video> element positioned at the desired time and a <canvas> capture. In remote mode, server-side FFmpeg or Pillow is preferred for small thumbnails of approximately 2–50 KB, avoiding the need to download a whole video just to show its cover.
The practical limit of a media manager is often not decoding speed, but how many things it tries to create at the same time.
4. Four Views for Four Ways of Finding Something
People do not always look for files in the same way.
The application therefore offers:
- Grid / masonry: quick visual browsing.
- List: filenames, formats, sizes, dates, and durations.
- Timeline: collapsible year, month, and day groups.
- Folders: the disk-like hierarchy, with breadcrumbs.
The first three answer “What am I looking for?” Folders answer “I know where I put it.”
Search and filters follow actual usage:
- Images or videos.
- Date, format, size, and resolution.
- Fuzzy filename search.
- Pinyin initials, so
wjgpcan find a Chinese filename meaning “wedding shoot.” #tagsearch.- Multiple conditions together.
For Chinese filenames, initial-letter matching is a small but valuable detail. I can find something without changing input methods or recalling its exact name.
5. Pictures and Videos Should Not Require Separate Applications
The image viewer supports wheel zoom, panning, 90-degree rotation, horizontal and vertical flips, fit-to-window, fit-to-width, original-size viewing, and EXIF/GPS information.
The video player supports:
- Playback, seeking, volume, and mute.
- Speeds from 0.5× to 2×.
- Frame stepping.
- Ten-second jumps.
- Thumbnail previews over the progress bar.
- Remembering the last playback position.
- Single-item and playlist looping.
One platform trade-off became particularly clear.
On desktop, custom controls fit the rest of the application. On iPhone, inline native video controls are more limited, so I tried taking over with my own. Loading, gestures, and fullscreen behavior became less reliable.
The final choice was native controls on phones, custom controls on desktop.
That is not giving up. It is acknowledging the platform. A web application can resemble a native app without fighting every system control.
6. From Virtual Groups to Real Folders
Early groups allowed a media item to belong to several albumIds. That works for collections, but not for a familiar file-manager hierarchy.
The later model introduced:
- A
parentIdfor each folder. - One primary location for each media item.
- Create, rename, delete, move, and drag operations.
- Breadcrumbs.
- The existing 2×2 thumbnail folder covers.
- Trash and restoration that preserve the hierarchy.
This was a data-model migration, not just a new UI.
Old groups allowed multiple memberships; real folders require one main location. Existing groups needed conversion into top-level folders, while duplicate memberships, favorites, tags, shared links, and trash references all needed attention.
A full backup came before migration. Tests included 500 folders and nested directories:
- The first screen inside a folder stayed bounded at 120 items.
- Switching folders did not reload the whole page.
- Breadcrumbs and parent navigation worked.
- The console remained free of errors.
The difficult part of a file manager is not drawing a tree. It is maintaining paths, references, and migrations.
7. Small Problems That Were Not Small
The iOS Safe Area Squeezed the Bottom Navigation
After adding viewport-fit=cover, env(safe-area-inset-bottom) became active. The floating tab bar already used a bottom offset that accounted for it, but inherited another safe-area padding rule. A 60-pixel bar lost 34 pixels of usable space, leaving half-visible controls.
The fix was one line. Understanding it took longer: when the floating bar’s position already handles the safe area, do not charge for it again inside the bar.
Video Previews Cannot Seek on Every Pointer Move
Moving over a progress bar creates many events. Seeking a hidden video and painting a canvas for each one can overwhelm playback.
The solution throttles seeks, keeps the latest meaningful position, and reuses the hidden video and canvas. A tiny preview panel has its own scheduling problem behind it.
Custom Controls Are Not Automatically Better
I initially thought custom video controls would unify desktop and mobile. On phones, they made loading, fullscreen, and gestures worse.
Native controls may be less visually consistent, but they are stable and meet platform expectations. Uniformity is not worth making the application harder to use.
8. Privacy Is More Than a Slogan
What makes me comfortable with this tool is that I do not have to hand my photos to another company.
In local mode:
- IndexedDB holds indexes, thumbnails, tags, and groups.
- LocalStorage holds settings.
- The password lock stores a SHA-256 hash.
- No external network requests are required.
- Original media remains in its existing directories.
In remote mode, data stays on my own server. Media storage is separate from application code, so rebuilding a container does not delete the host’s photos or videos.
The essential reminder remains: back up the server’s media directory regularly.
Code can be redeployed. Indexes and thumbnails can be rebuilt. Original photographs cannot.
9. Where the Project Was at This Point
From the first version in late August 2026 to this article:
- Web version
v160. - macOS client
2.22.0. - Approximately 8,600 lines of front-end JavaScript.
- Approximately 3,900 lines across markup, styles, and the Python backend.
- Approximately 2,500 lines in the SwiftUI client.
- Several terabytes of media.
- Folders, trash, duplicate detection, offline maps, image annotation, password locking, remote uploads, and mobile access.
Version numbers climbed quickly. v153 addressed safe areas, v154 the tab bar, v155 the mobile player, v156 returned to native controls, v157 fixed a scroll animation, v158 began real folders, and v160 completed nested directories.
Every small version came from something that felt wrong in real use.
10. In the End
This project has no funding round, growth target, or commercial cloud.
It solves one concrete problem:
My own photos and videos should remain mine to manage.
It also changed how I see the browser. File access, IndexedDB, workers, Canvas, Range streaming, and offline caching can form a genuinely useful media tool.
The choices matter more than the stack:
- Do not force every platform to look identical.
- Do not trade loading speed for a longer feature list.
- Keep data on the owner’s devices.
- Do not add complexity that a personal tool does not need.
Sometimes the best cloud is the computer you already own.
Open-Source Projects and Third-Party Components
Henry wrote the core application. Its runtime, media processing, and deployment environment use the following projects and platform capabilities. Names, code, and trademarks belong to their respective owners.
Python
- Project: Python.
- Website: python.org.
- License: Python Software Foundation License 2.0.
- Use: local HTTP service, file operations, Range streaming, metadata, and thumbnail scheduling.
Pillow
- Repository: python-pillow/Pillow.
- License: HPND / PIL License.
- Use: image loading, thumbnails, and basic EXIF information. Without Pillow, the application falls back to original files or FFmpeg paths.
FFmpeg / FFprobe
- Website: ffmpeg.org.
- License: LGPL-2.1-or-later; particular binaries may include GPL components depending on their build.
- Use: video covers, metadata, and formats not directly readable by the browser. The project does not modify FFmpeg source.
Docker Engine
- Website: docker.com.
- License: Apache-2.0 for Docker Engine.
- Use: building and running server containers.
Nginx / OpenResty
- Nginx: nginx.org, BSD-2-Clause; reverse proxy, HTTPS, and Range forwarding.
- OpenResty, when supplied by 1Panel: openresty.org, its BSD-style license as detailed in COPYRIGHT; website and HTTPS runtime.
1Panel
- Repository: 1Panel-dev/1Panel.
- License: GPL-3.0.
- Use: personal server management, container orchestration, and reverse-proxy configuration.
Let’s Encrypt
- Website: letsencrypt.org.
- Nature: a free public certificate authority, not code bundled into this project.
- Use: TLS certificates for remote access.
Apple Frameworks
The macOS client uses no third-party Swift packages. It uses SwiftUI, AppKit, AVKit / AVFoundation, WebKit, Combine, and CryptoKit.
These are Apple system frameworks, not this project’s open-source dependencies, and are not distributed with its source. Apple and its product names remain their owners’ trademarks.
Web Platform Capabilities
IndexedDB, LocalStorage, Web Workers, Canvas, File System Access API, HTTP Range, and Service Workers are browser-platform capabilities, not third-party JavaScript frameworks.
The current front end does not use React, Vue, Svelte, jQuery, an external UI library, or an external CDN.
This Project
Henry Media Library is independently developed by Henry. Its source is not currently public and no open-source license has been declared for the project itself.
Architecture, features, and versions are recorded here for development history, not as a promise of source publication, redistribution, or commercial licensing. This article contains no real access tokens, server addresses, or sensitive credentials.
Project and article by Henry. Recorded versions: Web v160, macOS 2.22.0. Source code is not currently public.
Comments 0
Leave a thought or add to the conversation. Comments appear after moderation.
Loading comments…
Be the first to leave a thought.