{ ... }: # 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" = { }; }