mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-15 17:00:43 +08:00
commit
bec5ea1af3
83
app/run.py
83
app/run.py
@ -25,6 +25,8 @@ import base64
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
@ -683,6 +685,18 @@ def add_task():
|
|||||||
# 定时任务执行的函数
|
# 定时任务执行的函数
|
||||||
def run_python(args):
|
def run_python(args):
|
||||||
logging.info(f">>> 定时运行任务")
|
logging.info(f">>> 定时运行任务")
|
||||||
|
# 检查是否需要随机延迟执行
|
||||||
|
if delay := config_data.get("crontab_delay"):
|
||||||
|
try:
|
||||||
|
delay_seconds = int(delay)
|
||||||
|
if delay_seconds > 0:
|
||||||
|
# 在0到设定值之间随机选择一个延迟时间
|
||||||
|
random_delay = random.randint(0, delay_seconds)
|
||||||
|
logging.info(f">>> 随机延迟执行 {random_delay}秒")
|
||||||
|
time.sleep(random_delay)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logging.warning(f">>> 延迟执行设置无效: {delay}")
|
||||||
|
|
||||||
os.system(f"{PYTHON_PATH} {args}")
|
os.system(f"{PYTHON_PATH} {args}")
|
||||||
|
|
||||||
|
|
||||||
@ -708,6 +722,9 @@ def reload_tasks():
|
|||||||
logging.info(">>> 重载调度器")
|
logging.info(">>> 重载调度器")
|
||||||
logging.info(f"调度状态: {scheduler_state_map[scheduler.state]}")
|
logging.info(f"调度状态: {scheduler_state_map[scheduler.state]}")
|
||||||
logging.info(f"定时规则: {crontab}")
|
logging.info(f"定时规则: {crontab}")
|
||||||
|
# 记录延迟执行设置
|
||||||
|
if delay := config_data.get("crontab_delay"):
|
||||||
|
logging.info(f"延迟执行: 0-{delay}秒")
|
||||||
logging.info(f"现有任务: {scheduler.get_jobs()}")
|
logging.info(f"现有任务: {scheduler.get_jobs()}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@ -741,6 +758,10 @@ def init():
|
|||||||
if not config_data.get("crontab"):
|
if not config_data.get("crontab"):
|
||||||
config_data["crontab"] = "0 8,18,20 * * *"
|
config_data["crontab"] = "0 8,18,20 * * *"
|
||||||
|
|
||||||
|
# 默认延迟执行设置
|
||||||
|
if "crontab_delay" not in config_data:
|
||||||
|
config_data["crontab_delay"] = 0
|
||||||
|
|
||||||
# 初始化插件配置
|
# 初始化插件配置
|
||||||
_, plugins_config_default, task_plugins_config_default = Config.load_plugins()
|
_, plugins_config_default, task_plugins_config_default = Config.load_plugins()
|
||||||
plugins_config_default.update(config_data.get("plugins", {}))
|
plugins_config_default.update(config_data.get("plugins", {}))
|
||||||
@ -815,6 +836,63 @@ def get_history_records():
|
|||||||
return jsonify({"success": True, "data": result})
|
return jsonify({"success": True, "data": result})
|
||||||
|
|
||||||
|
|
||||||
|
# 删除转存记录
|
||||||
|
@app.route("/delete_history_records", methods=["POST"])
|
||||||
|
def delete_history_records():
|
||||||
|
if not is_login():
|
||||||
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
|
|
||||||
|
# 获取要删除的记录ID列表
|
||||||
|
record_ids = request.json.get("record_ids", [])
|
||||||
|
|
||||||
|
if not record_ids:
|
||||||
|
return jsonify({"success": False, "message": "未提供要删除的记录ID"})
|
||||||
|
|
||||||
|
# 初始化数据库
|
||||||
|
db = RecordDB()
|
||||||
|
|
||||||
|
# 删除记录
|
||||||
|
deleted_count = 0
|
||||||
|
for record_id in record_ids:
|
||||||
|
deleted_count += db.delete_record(record_id)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": f"成功删除 {deleted_count} 条记录",
|
||||||
|
"deleted_count": deleted_count
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# 删除单条转存记录
|
||||||
|
@app.route("/delete_history_record", methods=["POST"])
|
||||||
|
def delete_history_record():
|
||||||
|
if not is_login():
|
||||||
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
|
|
||||||
|
# 获取要删除的记录ID
|
||||||
|
record_id = request.json.get("id")
|
||||||
|
|
||||||
|
if not record_id:
|
||||||
|
return jsonify({"success": False, "message": "未提供要删除的记录ID"})
|
||||||
|
|
||||||
|
# 初始化数据库
|
||||||
|
db = RecordDB()
|
||||||
|
|
||||||
|
# 删除记录
|
||||||
|
deleted = db.delete_record(record_id)
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "成功删除 1 条记录",
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "记录删除失败,可能记录不存在",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# 辅助函数:格式化记录
|
# 辅助函数:格式化记录
|
||||||
def format_records(records):
|
def format_records(records):
|
||||||
for record in records:
|
for record in records:
|
||||||
@ -871,10 +949,13 @@ def get_user_info():
|
|||||||
"is_active": account.is_active
|
"is_active": account.is_active
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
|
# 检查是否有移动端参数
|
||||||
|
has_mparam = bool(account.mparam)
|
||||||
user_info_list.append({
|
user_info_list.append({
|
||||||
"index": idx,
|
"index": idx,
|
||||||
"nickname": "",
|
"nickname": "",
|
||||||
"is_active": False
|
"is_active": False,
|
||||||
|
"has_mparam": has_mparam
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify({"success": True, "data": user_info_list})
|
return jsonify({"success": True, "data": user_info_list})
|
||||||
|
|||||||
@ -92,6 +92,8 @@ body.login-page {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
width: auto;
|
||||||
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-custom {
|
.toast-custom {
|
||||||
@ -100,13 +102,15 @@ body.login-page {
|
|||||||
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-body-custom {
|
.toast-body-custom {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1rem 0.75rem;
|
padding: 1rem 1.2rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------- 底部按钮 --------------- */
|
/* --------------- 底部按钮 --------------- */
|
||||||
@ -3724,3 +3728,128 @@ input::-moz-list-button {
|
|||||||
background-color: #f7f7fa; /* 表头悬停背景色 */
|
background-color: #f7f7fa; /* 表头悬停背景色 */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移除number类型输入框的上下箭头 */
|
||||||
|
input.no-spinner::-webkit-outer-spin-button,
|
||||||
|
input.no-spinner::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox也需要特别处理 */
|
||||||
|
input.no-spinner {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 秒字框正方形样式 */
|
||||||
|
.square-append {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保Crontab和延迟执行框在移动端也保持一行 */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.row.mb-2 .col-sm-6 {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.mb-2 .col-sm-6.pr-1 {
|
||||||
|
padding-right: 4px !important;
|
||||||
|
padding-left: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.mb-2 .col-sm-6.pl-1 {
|
||||||
|
padding-left: 4px !important;
|
||||||
|
padding-right: 15px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------- 转存记录相关样式 --------------- */
|
||||||
|
.selected-record {
|
||||||
|
background-color: var(--button-gray-background-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除按钮样式 */
|
||||||
|
.delete-record-btn {
|
||||||
|
color: #dc3545;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
visibility: hidden; /* 默认隐藏 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除按钮图标大小 */
|
||||||
|
.delete-record-btn .bi-trash3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中行或鼠标悬停行时显示删除按钮 */
|
||||||
|
tr.selected-record .delete-record-btn,
|
||||||
|
.selectable-records tbody tr:hover .delete-record-btn {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表头中的删除按钮仅在有选中行时显示 */
|
||||||
|
table th .delete-record-btn {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁止在表格中选择文本,以便更好地支持点击选择 */
|
||||||
|
table.selectable-records {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.selectable-records tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改表格行悬停样式,使用变量保持一致性 */
|
||||||
|
table.selectable-records tbody tr:hover {
|
||||||
|
background-color: var(--button-gray-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保展开按钮在选中状态下仍然可见 */
|
||||||
|
tr.selected-record .expand-button {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */
|
||||||
|
table.selectable-records .expand-button:hover,
|
||||||
|
table.selectable-records .delete-record-btn:hover {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中行或鼠标悬停行的大小列样式 */
|
||||||
|
tr.selected-record .file-size-cell .file-size-value,
|
||||||
|
.selectable-records tbody tr:hover .file-size-cell .file-size-value {
|
||||||
|
display: none; /* 隐藏文件大小信息 */
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.selected-record .file-size-cell .delete-record-btn,
|
||||||
|
.selectable-records tbody tr:hover .file-size-cell .delete-record-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start; /* 居左对齐 */
|
||||||
|
align-items: center;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
margin-left: 0; /* 确保没有左边距 */
|
||||||
|
padding-left: 0; /* 确保没有左内边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */
|
||||||
|
table.selectable-records .expand-button:hover {
|
||||||
|
background-color: #fff !important; /* 保持展开按钮原有的白色背景 */
|
||||||
|
}
|
||||||
|
|||||||
@ -252,7 +252,7 @@
|
|||||||
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
|
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
|
||||||
<div class="input-group-prepend" v-if="userInfoList[index]">
|
<div class="input-group-prepend" v-if="userInfoList[index]">
|
||||||
<span class="input-group-text" :style="userInfoList[index].nickname ? (userInfoList[index].is_active ? 'color: var(--dark-text-color);' : 'color: red;') : 'color: var(--dark-text-color);'">
|
<span class="input-group-text" :style="userInfoList[index].nickname ? (userInfoList[index].is_active ? 'color: var(--dark-text-color);' : 'color: red;') : 'color: var(--dark-text-color);'">
|
||||||
{{ userInfoList[index].nickname || '未登录' }}
|
{{ userInfoList[index].nickname || (userInfoList[index].has_mparam ? '仅签到' : '未登录') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group-prepend" v-else>
|
<div class="input-group-prepend" v-else>
|
||||||
@ -273,11 +273,26 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group mb-2">
|
<div class="row mb-2">
|
||||||
<div class="input-group-prepend">
|
<div class="col-sm-6 pr-1">
|
||||||
<span class="input-group-text">Crontab</span>
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Crontab</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" v-model="formData.crontab" class="form-control" placeholder="必填">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 pl-1">
|
||||||
|
<div class="input-group" title="添加随机延迟时间:定时任务将在0到设定秒数之间随机延迟执行,可设范围:0–3600秒,0表示不延迟">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">延迟执行</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" v-model="formData.crontab_delay" class="form-control no-spinner" placeholder="0-3600" @input="validateNumberInput($event, 'crontab_delay', 3600)">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text square-append">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" v-model="formData.crontab" class="form-control" placeholder="必填">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row title" title="通知推送,支持多个渠道,查阅Wiki了解详情">
|
<div class="row title" title="通知推送,支持多个渠道,查阅Wiki了解详情">
|
||||||
@ -670,14 +685,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover selectable-records">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:10%" class="cursor-pointer" @click="sortHistory('transfer_time')">转存日期 <i v-if="historyParams.sortBy === 'transfer_time'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
<th style="width:10%" class="cursor-pointer" @click="sortHistory('transfer_time')">转存日期 <i v-if="historyParams.sortBy === 'transfer_time'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||||||
<th style="width:18%" class="cursor-pointer" @click="sortHistory('task_name')">任务名称 <i v-if="historyParams.sortBy === 'task_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
<th style="width:18%" class="cursor-pointer" @click="sortHistory('task_name')">任务名称 <i v-if="historyParams.sortBy === 'task_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||||||
<th style="width:25%" class="cursor-pointer" @click="sortHistory('original_name')">原文件 <i v-if="historyParams.sortBy === 'original_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
<th style="width:25%" class="cursor-pointer" @click="sortHistory('original_name')">原文件 <i v-if="historyParams.sortBy === 'original_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||||||
<th style="width:25%" class="cursor-pointer" @click="sortHistory('renamed_to')">转存为 <i v-if="historyParams.sortBy === 'renamed_to'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
<th style="width:25%" class="cursor-pointer" @click="sortHistory('renamed_to')">转存为 <i v-if="historyParams.sortBy === 'renamed_to'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||||||
<th style="width:7%" class="cursor-pointer" @click="sortHistory('file_size')">大小 <i v-if="historyParams.sortBy === 'file_size'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
<th style="width:7%" class="cursor-pointer file-size-column" @click="sortHistory('file_size')">大小 <i v-if="historyParams.sortBy === 'file_size'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||||||
<th style="width:10%" class="cursor-pointer" @click="sortHistory('modify_date')">修改日期 <i v-if="historyParams.sortBy === 'modify_date'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
<th style="width:10%" class="cursor-pointer" @click="sortHistory('modify_date')">修改日期 <i v-if="historyParams.sortBy === 'modify_date'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -685,7 +700,9 @@
|
|||||||
<tr v-if="filteredHistoryRecords.length === 0">
|
<tr v-if="filteredHistoryRecords.length === 0">
|
||||||
<td colspan="6" class="text-center">暂无记录</td>
|
<td colspan="6" class="text-center">暂无记录</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="(record, index) in filteredHistoryRecords" :key="record.id">
|
<tr v-for="(record, index) in filteredHistoryRecords" :key="record.id"
|
||||||
|
:class="{'selected-record': selectedRecords.includes(record.id)}"
|
||||||
|
@click="selectRecord($event, record.id)">
|
||||||
<td>{{ record.transfer_time_readable }}</td>
|
<td>{{ record.transfer_time_readable }}</td>
|
||||||
<td class="position-relative">
|
<td class="position-relative">
|
||||||
<div v-if="!record._expandedFields || !record._expandedFields.includes('task_name')"
|
<div v-if="!record._expandedFields || !record._expandedFields.includes('task_name')"
|
||||||
@ -694,7 +711,7 @@
|
|||||||
v-check-overflow="index + '|task_name'">
|
v-check-overflow="index + '|task_name'">
|
||||||
{{ record.task_name }}
|
{{ record.task_name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="expand-button" v-if="isTextTruncated(record.task_name, index, 'task_name')" @click="toggleExpand(index, 'task_name')">
|
<div class="expand-button" v-if="isTextTruncated(record.task_name, index, 'task_name')" @click.stop="toggleExpand(index, 'task_name', $event)">
|
||||||
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('task_name')">
|
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('task_name')">
|
||||||
@ -708,7 +725,7 @@
|
|||||||
v-check-overflow="index + '|original_name'">
|
v-check-overflow="index + '|original_name'">
|
||||||
{{ record.original_name }}
|
{{ record.original_name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="expand-button" v-if="isTextTruncated(record.original_name, index, 'original_name')" @click="toggleExpand(index, 'original_name')">
|
<div class="expand-button" v-if="isTextTruncated(record.original_name, index, 'original_name')" @click.stop="toggleExpand(index, 'original_name', $event)">
|
||||||
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('original_name')">
|
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('original_name')">
|
||||||
@ -722,14 +739,19 @@
|
|||||||
v-check-overflow="index + '|renamed_to'">
|
v-check-overflow="index + '|renamed_to'">
|
||||||
{{ record.renamed_to }}
|
{{ record.renamed_to }}
|
||||||
</div>
|
</div>
|
||||||
<div class="expand-button" v-if="isTextTruncated(record.renamed_to, index, 'renamed_to')" @click="toggleExpand(index, 'renamed_to')">
|
<div class="expand-button" v-if="isTextTruncated(record.renamed_to, index, 'renamed_to')" @click.stop="toggleExpand(index, 'renamed_to', $event)">
|
||||||
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('renamed_to')">
|
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('renamed_to')">
|
||||||
{{ record.renamed_to }}
|
{{ record.renamed_to }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ record.file_size_readable }}</td>
|
<td class="file-size-cell">
|
||||||
|
<span class="file-size-value">{{ record.file_size_readable }}</span>
|
||||||
|
<span class="delete-record-btn" @click.stop="deleteRecord(record.id, record.original_name)" title="删除记录">
|
||||||
|
<i class="bi bi-trash3"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>{{ record.modify_date_readable }}</td>
|
<td>{{ record.modify_date_readable }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -739,7 +761,7 @@
|
|||||||
<!-- 分页控制 -->
|
<!-- 分页控制 -->
|
||||||
<div class="pagination-container d-flex justify-content-between align-items-center mt-3">
|
<div class="pagination-container d-flex justify-content-between align-items-center mt-3">
|
||||||
<div class="page-info text-secondary">
|
<div class="page-info text-secondary">
|
||||||
显示 {{ history.pagination && history.pagination.total_records > 0 ? ((historyParams.page - 1) * historyParams.page_size + 1) + '-' + Math.min(historyParams.page * historyParams.page_size, history.pagination.total_records) : '0' }} 条,共 {{ history.pagination ? history.pagination.total_records : 0 }} 条记录
|
显示 {{ history.pagination && history.pagination.total_records > 0 ? ((historyParams.page - 1) * historyParams.page_size + 1) + '-' + Math.min(historyParams.page * historyParams.page_size, history.pagination.total_records) : '0' }} 条,共 {{ history.pagination ? history.pagination.total_records : 0 }} 条记录{{ selectedRecords.length > 0 ? ',已选中 ' + selectedRecords.length + ' 条记录' : '' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination-controls d-flex align-items-center">
|
<div class="pagination-controls d-flex align-items-center">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: historyParams.page <= 1 }" @click="changePage(historyParams.page - 1)" :disabled="historyParams.page <= 1">
|
<button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: historyParams.page <= 1 }" @click="changePage(historyParams.page - 1)" :disabled="historyParams.page <= 1">
|
||||||
@ -1043,6 +1065,8 @@
|
|||||||
displayedPages: [],
|
displayedPages: [],
|
||||||
allTaskNames: [],
|
allTaskNames: [],
|
||||||
toastMessage: "",
|
toastMessage: "",
|
||||||
|
selectedRecords: [],
|
||||||
|
lastSelectedRecordIndex: -1, // 记录最后选择的记录索引,用于Shift选择
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
episodePatternsText: {
|
episodePatternsText: {
|
||||||
@ -1223,6 +1247,10 @@
|
|||||||
// 只隐藏下拉菜单,不清空搜索结果,这样点击同一任务的输入框时还能看到之前的搜索结果
|
// 只隐藏下拉菜单,不清空搜索结果,这样点击同一任务的输入框时还能看到之前的搜索结果
|
||||||
this.smart_param.showSuggestions = false;
|
this.smart_param.showSuggestions = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加点击事件监听器,用于在点击表格外区域时取消选择记录
|
||||||
|
document.addEventListener('click', this.handleOutsideClick);
|
||||||
|
|
||||||
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
|
|
||||||
// 监听模态框显示事件,检查滚动条状态
|
// 监听模态框显示事件,检查滚动条状态
|
||||||
@ -1282,6 +1310,8 @@
|
|||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
|
// 移除点击事件监听器
|
||||||
|
document.removeEventListener('click', this.handleOutsideClick);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 添加格式化分享链接警告信息的方法
|
// 添加格式化分享链接警告信息的方法
|
||||||
@ -1438,6 +1468,7 @@
|
|||||||
|
|
||||||
return task;
|
return task;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取所有任务父目录
|
// 获取所有任务父目录
|
||||||
config_data.tasklist.forEach(item => {
|
config_data.tasklist.forEach(item => {
|
||||||
parentDir = this.getParentDirectory(item.savepath)
|
parentDir = this.getParentDirectory(item.savepath)
|
||||||
@ -1540,7 +1571,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeCookie(index) {
|
removeCookie(index) {
|
||||||
if (this.formData.cookie[index] == "" || confirm("确认删除吗?")) {
|
if (this.formData.cookie[index] == "" || confirm("确定要删除吗?")) {
|
||||||
this.formData.cookie.splice(index, 1);
|
this.formData.cookie.splice(index, 1);
|
||||||
// 删除对应的用户信息
|
// 删除对应的用户信息
|
||||||
if (this.userInfoList.length > index) {
|
if (this.userInfoList.length > index) {
|
||||||
@ -1558,7 +1589,7 @@
|
|||||||
this.$set(this.formData.push_config, key, "");
|
this.$set(this.formData.push_config, key, "");
|
||||||
},
|
},
|
||||||
removePush(key) {
|
removePush(key) {
|
||||||
if (confirm("确认删除吗?"))
|
if (confirm("确定要删除吗?"))
|
||||||
this.$delete(this.formData.push_config, key);
|
this.$delete(this.formData.push_config, key);
|
||||||
},
|
},
|
||||||
addTask() {
|
addTask() {
|
||||||
@ -1836,7 +1867,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeTask(index) {
|
removeTask(index) {
|
||||||
if (confirm("确认删除任务 [#" + (index + 1) + ": " + this.formData.tasklist[index].taskname + "] 吗?"))
|
if (confirm("确定要删除任务 [#" + (index + 1) + ": " + this.formData.tasklist[index].taskname + "] 吗?"))
|
||||||
this.formData.tasklist.splice(index, 1);
|
this.formData.tasklist.splice(index, 1);
|
||||||
},
|
},
|
||||||
changeShareurl(task) {
|
changeShareurl(task) {
|
||||||
@ -2225,12 +2256,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeMagicRegex(key) {
|
removeMagicRegex(key) {
|
||||||
if (confirm(`确认删除魔法匹配规则 [${key}] 吗?`)) {
|
if (confirm(`确定要删除魔法匹配规则 [${key}] 吗?`)) {
|
||||||
this.$delete(this.formData.magic_regex, key);
|
this.$delete(this.formData.magic_regex, key);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteFile(fid, fname, isDir) {
|
deleteFile(fid, fname, isDir) {
|
||||||
if (fid != "" && confirm(`确认删除${isDir ? '目录' : '文件'} [${fname}] 吗?`))
|
if (fid != "" && confirm(`确定要删除${isDir ? '目录' : '文件'} [${fname}] 吗?`))
|
||||||
axios.post('/delete_file', {
|
axios.post('/delete_file', {
|
||||||
fid: fid
|
fid: fid
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
@ -2782,7 +2813,12 @@
|
|||||||
|
|
||||||
return record._isOverflowing && record._isOverflowing[field];
|
return record._isOverflowing && record._isOverflowing[field];
|
||||||
},
|
},
|
||||||
toggleExpand(index, field) {
|
toggleExpand(index, field, event) {
|
||||||
|
// 阻止事件冒泡,避免触发行选择
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前记录
|
// 获取当前记录
|
||||||
const record = this.filteredHistoryRecords[index];
|
const record = this.filteredHistoryRecords[index];
|
||||||
// 初始化_expandedFields属性(如果不存在)
|
// 初始化_expandedFields属性(如果不存在)
|
||||||
@ -3072,6 +3108,186 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
validateNumberInput(event, field, max) {
|
||||||
|
// 获取当前输入值
|
||||||
|
let value = event.target.value;
|
||||||
|
// 获取输入框的当前光标位置
|
||||||
|
const cursorPosition = event.target.selectionStart;
|
||||||
|
|
||||||
|
// 记录原始长度
|
||||||
|
const originalLength = value.length;
|
||||||
|
|
||||||
|
// 移除非数字字符
|
||||||
|
const cleanValue = value.replace(/[^\d]/g, '');
|
||||||
|
|
||||||
|
// 如果有非数字字符被移除
|
||||||
|
if (cleanValue !== value) {
|
||||||
|
// 计算移除了多少个字符
|
||||||
|
const diff = originalLength - cleanValue.length;
|
||||||
|
|
||||||
|
// 更新输入框的值
|
||||||
|
event.target.value = cleanValue;
|
||||||
|
|
||||||
|
// 调整光标位置(考虑到字符被移除)
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.setSelectionRange(Math.max(0, cursorPosition - diff), Math.max(0, cursorPosition - diff));
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保不超过最大值
|
||||||
|
if (cleanValue !== '' && parseInt(cleanValue) > max) {
|
||||||
|
event.target.value = max.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据模型,如果为空则默认为0
|
||||||
|
this.formData[field] = event.target.value === '' ? 0 : parseInt(event.target.value);
|
||||||
|
},
|
||||||
|
selectRecord(event, recordId) {
|
||||||
|
// 获取当前记录的索引
|
||||||
|
const currentIndex = this.filteredHistoryRecords.findIndex(record => record.id === recordId);
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
// 如果是Shift+点击,选择范围
|
||||||
|
if (event.shiftKey && this.selectedRecords.length > 0) {
|
||||||
|
// 找出所有已选中记录的索引
|
||||||
|
const selectedIndices = this.selectedRecords.map(id =>
|
||||||
|
this.filteredHistoryRecords.findIndex(record => record.id === id)
|
||||||
|
).filter(index => index !== -1); // 过滤掉未找到的记录
|
||||||
|
|
||||||
|
if (selectedIndices.length > 0) {
|
||||||
|
// 找出已选中记录中最靠前的索引
|
||||||
|
const earliestSelectedIndex = Math.min(...selectedIndices);
|
||||||
|
// 确定最终的选择范围
|
||||||
|
const startIndex = Math.min(earliestSelectedIndex, currentIndex);
|
||||||
|
const endIndex = Math.max(earliestSelectedIndex, currentIndex);
|
||||||
|
|
||||||
|
// 获取新的选择范围内所有记录的ID
|
||||||
|
this.selectedRecords = this.filteredHistoryRecords
|
||||||
|
.slice(startIndex, endIndex + 1)
|
||||||
|
.map(record => record.id);
|
||||||
|
} else {
|
||||||
|
// 如果没有有效的选中记录(可能是由于列表刷新),则只选择当前记录
|
||||||
|
this.selectedRecords = [recordId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果是Ctrl/Cmd+点击,切换单个记录选择状态
|
||||||
|
else if (event.ctrlKey || event.metaKey) {
|
||||||
|
if (this.selectedRecords.includes(recordId)) {
|
||||||
|
this.selectedRecords = this.selectedRecords.filter(id => id !== recordId);
|
||||||
|
} else {
|
||||||
|
this.selectedRecords.push(recordId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 普通点击,清除当前选择并选择当前记录
|
||||||
|
else {
|
||||||
|
if (this.selectedRecords.length === 1 && this.selectedRecords.includes(recordId)) {
|
||||||
|
this.selectedRecords = [];
|
||||||
|
} else {
|
||||||
|
this.selectedRecords = [recordId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后选择的记录索引,只有在有选择记录时才更新
|
||||||
|
if (this.selectedRecords.length > 0) {
|
||||||
|
this.lastSelectedRecordIndex = currentIndex;
|
||||||
|
} else {
|
||||||
|
this.lastSelectedRecordIndex = -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteSelectedRecords() {
|
||||||
|
if (this.selectedRecords.length === 0) return;
|
||||||
|
|
||||||
|
if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条记录吗?`)) {
|
||||||
|
axios.post('/delete_history_records', {
|
||||||
|
record_ids: this.selectedRecords
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.showToast(response.data.message);
|
||||||
|
// 重新加载记录
|
||||||
|
this.loadHistoryRecords();
|
||||||
|
// 清空选择
|
||||||
|
this.selectedRecords = [];
|
||||||
|
this.lastSelectedRecordIndex = -1;
|
||||||
|
} else {
|
||||||
|
alert(response.data.message || '删除失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('删除记录时出错:', error);
|
||||||
|
alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteRecord(id, original_name) {
|
||||||
|
// 检查是否有多条记录被选中
|
||||||
|
if (this.selectedRecords.length > 1) {
|
||||||
|
// 如果有多条记录被选中,调用批量删除方法
|
||||||
|
if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条记录吗?`)) {
|
||||||
|
axios.post('/delete_history_records', {
|
||||||
|
record_ids: this.selectedRecords
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.showToast(`成功删除 ${this.selectedRecords.length} 条记录`);
|
||||||
|
// 重新加载记录
|
||||||
|
this.loadHistoryRecords();
|
||||||
|
// 清空选择
|
||||||
|
this.selectedRecords = [];
|
||||||
|
this.lastSelectedRecordIndex = -1;
|
||||||
|
} else {
|
||||||
|
alert(response.data.message || '删除失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('删除记录时出错:', error);
|
||||||
|
alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单条记录删除
|
||||||
|
if (confirm(`确定要删除此条记录吗?`)) {
|
||||||
|
axios.post('/delete_history_record', {
|
||||||
|
id: id
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.showToast('成功删除 1 条记录');
|
||||||
|
// 重新加载记录
|
||||||
|
this.loadHistoryRecords();
|
||||||
|
// 清空选择
|
||||||
|
this.selectedRecords = [];
|
||||||
|
this.lastSelectedRecordIndex = -1;
|
||||||
|
} else {
|
||||||
|
alert(response.data.message || '删除失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('删除记录时出错:', error);
|
||||||
|
alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 处理点击表格外区域的事件
|
||||||
|
handleOutsideClick(event) {
|
||||||
|
// 如果当前不是历史记录页面或者没有选中的记录,则不处理
|
||||||
|
if (this.activeTab !== 'history' || this.selectedRecords.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查点击是否在表格内
|
||||||
|
const tableElement = document.querySelector('.table.selectable-records');
|
||||||
|
// 检查点击是否在分页控制区域
|
||||||
|
const paginationElement = document.querySelector('.pagination-container');
|
||||||
|
|
||||||
|
// 如果点击不在表格内且不在分页控制区域内,则清除选择
|
||||||
|
if (tableElement && !tableElement.contains(event.target) &&
|
||||||
|
paginationElement && !paginationElement.contains(event.target)) {
|
||||||
|
this.selectedRecords = [];
|
||||||
|
this.lastSelectedRecordIndex = -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
268
plugins/alist.py
268
plugins/alist.py
@ -2,6 +2,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
class Alist:
|
class Alist:
|
||||||
@ -17,32 +18,82 @@ class Alist:
|
|||||||
quark_root_dir = None
|
quark_root_dir = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
"""初始化AList插件"""
|
||||||
|
# 标记插件名称,便于日志识别
|
||||||
|
self.plugin_name = self.__class__.__name__.lower()
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
# 加载配置
|
||||||
for key, _ in self.default_config.items():
|
for key, _ in self.default_config.items():
|
||||||
if key in kwargs:
|
if key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
else:
|
else:
|
||||||
print(f"{self.__class__.__name__} 模块缺少必要参数: {key}")
|
print(f"{self.plugin_name} 模块缺少必要参数: {key}")
|
||||||
if self.url and self.token:
|
|
||||||
if self.get_info():
|
# 检查基本配置
|
||||||
success, result = self.storage_id_to_path(self.storage_id)
|
if not self.url or not self.token or not self.storage_id:
|
||||||
if success:
|
return
|
||||||
self.storage_mount_path, self.quark_root_dir = result
|
|
||||||
self.is_active = True
|
# 确保URL格式正确
|
||||||
|
if not self.url.startswith(("http://", "https://")):
|
||||||
|
self.url = f"http://{self.url}"
|
||||||
|
|
||||||
|
# 移除URL末尾的斜杠
|
||||||
|
self.url = self.url.rstrip("/")
|
||||||
|
|
||||||
|
# 验证AList连接
|
||||||
|
if self.get_info():
|
||||||
|
# 解析存储ID
|
||||||
|
success, result = self.storage_id_to_path(self.storage_id)
|
||||||
|
if success:
|
||||||
|
self.storage_mount_path, self.quark_root_dir = result
|
||||||
|
|
||||||
|
# 确保路径格式正确
|
||||||
|
if self.quark_root_dir != "/":
|
||||||
|
if not self.quark_root_dir.startswith("/"):
|
||||||
|
self.quark_root_dir = f"/{self.quark_root_dir}"
|
||||||
|
self.quark_root_dir = self.quark_root_dir.rstrip("/")
|
||||||
|
|
||||||
|
if not self.storage_mount_path.startswith("/"):
|
||||||
|
self.storage_mount_path = f"/{self.storage_mount_path}"
|
||||||
|
self.storage_mount_path = self.storage_mount_path.rstrip("/")
|
||||||
|
|
||||||
|
self.is_active = True
|
||||||
|
else:
|
||||||
|
print(f"AList 刷新: 存储信息解析失败")
|
||||||
|
else:
|
||||||
|
print(f"AList 刷新: 服务器连接失败")
|
||||||
|
|
||||||
def run(self, task, **kwargs):
|
def run(self, task, **kwargs):
|
||||||
if task.get("savepath") and task.get("savepath").startswith(
|
"""
|
||||||
self.quark_root_dir
|
插件主入口,当有新文件保存时触发刷新AList目录
|
||||||
):
|
|
||||||
alist_path = os.path.normpath(
|
Args:
|
||||||
os.path.join(
|
task: 任务信息,包含savepath等关键信息
|
||||||
self.storage_mount_path,
|
**kwargs: 其他参数,包括tree和rename_logs
|
||||||
task["savepath"].replace(self.quark_root_dir, "", 1).lstrip("/"),
|
|
||||||
)
|
Returns:
|
||||||
).replace("\\", "/")
|
task: 返回原任务信息
|
||||||
self.refresh(alist_path)
|
"""
|
||||||
|
# 检查路径是否在夸克根目录内
|
||||||
|
if task.get("savepath"):
|
||||||
|
# 确保路径符合要求
|
||||||
|
quark_path = task.get("savepath", "")
|
||||||
|
if not quark_path.startswith("/"):
|
||||||
|
quark_path = f"/{quark_path}"
|
||||||
|
|
||||||
|
# 检查路径是否在夸克根目录下,或夸克根目录是否为根目录
|
||||||
|
if self.quark_root_dir == "/" or quark_path.startswith(self.quark_root_dir):
|
||||||
|
# 映射到AList路径
|
||||||
|
alist_path = self.map_quark_to_alist_path(quark_path)
|
||||||
|
|
||||||
|
# 执行刷新
|
||||||
|
self.refresh(alist_path)
|
||||||
|
|
||||||
|
return task
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
|
"""获取AList服务器信息"""
|
||||||
url = f"{self.url}/api/admin/setting/list"
|
url = f"{self.url}/api/admin/setting/list"
|
||||||
headers = {"Authorization": self.token}
|
headers = {"Authorization": self.token}
|
||||||
querystring = {"group": "1"}
|
querystring = {"group": "1"}
|
||||||
@ -51,26 +102,35 @@ class Alist:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
response = response.json()
|
response = response.json()
|
||||||
if response.get("code") == 200:
|
if response.get("code") == 200:
|
||||||
print(
|
print(f"AList 刷新: {response.get('data',[])[1].get('value','')} {response.get('data',[])[0].get('value','')}")
|
||||||
f"AList 刷新: {response.get('data',[])[1].get('value','')} {response.get('data',[])[0].get('value','')}"
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"AList 刷新: 连接失败 ❌ {response.get('message')}")
|
print(f"AList 刷新: 连接失败 ❌ {response.get('message')}")
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"获取 AList 信息出错: {e}")
|
print(f"AList 刷新: 连接出错 ❌ {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def storage_id_to_path(self, storage_id):
|
def storage_id_to_path(self, storage_id):
|
||||||
|
"""
|
||||||
|
将存储ID转换为挂载路径和夸克根目录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_id: 存储ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (成功状态, (挂载路径, 夸克根目录))
|
||||||
|
"""
|
||||||
storage_mount_path, quark_root_dir = None, None
|
storage_mount_path, quark_root_dir = None, None
|
||||||
|
|
||||||
# 1. 检查是否符合 /aaa:/bbb 格式
|
# 1. 检查是否符合 /aaa:/bbb 格式
|
||||||
if match := re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id):
|
if match := re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id):
|
||||||
# 存储挂载路径, 夸克根文件夹
|
# 存储挂载路径, 夸克根文件夹
|
||||||
storage_mount_path, quark_root_dir = match.group(1), match.group(2)
|
storage_mount_path, quark_root_dir = match.group(1), match.group(2)
|
||||||
file_list = self.get_file_list(storage_mount_path)
|
file_list = self.get_file_list(storage_mount_path)
|
||||||
if file_list.get("code") != 200:
|
if file_list.get("code") != 200:
|
||||||
print(f"AList 刷新: 获取挂载路径失败 ❌ {file_list.get('message')}")
|
print(f"AList 刷新: 挂载路径无效")
|
||||||
return False, (None, None)
|
return False, (None, None)
|
||||||
|
|
||||||
# 2. 检查是否数字,调用 Alist API 获取存储信息
|
# 2. 检查是否数字,调用 Alist API 获取存储信息
|
||||||
elif re.match(r"^\d+$", storage_id):
|
elif re.match(r"^\d+$", storage_id):
|
||||||
if storage_info := self.get_storage_info(storage_id):
|
if storage_info := self.get_storage_info(storage_id):
|
||||||
@ -82,14 +142,15 @@ class Alist:
|
|||||||
quark_root_dir = self.get_root_folder_full_path(
|
quark_root_dir = self.get_root_folder_full_path(
|
||||||
addition["cookie"], addition["root_folder_id"]
|
addition["cookie"], addition["root_folder_id"]
|
||||||
)
|
)
|
||||||
elif storage_info["driver"] == "QuarkTV":
|
|
||||||
print(
|
|
||||||
f"AList 刷新: [QuarkTV] 驱动 ⚠️ storage_id 请手动填入 /Alist挂载路径:/Quark目录路径"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print(f"AList 刷新: 不支持 [{storage_info['driver']}] 驱动 ❌")
|
print(f"AList 刷新: 不支持 [{storage_info['driver']}] 驱动")
|
||||||
|
else:
|
||||||
|
print(f"AList 刷新: 获取存储信息失败")
|
||||||
|
return False, (None, None)
|
||||||
else:
|
else:
|
||||||
print(f"AList 刷新: storage_id [{storage_id}] 格式错误 ❌")
|
print(f"AList 刷新: storage_id 格式错误")
|
||||||
|
return False, (None, None)
|
||||||
|
|
||||||
# 返回结果
|
# 返回结果
|
||||||
if storage_mount_path and quark_root_dir:
|
if storage_mount_path and quark_root_dir:
|
||||||
return True, (storage_mount_path, quark_root_dir)
|
return True, (storage_mount_path, quark_root_dir)
|
||||||
@ -97,6 +158,7 @@ class Alist:
|
|||||||
return False, (None, None)
|
return False, (None, None)
|
||||||
|
|
||||||
def get_storage_info(self, storage_id):
|
def get_storage_info(self, storage_id):
|
||||||
|
"""获取AList存储详细信息"""
|
||||||
url = f"{self.url}/api/admin/storage/get"
|
url = f"{self.url}/api/admin/storage/get"
|
||||||
headers = {"Authorization": self.token}
|
headers = {"Authorization": self.token}
|
||||||
querystring = {"id": storage_id}
|
querystring = {"id": storage_id}
|
||||||
@ -105,34 +167,119 @@ class Alist:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if data.get("code") == 200:
|
if data.get("code") == 200:
|
||||||
return data.get("data", [])
|
return data.get("data", {})
|
||||||
else:
|
else:
|
||||||
print(f"AList 刷新: 存储 {storage_id} 连接失败 ❌ {data.get('message')}")
|
print(f"AList 刷新: 获取存储信息失败 ({data.get('message', '未知错误')})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"AList 刷新: 获取 AList 存储出错 {e}")
|
print(f"AList 刷新: 获取存储信息出错 ({str(e)})")
|
||||||
return []
|
return None
|
||||||
|
|
||||||
def refresh(self, path):
|
def refresh(self, path, retry_count=2):
|
||||||
data = self.get_file_list(path, True)
|
"""
|
||||||
if data.get("code") == 200:
|
刷新AList目录,支持重试和自动回溯到父目录
|
||||||
print(f"📁 AList 刷新: 目录 [{path}] 成功 ✅")
|
|
||||||
return data.get("data")
|
Args:
|
||||||
elif "object not found" in data.get("message", ""):
|
path: 需要刷新的路径
|
||||||
# 如果是根目录就不再往上查找
|
retry_count: 重试次数,默认重试2次
|
||||||
if path == "/" or path == self.storage_mount_path:
|
"""
|
||||||
print(f"📁 AList 刷新: 根目录不存在,请检查 AList 配置")
|
# 实现重试机制
|
||||||
return False
|
for attempt in range(retry_count + 1):
|
||||||
# 获取父目录
|
if attempt > 0:
|
||||||
parent_path = os.path.dirname(path)
|
# 不输出重试信息
|
||||||
print(f"📁 AList 刷新: [{path}] 不存在,转父目录 [{parent_path}]")
|
pass
|
||||||
# 递归刷新父目录
|
|
||||||
return self.refresh(parent_path)
|
data = self.get_file_list(path, True)
|
||||||
|
|
||||||
|
if data.get("code") == 200:
|
||||||
|
print(f"📁 刷新 AList 目录: [{path}] 成功 ✅")
|
||||||
|
return data.get("data")
|
||||||
|
elif "object not found" in data.get("message", ""):
|
||||||
|
# 如果是根目录就不再往上查找
|
||||||
|
if path == "/" or path == self.storage_mount_path:
|
||||||
|
print(f"📁 AList 刷新: 根目录不存在,请检查配置")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 自动获取父目录并尝试刷新
|
||||||
|
parent_path = os.path.dirname(path)
|
||||||
|
|
||||||
|
# 先刷新父目录
|
||||||
|
parent_result = self.get_file_list(parent_path, True)
|
||||||
|
if parent_result.get("code") == 200:
|
||||||
|
# 再次尝试刷新原目录
|
||||||
|
retry_data = self.get_file_list(path, True)
|
||||||
|
if retry_data.get("code") == 200:
|
||||||
|
print(f"📁 刷新 AList 目录: [{path}] 成功 ✅")
|
||||||
|
return retry_data.get("data")
|
||||||
|
|
||||||
|
# 如果刷新父目录后仍不成功,则递归处理父目录
|
||||||
|
return self.refresh(parent_path, retry_count)
|
||||||
|
elif attempt < retry_count:
|
||||||
|
# 如果还有重试次数,等待后继续
|
||||||
|
time.sleep(1) # 等待1秒后重试
|
||||||
|
else:
|
||||||
|
# 已达到最大重试次数
|
||||||
|
error_msg = data.get("message", "未知错误")
|
||||||
|
print(f"📁 AList 刷新: 失败 ❌ {error_msg}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def map_quark_to_alist_path(self, quark_path):
|
||||||
|
"""
|
||||||
|
将夸克路径映射到AList路径
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quark_path: 夸克路径,例如 /Movies/2024
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 对应的AList路径,例如 /movies/2024
|
||||||
|
"""
|
||||||
|
# 确保路径格式正确
|
||||||
|
if not quark_path.startswith("/"):
|
||||||
|
quark_path = f"/{quark_path}"
|
||||||
|
|
||||||
|
# 特殊处理根目录的情况
|
||||||
|
if self.quark_root_dir == "/":
|
||||||
|
# 夸克根目录是/,直接映射到AList挂载路径
|
||||||
|
if quark_path == "/":
|
||||||
|
return self.storage_mount_path
|
||||||
|
else:
|
||||||
|
# 组合路径
|
||||||
|
alist_path = os.path.normpath(
|
||||||
|
os.path.join(self.storage_mount_path, quark_path.lstrip("/"))
|
||||||
|
).replace("\\", "/")
|
||||||
|
return alist_path
|
||||||
|
|
||||||
|
# 常规情况:检查路径是否在夸克根目录下
|
||||||
|
if not quark_path.startswith(self.quark_root_dir):
|
||||||
|
# 尝试强制映射,去掉前导路径
|
||||||
|
relative_path = quark_path.lstrip("/")
|
||||||
else:
|
else:
|
||||||
print(f"📁 AList 刷新: 失败 ❌ {data.get('message')}")
|
# 去除夸克根目录前缀,并确保路径格式正确
|
||||||
|
relative_path = quark_path.replace(self.quark_root_dir, "", 1).lstrip("/")
|
||||||
|
|
||||||
|
# 构建AList路径
|
||||||
|
alist_path = os.path.normpath(
|
||||||
|
os.path.join(self.storage_mount_path, relative_path)
|
||||||
|
).replace("\\", "/")
|
||||||
|
|
||||||
|
return alist_path
|
||||||
|
|
||||||
def get_file_list(self, path, force_refresh=False):
|
def get_file_list(self, path, force_refresh=False):
|
||||||
|
"""
|
||||||
|
获取AList指定路径下的文件列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: AList文件路径
|
||||||
|
force_refresh: 是否强制刷新,默认False
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: AList API返回的数据
|
||||||
|
"""
|
||||||
url = f"{self.url}/api/fs/list"
|
url = f"{self.url}/api/fs/list"
|
||||||
headers = {"Authorization": self.token}
|
headers = {
|
||||||
|
"Authorization": self.token,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
payload = {
|
payload = {
|
||||||
"path": path,
|
"path": path,
|
||||||
"refresh": force_refresh,
|
"refresh": force_refresh,
|
||||||
@ -140,15 +287,28 @@ class Alist:
|
|||||||
"page": 1,
|
"page": 1,
|
||||||
"per_page": 0,
|
"per_page": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.request("POST", url, headers=headers, json=payload)
|
response = requests.request(
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"📁 AList 刷新: 网络请求出错 ❌ {str(e)}")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"📁 AList 刷新: 解析数据出错 ❌ {str(e)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"📁 AList 刷新: 获取文件列表出错 ❌ {e}")
|
print(f"📁 AList 刷新: 未知错误 ❌ {str(e)}")
|
||||||
return {}
|
|
||||||
|
return {"code": 500, "message": "获取文件列表出错"}
|
||||||
|
|
||||||
def get_root_folder_full_path(self, cookie, pdir_fid):
|
def get_root_folder_full_path(self, cookie, pdir_fid):
|
||||||
|
"""获取夸克根文件夹的完整路径"""
|
||||||
if pdir_fid == "0":
|
if pdir_fid == "0":
|
||||||
return "/"
|
return "/"
|
||||||
url = "https://drive-h.quark.cn/1/clouddrive/file/sort"
|
url = "https://drive-h.quark.cn/1/clouddrive/file/sort"
|
||||||
@ -178,5 +338,5 @@ class Alist:
|
|||||||
path = f"{path}/{item['file_name']}"
|
path = f"{path}/{item['file_name']}"
|
||||||
return path
|
return path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"AList 刷新: 获取 Quark 路径出错 {e}")
|
print(f"AList 刷新: 获取路径出错 ❌ {str(e)}")
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -3004,7 +3004,6 @@ def verify_account(account):
|
|||||||
# 验证账号
|
# 验证账号
|
||||||
print(f"▶️ 验证第 {account.index} 个账号")
|
print(f"▶️ 验证第 {account.index} 个账号")
|
||||||
if "__uid" not in account.cookie:
|
if "__uid" not in account.cookie:
|
||||||
print(f"💡 不存在 cookie 必要参数,判断为仅签到")
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
account_info = account.init()
|
account_info = account.init()
|
||||||
@ -3052,7 +3051,8 @@ def do_sign(account):
|
|||||||
):
|
):
|
||||||
print(message)
|
print(message)
|
||||||
else:
|
else:
|
||||||
message = message.replace("今日", f"[{account.nickname}]今日")
|
if account.nickname:
|
||||||
|
message = message.replace("今日", f"{account.nickname} 今日")
|
||||||
add_notify(message)
|
add_notify(message)
|
||||||
else:
|
else:
|
||||||
print(f"📅 签到异常: {sign_return}")
|
print(f"📅 签到异常: {sign_return}")
|
||||||
@ -4061,24 +4061,65 @@ def do_save(account, tasklist=[]):
|
|||||||
if not display_files and file_nodes:
|
if not display_files and file_nodes:
|
||||||
# 查找目录中修改时间最新的文件(可能是刚刚转存的)
|
# 查找目录中修改时间最新的文件(可能是刚刚转存的)
|
||||||
today = datetime.now().strftime('%Y-%m-%d')
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
recent_files = []
|
recent_files = [] # 定义并初始化recent_files变量
|
||||||
|
|
||||||
# 首先尝试通过修改日期过滤当天的文件
|
# 首先尝试通过修改日期过滤当天的文件
|
||||||
for file in file_nodes:
|
for file in file_nodes:
|
||||||
# 如果有时间戳,转换为日期字符串
|
# 如果有时间戳,转换为日期字符串
|
||||||
if 'updated_at' in file and file['updated_at']:
|
if 'updated_at' in file and file['updated_at']:
|
||||||
update_time = datetime.fromtimestamp(file['updated_at']).strftime('%Y-%m-%d')
|
try:
|
||||||
if update_time == today:
|
# 检查时间戳是否在合理范围内 (1970-2100年)
|
||||||
recent_files.append(file)
|
timestamp = file['updated_at']
|
||||||
|
if timestamp > 4102444800: # 2100年的时间戳
|
||||||
|
# 可能是毫秒级时间戳,尝试转换为秒级
|
||||||
|
timestamp = timestamp / 1000
|
||||||
|
|
||||||
|
# 再次检查时间戳是否在合理范围内
|
||||||
|
if 0 < timestamp < 4102444800:
|
||||||
|
update_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d')
|
||||||
|
if update_time == today:
|
||||||
|
recent_files.append(file)
|
||||||
|
else:
|
||||||
|
print(f"警告: 文件 {file.get('file_name', '未知')} 的时间戳 {file['updated_at']} 超出范围")
|
||||||
|
except (ValueError, OSError, OverflowError) as e:
|
||||||
|
print(f"警告: 处理文件 {file.get('file_name', '未知')} 的时间戳时出错: {e}")
|
||||||
|
|
||||||
# 如果没有找到当天的文件,至少显示一个最新的文件
|
# 如果没有找到当天的文件,至少显示一个最新的文件
|
||||||
if not recent_files and file_nodes:
|
if not recent_files and file_nodes:
|
||||||
# 按修改时间排序
|
# 定义安全的排序键函数
|
||||||
recent_files = sorted(file_nodes, key=lambda x: x.get('updated_at', 0), reverse=True)
|
def safe_timestamp_key(x):
|
||||||
|
try:
|
||||||
|
timestamp = x.get('updated_at', 0)
|
||||||
|
# 如果时间戳太大,可能是毫秒级时间戳
|
||||||
|
if timestamp > 4102444800: # 2100年的时间戳
|
||||||
|
timestamp = timestamp / 1000
|
||||||
|
# 再次检查范围
|
||||||
|
if timestamp < 0 or timestamp > 4102444800:
|
||||||
|
return 0 # 无效时间戳返回0
|
||||||
|
return timestamp
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0 # 无效返回0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 按修改时间排序,使用安全的排序函数
|
||||||
|
recent_files = sorted(file_nodes, key=safe_timestamp_key, reverse=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"警告: 文件排序时出错: {e}")
|
||||||
|
# 如果排序出错,直接使用原始列表
|
||||||
|
recent_files = file_nodes
|
||||||
|
|
||||||
# 只取第一个作为显示
|
# 只取第一个作为显示
|
||||||
if recent_files:
|
if recent_files:
|
||||||
display_files.append(recent_files[0]['file_name'])
|
try:
|
||||||
|
display_files.append(recent_files[0]['file_name'])
|
||||||
|
except (IndexError, KeyError) as e:
|
||||||
|
print(f"警告: 获取文件名时出错: {e}")
|
||||||
|
# 如果出错,尝试添加第一个文件(如果有)
|
||||||
|
if file_nodes:
|
||||||
|
try:
|
||||||
|
display_files.append(file_nodes[0]['file_name'])
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
print("警告: 无法获取有效的文件名")
|
||||||
|
|
||||||
# 添加成功通知 - 修复问题:确保在有文件时添加通知
|
# 添加成功通知 - 修复问题:确保在有文件时添加通知
|
||||||
if display_files:
|
if display_files:
|
||||||
@ -4242,7 +4283,6 @@ def do_save(account, tasklist=[]):
|
|||||||
)
|
)
|
||||||
elif is_new_tree is False: # 明确没有新文件
|
elif is_new_tree is False: # 明确没有新文件
|
||||||
print(f"任务完成: 没有新的文件需要转存")
|
print(f"任务完成: 没有新的文件需要转存")
|
||||||
print()
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user