video.monerica.com: Monerica YouTube videos, no-JS, hosted on Bunny Stream
  • C# 74.9%
  • HTML 12.6%
  • Python 4.9%
  • CSS 4.2%
  • Shell 3.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-22 08:37:13 -06:00
CI Monerica Video: video.monerica.com 2026-10-22 08:37:13 -06:00
src Monerica Video: video.monerica.com 2026-10-22 08:37:13 -06:00
tools Monerica Video: video.monerica.com 2026-10-22 08:37:13 -06:00
.gitignore Monerica Video: video.monerica.com 2026-10-22 08:37:13 -06:00
LICENSE Monerica Video: video.monerica.com 2026-10-22 08:37:13 -06:00
README.md Monerica Video: video.monerica.com 2026-10-22 08:37:13 -06:00

Monerica Video

Source for https://video.monerica.com: the Monerica YouTube channel's videos, hosted on Bunny Stream and themed like monerica.com.

  • No JavaScript. The watch page uses a plain <video> tag: Safari/iOS play Bunny's HLS stream, and every other browser plays Bunny's MP4 fallback. Chapter and timestamp links reload the page at ?t=<seconds>.
  • PGP comments, no accounts. The flow is captcha → paste public key → decrypt a one-time code → write the comment → admin approval. It's the same scheme as Monerica reviews, and the PGP code is copied from Monerica. The key's primary fingerprint is the commenter's identity. Admin replies show a Moderator badge.
  • Stats without IPs. Page views are counted per video per day. A view counts once per visitor, per video, per UTC day, and at most 30 per visitor per hour. A "visitor" is an HMAC of IP + user agent keyed with a random secret that lives only in memory and changes daily, so nothing about visitors is stored or linkable across days. Bots, link previews, prefetches and HEAD requests aren't counted. Plays and watch time are synced from Bunny every 15 minutes.
  • Search terms, no IPs. Searches are stored as anonymous terms + counts (admin /admin/searches). A query-string guard rejects SQL-injection/XSS probes with a 400.
  • Sponsors. Monerica's main sponsors (public feed SponsorFeedUrl, cached 15 minutes) show in the sidebar, a footer strip and one rotating card on watch pages, all rel="sponsored". If the feed is down, the last good list is shown.
  • SEO. VideoObject JSON-LD (with chapters as Clips), a sitemap with Google video extensions, canonical URLs, og tags and transcripts on watch pages.
  • Original audio only. YouTube auto-dubs videos; the importer always takes the original-language audio track.

Layout

src/MonericaVideo.Data    EF Core (Postgres) models + migrations
src/MonericaVideo.Web     ASP.NET Core MVC app (net10.0)
src/Tests/...             xUnit tests (deploy refuses to release if any fail)
CI/setup-server.sh        one-time server setup (systemd, nginx, cert, Updating page)
CI/deploy.sh              routine deploy: test → publish → rsync → restart
CI/setup-import-jobs.sh   one-time: systemd timers for the automatic imports
tools/import_channel.py   YouTube → Bunny Stream importer (one video at a time)
tools/fetch_transcripts.py  YouTube captions → transcripts on watch pages

Secrets (never in this repo)

Where What
/etc/monericavideo/db.env (server) ConnectionStrings__Default
/etc/monericavideo/bunny.env (server) Bunny__LibraryId, Bunny__ApiKey, Bunny__CdnHost
/etc/monericavideo/admin.env (server) Admin__Username, Admin__PasswordHash (PBKDF2)
/etc/monericavideo/import.env (server) Import__ApiKey
/etc/monericavideo/importer.env (server) importer's site + Bunny keys (built by CI/setup-import-jobs.sh)
~/.config/monerica-video/importer.env (local) the same, for running the importer by hand
CI/deploy.env (local, git-ignored) DEPLOY_HOST=<ssh alias>

To change the admin password, run echo 'new password' | dotnet MonericaVideo.Web.dll hash-password. Put the output in Admin__PasswordHash and restart monericavideo.

Deploy

./CI/setup-server.sh   # once
./CI/deploy.sh         # every change (runs all tests first)

The app listens on 127.0.0.1:5250 (service monericavideo, files in /opt/monericavideo). EF migrations run on startup.

Importing videos

python3 tools/import_channel.py "https://www.youtube.com/channel/<id>/videos"      # whole channel (skips ones already imported)
python3 tools/import_channel.py "<url>" --only <youtubeId>                         # one video
python3 tools/import_channel.py --queue                                            # links queued on /admin/import
python3 tools/import_channel.py "<url>" --redub                                    # re-import videos that got a dubbed audio track

On the server, CI/setup-import-jobs.sh installs two low-priority systemd timers: one processes the /admin/import queue every 5 minutes, the other checks the channel for new uploads every 6 hours.

For each video it downloads the best quality (yt-dlp), uploads it to Bunny, sets the YouTube thumbnail, registers the title, description and date, then deletes the local file. Only one video is on disk at a time. New videos appear once Bunny finishes encoding (synced every 15 minutes, or via "Sync from Bunny now" in the admin).

Admin

/admin: stats, video editing (title, slug, description, category, related article, published), import queue (paste YouTube links), search terms, comment queue (approve, reject, Moderator reply), categories.