Compare commits
No commits in common. "8b65894c1a1566105ff1d7e264d56c9cc3d93030" and "bad8d6cd154d461886e76c5ebdcf688aefd02270" have entirely different histories.
8b65894c1a
...
bad8d6cd15
4
api.py
4
api.py
@ -9,9 +9,9 @@ from download import M3U8Downloader
|
|||||||
from function import crawl_missav
|
from function import crawl_missav
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', '123456')
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key-here')
|
||||||
|
|
||||||
downloader = M3U8Downloader(max_workers=10, output_dir=r"/app/download")
|
downloader = M3U8Downloader(max_workers=10, output_dir=r"download")
|
||||||
|
|
||||||
# 从环境变量获取用户名密码
|
# 从环境变量获取用户名密码
|
||||||
USERNAME = os.getenv('USER')
|
USERNAME = os.getenv('USER')
|
||||||
|
|||||||
@ -1,15 +1,5 @@
|
|||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
# 备份并重建 sources.list - 使用阿里云镜像
|
|
||||||
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak && \
|
|
||||||
echo "deb http://mirrors.aliyun.com/ubuntu/ noble main restricted universe multiverse" > /etc/apt/sources.list && \
|
|
||||||
echo "deb http://mirrors.aliyun.com/ubuntu/ noble-updates main restricted universe multiverse" >> /etc/apt/sources.list && \
|
|
||||||
echo "deb http://mirrors.aliyun.com/ubuntu/ noble-backports main restricted universe multiverse" >> /etc/apt/sources.list && \
|
|
||||||
echo "deb http://mirrors.aliyun.com/ubuntu/ noble-security main restricted universe multiverse" >> /etc/apt/sources.list
|
|
||||||
|
|
||||||
# 删除可能存在的其他源文件
|
|
||||||
RUN rm -rf /etc/apt/sources.list.d/*
|
|
||||||
|
|
||||||
# 安装系统依赖
|
# 安装系统依赖
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
nginx \
|
nginx \
|
||||||
@ -29,7 +19,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
libxfixes3 \
|
libxfixes3 \
|
||||||
libxrandr2 \
|
libxrandr2 \
|
||||||
libgbm1 \
|
libgbm1 \
|
||||||
libasound2t64 \
|
libasound2 \
|
||||||
libpango-1.0-0 \
|
libpango-1.0-0 \
|
||||||
libcairo2 \
|
libcairo2 \
|
||||||
libatspi2.0-0 \
|
libatspi2.0-0 \
|
||||||
@ -39,28 +29,23 @@ RUN apt-get update && apt-get install -y \
|
|||||||
python3 \
|
python3 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 复制server文件
|
# 复制server文件
|
||||||
COPY ./server/* /app/server/
|
COPY ./server/* /app/server/
|
||||||
|
|
||||||
# 复制html文件
|
|
||||||
COPY ./html/ /app/html/
|
|
||||||
|
|
||||||
# 复制nginx配置文件
|
# 复制nginx配置文件
|
||||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
# 安装Python依赖
|
# 安装Python依赖
|
||||||
RUN pip install --break-system-packages --proxy http://192.168.1.2:7890 -r /app/server/requirements.txt
|
RUN pip install --no-cache-dir -r /app/server/requirements.txt
|
||||||
|
|
||||||
# 设置Playwright使用国内镜像安装浏览器
|
# 配置Playwright代理和安装Chromium
|
||||||
ENV PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright
|
|
||||||
RUN playwright install chromium
|
RUN playwright install chromium
|
||||||
|
|
||||||
# 创建下载目录和nginx运行所需目录
|
# 创建下载目录和nginx运行所需目录
|
||||||
RUN mkdir -p /app/download /var/run/nginx
|
RUN mkdir -p /app/server/download /var/run/nginx
|
||||||
|
|
||||||
# 设置环境变量
|
# 设置环境变量
|
||||||
ENV USER=admin
|
ENV USER=admin
|
||||||
@ -68,7 +53,7 @@ ENV PASSWORD=password
|
|||||||
ENV SECRET_KEY='asd78yujncisa32r89'
|
ENV SECRET_KEY='asd78yujncisa32r89'
|
||||||
|
|
||||||
# 设置卷
|
# 设置卷
|
||||||
VOLUME ["/app/download"]
|
VOLUME ["/app/server/download"]
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
639
docker/html/home.html
Normal file
639
docker/html/home.html
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>响应式任务列表</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/layui@2.9.6/dist/css/layui.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-manager {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 150, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #1e6fde 0%, #0d5bc2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #f0f2ff;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item:hover {
|
||||||
|
background: #fafbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #e6f7ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #1e6fde;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
flex: 3;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: #1e6fde;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-processing {
|
||||||
|
background: #f0f6ff;
|
||||||
|
color: #1e6fde;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 50px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
color: #1e6fde;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state i {
|
||||||
|
font-size: 50px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-update {
|
||||||
|
text-align: right;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
border-top: 1px solid #f0f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.task-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-update {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条自定义样式 */
|
||||||
|
.layui-progress {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-progress-bar {
|
||||||
|
background: linear-gradient(90deg, #1e6fde, #4a90e2);
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: width 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新动画 */
|
||||||
|
.progress-updating {
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="task-manager">
|
||||||
|
<div class="task-header">
|
||||||
|
<div class="task-title">任务列表</div>
|
||||||
|
<button class="add-btn" id="addTask">
|
||||||
|
<i class="layui-icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-list" id="taskList">
|
||||||
|
<div class="loading-state">
|
||||||
|
<i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate"></i>
|
||||||
|
<div>正在加载任务数据...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/layui@2.9.6/dist/layui.min.js"></script>
|
||||||
|
<script>
|
||||||
|
layui.use(['element', 'jquery'], function () {
|
||||||
|
var element = layui.element;
|
||||||
|
var $ = layui.$;
|
||||||
|
var updateInterval;
|
||||||
|
var currentTasks = [];
|
||||||
|
|
||||||
|
|
||||||
|
// 从API获取任务数据
|
||||||
|
function fetchTasks() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 检查token是否存在
|
||||||
|
if (!localStorage.getItem('Authorization')) {
|
||||||
|
reject(new Error('Token不存在'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 发起带token的请求
|
||||||
|
fetch('/api/all-task', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': localStorage.getItem('Authorization'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
location.href='/';
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
location.href='/';
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取任务数据失败:', error);
|
||||||
|
|
||||||
|
// 如果是认证错误,已经处理过了,这里处理其他错误
|
||||||
|
if (error.message !== '认证失败') {
|
||||||
|
$('#taskList').html(`
|
||||||
|
<div class="error-state">
|
||||||
|
<i class="layui-icon"></i>
|
||||||
|
<div>获取数据失败: ${error.message}</div>
|
||||||
|
<button class="layui-btn layui-btn-primary layui-btn-sm" style="margin-top: 10px;" onclick="initTaskList()">重试</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染任务列表
|
||||||
|
function renderTaskList(tasks) {
|
||||||
|
tasks = tasks.data;
|
||||||
|
var taskHtml = '';
|
||||||
|
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
taskHtml = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="layui-icon"></i>
|
||||||
|
<div>暂无任务</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
tasks.forEach(function (task) {
|
||||||
|
var statusText = task.status === 'completed' ? '已完成' : '进行中';
|
||||||
|
var statusClass = task.status === 'completed' ? 'status-completed' : 'status-processing';
|
||||||
|
var progressPercent = Math.round(task.progress * 100) + '%';
|
||||||
|
|
||||||
|
taskHtml += `
|
||||||
|
<div class="task-item" data-id="${task.task_id}">
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">${task.filename}</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="layui-progress">
|
||||||
|
<div class="layui-progress-bar" lay-percent="${progressPercent}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-info">${progressPercent}</div>
|
||||||
|
<div class="task-status ${statusClass}">${statusText}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#taskList').html(taskHtml);
|
||||||
|
element.render('progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务进度(不重新渲染整个列表)
|
||||||
|
function updateTaskProgress(tasks) {
|
||||||
|
tasks = tasks.data;
|
||||||
|
tasks.forEach(function (item) {
|
||||||
|
var taskItem = $(`.task-item[data-id="${item.task_id}"]`);
|
||||||
|
if (taskItem.length > 0) {
|
||||||
|
var progressPercent = Math.round(item.progress * 100) + '%';
|
||||||
|
var statusText = item.status === 'completed' ? '已完成' : '进行中';
|
||||||
|
var statusClass = item.status === 'completed' ? 'status-completed' : 'status-processing';
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
var progressBar = taskItem.find('.layui-progress-bar');
|
||||||
|
progressBar.attr('lay-percent', progressPercent);
|
||||||
|
|
||||||
|
// 添加更新动画
|
||||||
|
progressBar.addClass('progress-updating');
|
||||||
|
|
||||||
|
// 使用动画更新进度
|
||||||
|
setTimeout(function () {
|
||||||
|
progressBar.css('width', progressPercent);
|
||||||
|
progressBar.removeClass('progress-updating');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 更新进度文本
|
||||||
|
taskItem.find('.progress-info').text(progressPercent);
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
var statusElement = taskItem.find('.task-status');
|
||||||
|
statusElement.text(statusText);
|
||||||
|
statusElement.removeClass('status-completed status-processing').addClass(statusClass);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化任务列表
|
||||||
|
function initTaskList() {
|
||||||
|
fetchTasks().then(tasks => {
|
||||||
|
currentTasks = tasks;
|
||||||
|
renderTaskList(tasks);
|
||||||
|
// 启动定时更新
|
||||||
|
startAutoUpdate();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('初始化任务列表失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动自动更新
|
||||||
|
function startAutoUpdate() {
|
||||||
|
// 清除现有定时器
|
||||||
|
if (updateInterval) {
|
||||||
|
clearInterval(updateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每5秒更新一次
|
||||||
|
updateInterval = setInterval(function () {
|
||||||
|
fetchTasks().then(tasks => {
|
||||||
|
currentTasks = tasks;
|
||||||
|
updateTaskProgress(tasks);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新任务
|
||||||
|
$('#addTask').on('click', function () {
|
||||||
|
layer.prompt({
|
||||||
|
title: '请输入URL',
|
||||||
|
formType: 0,
|
||||||
|
}, function (url, index) {
|
||||||
|
layer.close(index);
|
||||||
|
|
||||||
|
const token = localStorage.getItem('Authorization');
|
||||||
|
const loadingIndex = layer.load(2, {
|
||||||
|
shade: [0.1, '#000']
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`/api/check/${encodeURIComponent(url)}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('检查URL失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
layer.close(loadingIndex);
|
||||||
|
|
||||||
|
if (data.code !== 200 || !data.data) {
|
||||||
|
layer.msg('接口返回异常', { icon: 2 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { serial_number, title, url: urlList } = data.data;
|
||||||
|
|
||||||
|
// 检测设备类型
|
||||||
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
// 动态生成URL列表HTML - 响应式布局
|
||||||
|
const urlHtml = urlList.map((u, index) => {
|
||||||
|
if (isMobile) {
|
||||||
|
// 手机端布局 - 垂直排列
|
||||||
|
return `
|
||||||
|
<li class="layui-col-xs12" style="padding: 10px 0; border-bottom: 1px solid #f6f6f6;">
|
||||||
|
<div style="word-break: break-all; font-size: 13px; color: #666; margin-bottom: 8px;">
|
||||||
|
${u}
|
||||||
|
</div>
|
||||||
|
<button class="layui-btn layui-btn-sm layui-btn-normal select-url"
|
||||||
|
data-serial="${serial_number}"
|
||||||
|
data-url="${u}"
|
||||||
|
style="width: 100%;">
|
||||||
|
选择
|
||||||
|
</button>
|
||||||
|
</li>`;
|
||||||
|
} else {
|
||||||
|
// 电脑端布局 - 水平排列
|
||||||
|
return `
|
||||||
|
<li class="layui-row" style="padding: 8px 0; border-bottom: 1px solid #f6f6f6; margin: 0;">
|
||||||
|
<div class="layui-col-xs10" style="word-break: break-all; font-size: 13px; color: #666; line-height: 32px;">
|
||||||
|
${u}
|
||||||
|
</div>
|
||||||
|
<div class="layui-col-xs2" style="text-align: right;">
|
||||||
|
<button class="layui-btn layui-btn-sm layui-btn-normal select-url"
|
||||||
|
data-serial="${serial_number}"
|
||||||
|
data-url="${u}">
|
||||||
|
选择
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// 响应式弹窗设置
|
||||||
|
const popupSettings = isMobile ? {
|
||||||
|
area: ['90vw', '70vh'],
|
||||||
|
content: `
|
||||||
|
<div style="padding: 15px;">
|
||||||
|
<div class="layui-form-item" style="margin-bottom: 15px;">
|
||||||
|
<label class="layui-form-label" style="width: 80px; padding: 9px 5px;">番号:</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" class="layui-input" value="${serial_number || ''}" readonly
|
||||||
|
style="background-color: #f8f8f8; color: #666; font-size: 14px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item" style="margin-bottom: 15px;">
|
||||||
|
<label class="layui-form-label" style="width: 80px; padding: 9px 5px;">标题:</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" class="layui-input" value="${title || ''}" readonly
|
||||||
|
style="background-color: #f8f8f8; color: #666; font-size: 14px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label" style="width: 80px; padding: 9px 5px;">URL列表:</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<div style="max-height: 40vh; overflow-y: auto; border: 1px solid #e6e6e6; border-radius: 4px; padding: 10px;">
|
||||||
|
<ul style="list-style: none; padding: 0; margin: 0;">${urlHtml || '<li style="padding: 10px; text-align: center; color: #999;">无URL</li>'}</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
} : {
|
||||||
|
area: ['580px', '420px'],
|
||||||
|
content: `
|
||||||
|
<div style="padding: 15px;">
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label" style="width: 100px;">番号:</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" class="layui-input" value="${serial_number || ''}" readonly
|
||||||
|
style="background-color: #f8f8f8; color: #666;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label" style="width: 100px;">标题:</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" class="layui-input" value="${title || ''}" readonly
|
||||||
|
style="background-color: #f8f8f8; color: #666;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label" style="width: 100px;">URL列表:</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #e6e6e6; border-radius: 2px; padding: 0 10px;">
|
||||||
|
<ul style="list-style: none; padding: 0; margin: 0;">${urlHtml || '<li style="padding: 10px; text-align: center; color: #999;">无URL</li>'}</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
const popupIndex = layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: '链接详情',
|
||||||
|
...popupSettings,
|
||||||
|
btn: ['关闭'],
|
||||||
|
success: function (layero) {
|
||||||
|
// 为选择按钮绑定点击事件
|
||||||
|
$(layero).find('.select-url').on('click', function () {
|
||||||
|
const serial = $(this).data('serial');
|
||||||
|
const selectedUrl = $(this).data('url');
|
||||||
|
|
||||||
|
|
||||||
|
// 响应式提示框
|
||||||
|
const msgSettings = isMobile ? {
|
||||||
|
area: ['80vw', 'auto'],
|
||||||
|
time: 4000
|
||||||
|
} : {
|
||||||
|
area: ['400px', 'auto'],
|
||||||
|
time: 3000
|
||||||
|
};
|
||||||
|
const downloadLoading = layer.msg('正在提交下载任务...', {icon: 16, time: 0});
|
||||||
|
fetch('/api/download', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: serial,
|
||||||
|
url: selectedUrl
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('下载任务提交失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
layer.close(downloadLoading);
|
||||||
|
if (data.code === 200) {
|
||||||
|
layer.msg('下载任务提交成功', {icon: 1});
|
||||||
|
// 关闭所有弹出层
|
||||||
|
layer.closeAll();
|
||||||
|
// 重新加载任务列表
|
||||||
|
initTaskList();
|
||||||
|
} else {
|
||||||
|
layer.msg('下载任务提交失败: ' + data.msg, {icon: 2});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
layer.close(downloadLoading);
|
||||||
|
layer.msg('下载任务提交失败: ' + error.message, {icon: 2});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
yes: function(index) {
|
||||||
|
layer.close(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
layer.close(loadingIndex);
|
||||||
|
layer.msg('添加任务失败: ' + error.message, { icon: 2, time: 2000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
initTaskList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -3,636 +3,180 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MISS-Down</title>
|
<title>简洁登录页面</title>
|
||||||
<link rel="stylesheet" href="layui/css/layui.css">
|
<link rel="stylesheet" href="/layui/css/layui.css">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
background-color: #f0f2f5;
|
background: linear-gradient(135deg, #1E9FFF 0%, #0081FF 100%);
|
||||||
color: #333;
|
height: 100vh;
|
||||||
line-height: 1.6;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
.login-container {
|
||||||
.container {
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 400px;
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
|
.login-box {
|
||||||
.task-manager {
|
background-color: white;
|
||||||
background: #fff;
|
border-radius: 8px;
|
||||||
border-radius: 10px;
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 150, 0.1);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.login-header {
|
||||||
.task-header {
|
background: linear-gradient(135deg, #1E9FFF 0%, #0081FF 100%);
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #1e6fde 0%, #0d5bc2 100%);
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
|
||||||
|
|
||||||
.task-title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list {
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-bottom: 1px solid #f0f2ff;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-item:hover {
|
|
||||||
background: #fafbff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-info {
|
|
||||||
flex: 2;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #e6f7ff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 12px;
|
|
||||||
color: #1e6fde;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container {
|
|
||||||
flex: 3;
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-info {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
color: #1e6fde;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-status {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-completed {
|
|
||||||
background: #e6f7ff;
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-processing {
|
|
||||||
background: #f0f6ff;
|
|
||||||
color: #1e6fde;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state i {
|
|
||||||
font-size: 50px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #d9d9d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
color: #1e6fde;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-state i {
|
|
||||||
font-size: 50px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-update {
|
|
||||||
text-align: right;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
border-top: 1px solid #f0f2ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.task-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-info {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-info {
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-status {
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-header {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-title {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-update {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.login-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.login-body {
|
||||||
/* 进度条自定义样式 */
|
padding: 30px 25px;
|
||||||
.layui-progress {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
}
|
||||||
|
.layui-form-item {
|
||||||
.layui-progress-bar {
|
margin-bottom: 20px;
|
||||||
background: linear-gradient(90deg, #1e6fde, #4a90e2);
|
|
||||||
border-radius: 1px;
|
|
||||||
transition: width 0.5s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
.layui-input {
|
||||||
/* 更新动画 */
|
height: 40px;
|
||||||
.progress-updating {
|
border-radius: 4px;
|
||||||
animation: pulse 1s infinite;
|
|
||||||
}
|
}
|
||||||
|
.layui-btn {
|
||||||
@keyframes pulse {
|
height: 40px;
|
||||||
0% {
|
border-radius: 4px;
|
||||||
opacity: 1;
|
background-color: #1E9FFF;
|
||||||
}
|
}
|
||||||
50% {
|
.login-footer {
|
||||||
opacity: 0.7;
|
padding: 0 25px 25px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
100% {
|
.login-footer a {
|
||||||
opacity: 1;
|
color: #1E9FFF;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.login-footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.response-message {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #f0f9eb;
|
||||||
|
color: #67c23a;
|
||||||
|
border: 1px solid #e1f3d8;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #fef0f0;
|
||||||
|
color: #f56c6c;
|
||||||
|
border: 1px solid #fbc4c4;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="login-container">
|
||||||
<div class="task-manager">
|
<div class="login-box">
|
||||||
<div class="task-header">
|
<div class="login-header">
|
||||||
<div class="task-title">任务列表</div>
|
<h2>Miss下载器</h2>
|
||||||
<button class="add-btn" id="addTask">
|
|
||||||
<i class="layui-icon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="login-body">
|
||||||
|
<form class="layui-form" id="loginForm">
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<div class="layui-input-wrap">
|
||||||
|
<input type="text" name="username" placeholder="用户名" autocomplete="off" class="layui-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<div class="layui-input-wrap">
|
||||||
|
<input type="password" name="password"placeholder="密码" autocomplete="off" class="layui-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="login">登录</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="task-list" id="taskList">
|
<div class="response-message" id="responseMessage"></div>
|
||||||
<div class="loading-state">
|
|
||||||
<i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate"></i>
|
|
||||||
<div>正在加载任务数据...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="layui/layui.js"></script>
|
<script src="/layui/layui.js"></script>
|
||||||
<script>
|
<script>
|
||||||
layui.use(['element', 'jquery'], function () {
|
layui.use(['form', 'layer'], function(){
|
||||||
var element = layui.element;
|
var form = layui.form;
|
||||||
|
var layer = layui.layer;
|
||||||
var $ = layui.$;
|
var $ = layui.$;
|
||||||
var updateInterval;
|
|
||||||
var currentTasks = [];
|
|
||||||
|
|
||||||
|
// 表单提交
|
||||||
|
form.on('submit(login)', function(data){
|
||||||
|
var field = data.field;
|
||||||
|
var responseMessage = document.getElementById('responseMessage');
|
||||||
|
|
||||||
// 从API获取任务数据
|
// 显示加载状态
|
||||||
function fetchTasks() {
|
layer.msg('登录中...');
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// 检查token是否存在
|
|
||||||
if (!localStorage.getItem('Authorization')) {
|
|
||||||
reject(new Error('Token不存在'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// AJAX请求获取token
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/login',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
username: field.username,
|
||||||
|
password: field.password
|
||||||
|
}),
|
||||||
|
success: function(response) {
|
||||||
|
layer.closeAll(); // 关闭加载提示
|
||||||
|
|
||||||
// 发起带token的请求
|
if (response && response.data.token) {
|
||||||
fetch('/api/all-task', {
|
// 登录成功
|
||||||
method: 'GET',
|
responseMessage.className = 'response-message success';
|
||||||
headers: {
|
responseMessage.style.display = 'block';
|
||||||
'Authorization': localStorage.getItem('Authorization'),
|
localStorage.setItem('Authorization', response.data.token);
|
||||||
'Content-Type': 'application/json'
|
window.location.href = '/home.html';
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === 401) {
|
|
||||||
location.href='/login.html';
|
|
||||||
}
|
|
||||||
if (!response.ok) {
|
|
||||||
location.href='/login.html';
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
resolve(data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('获取任务数据失败:', error);
|
|
||||||
|
|
||||||
// 如果是认证错误,已经处理过了,这里处理其他错误
|
|
||||||
if (error.message !== '认证失败') {
|
|
||||||
$('#taskList').html(`
|
|
||||||
<div class="error-state">
|
|
||||||
<i class="layui-icon"></i>
|
|
||||||
<div>获取数据失败: ${error.message}</div>
|
|
||||||
<button class="layui-btn layui-btn-primary layui-btn-sm" style="margin-top: 10px;" onclick="initTaskList()">重试</button>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
resolve([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染任务列表
|
|
||||||
function renderTaskList(tasks) {
|
|
||||||
tasks = tasks.data;
|
|
||||||
var taskHtml = '';
|
|
||||||
|
|
||||||
if (tasks.length === 0) {
|
|
||||||
taskHtml = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="layui-icon"></i>
|
|
||||||
<div>暂无任务</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
} else {
|
||||||
tasks.forEach(function (task) {
|
// 登录失败
|
||||||
var statusText = task.status === 'completed' ? '已完成' : '进行中';
|
responseMessage.className = 'response-message error';
|
||||||
var statusClass = task.status === 'completed' ? 'status-completed' : 'status-processing';
|
responseMessage.innerHTML = '登录失败:' + (response.message || '未知错误');
|
||||||
var progressPercent = Math.round(task.progress * 100) + '%';
|
responseMessage.style.display = 'block';
|
||||||
|
|
||||||
taskHtml += `
|
|
||||||
<div class="task-item" data-id="${task.task_id}">
|
|
||||||
<div class="file-info">
|
|
||||||
<div class="file-name">${task.filename}</div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="layui-progress">
|
|
||||||
<div class="layui-progress-bar" lay-percent="${progressPercent}"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-info">${progressPercent}</div>
|
|
||||||
<div class="task-status ${statusClass}">${statusText}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#taskList').html(taskHtml);
|
|
||||||
element.render('progress');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新任务进度(不重新渲染整个列表)
|
|
||||||
function updateTaskProgress(tasks) {
|
|
||||||
tasks = tasks.data;
|
|
||||||
tasks.forEach(function (item) {
|
|
||||||
var taskItem = $(`.task-item[data-id="${item.task_id}"]`);
|
|
||||||
if (taskItem.length > 0) {
|
|
||||||
var progressPercent = Math.round(item.progress * 100) + '%';
|
|
||||||
var statusText = item.status === 'completed' ? '已完成' : '进行中';
|
|
||||||
var statusClass = item.status === 'completed' ? 'status-completed' : 'status-processing';
|
|
||||||
|
|
||||||
// 更新进度条
|
|
||||||
var progressBar = taskItem.find('.layui-progress-bar');
|
|
||||||
progressBar.attr('lay-percent', progressPercent);
|
|
||||||
|
|
||||||
// 添加更新动画
|
|
||||||
progressBar.addClass('progress-updating');
|
|
||||||
|
|
||||||
// 使用动画更新进度
|
|
||||||
setTimeout(function () {
|
|
||||||
progressBar.css('width', progressPercent);
|
|
||||||
progressBar.removeClass('progress-updating');
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// 更新进度文本
|
|
||||||
taskItem.find('.progress-info').text(progressPercent);
|
|
||||||
|
|
||||||
// 更新状态
|
|
||||||
var statusElement = taskItem.find('.task-status');
|
|
||||||
statusElement.text(statusText);
|
|
||||||
statusElement.removeClass('status-completed status-processing').addClass(statusClass);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化任务列表
|
|
||||||
function initTaskList() {
|
|
||||||
fetchTasks().then(tasks => {
|
|
||||||
currentTasks = tasks;
|
|
||||||
renderTaskList(tasks);
|
|
||||||
// 启动定时更新
|
|
||||||
startAutoUpdate();
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('初始化任务列表失败:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动自动更新
|
|
||||||
function startAutoUpdate() {
|
|
||||||
// 清除现有定时器
|
|
||||||
if (updateInterval) {
|
|
||||||
clearInterval(updateInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每5秒更新一次
|
|
||||||
updateInterval = setInterval(function () {
|
|
||||||
fetchTasks().then(tasks => {
|
|
||||||
currentTasks = tasks;
|
|
||||||
updateTaskProgress(tasks);
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新任务
|
|
||||||
$('#addTask').on('click', function () {
|
|
||||||
layer.prompt({
|
|
||||||
title: '请输入URL',
|
|
||||||
formType: 0,
|
|
||||||
}, function (url, index) {
|
|
||||||
layer.close(index);
|
|
||||||
|
|
||||||
const token = localStorage.getItem('Authorization');
|
|
||||||
const loadingIndex = layer.load(2, {
|
|
||||||
shade: [0.1, '#000']
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch(`/api/check/${encodeURIComponent(url)}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': token,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('检查URL失败');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
layer.close(loadingIndex);
|
|
||||||
|
|
||||||
if (data.code !== 200 || !data.data) {
|
|
||||||
layer.msg('接口返回异常', { icon: 2 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { serial_number, title, url: urlList } = data.data;
|
|
||||||
|
|
||||||
// 检测设备类型
|
|
||||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
// 动态生成URL列表HTML - 响应式布局
|
|
||||||
const urlHtml = urlList.map((u, index) => {
|
|
||||||
if (isMobile) {
|
|
||||||
// 手机端布局 - 垂直排列
|
|
||||||
return `
|
|
||||||
<li class="layui-col-xs12" style="padding: 10px 0; border-bottom: 1px solid #f6f6f6;">
|
|
||||||
<div style="word-break: break-all; font-size: 13px; color: #666; margin-bottom: 8px;">
|
|
||||||
${u}
|
|
||||||
</div>
|
|
||||||
<button class="layui-btn layui-btn-sm layui-btn-normal select-url"
|
|
||||||
data-serial="${serial_number}"
|
|
||||||
data-url="${u}"
|
|
||||||
style="width: 100%;">
|
|
||||||
选择
|
|
||||||
</button>
|
|
||||||
</li>`;
|
|
||||||
} else {
|
|
||||||
// 电脑端布局 - 水平排列
|
|
||||||
return `
|
|
||||||
<li class="layui-row" style="padding: 8px 0; border-bottom: 1px solid #f6f6f6; margin: 0;">
|
|
||||||
<div class="layui-col-xs10" style="word-break: break-all; font-size: 13px; color: #666; line-height: 32px;">
|
|
||||||
${u}
|
|
||||||
</div>
|
|
||||||
<div class="layui-col-xs2" style="text-align: right;">
|
|
||||||
<button class="layui-btn layui-btn-sm layui-btn-normal select-url"
|
|
||||||
data-serial="${serial_number}"
|
|
||||||
data-url="${u}">
|
|
||||||
选择
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>`;
|
|
||||||
}
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// 响应式弹窗设置
|
|
||||||
const popupSettings = isMobile ? {
|
|
||||||
area: ['90vw', '70vh'],
|
|
||||||
content: `
|
|
||||||
<div style="padding: 15px;">
|
|
||||||
<div class="layui-form-item" style="margin-bottom: 15px;">
|
|
||||||
<label class="layui-form-label" style="width: 80px; padding: 9px 5px;">番号:</label>
|
|
||||||
<div class="layui-input-block">
|
|
||||||
<input type="text" class="layui-input" value="${serial_number || ''}" readonly
|
|
||||||
style="background-color: #f8f8f8; color: #666; font-size: 14px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="layui-form-item" style="margin-bottom: 15px;">
|
|
||||||
<label class="layui-form-label" style="width: 80px; padding: 9px 5px;">标题:</label>
|
|
||||||
<div class="layui-input-block">
|
|
||||||
<input type="text" class="layui-input" value="${title || ''}" readonly
|
|
||||||
style="background-color: #f8f8f8; color: #666; font-size: 14px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="layui-form-item">
|
|
||||||
<label class="layui-form-label" style="width: 80px; padding: 9px 5px;">URL列表:</label>
|
|
||||||
<div class="layui-input-block">
|
|
||||||
<div style="max-height: 40vh; overflow-y: auto; border: 1px solid #e6e6e6; border-radius: 4px; padding: 10px;">
|
|
||||||
<ul style="list-style: none; padding: 0; margin: 0;">${urlHtml || '<li style="padding: 10px; text-align: center; color: #999;">无URL</li>'}</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
} : {
|
|
||||||
area: ['580px', '420px'],
|
|
||||||
content: `
|
|
||||||
<div style="padding: 15px;">
|
|
||||||
<div class="layui-form-item">
|
|
||||||
<label class="layui-form-label" style="width: 100px;">番号:</label>
|
|
||||||
<div class="layui-input-block">
|
|
||||||
<input type="text" class="layui-input" value="${serial_number || ''}" readonly
|
|
||||||
style="background-color: #f8f8f8; color: #666;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="layui-form-item">
|
|
||||||
<label class="layui-form-label" style="width: 100px;">标题:</label>
|
|
||||||
<div class="layui-input-block">
|
|
||||||
<input type="text" class="layui-input" value="${title || ''}" readonly
|
|
||||||
style="background-color: #f8f8f8; color: #666;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="layui-form-item">
|
|
||||||
<label class="layui-form-label" style="width: 100px;">URL列表:</label>
|
|
||||||
<div class="layui-input-block">
|
|
||||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #e6e6e6; border-radius: 2px; padding: 0 10px;">
|
|
||||||
<ul style="list-style: none; padding: 0; margin: 0;">${urlHtml || '<li style="padding: 10px; text-align: center; color: #999;">无URL</li>'}</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开弹窗
|
|
||||||
const popupIndex = layer.open({
|
|
||||||
type: 1,
|
|
||||||
title: '链接详情',
|
|
||||||
...popupSettings,
|
|
||||||
btn: ['关闭'],
|
|
||||||
success: function (layero) {
|
|
||||||
// 为选择按钮绑定点击事件
|
|
||||||
$(layero).find('.select-url').on('click', function () {
|
|
||||||
const serial = $(this).data('serial');
|
|
||||||
const selectedUrl = $(this).data('url');
|
|
||||||
|
|
||||||
|
|
||||||
// 响应式提示框
|
|
||||||
const msgSettings = isMobile ? {
|
|
||||||
area: ['80vw', 'auto'],
|
|
||||||
time: 4000
|
|
||||||
} : {
|
|
||||||
area: ['400px', 'auto'],
|
|
||||||
time: 3000
|
|
||||||
};
|
|
||||||
const downloadLoading = layer.msg('正在提交下载任务...', {icon: 16, time: 0});
|
|
||||||
fetch('/api/download', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': token,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
error: function(xhr, status, error) {
|
||||||
name: serial,
|
layer.closeAll(); // 关闭加载提示
|
||||||
url: selectedUrl
|
|
||||||
})
|
// 显示错误信息
|
||||||
})
|
responseMessage.className = 'response-message error';
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
if (xhr.status === 401) {
|
||||||
throw new Error('下载任务提交失败');
|
responseMessage.innerHTML = '登录失败:用户名或密码错误';
|
||||||
}
|
} else if (xhr.status === 0) {
|
||||||
return response.json();
|
responseMessage.innerHTML = '网络错误:无法连接到服务器';
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
layer.close(downloadLoading);
|
|
||||||
if (data.code === 200) {
|
|
||||||
layer.msg('下载任务提交成功', {icon: 1});
|
|
||||||
// 关闭所有弹出层
|
|
||||||
layer.closeAll();
|
|
||||||
// 重新加载任务列表
|
|
||||||
initTaskList();
|
|
||||||
} else {
|
} else {
|
||||||
layer.msg('下载任务提交失败: ' + data.msg, {icon: 2});
|
var errorMsg = xhr.responseJSON && xhr.responseJSON.message ?
|
||||||
|
xhr.responseJSON.message :
|
||||||
|
'服务器错误 (' + xhr.status + ')';
|
||||||
|
responseMessage.innerHTML = '登录失败:' + errorMsg;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
responseMessage.style.display = 'block';
|
||||||
layer.close(downloadLoading);
|
|
||||||
layer.msg('下载任务提交失败: ' + error.message, {icon: 2});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
yes: function(index) {
|
|
||||||
layer.close(index);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
layer.close(loadingIndex);
|
|
||||||
layer.msg('添加任务失败: ' + error.message, { icon: 2, time: 2000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
return false; // 阻止表单跳转
|
||||||
initTaskList();
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,183 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MISS-Down</title>
|
|
||||||
<link rel="stylesheet" href="layui/css/layui.css">
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #1E9FFF 0%, #0081FF 100%);
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
.login-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
.login-box {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.login-header {
|
|
||||||
background: linear-gradient(135deg, #1E9FFF 0%, #0081FF 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 30px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.login-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.login-body {
|
|
||||||
padding: 30px 25px;
|
|
||||||
}
|
|
||||||
.layui-form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.layui-input {
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.layui-btn {
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #1E9FFF;
|
|
||||||
}
|
|
||||||
.login-footer {
|
|
||||||
padding: 0 25px 25px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.login-footer a {
|
|
||||||
color: #1E9FFF;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.login-footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.response-message {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-align: center;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
background-color: #f0f9eb;
|
|
||||||
color: #67c23a;
|
|
||||||
border: 1px solid #e1f3d8;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
background-color: #fef0f0;
|
|
||||||
color: #f56c6c;
|
|
||||||
border: 1px solid #fbc4c4;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-container">
|
|
||||||
<div class="login-box">
|
|
||||||
<div class="login-header">
|
|
||||||
<h2>Miss下载器</h2>
|
|
||||||
</div>
|
|
||||||
<div class="login-body">
|
|
||||||
<form class="layui-form" id="loginForm">
|
|
||||||
<div class="layui-form-item">
|
|
||||||
<div class="layui-input-wrap">
|
|
||||||
<input type="text" name="username" placeholder="用户名" autocomplete="off" class="layui-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="layui-form-item">
|
|
||||||
<div class="layui-input-wrap">
|
|
||||||
<input type="password" name="password"placeholder="密码" autocomplete="off" class="layui-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="layui-form-item">
|
|
||||||
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="login">登录</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="response-message" id="responseMessage"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="layui/layui.js"></script>
|
|
||||||
<script>
|
|
||||||
layui.use(['form', 'layer'], function(){
|
|
||||||
var form = layui.form;
|
|
||||||
var layer = layui.layer;
|
|
||||||
var $ = layui.$;
|
|
||||||
|
|
||||||
// 表单提交
|
|
||||||
form.on('submit(login)', function(data){
|
|
||||||
var field = data.field;
|
|
||||||
var responseMessage = document.getElementById('responseMessage');
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
layer.msg('登录中...');
|
|
||||||
|
|
||||||
// AJAX请求获取token
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/login',
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify({
|
|
||||||
username: field.username,
|
|
||||||
password: field.password
|
|
||||||
}),
|
|
||||||
success: function(response) {
|
|
||||||
layer.closeAll(); // 关闭加载提示
|
|
||||||
|
|
||||||
if (response && response.data.token) {
|
|
||||||
// 登录成功
|
|
||||||
responseMessage.className = 'response-message success';
|
|
||||||
responseMessage.style.display = 'block';
|
|
||||||
localStorage.setItem('Authorization', response.data.token);
|
|
||||||
window.location.href = '/index.html';
|
|
||||||
} else {
|
|
||||||
// 登录失败
|
|
||||||
responseMessage.className = 'response-message error';
|
|
||||||
responseMessage.innerHTML = '登录失败:' + (response.message || '未知错误');
|
|
||||||
responseMessage.style.display = 'block';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
layer.closeAll(); // 关闭加载提示
|
|
||||||
|
|
||||||
// 显示错误信息
|
|
||||||
responseMessage.className = 'response-message error';
|
|
||||||
|
|
||||||
if (xhr.status === 401) {
|
|
||||||
responseMessage.innerHTML = '登录失败:用户名或密码错误';
|
|
||||||
} else if (xhr.status === 0) {
|
|
||||||
responseMessage.innerHTML = '网络错误:无法连接到服务器';
|
|
||||||
} else {
|
|
||||||
var errorMsg = xhr.responseJSON && xhr.responseJSON.message ?
|
|
||||||
xhr.responseJSON.message :
|
|
||||||
'服务器错误 (' + xhr.status + ')';
|
|
||||||
responseMessage.innerHTML = '登录失败:' + errorMsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
responseMessage.style.display = 'block';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return false; // 阻止表单跳转
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -21,7 +21,7 @@ http {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /app/html;
|
root /app/html;
|
||||||
try_files $uri $uri/ /home.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
|
|||||||
@ -7,11 +7,12 @@ from functools import wraps
|
|||||||
|
|
||||||
from download import M3U8Downloader
|
from download import M3U8Downloader
|
||||||
from function import crawl_missav
|
from function import crawl_missav
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', '123456')
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key-here')
|
||||||
|
|
||||||
downloader = M3U8Downloader(max_workers=10, output_dir=r"/app/download")
|
downloader = M3U8Downloader(max_workers=10, output_dir=r"download")
|
||||||
|
|
||||||
# 从环境变量获取用户名密码
|
# 从环境变量获取用户名密码
|
||||||
USERNAME = os.getenv('USER')
|
USERNAME = os.getenv('USER')
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import shutil
|
|||||||
|
|
||||||
|
|
||||||
class M3U8Downloader:
|
class M3U8Downloader:
|
||||||
def __init__(self, max_workers=5, output_dir="/app/download", cache_dir="cache"):
|
def __init__(self, max_workers=5, output_dir="/app/downloads", cache_dir="cache"):
|
||||||
self.max_workers = max_workers
|
self.max_workers = max_workers
|
||||||
self.output_dir = Path(output_dir)
|
self.output_dir = Path(output_dir)
|
||||||
self.cache_dir = Path(cache_dir)
|
self.cache_dir = Path(cache_dir)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import shutil
|
|||||||
|
|
||||||
|
|
||||||
class M3U8Downloader:
|
class M3U8Downloader:
|
||||||
def __init__(self, max_workers=5, output_dir="/app/download", cache_dir="cache"):
|
def __init__(self, max_workers=5, output_dir="/app/downloads", cache_dir="cache"):
|
||||||
self.max_workers = max_workers
|
self.max_workers = max_workers
|
||||||
self.output_dir = Path(output_dir)
|
self.output_dir = Path(output_dir)
|
||||||
self.cache_dir = Path(cache_dir)
|
self.cache_dir = Path(cache_dir)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user