blob: 6ec4d0f0255eb46906464abb7df31474637dd7c8 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
{ ... }:
# 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";
};
systemd.services.pinchflat.serviceConfig.UMask = "0002";
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" = { };
}
|