refactor: Configure QueryClient with memory management settings
This commit is contained in:
@@ -123,7 +123,23 @@ function AppContent() {
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
// Configure QueryClient with memory management settings
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Cache data for 5 minutes by default
|
||||
staleTime: 5 * 60 * 1000,
|
||||
// Keep unused data in cache for 10 minutes before garbage collection
|
||||
gcTime: 10 * 60 * 1000, // Previously called cacheTime
|
||||
// Retry failed requests 3 times instead of default
|
||||
retry: 3,
|
||||
// Refetch on window focus only if data is stale
|
||||
refetchOnWindowFocus: false,
|
||||
// Don't refetch on reconnect by default
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
||||
@@ -93,9 +93,11 @@ const Header: React.FC<HeaderProps> = ({
|
||||
};
|
||||
|
||||
checkActiveSubscriptions();
|
||||
// Poll every 10 seconds to update indicator
|
||||
const interval = setInterval(checkActiveSubscriptions, 10000);
|
||||
return () => clearInterval(interval);
|
||||
// Poll every 30 seconds to update indicator (reduced frequency)
|
||||
const interval = setInterval(checkActiveSubscriptions, 30000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [visitorMode]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
@@ -75,8 +75,25 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
setIsVideoPlaying(false);
|
||||
// Cleanup video element when mouse leaves
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
videoRef.current.src = '';
|
||||
videoRef.current.load();
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup video element on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
videoRef.current.src = '';
|
||||
videoRef.current.load();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Format the date (assuming format YYYYMMDD from youtube-dl)
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString || dateString.length !== 8) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Alert, Box, Divider, Stack } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { useVideoResolution } from '../../hooks/useVideoResolution';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useCloudStorageUrl } from '../../hooks/useCloudStorageUrl';
|
||||
import { useVideoResolution } from '../../hooks/useVideoResolution';
|
||||
import { Collection, Video } from '../../types';
|
||||
import EditableTitle from './VideoInfo/EditableTitle';
|
||||
import VideoActionButtons from './VideoInfo/VideoActionButtons';
|
||||
@@ -53,6 +53,18 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
|
||||
const { videoRef, videoResolution } = useVideoResolution(video);
|
||||
const videoUrl = useCloudStorageUrl(video.videoPath, 'video');
|
||||
|
||||
// Cleanup video element on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const videoElement = videoRef.current;
|
||||
if (videoElement) {
|
||||
// Clear video source to free memory
|
||||
videoElement.src = '';
|
||||
videoElement.load();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{/* Hidden video element to get resolution */}
|
||||
|
||||
@@ -82,8 +82,17 @@ export const DownloadProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
||||
const response = await axios.get(`${API_URL}/download-status`);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 2000,
|
||||
initialData: initialStatus || { activeDownloads: [], queuedDownloads: [] }
|
||||
// Only poll when there are active or queued downloads
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data as { activeDownloads?: any[]; queuedDownloads?: any[] } | undefined;
|
||||
const hasActive = (data?.activeDownloads?.length ?? 0) > 0;
|
||||
const hasQueued = (data?.queuedDownloads?.length ?? 0) > 0;
|
||||
// Poll every 2 seconds if there are downloads, otherwise stop polling
|
||||
return hasActive || hasQueued ? 2000 : false;
|
||||
},
|
||||
initialData: initialStatus || { activeDownloads: [], queuedDownloads: [] },
|
||||
staleTime: 1000, // Consider data stale after 1 second
|
||||
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes
|
||||
});
|
||||
|
||||
const activeDownloads = downloadStatus.activeDownloads || [];
|
||||
|
||||
@@ -66,8 +66,10 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
retry: 10,
|
||||
retry: 3, // Reduced from 10 to 3
|
||||
retryDelay: 1000,
|
||||
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
|
||||
gcTime: 30 * 60 * 1000, // Garbage collect after 30 minutes (videos are important data)
|
||||
});
|
||||
|
||||
// Filter invisible videos when in visitor mode
|
||||
@@ -85,8 +87,10 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
const response = await axios.get(`${API_URL}/settings`);
|
||||
return response.data;
|
||||
},
|
||||
retry: 10,
|
||||
retry: 3, // Reduced from 10 to 3
|
||||
retryDelay: 1000,
|
||||
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
|
||||
gcTime: 15 * 60 * 1000, // Garbage collect after 15 minutes
|
||||
});
|
||||
|
||||
const availableTags = settingsData?.tags || [];
|
||||
|
||||
@@ -21,7 +21,9 @@ export const VisitorModeProvider: React.FC<{ children: ReactNode }> = ({ childre
|
||||
const response = await axios.get(`${API_URL}/settings`);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 5000, // Refetch every 5 seconds to keep visitor mode state in sync
|
||||
refetchInterval: 30000, // Refetch every 30 seconds (reduced frequency)
|
||||
staleTime: 10000, // Consider data fresh for 10 seconds
|
||||
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
|
||||
});
|
||||
|
||||
const visitorMode = settingsData?.visitorMode === true;
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
interface CloudflareStatus {
|
||||
isRunning: boolean;
|
||||
tunnelId: string | null;
|
||||
accountTag: string | null;
|
||||
publicUrl: string | null;
|
||||
isRunning: boolean;
|
||||
tunnelId: string | null;
|
||||
accountTag: string | null;
|
||||
publicUrl: string | null;
|
||||
}
|
||||
|
||||
export const useCloudflareStatus = (enabled: boolean = true) => {
|
||||
return useQuery<CloudflareStatus>({
|
||||
queryKey: ['cloudflaredStatus'],
|
||||
queryFn: async () => {
|
||||
if (!enabled) return { isRunning: false, tunnelId: null, accountTag: null, publicUrl: null };
|
||||
const res = await axios.get(`${API_URL}/settings/cloudflared/status`);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!enabled,
|
||||
refetchInterval: 5000 // Poll every 5 seconds
|
||||
});
|
||||
return useQuery<CloudflareStatus>({
|
||||
queryKey: ["cloudflaredStatus"],
|
||||
queryFn: async () => {
|
||||
if (!enabled)
|
||||
return {
|
||||
isRunning: false,
|
||||
tunnelId: null,
|
||||
accountTag: null,
|
||||
publicUrl: null,
|
||||
};
|
||||
const res = await axios.get(`${API_URL}/settings/cloudflared/status`);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!enabled,
|
||||
refetchInterval: enabled ? 10000 : false, // Poll every 10 seconds when enabled (reduced frequency)
|
||||
staleTime: 5000, // Consider data fresh for 5 seconds
|
||||
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,112 +1,115 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Video } from '../types';
|
||||
import { useCloudStorageUrl } from './useCloudStorageUrl';
|
||||
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Video } from "../types";
|
||||
import { useCloudStorageUrl } from "./useCloudStorageUrl";
|
||||
|
||||
// Format video resolution from video object
|
||||
export const formatResolution = (video: Video): string | null => {
|
||||
// Check if resolution is directly available
|
||||
if (video.resolution) {
|
||||
const res = String(video.resolution).toUpperCase();
|
||||
// If it's already formatted like "720P", return it
|
||||
if (res.match(/^\d+P$/)) {
|
||||
return res;
|
||||
}
|
||||
// If it's "4K" or similar, return it
|
||||
if (res.match(/^\d+K$/)) {
|
||||
return res;
|
||||
}
|
||||
// Check if resolution is directly available
|
||||
if (video.resolution) {
|
||||
const res = String(video.resolution).toUpperCase();
|
||||
// If it's already formatted like "720P", return it
|
||||
if (res.match(/^\d+P$/)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// Check if width and height are available
|
||||
const width = (video as any).width;
|
||||
const height = (video as any).height;
|
||||
|
||||
if (width && height) {
|
||||
const h = typeof height === 'number' ? height : parseInt(String(height));
|
||||
if (!isNaN(h)) {
|
||||
if (h >= 2160) return '4K';
|
||||
if (h >= 1440) return '1440P';
|
||||
if (h >= 1080) return '1080P';
|
||||
if (h >= 720) return '720P';
|
||||
if (h >= 480) return '480P';
|
||||
if (h >= 360) return '360P';
|
||||
if (h >= 240) return '240P';
|
||||
if (h >= 144) return '144P';
|
||||
}
|
||||
// If it's "4K" or similar, return it
|
||||
if (res.match(/^\d+K$/)) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a format_id or format that might contain resolution info
|
||||
const formatId = (video as any).format_id;
|
||||
if (formatId && typeof formatId === 'string') {
|
||||
const match = formatId.match(/(\d+)p/i);
|
||||
if (match) {
|
||||
return match[1].toUpperCase() + 'P';
|
||||
}
|
||||
// Check if width and height are available
|
||||
const width = (video as any).width;
|
||||
const height = (video as any).height;
|
||||
|
||||
if (width && height) {
|
||||
const h = typeof height === "number" ? height : parseInt(String(height));
|
||||
if (!isNaN(h)) {
|
||||
if (h >= 2160) return "4K";
|
||||
if (h >= 1440) return "1440P";
|
||||
if (h >= 1080) return "1080P";
|
||||
if (h >= 720) return "720P";
|
||||
if (h >= 480) return "480P";
|
||||
if (h >= 360) return "360P";
|
||||
if (h >= 240) return "240P";
|
||||
if (h >= 144) return "144P";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// Check if there's a format_id or format that might contain resolution info
|
||||
const formatId = (video as any).format_id;
|
||||
if (formatId && typeof formatId === "string") {
|
||||
const match = formatId.match(/(\d+)p/i);
|
||||
if (match) {
|
||||
return match[1].toUpperCase() + "P";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useVideoResolution = (video: Video) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [detectedResolution, setDetectedResolution] = useState<string | null>(null);
|
||||
const videoUrl = useCloudStorageUrl(video.videoPath, 'video');
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [detectedResolution, setDetectedResolution] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const videoUrl = useCloudStorageUrl(video.videoPath, "video");
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current;
|
||||
const videoSrc = videoUrl || video.sourceUrl;
|
||||
if (!videoElement || !videoSrc) {
|
||||
setDetectedResolution(null);
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current;
|
||||
const videoSrc = videoUrl || video.sourceUrl;
|
||||
if (!videoElement || !videoSrc) {
|
||||
setDetectedResolution(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the video source
|
||||
if (videoElement.src !== videoSrc) {
|
||||
videoElement.src = videoSrc;
|
||||
videoElement.load(); // Force reload
|
||||
}
|
||||
// Set the video source
|
||||
if (videoElement.src !== videoSrc) {
|
||||
videoElement.src = videoSrc;
|
||||
videoElement.load(); // Force reload
|
||||
}
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
const height = videoElement.videoHeight;
|
||||
if (height && height > 0) {
|
||||
if (height >= 2160) setDetectedResolution('4K');
|
||||
else if (height >= 1440) setDetectedResolution('1440P');
|
||||
else if (height >= 1080) setDetectedResolution('1080P');
|
||||
else if (height >= 720) setDetectedResolution('720P');
|
||||
else if (height >= 480) setDetectedResolution('480P');
|
||||
else if (height >= 360) setDetectedResolution('360P');
|
||||
else if (height >= 240) setDetectedResolution('240P');
|
||||
else if (height >= 144) setDetectedResolution('144P');
|
||||
else setDetectedResolution(null);
|
||||
} else {
|
||||
setDetectedResolution(null);
|
||||
}
|
||||
};
|
||||
const handleLoadedMetadata = () => {
|
||||
const height = videoElement.videoHeight;
|
||||
if (height && height > 0) {
|
||||
if (height >= 2160) setDetectedResolution("4K");
|
||||
else if (height >= 1440) setDetectedResolution("1440P");
|
||||
else if (height >= 1080) setDetectedResolution("1080P");
|
||||
else if (height >= 720) setDetectedResolution("720P");
|
||||
else if (height >= 480) setDetectedResolution("480P");
|
||||
else if (height >= 360) setDetectedResolution("360P");
|
||||
else if (height >= 240) setDetectedResolution("240P");
|
||||
else if (height >= 144) setDetectedResolution("144P");
|
||||
else setDetectedResolution(null);
|
||||
} else {
|
||||
setDetectedResolution(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setDetectedResolution(null);
|
||||
};
|
||||
const handleError = () => {
|
||||
setDetectedResolution(null);
|
||||
};
|
||||
|
||||
videoElement.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
videoElement.addEventListener('error', handleError);
|
||||
|
||||
// If metadata is already loaded
|
||||
if (videoElement.readyState >= 1 && videoElement.videoHeight > 0) {
|
||||
handleLoadedMetadata();
|
||||
}
|
||||
videoElement.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
videoElement.addEventListener("error", handleError);
|
||||
|
||||
return () => {
|
||||
videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
videoElement.removeEventListener('error', handleError);
|
||||
};
|
||||
}, [videoUrl, video.sourceUrl, video.id]);
|
||||
// If metadata is already loaded
|
||||
if (videoElement.readyState >= 1 && videoElement.videoHeight > 0) {
|
||||
handleLoadedMetadata();
|
||||
}
|
||||
|
||||
// Try to get resolution from video object first, fallback to detected resolution
|
||||
const resolutionFromObject = formatResolution(video);
|
||||
const videoResolution = resolutionFromObject || detectedResolution;
|
||||
return () => {
|
||||
// Cleanup: remove event listeners
|
||||
videoElement.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
videoElement.removeEventListener("error", handleError);
|
||||
// Cleanup: clear video source to free memory
|
||||
videoElement.src = "";
|
||||
videoElement.load();
|
||||
};
|
||||
}, [videoUrl, video.sourceUrl, video.id]);
|
||||
|
||||
return { videoRef, videoResolution };
|
||||
// Try to get resolution from video object first, fallback to detected resolution
|
||||
const resolutionFromObject = formatResolution(video);
|
||||
const videoResolution = resolutionFromObject || detectedResolution;
|
||||
|
||||
return { videoRef, videoResolution };
|
||||
};
|
||||
|
||||
|
||||
@@ -49,14 +49,17 @@ const DownloadPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch history with polling
|
||||
// Fetch history with polling - only when on downloads page
|
||||
const { data: history = [] } = useQuery({
|
||||
queryKey: ['downloadHistory'],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get(`${API_URL}/downloads/history`);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 2000
|
||||
// Only poll when tab is active (downloads tab)
|
||||
refetchInterval: tabValue === 0 ? 2000 : false,
|
||||
staleTime: 1000, // Consider data stale after 1 second
|
||||
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes
|
||||
});
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
|
||||
@@ -423,7 +423,9 @@ const SettingsPage: React.FC = () => {
|
||||
const response = await axios.get(`${API_URL}/settings/last-backup-info`);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 30000, // Refetch every 30 seconds
|
||||
refetchInterval: 60000, // Refetch every 60 seconds (reduced frequency)
|
||||
staleTime: 30000, // Consider data fresh for 30 seconds
|
||||
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
|
||||
});
|
||||
|
||||
// Restore from last backup mutation
|
||||
|
||||
@@ -75,6 +75,7 @@ const SubscriptionsPage: React.FC = () => {
|
||||
},
|
||||
refetchInterval: 30000, // Refetch every 30 seconds (less frequent)
|
||||
staleTime: 10000, // Consider data fresh for 10 seconds
|
||||
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
|
||||
});
|
||||
|
||||
const { data: tasks = [], refetch: refetchTasks } = useQuery({
|
||||
@@ -83,8 +84,15 @@ const SubscriptionsPage: React.FC = () => {
|
||||
const response = await axios.get(`${API_URL}/subscriptions/tasks`);
|
||||
return response.data as ContinuousDownloadTask[];
|
||||
},
|
||||
refetchInterval: 10000, // Poll every 10 seconds
|
||||
// Only poll when there are active tasks
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data as ContinuousDownloadTask[] | undefined;
|
||||
const hasActive = data?.some(task => task.status === 'active' || task.status === 'paused') ?? false;
|
||||
// Poll every 10 seconds if there are active tasks, otherwise every 60 seconds
|
||||
return hasActive ? 10000 : 60000;
|
||||
},
|
||||
staleTime: 5000, // Consider data fresh for 5 seconds
|
||||
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
|
||||
});
|
||||
|
||||
const handleUnsubscribeClick = (id: string, author: string) => {
|
||||
|
||||
Reference in New Issue
Block a user