1import json
2import os
3from pathlib import Path
4import sys
5from urllib.parse import urlsplit, urlunsplit
6import urllib.request
7import xml.etree.ElementTree as ET
8
9from studio import load
10
11
12data = json.load(sys.stdin)
13service = data["serviceId"]
14api_key = ET.parse(Path(data["hostRoot"]) / "config/config.xml").findtext("ApiKey")
15if not api_key:
16 raise ValueError(f"{service} has no API key in config.xml")
17
18nomad_headers = {"X-Nomad-Token": os.environ["NOMAD_TOKEN"]}
19
20
21def internal_endpoint(name):
22 jobs = json.load(urllib.request.urlopen(urllib.request.Request(
23 f"http://127.0.0.1:4646/v1/job/{name}/allocations", headers=nomad_headers,
24 ), timeout=10))
25 running = [job for job in jobs if job["ClientStatus"] == "running" and job["DesiredStatus"] == "run"]
26 if len(running) != 1:
27 raise ValueError(f"{name} needs one running allocation before configuring {service}")
28 allocation = json.load(urllib.request.urlopen(urllib.request.Request(
29 f"http://127.0.0.1:4646/v1/allocation/{running[0]['ID']}", headers=nomad_headers,
30 ), timeout=10))
31 ports = [port for port in allocation["AllocatedResources"]["Shared"]["Ports"]
32 if port["Label"] == "internal"]
33 if len(ports) != 1:
34 raise ValueError(f"{name} needs one reserved internal port before configuring {service}")
35 return ports[0]["HostIP"], ports[0]["Value"]
36
37
38nomad = urllib.request.Request(
39 f"http://127.0.0.1:4646/v1/service/{service}",
40 headers=nomad_headers,
41)
42allocations = json.load(urllib.request.urlopen(nomad, timeout=10))
43if len(allocations) != 1:
44 raise ValueError(f"{service} needs one running allocation before configuring download clients")
45allocation = allocations[0]
46base = f"http://{allocation['Address']}:{allocation['Port']}/api/v3/downloadclient"
47headers = {"X-Api-Key": api_key}
48clients = json.load(urllib.request.urlopen(urllib.request.Request(base, headers=headers), timeout=10))
49previous_hosts = {
50 field.get("value") for client in clients if client["implementation"] == "QBittorrent"
51 for field in client["fields"] if field["name"] == "host"
52}
53qbittorrent_host, qbittorrent_port = internal_endpoint("qbittorrent")
54if not any(client["implementation"] == "QBittorrent" for client in clients):
55 schemas = json.load(urllib.request.urlopen(urllib.request.Request(base + "/schema", headers=headers), timeout=10))
56 client = next(schema for schema in schemas if schema["implementation"] == "QBittorrent")
57 client["name"] = "qBittorrent"
58 client["enable"] = not data["preview"]
59 fields = {field["name"]: field for field in client["fields"]}
60 for name, value in {"host": qbittorrent_host, "port": qbittorrent_port, "useSsl": False}.items():
61 fields[name]["value"] = value
62 request = urllib.request.Request(
63 base,
64 data=json.dumps(client).encode(),
65 headers={**headers, "Content-Type": "application/json"},
66 method="POST",
67 )
68 urllib.request.urlopen(request, timeout=15).close()
69 clients = [client]
70
71for client in clients:
72 if client["implementation"] != "QBittorrent":
73 continue
74 fields = {field["name"]: field for field in client["fields"]}
75 if "host" not in fields or "port" not in fields or "useSsl" not in fields:
76 raise ValueError(f"{service} has an incomplete qBittorrent client")
77 expected = {"host": qbittorrent_host, "port": qbittorrent_port, "useSsl": False}
78 changed = any(fields[name]["value"] != value for name, value in expected.items())
79 for name, value in expected.items():
80 fields[name]["value"] = value
81 if data["preview"] and client["enable"]:
82 client["enable"] = False
83 changed = True
84 if not changed:
85 continue
86 request = urllib.request.Request(
87 f"{base}/{client['id']}",
88 data=json.dumps(client).encode(),
89 headers={**headers, "Content-Type": "application/json"},
90 method="PUT",
91 )
92 urllib.request.urlopen(request, timeout=15).close()
93
94local_volumes = load(service.split("-preview-", 1)[0], {})["containers"]["app"]["volumes"]
95remote_volumes = load("qbittorrent", {})["containers"]["app"]["volumes"]
96mapping_base = base.replace("/downloadclient", "/remotepathmapping")
97mappings = json.load(urllib.request.urlopen(urllib.request.Request(mapping_base, headers=headers), timeout=10))
98for remote_path, remote_volume in remote_volumes.items():
99 if not remote_volume.get("src"):
100 continue
101 source = Path(remote_volume["src"])
102 destinations = [
103 str(Path(local_path) / source.relative_to(volume["src"]))
104 for local_path, volume in local_volumes.items()
105 if volume.get("src") and source.is_relative_to(volume["src"])
106 ]
107 if not destinations:
108 continue
109 if len(destinations) != 1:
110 raise ValueError(f"{service} has ambiguous mounts for qBittorrent's {remote_path}")
111 local_path = destinations[0]
112 if remote_path == local_path:
113 continue
114 expected = {"host": qbittorrent_host, "remotePath": remote_path + "/", "localPath": local_path + "/"}
115 existing = next((mapping for mapping in mappings
116 if mapping["remotePath"].rstrip("/") == remote_path
117 and mapping["host"] in previous_hosts | {qbittorrent_host}), None)
118 if existing and all(existing[key] == value for key, value in expected.items()):
119 continue
120 body = {**existing, **expected} if existing else expected
121 request = urllib.request.Request(
122 f"{mapping_base}/{existing['id']}" if existing else mapping_base,
123 data=json.dumps(body).encode(),
124 headers={**headers, "Content-Type": "application/json"},
125 method="PUT" if existing else "POST",
126 )
127 urllib.request.urlopen(request, timeout=15).close()
128
129jackett_host, jackett_port = internal_endpoint("jackett")
130indexer_base = base.replace("/downloadclient", "/indexer")
131indexers = json.load(urllib.request.urlopen(urllib.request.Request(indexer_base, headers=headers), timeout=10))
132for indexer in indexers:
133 changed = False
134 if data["preview"]:
135 for setting in ("enableRss", "enableAutomaticSearch"):
136 if indexer[setting]:
137 indexer[setting] = False
138 changed = True
139 fields = {field["name"]: field for field in indexer["fields"]}
140 if "baseUrl" in fields and isinstance(fields["baseUrl"].get("value"), str):
141 current = urlsplit(fields["baseUrl"]["value"])
142 if current.path.startswith("/api/v2.0/indexers/"):
143 url = urlunsplit(("http", f"{jackett_host}:{jackett_port}", current.path, current.query, current.fragment))
144 changed |= fields["baseUrl"]["value"] != url
145 fields["baseUrl"]["value"] = url
146 if not changed:
147 continue
148 request = urllib.request.Request(
149 f"{indexer_base}/{indexer['id']}?forceSave=true",
150 data=json.dumps(indexer).encode(),
151 headers={**headers, "Content-Type": "application/json"},
152 method="PUT",
153 )
154 urllib.request.urlopen(request, timeout=15).close()