aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/services/Rtorrent.vue
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/services/Rtorrent.vue')
-rw-r--r--src/components/services/Rtorrent.vue153
1 files changed, 153 insertions, 0 deletions
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