summaryrefslogtreecommitdiff
path: root/hostnix
diff options
context:
space:
mode:
Diffstat (limited to 'hostnix')
-rw-r--r--hostnix/elmo/configuration.nix1
-rw-r--r--hostnix/elmo/pinchflat.nix85
2 files changed, 86 insertions, 0 deletions
diff --git a/hostnix/elmo/configuration.nix b/hostnix/elmo/configuration.nix
index dc1eb21..3835481 100644
--- a/hostnix/elmo/configuration.nix
+++ b/hostnix/elmo/configuration.nix
@@ -16,6 +16,7 @@
./home-assistant.nix
./media.nix
./oidc.nix
+ ./pinchflat.nix
./rss.nix
./syncthing.nix
./system.nix
diff --git a/hostnix/elmo/pinchflat.nix b/hostnix/elmo/pinchflat.nix
new file mode 100644
index 0000000..4374607
--- /dev/null
+++ b/hostnix/elmo/pinchflat.nix
@@ -0,0 +1,85 @@
+{ ... }:
+
+# Self-hosted YouTube downloader: https://github.com/kieraneglin/pinchflat
+#
+# Coexists with ytdl-sub (media.nix) rather than replacing it. Each gets its
+# own tree under /srv/media/incoming so the two never manage the same files.
+
+# Manual configuration:
+# - Jellyfin: add Media Library /srv/media/incoming/Pinchflat (Shows)
+# - Copy feed URLs from https://pf.elmo.mou.fo, never from localhost: the XML
+# is generated with whatever host and X-Forwarded-Proto the request carried.
+
+let
+ mediaDir = "/srv/media/incoming/Pinchflat";
+ upstream = "http://127.0.0.1:8945";
+
+ # Podcast clients can't log in, so the feed endpoints have to sit outside
+ # oauth2-proxy. These are exactly the routes pinchflat itself serves
+ # unauthenticated (the maybe_basic_auth scope in router.ex); each is
+ # addressed by an unguessable UUID rather than a sequential id, which is the
+ # only thing keeping them private.
+ #
+ # The trailing extension is optional because the feed builder emits
+ # feed.xml, stream.mp4, episode_image.jpg and so on, while endpoint.ex
+ # strips the extension again before routing.
+ feedRoutes = "~* ^/(sources/[^/]+/feed(_image)?|media/[^/]+/(stream|episode_image))(\\.[a-zA-Z0-9]+)?$";
+
+ # Lists every source's feed for bulk import. Unlike the routes above this one
+ # is not public: pinchflat 401s unless ?route_token= matches.
+ opmlRoute = "~* ^/sources/opml(\\.[a-zA-Z0-9]+)?$";
+in
+{
+ # Setgid so downloads land in the media group, as Jellyfin expects.
+ systemd.tmpfiles.rules = [
+ "d ${mediaDir} 2775 pinchflat media -"
+ ];
+
+ services.pinchflat = {
+ enable = true;
+ inherit mediaDir;
+ # Uses a weak built-in SECRET_KEY_BASE instead of a hand-managed
+ # /var/secrets file. Acceptable here: the port is not opened in the
+ # firewall and the vhost is behind oauth2-proxy.
+ selfhosted = true;
+ # A no-op while BASIC_AUTH_* is unset, since authentication is nginx's job
+ # (see feedRoutes). Set so the feeds keep working if basic auth is ever
+ # turned on.
+ extraConfig.EXPOSE_FEED_ENDPOINTS = "yes";
+ };
+
+ users.users.pinchflat = {
+ extraGroups = [ "media" ];
+ };
+
+ # TODO allocate domain?
+ services.nginx.virtualHosts."pf.elmo.mou.fo" = {
+ useACMEHost = "elmo.mou.fo";
+ forceSSL = true;
+ locations = {
+ "/" = {
+ # Phoenix listens on all interfaces; only nginx should reach it.
+ proxyPass = upstream;
+ # LiveView drives the whole UI over a websocket.
+ proxyWebsockets = true;
+ };
+ ${feedRoutes} = {
+ proxyPass = upstream;
+ extraConfig = ''
+ auth_request off;
+ # Whole episodes stream through here, and the app serves its own
+ # Range requests; don't spool them into nginx temp files first.
+ proxy_buffering off;
+ '';
+ };
+ ${opmlRoute} = {
+ proxyPass = upstream;
+ extraConfig = ''
+ auth_request off;
+ '';
+ };
+ };
+ };
+
+ services.oauth2-proxy.nginx.virtualHosts."pf.elmo.mou.fo" = { };
+}