graph TD Tapes["17 digitised family tapes<br/>41 GB, one file per tape"] --> Look["A frame every 30 seconds<br/>+ camcorder date stamps"] Look --> TV["Two tapes: only TV<br/>deleted"] Look --> Mixed["Most tapes: family events<br/>then hours of recorded TV"] Mixed --> Cut["Lossless cuts at scene changes<br/>27 files, one per event, each dated"] Cut --> Upload["Upload to the photo server"] Upload --> Oops["'Put my uploads in an album'<br/>→ album with 63,050 items"] Oops --> Fix["Delete the album, count the library,<br/>rebuild from the 27 known IDs"]
A family member had our old VHS tapes digitised. The result was 17 big video files, named 1.mp4 to 17.mp4, about 41 GB in total. The goal: get the family videos into the self-hosted photo library, each on the right day in the timeline.
Looking at hours of tape without watching it
A first pass took 19 frames from each tape. It looked like enough, and it wasn’t: it labelled one tape “TV only”, when a closer look showed the first nine and a half minutes were a family wedding.
The second pass took one frame every 30 seconds and tiled each tape into a contact sheet. TV gives itself away: a channel logo in the corner, subtitles, ad breaks, end credits. Home video gives itself away too, because most camcorders burned the date into the picture.
Except when nobody set the clock. Several tapes say “1.1.89” on every frame, whatever year they were really filmed. Those dates came from the owner, one tape at a time: “that one is from 1999”.
The pattern on most tapes was the same: a family event or three, and then whatever was on TV the night someone forgot to stop recording. Two tapes held nothing but TV and were deleted.
Cutting without losing anything
Re-encoding old video always loses a little quality, and VHS has none to spare. So nothing was re-encoded. ffmpeg -c copy cuts the existing stream as it is; the catch is that a cut can only start on a keyframe, which on these files means every 6 seconds. For family footage that’s fine.
To find the cut points, each tape was reduced to one tiny 16×12 grayscale frame per second. Runs of near-uniform frames are blank tape; a big jump between neighbouring frames is a scene change. Each proposed boundary was then checked against a sheet of real frames before cutting.
The same fingerprints answered another question. Two tapes looked like the same wedding. Was one a copy of the other? Matching their frames second by second said no: almost nothing lined up, apart from one moment filmed from a different angle. Two cameras at one wedding, so both were kept.
The result was 27 files, one per event, each with its date written into the file’s metadata so the photo server files it on the right day.
The album that took everything
The upload went through the photo server’s API, and the plan was to then find “everything uploaded from this import” and put it in one album.
The search call accepted a device filter and quietly ignored it. It returned the whole library, and the next line created an album from the result. The new album held 63,050 items.
Nothing was lost: an album only points at photos, so deleting it touches nothing else. The album was deleted, the library count checked (64,310, the same as before plus the 27 new videos), and the album rebuilt from the 27 asset IDs the upload had already returned. Those IDs had been the right source all along.
Two smaller traps on the way:
- Seven uploads came back empty. All seven had a comma in the file name, and curl’s
-F "file=@name"syntax treats a comma as a separator, so nothing was sent. Quoting the name fixed it. - “Uploaded” isn’t proof. Each stored file’s SHA-1 was compared with the original, and with the checksum the server recorded itself. All 27 matched.
What I’d do differently
- Never write from a search result you haven’t counted. A filter that’s silently ignored looks just like one that works, until you look at the number.
- Keep the IDs a write gives you. The upload responses already held exactly the 27 IDs needed; the search was an extra step that added risk.
- Sample densely before deleting. 19 frames per tape nearly threw away a wedding; one frame every 30 seconds caught it.