daemon插件v2

在右上角添加按钮并点击发布

目前为 2025-03-01 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name daemon插件v2
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2
  5. // @description 在右上角添加按钮并点击发布
  6. // @author Your name
  7. // @match http*://*/upload.php*
  8. // @match http*://*/details.php*
  9. // @match http*://*/edit.php*
  10. // @match http*://*/torrents.php*
  11. // @match https://kp.m-team.cc/detail/*
  12. // @match https://*/torrents*
  13. // @match https://totheglory.im/t/*
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_download
  18.  
  19. // @license MIT
  20. // ==/UserScript==
  21.  
  22. // 在脚本开头添加样式表
  23. const style = document.createElement('style');
  24. style.textContent = `
  25. /* 消息框样式 */
  26. .daemon-msg {
  27. position: fixed;
  28. top: 10px;
  29. left: 50%;
  30. transform: translateX(-50%);
  31. z-index: 9999;
  32. width: 50%;
  33. padding: 5px;
  34. background: rgba(230, 247, 255, 0.8); /* 浅蓝色背景,透明度为 0.8 */
  35. border: 1px solid #000; /* 黑色边框 */
  36. border-radius: 4px;
  37. font: bold 14px/1.4 Arial; /* 修改字体大小和加粗 */
  38. resize: none;
  39. overflow: auto;
  40. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  41. overflow-wrap: break-word; /* 确保内容在边框宽度时换行 */
  42. margin: 0; /* 减少空白部分 */
  43. white-space: pre-wrap; /* 保留换行和空白 */
  44. word-break: break-all; /* 允许长单词换行 */
  45. }
  46.  
  47. /* 失败/错误样式 */
  48. .daemon-msg-fail {
  49. color: red; /* 新增字体颜色设置 */
  50. font: bold 18px/1.4 Arial; /* 修改字体大小和加粗 */
  51. }
  52.  
  53. /* 列表项样式 */
  54. .list-item {
  55. margin-bottom: 10px;
  56. }
  57.  
  58. .list-item h3 {
  59. font-size: 14px;
  60. margin: 0 0 5px;
  61. }
  62.  
  63. .list-item p {
  64. margin: 0;
  65. }
  66.  
  67. .status-flag {
  68. display: inline-block;
  69. padding: 2px 6px;
  70. border-radius: 3px;
  71. font-size: 0.8em;
  72. }
  73.  
  74. .true-flag {
  75. background-color: #d4edda;
  76. color: #155724;
  77. }
  78.  
  79. .false-flag {
  80. background-color: #f8d7da;
  81. color: #721c24;
  82. }
  83. `;
  84. // 在样式表中添加新样式
  85. style.textContent += `
  86. /* 容器标题栏 */
  87. .list-header {
  88. position: relative;
  89. padding: 8px 30px 8px 15px;
  90. background: #f8f9fa;
  91. border-bottom: 1px solid #ddd;
  92. }
  93.  
  94. /* 折叠按钮 */
  95. .toggle-btn {
  96. position: absolute;
  97. right: 8px;
  98. top: 50%;
  99. transform: translateY(-50%);
  100. background: none;
  101. border: none;
  102. cursor: pointer;
  103. padding: 2px 5px;
  104. border-radius: 3px;
  105. transition: 0.2s;
  106. }
  107.  
  108. .toggle-btn:hover {
  109. background: rgba(0,0,0,0.1);
  110. }
  111.  
  112. /* 内容区域过渡动画 */
  113. .list-content {
  114. max-height: 500px;
  115. overflow: auto;
  116. transition: max-height 0.3s ease-out;
  117. }
  118.  
  119. .list-content.collapsed {
  120. max-height: 0;
  121. overflow: hidden;
  122. }
  123. `;
  124. // 在样式表中添加新样式
  125. style.textContent += `
  126. .daemon-list {
  127. position: fixed;
  128. top: 50%;
  129. left: 50%;
  130. transform: translate(-50%, -50%);
  131. width: 70%;
  132. // max-width: 1000px;
  133. max-height: 80vh;
  134. background: white;
  135. box-shadow: 0 0 20px rgba(0,0,0,0.2);
  136. z-index: 10000;
  137. transition: all 0.3s ease;
  138. display: none; /* 默认隐藏 */
  139. }
  140.  
  141. .daemon-list.visible {
  142. display: block;
  143. }
  144.  
  145. .close-btn {
  146. position: absolute;
  147. right: 15px;
  148. top: 12px;
  149. background: #dc3545;
  150. color: white;
  151. border: none;
  152. border-radius: 50%;
  153. width: 24px;
  154. height: 24px;
  155. cursor: pointer;
  156. display: flex;
  157. align-items: center;
  158. justify-content: center;
  159. transition: 0.2s;
  160. }
  161.  
  162. .close-btn:hover {
  163. background: #bb2d3b;
  164. transform: scale(1.1);
  165. }
  166. `;
  167. // 在样式表中添加删除按钮样式
  168. style.textContent += `
  169. .delete-btn {
  170. padding: 4px 8px;
  171. background: #dc3545;
  172. color: white;
  173. border: none;
  174. border-radius: 4px;
  175. cursor: pointer;
  176. transition: 0.2s;
  177. font-size: 12px;
  178. }
  179.  
  180. .delete-btn:hover {
  181. background: #bb2d3b;
  182. transform: scale(1.05);
  183. }
  184.  
  185. .loading {
  186. position: relative;
  187. pointer-events: none;
  188. opacity: 0.7;
  189. }
  190.  
  191. .loading::after {
  192. content: "";
  193. position: absolute;
  194. top: 50%;
  195. left: 50%;
  196. width: 16px;
  197. height: 16px;
  198. border: 2px solid #fff;
  199. border-radius: 50%;
  200. border-top-color: transparent;
  201. animation: spin 0.8s linear infinite;
  202. transform: translate(-50%, -50%);
  203. }
  204.  
  205. @keyframes spin {
  206. to { transform: translate(-50%, -50%) rotate(360deg); }
  207. }
  208. `;
  209. style.textContent += `
  210. /* 按钮容器样式 */
  211. #daemon-btn-container {
  212. position: fixed;
  213. right: 20px;
  214. top: 250px;
  215. z-index: 99999;
  216. display: flex;
  217. flex-direction: column;
  218. gap: 8px;
  219. cursor: move;
  220. background: rgba(255,255,255,0.1);
  221. padding: 10px;
  222. border-radius: 8px;
  223. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  224. transition: all 0.3s ease;
  225. }
  226.  
  227. /* 单个按钮样式 */
  228. .daemon-btn {
  229. padding: 12px 20px;
  230. background: linear-gradient(145deg, #e3f2fd, #bbdefb);
  231. color: #1976d2;
  232. border: none;
  233. border-radius: 6px;
  234. cursor: pointer;
  235. font: bold 14px 'Microsoft YaHei';
  236. box-shadow: 0 2px 4px rgba(25,118,210,0.2);
  237. transition: all 0.2s ease;
  238. position: relative;
  239. }
  240.  
  241. /* 按钮悬停效果 */
  242. .daemon-btn:hover {
  243. background: linear-gradient(145deg, #bbdefb, #90caf9);
  244. box-shadow: 0 4px 8px rgba(25,118,210,0.3);
  245. transform: translateY(-2px);
  246. }
  247.  
  248. /* 拖拽手柄样式 */
  249. .drag-handle {
  250. position: absolute;
  251. left: 4px;
  252. top: 50%;
  253. transform: translateY(-50%);
  254. width: 16px;
  255. height: 24px;
  256. opacity: 0.5;
  257. cursor: move;
  258. background:
  259. linear-gradient(to bottom,
  260. #666 20%,
  261. transparent 20%,
  262. transparent 40%,
  263. #666 40%,
  264. #666 60%,
  265. transparent 60%,
  266. transparent 80%,
  267. #666 80%
  268. );
  269. }
  270. `;
  271. // 在样式表中添加边界限制提示
  272. style.textContent += `
  273. #daemon-btn-container.boundary-hit {
  274. animation: boundary-shake 0.4s ease;
  275. }
  276.  
  277. @keyframes boundary-shake {
  278. 0%, 100% { transform: translate(0, 0); }
  279. 20% { transform: translate(-5px, 0); }
  280. 40% { transform: translate(5px, 0); }
  281. 60% { transform: translate(-3px, 0); }
  282. 80% { transform: translate(3px, 0); }
  283. }
  284. `;
  285.  
  286. style.textContent += `
  287. /* 主表格样式 */
  288. .daemon-table {
  289. width: 98%;
  290. border-collapse: collapse;
  291. margin-top: 10px;
  292. color: #000;
  293. }
  294.  
  295. .daemon-table th,
  296. .daemon-table td {
  297. padding: 8px;
  298. border: 1px solid #ddd;
  299. text-align: left;
  300. font-size: 12px;
  301. vertical-align: top;
  302. color: #000 !important; /* 新增强制黑色字体 */
  303. text-align: center; /* 文本居中 */
  304. vertical-align: middle; /* 垂直居中 */
  305. }
  306.  
  307. .daemon-table th {
  308. background-color: #f8f9fa;
  309. }
  310.  
  311. /* 嵌套表格样式 */
  312. .nested-table {
  313. width: 100%;
  314. border-collapse: collapse;
  315. margin-top: 5px;
  316. color: #000;
  317. }
  318.  
  319. .nested-table th,
  320. .nested-table td {
  321. padding: 5px;
  322. border: 1px solid #ccc;
  323. text-align: left;
  324. font-size: 10px;
  325. vertical-align: top;
  326. color: #000 !important; /* 新增强制黑色字体 */
  327. text-align: center; /* 文本居中 */
  328. vertical-align: middle; /* 垂直居中 */
  329. }
  330.  
  331. .nested-table th {
  332. background-color: #e9ecef;
  333. }
  334.  
  335. /* 操作按钮样式 */
  336. .action-buttons {
  337. display: flex;
  338. gap: 5px;
  339. }
  340.  
  341. .delete-btn, .force-push-btn {
  342. padding: 4px 8px;
  343. border: none;
  344. border-radius: 4px;
  345. cursor: pointer;
  346. font-size: 12px;
  347. }
  348.  
  349. .delete-btn {
  350. background-color: #dc3545;
  351. color: white;
  352. }
  353.  
  354. .delete-btn:hover {
  355. background-color: #bb2d3b;
  356. }
  357.  
  358. .force-push-btn {
  359. background-color: #28a745;
  360. color: white;
  361. }
  362.  
  363. .force-push-btn:hover {
  364. background-color: #218838;
  365. }
  366.  
  367. /* 刷新按钮样式 */
  368. .refresh-btn {
  369. background: none;
  370. border: none;
  371. cursor: pointer;
  372. font-size: 1.2em;
  373. margin-left: 10px;
  374. }
  375.  
  376. .refresh-btn:hover {
  377. color: #007bff;
  378. }
  379.  
  380. `;
  381. // daemon接口配置
  382. var apiurl = '';
  383. var deployapiurl = '';
  384. var listapiurl = '';
  385. var deleteapiurl = '';
  386.  
  387. // 初始化配置
  388. var config = {};
  389. initconfig();
  390.  
  391. const container = createListContainer();
  392.  
  393. var atBottom = false;
  394. // 页面加载完成后执行
  395. var site_url = decodeURI(window.location.href);
  396.  
  397. function initconfig() {
  398. config = loadConfig();
  399. debugger;
  400. apiurl = `${config.apidomain}/add_torrent`;
  401. deployapiurl = `${config.apidomain}/force_deploy`;
  402. listapiurl = `${config.apidomain}/get_info`;
  403. deleteapiurl = `${config.apidomain}/del_torrent`;
  404. }
  405. // 配置管理部分
  406. function loadConfig() {
  407. const defaultConfig = {
  408. apidomain: 'https://xx.xx.xx:8443',
  409. apikey: 'defaultKey',
  410. buttons: {
  411. panel: true,
  412. leechtorrent: true
  413. }
  414. };
  415.  
  416. const saved = GM_getValue('daemon_config', '');
  417. if (saved) {
  418. try {
  419. return JSON.parse(saved);
  420. } catch (error) {
  421. console.error('配置解析失败,使用默认配置:', error);
  422. return defaultConfig;
  423. }
  424. }
  425. return defaultConfig;
  426. }
  427.  
  428. function saveConfig(config) {
  429. GM_setValue('daemon_config', JSON.stringify(config));
  430. }
  431.  
  432.  
  433. // 设置按钮处理函数
  434. function handleSettings() {
  435. // 创建弹出框容器
  436. const modal = document.createElement('div');
  437. modal.style.position = 'fixed';
  438. modal.style.top = '50%';
  439. modal.style.left = '50%';
  440. modal.style.transform = 'translate(-50%, -50%)';
  441. modal.style.backgroundColor = '#fff';
  442. modal.style.padding = '20px';
  443. modal.style.borderRadius = '8px';
  444. modal.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
  445. modal.style.zIndex = '10000';
  446. modal.style.width = '400px';
  447.  
  448. // 创建标题
  449. const title = document.createElement('h3');
  450. title.textContent = '配置设置';
  451. title.style.marginTop = '0';
  452. modal.appendChild(title);
  453.  
  454. // 创建配置输入框
  455. const textarea = document.createElement('textarea');
  456. textarea.style.width = '100%';
  457. textarea.style.height = '200px';
  458. textarea.style.marginBottom = '10px';
  459. textarea.style.padding = '8px';
  460. textarea.style.border = '1px solid #ddd';
  461. textarea.style.borderRadius = '4px';
  462. textarea.style.fontFamily = 'Arial, sans-serif';
  463. textarea.style.fontSize = '14px';
  464.  
  465. // 加载当前配置
  466. textarea.value = JSON.stringify(config, null, 2);
  467. modal.appendChild(textarea);
  468.  
  469. // 创建保存按钮
  470. const saveButton = document.createElement('button');
  471. saveButton.textContent = '保存';
  472. saveButton.style.padding = '8px 16px';
  473. saveButton.style.backgroundColor = '#007bff';
  474. saveButton.style.color = '#fff';
  475. saveButton.style.border = 'none';
  476. saveButton.style.borderRadius = '4px';
  477. saveButton.style.cursor = 'pointer';
  478. saveButton.style.marginRight = '10px';
  479.  
  480. // 创建取消按钮
  481. const cancelButton = document.createElement('button');
  482. cancelButton.textContent = '取消';
  483. cancelButton.style.padding = '8px 16px';
  484. cancelButton.style.backgroundColor = '#dc3545';
  485. cancelButton.style.color = '#fff';
  486. cancelButton.style.border = 'none';
  487. cancelButton.style.borderRadius = '4px';
  488. cancelButton.style.cursor = 'pointer';
  489.  
  490. // 保存配置
  491. saveButton.addEventListener('click', () => {
  492. try {
  493. const config = JSON.parse(textarea.value);
  494.  
  495. // 保存配置到存储
  496. saveConfig(config);
  497. initconfig();
  498. addMsg('配置已保存!请刷新界面');
  499.  
  500. // 关闭弹出框
  501. document.body.removeChild(modal);
  502. } catch (error) {
  503. addMsg('配置格式错误,请检查 JSON 格式', 'error');
  504. }
  505. });
  506.  
  507. // 取消操作
  508. cancelButton.addEventListener('click', () => {
  509. document.body.removeChild(modal);
  510. });
  511.  
  512. // 添加按钮到弹出框
  513. const buttonContainer = document.createElement('div');
  514. buttonContainer.style.textAlign = 'right';
  515. buttonContainer.appendChild(saveButton);
  516. buttonContainer.appendChild(cancelButton);
  517. modal.appendChild(buttonContainer);
  518.  
  519. // 添加弹出框到页面
  520. document.body.appendChild(modal);
  521. }
  522.  
  523. // ==================== 拖拽 开始 ====================
  524. // 创建可拖拽容器
  525. const btnContainer = document.createElement('div');
  526. btnContainer.id = 'daemon-btn-container';
  527.  
  528. // 修改初始化位置加载部分
  529. const savedPosition = GM_getValue('btn_position', null);
  530. const containerRect = btnContainer.getBoundingClientRect();
  531. const defaultPosition = { x: 20, y: 250 };
  532.  
  533. // 拖拽功能实现
  534. let isDragging = false;
  535. let startX, startY;
  536. let initialX, initialY;
  537. let dragThreshold = 5; // 触发拖拽的阈值
  538.  
  539. // 新增位置修正函数
  540. function validatePosition(pos) {
  541. const viewportWidth = window.innerWidth;
  542. const viewportHeight = window.innerHeight;
  543. const containerWidth = btnContainer.offsetWidth;
  544. const containerHeight = btnContainer.offsetHeight;
  545.  
  546. return {
  547. x: Math.min(Math.max(pos.x, 0), viewportWidth - containerWidth - 10),
  548. y: Math.min(Math.max(pos.y, 10), viewportHeight - containerHeight - 10)
  549. };
  550. }
  551.  
  552. // 应用初始位置
  553. if (savedPosition) {
  554. const validPos = validatePosition(savedPosition);
  555. btnContainer.style.right = `${validPos.x}px`;
  556. btnContainer.style.top = `${validPos.y}px`;
  557. } else {
  558. btnContainer.style.right = `${defaultPosition.x}px`;
  559. btnContainer.style.top = `${defaultPosition.y}px`;
  560. }
  561.  
  562.  
  563. btnContainer.addEventListener('mousedown', function (e) {
  564. startX = e.clientX;
  565. startY = e.clientY;
  566. initialX = parseFloat(this.style.right) || 20;
  567. initialY = parseFloat(this.style.top) || 250;
  568. isDragging = false;
  569. });
  570.  
  571.  
  572. // 修改拖拽事件处理
  573. document.addEventListener('mousemove', function (e) {
  574. if (startX === undefined) return;
  575.  
  576. const deltaX = Math.abs(e.clientX - startX);
  577. const deltaY = Math.abs(e.clientY - startY);
  578.  
  579. if (!isDragging && (deltaX > dragThreshold || deltaY > dragThreshold)) {
  580. isDragging = true;
  581. btnContainer.style.transition = 'none';
  582. }
  583.  
  584. if (isDragging) {
  585. const viewportWidth = window.innerWidth;
  586. const viewportHeight = window.innerHeight;
  587. const containerRect = btnContainer.getBoundingClientRect();
  588.  
  589. // 计算边界限制
  590. let newX = initialX + (startX - e.clientX);
  591. let newY = initialY + (e.clientY - startY);
  592.  
  593. // X轴边界检查
  594. newX = Math.max(10, Math.min(newX, viewportWidth - containerRect.width - 10));
  595.  
  596. // Y轴边界检查
  597. newY = Math.max(10, Math.min(newY, viewportHeight - containerRect.height - 10));
  598.  
  599. // 应用限制后的位置
  600. btnContainer.style.right = `${newX}px`;
  601. btnContainer.style.top = `${newY}px`;
  602.  
  603. // 边界碰撞提示
  604. if (newX <= 10 || newX >= viewportWidth - containerRect.width - 10) {
  605. btnContainer.classList.add('boundary-hit');
  606. setTimeout(() => btnContainer.classList.remove('boundary-hit'), 400);
  607. }
  608. }
  609. });
  610.  
  611. // 添加窗口resize监听
  612. let resizeTimer;
  613. window.addEventListener('resize', () => {
  614. clearTimeout(resizeTimer);
  615. resizeTimer = setTimeout(() => {
  616. const currentX = parseFloat(btnContainer.style.right) || 20;
  617. const currentY = parseFloat(btnContainer.style.top) || 250;
  618. const validPos = validatePosition({ x: currentX, y: currentY });
  619.  
  620. btnContainer.style.transition = 'all 0.3s ease';
  621. btnContainer.style.right = `${validPos.x}px`;
  622. btnContainer.style.top = `${validPos.y}px`;
  623. }, 200);
  624. });
  625.  
  626. document.addEventListener('mouseup', function () {
  627. if (isDragging) {
  628. GM_setValue('btn_position', {
  629. x: parseFloat(btnContainer.style.right),
  630. y: parseFloat(btnContainer.style.top)
  631. });
  632. btnContainer.style.transition = 'all 0.3s ease';
  633. }
  634. startX = startY = undefined;
  635. isDragging = false;
  636. });
  637.  
  638. // 创建带拖拽手柄的按钮
  639. function createDragHandle() {
  640. const handle = document.createElement('div');
  641. handle.className = 'drag-handle';
  642. return handle;
  643. }
  644.  
  645. // ==================== 拖拽 结束 ====================
  646.  
  647. // ==================== 只添加到最外层 start ====================
  648. debugger;
  649. // 获取最外层文档的body
  650. if (window.self === window.top) {
  651. const rootBody = document.body;
  652. const rootHead = rootBody.ownerDocument.head;
  653. if (!rootBody.querySelector('#daemon-btn-container')) {
  654. rootBody.appendChild(btnContainer);
  655. }
  656. if (!rootHead.querySelector('#daemon-style')) {
  657. style.id = 'daemon-style';
  658. rootHead.appendChild(style);
  659. }
  660. }
  661.  
  662. // ==================== 只添加到最外层 end ====================
  663.  
  664.  
  665. // 初始化函数
  666. function init() {
  667. // 等待目标元素加载完成
  668. waitForElement(processDownload);
  669. }
  670. // 等待元素出现的函数
  671. function waitForElement(callback, maxTries = 30, interval = 1000) {
  672. let tries = 0;
  673.  
  674. function check() {
  675.  
  676.  
  677. if (getUrl()) {
  678. callback();
  679. return;
  680. }
  681.  
  682. tries++;
  683. if (tries < maxTries) {
  684. setTimeout(check, interval);
  685. }
  686. }
  687.  
  688. check();
  689. }
  690. function getUrl() {
  691. if (site_url.match(/torrents\/download_check/)) {
  692. // 获取所有包含 "torrents/download" 的链接
  693. const links = document.querySelectorAll('a[href*="torrents/download"]');
  694. if (links) {
  695. // 筛选包含 i 标签的链接
  696. const targetLink = Array.from(links).find(link => link.querySelector('i'));
  697. // 获取 href 属性
  698. return targetLink ? targetLink.getAttribute('href') : null;
  699. }
  700.  
  701.  
  702. } else {
  703. const element = document.getElementById('tDownUrl');
  704. if (element && element.value) {
  705. return element.value;
  706. }
  707. }
  708. return null;
  709. }
  710. // 主处理函数
  711. function processDownload() {
  712.  
  713. // 检查是否同域
  714. try {
  715. const currentDomain = new URL(window.location.href).hostname;
  716. const urlDomain = new URL(getUrl()).hostname;
  717.  
  718. if (currentDomain === urlDomain) {
  719. // 同域直接下载
  720. getFile(getUrl());
  721. } else {
  722. addMsg('请刷新界面后重试!');
  723. }
  724. } catch (error) {
  725. console.error('URL解析错误:', error);
  726. }
  727. }
  728.  
  729. function getFile(url, leechtorrent) {
  730. return new Promise((resolve, reject) => {
  731. GM_xmlhttpRequest({
  732. method: "GET",
  733. url: url,
  734. overrideMimeType: "text/plain; charset=x-user-defined",
  735. onload: (xhr) => {
  736. try {
  737. // 转换数据
  738. var raw = xhr.responseText;
  739. var bytes = new Uint8Array(raw.length);
  740. for (var i = 0; i < raw.length; i++) {
  741. bytes[i] = raw.charCodeAt(i) & 0xff;
  742. }
  743. // 创建 file
  744. var file = new File([bytes], 'tmp.torrent', { type: 'application/x-bittorrent' });
  745. // 上传文件
  746. sendTorrentFile(file, leechtorrent).then(resolve).catch(reject);
  747. } catch (error) {
  748. console.error('Error processing torrent:', error);
  749. addMsg('处理种子文件失败: ' + error.message);
  750. reject(error);
  751. }
  752. },
  753. onerror: function (res) {
  754. console.error('Download failed:', res);
  755. addMsg('下载种子文件失败');
  756. reject(res);
  757. }
  758. });
  759. });
  760. }
  761.  
  762.  
  763. function generateUUID() {
  764. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  765. const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
  766. return v.toString(16);
  767. });
  768. }
  769.  
  770. async function generateSignature(uuid, timestamp) {
  771. const signString = `${config.apikey}${uuid}${timestamp}`;
  772. return sha256(signString);
  773. }
  774.  
  775. function sha256(str) {
  776. const buffer = new TextEncoder().encode(str);
  777. return crypto.subtle.digest('SHA-256', buffer).then(hash => {
  778. return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
  779. });
  780. }
  781.  
  782. async function sendTorrentLink(torrentLink, leechtorrent) {
  783. const requestUUID = generateUUID();
  784. const timestamp = Math.floor(Date.now() / 1000).toString();
  785. const signature = await generateSignature(requestUUID, timestamp);
  786.  
  787. const payload = {
  788. torrent_link: torrentLink,
  789. uuid: requestUUID,
  790. timestamp: timestamp,
  791. signature: signature,
  792. forceadd: true,
  793. leechtorrent: leechtorrent || false
  794. };
  795.  
  796. GM_xmlhttpRequest({
  797. method: "POST",
  798. url: apiurl,
  799. headers: {
  800. "Content-Type": "application/json"
  801. },
  802. data: JSON.stringify(payload),
  803. onload: function (response) {
  804. console.log(response.responseText);
  805. var result = JSON.parse(response.responseText);
  806. if (response.status == 200 && result.status === 'success') {
  807. var msg = [
  808. '种子链接推送成功',
  809. '种 子 名: ' + result.torrent_name,
  810. 'tracker: ' + result.tracker
  811. ].join('\n');
  812. addMsg(msg);
  813. } else {
  814. var msg = [
  815. '种子链接推送失败',
  816. '失败原因: ' + result.message
  817. ].join('\n');
  818. addMsg(msg, 'error');
  819. }
  820. }
  821. });
  822. }
  823.  
  824. async function sendTorrentFile(torrentFile, leechtorrent) {
  825. return new Promise((resolve, reject) => {
  826. const requestUUID = generateUUID();
  827. const timestamp = Math.floor(Date.now() / 1000).toString();
  828.  
  829. generateSignature(requestUUID, timestamp)
  830. .then((signature) => {
  831. const reader = new FileReader();
  832. reader.onload = function (event) {
  833. const torrentBase64 = btoa(event.target.result);
  834. const payload = {
  835. uuid: requestUUID,
  836. timestamp: timestamp,
  837. signature: signature,
  838. torrent_bytesio: torrentBase64,
  839. forceadd: true,
  840. leechtorrent: leechtorrent || false
  841. };
  842.  
  843. GM_xmlhttpRequest({
  844. method: "POST",
  845. url: apiurl,
  846. headers: {
  847. "Content-Type": "application/json"
  848. },
  849. data: JSON.stringify(payload),
  850. onload: function (response) {
  851. console.log(response.responseText);
  852. const result = JSON.parse(response.responseText);
  853. if (response.status == 200 && result.status === 'success') {
  854. const msg = [
  855. '种子文件推送成功',
  856. '种 子 名: ' + result.torrent_name,
  857. 'tracker: ' + result.tracker
  858. ].join('\n');
  859. addMsg(msg);
  860. resolve();
  861. } else {
  862. const msg = [
  863. '种子文件推送失败',
  864. '失败原因: ' + result.message
  865. ].join('\n');
  866. addMsg(msg, 'error');
  867. reject(result.message);
  868. }
  869. },
  870. onerror: function (error) {
  871. console.error('上传失败:', error);
  872. addMsg('上传种子文件失败');
  873. reject(error);
  874. }
  875. });
  876. };
  877. reader.onerror = function (error) {
  878. console.error('文件读取失败:', error);
  879. addMsg('文件读取失败');
  880. reject(error);
  881. };
  882. reader.readAsBinaryString(torrentFile);
  883. })
  884. .catch((error) => {
  885. console.error('生成签名失败:', error);
  886. addMsg('生成签名失败: ' + error.message, 'error');
  887. reject(error);
  888. });
  889. });
  890. }
  891.  
  892.  
  893. async function listTorrent() {
  894. try {
  895. const requestUUID = generateUUID();
  896. const timestamp = Math.floor(Date.now() / 1000).toString();
  897. const signature = await generateSignature(requestUUID, timestamp);
  898.  
  899. const payload = {
  900. uuid: requestUUID,
  901. timestamp: timestamp,
  902. signature: signature,
  903. forceadd: true
  904. };
  905.  
  906. const response = await new Promise((resolve, reject) => {
  907. GM_xmlhttpRequest({
  908. method: "POST",
  909. url: listapiurl,
  910. headers: {
  911. "Content-Type": "application/json"
  912. },
  913. data: JSON.stringify(payload),
  914. onload: resolve,
  915. onerror: reject
  916. });
  917. });
  918.  
  919. console.log("Status Code:", response.status);
  920. console.log(response.responseText);
  921. if (response.status == 200) {
  922. const data = JSON.parse(response.responseText);
  923. if (data.status === "success" && data.action === "GETINFO") {
  924. const torrents = data.data.deployment_torrents_queue;
  925. const tableHTML = generateTableHTML(torrents);
  926.  
  927. const pre_leech_torrents = data.data.pre_leech_torrents;
  928. const leechTableHTML = generatLeechTableHTML(pre_leech_torrents);
  929.  
  930. displayTable(tableHTML, leechTableHTML);
  931. } else {
  932. addMsg('查询成功,但数据格式不正确', 'error');
  933. throw new Error('数据格式不正确');
  934. }
  935. } else {
  936. const result = JSON.parse(response.responseText);
  937. addMsg('查询失败: ' + result.message, 'error');
  938. throw new Error(result.message);
  939. }
  940. } catch (error) {
  941. console.error('查询失败:', error);
  942. addMsg('查询失败: ' + error.message, 'error');
  943. throw error;
  944. }
  945. }
  946.  
  947. function generateTableHTML(torrents) {
  948. let tableHTML = `
  949. <table class="daemon-table">
  950. <thead>
  951. <tr>
  952. <th style="width:30%">发布列表</th>
  953. <th>可用</th>
  954. <th>添加时间</th>
  955. <th>相关数据</th>
  956. </tr>
  957. </thead>
  958. <tbody>
  959. `;
  960.  
  961. if (!torrents || torrents.length === 0) {
  962. tableHTML += `
  963. <tr>
  964. <td colspan="4">无发布列表</td>
  965. </tr>
  966. `;
  967. } else {
  968. torrents.forEach(torrent => {
  969. tableHTML += `
  970. <tr>
  971. <td style="width:30%; word-wrap:break-word;">${torrent.torrent_name}</td>
  972. <td>${torrent.isavailable ? '是' : '否'}</td>
  973. <td>${new Date(torrent.added * 1000).toLocaleString()}</td>
  974. <td>${generateRelatedDataTable(torrent.related_data)}</td>
  975. </tr>
  976. `;
  977. });
  978. }
  979.  
  980. tableHTML += `
  981. </tbody>
  982. </table>
  983. `;
  984.  
  985. return tableHTML;
  986. }
  987. function generatLeechTableHTML(torrents) {
  988. let tableHTML = `
  989. <table class="daemon-table">
  990. <thead>
  991. <tr>
  992. <th style="width:70%">进货列表</th>
  993. <th>添加时间</th>
  994. </tr>
  995. </thead>
  996. <tbody>
  997. `;
  998. if (!torrents || torrents.length === 0) {
  999. tableHTML += `
  1000. <tr>
  1001. <td colspan="2">无进货列表</td>
  1002. </tr>
  1003. `;
  1004. } else {
  1005. torrents.forEach(torrent => {
  1006. tableHTML += `
  1007. <tr>
  1008. <td style="width:30%; word-wrap:break-word;">${torrent.torrent_name}</td>
  1009. <td>${new Date(torrent.added * 1000).toLocaleString()}</td>
  1010. </tr>
  1011. `;
  1012. });
  1013. }
  1014.  
  1015. tableHTML += `
  1016. </tbody>
  1017. </table>
  1018. `;
  1019.  
  1020. return tableHTML;
  1021. }
  1022. function generateRelatedDataTable(relatedData) {
  1023. if (!relatedData || relatedData.length === 0) {
  1024. return '无相关数据';
  1025. }
  1026.  
  1027. let nestedTableHTML = `
  1028. <table class="nested-table">
  1029. <thead>
  1030. <tr>
  1031. <th>Tracker</th>
  1032. <th>已推</th>
  1033. <th>优先级</th>
  1034. <th>添加时间</th>
  1035. <th>操作</th>
  1036. </tr>
  1037. </thead>
  1038. <tbody>
  1039. `;
  1040.  
  1041. relatedData.forEach(data => {
  1042. nestedTableHTML += `
  1043. <tr data-hash="${data[1]}" data-md5="${data[1]}">
  1044. <td>${data[2]}</td>
  1045. <td>${data[3] ? '是' : '否'}</td>
  1046. <td>${data[4]}</td>
  1047. <td>${new Date(data[5] * 1000).toLocaleString()}</td>
  1048. <td>
  1049. <div class="action-buttons">
  1050. <button class="delete-btn">删除</button>
  1051. <button class="force-push-btn">强推</button>
  1052. </div>
  1053. </td>
  1054. </tr>
  1055. `;
  1056. });
  1057.  
  1058. nestedTableHTML += `
  1059. </tbody>
  1060. </table>
  1061. `;
  1062.  
  1063. return nestedTableHTML;
  1064. }
  1065.  
  1066.  
  1067. function displayTable(tableHTML, leechTableHTML) {
  1068. const container = document.getElementById('daemon-list');
  1069. if (!container) {
  1070. const newContainer = createListContainer();
  1071. document.body.appendChild(newContainer);
  1072. newContainer.innerHTML = `
  1073. <div class="list-header">
  1074. <strong style="font-size:1.2em">种子监控面板</strong>
  1075. <button class="refresh-btn" title="刷新">🔄</button>
  1076. <button class="close-btn" title="关闭">×</button>
  1077. </div>
  1078. <div class="list-content">
  1079. ${leechTableHTML}
  1080. </div>
  1081. <div class="list-content">
  1082. ${tableHTML}
  1083. </div>
  1084. `;
  1085. } else {
  1086. container.innerHTML = `
  1087. <div class="list-header">
  1088. <strong style="font-size:1.2em">种子监控面板</strong>
  1089. <button class="refresh-btn" title="刷新">🔄</button>
  1090. <button class="close-btn" title="关闭">×</button>
  1091. </div>
  1092.  
  1093. <div class="list-content">
  1094. ${leechTableHTML}
  1095. </div>
  1096. <div class="list-content">
  1097. ${tableHTML}
  1098. </div>
  1099. `;
  1100. container.classList.add('visible');
  1101. }
  1102.  
  1103. // 绑定关闭按钮事件
  1104. const closeBtn = container.querySelector('.close-btn');
  1105. closeBtn.addEventListener('click', () => {
  1106. container.classList.remove('visible');
  1107. });
  1108.  
  1109. // 绑定刷新按钮事件
  1110. const refreshBtn = container.querySelector('.refresh-btn');
  1111. refreshBtn.addEventListener('click', () => {
  1112. // 禁用按钮
  1113. refreshBtn.disabled = true;
  1114. refreshBtn.classList.add('loading');
  1115.  
  1116. listTorrent()
  1117. .then(() => {
  1118. btn.disabled = false;
  1119. btn.classList.remove('loading');
  1120. })
  1121. .catch((error) => {
  1122. console.error('操作失败:', error);
  1123. btn.disabled = false;
  1124. btn.classList.remove('loading');
  1125. });
  1126. });
  1127.  
  1128. // 绑定嵌套表格中的删除和强推按钮事件
  1129. container.addEventListener('click', (e) => {
  1130. const deleteBtn = e.target.closest('.delete-btn');
  1131. const forcePushBtn = e.target.closest('.force-push-btn');
  1132. const row = e.target.closest('tr');
  1133.  
  1134. if (deleteBtn || forcePushBtn) {
  1135. const hash = row.dataset.hash;
  1136. const md5 = row.dataset.md5;
  1137. const tracker = row.querySelector('td:nth-child(1)').textContent; // 获取 Tracker
  1138. const addedTime = row.querySelector('td:nth-child(4)').textContent; // 获取嵌套表格的添加时间
  1139.  
  1140. if (deleteBtn) {
  1141. deleteRelatedData(hash, md5, tracker, addedTime);
  1142. }
  1143.  
  1144. if (forcePushBtn) {
  1145. forcePushRelatedData(hash, md5, tracker, addedTime);
  1146. }
  1147. }
  1148. });
  1149. }
  1150.  
  1151. function addMsg(msg, type) {
  1152. let msgBox = document.getElementById('daemon-msg');
  1153.  
  1154. // 如果元素不存在,则创建一个新的 textarea 元素
  1155. if (!msgBox) {
  1156. msgBox = document.createElement('textarea');
  1157. msgBox.id = 'daemon-msg'; // 设置 id
  1158. msgBox.readOnly = true; // 设置为只读
  1159. document.body.appendChild(msgBox); // 添加到页面中
  1160. }
  1161.  
  1162. // 设置 textarea 的内容为传入的 msg 参数,并确保换行符生效
  1163. msgBox.value = msg.replace(/\\n/g, '\n');
  1164.  
  1165. // 动态调整 textarea 的高度
  1166. // msgBox.style.height = 'auto'; // 先设置为 auto,以便根据内容计算高度
  1167. // msgBox.style.height = Math.min(msgBox.scrollHeight, 100) + 'px'; // 限制最大高度为 200px
  1168. msgBox.style.height = '100px'; // 先设置为 auto,以便根据内容计算高度
  1169.  
  1170. if (type && type == 'error') {
  1171. msgBox.className = 'daemon-msg daemon-msg-fail';
  1172. } else {
  1173. msgBox.className = 'daemon-msg';
  1174. }
  1175. }
  1176. async function deleteRelatedData(hash, md5, tracker, addedTime) {
  1177. if (!confirm(`确定要删除以下相关数据吗?\nTracker: ${tracker}\n添加时间: ${addedTime}\nHash: ${hash}`)) return;
  1178.  
  1179. try {
  1180. const requestUUID = generateUUID();
  1181. const timestamp = Math.floor(Date.now() / 1000).toString();
  1182. const signature = await generateSignature(requestUUID, timestamp);
  1183.  
  1184. const payload = {
  1185. uuid: requestUUID,
  1186. timestamp: timestamp,
  1187. signature: signature,
  1188. torrent_hash: hash,
  1189. nodropqbit: true
  1190. };
  1191.  
  1192. GM_xmlhttpRequest({
  1193. method: "POST",
  1194. url: deleteapiurl,
  1195. headers: {
  1196. "Content-Type": "application/json"
  1197. },
  1198. data: JSON.stringify(payload),
  1199. onload: function (response) {
  1200. console.log("del torrent_hash:", hash);
  1201. console.log("Status Code:", response.status);
  1202. console.log(response.responseText);
  1203. if (response.status == 200) {
  1204. addMsg('删除成功: \n' + response.responseText);
  1205. listTorrent(); // 刷新列表
  1206. } else {
  1207. var result = JSON.parse(response.responseText);
  1208. var msg = [
  1209. '删除失败',
  1210. '失败原因: ' + result.message
  1211. ].join('\n');
  1212. addMsg(msg, 'error');
  1213. }
  1214. }
  1215. });
  1216. } catch (error) {
  1217. console.error('删除失败:', error);
  1218. addMsg('删除失败: ' + error.message, 'error');
  1219. }
  1220. }
  1221.  
  1222. async function forcePushRelatedData(hash, md5, tracker, addedTime) {
  1223. if (!confirm(`确定要强制推送以下相关数据吗?\nTracker: ${tracker}\n添加时间: ${addedTime}\nHash: ${hash}`)) return;
  1224.  
  1225. // try {
  1226. // await doPostJson(deployapiurl, {
  1227. // torrent_hash: hash,
  1228. // torrent_md5: md5
  1229. // });
  1230. // addMsg('强制推送成功');
  1231. // } catch (error) {
  1232. // console.error('强制推送失败:', error);
  1233. // addMsg('强制推送失败: ' + error.message, 'error');
  1234. // }
  1235. }
  1236. var idx = 0;
  1237. // 修改按钮创建方式,使用addEventListener
  1238. function addButton(label, callback) {
  1239. const btn = document.createElement('button');
  1240. btn.className = 'daemon-btn';
  1241. btn.textContent = label;
  1242.  
  1243. btn.addEventListener('click', function (e) {
  1244. if (!isDragging && typeof callback === 'function') {
  1245. // 禁用按钮
  1246. btn.disabled = true;
  1247. btn.classList.add('loading');
  1248.  
  1249. // 执行回调函数
  1250. const result = callback();
  1251.  
  1252. // 如果回调函数返回 Promise,则在 Promise 完成后启用按钮
  1253. if (result && typeof result.then === 'function') {
  1254. result
  1255. .then(() => {
  1256. btn.disabled = false;
  1257. btn.classList.remove('loading');
  1258. })
  1259. .catch((error) => {
  1260. console.error('操作失败:', error);
  1261. btn.disabled = false;
  1262. btn.classList.remove('loading');
  1263. });
  1264. } else {
  1265. // 否则立即启用按钮
  1266. btn.disabled = false;
  1267. btn.classList.remove('loading');
  1268. }
  1269. }
  1270. });
  1271.  
  1272. btn.appendChild(createDragHandle());
  1273. btnContainer.appendChild(btn);
  1274. idx++;
  1275. }
  1276.  
  1277.  
  1278. // 简化的容器创建函数
  1279. function createListContainer() {
  1280. const container = document.createElement('div');
  1281. container.id = 'daemon-list';
  1282. container.className = 'daemon-list';
  1283. document.body.appendChild(container);
  1284. return container;
  1285. }
  1286.  
  1287. if (site_url.match(/details.php\?id=\d+&uploaded=1/) || site_url.match(/torrents\/download_check/)) {
  1288. if (document.readyState === 'loading') {
  1289. document.addEventListener('DOMContentLoaded', init);
  1290. } else {
  1291. init();
  1292. }
  1293. }
  1294. else if (site_url.match(/upload.php/)) {
  1295. addButton('点击发布', () => {
  1296. const publishButton = document.querySelector('input[value="发布"]');
  1297. if (publishButton) {
  1298. publishButton.click();
  1299. } else {
  1300. addMsg('未找到发布按钮!');
  1301. }
  1302. });
  1303. }
  1304. // 添加按钮
  1305. if (site_url.match(/details.php/) || site_url.match(/totheglory.im\/t\//)) {
  1306. addButton('编辑种子', () => {
  1307.  
  1308. const editButton = document.querySelector('a[href*="edit.php"]');
  1309. if (editButton) {
  1310. window.location.assign(editButton.href);
  1311. } else {
  1312. addMsg('未找到编辑按钮!');
  1313. }
  1314. });
  1315. }
  1316. if (site_url.match(/edit.php/)) {
  1317. addButton('编辑完成', () => {
  1318. debugger;
  1319. const editButton = document.querySelector('input[id="qr"]');
  1320. if(!editButton){
  1321. editButton = document.querySelector('input[value="编辑"]');
  1322. }
  1323. if(!editButton){
  1324. editButton = document.querySelector('input[type*="submit"]');
  1325. }
  1326. if (editButton) {
  1327. editButton.click();
  1328. return;
  1329. }
  1330. addMsg('未找到编辑按钮!');
  1331. });
  1332. }
  1333. // addButton('推送链接', () => {
  1334. // sendTorrentLink(getUrl())
  1335. // });
  1336. addButton('发|推送种子', () => {
  1337. return getFile(getUrl()); // 返回 Promise
  1338. });
  1339. addButton('发|本地种子', () => {
  1340. return new Promise((resolve, reject) => {
  1341. const input = document.createElement('input');
  1342. input.type = 'file';
  1343.  
  1344. // 文件选择事件
  1345. input.onchange = async (e) => {
  1346. try {
  1347. const file = e.target.files[0];
  1348. if (!file) {
  1349. throw new Error('未选择文件');
  1350. }
  1351. console.log('选择的文件:', file.name);
  1352. await sendTorrentFile(file); // 等待文件上传完成
  1353. resolve();
  1354. } catch (error) {
  1355. console.error('文件上传失败:', error);
  1356. addMsg('文件上传失败: ' + error, 'error');
  1357. reject(error);
  1358. }
  1359. };
  1360.  
  1361. // 文件选择取消事件
  1362. input.oncancel = () => {
  1363. console.log('文件选择已取消');
  1364. reject(new Error('文件选择已取消'));
  1365. };
  1366.  
  1367. input.click();
  1368. });
  1369. });
  1370. if(config.buttons.leechtorrent){
  1371. addButton('进|推送种子', () => {
  1372. if (!confirm(`确定进货?`)) return new Promise((resolve) => { });
  1373. return getFile(getUrl(), true);
  1374. });
  1375. addButton('进|本地种子', () => {
  1376. return new Promise((resolve, reject) => {
  1377. const input = document.createElement('input');
  1378. input.type = 'file';
  1379. // 文件选择事件
  1380. input.onchange = async (e) => {
  1381. try {
  1382. const file = e.target.files[0];
  1383. if (!file) {
  1384. throw new Error('未选择文件');
  1385. }
  1386. console.log('选择的文件:', file.name);
  1387. await sendTorrentFile(file, true); // 等待文件上传完成
  1388. resolve();
  1389. } catch (error) {
  1390. console.error('文件上传失败:', error);
  1391. addMsg('文件上传失败: ' + error, 'error');
  1392. reject(error);
  1393. }
  1394. };
  1395. // 文件选择取消事件
  1396. input.oncancel = () => {
  1397. console.log('文件选择已取消');
  1398. reject(new Error('文件选择已取消'));
  1399. };
  1400. input.click();
  1401. });
  1402. });
  1403. }
  1404. if(config.buttons.panel){
  1405. addButton('面板', () => {
  1406. return listTorrent(); // 返回 Promise
  1407. });
  1408. }
  1409. addButton('设置', handleSettings);
  1410.  
  1411. if(config.buttons.test){
  1412. addButton('获取信息', () => {
  1413. return getInfo(); // 返回 Promise
  1414. });
  1415. }
  1416.  
  1417.  
  1418. async function getInfo() {
  1419. return new Promise((resolve, reject) => {
  1420. const requestUUID = generateUUID();
  1421. const timestamp = Math.floor(Date.now() / 1000).toString();
  1422.  
  1423. generateSignature(requestUUID, timestamp)
  1424. .then((signature) => {
  1425. debugger;
  1426. const element = document.getElementById('tBlob');
  1427. const torrentBase64 = element.value;
  1428. const payload = {
  1429. uuid: requestUUID,
  1430. timestamp: timestamp,
  1431. signature: signature,
  1432. torrent_bytesio: torrentBase64,
  1433. forceadd: true,
  1434. leechtorrent: false
  1435. };
  1436.  
  1437. GM_xmlhttpRequest({
  1438. method: "POST",
  1439. url: apiurl,
  1440. headers: {
  1441. "Content-Type": "application/json"
  1442. },
  1443. data: JSON.stringify(payload),
  1444. onload: function (response) {
  1445. console.log(response.responseText);
  1446. const result = JSON.parse(response.responseText);
  1447. if (response.status == 200 && result.status === 'success') {
  1448. const msg = [
  1449. '种子文件推送成功',
  1450. '种 子 名: ' + result.torrent_name,
  1451. 'tracker: ' + result.tracker
  1452. ].join('\n');
  1453. addMsg(msg);
  1454. resolve();
  1455. } else {
  1456. const msg = [
  1457. '种子文件推送失败',
  1458. '失败原因: ' + result.message
  1459. ].join('\n');
  1460. addMsg(msg, 'error');
  1461. reject(result.message);
  1462. }
  1463. },
  1464. onerror: function (error) {
  1465. console.error('上传失败:', error);
  1466. addMsg('上传种子文件失败');
  1467. reject(error);
  1468. }
  1469. });
  1470. })
  1471. .catch((error) => {
  1472. console.error('生成签名失败:', error);
  1473. addMsg('生成签名失败: ' + error.message, 'error');
  1474. reject(error);
  1475. });
  1476. });
  1477. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址