文章目录
项目信息
- 项目:Henry 媒体库 / Henry Media Library
- 作者:Henry
- 当前版本:Web
v160/ macOS2.22.0- 技术组成:原生 ES Modules、IndexedDB、Python 标准库后端、SwiftUI
- 项目性质:个人本地媒体管理工具,源码目前未公开
- 本文作者:Henry
我的硬盘里有几万张照片和几百上千个视频。
婚礼跟拍、商拍、项目素材、日常记录,散落在几十个文件夹里。时间久了之后,找一张照片变成了一件很笨的事情:先回忆它在哪个盘,再猜它可能在哪个目录,最后用访达的搜索框碰运气。
系统自带的“照片”并不是为这种使用方式设计的。它会想把资料导入自己的库,建索引、同步、分析,还会不断提醒我使用云服务。QuickTime 和 IINA 能播放视频,却不能替我管理几万条素材。
我真正想要的其实很简单:
- 所有数据留在自己的设备或服务器上;
- 打开网页就能搜索、筛选和浏览;
- 图片和视频在同一个界面里;
- 手机上也能看,但不需要把媒体上传到第三方云盘;
- 我的文件仍然是我的文件,不被某个应用锁进私有数据库。
于是有了 Henry 媒体库。
一、它不是“云相册”
Henry 媒体库的第一条原则是本地优先。
所有索引、缩略图、标签、分组和设置都保存在浏览器的 IndexedDB 和 LocalStorage 中。本机模式下,它不联网、不上传、不依赖任何外部 CDN,断网也能打开。
但它也不是一个纯浏览器玩具。
浏览器可以读取文件、生成图片缩略图、播放视频、做检索和虚拟滚动,却不能直接删除原文件、打开文件所在位置、重命名磁盘上的文件。于是项目里有一个很薄的 Python 后端,只负责浏览器做不到的事情:
- 打开原位置;
- 删除或重命名原文件;
- 导出文件到指定目录;
- 远程模式下提供媒体上传和 Range 流式播放。
换句话说,能放在前端的能力全部放前端,只有必须碰磁盘的操作才交给后端。
这让项目拥有了两种形态:
- 在本机运行
python3 server.py,浏览器就是媒体库界面; - 部署到自己的服务器,手机、平板和另一台电脑都能访问。
它没有账号系统,也不需要订阅。远程服务器就是自己的电脑,所谓的“云”,只是自己机器在公网上的一个入口。
二、为什么最后没有用 React
这个项目没有构建步骤,没有 npm,没有打包器。
前端由 30 多个原生 ES Module 组成,大约 8600 行 JavaScript。打开页面就是最终产物,改一行代码刷新就生效。
这不是为了追求“纯原生”的浪漫,而是一个非常实际的判断:这是一个个人长期维护的工具,不是需要多人协作、频繁发布的产品。
不引入框架带来了几个好处:
- 两年后打开项目,不需要先修复依赖树;
- 页面可以直接运行,不需要 Node 环境;
- 状态、视图和数据库之间的关系是可见的;
- 出问题时,定位路径更短。
代价也很明显:路由、状态管理、事件总线、组件更新都要自己写。
最终的结构大概是:
app.js 主入口:导航、工具栏、筛选、快捷键
store.js 响应式状态 + 事件总线 + 检索管线
db.js IndexedDB 封装
scanner.js 增量扫描 + 元数据调度
thumbnails.js 缩略图 Worker 池 + 视频抽帧
views.js 四种视图 + 虚拟滚动
viewer.js 图片查看器
player.js 视频播放器
manage.js 重命名 / 删除 / 回收站 / 标签
advanced.js 重复检测 / 地图 / 密码锁
remote.js 远程模式客户端
这套结构没有“框架感”,但足够清楚。对于一个自己每天都会用的工具来说,清楚比先进更重要。
三、万级文件,真正的问题不是“能不能显示”
最早的版本会把所有媒体卡片一次性渲染出来。几千张图片还能勉强滚动,文件数量继续增加之后,页面就开始明显掉帧。
问题不在于浏览器不能显示图片,而在于它一次性创建了太多 DOM 节点。
后来视图层改成虚拟滚动:
- 根据滚动位置计算当前可见范围;
- 只渲染可视区域和前后少量缓冲项;
- 用一个足够高的占位元素维持滚动条;
- 切换视图或筛选条件时重新计算范围,而不是重建整个列表。
现在媒体数量超过 500 条就会自动进入虚拟滚动。万级索引不会创建万个卡片节点,首屏也不会因为生成缩略图而卡死。
缩略图也是同样的思路。
图片压缩和视频抽帧放在 Web Worker 中执行,主线程只负责排队和渲染。视频封面用 <video> 定位到指定时间,再用 <canvas> 画出来;远程模式下优先由服务器端的 ffmpeg 或 Pillow 生成 2~50KB 的小图,客户端不再下载整个视频来截一张封面。
媒体管理器的性能上限,往往不是解码速度,而是“你同时创建了多少东西”。
四、四种视图,其实对应四种找文件的方式
同一个媒体库,用户找文件的方式并不只有一种。
所以页面提供了四种视图:
- 网格 / 瀑布流:适合快速浏览和看整体氛围;
- 列表:适合看文件名、格式、大小、时间和时长;
- 时间线:按年 → 月 → 日折叠,适合回忆某次拍摄;
- 文件夹:还原磁盘目录结构,带面包屑导航。
前三种视图解决“我想看什么”,文件夹视图解决“我知道它在哪”。
筛选和搜索也尽量贴近真实使用:
- 按图片 / 视频筛选;
- 按时间、格式、大小、分辨率筛选;
- 文件名模糊搜索;
- 拼音首字母匹配,例如
wjgp可以找到“婚礼跟拍”; #标签搜索;- 多个条件叠加。
对于中文文件名来说,拼音首字母匹配是非常实用的小功能。它不要求用户切换输入法,也不要求记住完整标题,只要记得大概的发音顺序就能找到文件。
五、图片和视频,不该被拆成两个应用
图片查看器支持滚轮缩放、拖拽平移、90° 旋转、水平 / 垂直翻转、适应窗口、适应宽度和 1:1 原图模式,也可以直接查看 EXIF 和 GPS 信息。
视频播放器支持:
- 播放、暂停、进度拖动、音量和静音;
- 0.5x 到 2x 倍速;
- 逐帧前进 / 后退;
- 前后 10 秒跳转;
- 进度条悬停显示对应帧缩略图;
- 播放记忆,下次打开自动回到上次位置;
- 单曲循环和列表循环。
这里有一个很典型的平台取舍。
桌面端使用自定义播放器控制栏,因为可以做得更符合媒体库的整体界面;但在 iPhone 上,竖屏内嵌的 <video> 原生控件非常精简,和桌面端功能差距很大。我曾尝试完全用自定义控件接管手机播放器,结果加载速度、手势和全屏行为都变得不稳定。
最后的选择是:手机端回到原生控件,桌面端保留自定义控制栏。
这不是技术上的退让,而是承认平台边界。网页可以模仿原生,但没必要在每个平台上都和系统控件对抗。
六、从“分组”到真正的文件夹
项目早期使用的是虚拟分组:一个媒体可以同时属于多个 albumIds。它适合做收藏和主题集合,但不像电脑上的文件夹。
后来我把它改成了真正的文件管理器结构:
- 每个文件夹有
parentId,支持无限层级; - 每个媒体有一个固定主位置;
- 支持新建、重命名、删除、移动和拖拽;
- 顶部显示面包屑;
- 文件夹封面保留原来的 2×2 缩略图;
- 删除文件夹时进入回收站,恢复时保留原层级。
这次改造不是换一套 UI,而是数据模型迁移。
原来的分组允许一个文件同时出现在多个集合里;真正的文件夹只允许一个主位置。为了兼容旧数据,需要先把现有分组转换成顶层文件夹,再处理重复归属、收藏、标签、分享链接和回收站里的路径引用。
正式迁移前做了完整备份,并在 500 个文件夹、多层目录的场景下测试:
- 进入子目录后首屏保持 120 项;
- 切换目录不整页重载;
- 面包屑和返回上级正常;
- 控制台无报错。
这次改造让我更清楚地认识到:文件管理器的难点不是树形结构,而是路径、引用和迁移。
七、几个看起来很小、实际很难的坑
iOS 安全区把底部导航挤坏
加入 viewport-fit=cover 之后,env(safe-area-inset-bottom) 开始生效。悬浮 tab 栏已经通过 bottom: calc(8px + env(...)) 浮在安全区上方,却又继承了另一套样式里的底部 padding,导致 60px 高的栏被 34px 的安全区吃掉,按钮只露出半截。
修复只有一行,但前提是先理解:悬浮导航的 bottom 定位已经处理了安全区,内部就不能再重复计算一次。
视频进度条悬停预览不能每次都 seek
鼠标在进度条上移动时会触发大量事件。如果每次移动都让隐藏的 <video> seek 到对应时间再画 canvas,播放器会直接被拖垮。
最后的做法是对 seek 做节流,只保留最后一次有效位置,并复用隐藏的视频元素和 canvas。看起来只是一个小浮层,背后其实是一套独立的帧调度。
自定义播放器不一定比原生好
我曾经认为自定义播放器可以统一桌面和手机体验。实际结果是手机上的加载、全屏和手势都更差。
原生控件不漂亮,但它稳定、符合系统预期,也自动处理了很多边界情况。网页应用不应该为了“统一”而牺牲平台本身的可用性。
八、隐私不是一句口号
这个项目最让我安心的地方,是它不需要把照片交给别人。
本地模式下:
- 索引、缩略图、标签、分组存在浏览器 IndexedDB;
- 设置存在 LocalStorage;
- 密码锁只保存 SHA-256 哈希;
- 不发起外部网络请求;
- 媒体文件仍然在原来的磁盘目录里。
远程模式下,数据也只在自己的服务器上。媒体目录独立于应用代码,删除或重建容器不会删除宿主机上的照片和视频。
唯一需要反复提醒自己的事情是:服务器上的媒体目录一定要定期备份。
代码可以重新部署,索引可以重建,缩略图可以重新生成,但原始照片不能。
九、现在做到哪了
从 2026 年 8 月底写下第一版,到现在大约两个月:
- 网页端迭代到
v160; - macOS 客户端
2.22.0; - 前端约 8600 行 JavaScript;
- 页面、样式与 Python 后端约 3900 行;
- SwiftUI 客户端约 2500 行;
- 媒体库已积累到几个 TB 的照片和视频;
- 文件夹、回收站、重复检测、离线地图、图片批注、密码锁、远程上传和移动端访问都已落地。
版本号涨得很快。v153 修安全区,v154 修 tab 栏,v155 重做手机播放器,v156 改回原生控件,v157 修下滑动画,v158 开始做真实文件夹,v160 完成多层目录。
每一个小版本背后,都是一次真实使用中发现的“不顺手”。
十、写在最后
这个项目没有融资,没有用户增长,也没有云端。
它只是解决了一个很具体的问题:
我自己的照片和视频,应该由我自己来管。
做完之后,我对浏览器有了新的认识。文件系统、IndexedDB、Web Worker、Canvas、Range 流媒体、离线缓存,这些能力组合起来,足以支撑一个真正可用的媒体管理工具。
但更重要的不是技术栈,而是取舍:
- 不追求所有平台都长一样;
- 不为了功能数量牺牲加载速度;
- 不把用户数据搬到自己的服务器之外;
- 不在一个个人工具里引入不必要的复杂度。
有时候,最好的云,就是你自己那台电脑。
开源项目与第三方组件声明
Henry 媒体库的核心代码由 Henry 编写,但运行时、媒体处理和部署环境使用了以下开源软件或平台能力。相关名称、代码和商标归各自作者、维护者与项目所有。
Python
- 项目:Python
- 官网:python.org
- 许可证:Python Software Foundation License 2.0(PSF-2.0)
- 用途:实现本地 HTTP 服务、文件操作接口、Range 流媒体、元数据与缩略图调度。
Pillow
- 项目:Pillow
- 仓库:github.com/python-pillow/Pillow
- 许可证:HPND / PIL License
- 用途:读取图片、生成缩略图和处理基础 EXIF 信息。没有安装 Pillow 时,项目会退化为原始文件或 FFmpeg 路径。
FFmpeg / FFprobe
- 项目:FFmpeg
- 官网:ffmpeg.org
- 许可证:LGPL-2.1-or-later;具体二进制取决于构建选项,也可能包含 GPL 组件
- 用途:生成视频封面、提取媒体信息和处理部分浏览器不能直接读取的格式。项目没有修改 FFmpeg 源码。
Docker Engine
- 项目:Docker Engine
- 官网:docker.com
- 许可证:Apache-2.0
- 用途:服务器端容器的构建与运行。
Nginx / OpenResty
- 项目:Nginx
- 官网:nginx.org
- 许可证:BSD-2-Clause
- 用途:反向代理、HTTPS 入口以及视频 Range 流的转发。
如果部署时使用 1Panel 提供的 OpenResty:
- 项目:OpenResty
- 官网:openresty.org
- 许可证:BSD 风格的 OpenResty 许可证,详见其 COPYRIGHT
- 用途:作为 1Panel 网站与 HTTPS 的运行环境。
1Panel
- 项目:1Panel
- 仓库:github.com/1Panel-dev/1Panel
- 许可证:GPL-3.0
- 用途:个人服务器管理、容器编排和反向代理配置。
Let’s Encrypt
- 服务:Let’s Encrypt
- 官网:letsencrypt.org
- 性质:免费的公共证书颁发机构,不属于项目内置代码。
- 用途:为远程访问地址签发 TLS 证书。
Apple 系统框架
macOS 客户端没有引入第三方 Swift Package。项目使用的主要能力来自 Apple 系统框架:
- SwiftUI
- AppKit
- AVKit / AVFoundation
- WebKit
- Combine
- CryptoKit
这些框架随 macOS 和 Xcode 提供,不是本项目的开源依赖,也不随项目源码分发。SwiftUI、macOS、iOS、iPhone 和 Apple 是其各自权利人的商标。
Web 平台能力
项目还使用了 IndexedDB、LocalStorage、Web Worker、Canvas、File System Access API、HTTP Range 和 Service Worker 等浏览器标准能力。
这些属于 Web 平台 API,不是第三方 JavaScript 框架。当前前端没有使用 React、Vue、Svelte、jQuery 或外部 UI 组件库,也没有依赖外部 CDN。
项目自身
Henry 媒体库由 Henry 独立开发,源码目前未公开,项目没有额外声明开源许可证。
本文中的代码结构、功能描述和项目版本信息仅用于开发记录,不构成对源码发布、二次分发或商业授权的承诺。文中不包含真实访问令牌、服务器地址或其他敏感凭据。
项目信息: Henry 媒体库 / Henry Media Library。项目作者与本文作者均为 Henry。当前版本 Web v160、macOS 2.22.0,源码目前未公开。
评论 0
留下想法或补充。评论经审核后会显示在这里。
正在读取评论…
成为第一个留下想法的人。