Compare commits

..

No commits in common. "main" and "v0.7.7" have entirely different histories.
main ... v0.7.7

10 changed files with 59 additions and 397 deletions

View File

@ -58,7 +58,7 @@
- 媒体库整合 - 媒体库整合
- [x] 根据任务名搜索 Emby 媒体库 - [x] 根据任务名搜索 Emby 媒体库
- [x] 追更或整理后自动刷新 Emby 媒体库 - [x] 追更或整理后自动刷新 Emby 媒体库
- [x] 插件模块化,允许自行开发和挂载[插件](./plugins) - [x] 媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./plugins)
- 其它 - 其它
- [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup> - [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup>
@ -69,7 +69,7 @@
### Docker 部署 ### Docker 部署
Docker 部署提供 WebUI 进行管理配置,部署命令: Docker 部署提供 WebUI 管理配置,图形化配置已能满足绝大多数需求。部署命令:
```shell ```shell
docker run -d \ docker run -d \
@ -113,7 +113,6 @@ services:
| `WEBUI_PASSWORD` | `admin123` | 管理密码 | | `WEBUI_PASSWORD` | `admin123` | 管理密码 |
| `PORT` | `5005` | 管理后台端口 | | `PORT` | `5005` | 管理后台端口 |
| `PLUGIN_FLAGS` | | 插件标志,如 `-emby,-aria2` 禁用某些插件 | | `PLUGIN_FLAGS` | | 插件标志,如 `-emby,-aria2` 禁用某些插件 |
| `TASK_TIMEOUT` | `1800` | 任务执行超时时间(秒),超时则任务结束 |
#### 一键更新 #### 一键更新
@ -163,40 +162,6 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
请参考 Wiki [使用技巧集锦](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦) 请参考 Wiki [使用技巧集锦](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦)
## 生态项目
以下展示 QAS 生态项目,包括官方项目和第三方项目。
### 官方项目
* [QAS一键推送助手](https://greasyfork.org/zh-CN/scripts/533201-qas一键推送助手)
油猴脚本,在夸克网盘分享页面添加推送到 QAS 的按钮
* [SmartStrm](https://github.com/Cp0204/SmartStrm)
STRM 文件生成工具,用于转存后处理,媒体免下载入库播放。
### 第三方开源项目
> [!TIP]
>
> 以下第三方开源项目均由社区开发并保持开源,与 QAS 作者无直接关联。在部署到生产环境前,请自行评估相关风险。
>
> 如果您有新的项目没有在此列出,可以通过 Issues 提交。
* [nonebot-plugin-quark-autosave](https://github.com/fllesser/nonebot-plugin-quark-autosave)
QAS Telegram 机器人,快速管理自动转存任务
* [Astrbot_plugin_quarksave](https://github.com/lm379/astrbot_plugin_quarksave)
AstrBot 插件,调用 quark_auto_save 实现自动转存资源到夸克网盘
* [Telegram 媒体资源管理机器人](https://github.com/2beetle/tgbot)
一个功能丰富的 Telegram 机器人专注于媒体资源管理、Emby 集成、自动下载和夸克网盘资源管理。
## 打赏 ## 打赏
如果这个项目让你受益你可以无偿赠与我1块钱让我知道开源有价值。谢谢 如果这个项目让你受益你可以无偿赠与我1块钱让我知道开源有价值。谢谢

View File

@ -23,7 +23,6 @@ import subprocess
import requests import requests
import hashlib import hashlib
import logging import logging
import traceback
import base64 import base64
import sys import sys
import os import os
@ -33,19 +32,6 @@ parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, parent_dir) sys.path.insert(0, parent_dir)
from quark_auto_save import Quark, Config, MagicRename from quark_auto_save import Quark, Config, MagicRename
print(
r"""
____ ___ _____
/ __ \ / | / ___/
/ / / / / /| | \__ \
/ /_/ / / ___ |___/ /
\___\_\/_/ |_/____/
-- Quark-Auto-Save --
"""
)
sys.stdout.flush()
def get_app_ver(): def get_app_ver():
"""获取应用版本""" """获取应用版本"""
@ -73,7 +59,6 @@ PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true" DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
HOST = os.environ.get("HOST", "0.0.0.0") HOST = os.environ.get("HOST", "0.0.0.0")
PORT = os.environ.get("PORT", 5005) PORT = os.environ.get("PORT", 5005)
TASK_TIMEOUT = int(os.environ.get("TASK_TIMEOUT", 1800))
config_data = {} config_data = {}
task_plugins_config_default = {} task_plugins_config_default = {}
@ -97,8 +82,6 @@ logging.basicConfig(
# 过滤werkzeug日志输出 # 过滤werkzeug日志输出
if not DEBUG: if not DEBUG:
logging.getLogger("werkzeug").setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
logging.getLogger("apscheduler").setLevel(logging.ERROR)
sys.modules["flask.cli"].show_server_banner = lambda *x: None
def gen_md5(string): def gen_md5(string):
@ -487,36 +470,7 @@ def add_task():
# 定时任务执行的函数 # 定时任务执行的函数
def run_python(args): def run_python(args):
logging.info(f">>> 定时运行任务") logging.info(f">>> 定时运行任务")
try: os.system(f"{PYTHON_PATH} {args}")
result = subprocess.run(
f"{PYTHON_PATH} {args}",
shell=True,
timeout=TASK_TIMEOUT,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
# 输出执行日志
if result.stdout:
for line in result.stdout.strip().split("\n"):
if line.strip():
logging.info(line)
if result.returncode == 0:
logging.info(f">>> 任务执行成功")
else:
logging.error(f">>> 任务执行失败,返回码: {result.returncode}")
if result.stderr:
logging.error(f"错误信息: {result.stderr[:500]}")
except subprocess.TimeoutExpired as e:
logging.error(f">>> 任务执行超时(>{TASK_TIMEOUT}s),强制终止")
except Exception as e:
logging.error(f">>> 任务执行异常: {str(e)}")
logging.error(traceback.format_exc())
finally:
# 确保函数能够正常返回
logging.debug(f">>> run_python 函数执行完成")
# 重新加载任务 # 重新加载任务
@ -532,10 +486,6 @@ def reload_tasks():
trigger=trigger, trigger=trigger,
args=[f"{SCRIPT_PATH} {CONFIG_PATH}"], args=[f"{SCRIPT_PATH} {CONFIG_PATH}"],
id=SCRIPT_PATH, id=SCRIPT_PATH,
max_instances=1, # 最多允许1个实例运行
coalesce=True, # 合并错过的任务,避免堆积
misfire_grace_time=300, # 错过任务的宽限期(秒),超过则跳过
replace_existing=True, # 替换已存在的同ID任务
) )
if scheduler.state == 0: if scheduler.state == 0:
scheduler.start() scheduler.start()
@ -554,7 +504,7 @@ def reload_tasks():
def init(): def init():
global config_data, task_plugins_config_default global config_data, task_plugins_config_default
logging.info(">>> 初始化配置") logging.info(f">>> 初始化配置")
# 检查配置文件是否存在 # 检查配置文件是否存在
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
if not os.path.exists(os.path.dirname(CONFIG_PATH)): if not os.path.exists(os.path.dirname(CONFIG_PATH)):
@ -592,8 +542,6 @@ def init():
if __name__ == "__main__": if __name__ == "__main__":
init() init()
reload_tasks() reload_tasks()
logging.info(">>> 启动Web服务")
logging.info(f"运行在: http://{HOST}:{PORT}")
app.run( app.run(
debug=DEBUG, debug=DEBUG,
host=HOST, host=HOST,

View File

@ -45,7 +45,7 @@ body {
margin-bottom: 10px; margin-bottom: 10px;
} }
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child { table.jsoneditor-tree > tbody > tr.jsoneditor-expandable:first-child {
display: none; display: none;
} }
@ -196,48 +196,3 @@ table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
/* Toast */
.toast-container {
position: fixed;
top: 80px;
right: 20px;
z-index: 9999;
width: 300px;
}
.toast {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px;
border-width: 0 0 0 5px !important;
margin-bottom: 10px;
animation: slideIn 0.3s ease forwards;
}
.toast.success {
border-left-color: #28a745 !important;
}
.toast.error {
border-left-color: #dc3545 !important;
}
.toast.warning {
border-left-color: #ffc107 !important;
}
.toast.info {
border-left-color: #17a2b8 !important;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

View File

@ -2,7 +2,7 @@
// @name QAS一键推送助手 // @name QAS一键推送助手
// @namespace https://github.com/Cp0204/quark-auto-save // @namespace https://github.com/Cp0204/quark-auto-save
// @license AGPL // @license AGPL
// @version 0.6 // @version 0.5
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮 // @description 在夸克网盘分享页面添加推送到 QAS 的按钮
// @icon https://pan.quark.cn/favicon.ico // @icon https://pan.quark.cn/favicon.ico
// @author Cp0204 // @author Cp0204
@ -76,16 +76,16 @@
} }
} }
waitForElement('.pc-member-entrance', (PcMemberButton) => { waitForElement('.DetailLayout--client-download--FpyCkdW.ant-dropdown-trigger', (clientDownloadButton) => {
const qasSettingButton = document.createElement('div'); const qasSettingButton = document.createElement('div');
qasSettingButton.className = 'pc-member-entrance'; qasSettingButton.className = 'DetailLayout--client-download--FpyCkdW ant-dropdown-trigger';
qasSettingButton.innerHTML = 'QAS设置'; qasSettingButton.innerHTML = 'QAS设置';
qasSettingButton.addEventListener('click', () => { qasSettingButton.addEventListener('click', () => {
showQASSettingDialog(); showQASSettingDialog();
}); });
PcMemberButton.parentNode.insertBefore(qasSettingButton, PcMemberButton.nextSibling); clientDownloadButton.parentNode.insertBefore(qasSettingButton, clientDownloadButton.nextSibling);
}); });
} }
@ -155,63 +155,6 @@
}, },
data: JSON.stringify(data), data: JSON.stringify(data),
onload: function (response) { onload: function (response) {
// 检查 HTTP 状态码
if (response.status === 401) {
Swal.fire({
title: '认证失败',
text: 'Token 无效或已过期,请重新配置 QAS Token',
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
if (response.status === 503) {
Swal.fire({
title: '服务器不可用',
html: `服务器暂时无法处理请求 (503)<br><br>
<small>可能原因<br>
QAS 服务未运行<br>
服务器过载<br>
网络连接问题</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
// 检查响应内容类型
const contentType = response.responseHeaders.match(/content-type:\s*([^;\s]+)/i);
if (contentType && !contentType[1].includes('application/json')) {
Swal.fire({
title: '认证失败',
html: `服务器返回了非 JSON 响应,可能是 Token 错误<br><br>
<small>响应类型: ${contentType[1]}</small><br>
<small>响应状态: ${response.status}</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
try { try {
const jsonResponse = JSON.parse(response.responseText); const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.success) { if (jsonResponse.success) {
@ -234,34 +177,16 @@
} catch (e) { } catch (e) {
Swal.fire({ Swal.fire({
title: '解析响应失败', title: '解析响应失败',
html: `<small> text: `无法解析 JSON 响应: ${response.responseText}`,
响应状态: ${response.status}<br> icon: 'error'
响应内容: ${response.responseText.substring(0, 200)}...<br><br>
错误详情: ${e.message}
</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
}); });
} }
}, },
onerror: function (error) { onerror: function (error) {
Swal.fire({ Swal.fire({
title: '网络请求失败', title: '任务创建失败',
text: '无法连接到 QAS 服务器,请检查网络连接和服务器地址', text: error,
icon: 'error', icon: 'error'
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
}); });
} }
}); });

View File

@ -310,10 +310,9 @@
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-outline-primary btn-sm" @click="copyTaskToClipboard(index)" title="复制任务参数到粘贴板"><i class=" bi bi-clipboard-check-fill"></i></button> <button class="btn btn-warning" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-fill"></i></button>
<button class="btn btn-warning btn-sm" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-fill"></i></button> <button type="button" class="btn btn-outline-primary" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button>
<button type="button" class="btn btn-outline-primary btn-sm" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button> <button type="button" class="btn btn-outline-danger" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-fill"></i></button>
<button type="button" class="btn btn-outline-danger btn-sm" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-fill"></i></button>
</div> </div>
</div> </div>
<div class="collapse ml-3" :id="'collapse_'+index"> <div class="collapse ml-3" :id="'collapse_'+index">
@ -380,7 +379,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.switchShare=false;fileSelect.previewRegex=true;fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showShareSelect(index)" title="预览正则处理效果">正则处理</button> <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.switchShare=false;fileSelect.previewRegex=true;fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showShareSelect(index)" title="预览正则处理效果">正则处理</button>
</div> </div>
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex" @dblclick="inputRawMagicRegex(task)" title="双击可将魔法匹配释放为填入原始正则表达式"> <input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex">
<input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式"> <input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
<div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存"> <div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存">
<div class="input-group-text"> <div class="input-group-text">
@ -447,10 +446,7 @@
</div> </div>
<div class="row mt-5"> <div class="row mt-5">
<div class="col-sm-12 text-center"> <div class="col-sm-12 text-center">
<div class="btn-group" role="group" aria-label="任务操作">
<button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button> <button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button>
<button type="button" class="btn btn-primary" @click="addTaskForClipboard()" title="从粘贴板导入"><i class="bi bi-clipboard-plus"></i></button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -596,19 +592,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Toast 提示 -->
<div class="toast-container">
<div v-for="toast in toasts" :key="toast.id" class="toast show shadow-sm" :class="toast.type" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body d-flex align-items-center">
<i class="bi mr-2" :class="getToastIcon(toast.type)"></i>
<span>{{ toast.message }}</span>
<button type="button" class="ml-auto close" @click="removeToast(toast.id)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
</div>
</div> </div>
@ -641,7 +624,6 @@
} }
}, },
}, },
toasts: [],
newTask: { newTask: {
taskname: "", taskname: "",
shareurl: "", shareurl: "",
@ -817,10 +799,8 @@
.then(response => { .then(response => {
if (response.data.success) { if (response.data.success) {
this.configModified = false; this.configModified = false;
this.showToast(response.data.message, 'success');
} else {
this.showToast(response.data.message, 'error');
} }
alert(response.data.message);
console.log('Config saved result:', response.data); console.log('Config saved result:', response.data);
}) })
.catch(error => { .catch(error => {
@ -1083,7 +1063,7 @@
updateMagicRegexKey(oldKey, newKey) { updateMagicRegexKey(oldKey, newKey) {
if (oldKey !== newKey) { if (oldKey !== newKey) {
if (this.formData.magic_regex[newKey]) { if (this.formData.magic_regex[newKey]) {
this.showToast(`魔法名 [${newKey}] 已存在,请使用其他名称`, 'warning'); alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
return; return;
} }
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]); this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
@ -1103,7 +1083,7 @@
if (response.data.code == 0) { if (response.data.code == 0) {
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid); this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid);
} else { } else {
this.showToast('删除失败:' + response.data.message, 'error'); alert('删除失败:' + response.data.message);
} }
}).catch(error => { }).catch(error => {
console.error('Error /delete_file:', error); console.error('Error /delete_file:', error);
@ -1186,9 +1166,9 @@
currentIndex = this.smart_param.taskSuggestions.data.indexOf(this.fileSelect.share); currentIndex = this.smart_param.taskSuggestions.data.indexOf(this.fileSelect.share);
nextIndex = currentIndex + index; nextIndex = currentIndex + index;
if (nextIndex < 0) { if (nextIndex < 0) {
this.showToast("没有上一个啦", "info"); alert("没有上一个啦");
} else if (nextIndex >= this.smart_param.taskSuggestions.data.length) { } else if (nextIndex >= this.smart_param.taskSuggestions.data.length) {
this.showToast("没有下一个啦", "info"); alert("没有下一个啦");
} else { } else {
this.fileSelect.error = ""; this.fileSelect.error = "";
this.fileSelect.stoken = ""; this.fileSelect.stoken = "";
@ -1265,86 +1245,7 @@
if (valA > valB) return this.fileSelect.sortOrder === "asc" ? 1 : -1; if (valA > valB) return this.fileSelect.sortOrder === "asc" ? 1 : -1;
return 0; return 0;
}); });
},
inputRawMagicRegex(task) {
const item = this.formData.magic_regex[task.pattern];
if (item) {
task.pattern = item.pattern;
task.replace = item.replace;
} }
},
copyText(text, callback = () => { }) {
if (!text) {
console.error('No text to copy');
return;
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, 99999);
document.execCommand("copy");
document.body.removeChild(textarea);
}
callback()
},
copyTaskToClipboard(index) {
const task = { ...this.formData.tasklist[index] };
delete task.addition;
const _this = this;
this.copyText(JSON.stringify(task), function () {
_this.showToast("任务参数已复制到剪贴板", "success");
});
},
async addTaskForClipboard() {
text = null
try {
text = await navigator.clipboard.readText();
} catch (error) {
text = prompt("当前环境不支持自动读取粘贴板,请手动粘贴任务参数", "");
}
if (text) {
try {
let task = JSON.parse(text);
task = { ...this.newTask, ...task };
this.formData.tasklist.push(task);
this.showToast("剪贴板参数已成功导入任务", "success");
// 滚到最下
setTimeout(() => {
$('#collapse_' + (this.formData.tasklist.length - 1)).collapse('show').on('shown.bs.collapse', () => {
this.scrollToX();
});
}, 1);
} catch (error) {
this.showToast("解析剪贴板内容失败", "error");
}
}
},
showToast(message, type = 'info', duration = 3000) {
const id = Date.now();
this.toasts.push({ id, message, type });
setTimeout(() => {
this.removeToast(id);
}, duration);
},
removeToast(id) {
this.toasts = this.toasts.filter(t => t.id !== id);
},
getToastIcon(type) {
switch (type) {
case 'success': return 'bi-check-circle-fill text-success';
case 'error': return 'bi-exclamation-circle-fill text-danger';
case 'warning': return 'bi-exclamation-triangle-fill text-warning';
default: return 'bi-info-circle-fill text-info';
}
},
} }
}); });
</script> </script>

View File

@ -1,12 +1,10 @@
[ [
"smartstrm",
"fnv_refresh_v2",
"alist", "alist",
"smartstrm",
"alist_strm", "alist_strm",
"alist_strm_gen", "alist_strm_gen",
"alist_sync", "alist_sync",
"aria2", "aria2",
"emby", "emby",
"plex", "plex"
"fnv"
] ]

View File

@ -25,7 +25,6 @@ class Fnv:
default_task_config = { default_task_config = {
"auto_refresh": False, # 是否自动刷新媒体库 "auto_refresh": False, # 是否自动刷新媒体库
"mdb_name": "", # 飞牛影视目标媒体库名称 "mdb_name": "", # 飞牛影视目标媒体库名称
"mdb_dir_list": "", # 飞牛影视目标媒体库文件夹路径列表,多个用逗号分隔
} }
# 定义一个可选键的集合 # 定义一个可选键的集合
@ -85,17 +84,13 @@ class Fnv:
if not target_library_name: if not target_library_name:
print("飞牛影视: 未指定媒体库名称,跳过处理。") print("飞牛影视: 未指定媒体库名称,跳过处理。")
return return
target_library_mdb_dir_list = task_config.get("mdb_dir_list")
dir_list = []
if target_library_mdb_dir_list:
dir_list = [dir_path.strip() for dir_path in target_library_mdb_dir_list.split(",") if dir_path.strip()]
# 获取媒体库ID # 获取媒体库ID
library_id = self._get_library_id(target_library_name) library_id = self._get_library_id(target_library_name)
if library_id: if library_id:
# 获取ID成功后刷新该媒体库 # 获取ID成功后刷新该媒体库
self._refresh_library(library_id, dir_list=dir_list) self._refresh_library(library_id)
# ===================================================================== # =====================================================================
# Internal Methods (内部实现方法) # Internal Methods (内部实现方法)
@ -136,8 +131,7 @@ class Fnv:
try: try:
response = self.session.request( response = self.session.request(
method, url, headers=headers, params=params, method, url, headers=headers, params=params, json=data if data is not None else {}
data=self._serialize_data(data if data is not None else {})
) )
response.raise_for_status() response.raise_for_status()
response_data = response.json() response_data = response.json()
@ -212,7 +206,7 @@ class Fnv:
print(f"飞牛影视: 未在媒体库列表中找到名为 '{library_name}' 的媒体库 ❌") print(f"飞牛影视: 未在媒体库列表中找到名为 '{library_name}' 的媒体库 ❌")
return None return None
def _refresh_library(self, library_id: str, dir_list: list[str] = None) -> bool: def _refresh_library(self, library_id: str) -> bool:
""" """
根据给定的媒体库ID触发一次媒体库扫描/刷新 根据给定的媒体库ID触发一次媒体库扫描/刷新
""" """
@ -220,13 +214,9 @@ class Fnv:
print("飞牛影视: 必须先登录才能刷新媒体库。") print("飞牛影视: 必须先登录才能刷新媒体库。")
return False return False
if dir_list:
print(f"飞牛影视: 正在为媒体库 {library_id} 发送部分目录{dir_list}刷新指令...")
else:
print(f"飞牛影视: 正在为媒体库 {library_id} 发送刷新指令...") print(f"飞牛影视: 正在为媒体库 {library_id} 发送刷新指令...")
rel_url = self.API_MDB_SCAN.format(library_id) rel_url = self.API_MDB_SCAN.format(library_id)
request_body = {"dir_list": dir_list} if dir_list else {} response_json = self._make_request('post', rel_url, data={})
response_json = self._make_request('post', rel_url, data=request_body)
if not response_json: return False if not response_json: return False
@ -273,9 +263,7 @@ class Fnv:
nonce = str(random.randint(100000, 999999)) nonce = str(random.randint(100000, 999999))
timestamp = str(int(time.time() * 1000)) timestamp = str(int(time.time() * 1000))
serialized_str = "" if method.lower() == 'get' and params:
if method.lower() == 'get':
if params:
serialized_str = urlencode(sorted(params.items())) serialized_str = urlencode(sorted(params.items()))
else: else:
serialized_str = self._serialize_data(data) serialized_str = self._serialize_data(data)
@ -304,7 +292,7 @@ class Fnv:
将请求体数据序列化为紧凑的JSON字符串 将请求体数据序列化为紧凑的JSON字符串
""" """
if isinstance(data, dict): if isinstance(data, dict):
return json.dumps(data, sort_keys=True, separators=(',', ':'), ensure_ascii=False) return json.dumps(data, sort_keys=True, separators=(',', ':'))
if isinstance(data, str): if isinstance(data, str):
return data return data
if not data: if not data:

Binary file not shown.

View File

@ -96,26 +96,20 @@ class Config:
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "").split(",") PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "").split(",")
plugins_available = {} plugins_available = {}
task_plugins_config = {} task_plugins_config = {}
# 获取所有模块
py_ext = [".py", ".pyd"] if sys.platform == "win32" else [".py", ".so"]
all_modules = [ all_modules = [
f.replace(ext, "") f.replace(".py", "") for f in os.listdir(plugins_dir) if f.endswith(".py")
for f in os.listdir(plugins_dir)
for ext in py_ext
if f.endswith(ext)
] ]
# 调整模块优先级 # 调整模块优先级
priority_path = os.path.join(plugins_dir, "_priority.json") priority_path = os.path.join(plugins_dir, "_priority.json")
try: try:
with open(priority_path, encoding="utf-8") as f: with open(priority_path, encoding="utf-8") as f:
priority_modules = json.load(f) priority_modules = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
priority_modules = []
if priority_modules: if priority_modules:
all_modules = [ all_modules = [
module for module in priority_modules if module in all_modules module for module in priority_modules if module in all_modules
] + [module for module in all_modules if module not in priority_modules] ] + [module for module in all_modules if module not in priority_modules]
# 加载模块 except (FileNotFoundError, json.JSONDecodeError):
priority_modules = []
for module_name in all_modules: for module_name in all_modules:
if f"-{module_name}" in PLUGIN_FLAGS: if f"-{module_name}" in PLUGIN_FLAGS:
continue continue
@ -134,6 +128,7 @@ class Config:
task_plugins_config[module_name] = plugin.default_task_config task_plugins_config[module_name] = plugin.default_task_config
except (ImportError, AttributeError) as e: except (ImportError, AttributeError) as e:
print(f"载入模块 {module_name} 失败: {e}") print(f"载入模块 {module_name} 失败: {e}")
print()
return plugins_available, plugins_config, task_plugins_config return plugins_available, plugins_config, task_plugins_config
def breaking_change_update(config_data): def breaking_change_update(config_data):
@ -573,8 +568,6 @@ class Quark:
"_fetch_sub_dirs": "0", "_fetch_sub_dirs": "0",
"_sort": "file_type:asc,updated_at:desc", "_sort": "file_type:asc,updated_at:desc",
"_fetch_full_path": kwargs.get("fetch_full_path", 0), "_fetch_full_path": kwargs.get("fetch_full_path", 0),
"fetch_all_file": 1, # 跟随Web端作用未知
"fetch_risk_file_name": 1, # 如无此参数,违规文件名会被变 ***
} }
response = self._send_request("GET", url, params=querystring).json() response = self._send_request("GET", url, params=querystring).json()
if response["code"] != 0: if response["code"] != 0:
@ -627,8 +620,6 @@ class Quark:
"__t": datetime.now().timestamp(), "__t": datetime.now().timestamp(),
} }
response = self._send_request("GET", url, params=querystring).json() response = self._send_request("GET", url, params=querystring).json()
if response["status"] != 200:
return response
if response["data"]["status"] == 2: if response["data"]["status"] == 2:
if retry_index > 0: if retry_index > 0:
print() print()
@ -1005,7 +996,6 @@ class Quark:
if err_msg: if err_msg:
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n") add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
# 建立目录树 # 建立目录树
if len(need_save_list) == len(save_as_top_fids):
for index, item in enumerate(need_save_list): for index, item in enumerate(need_save_list):
icon = self._get_file_icon(item) icon = self._get_file_icon(item)
tree.create_node( tree.create_node(
@ -1115,7 +1105,6 @@ def do_save(account, tasklist=[]):
plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins( plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins(
CONFIG_DATA.get("plugins", {}) CONFIG_DATA.get("plugins", {})
) )
print()
print(f"转存账号: {account.nickname}") print(f"转存账号: {account.nickname}")
# 获取全部保存目录fid # 获取全部保存目录fid
account.update_savepath_fid(tasklist) account.update_savepath_fid(tasklist)
@ -1183,13 +1172,6 @@ def do_save(account, tasklist=[]):
plugin.run(task, account=account, tree=is_new_tree) or task plugin.run(task, account=account, tree=is_new_tree) or task
) )
print() print()
print(f"===============插件收尾===============")
for plugin_name, plugin in plugins.items():
if plugin.is_active and hasattr(plugin, "task_after"):
data = plugin.task_after()
if data.get("config"):
CONFIG_DATA["plugins"][plugin_name] = data["config"]
print()
def main(): def main():

View File

@ -23,10 +23,10 @@
}, },
"$SHOW_MAGIC": { "$SHOW_MAGIC": {
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*", "pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
"replace": "{TASKNAME}.{SXX}E{II}.第{E}期{PART}.{EXT}" "replace": "{II}.{TASKNAME}.{DATE}.第{E}期{PART}.{EXT}"
}, },
"$TV_MAGIC": { "$TV_MAGIC": {
"pattern": ".*\\.(mp4|mkv|mov|m4v|avi|mpeg|ts)$", "pattern": "",
"replace": "{TASKNAME}.{SXX}E{E}.{EXT}" "replace": "{TASKNAME}.{SXX}E{E}.{EXT}"
} }
}, },