✨ feat: Added average download speed settings and calculation functionality.
This commit is contained in:
parent
03f5b92c01
commit
88f65da57b
6 changed files with 203 additions and 97 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export const SettingKeys = [
|
|||
"showSensitiveContent",
|
||||
"autoDownloadLimit",
|
||||
"proxys",
|
||||
"avgSpeedInterval",
|
||||
] as const;
|
||||
|
||||
export type SettingKey = (typeof SettingKeys)[number];
|
||||
|
|
|
|||
Loading…
Reference in a new issue