diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx
index d1fe92b..c576caf 100644
--- a/src/components/UserMenu.tsx
+++ b/src/components/UserMenu.tsx
@@ -124,6 +124,7 @@ export const UserMenu: React.FC = () => {
const [danmakuHeatmapDisabled, setDanmakuHeatmapDisabled] = useState(false);
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false);
const [exactSearch, setExactSearch] = useState(true);
+ const [maxConcurrentDownloads, setMaxConcurrentDownloads] = useState(6);
// 邮件通知设置
const [userEmail, setUserEmail] = useState('');
@@ -479,6 +480,12 @@ export const UserMenu: React.FC = () => {
if (savedExactSearch !== null) {
setExactSearch(savedExactSearch === 'true');
}
+
+ // 加载最大同时下载限制设置
+ const savedMaxConcurrentDownloads = localStorage.getItem('maxConcurrentDownloads');
+ if (savedMaxConcurrentDownloads !== null) {
+ setMaxConcurrentDownloads(Number(savedMaxConcurrentDownloads));
+ }
}
}, []);
@@ -820,6 +827,13 @@ export const UserMenu: React.FC = () => {
}
};
+ const handleMaxConcurrentDownloadsChange = (value: number) => {
+ setMaxConcurrentDownloads(value);
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('maxConcurrentDownloads', String(value));
+ }
+ };
+
const handleFluidSearchToggle = (value: boolean) => {
setFluidSearch(value);
if (typeof window !== 'undefined') {
@@ -1937,6 +1951,69 @@ export const UserMenu: React.FC = () => {
+
+ {/* 分割线 */}
+
+
+ {/* 最大同时下载限制 */}
+
+
+
+ 最大同时下载限制
+
+
+ 控制播放页面下载时的同时下载数量
+
+
+
+
+ 同时下载数量
+
+
+ {maxConcurrentDownloads}个
+
+
+
+ handleMaxConcurrentDownloadsChange(Number(e.target.value))}
+ className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700'
+ style={{
+ background: `linear-gradient(to right, #10b981 0%, #10b981 ${((maxConcurrentDownloads - 1) / (10 - 1)) * 100}%, #e5e7eb ${((maxConcurrentDownloads - 1) / (10 - 1)) * 100}%, #e5e7eb 100%)`
+ }}
+ />
+
+
+
+
+
+
+
+
)}
diff --git a/src/contexts/DownloadContext.tsx b/src/contexts/DownloadContext.tsx
index b0bb1ab..93d8db4 100644
--- a/src/contexts/DownloadContext.tsx
+++ b/src/contexts/DownloadContext.tsx
@@ -21,28 +21,85 @@ interface DownloadContextType {
const DownloadContext = createContext(undefined);
export function DownloadProvider({ children }: { children: React.ReactNode }) {
+ const [tasks, setTasks] = useState([]);
+ const [showDownloadPanel, setShowDownloadPanel] = useState(false);
+ const [startingTaskIds, setStartingTaskIds] = useState>(new Set());
+
+ // 自动启动下一个等待的任务
+ const startNextPendingTask = useCallback((currentDownloader: M3U8Downloader) => {
+ // 从localStorage读取最大同时下载限制,默认6个
+ const maxConcurrentDownloads = typeof window !== 'undefined'
+ ? Number(localStorage.getItem('maxConcurrentDownloads') || 6)
+ : 6;
+
+ const allTasks = currentDownloader.getAllTasks();
+ const downloadingCount = allTasks.filter(t => t.status === 'downloading').length;
+
+ // 如果当前下载数量小于限制,启动下一个ready任务
+ if (downloadingCount < maxConcurrentDownloads) {
+ const readyTask = allTasks.find(t => t.status === 'ready');
+ if (readyTask) {
+ currentDownloader.startTask(readyTask.id);
+ setTasks(currentDownloader.getAllTasks());
+ }
+ }
+ }, []);
+
const [downloader] = useState(() => new M3U8Downloader({
onProgress: (task) => {
setTasks(downloader.getAllTasks());
},
onComplete: (task) => {
setTasks(downloader.getAllTasks());
+ // 任务完成后,尝试启动下一个等待的任务
+ startNextPendingTask(downloader);
},
onError: (task, error) => {
console.error('下载错误:', error);
setTasks(downloader.getAllTasks());
+ // 任务出错后,尝试启动下一个等待的任务
+ startNextPendingTask(downloader);
},
}));
- const [tasks, setTasks] = useState([]);
- const [showDownloadPanel, setShowDownloadPanel] = useState(false);
-
const addDownloadTask = useCallback(async (url: string, title: string, type: 'TS' | 'MP4' = 'TS') => {
try {
const taskId = await downloader.createTask(url, title, type);
setTasks(downloader.getAllTasks());
- await downloader.startTask(taskId);
- setTasks(downloader.getAllTasks());
+
+ // 从localStorage读取最大同时下载限制,默认6个
+ const maxConcurrentDownloads = typeof window !== 'undefined'
+ ? Number(localStorage.getItem('maxConcurrentDownloads') || 6)
+ : 6;
+
+ // 检查当前正在下载的任务数量(包括正在启动的)
+ setStartingTaskIds(prev => {
+ const allTasks = downloader.getAllTasks();
+ const currentDownloadingCount = allTasks.filter(t => t.status === 'downloading').length;
+ const totalActiveCount = currentDownloadingCount + prev.size;
+
+ // 如果未超过限制,标记为正在启动并启动任务
+ if (totalActiveCount < maxConcurrentDownloads) {
+ const newSet = new Set(prev);
+ newSet.add(taskId);
+
+ // 异步启动任务
+ downloader.startTask(taskId).then(() => {
+ setTasks(downloader.getAllTasks());
+ // 启动完成后,从正在启动列表中移除
+ setStartingTaskIds(current => {
+ const updated = new Set(current);
+ updated.delete(taskId);
+ return updated;
+ });
+ });
+
+ return newSet;
+ }
+
+ // 否则任务保持ready状态,等待其他任务完成后自动启动
+ return prev;
+ });
} catch (error) {
console.error('添加下载任务失败:', error);
throw error;
@@ -50,19 +107,33 @@ export function DownloadProvider({ children }: { children: React.ReactNode }) {
}, [downloader]);
const startTask = useCallback((taskId: string) => {
- downloader.startTask(taskId);
- setTasks(downloader.getAllTasks());
+ // 从localStorage读取最大同时下载限制,默认6个
+ const maxConcurrentDownloads = typeof window !== 'undefined'
+ ? Number(localStorage.getItem('maxConcurrentDownloads') || 6)
+ : 6;
+
+ const currentDownloadingCount = downloader.getAllTasks().filter(t => t.status === 'downloading').length;
+
+ // 如果未超过限制,启动任务
+ if (currentDownloadingCount < maxConcurrentDownloads) {
+ downloader.startTask(taskId);
+ setTasks(downloader.getAllTasks());
+ }
}, [downloader]);
const pauseTask = useCallback((taskId: string) => {
downloader.pauseTask(taskId);
setTasks(downloader.getAllTasks());
- }, [downloader]);
+ // 暂停任务后,尝试启动下一个等待的任务
+ startNextPendingTask(downloader);
+ }, [downloader, startNextPendingTask]);
const cancelTask = useCallback((taskId: string) => {
downloader.cancelTask(taskId);
setTasks(downloader.getAllTasks());
- }, [downloader]);
+ // 取消任务后,尝试启动下一个等待的任务
+ startNextPendingTask(downloader);
+ }, [downloader, startNextPendingTask]);
const retryFailedSegments = useCallback((taskId: string) => {
downloader.retryFailedSegments(taskId);