peertube-plugin-video-lifecycle
v0.4.0
Published
PeerTube video lifecycle: minors presence declaration, validity period, low-visibility handling and automated deletion.
Maintainers
Readme
peertube-plugin-video-lifecycle
A PeerTube plugin that gives videos a lifecycle, built for apps.education.fr, fit for any instance.
Its purpose is twofold, and both halves are about what earns its place on disk:
- A legal basis. A video showing identifiable minors is published under an authorisation their guardians gave, and that authorisation expires. Past it, keeping the video is the breach and deleting it is the duty.
- A cost. Storage is not free and it only grows. A video nobody has opened in a year is paid for every month, by whoever runs the instance.
The plugin was built for apps.education.fr, where the eleven PeerTube platforms of the French Éducation nationale hold 290 000 videos and some 67 TiB — one service among several whose shared storage runs to petabytes, for the agents of primary and secondary education. Nothing of that case is wired in: the school-year rule is a setting, the thresholds are settings, and the figures appear only where they make a trap concrete. If you run an instance where storage costs money and some videos are kept under a permission that ends, the shape fits.
Before turning a cycle on anywhere with tens of thousands of videos, read at scale — a first pass has a measured cost, and the shipped defaults were not chosen against numbers like these.
The two cycles
A lifecycle consists of a declaration, a materialised deadline, a public display, a warning, a grace period, and deletion at expiry.
- Minors present — asked from the start, deleting only once you switch that on.
The publication form asks whether a video shows identifiable minors.
An answer of yes materialises a deletion date on the spot; once deletion is armed, that
date is announced on the watch page and the video is deleted when the day comes.
The answer is one-way and the date cannot be moved.
Its owner is mailed when the grace period opens, unless
notificationshands that to an orchestrator — in which case the date on that page, and a row on their countdown list, are all they get until the orchestrator writes to them. See the minors cycle. - Low visibility — off until you enable it. A view threshold and an observation window decide which videos count as unwatched. Their owner is warned, in the application and by mail if you turn it on, then a grace period runs and the video is deleted. A view or a press of Renew lifts the deadline. See the low-visibility cycle.
Whichever cycle started the countdown, an owner sees it in one place: My account → Videos scheduled for deletion lists the videos of theirs in a grace period, soonest first, with a Renew button on each one that can still be saved. A video declared as showing minors is listed there without that button — its date cannot be moved, so downloading a copy before the day is its owner's only recourse.
Deletion applies to the instance only — backups and archives are out of scope. Deciding whether content is lawful remains the uploader's responsibility.
Settings
| Setting | Default | Description |
| --- | --- | --- |
| minors-schedule | rolling-months | Which rule computes a minors deadline. rolling-months counts minors-validity-months from each video; school-year gives every video the end of the school year it was published in. See Two rules for one deadline. |
| minors-grace-months | 3 | Added after the validity period; the watch page shows the sum. Applies to videos declared from then on. |
| minors-validity-months | 12 | How long a video declared as showing minors stays online, under rolling-months. Applies to videos declared from then on. |
| school-year-ends-on | 08-31 | The day every video declared as showing minors expires under school-year, as MM-DD. An impossible day (02-30, 13-01) falls back to the default. |
| minors-declaration-enabled | true | Asking, and only that: the publication form puts the question, and a yes is recorded with a deletion date computed and frozen on the spot. Deletes nothing and shows no date on its own. While unchecked, the form does not ask; what is already recorded is kept. |
| minors-deletion-enabled | false | Acting: a declared video is unpublished when its grace opens, its date is shown on the watch page, its owner is mailed, and it is deleted on the day. Off on a fresh install, like the other cycle. Checking it later applies the dates already recorded, reopening from that pass any that went by meanwhile. |
| low-visibility-enabled | false | The low-visibility cycle, off on a fresh install too. While unchecked, it is paused: nothing is taken in, nothing is deleted, and watch pages announce nothing. |
| low-visibility-views-threshold | 1 | View count below which a video counts as unwatched. The test is strict, so a video sitting on the threshold is out, and 0 qualifies nothing. |
| low-visibility-window-months | 12 | Observation window for the view count. |
| max-per-pass | 100 | What one pass may do to owners, for both cycles: how many videos it takes into the low-visibility cycle, and how many owners it warns. So switching a cycle on does not flag the whole back catalogue, and a first pass over videos already declared as showing minors does not write to all of them the same day. The scan reports moreToFlag and moreToWarn when it stopped short. 0 lifts the cap. |
| low-visibility-grace-months | 3 | Grace period before deletion. Applies to videos flagged from then on. |
| notifications | this-instance | Who warns an owner before a deletion, for both cycles: this-instance mails them, external-orchestrator leaves that to something upstream — see Warning the owner. |
| low-visibility-mail-subject | (empty) | Subject of the low-visibility warning mail; {date} is substituted. Empty falls back to a built-in text, resolved in the owner's own language — so filling this field replaces that with one wording for everyone. |
| low-visibility-mail-text | (empty) | Body of that mail; {date} is substituted. A button to the owner's countdown page is added under it. Translated by default, as the subject is. |
| low-visibility-mail-link-label | (empty) | Label of that button. Translated by default; set it with the two above. |
| minors-mail-subject | (empty) | Subject of the minors warning mail. Translated by default, as the two above. It must not say what the video contains. |
| minors-mail-text | (empty) | Body of that mail; {date} is substituted. Do not offer a way to postpone the date — there is none. |
| minors-mail-link-label | (empty) | Label of the button under it. It leads to the video's watch page, where the date has been shown since the declaration, not to the countdown page. |
| scan-interval-hours | 24 | How often the periodic scan runs. 0 disables the internal timer and leaves an external cron to drive the passes. |
| automation-secret | (empty) | Shared secret for the machine endpoints. While it is empty those endpoints stay closed — see plugin API. |
| minors-notice-text | (empty) | Overrides the minors notice; {date} is substituted. Empty falls back to the built-in translated text. |
| low-visibility-notice-text | (empty) | Overrides the low-visibility notice; {date} is substituted here too. |
| renew-button-label | (empty) | Overrides the Renew button label. That button is offered during a low-visibility grace period, on the watch page and in the owner's countdown list. |
A deadline is materialised once — at declaration, or when a video is flagged — and then
frozen.
Changing a setting above reaches videos taken in from then on; no announced date moves.
For the minors cycle nothing moves it ever: that deadline is immutable (DNE, 2026-09-23),
and a declaration made in error is cleared rather than deferred.
The one exception is a deadline materialised before minors-grace-months existed, which
gains the grace on the next pass — the only time an announced date moves, and it moves
later.
A value the form cannot use — a count that is not digits, or a count of months past the ceiling of a century — falls back to the default above rather than failing the call. A typo is therefore ignored, not applied.
The two cycles can hold the same video, and then the earlier deadline commands — the public date only ever moves earlier, never later (DNE, 2026-09-03). See when a video is under both cycles.
Warning the owner
A deletion date is on the video's own watch page from the moment it is set, and it holds: the video goes on that day even if no warning ever reached anybody.
Four surfaces carry a warning, and only some can prove it arrived. That proof is what stops the plugin warning again on every pass:
| Surface | Cycle | Proves delivery |
| --- | --- | --- |
| The notice on the video's own watch page | both | No — only whoever opens the video sees it |
| My account → Videos scheduled for deletion, plus a one-line notice above whatever page the owner is on | both are listed | Low visibility only |
| Mail sent by the plugin | both, under notifications | Yes |
| Mail sent by your own orchestrator, which then calls POST /videos/:id/lifecycle/notified | either | Yes |
One setting picks the mailer, for both cycles: an owner should hear one voice about their own videos, and two warnings for one video is the confusing direction.
Six things worth knowing before you change it:
- This setting picks the channel, it does not switch the engine off. A deadline holds whichever value it has: to stop a cycle, uncheck the cycle.
- A built-in wording reaches each owner in their own language, falling back to the
instance's default. Filling one of the mail fields replaces that with the one wording
you typed, for everyone.
GET /mail-textsserves whichever applies, so an orchestrator doing the mailing can send the instance's own words. - Mail needs PeerTube 8.3 and SMTP configured on the instance. Failing either, nothing is sent and nothing is recorded, and the warning is tried again next pass.
- Under
external-orchestratorthe minors cycle warns nobody by itself. Its videos are on the countdown page, but that listing is not accepted as a warning: warn them yourself and callPOST /videos/:id/lifecycle/notified. - A minors deadline cannot be postponed. The mail is a courtesy before a date the owner has been shown for a year; downloading a copy is the recourse, not Renew.
- No mail says what a video contains. Subject lines get forwarded.
How each surface is built, what counts as seen, and which worklist an orchestrator should poll: warning channels.
Monitoring missed deadlines
A low-visibility video the engine forgot is merely kept longer than intended. A video declared as showing minors that outlives the date on its own watch page is not acceptable, so that case is made visible from outside:
curl -sH "X-Automation-Secret: $SECRET" \
"https://<instance>/plugins/video-lifecycle/router/overdue"
# { "count": 0, "unreadable": 0, "videos": [] }| Alert on | Read from | Watch for |
| --- | --- | --- |
| count > 0 or unreadable > 0 | GET /overdue | Both answer null while minorsDeletionEnabled is false: nothing deletes, so nothing is overdue. |
| A status other than 200 | GET /overdue | A fresh install has no automation-secret, so the route answers 403 with no count; a probe reading undefined stays quiet for ever. |
| The probe itself going missing | your own monitor | The ALERT line is written by a pass, so no pass means no line. The likely failure with scan-interval-hours = 0. |
| overdueMinors !== 0 | the POST /scan summary | null means the check itself failed, not "nothing overdue". |
| skipped === "stopped" | the POST /scan reply | Nothing scans again on this registration, and it carries no since, so a duration rule never fires. |
| now - since past what an honest pass takes | the POST /scan reply, when skipped is already-running | That body carries none of the summary, so read skipped first. |
What each figure can and cannot say, and what happens when the index itself cannot be read: monitoring. Where a metrics scraper and a log shipper plug in, and who owns the retention: collecting.
Plugin API
Every route, the automation secret, and running your own orchestrator.
Before you switch a cycle on
Each section of the plugin's own settings page states, under its heading, what that cycle currently reaches: how many videos are declared or already counting down, how many await a warning, how many are past their date, and what has been deleted and reclaimed so far. The low-visibility section adds how many videos the platform holds and how many the cycle would take in right now.
That intake figure is the one no API can answer from outside — a view count is not
filterable and the declaration is not indexed — so it is computed inside, on
GET /forecast, which is the route those lines render.
At scale
What a pass costs as the tracked index grows, how to count your videos without walking them, what a first run does to a back catalogue, and how to size the two settings that govern both: at scale.
Development
The layers, the build, a local instance, mail, and the smoke test.
Licence
EUPL-1.2 — the European Union Public Licence, whose text ships with the published package.
