Add app runtime statistics
This commit is contained in:
parent
971f669bb6
commit
1f9a26e4b0
@ -76,6 +76,7 @@ export class AppController {
|
|||||||
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
||||||
|
|
||||||
private autoResumePending = false;
|
private autoResumePending = false;
|
||||||
|
private runtimeStatsTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
configureLogger(this.storagePaths.baseDir);
|
configureLogger(this.storagePaths.baseDir);
|
||||||
@ -114,6 +115,11 @@ export class AppController {
|
|||||||
runtimeDir: this.storagePaths.baseDir
|
runtimeDir: this.storagePaths.baseDir
|
||||||
});
|
});
|
||||||
startDebugServer(this.manager, this.storagePaths.baseDir);
|
startDebugServer(this.manager, this.storagePaths.baseDir);
|
||||||
|
this.runtimeStatsTimer = setInterval(() => {
|
||||||
|
this.manager.persistRuntimeStats();
|
||||||
|
this.settings = this.manager.getSettings();
|
||||||
|
}, 60_000);
|
||||||
|
this.runtimeStatsTimer.unref?.();
|
||||||
|
|
||||||
if (this.settings.autoResumeOnStart) {
|
if (this.settings.autoResumeOnStart) {
|
||||||
const snapshot = this.manager.getSnapshot();
|
const snapshot = this.manager.getSnapshot();
|
||||||
@ -237,6 +243,7 @@ export class AppController {
|
|||||||
const liveSettings = this.manager.getSettings();
|
const liveSettings = this.manager.getSettings();
|
||||||
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||||
nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||||
|
nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs());
|
||||||
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
||||||
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
||||||
nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
||||||
@ -639,6 +646,10 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public shutdown(): void {
|
public shutdown(): void {
|
||||||
|
if (this.runtimeStatsTimer) {
|
||||||
|
clearInterval(this.runtimeStatsTimer);
|
||||||
|
this.runtimeStatsTimer = null;
|
||||||
|
}
|
||||||
stopDebugServer();
|
stopDebugServer();
|
||||||
abortActiveUpdateDownload();
|
abortActiveUpdateDownload();
|
||||||
this.manager.prepareForShutdown();
|
this.manager.prepareForShutdown();
|
||||||
|
|||||||
@ -104,6 +104,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
confirmDeleteSelection: true,
|
confirmDeleteSelection: true,
|
||||||
totalDownloadedAllTime: 0,
|
totalDownloadedAllTime: 0,
|
||||||
totalCompletedFilesAllTime: 0,
|
totalCompletedFilesAllTime: 0,
|
||||||
|
totalRuntimeAllTimeMs: 0,
|
||||||
bandwidthSchedules: [],
|
bandwidthSchedules: [],
|
||||||
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
|
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
|
||||||
extractCpuPriority: "high",
|
extractCpuPriority: "high",
|
||||||
|
|||||||
@ -681,7 +681,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
...buildStatsPayload(snapshot),
|
...buildStatsPayload(snapshot),
|
||||||
allTime: {
|
allTime: {
|
||||||
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
|
||||||
|
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1254,6 +1254,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private lastPersistAt = 0;
|
private lastPersistAt = 0;
|
||||||
private lastSettingsPersistAt = 0;
|
private lastSettingsPersistAt = 0;
|
||||||
|
private appSessionStartedAt = 0;
|
||||||
|
private runtimePersistedTotalMs = 0;
|
||||||
|
private runtimePersistedAt = 0;
|
||||||
|
|
||||||
private cleanupQueue: Promise<void> = Promise.resolve();
|
private cleanupQueue: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
@ -1332,6 +1335,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
|
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
|
||||||
super();
|
super();
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
const startedAt = nowMs();
|
||||||
|
this.appSessionStartedAt = startedAt;
|
||||||
|
this.runtimePersistedTotalMs = Math.max(0, Number(settings.totalRuntimeAllTimeMs || 0));
|
||||||
|
this.runtimePersistedAt = startedAt;
|
||||||
this.session = cloneSession(session);
|
this.session = cloneSession(session);
|
||||||
this.itemCount = Object.keys(this.session.items).length;
|
this.itemCount = Object.keys(this.session.items).length;
|
||||||
this.storagePaths = storagePaths;
|
this.storagePaths = storagePaths;
|
||||||
@ -1600,7 +1607,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const previous = this.settings;
|
const previous = this.settings;
|
||||||
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
|
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
|
||||||
next.totalCompletedFilesAllTime = Math.max(next.totalCompletedFilesAllTime || 0, this.settings.totalCompletedFilesAllTime || 0);
|
next.totalCompletedFilesAllTime = Math.max(next.totalCompletedFilesAllTime || 0, this.settings.totalCompletedFilesAllTime || 0);
|
||||||
|
const now = nowMs();
|
||||||
|
next.totalRuntimeAllTimeMs = Math.max(next.totalRuntimeAllTimeMs || 0, this.getLiveTotalRuntimeMs(now));
|
||||||
this.settings = next;
|
this.settings = next;
|
||||||
|
this.runtimePersistedTotalMs = this.settings.totalRuntimeAllTimeMs || 0;
|
||||||
|
this.runtimePersistedAt = now;
|
||||||
this.ensureProviderDailyUsageFresh(nowMs());
|
this.ensureProviderDailyUsageFresh(nowMs());
|
||||||
this.debridService.setSettings(next);
|
this.debridService.setSettings(next);
|
||||||
this.allDebridHostInfoCache.clear();
|
this.allDebridHostInfoCache.clear();
|
||||||
@ -1749,13 +1760,53 @@ export class DownloadManager extends EventEmitter {
|
|||||||
totalFilesSession: this.sessionCompletedFiles,
|
totalFilesSession: this.sessionCompletedFiles,
|
||||||
totalFilesAllTime: this.settings.totalCompletedFilesAllTime,
|
totalFilesAllTime: this.settings.totalCompletedFilesAllTime,
|
||||||
totalPackages: this.session.packageOrder.length,
|
totalPackages: this.session.packageOrder.length,
|
||||||
sessionStartedAt: this.session.runStartedAt
|
sessionStartedAt: this.session.runStartedAt,
|
||||||
|
appSessionStartedAt: this.appSessionStartedAt,
|
||||||
|
sessionRuntimeMs: this.getAppSessionRuntimeMs(now),
|
||||||
|
totalRuntimeMs: this.getLiveTotalRuntimeMs(now),
|
||||||
|
runtimeMeasuredAt: now
|
||||||
};
|
};
|
||||||
this.statsCache = stats;
|
this.statsCache = stats;
|
||||||
this.statsCacheAt = now;
|
this.statsCacheAt = now;
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLiveTotalRuntimeMs(now = nowMs()): number {
|
||||||
|
return Math.max(0, this.runtimePersistedTotalMs + Math.max(0, now - this.runtimePersistedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAppSessionRuntimeMs(now = nowMs()): number {
|
||||||
|
return this.appSessionStartedAt > 0 ? Math.max(0, now - this.appSessionStartedAt) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private foldRuntimeIntoSettings(now = nowMs()): boolean {
|
||||||
|
const totalRuntimeMs = this.getLiveTotalRuntimeMs(now);
|
||||||
|
if (!Number.isFinite(totalRuntimeMs) || totalRuntimeMs <= (this.settings.totalRuntimeAllTimeMs || 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.settings.totalRuntimeAllTimeMs = totalRuntimeMs;
|
||||||
|
this.runtimePersistedTotalMs = totalRuntimeMs;
|
||||||
|
this.runtimePersistedAt = now;
|
||||||
|
this.invalidateStatsCache();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public persistRuntimeStats(sync = false): void {
|
||||||
|
if (this.blockAllPersistence) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = nowMs();
|
||||||
|
if (!this.foldRuntimeIntoSettings(now)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastSettingsPersistAt = now;
|
||||||
|
if (sync) {
|
||||||
|
saveSettings(this.storagePaths, this.settings);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void saveSettingsAsync(this.storagePaths, this.settings).catch((err) => logger.warn(`saveSettingsAsync Fehler: ${compactErrorText(err as Error)}`));
|
||||||
|
}
|
||||||
|
|
||||||
private invalidateStatsCache(): void {
|
private invalidateStatsCache(): void {
|
||||||
this.statsCache = null;
|
this.statsCache = null;
|
||||||
this.statsCacheAt = 0;
|
this.statsCacheAt = 0;
|
||||||
@ -4293,6 +4344,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const pkgCount = Object.keys(this.session.packages).length;
|
const pkgCount = Object.keys(this.session.packages).length;
|
||||||
const itemCount = Object.keys(this.session.items).length;
|
const itemCount = Object.keys(this.session.items).length;
|
||||||
logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`);
|
logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`);
|
||||||
|
this.foldRuntimeIntoSettings(nowMs());
|
||||||
saveSession(this.storagePaths, this.session);
|
saveSession(this.storagePaths, this.session);
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
} else {
|
} else {
|
||||||
@ -4614,6 +4666,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.lastPersistAt = now;
|
this.lastPersistAt = now;
|
||||||
void saveSessionAsync(this.storagePaths, this.session).catch((err) => logger.warn(`saveSessionAsync Fehler: ${compactErrorText(err)}`));
|
void saveSessionAsync(this.storagePaths, this.session).catch((err) => logger.warn(`saveSessionAsync Fehler: ${compactErrorText(err)}`));
|
||||||
if (now - this.lastSettingsPersistAt >= 30000) {
|
if (now - this.lastSettingsPersistAt >= 30000) {
|
||||||
|
this.foldRuntimeIntoSettings(now);
|
||||||
this.lastSettingsPersistAt = now;
|
this.lastSettingsPersistAt = now;
|
||||||
void saveSettingsAsync(this.storagePaths, this.settings).catch((err) => logger.warn(`saveSettingsAsync Fehler: ${compactErrorText(err as Error)}`));
|
void saveSettingsAsync(this.storagePaths, this.settings).catch((err) => logger.warn(`saveSettingsAsync Fehler: ${compactErrorText(err as Error)}`));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -384,6 +384,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
|
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
|
||||||
totalDownloadedAllTime: typeof settings.totalDownloadedAllTime === "number" && settings.totalDownloadedAllTime >= 0 ? settings.totalDownloadedAllTime : defaults.totalDownloadedAllTime,
|
totalDownloadedAllTime: typeof settings.totalDownloadedAllTime === "number" && settings.totalDownloadedAllTime >= 0 ? settings.totalDownloadedAllTime : defaults.totalDownloadedAllTime,
|
||||||
totalCompletedFilesAllTime: typeof settings.totalCompletedFilesAllTime === "number" && settings.totalCompletedFilesAllTime >= 0 ? settings.totalCompletedFilesAllTime : defaults.totalCompletedFilesAllTime,
|
totalCompletedFilesAllTime: typeof settings.totalCompletedFilesAllTime === "number" && settings.totalCompletedFilesAllTime >= 0 ? settings.totalCompletedFilesAllTime : defaults.totalCompletedFilesAllTime,
|
||||||
|
totalRuntimeAllTimeMs: typeof settings.totalRuntimeAllTimeMs === "number" && settings.totalRuntimeAllTimeMs >= 0 ? settings.totalRuntimeAllTimeMs : defaults.totalRuntimeAllTimeMs,
|
||||||
theme: VALID_THEMES.has(settings.theme) ? settings.theme : defaults.theme,
|
theme: VALID_THEMES.has(settings.theme) ? settings.theme : defaults.theme,
|
||||||
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules),
|
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules),
|
||||||
columnOrder: normalizeColumnOrder(settings.columnOrder),
|
columnOrder: normalizeColumnOrder(settings.columnOrder),
|
||||||
|
|||||||
@ -89,7 +89,8 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
|||||||
...buildStatsPayload(snapshot),
|
...buildStatsPayload(snapshot),
|
||||||
allTime: {
|
allTime: {
|
||||||
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
|
||||||
|
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
addJson(zip, "overview/debug-setup.json", debugSetup);
|
addJson(zip, "overview/debug-setup.json", debugSetup);
|
||||||
|
|||||||
@ -134,6 +134,7 @@ export function buildRedactedSettingsPayload(settings: AppSettings): Record<stri
|
|||||||
statistics: {
|
statistics: {
|
||||||
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
|
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
|
||||||
|
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs,
|
||||||
providerDailyLimitBytes: settings.providerDailyLimitBytes,
|
providerDailyLimitBytes: settings.providerDailyLimitBytes,
|
||||||
providerDailyUsageBytes: settings.providerDailyUsageBytes,
|
providerDailyUsageBytes: settings.providerDailyUsageBytes,
|
||||||
providerTotalUsageBytes: settings.providerTotalUsageBytes,
|
providerTotalUsageBytes: settings.providerTotalUsageBytes,
|
||||||
|
|||||||
@ -764,7 +764,11 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
totalFilesSession: 0,
|
totalFilesSession: 0,
|
||||||
totalFilesAllTime: 0,
|
totalFilesAllTime: 0,
|
||||||
totalPackages: 0,
|
totalPackages: 0,
|
||||||
sessionStartedAt: 0
|
sessionStartedAt: 0,
|
||||||
|
appSessionStartedAt: 0,
|
||||||
|
sessionRuntimeMs: 0,
|
||||||
|
totalRuntimeMs: 0,
|
||||||
|
runtimeMeasuredAt: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
@ -783,7 +787,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
|||||||
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
||||||
theme: "dark", collapseNewPackages: true, autoSortPackagesByProgress: true, autoSkipExtracted: false, confirmDeleteSelection: true,
|
theme: "dark", collapseNewPackages: true, autoSortPackagesByProgress: true, autoSkipExtracted: false, confirmDeleteSelection: true,
|
||||||
accountListShowDetailedDebridLinkKeys: false,
|
accountListShowDetailedDebridLinkKeys: false,
|
||||||
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0,
|
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
|
||||||
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
|
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
|
||||||
autoExtractWhenStopped: true,
|
autoExtractWhenStopped: true,
|
||||||
disabledProviders: [],
|
disabledProviders: [],
|
||||||
@ -928,6 +932,55 @@ function humanSize(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(3)} TB`;
|
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(3)} TB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRuntimeDuration(durationMs: number): string {
|
||||||
|
const totalSeconds = Math.max(0, Math.floor((durationMs || 0) / 1000));
|
||||||
|
const minuteSeconds = 60;
|
||||||
|
const hourSeconds = 60 * minuteSeconds;
|
||||||
|
const daySeconds = 24 * hourSeconds;
|
||||||
|
const weekSeconds = 7 * daySeconds;
|
||||||
|
const monthSeconds = 30 * daySeconds;
|
||||||
|
|
||||||
|
const formatUnit = (value: number, singular: string, plural: string, padTo = 0): string => {
|
||||||
|
const normalized = Math.max(0, Math.floor(value));
|
||||||
|
const text = padTo > 0 ? String(normalized).padStart(padTo, "0") : String(normalized);
|
||||||
|
return `${text} ${normalized === 1 ? singular : plural}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (totalSeconds < hourSeconds) {
|
||||||
|
const minutes = Math.floor(totalSeconds / minuteSeconds);
|
||||||
|
const seconds = totalSeconds % minuteSeconds;
|
||||||
|
return `${formatUnit(minutes, "Minute", "Minuten")}, ${formatUnit(seconds, "Sekunde", "Sekunden", 2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalSeconds < daySeconds) {
|
||||||
|
const hours = Math.floor(totalSeconds / hourSeconds);
|
||||||
|
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
|
||||||
|
return `${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten", 2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalSeconds < weekSeconds) {
|
||||||
|
const days = Math.floor(totalSeconds / daySeconds);
|
||||||
|
const hours = Math.floor((totalSeconds % daySeconds) / hourSeconds);
|
||||||
|
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
|
||||||
|
return `${formatUnit(days, "Tag", "Tage")}, ${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalSeconds < monthSeconds) {
|
||||||
|
const weeks = Math.floor(totalSeconds / weekSeconds);
|
||||||
|
const days = Math.floor((totalSeconds % weekSeconds) / daySeconds);
|
||||||
|
const hours = Math.floor((totalSeconds % daySeconds) / hourSeconds);
|
||||||
|
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
|
||||||
|
return `${formatUnit(weeks, "Woche", "Wochen")}, ${formatUnit(days, "Tag", "Tage")}, ${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const months = Math.floor(totalSeconds / monthSeconds);
|
||||||
|
const weeks = Math.floor((totalSeconds % monthSeconds) / weekSeconds);
|
||||||
|
const days = Math.floor((totalSeconds % weekSeconds) / daySeconds);
|
||||||
|
const hours = Math.floor((totalSeconds % daySeconds) / hourSeconds);
|
||||||
|
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
|
||||||
|
return `${formatUnit(months, "Monat", "Monate")}, ${formatUnit(weeks, "Woche", "Wochen")}, ${formatUnit(days, "Tag", "Tage")}, ${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten")}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatAllDebridSourceLabel(source: AllDebridHostInfo["source"]): string {
|
function formatAllDebridSourceLabel(source: AllDebridHostInfo["source"]): string {
|
||||||
return source === "web" ? "Web-Login" : "API-Key";
|
return source === "web" ? "Web-Login" : "API-Key";
|
||||||
}
|
}
|
||||||
@ -1317,6 +1370,7 @@ export function App(): ReactElement {
|
|||||||
const [schedulePickerOpen, setSchedulePickerOpen] = useState(false);
|
const [schedulePickerOpen, setSchedulePickerOpen] = useState(false);
|
||||||
const [scheduleTimeInput, setScheduleTimeInput] = useState("");
|
const [scheduleTimeInput, setScheduleTimeInput] = useState("");
|
||||||
const [scheduleCountdown, setScheduleCountdown] = useState("");
|
const [scheduleCountdown, setScheduleCountdown] = useState("");
|
||||||
|
const [runtimeNow, setRuntimeNow] = useState(() => Date.now());
|
||||||
const settingsDirtyRef = useRef(false);
|
const settingsDirtyRef = useRef(false);
|
||||||
const settingsDraftRevisionRef = useRef(0);
|
const settingsDraftRevisionRef = useRef(0);
|
||||||
const panelDirtyRevisionRef = useRef(0);
|
const panelDirtyRevisionRef = useRef(0);
|
||||||
@ -1535,6 +1589,11 @@ export function App(): ReactElement {
|
|||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [snapshot.settings.scheduledStartEpochMs]);
|
}, [snapshot.settings.scheduledStartEpochMs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => setRuntimeNow(Date.now()), 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const showToast = useCallback((message: string, timeoutMs = 2200): void => {
|
const showToast = useCallback((message: string, timeoutMs = 2200): void => {
|
||||||
setStatusToast(message);
|
setStatusToast(message);
|
||||||
if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); }
|
if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); }
|
||||||
@ -3716,6 +3775,12 @@ export function App(): ReactElement {
|
|||||||
return Object.entries(stats);
|
return Object.entries(stats);
|
||||||
}, [snapshot.session.items]);
|
}, [snapshot.session.items]);
|
||||||
|
|
||||||
|
const runtimeOffsetMs = snapshot.stats.runtimeMeasuredAt > 0
|
||||||
|
? Math.max(0, runtimeNow - snapshot.stats.runtimeMeasuredAt)
|
||||||
|
: 0;
|
||||||
|
const liveSessionRuntimeMs = Math.max(0, (snapshot.stats.sessionRuntimeMs || 0) + runtimeOffsetMs);
|
||||||
|
const liveTotalRuntimeMs = Math.max(0, (snapshot.stats.totalRuntimeMs || 0) + runtimeOffsetMs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`app-shell${dragOver ? " drag-over" : ""}`}
|
className={`app-shell${dragOver ? " drag-over" : ""}`}
|
||||||
@ -4370,6 +4435,14 @@ export function App(): ReactElement {
|
|||||||
<span className="stat-label">Heruntergeladen (Gesamt)</span>
|
<span className="stat-label">Heruntergeladen (Gesamt)</span>
|
||||||
<span className="stat-value">{humanSize(snapshot.stats.totalDownloadedAllTime)}</span>
|
<span className="stat-value">{humanSize(snapshot.stats.totalDownloadedAllTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="stat-item">
|
||||||
|
<span className="stat-label">Laufzeit (Session)</span>
|
||||||
|
<span className="stat-value">{formatRuntimeDuration(liveSessionRuntimeMs)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="stat-item">
|
||||||
|
<span className="stat-label">Laufzeit (Gesamt)</span>
|
||||||
|
<span className="stat-value">{formatRuntimeDuration(liveTotalRuntimeMs)}</span>
|
||||||
|
</div>
|
||||||
<div className="stat-item">
|
<div className="stat-item">
|
||||||
<span className="stat-label">Fertige Dateien (Gesamt)</span>
|
<span className="stat-label">Fertige Dateien (Gesamt)</span>
|
||||||
<span className="stat-value">{snapshot.stats.totalFilesAllTime}</span>
|
<span className="stat-value">{snapshot.stats.totalFilesAllTime}</span>
|
||||||
|
|||||||
@ -45,6 +45,10 @@ export interface DownloadStats {
|
|||||||
totalFilesAllTime: number;
|
totalFilesAllTime: number;
|
||||||
totalPackages: number;
|
totalPackages: number;
|
||||||
sessionStartedAt: number;
|
sessionStartedAt: number;
|
||||||
|
appSessionStartedAt: number;
|
||||||
|
sessionRuntimeMs: number;
|
||||||
|
totalRuntimeMs: number;
|
||||||
|
runtimeMeasuredAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
@ -110,6 +114,7 @@ export interface AppSettings {
|
|||||||
confirmDeleteSelection: boolean;
|
confirmDeleteSelection: boolean;
|
||||||
totalDownloadedAllTime: number;
|
totalDownloadedAllTime: number;
|
||||||
totalCompletedFilesAllTime: number;
|
totalCompletedFilesAllTime: number;
|
||||||
|
totalRuntimeAllTimeMs: number;
|
||||||
bandwidthSchedules: BandwidthScheduleEntry[];
|
bandwidthSchedules: BandwidthScheduleEntry[];
|
||||||
columnOrder: string[];
|
columnOrder: string[];
|
||||||
extractCpuPriority: ExtractCpuPriority;
|
extractCpuPriority: ExtractCpuPriority;
|
||||||
|
|||||||
@ -173,7 +173,11 @@ function buildSnapshot(baseDir: string): UiSnapshot {
|
|||||||
totalFilesSession: 0,
|
totalFilesSession: 0,
|
||||||
totalFilesAllTime: 0,
|
totalFilesAllTime: 0,
|
||||||
totalPackages: 1,
|
totalPackages: 1,
|
||||||
sessionStartedAt: Date.now() - 30_000
|
sessionStartedAt: Date.now() - 30_000,
|
||||||
|
appSessionStartedAt: Date.now() - 60_000,
|
||||||
|
sessionRuntimeMs: 60_000,
|
||||||
|
totalRuntimeMs: 3 * 60_000,
|
||||||
|
runtimeMeasuredAt: Date.now()
|
||||||
},
|
},
|
||||||
speedText: "8.0 MB/s",
|
speedText: "8.0 MB/s",
|
||||||
etaText: "ETA: 00:25",
|
etaText: "ETA: 00:25",
|
||||||
@ -209,7 +213,8 @@ async function createFixture() {
|
|||||||
debridLinkApiKeys,
|
debridLinkApiKeys,
|
||||||
debridLinkDisabledKeyIds: debridLinkKeyIds[1] ? [debridLinkKeyIds[1]] : [],
|
debridLinkDisabledKeyIds: debridLinkKeyIds[1] ? [debridLinkKeyIds[1]] : [],
|
||||||
totalDownloadedAllTime: 128 * 1024 * 1024,
|
totalDownloadedAllTime: 128 * 1024 * 1024,
|
||||||
totalCompletedFilesAllTime: 12
|
totalCompletedFilesAllTime: 12,
|
||||||
|
totalRuntimeAllTimeMs: 5 * 60_000
|
||||||
});
|
});
|
||||||
saveHistory(storagePaths, [
|
saveHistory(storagePaths, [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6709,6 +6709,36 @@ describe("download manager", () => {
|
|||||||
expect(fs.existsSync(originalExtractedPath)).toBe(false);
|
expect(fs.existsSync(originalExtractedPath)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("tracks app runtime for session and all-time statistics", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
const stateDir = path.join(root, "state");
|
||||||
|
const storagePaths = createStoragePaths(stateDir);
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "rd-token",
|
||||||
|
outputDir: path.join(root, "downloads"),
|
||||||
|
extractDir: path.join(root, "extract"),
|
||||||
|
totalRuntimeAllTimeMs: 2 * 60 * 60 * 1000
|
||||||
|
},
|
||||||
|
emptySession(),
|
||||||
|
storagePaths
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||||
|
|
||||||
|
const stats = manager.getStats();
|
||||||
|
expect(stats.sessionRuntimeMs).toBeGreaterThanOrEqual(100);
|
||||||
|
expect(stats.totalRuntimeMs).toBeGreaterThanOrEqual(2 * 60 * 60 * 1000 + 100);
|
||||||
|
expect(stats.runtimeMeasuredAt).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
manager.persistRuntimeStats(true);
|
||||||
|
const savedSettings = JSON.parse(fs.readFileSync(storagePaths.configFile, "utf8")) as { totalRuntimeAllTimeMs?: number };
|
||||||
|
expect(savedSettings.totalRuntimeAllTimeMs || 0).toBeGreaterThanOrEqual(2 * 60 * 60 * 1000 + 100);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
it("writes auto-rename details into rename and item logs", async () => {
|
it("writes auto-rename details into rename and item logs", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
@ -115,7 +115,11 @@ function buildSnapshot(): UiSnapshot {
|
|||||||
totalFilesSession: 0,
|
totalFilesSession: 0,
|
||||||
totalFilesAllTime: 0,
|
totalFilesAllTime: 0,
|
||||||
totalPackages: 2,
|
totalPackages: 2,
|
||||||
sessionStartedAt: 0
|
sessionStartedAt: 0,
|
||||||
|
appSessionStartedAt: 0,
|
||||||
|
sessionRuntimeMs: 0,
|
||||||
|
totalRuntimeMs: 0,
|
||||||
|
runtimeMeasuredAt: 0
|
||||||
},
|
},
|
||||||
speedText: "",
|
speedText: "",
|
||||||
etaText: "",
|
etaText: "",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user