From d167bc15da3bc3da355d69eadb9420abb3a637e1 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Thu, 13 Aug 2026 02:45:16 -0400 Subject: Add pinchflat Coexists with ytdl-sub rather than replacing it, downloading into its own tree under /srv/media/incoming so the two never manage the same files. Podcast clients cannot authenticate, so the feed, cover art, episode art and media stream routes bypass oauth2-proxy with auth_request off. Those are exactly the routes pinchflat serves unauthenticated itself, each addressed by an unguessable UUID. The OPML endpoint is exposed as well but stays token-protected by the app. Co-Authored-By: Claude Opus 5 --- hostnix/elmo/configuration.nix | 1 + hostnix/elmo/pinchflat.nix | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 hostnix/elmo/pinchflat.nix (limited to 'hostnix/elmo') 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" = { }; +} -- cgit v1.3.1