feat: Added average download speed settings and calculation functionality.

This commit is contained in:
jarvis2f 2025-01-07 10:52:22 +08:00
parent 03f5b92c01
commit 88f65da57b
6 changed files with 203 additions and 97 deletions

View file

@ -37,6 +37,7 @@ public class AvgSpeed {
*/
public void update(long downloadedSize, long timestamp) {
if (downloadedSize <= 0) {
removeOldPoints(timestamp);
return;
}
// Calculate speed since last point
@ -50,7 +51,10 @@ public class AvgSpeed {
// Add new speed point
speedPoints.put(timestamp, new SpeedPoint(downloadedSize, speed, timestamp));
// Remove old points outside the interval
removeOldPoints(timestamp);
}
private void removeOldPoints(long timestamp) {
long cutoffTime = timestamp - interval * 1000L; // Convert interval to milliseconds
speedPoints.headMap(cutoffTime).clear();
}

View file

@ -24,6 +24,7 @@ import io.vertx.ext.web.handler.CorsHandler;
import io.vertx.ext.web.handler.SessionHandler;
import io.vertx.ext.web.sstore.LocalSessionStore;
import io.vertx.ext.web.sstore.SessionStore;
import telegram.files.repository.SettingKey;
import telegram.files.repository.SettingRecord;
import telegram.files.repository.TelegramRecord;
@ -261,18 +262,25 @@ public class HttpVerticle extends AbstractVerticle {
}
private void handleSettings(RoutingContext ctx) {
String keys = ctx.request().getParam("keys");
if (StrUtil.isBlank(keys)) {
String keysStr = ctx.request().getParam("keys");
if (StrUtil.isBlank(keysStr)) {
ctx.fail(400);
return;
}
List<String> keys = Arrays.asList(keysStr.split(","));
DataVerticle.settingRepository
.getByKeys(Arrays.asList(keys.split(",")))
.getByKeys(keys)
.onSuccess(settings -> {
JsonObject object = new JsonObject();
for (SettingRecord record : settings) {
object.put(record.key(), record.value());
}
for (String key : keys) {
if (object.containsKey(key)) {
continue;
}
object.put(key, SettingKey.valueOf(key).defaultValue);
}
ctx.json(object);
})
.onFailure(ctx::fail);

View file

@ -57,7 +57,9 @@ public class TelegramVerticle extends AbstractVerticle {
public TelegramRecord telegramRecord;
private final AvgSpeed avgSpeed = new AvgSpeed();
private AvgSpeed avgSpeed = new AvgSpeed();
private long avgSpeedPersistenceTimerId;
static {
Client.setLogMessageHandler(0, new LogMessageHandler());
@ -103,8 +105,9 @@ public class TelegramVerticle extends AbstractVerticle {
telegramUpdateHandler.setOnFileDownloadsUpdated(this::onFileDownloadsUpdated);
telegramUpdateHandler.setOnMessageReceived(this::onMessageReceived);
client = Client.create(telegramUpdateHandler, this::handleException, this::handleException);
vertx.setPeriodic(60 * 1000, id -> handleSaveAvgSpeed());
this.enableProxy(this.proxyName)
Future.all(initEventConsumer(), initAvgSpeed())
.compose(r -> this.enableProxy(this.proxyName))
.onSuccess(r -> startPromise.complete())
.onFailure(startPromise::fail);
}
@ -225,6 +228,7 @@ public class TelegramVerticle extends AbstractVerticle {
return this.execute(new TdApi.GetMessages(chatId, messageIds))
.map(m -> {
Map<Long, TdApi.Message> messageMap = Arrays.stream(m.messages)
.filter(Objects::nonNull)
.collect(Collectors.toMap(message -> message.id, Function.identity()));
List<FileRecord> fileRecords = r.v1.stream()
.map(fileRecord -> {
@ -666,6 +670,37 @@ public class TelegramVerticle extends AbstractVerticle {
StatisticRecord.Type.speed,
System.currentTimeMillis(),
data.encode()));
// Avoid speed not being updated for a long time
avgSpeed.update(0, System.currentTimeMillis());
}
private Future<Void> initAvgSpeed() {
return DataVerticle.settingRepository.<Integer>getByKey(SettingKey.avgSpeedInterval)
.compose(interval -> {
if (Objects.equals(interval, avgSpeed.getSpeedStats().interval())) {
if (avgSpeedPersistenceTimerId == 0) {
avgSpeedPersistenceTimerId = vertx.setPeriodic(interval * 1000, id -> handleSaveAvgSpeed());
}
return Future.succeededFuture();
}
avgSpeed = new AvgSpeed(interval);
if (avgSpeedPersistenceTimerId != 0) {
vertx.cancelTimer(avgSpeedPersistenceTimerId);
}
avgSpeedPersistenceTimerId = vertx.setPeriodic(interval * 1000, id -> handleSaveAvgSpeed());
return Future.succeededFuture();
});
}
private Future<Void> initEventConsumer() {
vertx.eventBus().consumer(EventEnum.SETTING_UPDATE.address(SettingKey.avgSpeedInterval.name()), message -> {
log.debug("Avg Speed Interval update: %s".formatted(message.body()));
this.initAvgSpeed();
});
return Future.succeededFuture();
}
private void onAuthorizationStateUpdated(TdApi.AuthorizationState authorizationState) {
@ -883,16 +918,18 @@ public class TelegramVerticle extends AbstractVerticle {
.put("avgSpeed", 0)
.put("medianSpeed", 0)
.put("maxSpeed", 0)
.put("minSpeed", Long.MAX_VALUE),
.put("minSpeed", 0),
(a, b) -> new JsonObject()
.put("avgSpeed", a.getLong("avgSpeed") + b.getLong("avgSpeed"))
.put("medianSpeed", a.getLong("medianSpeed") + b.getLong("medianSpeed"))
.put("maxSpeed", Math.max(a.getLong("maxSpeed"), b.getLong("maxSpeed")))
.put("minSpeed", Math.min(a.getLong("minSpeed"), b.getLong("minSpeed")))
.put("maxSpeed", a.getLong("maxSpeed") + b.getLong("maxSpeed"))
.put("minSpeed", a.getLong("minSpeed") + b.getLong("minSpeed"))
);
int size = entry.getValue().size();
speedStat.put("avgSpeed", speedStat.getLong("avgSpeed") / size)
.put("medianSpeed", speedStat.getLong("medianSpeed") / size);
.put("medianSpeed", speedStat.getLong("medianSpeed") / size)
.put("maxSpeed", speedStat.getLong("maxSpeed") / size)
.put("minSpeed", speedStat.getLong("minSpeed") / size);
return new JsonObject()
.put("time", entry.getKey())
.put("data", speedStat);

View file

@ -21,7 +21,7 @@ import {
YAxis,
} from "recharts";
import { request } from "@/lib/api";
import prettyBytes from "pretty-bytes"; // Type definitions
import prettyBytes from "pretty-bytes";
// Type definitions
type TimeRange = "1" | "2" | "3" | "4";
@ -144,63 +144,75 @@ const TelegramStats: React.FC<TelegramStatsProps> = ({ telegramId }) => {
</CardHeader>
<CardContent className="px-1">
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={speedChartData}>
<CartesianGrid stroke="#e5e7eb" />
<XAxis
dataKey="time"
tick={axisStyle}
interval="preserveStartEnd"
/>
<YAxis
tickFormatter={(value: number) =>
prettyBytes(value, { bits: true })
}
tick={axisStyle}
interval="preserveStartEnd"
/>
<Tooltip
formatter={(value: number) =>
prettyBytes(value, { bits: true })
}
contentStyle={{
backgroundColor: "rgba(255, 255, 255, 0.9)",
border: "none",
borderRadius: "6px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
}}
/>
<Legend wrapperStyle={axisStyle} iconType="circle" />
<Area
type="monotone"
dataKey="Max Speed"
stroke="#06b6d4"
fill="#06b6d4"
fillOpacity={0.6}
/>
<Area
type="monotone"
dataKey="Average Speed"
stroke="#8b5cf6"
fill="#8b5cf6"
fillOpacity={0.8}
/>
<Area
type="monotone"
dataKey="Median Speed"
stroke="#f59e0b"
fill="#f59e0b"
fillOpacity={0.2}
/>
<Area
type="monotone"
dataKey="Min Speed"
stroke="#ec4899"
fill="#ec4899"
fillOpacity={0.7}
/>
</AreaChart>
</ResponsiveContainer>
{!speedChartData || speedChartData.length === 0 ? (
<div className="flex h-full items-center justify-center text-gray-500">
No data available
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={speedChartData}>
<CartesianGrid stroke="#e5e7eb" vertical={false} />
<XAxis
dataKey="time"
tick={axisStyle}
tickMargin={10}
interval="preserveStartEnd"
tickLine={false}
axisLine={false}
/>
<YAxis
tickFormatter={(value: number) =>
prettyBytes(value, { bits: true })
}
tick={axisStyle}
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
/>
<Tooltip
formatter={(value: number) =>
prettyBytes(value, { bits: true })
}
contentStyle={{
backgroundColor: "rgba(255, 255, 255, 0.9)",
border: "none",
borderRadius: "6px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
fontSize: "12px",
}}
/>
<Legend wrapperStyle={axisStyle} iconType="circle" />
<Area
type="monotone"
dataKey="Max Speed"
stroke="#06b6d4"
fill="#06b6d4"
fillOpacity={0.6}
/>
<Area
type="monotone"
dataKey="Average Speed"
stroke="#8b5cf6"
fill="#8b5cf6"
fillOpacity={0.8}
/>
<Area
type="monotone"
dataKey="Median Speed"
stroke="#f59e0b"
fill="#f59e0b"
fillOpacity={0.2}
/>
<Area
type="monotone"
dataKey="Min Speed"
stroke="#ec4899"
fill="#ec4899"
fillOpacity={0.7}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
</CardContent>
</Card>
@ -211,34 +223,48 @@ const TelegramStats: React.FC<TelegramStatsProps> = ({ telegramId }) => {
</CardHeader>
<CardContent className="px-1">
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={completionChartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
tickMargin={10}
axisLine={false}
/>
<YAxis tick={axisStyle} interval="preserveStartEnd" />
<Tooltip
cursor={{ fill: "rgba(59, 130, 246, 0.1)" }}
contentStyle={{
backgroundColor: "rgba(255, 255, 255, 0.9)",
border: "none",
borderRadius: "6px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
}}
/>
<Legend wrapperStyle={axisStyle} iconType="rect" />
<Bar
dataKey="Completed Downloads"
fill="#299d90"
fillOpacity={0.8}
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
{!completionChartData || completionChartData.length === 0 ? (
<div className="flex h-full items-center justify-center text-gray-500">
No data available
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={completionChartData}>
<CartesianGrid stroke="#e5e7eb" vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
tickMargin={10}
axisLine={false}
tick={axisStyle}
/>
<YAxis
tick={axisStyle}
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
/>
<Tooltip
cursor={false}
contentStyle={{
backgroundColor: "rgba(255, 255, 255, 0.9)",
border: "none",
borderRadius: "6px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
fontSize: "12px",
}}
/>
<Legend wrapperStyle={axisStyle} iconType="rect" />
<Bar
dataKey="Completed Downloads"
fill="#299d90"
fillOpacity={0.8}
maxBarSize={100}
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
)}
</div>
</CardContent>
</Card>

View file

@ -28,6 +28,14 @@ export default function SettingsForm() {
{ value: "d", label: "crop 1280x1280" },
];
const avgSpeedIntervalOptions = [
{ value: "60", label: "1 minute" },
{ value: "300", label: "5 minutes" },
{ value: "600", label: "10 minutes" },
{ value: "900", label: "15 minutes" },
{ value: "1800", label: "30 minutes" },
]
const handleSave = async (e: FormEvent) => {
e.preventDefault();
await updateSettings();
@ -149,6 +157,28 @@ export default function SettingsForm() {
}}
/>
</div>
<div className="flex flex-col space-y-2">
<Label htmlFor="avg-speed-interval">Avg Speed Interval</Label>
<Select
value={String(settings?.avgSpeedInterval)}
onValueChange={(v) => void setSetting("avgSpeedInterval", v)}
>
<SelectTrigger id="avg-speed-interval">
<SelectValue placeholder="Select Avg Speed Interval" />
</SelectTrigger>
<SelectContent>
{avgSpeedIntervalOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
The interval to calculate the average download speed. <br />
Longer intervals may consume more memory.
</p>
</div>
</div>
</div>
<div className="mt-2 flex flex-1 justify-end">

View file

@ -90,6 +90,7 @@ export const SettingKeys = [
"showSensitiveContent",
"autoDownloadLimit",
"proxys",
"avgSpeedInterval",
] as const;
export type SettingKey = (typeof SettingKeys)[number];