feat: client-side video thumbnail generation (no ffmpeg)

Backend:
- get_thumbnail: for non-images, serve cached thumb or 204 (not 400)
- store_external_thumbnail: validate + re-encode to WebP + persist
- get_cached_thumbnail: memory → disk lookup without generation
- PUT /api/files/{id}/thumbnail/{size} endpoint for client uploads
- ThumbnailPort trait: add get_cached_thumbnail + store_external_thumbnail

Frontend (photos.js):
- On <img> error for video tiles, use <video> + <canvas> to extract
  a frame at 25% duration using browser's native codec
- Show frame immediately via blob URL
- Fire-and-forget PUT to server for permanent caching
- Subsequent visits serve cached WebP instantly (no re-extraction)

Zero server-side dependencies — CPU distributed across clients.
This commit is contained in:
Diocrafts
2026-03-07 18:55:44 +01:00
parent 54ce2d085a
commit db93b48149
5 changed files with 306 additions and 8 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
.route("/", get(FileHandler::list_files_query))
.route("/upload", post(FileHandler::upload_file_with_thumbnails))
.route("/{id}", get(FileHandler::download_file))
.route("/{id}/thumbnail/{size}", get(FileHandler::get_thumbnail))
.route("/{id}/thumbnail/{size}", get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail))
.route("/{id}/metadata", get(FileHandler::get_file_metadata))
.layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) // 10 GB for file uploads
.with_state(app_state.clone());