aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--docs/customservices.md22
-rw-r--r--dummy-data/rtorrent/download_list10
-rw-r--r--dummy-data/rtorrent/global_down6
-rw-r--r--dummy-data/rtorrent/global_up6
-rw-r--r--src/components/services/Rtorrent.vue153
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
30If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page. 31If 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 :
249The url must be the root url of the Healthchecks application. 250The url must be the root url of the Healthchecks application.
250The Healthchecks API key can be found in Settings > API Access > API key (read-only). The key is needed to access Healthchecks API. 251The 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
255This service displays the global upload and download rates, as well as the number of torrents
256listed in rTorrent. The service communicates with the rTorrent XML-RPC interface which needs
257to be accessible from the browser. Please consult
258[the instructions](https://github.com/rakshasa/rtorrent-doc/blob/master/RPC-Setup-XMLRPC.md)
259for setting up rTorrent and make sure the correct CORS-settings are applied. Examples for various
260servers 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
254This 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). 276This 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>
27import Generic from './Generic.vue';
28
29// Units to add to download and upload rates.
30const 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.
35const 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
47export 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