pull/972/merge
Igor Kulman 2025-08-24 10:37:24 +02:00 committed by GitHub
commit ddac775d4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 226 additions and 0 deletions

View File

@ -52,6 +52,7 @@ within Homer:
- [Tautulli](#tautulli)
- [Tdarr](#tdarr)
- [Traefik](#traefik)
- [Transmission](#transmission)
- [TrueNas Scale](#truenas-scale)
- [Uptime Kuma](#uptime-kuma)
- [Vaultwarden](#vaultwarden)
@ -712,6 +713,32 @@ This service displays a version string instead of a subtitle. Example configurat
url: http://traefik.example.com
```
## Transmission
This service displays the global upload and download rates, as well as the number of active torrents from your Transmission daemon. The service communicates with the Transmission RPC interface which needs to be accessible from the browser. Make sure to configure appropriate CORS headers if accessing from a different domain.
```yaml
- name: "Transmission"
logo: "assets/tools/sample.png"
url: "http://192.168.1.2:9091" # Your Transmission web interface URL
type: "Transmission"
username: "your_username" # Optional: HTTP Basic Auth username
password: "your_password" # Optional: HTTP Basic Auth password
showWhenEmpty: true # Optional: Show data even when no torrents (default: true)
rateInterval: 5000 # Optional: Interval for updating download/upload rates (ms)
torrentInterval: 30000 # Optional: Interval for updating torrent count (ms)
target: "_blank" # Optional: HTML a tag target attribute
```
**Configuration Options:**
- `username/password`: Optional HTTP Basic Authentication credentials
- `showWhenEmpty`: Controls whether to display rates and count when no torrents are active (default: true)
- `rateInterval`: How often to refresh transfer rates in milliseconds
- `torrentInterval`: How often to refresh torrent count in milliseconds
The service automatically handles Transmission's session management and CSRF protection.
## Truenas Scale
This service displays a version string instead of a subtitle. Example configuration:

View File

@ -0,0 +1,199 @@
<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-if="count > 0 || shouldShowWhenEmpty">
<span class="down monospace">
<p class="fas fa-download"></p>
{{ downRate }}
</span>
<span class="up monospace">
<p class="fas fa-upload"></p>
{{ upRate }}
</span>
</template>
</p>
</template>
<template #indicator>
<span v-if="!error && (count > 0 || shouldShowWhenEmpty)" class="count"
>{{ count || 0 }}
<template v-if="(count || 0) === 1">torrent</template>
<template v-else>torrents</template>
</span>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
const units = ["B", "KB", "MB", "GB"];
// 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: "Transmission",
mixins: [service],
props: { item: Object },
data: () => ({
dl: null,
ul: null,
count: null,
error: null,
sessionId: null,
}),
computed: {
downRate: function () {
return displayRate(this.dl);
},
upRate: function () {
return displayRate(this.ul);
},
shouldShowWhenEmpty: function () {
// Default to true (show when empty) unless explicitly set to false
return this.item.showWhenEmpty !== false;
},
},
created() {
// Validate that endpoint is configured
if (!this.endpoint) {
this.error = true;
console.error("Transmission service: No endpoint configured");
return;
}
const rateInterval = parseInt(this.item.rateInterval, 10) || 0;
const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0;
// Set up intervals if configured (rate and torrent intervals can be different)
if (rateInterval > 0) {
setInterval(() => this.getStats(), rateInterval);
}
if (torrentInterval > 0) {
setInterval(() => this.getStats(), torrentInterval);
}
// Initial fetch
this.getStats();
},
methods: {
/**
* Makes a request to Transmission RPC API with proper session handling
* @param {string} method - The RPC method to call
* @param {Object} requestArgs - Arguments for the RPC method
* @returns {Promise<Object>} RPC response
*/
transmissionRequest: async function (method, requestArgs = {}) {
const requestData = {
method: method,
arguments: requestArgs,
};
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestData),
};
// Add HTTP Basic Auth if credentials are provided
if (this.item.username && this.item.password) {
const credentials = btoa(`${this.item.username}:${this.item.password}`);
options.headers["Authorization"] = `Basic ${credentials}`;
}
// Add session ID header if we have one
if (this.sessionId) {
options.headers["X-Transmission-Session-Id"] = this.sessionId;
}
try {
const response = await fetch(
this.endpoint + "/transmission/rpc",
options,
);
// Handle session ID requirement
if (response.status === 409) {
this.sessionId = response.headers.get("X-Transmission-Session-Id");
if (this.sessionId) {
options.headers["X-Transmission-Session-Id"] = this.sessionId;
const retryResponse = await fetch(
this.endpoint + "/transmission/rpc",
options,
);
return await retryResponse.json();
}
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Transmission RPC error:", error);
throw error;
}
},
getStats: async function () {
try {
// Get session stats for transfer rates and torrent count
const statsResponse = await this.transmissionRequest("session-stats");
if (statsResponse && statsResponse.result === "success") {
const stats = statsResponse.arguments;
this.dl = stats.downloadSpeed ?? 0;
this.ul = stats.uploadSpeed ?? 0;
this.count = stats.activeTorrentCount ?? 0;
this.error = false;
} else {
throw new Error(
`Transmission RPC failed: ${statsResponse?.result || "Unknown error"}`,
);
}
} catch (e) {
this.error = true;
console.error("Transmission service error:", e);
}
},
},
};
</script>
<style scoped lang="scss">
.error {
color: #e51111 !important;
}
.down {
margin-right: 1em;
}
.count {
color: var(--text);
font-size: 0.8em;
}
.monospace {
font-weight: 300;
font-family: monospace;
}
</style>