Add ability to control Deluge's upload and download speeds based on Jellyfin streams.

This commit is contained in:
2026-04-20 20:30:43 +10:00
parent 758e4ea9a4
commit 90e38c0fa8
4 changed files with 252 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
title: Deluge Bridge
sections:
deluge:
type: fields
fields:
delugeUrl:
label: Deluge URL
type: url
width: 1/2
delugePassword:
label: Deluge Password
type: text
width: 1/2
minDownSpeed:
label: Minimum Download Speed
type: number
min: 0
default: 4096
after: KiB/s
width: 1/2
maxDownSpeed:
label: Maximum Download Speed
type: number
min: 0
default: 10240
after: KiB/s
width: 1/2
minUpSpeed:
label: Minimum Upload Speed
type: number
min: 0
default: 512
after: KiB/s
width: 1/2
maxUpSpeed:
label: Maximum Upload Speed
type: number
min: 0
default: 4096
after: KiB/s
width: 1/2
maxTimeAtMin:
label: Maximum Time At Minimum Speed
type: number
min: 0
default: 240
after: minutes
jellyfin:
type: fields
fields:
jellyfinUrl:
label: Jellyfin URL
type: url
width: 1/2
jellyfinApiKey:
label: Jellyfin API Key
type: text
width: 1/2
currentStreams:
label: Current Streams
type: number
min: 0
default: 0
disabled: true
width: 1/2
lastSeen:
label: Last Seen Stream
type: number
min: 0
default: 0
disabled: true
width: 1/2

147
controllers/deluge.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
/**
* Copyright 2026, Dreytac <dreytac@hobbyhome.net>
*
* This file is part of Kirby Deluge.
*
* Kirby Deluge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
*
* Kirby Deluge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with Kirby Deluge. If not, see <https://www.gnu.org/licenses/>.
*/
return function ($page) {
$currentStreams = $page->currentStreams()->toInt();
$lastSeen = $page->lastSeen()->toInt();
$curl = curl_init($page->delugeUrl()->toString() . "/json");
$curlOptions = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => ["Accept: application/json", "Content-Type: application/json"],
CURLOPT_ENCODING => "",
CURLOPT_COOKIEJAR => "",
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLINFO_HEADER_OUT => true,
];
curl_setopt_array($curl, $curlOptions);
$reqId = 0;
$response = site()->makeRequest($curl, $reqId, "auth.login", [$page->delugePassword()->toString()]);
$reqId++;
if (get("s") == "inc") {
// Increment number of streams and ensure speeds are set to minimums.
$currentStreams++;
$lastSeen = time();
site()->makeRequest($curl, $reqId, "core.set_config", [["max_download_speed" => $page->minDownSpeed()->toInt()]]);
$reqId++;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_upload_speed" => $page->minUpSpeed()->toInt()]]);
} elseif (get("s") == "dec") {
// Decrement number of streams and, if 0 active streams, ensure speeds are set to maximum.
$currentStreams--;
if ($lastSeen > 0) {
if ($lastSeen < time() - ($page->maxTimeAtMin()->toInt() * 60)) {
// Assume all sessions have finished and clear them.
$currentStreams = 0;
$lastSeen = 0;
}
}
if ($currentStreams <= 0) {
site()->makeRequest($curl, $reqId, "core.set_config", [["max_download_speed" => $page->maxDownSpeed()->toInt()]]);
$reqId++;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_upload_speed" => $page->maxUpSpeed()->toInt()]]);
}
} elseif (get("s") == "reset") {
// Reset streams and speeds back to 0 and maximum respectively.
$currentStreams = 0;
$lastSeen = 0;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_download_speed" => $page->maxDownSpeed()->toInt()]]);
$reqId++;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_upload_speed" => $page->maxUpSpeed()->toInt()]]);
} elseif (get("s") == "update") {
// Forceably get a list of active sessions from Jellyfin and update Deluge accordingly.
$jfCurl = curl_init($page->jellyfinUrl()->toString() . "/Sessions");
$jfCurlOptions = [
CURLOPT_RETURNTRANSFER => true,
// CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => ["Accept: application/json", "Content-Type: application/json", "Authorization: MediaBrowser Token=\"{$page->jellyfinApiKey()->toString()}\""],
CURLOPT_ENCODING => "",
CURLOPT_COOKIEJAR => "",
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLINFO_HEADER_OUT => true,
CURLOPT_VERBOSE => true,
];
curl_setopt_array($jfCurl, $jfCurlOptions);
$sessions = site()->makeRequest($jfCurl, $reqId, "get");
$reqId++;
curl_close($jfCurl);
$sessionsActive = 0;
if (!is_null($sessions)) {
foreach ($sessions as $session) {
if (isset($session["NowPlayingItem"])) {
if ($session["NowPlayingItem"]["Type"] !== "Audio") {
$sessionsActive++;
}
}
}
if ($sessionsActive == 0) {
// There are no sessions active. Set to maximum speed.
$currentStreams = 0;
$lastSeen = 0;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_download_speed" => $page->maxDownSpeed()->toInt()]]);
$reqId++;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_upload_speed" => $page->maxUpSpeed()->toInt()]]);
} else {
// There are sessions active. Set to minimum speed.
$currentStreams = $sessionsActive;
$lastSeen = time();
site()->makeRequest($curl, $reqId, "core.set_config", [["max_download_speed" => $page->minDownSpeed()->toInt()]]);
$reqId++;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_upload_speed" => $page->minUpSpeed()->toInt()]]);
}
} else {
// We couldn't access Jellyfin. Assume there's something wrong and that no sessions are active.
$currentStreams = 0;
$lastSeen = 0;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_download_speed" => $page->maxDownSpeed()->toInt()]]);
$reqId++;
site()->makeRequest($curl, $reqId, "core.set_config", [["max_upload_speed" => $page->maxUpSpeed()->toInt()]]);
}
}
// We should never have less than 0 streams active.
$currentStreams = $currentStreams < 0 ? 0 : $currentStreams;
$page = kirby()->impersonate("kirby", function () use ($page, $currentStreams, $lastSeen) {
return $page->update([
"currentStreams" => $currentStreams,
"lastSeen" => $lastSeen,
]);
});
curl_close($curl);
if (!($user = kirby()->user() and $user->role()->isAdmin())) {
// Limit access to page to administrators only.
echo new Response(Data::encode(["currentStreams" => $currentStreams, "lastSeen" => $lastSeen], "json"), "text/json");
exit();
}
return compact("page");
};

View File

@@ -14,6 +14,15 @@
Kirby::plugin( Kirby::plugin(
name: "hobbyhome/deluge", name: "hobbyhome/deluge",
extends: [ extends: [
"blueprints" => [
"pages/deluge" => __DIR__ . "/blueprints/pages/deluge.yml",
],
"templates" => [
"deluge" => __DIR__ . "/templates/deluge.php",
],
"controllers" => [
"deluge" => require __DIR__ . "/controllers/deluge.php",
],
], ],
info: [ info: [
"authors" => [[ "authors" => [[

23
templates/deluge.php Normal file
View File

@@ -0,0 +1,23 @@
<?= snippet("header") ?>
<?= snippet("page/title") ?>
<div class="container">
<p><strong>Current Streams</strong>: <?= $page->currentStreams() ?></p>
<?php if ($page->currentStreams()->toInt() > 0) : ?>
<p><strong>Last Seen</strong>: <?= date('l \t\h\e\ jS \of F Y \a\t g:ia', $page->lastSeen()->toInt()) ?></p>
<?php endif ?>
</div>
<div class="row mb-3">
<div class="d-grid gap-2 col-6 mx-auto">
<a class="btn btn-primary" href="?s=update" role="button">Update</a>
<a class="btn btn-warning" href="?s=reset" role="button">Reset</a>
</div>
<div class="d-grid gap-2 col-6 mx-auto">
<a class="btn btn-danger" href="?s=inc" role="button">Increment Streams</a>
<a class="btn btn-danger" href="?s=dec" role="button">Decrement Streams</a>
</div>
</div>
<?= snippet("footer") ?>