peertube-plugin-ghost-private-podcast
v1.0.2
Published
Private, authenticated podcast RSS feeds for PeerTube channels, gated by Ghost membership tiers
Maintainers
Readme
peertube-plugin-ghost-private-podcast
Private, authenticated podcast RSS feeds for PeerTube channels, gated by Ghost membership tiers. Each member gets a personal, token-authenticated feed URL that works in any podcast app. You pick which Public/Unlisted videos appear by ticking a per-video "Show in private podcast feed" box; unticked and private videos stay hidden. Members sync automatically from Ghost tiers.
Features
- Per-member private feeds. Every member gets a unique, HMAC-signed feed URL. Compatible with Apple Podcasts, Overcast, Pocket Casts, AntennaPod, and any other app.
- Ghost member sync. Point the plugin at your Ghost blog and choose which membership tiers get access. Members joining or leaving a tier are added or suspended automatically, in real time via webhooks plus periodic polling.
- Per-video opt-in. Each video has a "Show in private podcast feed" checkbox on its edit page. Videos are excluded by default; only the ones you tick (and that are Public or Unlisted) appear in members' feeds, so you can keep an Unlisted video off the feed until it's ready. Private videos never qualify. No expiring API token is needed: the plugin reads the channel's videos straight from PeerTube's database.
- Authenticated media proxy. Audio/video is streamed through a token-gated proxy that serves the smallest video rendition (light downloads) and supports range requests for seeking. The audio-only rendition is never served.
- Manual members too. Add or remove subscribers by email, independent of Ghost.
- Anti-sharing tools. Optional per-member first name in the feed title (a soft deterrent), one-click key rotation to kill a leaked URL, a member self-service "reset my feed" control for the podcast page, and sharing detection that flags a member when their feed is pulled from an unusual number of distinct IPs / apps.
- Access stats. Per-member daily counters for feed and media access.
- Management UI. A page under My Account > Private Podcasts to enable channels, map tiers, run a sync, copy or rotate member feed URLs, and see sharing flags.
Requirements
- PeerTube >= 7.0.0 (developed and verified on 8.1.x).
- A Ghost blog (optional, only for automatic member sync). Without Ghost you can still add members manually.
Installation
A. Managed / hosted PeerTube (admin UI only)
Managed hosts install plugins by npm name from the admin UI. Publish the package to npm (it contains no secrets, so a public publish is safe), then:
- Admin > Plugins/Themes > Search
- Search for
ghost-private-podcast - Install
If your host restricts installs to the official plugin index, send the provider this
package (or the tarball from npm pack) and ask them to install it server-side.
B. Self-hosted PeerTube (shell access)
# build + package
npm install
npm run build
npm pack # -> peertube-plugin-ghost-private-podcast-<ver>.tgz
# copy to the server, extract, and install by path
# the extracted directory MUST be named peertube-plugin-ghost-private-podcast
tar -xzf peertube-plugin-ghost-private-podcast-*.tgz # -> ./package
mv package peertube-plugin-ghost-private-podcast
# Docker:
docker cp peertube-plugin-ghost-private-podcast <peertube-container>:/tmp/
docker exec -u peertube <peertube-container> \
npm run plugin:install -- --plugin-path /tmp/peertube-plugin-ghost-private-podcast
# bare-metal (from the PeerTube install dir, as the peertube user):
# sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production \
# npm run plugin:install -- --plugin-path /path/to/peertube-plugin-ghost-private-podcastConfiguration
In Admin > Plugins > Ghost Private Podcast > Settings:
| Setting | Purpose |
|---|---|
| Ghost Blog URL | e.g. https://blog.example.com. Leave empty to disable Ghost sync. |
| Ghost Admin API Key | id:secret from a Ghost Admin integration. |
| Ghost Sync Interval (minutes) | Poll cadence. Default 15, minimum 5. |
| Feed title | Podcast title shown in apps. Empty falls back to Channel (Members Only). |
| Feed cover image URL | Square (>=1400px) cover art, publicly reachable. |
| Personalize feed title with member first name | Titles each member's feed First's <title> using their Ghost name. Off by default. |
| Sharing alert: distinct IPs (30 days) | Flag a member as a possible sharer at/above this many distinct IPs in 30 days (default 8; 0 disables). IPs are stored only as salted hashes. |
Note: PeerTube saves plugin settings by replacing the whole object. When you change one field, keep the others filled in the same save or they get cleared.
Ghost setup
The Ghost integration uses a single instance-wide Admin API key, so configuring tiers and running syncs requires a PeerTube administrator account (see Security model). Managing manual members of your own channel does not.
- In Ghost Admin > Settings > Integrations, create a Custom Integration; copy its Admin API Key into the plugin setting above.
- Open My Account > Private Podcasts in PeerTube, pick your channel, and Enable Private Podcast.
- As an admin, choose the tiers that should get feed access, and Save.
- Add a Ghost webhook for
member.added,member.updated, andmember.deleted, pointing at the webhook URL shown on that page (.../plugins/ghost-private-podcast/router/ghost/webhook/<secret>). - Run Sync Now for the initial import.
Ghost theme integration (optional)
To show members their personal feed URL after they sign in, call the plugin's lookup endpoint from your Ghost theme with the member's UUID:
fetch(PEERTUBE_URL + "/plugins/ghost-private-podcast/router/member-feed/"
+ CHANNEL_ID + "/" + encodeURIComponent(memberUuid))
.then(r => r.json())
.then(d => { /* d.feedUrl is this member's private feed */ });The endpoint returns 404 for non-members, and reflects your Ghost origin in CORS. No login or token is ever exposed to the browser.
How it works
- Feed URL:
/plugins/ghost-private-podcast/router/feed/<channelId>/<token>, where<token>isHMAC(secret, member + channel). The secret is generated once per install and stored server-side. - Listing: videos are read from PeerTube's database (published,
privacy IN (public, unlisted)), then filtered to those opted into the feed via the per-video checkbox (stored in the video'spluginData, read by a server hook), so episodes appear without any OAuth token. The result is cached per channel (short TTL, single-flight) so a burst of member polls collapses to one DB query. Order and pubDate use the original publication date when set. - Media:
/media/<channelId>/<videoUuid>/<token>proxies the smallest video web-video. Public/unlisted files need no token, so nothing expires.
Building from source
The server library must be CommonJS (PeerTube requires it); the client
script must be an ES module (the browser loads it via import()). The build uses two
tsconfigs:
npm run build # tsc (server, CommonJS) && tsc -p tsconfig.client.json (client, ESM)
npm test # tsc -p tsconfig.test.json && node --test (no extra dependencies)Tests use Node's built-in test runner against the compiled sources; there are no third-party test dependencies.
Security model
- The Ghost integration is admin-gated. The Ghost URL + Admin API key are one instance-wide credential, so the routes that use it (tier listing, tier configuration, and sync) require a PeerTube administrator. The plugin assumes one Ghost blog per PeerTube instance.
- Per-member feeds are gated by a 128-bit HMAC token (constant-time compared); the media proxy only serves a channel's own Public/Unlisted videos, never Private ones, and fetches only PeerTube-internal file URLs (no SSRF).
- Recommended at deployment: put a reverse-proxy / CDN rate limit in front of
the public routes (
feed,media,member-feed,member-rotate,webhook), and verify Ghost'sx-ghost-signatureon the webhook. Note that feed tokens and the webhook secret appear in URL paths, so they will show up in access logs. - IP-based sharing detection stores only salted HMAC hashes, never raw IPs.
License
MIT
