mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-15 21:41:58 +01:00
fix: download speed
This commit is contained in:
@@ -26,6 +26,11 @@ import type {
|
||||
TrickPlayData,
|
||||
} from "../types";
|
||||
import { generateFilename } from "../utils";
|
||||
import {
|
||||
addSpeedDataPoint,
|
||||
calculateWeightedSpeed,
|
||||
clearSpeedData,
|
||||
} from "./useDownloadSpeedCalculator";
|
||||
|
||||
interface UseDownloadEventHandlersProps {
|
||||
taskMapRef: MutableRefObject<Map<number, string>>;
|
||||
@@ -80,6 +85,7 @@ export function useDownloadEventHandlers({
|
||||
taskId: event.taskId,
|
||||
progress: event.progress,
|
||||
bytesWritten: event.bytesWritten,
|
||||
totalBytes: event.totalBytes,
|
||||
taskMapSize: taskMapRef.current.size,
|
||||
taskMapKeys: Array.from(taskMapRef.current.keys()),
|
||||
});
|
||||
@@ -93,19 +99,52 @@ export function useDownloadEventHandlers({
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate event data before processing
|
||||
if (
|
||||
typeof event.bytesWritten !== "number" ||
|
||||
event.bytesWritten < 0 ||
|
||||
!Number.isFinite(event.bytesWritten)
|
||||
) {
|
||||
console.warn(
|
||||
`[DPL] Invalid bytesWritten for taskId ${event.taskId}: ${event.bytesWritten}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof event.progress !== "number" ||
|
||||
event.progress < 0 ||
|
||||
event.progress > 1 ||
|
||||
!Number.isFinite(event.progress)
|
||||
) {
|
||||
console.warn(
|
||||
`[DPL] Invalid progress for taskId ${event.taskId}: ${event.progress}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = Math.min(
|
||||
Math.floor(event.progress * 100),
|
||||
99, // Cap at 99% until completion
|
||||
);
|
||||
|
||||
// Add data point and calculate speed (validation happens inside)
|
||||
addSpeedDataPoint(processId, event.bytesWritten);
|
||||
const speed = calculateWeightedSpeed(processId);
|
||||
|
||||
console.log(
|
||||
`[DPL] Progress update for processId: ${processId}, taskId: ${event.taskId}, progress: ${progress}%, bytesWritten: ${event.bytesWritten}`,
|
||||
`[DPL] Progress update for processId: ${processId}, taskId: ${event.taskId}, progress: ${progress}%, bytesWritten: ${event.bytesWritten}, speed: ${speed ? (speed / 1024 / 1024).toFixed(2) : "N/A"} MB/s`,
|
||||
);
|
||||
|
||||
updateProcess(processId, {
|
||||
progress,
|
||||
bytesDownloaded: event.bytesWritten,
|
||||
lastProgressUpdateTime: new Date(),
|
||||
speed,
|
||||
estimatedTotalSizeBytes:
|
||||
event.totalBytes > 0 && Number.isFinite(event.totalBytes)
|
||||
? event.totalBytes
|
||||
: undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -197,6 +236,9 @@ export function useDownloadEventHandlers({
|
||||
|
||||
onSuccess?.();
|
||||
|
||||
// Clean up speed data when download completes
|
||||
clearSpeedData(processId);
|
||||
|
||||
// Remove process after short delay
|
||||
setTimeout(() => {
|
||||
removeProcess(processId);
|
||||
@@ -204,6 +246,7 @@ export function useDownloadEventHandlers({
|
||||
} catch (error) {
|
||||
console.error("Error handling download completion:", error);
|
||||
updateProcess(processId, { status: "error" });
|
||||
clearSpeedData(processId);
|
||||
removeProcess(processId);
|
||||
}
|
||||
},
|
||||
@@ -236,6 +279,9 @@ export function useDownloadEventHandlers({
|
||||
|
||||
updateProcess(processId, { status: "error" });
|
||||
|
||||
// Clean up speed data
|
||||
clearSpeedData(processId);
|
||||
|
||||
const notificationContent = getNotificationContent(
|
||||
process.item,
|
||||
false,
|
||||
|
||||
196
providers/Downloads/hooks/useDownloadSpeedCalculator.ts
Normal file
196
providers/Downloads/hooks/useDownloadSpeedCalculator.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
interface SpeedDataPoint {
|
||||
timestamp: number;
|
||||
bytesDownloaded: number;
|
||||
}
|
||||
|
||||
const WINDOW_DURATION = 60000; // 1 minute in ms
|
||||
const MIN_DATA_POINTS = 3; // Need at least 3 points for accurate speed
|
||||
const MAX_REASONABLE_SPEED = 1024 * 1024 * 1024; // 1 GB/s sanity check
|
||||
|
||||
// Private state
|
||||
const dataPoints = new Map<string, SpeedDataPoint[]>();
|
||||
|
||||
function isValidBytes(bytes: number): boolean {
|
||||
return typeof bytes === "number" && Number.isFinite(bytes) && bytes >= 0;
|
||||
}
|
||||
|
||||
function isValidTimestamp(timestamp: number): boolean {
|
||||
return (
|
||||
typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function addSpeedDataPoint(
|
||||
processId: string,
|
||||
bytesDownloaded: number,
|
||||
): void {
|
||||
// Validate input
|
||||
if (!isValidBytes(bytesDownloaded)) {
|
||||
console.warn(
|
||||
`[SpeedCalc] Invalid bytes value for ${processId}: ${bytesDownloaded}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (!isValidTimestamp(now)) {
|
||||
console.warn(`[SpeedCalc] Invalid timestamp: ${now}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataPoints.has(processId)) {
|
||||
dataPoints.set(processId, []);
|
||||
}
|
||||
|
||||
const points = dataPoints.get(processId)!;
|
||||
|
||||
// Validate that bytes are increasing (or at least not decreasing)
|
||||
if (points.length > 0) {
|
||||
const lastPoint = points[points.length - 1];
|
||||
if (bytesDownloaded < lastPoint.bytesDownloaded) {
|
||||
console.warn(
|
||||
`[SpeedCalc] Bytes decreased for ${processId}: ${lastPoint.bytesDownloaded} -> ${bytesDownloaded}. Resetting.`,
|
||||
);
|
||||
// Reset the data for this process
|
||||
dataPoints.set(processId, []);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new data point
|
||||
points.push({
|
||||
timestamp: now,
|
||||
bytesDownloaded,
|
||||
});
|
||||
|
||||
// Remove data points older than 1 minute
|
||||
const cutoffTime = now - WINDOW_DURATION;
|
||||
while (points.length > 0 && points[0].timestamp < cutoffTime) {
|
||||
points.shift();
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateSpeed(processId: string): number | undefined {
|
||||
const points = dataPoints.get(processId);
|
||||
|
||||
if (!points || points.length < MIN_DATA_POINTS) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const oldest = points[0];
|
||||
const newest = points[points.length - 1];
|
||||
|
||||
// Validate data points
|
||||
if (
|
||||
!isValidBytes(oldest.bytesDownloaded) ||
|
||||
!isValidBytes(newest.bytesDownloaded) ||
|
||||
!isValidTimestamp(oldest.timestamp) ||
|
||||
!isValidTimestamp(newest.timestamp)
|
||||
) {
|
||||
console.warn(`[SpeedCalc] Invalid data points for ${processId}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timeDelta = (newest.timestamp - oldest.timestamp) / 1000; // seconds
|
||||
const bytesDelta = newest.bytesDownloaded - oldest.bytesDownloaded;
|
||||
|
||||
// Validate calculations
|
||||
if (timeDelta < 0.5) {
|
||||
// Not enough time has passed
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (bytesDelta < 0) {
|
||||
console.warn(
|
||||
`[SpeedCalc] Negative bytes delta for ${processId}: ${bytesDelta}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const speed = bytesDelta / timeDelta; // bytes per second
|
||||
|
||||
// Sanity check: if speed is unrealistically high, something is wrong
|
||||
if (!Number.isFinite(speed) || speed < 0 || speed > MAX_REASONABLE_SPEED) {
|
||||
console.warn(`[SpeedCalc] Unrealistic speed for ${processId}: ${speed}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return speed;
|
||||
}
|
||||
|
||||
// Calculate weighted average speed (more recent data has higher weight)
|
||||
export function calculateWeightedSpeed(processId: string): number | undefined {
|
||||
const points = dataPoints.get(processId);
|
||||
|
||||
if (!points || points.length < MIN_DATA_POINTS) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let totalWeightedSpeed = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
// Calculate speed between consecutive points with exponential weighting
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prevPoint = points[i - 1];
|
||||
const currPoint = points[i];
|
||||
|
||||
// Validate both points
|
||||
if (
|
||||
!isValidBytes(prevPoint.bytesDownloaded) ||
|
||||
!isValidBytes(currPoint.bytesDownloaded) ||
|
||||
!isValidTimestamp(prevPoint.timestamp) ||
|
||||
!isValidTimestamp(currPoint.timestamp)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const timeDelta = (currPoint.timestamp - prevPoint.timestamp) / 1000;
|
||||
const bytesDelta = currPoint.bytesDownloaded - prevPoint.bytesDownloaded;
|
||||
|
||||
// Skip invalid deltas
|
||||
if (timeDelta < 0.1 || bytesDelta < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const speed = bytesDelta / timeDelta;
|
||||
|
||||
// Sanity check
|
||||
if (!Number.isFinite(speed) || speed < 0 || speed > MAX_REASONABLE_SPEED) {
|
||||
console.warn(`[SpeedCalc] Skipping unrealistic speed point: ${speed}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// More recent points get exponentially higher weight
|
||||
const weight = 2 ** i;
|
||||
totalWeightedSpeed += speed * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
if (totalWeight === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const weightedSpeed = totalWeightedSpeed / totalWeight;
|
||||
|
||||
// Final sanity check
|
||||
if (!Number.isFinite(weightedSpeed) || weightedSpeed < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return weightedSpeed;
|
||||
}
|
||||
|
||||
export function clearSpeedData(processId: string): void {
|
||||
dataPoints.delete(processId);
|
||||
}
|
||||
|
||||
export function resetAllSpeedData(): void {
|
||||
dataPoints.clear();
|
||||
}
|
||||
|
||||
// Debug function to inspect current state
|
||||
export function getSpeedDataDebug(
|
||||
processId: string,
|
||||
): SpeedDataPoint[] | undefined {
|
||||
return dataPoints.get(processId);
|
||||
}
|
||||
51
utils/downloadStats.ts
Normal file
51
utils/downloadStats.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export function formatSpeed(bytesPerSecond: number | undefined): string {
|
||||
if (!bytesPerSecond || bytesPerSecond <= 0) return "N/A";
|
||||
|
||||
const mbps = bytesPerSecond / (1024 * 1024);
|
||||
if (mbps >= 1) {
|
||||
return `${mbps.toFixed(2)} MB/s`;
|
||||
}
|
||||
|
||||
const kbps = bytesPerSecond / 1024;
|
||||
return `${kbps.toFixed(0)} KB/s`;
|
||||
}
|
||||
|
||||
export function formatETA(
|
||||
bytesDownloaded: number | undefined,
|
||||
totalBytes: number | undefined,
|
||||
speed: number | undefined,
|
||||
): string {
|
||||
if (!bytesDownloaded || !totalBytes || !speed || speed <= 0) {
|
||||
return "Calculating...";
|
||||
}
|
||||
|
||||
const remainingBytes = totalBytes - bytesDownloaded;
|
||||
if (remainingBytes <= 0) return "0s";
|
||||
|
||||
const secondsRemaining = remainingBytes / speed;
|
||||
|
||||
if (secondsRemaining < 60) {
|
||||
return `${Math.ceil(secondsRemaining)}s`;
|
||||
}
|
||||
if (secondsRemaining < 3600) {
|
||||
const minutes = Math.floor(secondsRemaining / 60);
|
||||
const seconds = Math.ceil(secondsRemaining % 60);
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
const hours = Math.floor(secondsRemaining / 3600);
|
||||
const minutes = Math.floor((secondsRemaining % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
export function calculateETA(
|
||||
bytesDownloaded: number | undefined,
|
||||
totalBytes: number | undefined,
|
||||
speed: number | undefined,
|
||||
): number | undefined {
|
||||
if (!bytesDownloaded || !totalBytes || !speed || speed <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const remainingBytes = totalBytes - bytesDownloaded;
|
||||
return remainingBytes / speed; // seconds
|
||||
}
|
||||
Reference in New Issue
Block a user