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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
<template>
<Generic :item="item">
<template #content>
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">
<span v-if="error" class="error">An error has occurred.</span>
<template v-else>
<span class="down">
<i class="fas fa-download"></i> {{ downRate }}
</span>
<span class="up">
<i class="fas fa-upload"></i> {{ upRate }}
</span>
</template>
</p>
</template>
<template #indicator>
<span v-if="!error" class="count">{{ count }}
<template v-if="count === 1">torrent</template>
<template v-else>torrents</template>
</span>
</template>
</Generic>
</template>
<script>
import Generic from './Generic.vue';
// Units to add to download and upload rates.
const units = ['B', 'kiB', 'MiB', 'GiB'];
// Take the rate in bytes and keep dividing it by 1k until the lowest
// value for which we have a unit is determined. Return the value with
// up to two decimals as a string and unit/s appended.
const displayRate = (rate) => {
let i = 0;
while (rate > 1000 && i < units.length) {
rate /= 1000;
i++;
}
return Intl.NumberFormat(undefined, {maximumFractionDigits: 2})
.format(rate || 0) + ` ${units[i]}/s`;
}
export default {
name: 'rTorrent',
props: {item: Object},
components: {Generic},
// Properties for download, upload, torrent count and errors.
data: () => ({dl: null, ul: null, count: null, error: null}),
// Computed properties for the rate labels.
computed: {
downRate: function() {
return displayRate(this.dl);
},
upRate: function() {
return displayRate(this.ul);
},
},
created() {
// Set intervals if configured so the rates and/or torrent count
// will be updated.
const rateInterval = parseInt(this.item.rateInterval, 10) || 0;
const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0;
if (rateInterval > 0) {
setInterval(() => this.fetchRates(), rateInterval);
}
if (torrentInterval > 0) {
setInterval(() => this.fetchCount(), torrentInterval);
}
// Fetch the initial values.
this.fetchRates();
this.fetchCount();
},
methods: {
// Perform two calls to the XML-RPC service and fetch download
// and upload rates. Values are saved to the `ul` and `dl`
// properties.
fetchRates: async function() {
this.getRate('throttle.global_up.rate')
.then((ul) => this.ul = ul)
.catch(() => this.error = true);
this.getRate('throttle.global_down.rate')
.then((dl) => this.dl = dl)
.catch(() => this.error = true);
},
// Perform a call to the XML-RPC service to fetch the number of
// torrents.
fetchCount: async function() {
this.getCount().catch(() => this.error = true);
},
// Fetch a numeric value from the XML-RPC service by requesting
// the specified method name and parsing the XML. The response
// is expected to adhere to the structure of a single numeric
// value.
getRate: async function(methodName) {
return this.getXml(methodName)
.then((xml) => parseInt(xml.getElementsByTagName('value')[0].firstChild.textContent, 10));
},
// Fetch the numer of torrents by requesting the download list
// and counting the number of entries therein.
getCount: async function() {
return this.getXml('download_list')
.then((xml) => {
const arrayEl = xml.getElementsByTagName('array');
this.count = arrayEl ? arrayEl[0].getElementsByTagName('value').length : 0;
});
},
// Perform a call to the XML-RPC service and parse the response
// as XML, which is then returned.
getXml: async function(methodName) {
const headers = {'Content-Type': 'text/xml'};
if (this.item.username && this.item.password) {
headers['Authorization'] = `${this.item.username}:${this.item.password}`;
}
return fetch(`${this.item.xmlrpc.replace(/\/$/, '')}/RPC2`, {
method: 'POST',
headers,
body: `<methodCall><methodName>${methodName}</methodName></methodCall>`
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.text();
})
.then((text) => Promise.resolve(new DOMParser().parseFromString(text, 'text/xml')));
}
}
}
</script>
<style scoped lang="scss">
.error {
color: #e51111 !important;
}
.down {
margin-right: 1em;
}
.count {
color: var(--text);
font-size: 0.8em;
}
</style>
|