mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-17 18:30:43 +08:00
Compare commits
6 Commits
7c20a03c5f
...
0c917d12ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c917d12ad | ||
|
|
c02d16caa0 | ||
|
|
e34c039e0c | ||
|
|
9ebccdcb89 | ||
|
|
827d1ee60e | ||
|
|
027822e127 |
@ -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
162
app/sdk/cloudsaver.py
Normal 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("&", "&").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)
|
||||
@ -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
|
||||
}
|
||||
190
app/static/js/qas.addtask.user.js
Normal file
190
app/static/js/qas.addtask.user.js
Normal 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 按钮
|
||||
}
|
||||
})(); // 立即执行初始化
|
||||
})();
|
||||
@ -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"> 忽略后缀
|
||||
@ -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>
|
||||
|
||||
@ -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后可测试任务是否正常)",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user