@lzwme/m3u8-dl 1.4.3 → 1.6.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +361 -68
- package/cjs/cli.js +37 -23
- package/cjs/i18n/locales/en.d.ts +108 -0
- package/cjs/i18n/locales/en.js +109 -0
- package/cjs/i18n/locales/zh.d.ts +108 -0
- package/cjs/i18n/locales/zh.js +109 -0
- package/cjs/index.d.ts +1 -1
- package/cjs/index.js +1 -1
- package/cjs/lib/file-download.js +8 -5
- package/cjs/lib/getM3u8Urls.d.ts +6 -0
- package/cjs/lib/getM3u8Urls.js +45 -27
- package/cjs/lib/i18n.d.ts +27 -0
- package/cjs/lib/i18n.js +89 -0
- package/cjs/lib/m3u8-convert.js +5 -4
- package/cjs/lib/m3u8-download.js +37 -11
- package/cjs/lib/utils.d.ts +1 -1
- package/cjs/lib/utils.js +5 -5
- package/cjs/server/download-server.d.ts +1 -0
- package/cjs/server/download-server.js +111 -39
- package/cjs/types/index.d.ts +1 -1
- package/cjs/types/index.js +1 -1
- package/cjs/types/m3u8.d.ts +4 -0
- package/client/assets/main-DYJAIw1q.css +1 -0
- package/client/assets/main-XL0wiaDU.js +25 -0
- package/client/index.html +4 -1144
- package/client/play.html +221 -14
- package/package.json +20 -16
- package/client/style.css +0 -137
package/client/index.html
CHANGED
|
@@ -1,1161 +1,21 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="zh-CN">
|
|
3
|
-
|
|
4
3
|
<head>
|
|
5
4
|
<meta charset="UTF-8">
|
|
6
5
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
7
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
9
8
|
<title>M3U8 下载器</title>
|
|
10
|
-
<link rel="icon" type="image/png" href="logo.png">
|
|
9
|
+
<link rel="icon" type="image/png" href="/logo.png">
|
|
11
10
|
<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
|
12
11
|
integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
|
|
13
12
|
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
14
|
-
<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.css"
|
|
15
|
-
integrity="sha512-WnmDqbbAeHb7Put2nIAp7KNlnMup0FXVviOctducz1omuXB/hHK3s2vd3QLffK/CvvFUKrpioxdo+/Jo3k/xIw=="
|
|
16
|
-
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
17
|
-
<link rel="stylesheet" href="style.css?v={{version}}">
|
|
18
|
-
|
|
19
|
-
<script src="https://cdn.tailwindcss.com/3.4.16"></script>
|
|
20
|
-
<script src="https://s4.zstatic.net/ajax/libs/vue/2.7.16/vue.min.js"
|
|
21
|
-
integrity="sha512-Wx8niGbPNCD87mSuF0sBRytwW2+2ZFr7HwVDF8krCb3egstCc4oQfig+/cfg2OHd82KcUlOYxlSDAqdHqK5TCw=="
|
|
22
|
-
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
23
|
-
<script src="https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.js"
|
|
24
|
-
integrity="sha512-LGHBR+kJ5jZSIzhhdfytPoEHzgaYuTRifq9g5l6ja6/k9NAOsAi5dQh4zQF6JIRB8cAYxTRedERUF+97/KuivQ=="
|
|
25
|
-
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
26
13
|
<script src="https://s4.zstatic.net/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js" crossorigin="anonymous"
|
|
27
14
|
referrerpolicy="no-referrer"></script>
|
|
28
|
-
|
|
15
|
+
<script type="module" crossorigin src="/assets/main-XL0wiaDU.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/main-DYJAIw1q.css">
|
|
29
17
|
</head>
|
|
30
|
-
|
|
31
18
|
<body>
|
|
32
|
-
<div id="app"
|
|
33
|
-
<button class="menu-toggle" @click="toggleSidebar">
|
|
34
|
-
<i class="fas" :class="sidebarCollapsed ? 'fa-bars' : 'fa-times'"></i>
|
|
35
|
-
</button>
|
|
36
|
-
<div class="sidebar p-4" :class="{ 'show': !sidebarCollapsed }">
|
|
37
|
-
<div class="mb-8">
|
|
38
|
-
<div class="flex items-center mb-4">
|
|
39
|
-
<img src="logo.png" alt="M3U8 下载器" class="w-8 h-8 mr-2">
|
|
40
|
-
<h1 class="text-xl font-bold text-gray-800">M3U8 下载器</h1>
|
|
41
|
-
</div>
|
|
42
|
-
<button @click="showNewDownloadDialog"
|
|
43
|
-
class="w-full bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-lg flex items-center justify-center">
|
|
44
|
-
<i class="fas fa-plus mr-2"></i>新建下载
|
|
45
|
-
</button>
|
|
46
|
-
</div>
|
|
47
|
-
<nav class="space-y-2">
|
|
48
|
-
<button @click="switchSection('download')" class="nav-item w-full p-3 rounded-lg flex items-center"
|
|
49
|
-
:class="{ 'active': activeSection === 'download' }">
|
|
50
|
-
<i class="fas fa-download mr-3"></i>下载管理
|
|
51
|
-
</button>
|
|
52
|
-
<button @click="switchSection('config')" class="nav-item w-full p-3 rounded-lg flex items-center"
|
|
53
|
-
:class="{ 'active': activeSection === 'config' }">
|
|
54
|
-
<i class="fas fa-cog mr-3"></i>配置设置
|
|
55
|
-
</button>
|
|
56
|
-
<button @click="switchSection('about')" class="nav-item w-full p-3 rounded-lg flex items-center"
|
|
57
|
-
:class="{ 'active': activeSection === 'about' }">
|
|
58
|
-
<i class="fas fa-info-circle mr-3"></i>关于项目
|
|
59
|
-
</button>
|
|
60
|
-
<!-- 跳转 ariang 链接 -->
|
|
61
|
-
<button @click="switchSection('ariang')" class="nav-item w-full p-3 rounded-lg flex items-center">
|
|
62
|
-
<i class="fas fa-link mr-3"></i>Ariang
|
|
63
|
-
</button>
|
|
64
|
-
</nav>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div class="main-content p-1 md:p-4"
|
|
68
|
-
:style="{ marginLeft: sidebarCollapsed ? '0' : '16rem', width: sidebarCollapsed ? '100%' : 'calc(100% - 16rem)' }">
|
|
69
|
-
<div v-if="activeSection === 'download'" class="space-y-6">
|
|
70
|
-
<div class="bg-white rounded-lg shadow">
|
|
71
|
-
<div class="p-4">
|
|
72
|
-
<div class="flex justify-between items-center">
|
|
73
|
-
<h2 class="text-xl font-semibold">下载任务</h2>
|
|
74
|
-
<div class="flex space-x-2">
|
|
75
|
-
<button @click="showNewDownloadDialog"
|
|
76
|
-
class="px-3 py-1 text-sm bg-blue-500 hover:bg-blue-600 text-white rounded">
|
|
77
|
-
<i class="fas fa-plus mr-1"></i>新建
|
|
78
|
-
</button>
|
|
79
|
-
<button v-if="selectedTasks.length > 0" @click="pauseDownload(selectedTasks)"
|
|
80
|
-
class="px-3 py-1 text-sm bg-yellow-500 text-white rounded hover:bg-yellow-600">
|
|
81
|
-
<i class="fas fa-pause mr-1"></i>暂停选中
|
|
82
|
-
</button>
|
|
83
|
-
<button v-if="selectedTasks.length > 0" @click="resumeDownload(selectedTasks)"
|
|
84
|
-
class="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600">
|
|
85
|
-
<i class="fas fa-play mr-1"></i>开始选中
|
|
86
|
-
</button>
|
|
87
|
-
<button v-if="selectedTasks.length > 0" @click="deleteDownload(selectedTasks)"
|
|
88
|
-
class="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600">
|
|
89
|
-
<i class="fas fa-trash mr-1"></i>删除选中
|
|
90
|
-
</button>
|
|
91
|
-
<button v-if="selectedTasks.length === 0" @click="pauseDownload('all')"
|
|
92
|
-
class="px-3 py-1 text-sm bg-yellow-500 text-white rounded hover:bg-yellow-600">
|
|
93
|
-
<i class="fas fa-pause mr-1"></i>全部暂停
|
|
94
|
-
</button>
|
|
95
|
-
<button v-if="selectedTasks.length === 0" @click="resumeDownload('all')"
|
|
96
|
-
class="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600">
|
|
97
|
-
<i class="fas fa-play mr-1"></i>全部开始
|
|
98
|
-
</button>
|
|
99
|
-
<button v-if="selectedTasks.length === 0" @click="clearQueue"
|
|
100
|
-
class="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600">
|
|
101
|
-
<i class="fas fa-trash mr-1"></i>清空队列
|
|
102
|
-
</button>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
<div class="mt-4 flex items-center space-x-4">
|
|
106
|
-
<div class="flex-1">
|
|
107
|
-
<div class="relative">
|
|
108
|
-
<input type="text" v-model="searchQuery"
|
|
109
|
-
class="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
|
110
|
-
placeholder="搜索任务名称或URL" aria-label="搜索任务">
|
|
111
|
-
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
<div class="flex items-center space-x-2">
|
|
115
|
-
<select v-model="statusFilter"
|
|
116
|
-
class="px-3 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500" title="按状态筛选"
|
|
117
|
-
aria-label="按状态筛选">
|
|
118
|
-
<option value="">全部状态</option>
|
|
119
|
-
<option value="resume">下载中</option>
|
|
120
|
-
<option value="pending">等待中</option>
|
|
121
|
-
<option value="pause">已暂停</option>
|
|
122
|
-
<option value="error">异常</option>
|
|
123
|
-
<option value="done">已完成</option>
|
|
124
|
-
</select>
|
|
125
|
-
<button @click="clearFilters" class="px-3 py-2 text-gray-600 hover:text-gray-800" title="清除筛选条件"
|
|
126
|
-
aria-label="清除筛选条件">
|
|
127
|
-
<i class="fas fa-times"></i>
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
<div class="text-sm text-gray-600 border-b bg-gray-50">
|
|
134
|
-
<div class="flex items-center space-x-4 px-4 py-2">
|
|
135
|
-
<!-- 全选/反选复选框 -->
|
|
136
|
-
<div class="flex items-center">
|
|
137
|
-
<input type="checkbox"
|
|
138
|
-
:checked="selectedTasks.length === filteredTasks.length && filteredTasks.length > 0"
|
|
139
|
-
:indeterminate.prop="selectedTasks.length > 0 && selectedTasks.length < filteredTasks.length"
|
|
140
|
-
@change="toggleSelectAll" class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500 mr-2"
|
|
141
|
-
title="全选/反选">
|
|
142
|
-
<span class="text-gray-700 text-sm">全选/反选</span>
|
|
143
|
-
<span class="ml-4 text-gray-400 text-xs">已选 {{selectedTasks.length}} / {{filteredTasks.length}}</span>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<div class="flex items-center">
|
|
147
|
-
<i class="fas fa-tasks text-blue-600 mr-1"></i>
|
|
148
|
-
<span>总数: {{ filteredTasks.length }}</span>
|
|
149
|
-
</div>
|
|
150
|
-
<div class="flex items-center">
|
|
151
|
-
<i class="fas fa-clock text-yellow-500 mr-1"></i>
|
|
152
|
-
<span>等待中: {{ queueStatus.queueLength }}</span>
|
|
153
|
-
</div>
|
|
154
|
-
<div class="flex items-center">
|
|
155
|
-
<i class="fas fa-download text-green-500 mr-1"></i>
|
|
156
|
-
<span>下载中: {{ queueStatus.activeDownloads.length }}</span>
|
|
157
|
-
</div>
|
|
158
|
-
<!-- <div class="flex items-center">
|
|
159
|
-
<i class="fas fa-sliders-h text-blue-500 mr-1"></i>
|
|
160
|
-
<span>最大并发: {{ queueStatus.maxConcurrent }}</span>
|
|
161
|
-
</div> -->
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
<div class="divide-y overflow-auto max-h-[calc(100vh-200px)]">
|
|
166
|
-
<div v-for="task in filteredTasks" :key="task.url" class="download-item p-4 hover:bg-gray-50 relative">
|
|
167
|
-
<div class="flex items-center justify-between mb-2">
|
|
168
|
-
<div class="flex-1">
|
|
169
|
-
<div class="flex items-center">
|
|
170
|
-
<input type="checkbox" :checked="selectedTasks.includes(task.url)"
|
|
171
|
-
@change="toggleTaskSelection(task.url)"
|
|
172
|
-
class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500 mr-2"
|
|
173
|
-
:title="'选择任务:' + task.showName">
|
|
174
|
-
<h3 class="font-bold text-green-600 truncate max-w-[calc(100vw-100px)]" :title="task.url">
|
|
175
|
-
{{ task.showName }}
|
|
176
|
-
</h3>
|
|
177
|
-
<div class="absolute right-1 top-1 text-xs rounded overflow-hidden">
|
|
178
|
-
<span v-if="task.status === 'pending'"
|
|
179
|
-
class="px-2 py-0.5 bg-yellow-100 text-yellow-800">等待中</span>
|
|
180
|
-
<span v-else-if="task.status === 'resume'"
|
|
181
|
-
class="px-2 py-0.5 bg-green-100 text-green-800">下载中</span>
|
|
182
|
-
<span v-else-if="task.status === 'pause'" class="px-2 py-0.5 bg-gray-100 text-gray-800">已暂停</span>
|
|
183
|
-
<span v-else-if="task.status === 'done'" class="px-2 py-0.5 bg-blue-100 text-blue-800">已完成</span>
|
|
184
|
-
<span v-else-if="task.status === 'error'" class="px-2 py-0.5 bg-red-100 text-red-600"
|
|
185
|
-
:title="task.errmsg">{{task.errmsg || '异常'}} <i class="fas fa-info-circle"></i></span>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
<div class="flex items-center text-sm text-gray-500 mt-1 flex-wrap gap-2">
|
|
189
|
-
<span class="cursor-pointer text-blue-600 hover:text-blue-800" @click="showTaskDetail(task)">
|
|
190
|
-
<i class="fas fa-info-circle mr-1"></i>
|
|
191
|
-
<span>详情</span>
|
|
192
|
-
</span>
|
|
193
|
-
<span v-if="config.showPreview" class="text-blue-500 hover:text-blue-600 cursor-pointer"
|
|
194
|
-
@click="preview(task.url)">
|
|
195
|
-
<i class="fas fa-eye mr-1"></i>预览
|
|
196
|
-
</span>
|
|
197
|
-
<span v-if="config.showLocalPlay" class="text-green-500 hover:text-green-600 cursor-pointer"
|
|
198
|
-
@click="localPlay(task)">
|
|
199
|
-
<i class="fas fa-play-circle mr-1"></i>{{ task.localVideo ? '播放' : '边下边播' }}
|
|
200
|
-
</span>
|
|
201
|
-
<span v-if="task.duration" class="flex items-center">
|
|
202
|
-
<i class="fas fa-clock mr-1"></i>
|
|
203
|
-
<span>时长: {{ T.formatTimeCost(task.duration * 1000) }}</span>
|
|
204
|
-
</span><span class="flex items-center">
|
|
205
|
-
<i class="fas fa-file-video mr-1"></i>
|
|
206
|
-
<span>大小: {{ T.formatSize(task.downloadedSize) }}</span>
|
|
207
|
-
</span>
|
|
208
|
-
<span v-if="task.tsCount" class="flex items-center">
|
|
209
|
-
<i class="fas fa-file-alt mr-1"></i>
|
|
210
|
-
<span>分片: {{ task.tsSuccess + task.tsFailed }}/{{ task.tsCount }}</span>
|
|
211
|
-
</span>
|
|
212
|
-
<span v-if="task.status === 'resume'" class="flex items-center">
|
|
213
|
-
<i class="fas fa-hourglass-half mr-1"></i>
|
|
214
|
-
<span>剩余: {{ T.formatTimeCost(task.remainingTime || 0) }}</span>
|
|
215
|
-
</span>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
<div class="flex space-x-2">
|
|
219
|
-
<button v-if="task.status === 'resume' || task.status === 'pending'"
|
|
220
|
-
@click="pauseDownload([task.url])" class="p-2 text-yellow-500 hover:bg-yellow-50 rounded"
|
|
221
|
-
title="暂停">
|
|
222
|
-
<i class="fas fa-pause"></i>
|
|
223
|
-
</button>
|
|
224
|
-
<button v-if="task.status === 'pause' || task.status === 'error'" @click="resumeDownload([task.url])"
|
|
225
|
-
class="p-2 text-green-500 hover:bg-green-50 rounded" title="继续">
|
|
226
|
-
<i class="fas fa-play"></i>
|
|
227
|
-
</button>
|
|
228
|
-
<button @click="deleteDownload([task.url])" class="p-2 text-red-500 hover:bg-red-50 rounded"
|
|
229
|
-
title="删除">
|
|
230
|
-
<i class="fas fa-trash"></i>
|
|
231
|
-
</button>
|
|
232
|
-
</div>
|
|
233
|
-
</div>
|
|
234
|
-
<div class="relative pt-1">
|
|
235
|
-
<div class="flex mb-2 items-center justify-between">
|
|
236
|
-
<div class="flex items-center">
|
|
237
|
-
<span class="text-xs font-semibold inline-block text-blue-600">
|
|
238
|
-
{{ task.progress || 0 }}%
|
|
239
|
-
</span>
|
|
240
|
-
</div>
|
|
241
|
-
<div>
|
|
242
|
-
<span class="text-xs font-semibold inline-block py-1 px-2 rounded text-green-600">
|
|
243
|
-
<!-- <i class="fas fa-tachometer-alt mr-1"></i> -->
|
|
244
|
-
{{ task.speedDesc }}
|
|
245
|
-
</span>
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
<div class="overflow-hidden h-2 text-xs flex rounded bg-blue-200">
|
|
249
|
-
<div :style="{ width: (task.progress || 0) + '%' }"
|
|
250
|
-
class="progress-bar shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-blue-500">
|
|
251
|
-
</div>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
<div v-if="filteredTasks.length === 0" class="p-8 text-center text-gray-500">
|
|
256
|
-
<i class="fas fa-download text-4xl mb-4"></i>
|
|
257
|
-
<p>暂无下载任务</p>
|
|
258
|
-
<button @click="showNewDownloadDialog"
|
|
259
|
-
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
|
|
260
|
-
<i class="fas fa-plus mr-1"></i>添加下载任务
|
|
261
|
-
</button>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
266
|
-
|
|
267
|
-
<div v-if="activeSection === 'config'" class="bg-white rounded-lg shadow p-6 mb-6">
|
|
268
|
-
<h2 class="text-xl font-semibold mb-6">下载设置</h2>
|
|
269
|
-
<form @submit.prevent="updateConfig" class="space-y-6">
|
|
270
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
271
|
-
<!-- 线程数 -->
|
|
272
|
-
<div>
|
|
273
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">单任务并发下载线程数</label>
|
|
274
|
-
<input v-model.number="config.threadNum" type="number" min="0" max="16"
|
|
275
|
-
class="w-full p-2 border rounded-lg focus:ring-blue-500" placeholder=" 请输入线程数(1-16)" />
|
|
276
|
-
<p class="mt-1 text-sm text-gray-500">建议不超过 8 个,对单个服务器的并发请求数过多可能会导致被封 IP</p>
|
|
277
|
-
</div>
|
|
278
|
-
|
|
279
|
-
<!-- 最大并发下载数 -->
|
|
280
|
-
<div>
|
|
281
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">最多同时下载视频数</label>
|
|
282
|
-
<input v-model.number="config.maxDownloads" type="number" min="1" max="10"
|
|
283
|
-
class="w-full p-2 border rounded-lg focus:ring-blue-500" placeholder="请输入最大并发下载数(1-10)" />
|
|
284
|
-
<p class="mt-1 text-sm text-gray-500">最多同时下载任务数量,默认为 3</p>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
<!-- 保存目录 -->
|
|
288
|
-
<div>
|
|
289
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">视频保存目录</label>
|
|
290
|
-
<div class="flex">
|
|
291
|
-
<input v-model="config.saveDir" type="text" class="w-full p-2 border rounded-lg focus:ring-blue-500"
|
|
292
|
-
placeholder="请输入保存目录路径" />
|
|
293
|
-
</div>
|
|
294
|
-
<p class="mt-1 text-sm text-gray-500">默认为当前目录下 downloads 文件夹</p>
|
|
295
|
-
</div>
|
|
296
|
-
|
|
297
|
-
<!-- 删除缓存 -->
|
|
298
|
-
<div>
|
|
299
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">下载完成后删除ts分片缓存</label>
|
|
300
|
-
<div class="flex items-center mt-2">
|
|
301
|
-
<label class="inline-flex items-center">
|
|
302
|
-
<input type="checkbox" v-model="config.delCache"
|
|
303
|
-
class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500">
|
|
304
|
-
<span class="ml-2 text-gray-700">删除分片文件</span>
|
|
305
|
-
</label>
|
|
306
|
-
</div>
|
|
307
|
-
<p class="mt-1 text-sm text-gray-500">保存临时文件可以在重复下载时识别缓存</p>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
|
-
<!-- 转换格式 -->
|
|
311
|
-
<div>
|
|
312
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">下载完成后转换格式</label>
|
|
313
|
-
<div class="flex items-center mt-2">
|
|
314
|
-
<label class="inline-flex items-center">
|
|
315
|
-
<input type="checkbox" v-model="config.convert"
|
|
316
|
-
class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500">
|
|
317
|
-
<span class="ml-2 text-gray-700">合并转换为 MP4/TS 文件</span>
|
|
318
|
-
</label>
|
|
319
|
-
</div>
|
|
320
|
-
</div>
|
|
321
|
-
|
|
322
|
-
<!-- 显示预览按钮 -->
|
|
323
|
-
<div>
|
|
324
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">显示预览按钮</label>
|
|
325
|
-
<div class="flex items-center mt-2">
|
|
326
|
-
<label class="inline-flex items-center">
|
|
327
|
-
<input type="checkbox" v-model="config.showPreview"
|
|
328
|
-
class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500">
|
|
329
|
-
<span class="ml-2 text-gray-700">在下载列表中显示预览按钮</span>
|
|
330
|
-
</label>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
|
|
334
|
-
<!-- 显示边下边播按钮 -->
|
|
335
|
-
<div>
|
|
336
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">显示边下边播按钮</label>
|
|
337
|
-
<div class="flex items-center mt-2">
|
|
338
|
-
<label class="inline-flex items-center">
|
|
339
|
-
<input type="checkbox" v-model="config.showLocalPlay"
|
|
340
|
-
class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500">
|
|
341
|
-
<span class="ml-2 text-gray-700">在下载列表中显示边下边播按钮</span>
|
|
342
|
-
</label>
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
</div>
|
|
346
|
-
|
|
347
|
-
<div class="flex justify-end space-x-4">
|
|
348
|
-
<!-- <button type="button" @click="resetConfig"
|
|
349
|
-
class="px-6 py-2 text-gray-600 hover:bg-gray-100 rounded-lg border">
|
|
350
|
-
重置配置
|
|
351
|
-
</button> -->
|
|
352
|
-
<button type="submit" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">
|
|
353
|
-
保存配置
|
|
354
|
-
</button>
|
|
355
|
-
</div>
|
|
356
|
-
</form>
|
|
357
|
-
</div>
|
|
358
|
-
|
|
359
|
-
<div v-if="activeSection === 'config'" class="bg-white rounded-lg shadow p-6">
|
|
360
|
-
<h2 class="text-xl font-semibold mb-6">本地设置</h2>
|
|
361
|
-
<form @submit.prevent="updateLocalConfig" class="space-y-6">
|
|
362
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
363
|
-
<!-- 访问密码 -->
|
|
364
|
-
<div>
|
|
365
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">访问密码</label>
|
|
366
|
-
<div class="flex">
|
|
367
|
-
<input v-model="token" type="password" maxlength="256"
|
|
368
|
-
class="w-full p-2 border rounded-lg focus:ring-blue-500" placeholder="请输入访问密码" />
|
|
369
|
-
</div>
|
|
370
|
-
<p class="mt-1 text-sm text-gray-500">若服务端设置了访问密码(token),请在此输入</p>
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
|
|
374
|
-
<div class="flex justify-end space-x-4">
|
|
375
|
-
<!-- <button type="button" @click="resetConfig"
|
|
376
|
-
class="px-6 py-2 text-gray-600 hover:bg-gray-100 rounded-lg border">
|
|
377
|
-
重置配置
|
|
378
|
-
</button> -->
|
|
379
|
-
<button type="submit" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">
|
|
380
|
-
保存配置
|
|
381
|
-
</button>
|
|
382
|
-
</div>
|
|
383
|
-
</form>
|
|
384
|
-
</div>
|
|
385
|
-
|
|
386
|
-
<div v-if="activeSection === 'about'" class="bg-white rounded-lg shadow p-6">
|
|
387
|
-
<h2 class="text-xl font-semibold mb-6 text-center">关于项目</h2>
|
|
388
|
-
<div class="space-y-6">
|
|
389
|
-
<div>
|
|
390
|
-
<h3 class="text-lg font-medium text-green-700 mb-2">项目信息</h3>
|
|
391
|
-
<div class="bg-gray-50 p-4 rounded-lg">
|
|
392
|
-
<p class="text-gray-600 mb-2"><strong>许可证:</strong>MIT</p>
|
|
393
|
-
<p class="text-gray-600 mb-2"><strong>作者:</strong><a href="https://lzw.me" target="_blank" rel="noopener"
|
|
394
|
-
class="text-blue-500 hover:text-blue-600">renxia</a (https://github.com/renxia)</p>
|
|
395
|
-
<p class="text-gray-600 mb-2"><strong>GitHub:</strong>
|
|
396
|
-
<a href="https://github.com/lzwme/m3u8-dl.git" target="_blank" rel="noopener"
|
|
397
|
-
class="text-blue-500 hover:text-blue-600">https://github.com/lzwme/m3u8-dl.git</a>
|
|
398
|
-
</p>
|
|
399
|
-
<p class="text-gray-600"><strong>问题反馈:</strong>
|
|
400
|
-
<a href="https://github.com/lzwme/m3u8-dl/issues" target="_blank" rel="noopener"
|
|
401
|
-
class="text-blue-500 hover:text-blue-600">
|
|
402
|
-
https://github.com/lzwme/m3u8-dl/issues</a>
|
|
403
|
-
</p>
|
|
404
|
-
<p class="text-gray-600"><strong>当前版本:</strong>
|
|
405
|
-
<a href="https://github.com/lzwme/m3u8-dl/release" target="_blank" rel="noopener"
|
|
406
|
-
class="text-blue-500 hover:text-blue-600">
|
|
407
|
-
{{serverInfo.version}}</a>
|
|
408
|
-
</p>
|
|
409
|
-
<p class="text-gray-600"><strong>检测版本:</strong>
|
|
410
|
-
<button @click="checkNewVersion" v-if="!serverInfo.appUpdateMessage"
|
|
411
|
-
class="px-2 py-1 text-sm bg-green-600 hover:bg-green-700 text-white rounded">
|
|
412
|
-
<i class="fas fa-check mr-1"></i>检测新版本
|
|
413
|
-
</button>
|
|
414
|
-
<span v-if="serverInfo.newVersion" class="text-blue-600">发现新版本![{{serverInfo.newVersion}}]</span>
|
|
415
|
-
<span v-if="serverInfo.appUpdateMessage" class="text-green-600">{{serverInfo.appUpdateMessage}}</span>
|
|
416
|
-
</p>
|
|
417
|
-
</div>
|
|
418
|
-
</div>
|
|
419
|
-
|
|
420
|
-
<div>
|
|
421
|
-
<h3 class="text-lg font-medium text-green-700 mb-2">项目简介</h3>
|
|
422
|
-
<p class="text-gray-600"><a href="https://github.com/lzwme/m3u8-dl" target="_blank" rel="noopener"
|
|
423
|
-
class="text-blue-500 hover:text-blue-600">@lzwme/m3u8-dl</a> 是一个功能强大的 m3u8
|
|
424
|
-
文件视频批量下载工具,支持多线程下载、边下边播、缓存续传等特性。</p>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
<div>
|
|
428
|
-
<h3 class="text-lg font-medium text-green-700 mb-2">主要特性</h3>
|
|
429
|
-
<ul class="list-disc list-inside text-gray-600 space-y-2">
|
|
430
|
-
<li>多线程下载:采用线程池模式的多线程下载</li>
|
|
431
|
-
<li>边下边播模式:支持使用已下载的 ts 缓存文件在线播放</li>
|
|
432
|
-
<li>批量下载:支持指定多个 m3u8 地址批量下载</li>
|
|
433
|
-
<li>缓存续传:下载失败会保留缓存,重试时只下载失败的片段</li>
|
|
434
|
-
<li>加密支持:支持常见的 AES 加密视频流解密</li>
|
|
435
|
-
<li>格式转换:支持自动转换为 mp4(需安装 ffmpeg)</li>
|
|
436
|
-
<li>搜索功能:支持指定采集站标准 API,以命令行交互的方式搜索和下载</li>
|
|
437
|
-
<li>WebUI:提供下载中心,支持启动为 webui 服务方式进行下载管理</li>
|
|
438
|
-
</ul>
|
|
439
|
-
</div>
|
|
440
|
-
|
|
441
|
-
<div>
|
|
442
|
-
<h3 class="text-lg font-medium text-green-700 mb-2">安装使用</h3>
|
|
443
|
-
<div class="bg-gray-50 p-4 rounded-lg">
|
|
444
|
-
<p class="text-gray-600 mb-2">全局安装:</p>
|
|
445
|
-
<pre
|
|
446
|
-
class="bg-gray-800 text-gray-100 p-3 rounded-lg overflow-x-auto">npm i -g @lzwme/m3u8-dl<br>m3u8dl -h</pre>
|
|
447
|
-
<p class="text-gray-600 mt-4 mb-2">使用 npx:</p>
|
|
448
|
-
<pre class="bg-gray-800 text-gray-100 p-3 rounded-lg overflow-x-auto">npx @lzwme/m3u8-dl -h</pre>
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
|
|
452
|
-
<div>
|
|
453
|
-
<h3 class="text-lg font-medium text-green-700 mb-2">Docker 部署</h3>
|
|
454
|
-
<div class="bg-gray-50 p-4 rounded-lg">
|
|
455
|
-
<p class="text-gray-600 mb-2">使用 Docker 命令运行:</p>
|
|
456
|
-
<pre
|
|
457
|
-
class="bg-gray-800 text-gray-100 p-3 rounded-lg overflow-x-auto">docker run -d --name m3u8-dl -p 6600:6600 -v ./downloads:/app/downloads -v ./cache:/app/cache lzwme/m3u8-dl</pre>
|
|
458
|
-
<p class="text-gray-600 mt-4 mb-2">使用 docker-compose 运行:</p>
|
|
459
|
-
<pre class="bg-gray-800 text-gray-100 p-3 rounded-lg overflow-x-auto">version: '3'
|
|
460
|
-
services:
|
|
461
|
-
m3u8-dl:
|
|
462
|
-
image: lzwme/m3u8-dl
|
|
463
|
-
container_name: m3u8-dl
|
|
464
|
-
ports:
|
|
465
|
-
- "6600:6600"
|
|
466
|
-
volumes:
|
|
467
|
-
- ./downloads:/app/downloads
|
|
468
|
-
- ./cache:/app/cache
|
|
469
|
-
restart: unless-stopped</pre>
|
|
470
|
-
<p class="text-gray-600 mt-4">部署完成后,访问 <a href="http://localhost:6600" target="_blank" rel="noopener"
|
|
471
|
-
class="text-blue-500 hover:text-blue-600">http://localhost:6600</a> 即可使用 WebUI 界面。</p>
|
|
472
|
-
</div>
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
475
|
-
</div>
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
<div id="toast" class="grid fixed top-4 right-4 z-50 overflow-x-hidden overflow-y-auto"></div>
|
|
479
|
-
<script>
|
|
480
|
-
const T = {
|
|
481
|
-
token: '',
|
|
482
|
-
taskStatus: {
|
|
483
|
-
resume: '下载中',
|
|
484
|
-
pending: '等待中',
|
|
485
|
-
pause: '已暂停',
|
|
486
|
-
error: '异常',
|
|
487
|
-
done: '已完成',
|
|
488
|
-
},
|
|
489
|
-
reqHeaders: {
|
|
490
|
-
'content-type': 'application/json',
|
|
491
|
-
authorization: localStorage.getItem('token') || '',
|
|
492
|
-
},
|
|
493
|
-
initTJ() {
|
|
494
|
-
if (!window._hmt) window._hmt = [];
|
|
495
|
-
const hm = document.createElement("script");
|
|
496
|
-
hm.src = "https://hm.baidu.com/hm.js?0b21eda331ac9677a4c546dea88616d0";
|
|
497
|
-
const s = document.getElementsByTagName("script")[0];
|
|
498
|
-
s.parentNode.insertBefore(hm, s);
|
|
499
|
-
},
|
|
500
|
-
request(method, url, data, headers = {}) {
|
|
501
|
-
return fetch(url, {
|
|
502
|
-
method,
|
|
503
|
-
body: data ? JSON.stringify(data) : null,
|
|
504
|
-
headers: { ...this.reqHeaders, ...headers }
|
|
505
|
-
}).then(d => d.json())
|
|
506
|
-
.then(d => {
|
|
507
|
-
if (d.code) T.toast(d.message || `[${url}] 请求失败`, { icon: 'error', duration: 10000 });
|
|
508
|
-
return d;
|
|
509
|
-
})
|
|
510
|
-
.catch(e => {
|
|
511
|
-
T.toast(`请求失败: ${e.message}`, { icon: 'error', duration: 10000 });
|
|
512
|
-
return { code: -1, message: e.message };
|
|
513
|
-
});
|
|
514
|
-
},
|
|
515
|
-
post(url, data) {
|
|
516
|
-
return this.request('POST', url, data);
|
|
517
|
-
},
|
|
518
|
-
get(url) {
|
|
519
|
-
return this.request('GET', url);
|
|
520
|
-
},
|
|
521
|
-
alert(msg, p) {
|
|
522
|
-
p = typeof msg === 'object' ? msg : Object.assign({ text: msg }, p);
|
|
523
|
-
if (!p.toast) p.allowOutsideClick = false;
|
|
524
|
-
return Swal.fire(Object.assign({ icon: 'info', showConfirmButton: false, showCloseButton: true, confirmButtonText: '确定', cancelButtonText: '关闭' }, p));
|
|
525
|
-
},
|
|
526
|
-
confirm(msg, p) {
|
|
527
|
-
return this.alert(msg, { showConfirmButton: true, showCancelButton: true, showCloseButton: true, confirmButtonText: '确认', cancelButtonText: '取消' });
|
|
528
|
-
},
|
|
529
|
-
toast(msg, p) {
|
|
530
|
-
p = (typeof msg === 'object' ? msg : Object.assign({ text: msg }, p));
|
|
531
|
-
const config = Object.assign({ type: p.icon || 'success', duration: p.timer || 3000 }, p);
|
|
532
|
-
const toast = document.createElement('div');
|
|
533
|
-
const iconMap = {
|
|
534
|
-
success: 'fa-check-circle text-green-600',
|
|
535
|
-
error: 'fa-times-circle text-red-600',
|
|
536
|
-
warning: 'fa-exclamation-triangle text-yellow-600',
|
|
537
|
-
info: 'fa-info-circle text-blue-500'
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
toast.className = `custom-toast custom-toast-${config.type}`;
|
|
541
|
-
toast.innerHTML = [
|
|
542
|
-
`<i class="custom-toast-icon mr-2 fa ${iconMap[config.type] || iconMap.info}"></i>`,
|
|
543
|
-
`<span class="break-all">${config.text || ''}</span>`,
|
|
544
|
-
`<i class="custom-toast-close fa fa-close fixed right-2 cursor-pointer text-lg" onclick="this.parentElement.remove()"></i>`
|
|
545
|
-
].join('');
|
|
546
|
-
|
|
547
|
-
document.body.appendChild(toast);
|
|
548
|
-
setTimeout(() => toast.classList.add('show'), 10);
|
|
549
|
-
|
|
550
|
-
const close = () => {
|
|
551
|
-
toast.classList.remove('show');
|
|
552
|
-
toast.classList.add('hide');
|
|
553
|
-
setTimeout(() => toast.remove(), 300);
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
if (config.duration > 0) setTimeout(() => close(), config.duration);
|
|
557
|
-
|
|
558
|
-
return { element: toast, close };
|
|
559
|
-
},
|
|
560
|
-
// 格式化大小
|
|
561
|
-
formatSize: function (size) {
|
|
562
|
-
if (size < 1024) {
|
|
563
|
-
return size + ' B';
|
|
564
|
-
} else if (size < 1024 * 1024) {
|
|
565
|
-
return (size / 1024).toFixed(2) + ' KB';
|
|
566
|
-
} else if (size < 1024 * 1024 * 1024) {
|
|
567
|
-
return (size / (1024 * 1024)).toFixed(2) + ' MB';
|
|
568
|
-
} else {
|
|
569
|
-
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
// 格式化速度
|
|
573
|
-
formatSpeed: function (speed) {
|
|
574
|
-
if (speed < 1024) {
|
|
575
|
-
return speed + ' B/s';
|
|
576
|
-
} else if (speed < 1024 * 1024) {
|
|
577
|
-
return (speed / 1024).toFixed(2) + ' KB/s';
|
|
578
|
-
} else if (speed < 1024 * 1024 * 1024) {
|
|
579
|
-
return (speed / (1024 * 1024)).toFixed(2) + ' MB/s';
|
|
580
|
-
} else {
|
|
581
|
-
return (speed / (1024 * 1024 * 1024)).toFixed(2) + ' GB/s';
|
|
582
|
-
}
|
|
583
|
-
},
|
|
584
|
-
// 格式化时间
|
|
585
|
-
formatTimeCost: function (seconds) {
|
|
586
|
-
seconds /= 1000;
|
|
587
|
-
if (seconds < 60) {
|
|
588
|
-
return seconds + '秒';
|
|
589
|
-
} else if (seconds < 60 * 60) {
|
|
590
|
-
return (seconds / 60).toFixed(2) + '分钟';
|
|
591
|
-
} else if (seconds < 60 * 60 * 24) {
|
|
592
|
-
return (seconds / (60 * 60)).toFixed(2) + '小时';
|
|
593
|
-
} else {
|
|
594
|
-
return (seconds / (60 * 60 * 24)).toFixed(2) + '天';
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
safeJSONParse(data) {
|
|
598
|
-
try {
|
|
599
|
-
return JSON.parse(data);
|
|
600
|
-
} catch (error) {
|
|
601
|
-
console.error('解析 JSON 失败:', data, error);
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
Vue.prototype.T = T;
|
|
608
|
-
T.initTJ();
|
|
609
|
-
window.APP = new Vue({
|
|
610
|
-
el: '#app',
|
|
611
|
-
data: {
|
|
612
|
-
ws: null,
|
|
613
|
-
serverInfo: {
|
|
614
|
-
version: '{{version}}',
|
|
615
|
-
ariang: false,
|
|
616
|
-
newVersion: '',
|
|
617
|
-
appUpdateMessage: '',
|
|
618
|
-
},
|
|
619
|
-
config: {
|
|
620
|
-
/** 并发下载线程数。取决于服务器限制,过多可能会容易下载失败。一般建议不超过 8 个。默认为 cpu数 * 2,但不超过 8 */
|
|
621
|
-
threadNum: 0,
|
|
622
|
-
/** 下载文件保存的路径。默认为当前目录 */
|
|
623
|
-
saveDir: '',
|
|
624
|
-
/** 下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存 */
|
|
625
|
-
delCache: true,
|
|
626
|
-
/** 下载完毕后,是否合并转换为 mp4 或 ts 文件。默认为 true */
|
|
627
|
-
convert: true,
|
|
628
|
-
/** 是否显示预览按钮 */
|
|
629
|
-
showPreview: true,
|
|
630
|
-
/** 是否显示边下边播按钮 */
|
|
631
|
-
showLocalPlay: true,
|
|
632
|
-
/** 最大并发下载数 */
|
|
633
|
-
maxDownloads: 3,
|
|
634
|
-
},
|
|
635
|
-
/** 访问密码(token) */
|
|
636
|
-
token: T.reqHeaders.authorization,
|
|
637
|
-
/** 下载任务列表,以 url 为 key */
|
|
638
|
-
tasks: {},
|
|
639
|
-
/** 选中的任务列表 */
|
|
640
|
-
selectedTasks: [],
|
|
641
|
-
/** 队列状态 */
|
|
642
|
-
queueStatus: {
|
|
643
|
-
queueLength: 0,
|
|
644
|
-
activeDownloads: [],
|
|
645
|
-
maxConcurrent: 5
|
|
646
|
-
},
|
|
647
|
-
activeSection: 'download',
|
|
648
|
-
sidebarCollapsed: window.innerWidth <= 768,
|
|
649
|
-
/** 搜索关键词 */
|
|
650
|
-
searchQuery: '',
|
|
651
|
-
/** 状态筛选 */
|
|
652
|
-
statusFilter: '',
|
|
653
|
-
/** 最近一次执行 ui update 的时间 */
|
|
654
|
-
forceUpdateTime: 0,
|
|
655
|
-
},
|
|
656
|
-
computed: {
|
|
657
|
-
/** 过滤后的任务列表 */
|
|
658
|
-
filteredTasks: function () {
|
|
659
|
-
let tasks = Object.values(this.tasks);
|
|
660
|
-
|
|
661
|
-
// 搜索过滤
|
|
662
|
-
if (this.searchQuery) {
|
|
663
|
-
const query = this.searchQuery.toLowerCase();
|
|
664
|
-
tasks = tasks.filter(task => {
|
|
665
|
-
const filename = (task.localVideo || task.filename || task.dlOptions?.filename || task.url).toLowerCase();
|
|
666
|
-
return filename.includes(query) || task.url.toLowerCase().includes(query);
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// 状态过滤
|
|
671
|
-
if (this.statusFilter) {
|
|
672
|
-
tasks = tasks.filter(task => task.status === this.statusFilter);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// 排序:resume > pending > pause > error > done
|
|
676
|
-
const statusOrder = { resume: 0, pending: 1, pause: 2, error: 3, done: 4 };
|
|
677
|
-
tasks.sort((a, b) => (statusOrder[a.status] - statusOrder[b.status]) || (a.status === 'done' ? (b.filename - a.filename) : ((a.startTime || 0) - (b.startTime || 0))));
|
|
678
|
-
|
|
679
|
-
// 更新 queueStatus
|
|
680
|
-
const queueStatus = {
|
|
681
|
-
queueLength: 0,
|
|
682
|
-
activeDownloads: [],
|
|
683
|
-
maxConcurrent: this.config.maxDownloads,
|
|
684
|
-
};
|
|
685
|
-
tasks.forEach(task => {
|
|
686
|
-
task.showName = task.filename || task.dlOptions?.filename || task.localVideo || task.url;
|
|
687
|
-
if (task.status === 'pending') {
|
|
688
|
-
queueStatus.queueLength++;
|
|
689
|
-
} else if (task.status === 'resume') {
|
|
690
|
-
queueStatus.activeDownloads.push(task.url);
|
|
691
|
-
}
|
|
692
|
-
});
|
|
693
|
-
this.queueStatus = queueStatus;
|
|
694
|
-
|
|
695
|
-
return tasks;
|
|
696
|
-
}
|
|
697
|
-
},
|
|
698
|
-
methods: {
|
|
699
|
-
initEventsForApp() {
|
|
700
|
-
if (!window.electron) return;
|
|
701
|
-
const ipc = window.electron.ipc;
|
|
702
|
-
ipc.on('message', (ev) => {
|
|
703
|
-
if (typeof ev.data === 'string') T.toast(ev.data, { icon: 'info' });
|
|
704
|
-
else console.log(ev.data);
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
ipc.on('downloadProgress', (data) => {
|
|
708
|
-
console.log('downloadProgress', data);
|
|
709
|
-
this.serverInfo.appUpdateMessage = `下载中:${Number(data.percent).toFixed(2)}% [${T.formatSpeed(data.bytesPerSecond)}] [${T.formatSize(data.transferred)}/${T.formatSize(data.total)}]`;
|
|
710
|
-
});
|
|
711
|
-
},
|
|
712
|
-
async checkNewVersion() {
|
|
713
|
-
try {
|
|
714
|
-
const r = await fetch(`https://registry.npmmirror.com/@lzwme/m3u8-dl/latest`).then(r => r.json());
|
|
715
|
-
if (r.version) {
|
|
716
|
-
if (r.version === this.serverInfo.version) T.toast(`已是最新版本,无需更新[${r.version}]`);
|
|
717
|
-
else {
|
|
718
|
-
this.serverInfo.newVersion = r.version;
|
|
719
|
-
if (window.electron) {
|
|
720
|
-
window.electron.ipc.send('checkForUpdate');
|
|
721
|
-
} else {
|
|
722
|
-
T.alert(`发现新版本[${r.version}],请前往 https://github.com/lzwme/m3u8-dl/releases 下载最新版本`, { icon: 'success' });
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
} catch (error) {
|
|
727
|
-
console.error('检查新版本失败:', error);
|
|
728
|
-
T.alert(`版本检查失败:${error.message}`, { icon: 'error' });
|
|
729
|
-
}
|
|
730
|
-
},
|
|
731
|
-
forceUpdate: function () {
|
|
732
|
-
const now = Date.now();
|
|
733
|
-
if (now - this.forceUpdateTime > 500) {
|
|
734
|
-
this.forceUpdateTime = now;
|
|
735
|
-
this.tasks = { ...this.tasks };
|
|
736
|
-
this.$forceUpdate();
|
|
737
|
-
} else {
|
|
738
|
-
if (this.forceUpdateTimeout) clearTimeout(this.forceUpdateTimeout);
|
|
739
|
-
this.forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 500);
|
|
740
|
-
}
|
|
741
|
-
},
|
|
742
|
-
wsConnect: function (reconnectDelay = 3000) {
|
|
743
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
744
|
-
this.ws.close();
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const ws = new WebSocket(`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws?token=${this.token}`);
|
|
748
|
-
this.ws = ws;
|
|
749
|
-
ws.onmessage = (e) => {
|
|
750
|
-
let { type, data } = T.safeJSONParse(e.data);
|
|
751
|
-
|
|
752
|
-
switch (type) {
|
|
753
|
-
case 'serverInfo':
|
|
754
|
-
Object.assign(this.serverInfo, data);
|
|
755
|
-
break;
|
|
756
|
-
case 'tasks':
|
|
757
|
-
this.tasks = data;
|
|
758
|
-
break;
|
|
759
|
-
case 'progress':
|
|
760
|
-
if (!Array.isArray(data)) data = [data];
|
|
761
|
-
this.$nextTick(() => {
|
|
762
|
-
data.forEach(item => item.url && (this.tasks[item.url] = item));
|
|
763
|
-
this.forceUpdate();
|
|
764
|
-
});
|
|
765
|
-
break;
|
|
766
|
-
case 'delete':
|
|
767
|
-
if (Array.isArray(data)) {
|
|
768
|
-
data.forEach(url => delete this.tasks[url]);
|
|
769
|
-
this.forceUpdate();
|
|
770
|
-
}
|
|
771
|
-
break;
|
|
772
|
-
case 'queueStatus':
|
|
773
|
-
this.queueStatus = data;
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
ws.onopen = () => {
|
|
778
|
-
T.toast('ws连接成功', { icon: 'success' });
|
|
779
|
-
};
|
|
780
|
-
ws.onclose = (ev) => {
|
|
781
|
-
console.error('ws连接关闭:', ev.code, ev.reason);
|
|
782
|
-
if (ev.code === 1008) {
|
|
783
|
-
return T.alert('未授权或密码不正确,请在设置界面输入正确的访问密码', { icon: 'error' });
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
T.toast(`连接已断开,${reconnectDelay / 1000}s 后将重试...`, { icon: 'error' });
|
|
787
|
-
setTimeout(() => {
|
|
788
|
-
this.wsConnect(reconnectDelay + 1000);
|
|
789
|
-
}, reconnectDelay);
|
|
790
|
-
};
|
|
791
|
-
},
|
|
792
|
-
/** 获取配置 */
|
|
793
|
-
fetchConfig: async function () {
|
|
794
|
-
const config = await T.get('/api/config');
|
|
795
|
-
if (config.code) {
|
|
796
|
-
console.error('获取配置失败:', config);
|
|
797
|
-
T.alert('获取配置失败: ' + config.message, { icon: 'error' });
|
|
798
|
-
return false;
|
|
799
|
-
}
|
|
800
|
-
for (const key in config) {
|
|
801
|
-
if (key in this.config) this.config[key] = config[key];
|
|
802
|
-
}
|
|
803
|
-
return true;
|
|
804
|
-
},
|
|
805
|
-
/** 更新配置 */
|
|
806
|
-
updateConfig: async function () {
|
|
807
|
-
const result = await T.post('/api/config', this.config);
|
|
808
|
-
T.toast(result.message || '配置已更新', { icon: result.code ? 'error' : 'success' });
|
|
809
|
-
},
|
|
810
|
-
updateLocalConfig: async function () {
|
|
811
|
-
this.updateToken();
|
|
812
|
-
},
|
|
813
|
-
updateToken() {
|
|
814
|
-
const isUpdated = this.token !== T.reqHeaders.authorization;
|
|
815
|
-
if (!isUpdated) return;
|
|
816
|
-
|
|
817
|
-
if (this.token) this.token = md5(this.token).slice(0, 8);
|
|
818
|
-
|
|
819
|
-
T.reqHeaders.authorization = this.token || '';
|
|
820
|
-
if (this.token) {
|
|
821
|
-
localStorage.setItem('token', this.token);
|
|
822
|
-
this.fetchConfig().then(d => d && this.wsConnect());
|
|
823
|
-
} else {
|
|
824
|
-
localStorage.removeItem('token');
|
|
825
|
-
}
|
|
826
|
-
},
|
|
827
|
-
/** 显示新建下载弹窗 */
|
|
828
|
-
showNewDownloadDialog() {
|
|
829
|
-
Swal.fire({
|
|
830
|
-
title: '新建下载',
|
|
831
|
-
width: '900px',
|
|
832
|
-
html: `
|
|
833
|
-
<div class="text-left">
|
|
834
|
-
<div class="flex flex-row gap-4">
|
|
835
|
-
<input type="text" id="playUrl" placeholder="[实验性]输入视频播放页地址,尝试提取m3u8下载链接" autocomplete="off" id="urlInput" class="flex-1 border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400" value="">
|
|
836
|
-
<div class="flex flex-row gap-1">
|
|
837
|
-
<button type="button" id="getM3u8UrlsBtn" class="player-btn px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none">
|
|
838
|
-
提取
|
|
839
|
-
</button>
|
|
840
|
-
</div>
|
|
841
|
-
</div>
|
|
842
|
-
|
|
843
|
-
<div class="mt-4">
|
|
844
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">视频链接(每行一个,支持m3u8地址及抖音、微博、皮皮虾视频分享链接)</label>
|
|
845
|
-
<textarea id="downloadUrls" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="格式: URL | 名称(可选)、名称 | URL"></textarea>
|
|
846
|
-
</div>
|
|
847
|
-
|
|
848
|
-
<div class="mt-4">
|
|
849
|
-
<div class="flex flex-row gap-2 items-center">
|
|
850
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">视频名称</label>
|
|
851
|
-
<input id="filename" class="flex-1 p-2 border rounded-lg focus:border-blue-500" placeholder="请输入视频名称(可选)">
|
|
852
|
-
</div>
|
|
853
|
-
<p class="ml-2 mt-1 text-sm text-gray-500">若输入多个链接,将依次以"视频名称+第N集"命名</p>
|
|
854
|
-
</div>
|
|
855
|
-
|
|
856
|
-
<div class="mt-4 flex flex-row gap-2 items-center">
|
|
857
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">保存位置</label>
|
|
858
|
-
<input id="saveDir" class="flex-1 p-2 border rounded-lg focus:ring-blue-500" placeholder="请输入保存路径" value="${this.config.saveDir || ''}">
|
|
859
|
-
</div>
|
|
860
|
-
|
|
861
|
-
<div class="mt-4">
|
|
862
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">删除时间片段(适用于移除广告片段的情况)</label>
|
|
863
|
-
<input id="ignoreSegments" class="w-full p-2 border rounded-lg focus:ring-blue-500" placeholder="以-分割起止时间,多个以逗号分隔。示例:0-10,20-100">
|
|
864
|
-
</div>
|
|
865
|
-
|
|
866
|
-
<div class="mt-4">
|
|
867
|
-
<label class="block text-sm font-bold text-gray-700 mb-1">自定义请求头</label>
|
|
868
|
-
<textarea id="headers" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="每行一个(微博视频须设置 Cookie),格式:Key: Value。例如: Referer: https://example.com Cookie: token=123"></textarea>
|
|
869
|
-
</div>
|
|
870
|
-
</div>
|
|
871
|
-
`,
|
|
872
|
-
showCancelButton: true,
|
|
873
|
-
confirmButtonText: '开始下载',
|
|
874
|
-
cancelButtonText: '取消',
|
|
875
|
-
focusConfirm: false,
|
|
876
|
-
showCloseButton: true,
|
|
877
|
-
allowOutsideClick: false,
|
|
878
|
-
preConfirm: () => {
|
|
879
|
-
const urlsText = document.getElementById('downloadUrls').value.trim();
|
|
880
|
-
const filename = document.getElementById('filename').value.trim();
|
|
881
|
-
let saveDir = document.getElementById('saveDir').value.trim();
|
|
882
|
-
const headers = document.getElementById('headers').value.trim();
|
|
883
|
-
const ignoreSegments = document.getElementById('ignoreSegments').value.trim();
|
|
884
|
-
|
|
885
|
-
if (!urlsText) {
|
|
886
|
-
Swal.showValidationMessage('请输入至少一个 M3U8 链接');
|
|
887
|
-
return false;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// 解析链接和文件名
|
|
891
|
-
const urls = urlsText.split('\n').map(line => {
|
|
892
|
-
let [url, name = ''] = line.split(/[\s|$]+/).map(s => s.trim());
|
|
893
|
-
if (name.startsWith('http')) [name, url] = [url, name];
|
|
894
|
-
return { url, name };
|
|
895
|
-
}).filter(item => item.url.startsWith('http'));
|
|
896
|
-
|
|
897
|
-
// 验证链接格式
|
|
898
|
-
if (!urls.length) {
|
|
899
|
-
Swal.showValidationMessage('请输入正确的 M3U8 链接');
|
|
900
|
-
return false;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
if (urls.length > 1 && filename && !saveDir.includes(filename)) {
|
|
904
|
-
if (!saveDir) saveDir = this.config.saveDir;
|
|
905
|
-
saveDir = saveDir.replace(/\/?$/, '') + '/' + filename;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
return urls.map((item, idx) => ({
|
|
909
|
-
url: item.url,
|
|
910
|
-
filename: item.name || (filename ? `${filename}${urls.length > 1 ? `第${idx + 1}集` : ''}` : ''),
|
|
911
|
-
saveDir,
|
|
912
|
-
headers,
|
|
913
|
-
ignoreSegments,
|
|
914
|
-
}));
|
|
915
|
-
}
|
|
916
|
-
}).then((result) => {
|
|
917
|
-
if (result.isConfirmed) this.startBatchDownload(result.value);
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
setTimeout(() => {
|
|
921
|
-
const btn = document.getElementById('getM3u8UrlsBtn');
|
|
922
|
-
if (!btn) return;
|
|
923
|
-
|
|
924
|
-
btn.addEventListener('click', async () => {
|
|
925
|
-
const url = document.getElementById('playUrl').value.trim();
|
|
926
|
-
if (!/^https?:/.test(url)) {
|
|
927
|
-
return Swal.showValidationMessage('请输入正确的 URL 地址');
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
btn.setAttribute('disabled', 'disabled');
|
|
931
|
-
btn.innerText = '解析中...';
|
|
932
|
-
|
|
933
|
-
T.post('/api/getM3u8Urls', { url, headers: document.getElementById('headers').value.trim() }).then(r => {
|
|
934
|
-
if (Array.isArray(r.data)) {
|
|
935
|
-
document.getElementById('downloadUrls').value = r.data.map(d => d.join(' | ')).join('\n');
|
|
936
|
-
T.toast(r.message || `解析完成!获取到 ${r.data.length} 个地址`);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
btn.removeAttribute('disabled');
|
|
940
|
-
btn.innerText = '提取';
|
|
941
|
-
});
|
|
942
|
-
});
|
|
943
|
-
}, 500);
|
|
944
|
-
},
|
|
945
|
-
/** 批量下载 */
|
|
946
|
-
startBatchDownload: async function (list) {
|
|
947
|
-
try {
|
|
948
|
-
list.forEach(async (item, idx) => {
|
|
949
|
-
Object.entries(item).forEach(([key, value]) => !value && delete item[key]);
|
|
950
|
-
if (!/\.html?$/.test(item.url)) this.tasks[item.url] = { status: 'resume', progress: 0, speed: 0, remainingTime: 0, size: 0 };
|
|
951
|
-
});
|
|
952
|
-
const r = await T.post('/api/download', { list });
|
|
953
|
-
if (!r.code) T.toast(r.message || '批量下载已开始');
|
|
954
|
-
this.forceUpdate();
|
|
955
|
-
} catch (error) {
|
|
956
|
-
console.error('批量下载失败:', error);
|
|
957
|
-
T.alert('下载失败: ' + error.message, { icon: 'error' });
|
|
958
|
-
}
|
|
959
|
-
},
|
|
960
|
-
/** 暂停下载 */
|
|
961
|
-
pauseDownload: async function (urls) {
|
|
962
|
-
if (!urls) urls = this.selectedTasks;
|
|
963
|
-
if (typeof urls === 'string') urls = [urls];
|
|
964
|
-
const r = await T.post('/api/pause', { urls, all: urls[0] === 'all' });
|
|
965
|
-
if (!r.code) T.toast(r.message || '已暂停下载');
|
|
966
|
-
if (urls === this.selectedTasks) this.selectedTasks = [];
|
|
967
|
-
},
|
|
968
|
-
/** 恢复下载 */
|
|
969
|
-
resumeDownload: async function (urls) {
|
|
970
|
-
if (!urls) urls = this.selectedTasks;
|
|
971
|
-
if (typeof urls === 'string') urls = [urls];
|
|
972
|
-
const r = await T.post('/api/resume', { urls, all: urls[0] === 'all' });
|
|
973
|
-
if (!r.code) T.toast(r.message || '已恢复下载');
|
|
974
|
-
if (urls === this.selectedTasks) this.selectedTasks = [];
|
|
975
|
-
},
|
|
976
|
-
/** 删除选中的任务 */
|
|
977
|
-
async deleteDownload(urls = this.selectedTasks) {
|
|
978
|
-
if (!urls.length) return;
|
|
979
|
-
try {
|
|
980
|
-
const result = await Swal.fire({
|
|
981
|
-
title: '确认删除',
|
|
982
|
-
html: `
|
|
983
|
-
<div class="text-left">
|
|
984
|
-
<div class="mb-4">
|
|
985
|
-
<label class="inline-flex items-center">
|
|
986
|
-
<input type="checkbox" id="deleteCache" class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500" checked>
|
|
987
|
-
<span class="ml-2 text-gray-700">同时删除已下载的缓存</span>
|
|
988
|
-
</label>
|
|
989
|
-
</div>
|
|
990
|
-
<div>
|
|
991
|
-
<label class="inline-flex items-center">
|
|
992
|
-
<input type="checkbox" id="deleteVideo" class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500" checked>
|
|
993
|
-
<span class="ml-2 text-red-700">同时删除已下载的视频</span>
|
|
994
|
-
</label>
|
|
995
|
-
</div>
|
|
996
|
-
</div>
|
|
997
|
-
`,
|
|
998
|
-
showCancelButton: true,
|
|
999
|
-
confirmButtonText: '确认删除',
|
|
1000
|
-
cancelButtonText: '取消',
|
|
1001
|
-
confirmButtonColor: '#ef4444',
|
|
1002
|
-
focusConfirm: false,
|
|
1003
|
-
preConfirm: () => {
|
|
1004
|
-
return {
|
|
1005
|
-
deleteCache: document.getElementById('deleteCache').checked,
|
|
1006
|
-
deleteVideo: document.getElementById('deleteVideo').checked
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
if (result.isConfirmed) {
|
|
1012
|
-
const r = await T.post('/api/delete', {
|
|
1013
|
-
urls,
|
|
1014
|
-
deleteCache: result.value.deleteCache,
|
|
1015
|
-
deleteVideo: result.value.deleteVideo
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
if (!r.code) {
|
|
1019
|
-
T.toast(r.message || '已删除选中的下载');
|
|
1020
|
-
urls.forEach(url => (delete this.tasks[url]));
|
|
1021
|
-
if (urls === this.selectedTasks) this.selectedTasks = [];
|
|
1022
|
-
this.forceUpdate();
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
console.error('删除下载失败:', error);
|
|
1027
|
-
T.alert('删除下载失败: ' + error.message);
|
|
1028
|
-
}
|
|
1029
|
-
},
|
|
1030
|
-
getTasks: async function () {
|
|
1031
|
-
this.tasks = await T.get('/api/tasks');
|
|
1032
|
-
},
|
|
1033
|
-
showTaskDetail(task) {
|
|
1034
|
-
console.log(task);
|
|
1035
|
-
const isResume = task.status === 'resume';
|
|
1036
|
-
const taskInfo = {
|
|
1037
|
-
名称: task.filename || task.localVideo,
|
|
1038
|
-
状态: T.taskStatus[task.status] || task.status,
|
|
1039
|
-
大小: `${T.formatSize(task.downloadedSize || 0)} / ${task.size ? T.formatSize(task.size) : ''}`,
|
|
1040
|
-
分片: task.tsCount ? `<span class=text-green-600>${task.tsSuccess}</span> / <span class=text-red-500>${task.tsFailed}</span> / ${task.tsCount}` : '-',
|
|
1041
|
-
进度: `${task.progress || '-'}%`,
|
|
1042
|
-
平均速度: `${task.avgSpeedDesc || '-'}/s`,
|
|
1043
|
-
并发线程: task.threadNum,
|
|
1044
|
-
下载地址: task.url,
|
|
1045
|
-
保存位置: task.localVideo || task.options?.saveDir,
|
|
1046
|
-
开始时间: task.startTime && new Date(task.startTime).toLocaleString(),
|
|
1047
|
-
结束时间: !isResume && task.endTime && new Date(task.endTime).toLocaleString(),
|
|
1048
|
-
预估还需: isResume && task.remainingTime && T.formatTimeCost(task.remainingTime),
|
|
1049
|
-
相关信息: task.errmsg && `<span class=text-red-600>${task.errmsg}</span>`
|
|
1050
|
-
};
|
|
1051
|
-
T.alert({
|
|
1052
|
-
title: '任务详情',
|
|
1053
|
-
width: 1000,
|
|
1054
|
-
icon: '',
|
|
1055
|
-
html: [
|
|
1056
|
-
'<div class="flex-col full-width text-left">',
|
|
1057
|
-
Object.entries(taskInfo).filter(d => d[1]).map(
|
|
1058
|
-
([key, value]) => `<div class="flex"><label class="font-bold text-right inline-block" style="min-width:80px">${key}:</label>
|
|
1059
|
-
<span class="ml-2 text-gray-400 word-break">${value}</span></div>`
|
|
1060
|
-
).join(''),
|
|
1061
|
-
'</div>'
|
|
1062
|
-
].join(''),
|
|
1063
|
-
})
|
|
1064
|
-
},
|
|
1065
|
-
/** 边下边播 */
|
|
1066
|
-
localPlay: function (task) {
|
|
1067
|
-
const url = location.origin + `/localplay/${encodeURIComponent(task.localVideo || '') || task.localM3u8}`;
|
|
1068
|
-
console.log(task);
|
|
1069
|
-
Swal.fire({
|
|
1070
|
-
title: task.options?.filename || task.url,
|
|
1071
|
-
width: '1000px',
|
|
1072
|
-
padding: 0,
|
|
1073
|
-
allowOutsideClick: false,
|
|
1074
|
-
showCloseButton: true,
|
|
1075
|
-
showConfirmButton: false,
|
|
1076
|
-
html: `<iframe src="./play.html?url=${encodeURIComponent(url)}" style="width: 100%; height: 550px; max-height: 90vh" frameborder="0" allowfullscreen></iframe>`,
|
|
1077
|
-
});
|
|
1078
|
-
},
|
|
1079
|
-
preview: function (url) {
|
|
1080
|
-
window.open(`https://lzw.me/x/m3u8-player/?url=${encodeURIComponent(url)}`);
|
|
1081
|
-
},
|
|
1082
|
-
// 切换侧栏状态
|
|
1083
|
-
toggleSidebar() {
|
|
1084
|
-
this.sidebarCollapsed = !this.sidebarCollapsed;
|
|
1085
|
-
},
|
|
1086
|
-
// 切换页面并自动收起侧栏
|
|
1087
|
-
switchSection(section) {
|
|
1088
|
-
if (section === 'ariang') {
|
|
1089
|
-
window.open(this.serverInfo.ariang ? './ariang/' : 'http://lzw.me/x/ariang/');
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
this.activeSection = section;
|
|
1093
|
-
if (window.innerWidth <= 768) {
|
|
1094
|
-
this.sidebarCollapsed = true;
|
|
1095
|
-
}
|
|
1096
|
-
},
|
|
1097
|
-
// 监听窗口大小变化
|
|
1098
|
-
handleResize() {
|
|
1099
|
-
this.sidebarCollapsed = window.innerWidth <= 768;
|
|
1100
|
-
},
|
|
1101
|
-
// 更新任务优先级
|
|
1102
|
-
updatePriority: async function (url, priority) {
|
|
1103
|
-
const r = await T.post('/api/priority', { url, priority: parseInt(priority) });
|
|
1104
|
-
T.toast(r.message || '已更新优先级');
|
|
1105
|
-
},
|
|
1106
|
-
// 获取队列状态
|
|
1107
|
-
getQueueStatus: async function () {
|
|
1108
|
-
const status = await T.get('/api/queue/status');
|
|
1109
|
-
if (status?.maxConcurrent) this.queueStatus = status;
|
|
1110
|
-
},
|
|
1111
|
-
// 清空下载队列
|
|
1112
|
-
clearQueue: async function () {
|
|
1113
|
-
const result = await Swal.fire({
|
|
1114
|
-
title: '确认清空队列',
|
|
1115
|
-
text: '这将取消并删除所有等待中的下载任务,已开始下载的任务将继续进行。',
|
|
1116
|
-
icon: 'warning',
|
|
1117
|
-
showCancelButton: true,
|
|
1118
|
-
confirmButtonText: '确认清空',
|
|
1119
|
-
cancelButtonText: '取消',
|
|
1120
|
-
confirmButtonColor: '#ef4444'
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
if (result.isConfirmed) {
|
|
1124
|
-
const r = await T.post('/api/queue/clear');
|
|
1125
|
-
T.toast(r.message || '已清空下载队列');
|
|
1126
|
-
this.getQueueStatus();
|
|
1127
|
-
}
|
|
1128
|
-
},
|
|
1129
|
-
clearFilters: function () {
|
|
1130
|
-
this.searchQuery = '';
|
|
1131
|
-
this.statusFilter = '';
|
|
1132
|
-
},
|
|
1133
|
-
/** 切换任务选中状态 */
|
|
1134
|
-
toggleTaskSelection(url) {
|
|
1135
|
-
const index = this.selectedTasks.indexOf(url);
|
|
1136
|
-
if (index === -1) {
|
|
1137
|
-
this.selectedTasks.push(url);
|
|
1138
|
-
} else {
|
|
1139
|
-
this.selectedTasks.splice(index, 1);
|
|
1140
|
-
}
|
|
1141
|
-
},
|
|
1142
|
-
/** 全选/反选 */
|
|
1143
|
-
toggleSelectAll() {
|
|
1144
|
-
const isSelectedAll = this.selectedTasks.length === this.filteredTasks.length;
|
|
1145
|
-
this.selectedTasks = isSelectedAll ? [] : this.filteredTasks.map(task => task.url);
|
|
1146
|
-
},
|
|
1147
|
-
},
|
|
1148
|
-
mounted() {
|
|
1149
|
-
T.reqHeaders.authorization = this.token ? `${this.token}` : '';
|
|
1150
|
-
this.fetchConfig().then(d => d && this.wsConnect());
|
|
1151
|
-
window.addEventListener('resize', this.handleResize);
|
|
1152
|
-
this.initEventsForApp();
|
|
1153
|
-
},
|
|
1154
|
-
beforeDestroy() {
|
|
1155
|
-
window.removeEventListener('resize', this.handleResize);
|
|
1156
|
-
}
|
|
1157
|
-
});
|
|
1158
|
-
</script>
|
|
19
|
+
<div id="app"></div>
|
|
1159
20
|
</body>
|
|
1160
|
-
|
|
1161
21
|
</html>
|