diff options
-rw-r--r-- | docs/customservices.md | 22 | ||||
-rw-r--r-- | dummy-data/rtorrent/download_list | 10 | ||||
-rw-r--r-- | dummy-data/rtorrent/global_down | 6 | ||||
-rw-r--r-- | dummy-data/rtorrent/global_up | 6 | ||||
-rw-r--r-- | src/components/services/Rtorrent.vue | 153 |
5 files changed, 197 insertions, 0 deletions
diff --git a/docs/customservices.md b/docs/customservices.md index 3f17abc..fb3f2a7 100644 --- a/docs/customservices.md +++ b/docs/customservices.md | |||
@@ -25,6 +25,7 @@ within Homer: | |||
25 | - [Mealie](#mealie) | 25 | - [Mealie](#mealie) |
26 | - [Healthchecks](#healthchecks) | 26 | - [Healthchecks](#healthchecks) |
27 | - [Proxmox](#proxmox) | 27 | - [Proxmox](#proxmox) |
28 | - [rTorrent](#rtorrent) | ||
28 | - [qBittorrent](#qbittorrent) | 29 | - [qBittorrent](#qbittorrent) |
29 | 30 | ||
30 | If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page. | 31 | If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page. |
@@ -249,6 +250,27 @@ Two lines are needed in the config.yml : | |||
249 | The url must be the root url of the Healthchecks application. | 250 | The url must be the root url of the Healthchecks application. |
250 | The Healthchecks API key can be found in Settings > API Access > API key (read-only). The key is needed to access Healthchecks API. | 251 | The Healthchecks API key can be found in Settings > API Access > API key (read-only). The key is needed to access Healthchecks API. |
251 | 252 | ||
253 | ## rTorrent | ||
254 | |||
255 | This service displays the global upload and download rates, as well as the number of torrents | ||
256 | listed in rTorrent. The service communicates with the rTorrent XML-RPC interface which needs | ||
257 | to be accessible from the browser. Please consult | ||
258 | [the instructions](https://github.com/rakshasa/rtorrent-doc/blob/master/RPC-Setup-XMLRPC.md) | ||
259 | for setting up rTorrent and make sure the correct CORS-settings are applied. Examples for various | ||
260 | servers can be found at https://enable-cors.org/server.html. | ||
261 | |||
262 | ```yaml | ||
263 | - name: "rTorrent" | ||
264 | logo: "assets/tools/sample.png" | ||
265 | url: "http://192.168.0.151" # Your rTorrent web UI, f.e. ruTorrent or Flood. | ||
266 | xmlrpc: "http://192.168.0.151:8081" # Reverse proxy for rTorrent's XML-RPC. | ||
267 | type: "Rtorrent" | ||
268 | rateInterval: 5000 # Interval for updating the download and upload rates. | ||
269 | torrentInterval: 60000 # Interval for updating the torrent count. | ||
270 | username: "username" # Username for logging into rTorrent (if applicable). | ||
271 | password: "password" # Password for logging into rTorrent (if applicable). | ||
272 | |||
273 | |||
252 | ## Proxmox | 274 | ## Proxmox |
253 | 275 | ||
254 | This service displays status information of a Proxmox node (VMs running and disk, memory and cpu used). It uses the proxmox API and [API Tokens](https://pve.proxmox.com/pve-docs/pveum-plain.html) for authorization so you need to generate one to set in the yaml config. You can set it up in Proxmox under Permissions > API Tokens. You also need to know the realm the user of the API Token is assigned to (by default pam). | 276 | This service displays status information of a Proxmox node (VMs running and disk, memory and cpu used). It uses the proxmox API and [API Tokens](https://pve.proxmox.com/pve-docs/pveum-plain.html) for authorization so you need to generate one to set in the yaml config. You can set it up in Proxmox under Permissions > API Tokens. You also need to know the realm the user of the API Token is assigned to (by default pam). |
diff --git a/dummy-data/rtorrent/download_list b/dummy-data/rtorrent/download_list new file mode 100644 index 0000000..1d08e51 --- /dev/null +++ b/dummy-data/rtorrent/download_list | |||
@@ -0,0 +1,10 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <methodResponse> | ||
3 | <params> | ||
4 | <param><value><array><data> | ||
5 | <value><string>2BAC78C9E10D82415142E57D24601F2FD8927816</string></value> | ||
6 | <value><string>8BB10DB9EA239106D4907601C342ABBA29BE4391</string></value> | ||
7 | <value><string>2790CE71493BE7083929D5A1CE9CFD6B8394F224</string></value> | ||
8 | </data></array></value></param> | ||
9 | </params> | ||
10 | </methodResponse> \ No newline at end of file | ||
diff --git a/dummy-data/rtorrent/global_down b/dummy-data/rtorrent/global_down new file mode 100644 index 0000000..d48e884 --- /dev/null +++ b/dummy-data/rtorrent/global_down | |||
@@ -0,0 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <methodResponse> | ||
3 | <params> | ||
4 | <param><value><i8>149279</i8></value></param> | ||
5 | </params> | ||
6 | </methodResponse> \ No newline at end of file | ||
diff --git a/dummy-data/rtorrent/global_up b/dummy-data/rtorrent/global_up new file mode 100644 index 0000000..93a009c --- /dev/null +++ b/dummy-data/rtorrent/global_up | |||
@@ -0,0 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <methodResponse> | ||
3 | <params> | ||
4 | <param><value><i8>45616</i8></value></param> | ||
5 | </params> | ||
6 | </methodResponse> \ No newline at end of file | ||
diff --git a/src/components/services/Rtorrent.vue b/src/components/services/Rtorrent.vue new file mode 100644 index 0000000..75efb7b --- /dev/null +++ b/src/components/services/Rtorrent.vue | |||
@@ -0,0 +1,153 @@ | |||
1 | <template> | ||
2 | <Generic :item="item"> | ||
3 | <template #content> | ||
4 | <p class="title is-4">{{ item.name }}</p> | ||
5 | <p class="subtitle is-6"> | ||
6 | <span v-if="error" class="error">An error has occurred.</span> | ||
7 | <template v-else> | ||
8 | <span class="down"> | ||
9 | <i class="fas fa-download"></i> {{ downRate }} | ||
10 | </span> | ||
11 | <span class="up"> | ||
12 | <i class="fas fa-upload"></i> {{ upRate }} | ||
13 | </span> | ||
14 | </template> | ||
15 | </p> | ||
16 | </template> | ||
17 | <template #indicator> | ||
18 | <span v-if="!error" class="count">{{ count }} | ||
19 | <template v-if="count === 1">torrent</template> | ||
20 | <template v-else>torrents</template> | ||
21 | </span> | ||
22 | </template> | ||
23 | </Generic> | ||
24 | </template> | ||
25 | |||
26 | <script> | ||
27 | import Generic from './Generic.vue'; | ||
28 | |||
29 | // Units to add to download and upload rates. | ||
30 | const units = ['B', 'kiB', 'MiB', 'GiB']; | ||
31 | |||
32 | // Take the rate in bytes and keep dividing it by 1k until the lowest | ||
33 | // value for which we have a unit is determined. Return the value with | ||
34 | // up to two decimals as a string and unit/s appended. | ||
35 | const displayRate = (rate) => { | ||
36 | let i = 0; | ||
37 | |||
38 | while (rate > 1000 && i < units.length) { | ||
39 | rate /= 1000; | ||
40 | i++; | ||
41 | } | ||
42 | |||
43 | return Intl.NumberFormat(undefined, {maximumFractionDigits: 2}) | ||
44 | .format(rate || 0) + ` ${units[i]}/s`; | ||
45 | } | ||
46 | |||
47 | export default { | ||
48 | name: 'rTorrent', | ||
49 | props: {item: Object}, | ||
50 | components: {Generic}, | ||
51 | // Properties for download, upload, torrent count and errors. | ||
52 | data: () => ({dl: null, ul: null, count: null, error: null}), | ||
53 | // Computed properties for the rate labels. | ||
54 | computed: { | ||
55 | downRate: function() { | ||
56 | return displayRate(this.dl); | ||
57 | }, | ||
58 | upRate: function() { | ||
59 | return displayRate(this.ul); | ||
60 | }, | ||
61 | }, | ||
62 | created() { | ||
63 | // Set intervals if configured so the rates and/or torrent count | ||
64 | // will be updated. | ||
65 | const rateInterval = parseInt(this.item.rateInterval, 10) || 0; | ||
66 | const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0; | ||
67 | |||
68 | if (rateInterval > 0) { | ||
69 | setInterval(() => this.fetchRates(), rateInterval); | ||
70 | } | ||
71 | |||
72 | if (torrentInterval > 0) { | ||
73 | setInterval(() => this.fetchCount(), torrentInterval); | ||
74 | } | ||
75 | |||
76 | // Fetch the initial values. | ||
77 | this.fetchRates(); | ||
78 | this.fetchCount(); | ||
79 | }, | ||
80 | methods: { | ||
81 | // Perform two calls to the XML-RPC service and fetch download | ||
82 | // and upload rates. Values are saved to the `ul` and `dl` | ||
83 | // properties. | ||
84 | fetchRates: async function() { | ||
85 | this.getRate('throttle.global_up.rate') | ||
86 | .then((ul) => this.ul = ul) | ||
87 | .catch(() => this.error = true); | ||
88 | |||
89 | this.getRate('throttle.global_down.rate') | ||
90 | .then((dl) => this.dl = dl) | ||
91 | .catch(() => this.error = true); | ||
92 | }, | ||
93 | // Perform a call to the XML-RPC service to fetch the number of | ||
94 | // torrents. | ||
95 | fetchCount: async function() { | ||
96 | this.getCount().catch(() => this.error = true); | ||
97 | }, | ||
98 | // Fetch a numeric value from the XML-RPC service by requesting | ||
99 | // the specified method name and parsing the XML. The response | ||
100 | // is expected to adhere to the structure of a single numeric | ||
101 | // value. | ||
102 | getRate: async function(methodName) { | ||
103 | return this.getXml(methodName) | ||
104 | .then((xml) => parseInt(xml.getElementsByTagName('value')[0].firstChild.textContent, 10)); | ||
105 | }, | ||
106 | // Fetch the numer of torrents by requesting the download list | ||
107 | // and counting the number of entries therein. | ||
108 | getCount: async function() { | ||
109 | return this.getXml('download_list') | ||
110 | .then((xml) => { | ||
111 | const arrayEl = xml.getElementsByTagName('array'); | ||
112 | this.count = arrayEl ? arrayEl[0].getElementsByTagName('value').length : 0; | ||
113 | }); | ||
114 | }, | ||
115 | // Perform a call to the XML-RPC service and parse the response | ||
116 | // as XML, which is then returned. | ||
117 | getXml: async function(methodName) { | ||
118 | const headers = {'Content-Type': 'text/xml'}; | ||
119 | |||
120 | if (this.item.username && this.item.password) { | ||
121 | headers['Authorization'] = `${this.item.username}:${this.item.password}`; | ||
122 | } | ||
123 | |||
124 | return fetch(`${this.item.xmlrpc.replace(/\/$/, '')}/RPC2`, { | ||
125 | method: 'POST', | ||
126 | headers, | ||
127 | body: `<methodCall><methodName>${methodName}</methodName></methodCall>` | ||
128 | }) | ||
129 | .then((response) => { | ||
130 | if (!response.ok) { | ||
131 | throw Error(response.statusText); | ||
132 | } | ||
133 | |||
134 | return response.text(); | ||
135 | }) | ||
136 | .then((text) => Promise.resolve(new DOMParser().parseFromString(text, 'text/xml'))); | ||
137 | } | ||
138 | } | ||
139 | } | ||
140 | </script> | ||
141 | |||
142 | <style scoped lang="scss"> | ||
143 | .error { | ||
144 | color: #e51111 !important; | ||
145 | } | ||
146 | .down { | ||
147 | margin-right: 1em; | ||
148 | } | ||
149 | .count { | ||
150 | color: var(--text); | ||
151 | font-size: 0.8em; | ||
152 | } | ||
153 | </style> \ No newline at end of file | ||