Compare commits

...

6 Commits

Author SHA1 Message Date
x1ao4
0c917d12ad 优化顺序命名模式的提示信息 2025-04-21 12:19:12 +08:00
x1ao4
c02d16caa0 优化顺序命名预览界面 2025-04-21 05:54:49 +08:00
x1ao4
e34c039e0c 🔧 增强任务推送成功信息提示 2025-04-21 05:04:52 +08:00
x1ao4
9ebccdcb89 优化 CloudSaver 资源搜索功能 2025-04-21 05:04:36 +08:00
x1ao4
827d1ee60e 📝 更新 CloudSaver 相关说明 2025-04-21 05:01:54 +08:00
x1ao4
027822e127 🔧 优化默认的 $TV 正则表达式 2025-04-21 05:01:42 +08:00
6 changed files with 378 additions and 8 deletions

View File

@ -41,6 +41,7 @@
- [x] 支持分享链接的子目录
- [x] 记录失效分享并跳过任务
- [x] 支持需提取码的分享链接 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#支持需提取码的分享链接)</sup>
- [x] 智能搜索资源并自动填充 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源)</sup>
- 文件管理
- [x] 目标目录不存在时自动新建

162
app/sdk/cloudsaver.py Normal file
View File

@ -0,0 +1,162 @@
import re
import requests
class CloudSaver:
"""
CloudSaver 用于获取云盘资源
"""
def __init__(self, server):
self.server = server
self.username = None
self.password = None
self.token = None
self.session = requests.Session()
self.session.headers.update({"Content-Type": "application/json"})
def set_auth(self, username, password, token=""):
self.username = username
self.password = password
self.token = token
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
def login(self):
if not self.username or not self.password:
return {"success": False, "message": "CloudSaver未设置用户名或密码"}
try:
url = f"{self.server}/api/user/login"
data = {"username": self.username, "password": self.password}
response = self.session.post(url, json=data)
result = response.json()
if result.get("success"):
self.token = result.get("data", {}).get("token")
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
return {"success": True, "token": self.token}
else:
return {
"success": False,
"message": f"CloudSaver登录{result.get('message', '未知错误')}",
}
except Exception as e:
return {"success": False, "message": str(e)}
def search(self, keyword, last_message_id=""):
"""
搜索资源
Args:
keyword (str): 搜索关键词
last_message_id (str): 上一条消息ID用于分页
Returns:
list: 搜索结果列表
"""
try:
url = f"{self.server}/api/search"
params = {"keyword": keyword, "lastMessageId": last_message_id}
response = self.session.get(url, params=params)
result = response.json()
if result.get("success"):
data = result.get("data", [])
return {"success": True, "data": data}
else:
return {"success": False, "message": result.get("message", "未知错误")}
except Exception as e:
return {"success": False, "message": str(e)}
def auto_login_search(self, keyword, last_message_id=""):
"""
自动登录并搜索资源
Args:
keyword (str): 搜索关键词
last_message_id (str): 上一条消息ID用于分页
"""
result = self.search(keyword, last_message_id)
if result.get("success"):
return result
else:
if (
result.get("message") == "无效的 token"
or result.get("message") == "未提供 token"
):
login_result = self.login()
if login_result.get("success"):
result = self.search(keyword, last_message_id)
result["new_token"] = login_result.get("token")
return result
else:
return {
"success": False,
"message": login_result.get("message", "未知错误"),
}
return {"success": False, "message": result.get("message", "未知错误")}
def clean_search_results(self, search_results):
"""
清洗搜索结果
Args:
search_results (list): 搜索结果列表
Returns:
list: 夸克网盘链接列表
"""
pattern_title = r"(名称|标题)[:]?(.*)"
pattern_content = r"(描述|简介)[:]?(.*)(链接|标签)"
clean_results = []
link_array = []
for channel in search_results:
for item in channel.get("list", []):
cloud_links = item.get("cloudLinks", [])
for link in cloud_links:
if link.get("cloudType") == "quark":
# 清洗标题
title = item.get("title", "")
if match := re.search(pattern_title, title, re.DOTALL):
title = match.group(2)
title = title.replace("&amp;", "&").strip()
# 清洗内容
content = item.get("content", "")
if match := re.search(pattern_content, content, re.DOTALL):
content = match.group(2)
content = content.replace('<mark class="highlight">', "")
content = content.replace("</mark>", "")
content = content.strip()
# 链接去重
if link.get("link") not in link_array:
link_array.append(link.get("link"))
clean_results.append(
{
"shareurl": link.get("link"),
"taskname": title,
"content": content,
"tags": item.get("tags", []),
"channel": item.get("channel", ""),
"channel_id": item.get("channelId", ""),
}
)
return clean_results
# 测试示例
if __name__ == "__main__":
# 创建CloudSaver实例
server = ""
username = ""
password = ""
token = ""
cloud_saver = CloudSaver(server)
cloud_saver.set_auth(username, password, token)
# 搜索资源
results = cloud_saver.auto_login_search("黑镜")
# 提取夸克网盘链接
clean_results = cloud_saver.clean_search_results(results.get("data", []))
# 打印结果
for item in clean_results:
print(f"标题: {item['taskname']}")
print(f"描述: {item['content']}")
print(f"链接: {item['shareurl']}")
print(f"标签: {' '.join(item['tags'])}")
print("-" * 50)

View File

@ -42,6 +42,7 @@ body {
.title {
margin-top: 30px;
margin-bottom: 10px;
}
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
@ -59,12 +60,13 @@ table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
.task-suggestions {
width: 100%;
max-height: 500px;
max-height: 250px;
overflow-y: auto;
transform: translate(0, -100%);
top: 0;
margin-top: -5px;
border: 1px solid #007bff;
z-index: 1021;
}
/*
@ -159,4 +161,8 @@ table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}
.cursor-pointer {
cursor: pointer
}

View File

@ -0,0 +1,190 @@
// ==UserScript==
// @name QAS一键推送助手
// @namespace https://github.com/Cp0204/quark-auto-save
// @license AGPL
// @version 0.3
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮
// @icon https://pan.quark.cn/favicon.ico
// @author Cp0204
// @match https://pan.quark.cn/s/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @downloadURL https://update.greasyfork.org/scripts/533201/QAS%E4%B8%80%E9%94%AE%E6%8E%A8%E9%80%81%E5%8A%A9%E6%89%8B.user.js
// @updateURL https://update.greasyfork.org/scripts/533201/QAS%E4%B8%80%E9%94%AE%E6%8E%A8%E9%80%81%E5%8A%A9%E6%89%8B.meta.js
// ==/UserScript==
(function() {
'use strict';
let qas_base = GM_getValue('qas_base', '');
let qas_token = GM_getValue('qas_token', '');
// QAS 设置弹窗函数
function showQASSettingDialog(callback) {
Swal.fire({
title: 'QAS 设置',
html: `
<label for="qas_base">QAS 服务器</label>
<input id="qas_base" class="swal2-input" placeholder="例如: 192.168.1.8:5005" value="${qas_base}">
<label for="qas_token">QAS Token</label>
<input id="qas_token" class="swal2-input" placeholder="v0.5+ 系统配置中查找" value="${qas_token}">
`,
focusConfirm: false,
preConfirm: () => {
qas_base = document.getElementById('qas_base').value;
qas_token = document.getElementById('qas_token').value;
if (!qas_base || !qas_token) {
Swal.showValidationMessage('请填写 QAS 服务器和 Token');
}
return { qas_base: qas_base, qas_token: qas_token }
}
}).then((result) => {
if (result.isConfirmed) {
GM_setValue('qas_base', result.value.qas_base);
GM_setValue('qas_token', result.value.qas_token);
qas_base = result.value.qas_base;
qas_token = result.value.qas_token;
if (callback) {
callback(); // 执行回调函数
}
}
});
}
// 添加 QAS 设置按钮
function addQASSettingButton() {
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
} else {
setTimeout(() => waitForElement(selector, callback), 500);
}
}
waitForElement('.DetailLayout--client-download--FpyCkdW.ant-dropdown-trigger', (clientDownloadButton) => {
const qasSettingButton = document.createElement('div');
qasSettingButton.className = 'DetailLayout--client-download--FpyCkdW ant-dropdown-trigger';
qasSettingButton.innerHTML = 'QAS设置';
qasSettingButton.addEventListener('click', () => {
showQASSettingDialog();
});
clientDownloadButton.parentNode.insertBefore(qasSettingButton, clientDownloadButton.nextSibling);
});
}
// 推送到 QAS 按钮
function addQASButton() {
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
} else {
setTimeout(() => waitForElement(selector, callback), 500);
}
}
waitForElement('.ant-btn.share-save', (saveButton) => {
const qasButton = document.createElement('button');
qasButton.type = 'button';
qasButton.className = 'ant-btn share-save';
qasButton.style.marginLeft = '10px';
qasButton.innerHTML = '<span class="share-save-ico"></span><span>推送到QAS</span>';
let taskname, shareurl, savepath; // 声明变量
// 获取数据函数
function getData() {
const currentUrl = window.location.href;
taskname = currentUrl.lastIndexOf('-') > 0 ? decodeURIComponent(currentUrl.match(/.*\/[^-]+-(.+)$/)[1]) : document.querySelector('.author-name').textContent;
shareurl = currentUrl;
let pathElement = document.querySelector('.path-name')
savepath = pathElement ? pathElement.title.replace('全部文件', '').trim() : "";
savepath += "/" + taskname
qasButton.title = `任务名称: ${taskname}\n分享链接: ${shareurl}\n保存路径: ${savepath}`;
}
// 添加鼠标悬停事件
qasButton.addEventListener('mouseover', () => {
getData(); // 鼠标悬停时获取数据
});
// 添加点击事件
qasButton.addEventListener('click', () => {
getData(); // 点击时重新获取数据,确保最新
const apiUrl = `http://${qas_base}/api/add_task?token=${qas_token}`;
const data = {
"taskname": taskname,
"shareurl": shareurl,
"savepath": savepath,
};
GM_xmlhttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data),
onload: function(response) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.success) {
Swal.fire({
title: '任务创建成功',
html: `<small>
<b>任务名称:</b> ${taskname}<br><br>
<b>保存路径:</b> ${savepath}<br><br>
<a href="http://${qas_base}" target="_blank"> QAS 查看</a>
<small>`,
icon: 'success'
});
} else {
Swal.fire({
title: '任务创建失败',
text: jsonResponse.message,
icon: 'error'
});
}
} catch (e) {
Swal.fire({
title: '解析响应失败',
text: `无法解析 JSON 响应: ${response.responseText}`,
icon: 'error'
});
}
},
onerror: function(error) {
Swal.fire({
title: '任务创建失败',
text: error,
icon: 'error'
});
}
});
});
saveButton.parentNode.insertBefore(qasButton, saveButton.nextSibling);
});
}
// 初始化
(function init() {
addQASSettingButton();
if (!qas_base || !qas_token) {
showQASSettingDialog(() => {
addQASButton(); // 在设置后添加 QAS 按钮
});
} else {
addQASButton(); // 如果配置存在,则直接添加 QAS 按钮
}
})(); // 立即执行初始化
})();

View File

@ -318,15 +318,16 @@
</div>
</div>
</div>
<div class="form-group row" title="可用作筛选,只转存匹配到的文件名的文件,留空则转存所有文件">
<div class="form-group row" :title="task.use_sequence_naming ? '输入带{}占位符的重命名格式,如剧名 - S01E{}、剧名.S03E{}等,{}将被替换为集序号,目录中的文件夹会被自动过滤' : '可用作筛选,只转存匹配到的文件名的文件,留空则转存所有文件'">
<label class="col-sm-2 col-form-label">保存规则</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;showShareSelect(index)" title="预览正则命名效果">{{ task.use_sequence_naming ? '顺序命名' : '正则命名' }}</button>
</div>
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式 (E{} 或 S01E{} 表示顺序命名模式)" list="magicRegex" @input="detectNamingMode(task)">
<input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式" :disabled="task.use_sequence_naming">
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" :placeholder="task.use_sequence_naming ? '输入带{}占位符的重命名格式,如剧名 - S01E{}' : '匹配表达式'" list="magicRegex" @input="detectNamingMode(task)">
<input v-if="!task.use_sequence_naming" type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
<input v-else type="text" class="form-control" disabled value="" style="background-color: #e9ecef;">
<div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存">
<div class="input-group-text">
<input type="checkbox" v-model="task.ignore_extension">&nbsp;忽略后缀
@ -339,11 +340,11 @@
</div>
</div>
<div class="form-group row" title="名称包含过滤词汇的项目不会被转存,多个词用逗号分隔">
<div class="form-group row" title="名称包含过滤词汇的项目不会被转存,多个词用逗号分隔,支持通过文件名和扩展名过滤文件和文件夹">
<label class="col-sm-2 col-form-label">过滤规则</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" name="filterwords[]" class="form-control" v-model="task.filterwords" placeholder="可选,输入过滤词汇,用逗号分隔,如:纯享,加更,超前企划,名称包含过滤词汇的项目不会被转存">
<input type="text" name="filterwords[]" class="form-control" v-model="task.filterwords" placeholder="可选,输入过滤词汇,用逗号分隔,如:纯享,txt,超前企划,名称包含过滤词汇的项目不会被转存">
</div>
</div>
</div>
@ -459,7 +460,7 @@
<!-- 文件列表 -->
<div class="mb-3" v-if="fileSelect.previewRegex">
<div v-if="formData.tasklist[fileSelect.index].use_sequence_naming">
<b>顺序命名式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
<b>顺序命名式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
</div>
<div v-else>
<b>匹配表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
@ -470,7 +471,7 @@
<thead>
<tr>
<th scope="col">文件名</th>
<th scope="col" v-if="fileSelect.selectShare">正则命名</th>
<th scope="col" v-if="fileSelect.selectShare">{{ fileSelect.index !== null && formData.tasklist[fileSelect.index] && formData.tasklist[fileSelect.index].use_sequence_naming ? '顺序命名' : '正则命名' }}</th>
<template v-if="!fileSelect.previewRegex">
<th scope="col">大小</th>
<th scope="col">修改日期 ↓</th>

View File

@ -12,6 +12,16 @@
"token": ""
}
},
"magic_regex": {
"$TV": {
"pattern": ".*?([Ss]\\d{1,2})?(?:[第EePpXx\\.\\-\\_\\( ]{1,2}|^)(\\d{1,3})(?!\\d).*?\\.(mp4|mkv)",
"replace": "\\1E\\2.\\3"
},
"$BLACK_WORD": {
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
"replace": ""
}
},
"tasklist": [
{
"taskname": "测试-魔法匹配剧集这是一组有效分享配置CK后可测试任务是否正常",