// ==UserScript== // @name 光鸭助手 // @namespace guangyaHelper // @version 0.1.2 // @description 光鸭云盘的辅助助手插件,支持批量整理电影、剧集、批量重命名、导入导出秒传文件,支持云雷云盘导出秒传文件。 // @author 唐生 // @match https://guangyapan.com/* // @match https://*.guangyapan.com/* // @match https://pan.xunlei.com/* // @icon https://www.guangyapan.com/icon.png // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_info // @connect * // @connect api.themoviedb.org // @run-at document-start // @noframes // @license Apache-2.0 // ==/UserScript== (function () { 'use strict'; const ROOT_ID = 'guangya-helper-root'; const STYLE_ID = 'guangya-helper-styles'; const isXunleiPan = /^pan\.xunlei\.com$/i.test(window.location.hostname); const TMDB_KEY_STORAGE = 'gyh-tmdb-key'; const MOVIE_NAMING_RULE_STORAGE = 'gyh-movie-naming-rule'; const TV_NAMING_RULE_STORAGE = 'gyh-tv-naming-rule'; const ACE_CORE_URL = 'https://s4.zstatic.net/ajax/libs/ace/1.43.2/ace.min.js'; const ACE_TOOLS_URL = 'https://s4.zstatic.net/ajax/libs/ace/1.43.2/ext-language_tools.min.js'; const ACE_MODE_JSON_URL = 'https://s4.zstatic.net/ajax/libs/ace/1.43.2/mode-json.js'; const ACE_THEME_TOMORROW_URL = 'https://s4.zstatic.net/ajax/libs/ace/1.43.2/theme-tomorrow.js'; const ACE_BASE_URL = 'https://s4.zstatic.net/ajax/libs/ace/1.43.2/'; const XUNLEI_API_BASE = 'https://api-pan.xunlei.com'; const XUNLEI_CLIENT_ID = 'Xqp0kJBXWhwaTpB6'; let aceLoadPromise = null; const xunleiContext = { token: '', deviceId: '', captchaToken: '', clientId: '', shareToken: '' }; let xunleiRetryNotifier = null; if (isXunleiPan) installXunleiRequestCapture(); const SCRIPT_VERSION = typeof GM_info !== 'undefined' && GM_info?.script?.version ? GM_info.script.version : '0.1.0'; const styles = ` #${ROOT_ID} { position: fixed; top: 50%; right: 22px; z-index: 2147483000; transform: translateY(-50%); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; } #${ROOT_ID} *, #${ROOT_ID} *::before, #${ROOT_ID} *::after { box-sizing: border-box; } .gyh-trigger { width: 52px; height: 52px; padding: 0; color: #ffffff; background: linear-gradient(145deg, #2e82f5, #245fd2); border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 16px; box-shadow: 0 10px 28px rgba(36, 95, 210, 0.3), 0 2px 7px rgba(18, 51, 106, 0.18); cursor: pointer; display: grid; place-items: center; align-content: center; gap: 1px; transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease; } .gyh-trigger:hover { background: linear-gradient(145deg, #4296ff, #2869e6); box-shadow: 0 13px 32px rgba(36, 95, 210, 0.36), 0 3px 9px rgba(18, 51, 106, 0.2); transform: translateY(-2px); } .gyh-trigger:focus-visible, .gyh-action:focus-visible { outline: 3px solid rgba(45, 130, 245, 0.35); outline-offset: 3px; } .gyh-trigger svg { width: 21px; height: 21px; transition: transform 200ms ease; } .gyh-trigger-label { font-size: 10px; font-weight: 650; line-height: 13px; letter-spacing: 0; } .gyh-trigger[aria-expanded="true"] svg { transform: rotate(-10deg) scale(0.93); } .gyh-panel { position: absolute; top: 50%; right: 64px; width: 264px; padding: 14px; color: #17233a; background: rgba(255, 255, 255, 0.96); border: 1px solid rgba(213, 225, 241, 0.92); border-radius: 14px; box-shadow: 0 18px 48px rgba(36, 66, 112, 0.18), 0 2px 8px rgba(36, 66, 112, 0.08); opacity: 0; pointer-events: none; transform: translateY(-50%) translateX(10px) scale(0.98); transform-origin: right center; transition: opacity 160ms ease, transform 160ms ease; } .gyh-panel::after { position: absolute; top: 50%; right: -7px; width: 12px; height: 12px; content: ""; background: #ffffff; border-top: 1px solid rgba(213, 225, 241, 0.92); border-right: 1px solid rgba(213, 225, 241, 0.92); transform: translateY(-50%) rotate(45deg); } .gyh-panel.is-open { opacity: 1; pointer-events: auto; transform: translateY(-50%) translateX(0) scale(1); } .gyh-panel-header { display: flex; align-items: flex-start; justify-content: space-between; padding: 3px 2px 12px; } .gyh-title { margin: 0; font-size: 15px; font-weight: 700; line-height: 21px; letter-spacing: 0; } .gyh-subtitle { margin: 2px 0 0; color: #8290a7; font-size: 12px; line-height: 17px; letter-spacing: 0; } .gyh-status { padding: 3px 7px; color: #2a78dd; background: #edf5ff; border-radius: 5px; font-size: 11px; font-weight: 600; line-height: 16px; white-space: nowrap; } .gyh-actions { display: grid; gap: 7px; } .gyh-action { width: 100%; height: 48px; padding: 5px 10px; text-align: left; color: #20304a; background: #f7faff; border: 1px solid #e5edf8; border-radius: 8px; cursor: pointer; display: grid; grid-template-columns: 34px minmax(0, 1fr) 17px; align-items: center; gap: 9px; transition: background 160ms ease, border-color 160ms ease; } .gyh-action:hover { background: #f1f7ff; border-color: #c8def8; } .gyh-action-icon { width: 34px; height: 34px; color: #2877de; background: #e8f2ff; border-radius: 8px; display: grid; place-items: center; } .gyh-action-icon svg { width: 18px; height: 18px; } .gyh-action:nth-child(2) .gyh-action-icon { color: #8a63cc; background: #f2ecff; } .gyh-action:nth-child(3) .gyh-action-icon { color: #de7a34; background: #fff1e8; } .gyh-action-name { display: block; font-size: 13px; font-weight: 650; line-height: 18px; letter-spacing: 0; } .gyh-action-note { display: block; margin-top: 1px; color: #8491a5; font-size: 11px; line-height: 15px; letter-spacing: 0; } .gyh-action-arrow { color: #a6b4c8; font-size: 18px; font-weight: 400; line-height: 18px; text-align: center; } .gyh-rename-overlay { position: fixed; inset: 0; z-index: 2147483001; padding: 16px; background: rgba(19, 32, 53, 0.42); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; animation: gyh-rename-fade-in 160ms ease; } .gyh-rename-modal { width: min(680px, calc(100vw - 32px)); max-height: min(640px, calc(100vh - 32px)); overflow: hidden; color: #17233a; background: #ffffff; border: 1px solid rgba(213, 225, 241, 0.96); border-radius: 16px; box-shadow: 0 24px 70px rgba(19, 44, 82, 0.28), 0 4px 14px rgba(19, 44, 82, 0.12); display: flex; flex-direction: column; animation: gyh-rename-rise-in 180ms ease; } .gyh-rename-header { padding: 10px; border-bottom: 1px solid #edf1f7; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } .gyh-rename-heading { display: flex; align-items: center; gap: 11px; min-width: 0; } .gyh-rename-heading-icon { width: 36px; height: 36px; flex: 0 0 auto; color: #2877de; background: #e8f2ff; border-radius: 10px; display: grid; place-items: center; } .gyh-rename-heading-icon svg { width: 19px; height: 19px; } .gyh-rename-title { margin: 0; font-size: 16px; font-weight: 700; line-height: 22px; } .gyh-rename-subtitle { margin: 2px 0 0; color: #8491a5; font-size: 12px; line-height: 17px; } .gyh-rename-close { width: 30px; height: 30px; flex: 0 0 auto; padding: 0; color: #8491a5; background: transparent; border: 0; border-radius: 7px; cursor: pointer; display: grid; place-items: center; transition: background 160ms ease, color 160ms ease; } .gyh-rename-close:hover { color: #263b5d; background: #f3f6fa; } .gyh-rename-close svg { width: 17px; height: 17px; } .gyh-rename-body { min-height: 0; padding: 10px; overflow-y: auto; } .gyh-rename-section { margin-top: 12px; padding: 10px; border: 1px solid #e7edf6; border-radius: 10px; } .gyh-rename-section:first-of-type { margin-top: 0; } .gyh-rename-section-head { min-height: 22px; margin-bottom: 5px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .gyh-rename-section-title { margin: 0; color: #263b5d; font-size: 13px; font-weight: 700; line-height: 20px; } .gyh-rename-section-hint { color: #8491a5; font-size: 11px; line-height: 17px; } .gyh-rename-modes { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 5px; } .gyh-rename-mode { min-height: 34px; padding: 5px 7px; color: #5e6f87; background: #f7f9fc; border: 1px solid transparent; border-radius: 6px; cursor: pointer; font-size: 12px; line-height: 16px; white-space: nowrap; transition: background 160ms ease, border-color 160ms ease, color 160ms ease; } .gyh-rename-mode:hover { color: #2961d9; background: #f1f6ff; } .gyh-rename-mode.is-active { color: #2961d9; background: #edf5ff; border-color: #c9defa; font-weight: 650; } .gyh-rename-mode:disabled { color: #a6b1c1; background: #f4f6f9; border-color: transparent; cursor: not-allowed; opacity: 0.72; } .gyh-rename-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .gyh-rename-field { min-width: 0; position: relative; } .gyh-rename-field-label { position: absolute; top: -7px; left: 9px; padding: 0 4px; color: #8491a5; background: #ffffff; font-size: 10px; line-height: 14px; } .gyh-rename-input { width: 100%; height: 36px; padding: 8px 10px; color: #263b5d; background: #ffffff; border: 1px solid #dfe7f1; border-radius: 7px; outline: 0; font: inherit; font-size: 12px; transition: border-color 160ms ease, box-shadow 160ms ease; } .gyh-rename-input:focus { border-color: #77a9ed; box-shadow: 0 0 0 3px rgba(45, 130, 245, 0.12); } .gyh-rename-input:disabled { color: #9aa6b7; background: #f5f7fa; border-color: #e4eaf2; cursor: not-allowed; } .gyh-rename-preview { overflow: hidden; border: 1px solid #e7edf6; border-radius: 9px; } .gyh-rename-preview-body { max-height: 180px; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: #b7c8de transparent; } .gyh-rename-preview-body::-webkit-scrollbar { width: 8px; } .gyh-rename-preview-body::-webkit-scrollbar-track { background: transparent; } .gyh-rename-preview-body::-webkit-scrollbar-thumb { background: #c3d2e4; border: 2px solid transparent; border-radius: 8px; background-clip: padding-box; } .gyh-rename-preview-body::-webkit-scrollbar-thumb:hover { background: #9fb8d5; border: 2px solid transparent; background-clip: padding-box; } .gyh-rename-preview-head, .gyh-rename-row { display: grid; grid-template-columns: minmax(0, 1fr) 24px minmax(0, 1fr) 44px; align-items: center; gap: 8px; } .gyh-rename-preview-head { padding: 6px 10px; color: #8491a5; background: #f8faff; font-size: 10px; line-height: 14px; } .gyh-rename-preview-head span:nth-child(3) { color: #6f8fbd; } .gyh-rename-row { min-height: 36px; padding: 4px 10px; border-top: 1px solid #eef2f7; font-size: 11px; line-height: 16px; } .gyh-rename-row-index { color: #8491a5; font-variant-numeric: tabular-nums; } .gyh-rename-row-name { min-width: 0; overflow: hidden; color: #5e6f87; text-overflow: ellipsis; white-space: nowrap; } .gyh-rename-row-name.is-new { color: #2961d9; font-weight: 600; } .gyh-rename-row-arrow { color: #a7b5c8; font-size: 15px; text-align: center; } .gyh-rename-row-status { padding: 2px 5px; color: #8491a5; background: #f4f6f9; border-radius: 4px; font-size: 10px; line-height: 14px; text-align: center; white-space: nowrap; } .gyh-rename-row.is-processing .gyh-rename-row-status { color: #2961d9; background: #edf5ff; } .gyh-rename-row.is-success .gyh-rename-row-status { color: #27895f; background: #eaf8f1; } .gyh-rename-row.is-failed .gyh-rename-row-status { color: #d64545; background: #fff0f0; } .gyh-rename-row.is-processing { background: #f8faff; } .gyh-rename-row.is-success .gyh-rename-row-name.is-new { color: #27895f; } .gyh-rename-row.is-failed .gyh-rename-row-name.is-new { color: #d64545; } .gyh-rename-footer { padding: 12px 20px; border-top: 1px solid #edf1f7; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .gyh-rename-footer-note { color: #8491a5; font-size: 11px; line-height: 17px; } .gyh-rename-footer-actions { display: flex; align-items: center; gap: 7px; } .gyh-rename-btn { min-width: 70px; height: 32px; padding: 5px 12px; color: #66758c; background: #ffffff; border: 1px solid #dce5f0; border-radius: 7px; cursor: pointer; font: inherit; font-size: 12px; line-height: 20px; } .gyh-rename-btn-primary { color: #ffffff; background: #2961d9; border-color: #2961d9; cursor: not-allowed; opacity: 0.58; } .gyh-rename-btn-primary:not(:disabled) { cursor: pointer; opacity: 1; } .gyh-rename-btn:hover:not(:disabled) { color: #2961d9; border-color: #a9c9f4; background: #f6f9ff; } .gyh-rename-video-filter { color: #60718b; cursor: pointer; font-size: 11px; line-height: 17px; display: inline-flex; align-items: center; gap: 5px; user-select: none; } .gyh-rename-video-filter input { width: 13px; height: 13px; margin: 0; accent-color: #2961d9; cursor: pointer; } .gyh-toast { position: fixed; top: 50%; left: 50%; z-index: 2147483003; width: max-content; max-width: calc(100vw - 32px); padding: 9px 14px; color: #31445f; background: rgba(244, 247, 252, 0.96); border: 1px solid #dbe4f0; border-radius: 8px; box-shadow: none; display: flex; align-items: center; gap: 9px; font-size: 13px; line-height: 20px; pointer-events: none; backdrop-filter: blur(10px); transform: translate(-50%, -46%) scale(0.98); opacity: 0; animation: gyh-toast-in 160ms ease forwards; } .gyh-toast-icon { width: 20px; height: 20px; flex: 0 0 auto; color: #3f6ea8; background: transparent; border-radius: 0; display: grid; place-items: center; } .gyh-toast-icon svg { width: 18px; height: 18px; } .gyh-toast-message { overflow: hidden; color: #31445f; font-size: 13px; font-weight: 500; line-height: 20px; text-overflow: ellipsis; white-space: nowrap; } .gyh-toast.is-error { background: #fff3ee; border-color: #ffccbc; box-shadow: none; } .gyh-toast.is-error .gyh-toast-icon { color: #ff8a65; background: transparent; } .gyh-toast.is-error .gyh-toast-message { color: #b85c40; } .gyh-toast.is-success { background: rgba(239, 249, 243, 0.97); border-color: #ccebd9; box-shadow: none; } .gyh-toast.is-success .gyh-toast-icon { color: #2f9c72; background: transparent; } .gyh-toast.is-success .gyh-toast-message { color: #276747; } @keyframes gyh-toast-in { to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes gyh-toast-out { to { opacity: 0; transform: translate(-50%, -55%) scale(0.96); } } @keyframes gyh-rename-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes gyh-rename-rise-in { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } } @media (max-width: 640px) { #${ROOT_ID} { right: 14px; } .gyh-trigger { width: 48px; height: 48px; border-radius: 14px; } .gyh-panel { right: 58px; width: min(264px, calc(100vw - 86px)); } .gyh-rename-overlay { padding: 8px; } .gyh-rename-modal { width: calc(100vw - 16px); max-height: calc(100vh - 16px); } .gyh-rename-header, .gyh-rename-body, .gyh-rename-footer { padding-left: 10px; padding-right: 10px; } .gyh-rename-modes { grid-template-columns: repeat(2, minmax(0, 1fr)); } .gyh-rename-fields { grid-template-columns: 1fr; } .gyh-rename-footer { align-items: stretch; flex-direction: column; } .gyh-rename-footer-actions { justify-content: flex-end; } } #${ROOT_ID} .gyh-trigger-label { font-size: 11px; } #${ROOT_ID} .gyh-panel-header { display: block; } #${ROOT_ID} .gyh-panel-title-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } #${ROOT_ID} .gyh-subtitle { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; } #${ROOT_ID} .gyh-author { color: #9aa8ba; font-size: 10px; white-space: nowrap; } #${ROOT_ID} .gyh-action:nth-child(4) .gyh-action-icon { color: #168476; background: #e7f7f3; } #${ROOT_ID} .gyh-submenu-heading { display: flex; align-items: center; gap: 7px; min-width: 0; } #${ROOT_ID} .gyh-submenu-back { width: 24px; height: 24px; flex: 0 0 auto; padding: 0; color: #5b86c6; background: #f1f6ff; border: 1px solid #dce7f6; border-radius: 6px; cursor: pointer; display: grid; place-items: center; } #${ROOT_ID} .gyh-submenu-back:hover { color: #2961d9; background: #e7f1ff; border-color: #c9defa; } #${ROOT_ID} .gyh-submenu-back svg { width: 15px; height: 15px; } #${ROOT_ID} .gyh-transfer-panel .gyh-action:nth-child(1) .gyh-action-icon { color: #168476; background: #e7f7f3; } #${ROOT_ID} .gyh-transfer-panel .gyh-action:nth-child(2) .gyh-action-icon { color: #9c5cab; background: #f6ebf8; } .gyh-movie-organizer-modal .gyh-rename-heading-icon { color: #2877de; background: #e8f2ff; } .gyh-tv-organizer-modal .gyh-rename-heading-icon { color: #8a63cc; background: #f2ecff; } .gyh-batch-rename-modal .gyh-rename-heading-icon { color: #de7a34; background: #fff1e8; } `; const movieStyles = `.gyh-movie-modal{width:min(760px,calc(100vw - 32px));max-height:min(680px,calc(100vh - 32px))}.gyh-movie-key-modal{width:min(500px,calc(100vw - 32px));max-height:min(440px,calc(100vh - 32px))}.gyh-movie-key-note{padding:10px 12px;color:#536985;background:#f5f8fd;border:1px solid #e2eaf4;border-radius:8px;font-size:12px;line-height:18px}.gyh-movie-key-note strong{color:#263b5d}.gyh-movie-key-input{letter-spacing:.3px}.gyh-movie-key-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;margin-top:10px}.gyh-movie-key-status{min-height:15px;margin-top:5px;color:#8491a5;font-size:11px;line-height:15px}.gyh-movie-key-status.is-error{color:#b85c40}.gyh-movie-key-status.is-success{color:#8491a5}.gyh-movie-correct-count{color:#27895f}.gyh-movie-scope{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.gyh-movie-scope-item{min-width:0;padding:6px 9px;background:#f8faff;border:1px solid #e5edf8;border-radius:8px;display:flex;align-items:center;gap:7px}.gyh-movie-scope-label{display:inline;color:#8491a5;font-size:10px;line-height:16px;white-space:nowrap}.gyh-movie-scope-value{display:block;margin-top:0;min-width:0;color:#263b5d;font-size:12px;font-weight:650;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gyh-movie-rule-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.gyh-movie-rule-separator{width:54px;height:24px;padding:3px 7px;color:#2961d9;background:#f7faff;border:1px solid #dce7f6;border-radius:6px;outline:0;font:inherit;font-size:12px;line-height:16px;text-align:center}.gyh-movie-rule-list{display:flex;flex-wrap:wrap;gap:5px}.gyh-movie-rule-options{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.gyh-movie-rule-token{height:26px;padding:0 6px;color:#2961d9;background:#edf5ff;border:1px solid #c9defa;border-radius:6px;display:inline-flex;align-items:center;gap:4px;font-size:11px;line-height:16px;cursor:grab;user-select:none;transition:background .15s,border-color .15s,opacity .15s,transform .15s}.gyh-movie-rule-token:active{cursor:grabbing}.gyh-movie-rule-token.is-dragging{opacity:.45;transform:scale(.98)}.gyh-movie-rule-token.is-drag-over{background:#dcecff;border-color:#8db8ee}.gyh-movie-rule-grip{width:12px;height:16px;color:#78a0d1;display:grid;place-items:center;pointer-events:none}.gyh-movie-rule-grip svg{width:11px;height:15px}.gyh-movie-rule-tool{width:18px;height:18px;margin-right:-3px;padding:0;color:#5b86c6;background:transparent;border:0;border-radius:4px;cursor:pointer;display:grid;place-items:center}.gyh-movie-rule-tool:hover{color:#2961d9;background:#dcecff}.gyh-movie-rule-tool:disabled{color:#aac2e0;background:transparent;cursor:not-allowed}.gyh-movie-rule-tool svg{width:12px;height:12px}.gyh-movie-rule-option{height:24px;padding:3px 8px;color:#60718b;background:#f7f9fc;border:1px solid #e3eaf3;border-radius:6px;cursor:pointer;font:inherit;font-size:11px;line-height:16px}.gyh-movie-rule-option:hover{color:#2961d9;background:#f1f6ff;border-color:#c9defa}.gyh-movie-rule-option.is-selected{color:#a5b0bf;background:#f4f6f9;border-color:#edf0f4;cursor:not-allowed}.gyh-movie-rule-preview{display:block;min-height:17px;margin-top:8px;overflow:hidden;color:#6f8fbd;font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.gyh-movie-preview-head,.gyh-movie-preview-row{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr) 76px;align-items:center;gap:8px}.gyh-movie-preview{overflow:hidden;border:1px solid #e7edf6;border-radius:9px}.gyh-movie-preview-head{padding:6px 10px;color:#8491a5;background:#f8faff;font-size:10px;line-height:14px}.gyh-movie-preview-body{max-height:220px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#b7c8de transparent}.gyh-movie-preview-body::-webkit-scrollbar{width:8px}.gyh-movie-preview-body::-webkit-scrollbar-track{background:transparent}.gyh-movie-preview-body::-webkit-scrollbar-thumb{background:#c3d2e4;border:2px solid transparent;border-radius:8px;background-clip:padding-box}.gyh-movie-preview-row{min-height:38px;padding:5px 10px;border-top:1px solid #eef2f7;font-size:11px;line-height:16px}.gyh-movie-preview-name{min-width:0;overflow:hidden;color:#5e6f87;text-overflow:ellipsis;white-space:nowrap}.gyh-movie-preview-match{min-width:0;overflow:hidden;color:#8491a5;text-overflow:ellipsis;white-space:nowrap}.gyh-movie-preview-status{padding:2px 5px;color:#8491a5;background:#f4f6f9;border-radius:4px;font-size:10px;line-height:14px;text-align:center;white-space:nowrap}.gyh-movie-footer-note{flex:1;min-width:0;overflow:hidden;color:#8491a5;font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.gyh-movie-footer-count{color:#27895f;font-weight:400}.gyh-movie-footer-failed{color:#d64545;font-weight:400}.gyh-movie-scope-item + .gyh-movie-scope-item{margin-left:0}.gyh-movie-scope + .gyh-movie-rule-list{margin-top:0}.gyh-movie-modal [data-tmdb-settings]{height:28px;padding:5px 10px}@media (max-width:640px){.gyh-movie-modal{width:calc(100vw - 16px);max-height:calc(100vh - 16px)}.gyh-movie-key-modal{width:calc(100vw - 16px);max-height:calc(100vh - 16px)}.gyh-movie-scope{grid-template-columns:1fr}.gyh-movie-preview-head,.gyh-movie-preview-row{grid-template-columns:minmax(0,1fr) 80px}}`; const movieCorrectionStyles = `.gyh-movie-preview-correct{width:35px;height:22px;padding:0;color:#5b86c6;background:#f7faff;border:1px solid #dce7f6;border-radius:5px;cursor:pointer;font:inherit;font-size:10px;line-height:14px;white-space:nowrap}.gyh-movie-preview-correct:hover{color:#2961d9;background:#edf5ff;border-color:#c9defa}.gyh-movie-preview-body.is-organizing .gyh-movie-preview-correct{display:none}.gyh-movie-preview-body.is-organizing .gyh-movie-preview-match{grid-column:span 2}.gyh-movie-correct-modal{width:min(430px,calc(100vw - 32px))}.gyh-movie-correct-modal .gyh-rename-body{overflow:hidden}.gyh-movie-correct-results{max-height:138px;margin-top:8px;padding-right:12px;overflow-y:scroll;scrollbar-gutter:stable;display:grid;gap:5px}.gyh-movie-correct-results::-webkit-scrollbar{width:8px}.gyh-movie-correct-results::-webkit-scrollbar-track{background:#f1f5fa;border-radius:8px}.gyh-movie-correct-results::-webkit-scrollbar-thumb{background:#c3d2e4;border:2px solid #f1f5fa;border-radius:8px;background-clip:padding-box}.gyh-movie-correct-option{width:100%;min-height:42px;padding:5px 8px;color:#536985;background:#f8faff;border:1px solid #e2eaf4;border-radius:7px;cursor:pointer;font:inherit;text-align:left;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px}.gyh-movie-correct-option:hover{background:#f1f6ff;border-color:#c9defa}.gyh-movie-correct-option.is-selected{color:#2961d9;background:#edf5ff;border-color:#8db8ee}.gyh-movie-correct-option-main{min-width:0;display:grid;gap:1px}.gyh-movie-correct-option-title{display:block;overflow:hidden;color:inherit;font-size:12px;font-weight:650;line-height:15px;text-overflow:ellipsis;white-space:nowrap}.gyh-movie-correct-option-original{display:block;overflow:hidden;color:#8491a5;font-size:10px;line-height:13px;text-overflow:ellipsis;white-space:nowrap}.gyh-movie-correct-option-year{padding:2px 5px;color:#6f8fbd;background:#ffffff;border:1px solid #dce7f6;border-radius:4px;font-size:10px;line-height:14px;white-space:nowrap}.gyh-movie-correct-option.is-selected .gyh-movie-correct-option-year{color:#2961d9;border-color:#b8d3f5}`; const tvStyles = `.gyh-tv-preview-target{width:100%;padding:0;color:#2961d9;background:transparent;border:0;cursor:pointer;font:inherit;text-align:left}.gyh-tv-preview-target:hover{color:#1f55c5;text-decoration:underline}.gyh-tv-preview-target:disabled{color:#8491a5;cursor:default;text-decoration:none}.gyh-movie-preview-body.is-organizing .gyh-tv-preview-target{grid-column:span 2}.gyh-movie-preview-body.is-organizing .gyh-tv-preview-correct-placeholder{display:none}`; const rapidImportStyles = `.gyh-rapid-import-modal{width:min(720px,calc(100vw - 32px));max-height:min(660px,calc(100vh - 32px))}.gyh-rapid-import-modal .gyh-rename-heading-icon{color:#168476;background:#e7f7f3}.gyh-rapid-import-file-row{height:15px;min-height:15px;margin-bottom:5px;display:flex;align-items:center;gap:8px}.gyh-rapid-import-tools{display:flex;align-items:center;gap:6px}.gyh-rapid-import-file-button{height:28px;min-width:0;padding:4px 10px;line-height:18px;display:inline-flex;align-items:center;gap:5px}.gyh-rapid-import-file-button svg{width:14px;height:14px}.gyh-rapid-import-file-button.is-disabled,.gyh-rapid-import-clear:disabled{color:#a6b1c1;background:#f4f6f9;border-color:#e4eaf2;cursor:not-allowed;pointer-events:none}.gyh-rapid-import-file-input{display:none}.gyh-rapid-import-file-name{min-width:0;overflow:hidden;color:#8491a5;font-size:11px;line-height:15px;text-overflow:ellipsis;white-space:nowrap}.gyh-rapid-import-editor{height:300px;overflow:hidden;border:1px solid #dfe7f1;border-radius:8px}.gyh-rapid-import-editor:focus-within{border-color:#77a9ed;box-shadow:0 0 0 3px rgba(45,130,245,.12)}.gyh-rapid-import-editor .ace_scrollbar::-webkit-scrollbar{width:8px;height:8px}.gyh-rapid-import-editor .ace_scrollbar::-webkit-scrollbar-track{background:transparent}.gyh-rapid-import-editor .ace_scrollbar::-webkit-scrollbar-thumb{background:#c3d2e4;border:2px solid transparent;border-radius:8px;background-clip:padding-box}.gyh-rapid-import-editor .ace_scrollbar::-webkit-scrollbar-thumb:hover{background:#9fb8d5;border:2px solid transparent;background-clip:padding-box}.gyh-rapid-import-fallback{width:100%;height:100%;padding:10px;color:#263b5d;background:#fafcff;border:0;outline:0;resize:none;font:12px/18px ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}.gyh-rapid-import-modal .ace_editor{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace!important;font-size:12px!important;line-height:18px!important}.gyh-rapid-import-progress{overflow:hidden;border:1px solid #e7edf6;border-radius:9px}.gyh-rapid-import-progress-head,.gyh-rapid-import-progress-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.3fr) 44px;align-items:center;gap:8px}.gyh-rapid-import-progress-head{padding:6px 10px;color:#8491a5;background:#f8faff;font-size:10px;line-height:14px}.gyh-rapid-import-progress-head span:nth-child(2){color:#6f8fbd}.gyh-rapid-import-progress-body{max-height:300px;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:#b7c8de transparent}.gyh-rapid-import-progress-body::-webkit-scrollbar{width:8px}.gyh-rapid-import-progress-body::-webkit-scrollbar-track{background:transparent}.gyh-rapid-import-progress-body::-webkit-scrollbar-thumb{background:#c3d2e4;border:2px solid transparent;border-radius:8px;background-clip:padding-box}.gyh-rapid-import-progress-body::-webkit-scrollbar-thumb:hover{background:#9fb8d5;border:2px solid transparent;background-clip:padding-box}.gyh-rapid-import-progress-row{min-height:36px;padding:4px 10px;border-top:1px solid #eef2f7;font-size:11px;line-height:16px}.gyh-rapid-import-progress-directory,.gyh-rapid-import-progress-file{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gyh-rapid-import-progress-directory{color:#5e6f87}.gyh-rapid-import-progress-file{color:#2961d9;font-weight:600}.gyh-rapid-import-progress-row.is-processing{background:#f8faff}.gyh-rapid-import-progress-row.is-processing .gyh-rename-row-status{color:#2961d9;background:#edf5ff}.gyh-rapid-import-progress-row.is-success .gyh-rename-row-status{color:#27895f;background:#eaf8f1}.gyh-rapid-import-progress-row.is-failed .gyh-rename-row-status{color:#d64545;background:#fff0f0}.gyh-rapid-import-progress-row.is-success .gyh-rapid-import-progress-file{color:#27895f}.gyh-rapid-import-progress-row.is-failed .gyh-rapid-import-progress-file{color:#d64545}.gyh-rapid-import-footer-count{color:#27895f;font-weight:400}.gyh-rapid-import-footer-failed{color:#d64545;font-weight:400}@media(max-width:640px){.gyh-rapid-import-progress-head,.gyh-rapid-import-progress-row{grid-template-columns:minmax(0,1fr) minmax(0,1fr) 44px}}`; const rapidExportStyles = `.gyh-rapid-export-modal{width:min(720px,calc(100vw - 32px));max-height:min(660px,calc(100vh - 32px))}.gyh-rapid-export-modal .gyh-rename-heading-icon{color:#9c5cab;background:#f6ebf8}.gyh-rapid-export-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.gyh-rapid-export-summary-item{min-width:0;padding:7px 9px;background:#f8faff;border:1px solid #e5edf8;border-radius:8px}.gyh-rapid-export-summary-label{display:block;color:#8491a5;font-size:10px;line-height:15px}.gyh-rapid-export-summary-value{display:block;overflow:hidden;color:#263b5d;font-size:12px;font-weight:650;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.gyh-rapid-export-code{height:300px;overflow:hidden;border:1px solid #dfe7f1;border-radius:8px}.gyh-rapid-export-code .ace_scrollbar::-webkit-scrollbar{width:8px;height:8px}.gyh-rapid-export-code .ace_scrollbar::-webkit-scrollbar-track{background:transparent}.gyh-rapid-export-code .ace_scrollbar::-webkit-scrollbar-thumb{background:#c3d2e4;border:2px solid transparent;border-radius:8px;background-clip:padding-box}.gyh-rapid-export-code .ace_scrollbar::-webkit-scrollbar-thumb:hover{background:#9fb8d5;border:2px solid transparent;background-clip:padding-box}.gyh-rapid-export-modal .ace_editor{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace!important;font-size:12px!important;line-height:18px!important}.gyh-rapid-export-fallback{width:100%;height:100%;padding:10px;color:#263b5d;background:#fafcff;border:0;outline:0;resize:none;font:12px/18px ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}.gyh-rapid-export-note{color:#8491a5;font-size:11px;line-height:17px}.gyh-rapid-export-progress-modal{width:min(390px,calc(100vw - 32px));max-height:min(340px,calc(100vh - 32px))}.gyh-rapid-export-progress-modal .gyh-rename-heading-icon{color:#9c5cab;background:#f6ebf8}.gyh-rapid-export-progress-main{padding:26px 14px 24px;text-align:center}.gyh-rapid-export-spinner{width:42px;height:42px;margin:0 auto 15px;border:3px solid #eee2f2;border-top-color:#9c5cab;border-radius:50%;display:block;animation:gyh-rapid-export-spin .8s linear infinite}.gyh-rapid-export-progress-title{margin:0;color:#263b5d;font-size:14px;font-weight:700;line-height:21px}.gyh-rapid-export-progress-text{min-height:18px;margin:5px 0 0;color:#6f8fbd;font-size:12px;line-height:18px}.gyh-rapid-export-progress-count{min-height:16px;margin:3px 0 0;color:#8491a5;font-size:11px;line-height:16px}.gyh-rapid-export-progress-modal .gyh-rename-footer{justify-content:center;padding:9px 14px}.gyh-rapid-export-progress-note{color:#8491a5;font-size:11px;line-height:17px}@keyframes gyh-rapid-export-spin{to{transform:rotate(360deg)}}@media(max-width:640px){.gyh-rapid-export-summary{grid-template-columns:1fr}}`; // 注入组件样式 function addStyles() { if (document.getElementById(STYLE_ID)) return; if (typeof GM_addStyle === 'function') { GM_addStyle(styles); GM_addStyle(movieStyles); GM_addStyle(movieCorrectionStyles); GM_addStyle(tvStyles); GM_addStyle(rapidImportStyles); GM_addStyle(rapidExportStyles); return; } const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = styles; document.head.appendChild(style); const movieStyle = document.createElement('style'); movieStyle.textContent = movieStyles + movieCorrectionStyles + tvStyles + rapidImportStyles + rapidExportStyles; document.head.appendChild(movieStyle); } const renameModes = [ { key: 'replace', label: '查找替换' }, { key: 'sequence', label: '按序号' }, { key: 'append', label: '追加' }, { key: 'regex', label: '正则替换' }, { key: 'format', label: '格式替换' }, ]; // 显示居中的状态提示 function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `gyh-toast${type === 'error' ? ' is-error' : type === 'success' ? ' is-success' : ''}`; const icon = type === 'success' ? '' : type === 'error' ? '' : ''; toast.innerHTML = ``; document.body.appendChild(toast); window.setTimeout(() => { toast.style.animation = 'gyh-toast-out 180ms ease forwards'; window.setTimeout(() => toast.remove(), 180); }, 2200); } // 按需加载 Ace 编辑器 function loadAceEditor() { if (typeof ace !== 'undefined' && typeof ace.edit === 'function') return Promise.resolve(true); if (aceLoadPromise) return aceLoadPromise; const loadScript = url => new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url, onload(response) { try { if (response.status < 200 || response.status >= 300) return resolve(false); (0, eval)(response.responseText); resolve(true); } catch (error) { resolve(false); } }, onerror() { resolve(false); }, ontimeout() { resolve(false); } }); }); aceLoadPromise = (async () => { if (!await loadScript(ACE_CORE_URL)) return false; ace.config.set('basePath', ACE_BASE_URL); ace.config.set('modePath', ACE_BASE_URL); ace.config.set('themePath', ACE_BASE_URL); ace.config.set('workerPath', ACE_BASE_URL); await loadScript(ACE_TOOLS_URL); await loadScript(ACE_MODE_JSON_URL); await loadScript(ACE_THEME_TOMORROW_URL); return typeof ace !== 'undefined' && typeof ace.edit === 'function'; })(); return aceLoadPromise; } // 等待指定时长 function delay(milliseconds) { return new Promise(resolve => window.setTimeout(resolve, milliseconds)); } // 读取 TMDB 密钥 async function getTmdbKey() { try { if (typeof GM_getValue === 'function') return String(await GM_getValue(TMDB_KEY_STORAGE, '') || '').trim(); return String(localStorage.getItem(TMDB_KEY_STORAGE) || '').trim(); } catch (error) { return ''; } } // 保存 TMDB 密钥 async function saveTmdbKey(value) { const key = String(value || '').trim(); if (typeof GM_setValue === 'function') await GM_setValue(TMDB_KEY_STORAGE, key); else localStorage.setItem(TMDB_KEY_STORAGE, key); return key; } // 规范 TMDB 密钥格式 function normalizeTmdbKey(value) { return String(value || '').trim().replace(/^Bearer\s+/i, '').trim(); } // 请求 TMDB 接口 function requestTmdb(url, key) { const isBearer = /^eyJ/i.test(key); const requestUrl = isBearer ? url : `${url}${url.includes('?') ? '&' : '?'}api_key=${encodeURIComponent(key)}`; return new Promise((resolve, reject) => { let settled = false; const finish = (callback, value) => { if (settled) return; settled = true; callback(value); }; GM_xmlhttpRequest({ method: 'GET', url: requestUrl, headers: isBearer ? { Authorization: `Bearer ${key}` } : {}, responseType: 'json', timeout: 15000, onload(response) { const data = response.response || (() => { try { return JSON.parse(response.responseText); } catch (error) { return null; } })(); if (response.status >= 200 && response.status < 300) finish(resolve, data); else finish(reject, new Error(data?.status_message || `TMDB 验证失败(${response.status})`)); }, onerror() { finish(reject, new Error('TMDB 网络请求失败')); }, ontimeout() { finish(reject, new Error('TMDB 请求超时,请尝试使用代理')); }, onabort() { finish(reject, new Error('TMDB 请求已取消')); } }); }); } // 验证 TMDB 密钥 async function validateTmdbKey(key) { const data = await requestTmdb('https://api.themoviedb.org/3/configuration', key); if (!data || data.images === undefined) throw new Error('TMDB 密钥无效'); return true; } const movieNamingFields = ['影片名', '上映时间', '来源', '分辨率', '视频编码', '音频编码', 'TMDB']; const defaultMovieNamingFields = ['影片名', '上映时间', '分辨率', '来源', '视频编码', 'TMDB']; const tvNamingFields = ['剧集名', '季集信息', '上映时间', '来源', '分辨率', '视频编码', '音频编码', 'TMDB']; const defaultTvNamingFields = ['剧集名', '季集信息', '上映时间', '分辨率', '来源', '视频编码']; // 移除站点推广标签 function stripMovieSiteTags(value) { return String(value || '').replace(/(?:【[^】]*(?:https?:\/\/|www\.|(?:[a-z0-9-]+\.)+(?:com|net|org|cn|edu|gov|info|biz|io|co|me|tv|cc|vip|top|xyz|site|online|club|live|app|pro|mobi|ai|cloud|fun|link|wiki|name|work|store|shop|tech|dev|icu|one|video|movie))[^】]*】|\[[^\]]*(?:https?:\/\/|www\.|(?:[a-z0-9-]+\.)+(?:com|net|org|cn|edu|gov|info|biz|io|co|me|tv|cc|vip|top|xyz|site|online|club|live|app|pro|mobi|ai|cloud|fun|link|wiki|name|work|store|shop|tech|dev|icu|one|video|movie))[^\]]*\]|『[^』]*(?:https?:\/\/|www\.|(?:[a-z0-9-]+\.)+(?:com|net|org|cn|edu|gov|info|biz|io|co|me|tv|cc|vip|top|xyz|site|online|club|live|app|pro|mobi|ai|cloud|fun|link|wiki|name|work|store|shop|tech|dev|icu|one|video|movie))[^』]*』)/ig, ' '); } // 提取文件夹中的中文片名 function extractMovieTitleHint(value) { const text = stripMovieSiteTags(value).replace(/【[^】]*】/g, ' '); const bracketTitles = [...text.matchAll(/\[([^\]]+)\]|(([^)]+))|\(([^)]+)\)/g)].map(match => match[1] || match[2] || match[3]).filter(title => title && !/(国语|国粤|配音|字幕|音轨|多音|中字|杜比|HDR|画质|高码|码率|版本|蓝光|原盘|UHD|4K|HQ|DV|Audio)/i.test(title)); if (bracketTitles.length) return bracketTitles[0].trim(); const chinese = text.replace(/\[[^\]]*\]|([^)]*)|\([^)]*\)/g, ' ').match(/[\u4e00-\u9fff][\u4e00-\u9fff·\d:]*/g) || []; return chinese.sort((a, b) => b.length - a.length)[0] || ''; } // 规范片源标记 function extractMovieSource(value) { const text = String(value || '').replace(/[\[\](){}【】]/g, '.'); if (/(?:^|[.\s_-])web[.\s_-]?dl(?:$|[.\s_-])/i.test(text)) return 'WEB-DL'; if (/(?:^|[.\s_-])(?:blu[.\s_-]?ray|bdrip|bdremux)(?:$|[.\s_-])/i.test(text)) return 'BluRay'; return ''; } // 规范分辨率标记 function extractMovieResolution(value) { const text = String(value || '').replace(/[\[\](){}【】]/g, '.'); const match = text.match(/(?:^|[.\s_-])(2160|1080|720|540)p(?:$|[.\s_-])/i); if (match) return match[1] + 'p'; return /(?:^|[.\s_-])4k(?:$|[.\s_-])/i.test(text) ? '2160p' : ''; } // 规范视频编码标记 function extractMovieVideoCodec(value) { const text = String(value || '').replace(/[\[\](){}【】]/g, '.'); if (/(?:^|[.\s_-])(?:hevc|hecv|x[.\s_-]?265|h[.\s_-]?265)(?:$|[.\s_-])/i.test(text)) return 'H265'; if (/(?:^|[.\s_-])(?:avc|x[.\s_-]?264|h[.\s_-]?264)(?:$|[.\s_-])/i.test(text)) return 'H264'; return ''; } // 提取动态范围标记 function extractMovieDynamicRange(value) { const text = String(value || '').replace(/[\[\](){}【】]/g, '.'); const hasDv = /(?:dolby[.\s_-]?vision|(?:^|[.\s_-])(?:dv|dovi)(?:$|[.\s_-]))/i.test(text); const hasHdr = /(?:^|[.\s_-])hdr(?:10\+?)?(?:$|[.\s_-])/i.test(text); if (hasDv && hasHdr) return 'DV.HDR'; if (hasDv) return 'DV'; return hasHdr ? 'HDR' : 'SDR'; } // 提取视频帧率标记 function extractMovieFrameRate(value) { const text = String(value || '').replace(/[\[\](){}【】]/g, '.'); const match = text.match(/(?:^|[.\s_-])(\d{2,3})[.\s_-]?fps(?:$|[.\s_-])/i); return match ? match[1] + 'fps' : ''; } // 提取音频编码标记 function extractMovieAudioCodec(value) { const text = String(value || '').replace(/[\[\](){}【】]/g, '.').replace(/([a-z])(?=\d)/ig, '$1.'); let codec = ''; if (/(?:^|[.\s_-])true[.\s_-]?hd(?:$|[.\s_-])/i.test(text)) codec = 'TrueHD'; else if (/(?:^|[.\s_-])dts[.\s_-]?hd[.\s_-]?ma(?:$|[.\s_-])/i.test(text)) codec = 'DTS-HD.MA'; else if (/(?:^|[.\s_-])dts[.\s_-]?x(?:$|[.\s_-])/i.test(text)) codec = 'DTS-X'; else if (/(?:^|[.\s_-])dts(?:$|[.\s_-])/i.test(text)) codec = 'DTS'; else if (/(?:^|[.\s_-])(?:ddp|e[.\s_-]?ac[.\s_-]?3|dd\+)(?:$|[.\s_-])/i.test(text)) codec = 'DDP'; else if (/(?:^|[.\s_-])(?:dd|ac[.\s_-]?3)(?:$|[.\s_-])/i.test(text)) codec = 'DD'; else if (/(?:^|[.\s_-])aac(?:$|[.\s_-])/i.test(text)) codec = 'AAC'; else if (/(?:^|[.\s_-])flac(?:$|[.\s_-])/i.test(text)) codec = 'FLAC'; else if (/(?:^|[.\s_-])(?:l?pcm)(?:$|[.\s_-])/i.test(text)) codec = 'PCM'; if (!codec) return ''; const channels = text.match(/(?:^|[.\s_-])(\d[.]\d)(?:$|[.\s_-])/); const atmos = /(?:^|[.\s_-])atmos(?:$|[.\s_-])/i.test(text); return codec + (channels ? channels[1] : '') + (atmos ? '.Atmos' : ''); } // 解析电影文件名 function parseMovieName(fileName, path = []) { const cleanName = stripMovieSiteTags(fileName); const extensionMatch = cleanName.match(/(\.[^.]+)$/); const extension = extensionMatch ? extensionMatch[1] : ''; const base = extension ? cleanName.slice(0, -extension.length) : cleanName; const yearMatch = [...base.matchAll(/(?:19|20)\d{2}/g)].filter(match => Number(match[0]) <= new Date().getFullYear() + 1).at(-1); const year = yearMatch ? yearMatch[0] : ''; const beforeYear = yearMatch ? base.slice(0, yearMatch.index) : base; const parentName = path.length ? path[path.length - 1] : ''; const parentHint = extractMovieTitleHint(parentName); const pathHint = /^(?:来自|云添加|下载|电影|影视|视频|资源|文件|新建文件夹|整理完成)$/.test(parentHint) ? '' : parentHint; const bracketHint = extractMovieTitleHint(beforeYear); const withoutBrackets = beforeYear.replace(/【[^】]*】|\[[^\]]*\]|『[^』]*』|([^)]*)|\([^)]*\)/g, ' '); const chinese = withoutBrackets.match(/[\u4e00-\u9fff][\u4e00-\u9fff·\s]*/g)?.map(value => value.trim()).filter(value => value.length > 1) || []; const englishBase = withoutBrackets.replace(/[._]+/g, ' ').replace(/\s+/g, ' ').trim(); const fallbackTitle = englishBase.split(' ').filter((value, index, values) => index === 0 || value !== values[index - 1]).join(' '); const title = (pathHint || bracketHint || chinese[chinese.length - 1] || fallbackTitle || base).replace(/^[\s._-]+|[\s._-]+$/g, '').trim(); const metadata = [base, ...path.map(stripMovieSiteTags)].join('.'); return { title, year, source: extractMovieSource(metadata), resolution: extractMovieResolution(metadata), videoCodec: extractMovieVideoCodec(metadata), dynamicRange: extractMovieDynamicRange(metadata), frameRate: extractMovieFrameRate(metadata), audioCodec: extractMovieAudioCodec(metadata), hq: /高码版|(?:^|[.\s_-])hq(?:$|[.\s_-])/i.test(metadata) ? 'HQ' : '', extension: extension.toLowerCase(), rawName: fileName }; } // 规范电影检索文本 function normalizeMovieText(value) { return String(value || '').toLowerCase().replace(/[._·::/\\()[\]{}【】「」"'’‘-]+/g, ' ').replace(/\s+/g, ' ').trim(); } // 获取 TMDB 电影候选 async function searchTmdbMovieCandidates(info) { const key = await getTmdbKey(); if (!key || !info.title) return null; const query = encodeURIComponent(info.title); const queryText = normalizeMovieText(info.title); const toCandidates = data => (data?.results || []).map(item => { const title = item.title || item.original_title || ''; const originalTitle = item.original_title || ''; const itemYear = String(item.release_date || '').slice(0, 4); const normalizedTitle = normalizeMovieText(title); const normalizedOriginal = normalizeMovieText(originalTitle); const exactTitle = normalizedTitle === queryText || normalizedOriginal === queryText; let score = 0; if (exactTitle) score += 1000; else if (normalizedTitle.includes(queryText) || normalizedOriginal.includes(queryText) || queryText.includes(normalizedTitle) || queryText.includes(normalizedOriginal)) score += 300; if (info.year && itemYear) { const yearGap = Math.abs(Number(info.year) - Number(itemYear)); if (yearGap === 0) score += 120; else if (yearGap === 1) score += 96; else score += Math.max(-80, 40 - yearGap * 8); } score += Math.min(12, Number(item.vote_count || 0) > 0 ? Math.log(Number(item.vote_count) + 1) * 2 : 0); return { id: item.id, title, originalTitle, year: itemYear, score, exactTitle, poster: item.poster_path ? 'https://image.tmdb.org/t/p/w92' + item.poster_path : '' }; }).sort((a, b) => b.score - a.score); const candidates = toCandidates(await requestTmdb('https://api.themoviedb.org/3/search/movie?language=zh-CN®ion=CN&query=' + query, key)); return candidates.filter(item => item.exactTitle || item.score >= 300).slice(0, 10); } // 获取最佳 TMDB 电影候选 async function searchTmdbMovie(info) { const candidates = await searchTmdbMovieCandidates(info); return candidates?.[0] || null; } // 提取剧集的季集编号 function extractTvSeasonEpisode(value) { const text = String(value || '').replace(/[【】[\](){}]/g, '.'); const match = text.match(/(?:^|[.\s_-])s(\d{1,2})[.\s_-]?e(\d{1,3})(?:$|[.\s_-])/i) || text.match(/第\s*(\d{1,2})\s*季\D{0,8}第?\s*(\d{1,3})\s*集/i); if (!match) return { season: '', episode: '', label: '' }; const season = String(Number(match[1])).padStart(2, '0'); const episode = String(Number(match[2])).padStart(2, '0'); return { season, episode, label: 'S' + season + 'E' + episode }; } // 提取剧集剧名 function extractTvTitle(value) { const text = stripMovieSiteTags(value).replace(/【[^】]*】/g, ' ').replace(/\[(?:全?\d+集|更新[^\]]*|国语[^\]]*|国粤[^\]]*|配音[^\]]*|字幕[^\]]*|音轨[^\]]*)\]/gi, ' ').replace(/(?:^|[.\s_-])s\d{1,2}(?:[.\s_-]?e\d{1,3})?(?:$|[.\s_-]).*$/i, ' '); const chinese = text.match(/[\u4e00-\u9fff][\u4e00-\u9fff·\d:]*/g)?.map(item => item.trim()).filter(item => item.length > 1 && !/(?:高清|剧集|发布|国语|配音|字幕|音轨|全集|更新|视频)/.test(item)) || []; if (chinese.length) return chinese.sort((a, b) => b.length - a.length)[0]; return extractMovieTitleHint(text) || parseMovieName(text).title; } // 解析剧集剧集信息 function parseTvEpisode(file, sourceFolder, path = []) { const metadata = file.name; const fileInfo = parseMovieName(file.name); const folderTitle = extractTvTitle(sourceFolder?.name || ''); const fileTitle = extractTvTitle(file.name); const fileSeasonEpisode = extractTvSeasonEpisode(file.name); const seasonEpisode = fileSeasonEpisode.label ? fileSeasonEpisode : extractTvSeasonEpisode(sourceFolder?.name); return { ...fileInfo, title: folderTitle || fileTitle || fileInfo.title, season: seasonEpisode.season, episode: seasonEpisode.episode, seasonEpisode: seasonEpisode.label, source: extractMovieSource(metadata), resolution: extractMovieResolution(metadata), videoCodec: extractMovieVideoCodec(metadata), dynamicRange: extractMovieDynamicRange(metadata), frameRate: extractMovieFrameRate(metadata), audioCodec: extractMovieAudioCodec(metadata), hq: /高码版|(?:^|[.\s_-])hq(?:$|[.\s_-])/i.test(metadata) ? 'HQ' : '' }; } // 获取 TMDB 剧集候选 async function searchTmdbTvCandidates(info) { const key = await getTmdbKey(); if (!key || !info.title) return []; const query = encodeURIComponent(info.title); const queryText = normalizeMovieText(info.title); const candidates = (data => (data?.results || []).map(item => { const title = item.name || item.original_name || ''; const originalTitle = item.original_name || ''; const year = String(item.first_air_date || '').slice(0, 4); const normalizedTitle = normalizeMovieText(title); const normalizedOriginal = normalizeMovieText(originalTitle); const exactTitle = normalizedTitle === queryText || normalizedOriginal === queryText; let score = exactTitle ? 1000 : (normalizedTitle.includes(queryText) || normalizedOriginal.includes(queryText) || queryText.includes(normalizedTitle) || queryText.includes(normalizedOriginal) ? 300 : 0); if (info.year && year) { const gap = Math.abs(Number(info.year) - Number(year)); score += gap === 0 ? 120 : gap === 1 ? 96 : Math.max(-80, 40 - gap * 8); } score += Math.min(12, Number(item.vote_count || 0) > 0 ? Math.log(Number(item.vote_count) + 1) * 2 : 0); return { id: item.id, title, originalTitle, year, score, exactTitle }; }).sort((a, b) => b.score - a.score))(await requestTmdb('https://api.themoviedb.org/3/search/tv?language=zh-CN®ion=CN&query=' + query, key)); return candidates.filter(item => item.exactTitle || item.score >= 300).slice(0, 10); } // 获取最佳 TMDB 剧集候选 async function searchTmdbTv(info) { const candidates = await searchTmdbTvCandidates(info); return candidates[0] || null; } // 获取剧集各季总集数 async function getTmdbTvSeasons(tvId) { const key = await getTmdbKey(); if (!key || !tvId) return []; const data = await requestTmdb('https://api.themoviedb.org/3/tv/' + encodeURIComponent(tvId) + '?language=zh-CN', key); return (data?.seasons || []).map(season => ({ season: Number(season.season_number), total: Number(season.episode_count) || 0, name: season.name || '', year: String(season.air_date || '').slice(0, 4) })).filter(season => Number.isFinite(season.season) && season.season >= 0); } // 判断视频文件大小 function getMovieFileSize(file) { const value = file.size ?? file.fileSize ?? file.file_size ?? file.fileSizeByte ?? 0; if (typeof value === 'number') return value; const match = String(value).match(/([\d.]+)\s*(KB|MB|GB|TB)?/i); if (!match) return 0; const units = { KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4 }; return Number(match[1]) * (units[String(match[2] || '').toUpperCase()] || 1); } // 递归收集大视频文件 async function collectMovieVideos(resources, onProgress) { const result = []; const visited = new Set(); const walk = async (resource, path = []) => { const id = String(resource.id || ''); if (!id || visited.has(id)) return; visited.add(id); if (resource.resType === 1) { if (isVideoFile(resource) && getMovieFileSize(resource) > 10 * 1024 * 1024) result.push({ ...resource, path }); return; } const children = await getGuangyaFiles(id); for (const child of children) await walk(child, [...path, resource.name]); if (onProgress) onProgress(result.length); }; for (const resource of resources) await walk(resource); return result; } // 查找或创建电影目录 async function ensureMovieDirectory(parentId, directoryName) { const files = await getGuangyaFiles(parentId); const existing = files.find(file => file.resType !== 1 && file.name === directoryName); if (existing) return existing.id; let response; try { response = await requestGuangyaApi('https://api.guangyapan.com/nd.bizuserres.s/v1/file/create_dir', { dirName: directoryName, parentId, failIfNameExist: true }); await waitGuangyaTask(response, '创建' + directoryName + '文件夹'); } catch (error) { const refreshed = await getGuangyaFiles(parentId); const created = refreshed.find(file => file.resType !== 1 && file.name === directoryName); if (created) return created.id; throw error; } const findId = value => { if (!value || typeof value !== 'object') return ''; if (value.fileId || value.dirId || value.id) return String(value.fileId || value.dirId || value.id); for (const child of Object.values(value)) { const id = findId(child); if (id) return id; } return ''; }; const createdId = findId(response); if (createdId) return createdId; const refreshed = await getGuangyaFiles(parentId); const created = refreshed.find(file => file.resType !== 1 && file.name === directoryName); if (created) return created.id; throw new Error(response?.msg || '创建' + directoryName + '文件夹失败'); } // 移动单个光鸭资源 async function moveGuangyaFile(fileId, parentId) { const response = await requestGuangyaApi('https://api.guangyapan.com/nd.bizuserres.s/v1/file/move_file', { fileIds: [String(fileId)], parentId: String(parentId) }); const message = response?.msg || response?.data?.msg; if (message && message !== 'success') throw new Error(message); await waitGuangyaTask(response, '移动文件'); return response; } // 读取光鸭登录令牌 function getGuangyaAuthToken() { try { const key = Object.keys(localStorage).find(item => item.startsWith('credentials_')); const credentials = key ? JSON.parse(localStorage.getItem(key)) : null; return credentials?.token_type && credentials?.access_token ? `${credentials.token_type} ${credentials.access_token}` : null; } catch (error) { return null; } } // 记录迅雷页面请求中的鉴权信息 function installXunleiRequestCapture() { const root = document.documentElement; if (!isXunleiPan || root?.dataset.gyhXunleiCapture) return; if (!root) { document.addEventListener('DOMContentLoaded', installXunleiRequestCapture, { once: true }); return; } root.dataset.gyhXunleiCapture = '1'; const eventName = 'gyh:xunlei-context'; document.addEventListener(eventName, event => { const detail = event.detail || {}; if (detail.token) xunleiContext.token = String(detail.token); if (detail.deviceId) xunleiContext.deviceId = String(detail.deviceId); if (detail.captchaToken) xunleiContext.captchaToken = String(detail.captchaToken); if (detail.clientId) xunleiContext.clientId = String(detail.clientId); if (detail.shareToken) xunleiContext.shareToken = String(detail.shareToken); }); const script = document.createElement('script'); script.textContent = `(() => { const eventName = ${JSON.stringify(eventName)}; const read = (headers, name) => { if (!headers) return ''; if (typeof headers.get === 'function') return headers.get(name) || ''; for (const key of Object.keys(headers)) if (key.toLowerCase() === name.toLowerCase()) return headers[key] || ''; return ''; }; const capture = (url, headers) => { const value = String(url || ''); if (!value.includes('api-pan.xunlei.com')) return; const authorization = String(read(headers, 'authorization') || ''); let shareToken = ''; try { shareToken = new URL(value, location.href).searchParams.get('pass_code_token') || ''; } catch (error) {} const detail = { token: authorization.replace(/^Bearer\\s+/i, ''), deviceId: read(headers, 'x-device-id') || read(headers, 'x-guid'), captchaToken: read(headers, 'x-captcha-token'), clientId: read(headers, 'x-client-id'), shareToken }; if (detail.token || detail.deviceId || detail.captchaToken || detail.clientId || detail.shareToken) document.dispatchEvent(new CustomEvent(eventName, { detail })); }; const originalFetch = window.fetch; window.fetch = function(input, init) { capture(typeof input === 'string' ? input : input?.url, init?.headers || input?.headers); return originalFetch.apply(this, arguments); }; const open = XMLHttpRequest.prototype.open; const setHeader = XMLHttpRequest.prototype.setRequestHeader; const send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this.__gyhUrl = url; this.__gyhHeaders = {}; return open.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function(name, value) { if (this.__gyhHeaders) this.__gyhHeaders[name] = value; return setHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { capture(this.__gyhUrl, this.__gyhHeaders); return send.apply(this, arguments); }; })();`; document.documentElement.appendChild(script); script.remove(); } // 从迅雷站点存储中读取上下文值 function getXunleiStoredValue(pattern) { let result = ''; const inspect = (value, key = '', depth = 0) => { if (depth > 3 || value == null) return; if (typeof value === 'string') { if (pattern.test(key) && value.length > result.length) result = value.replace(/^Bearer\s+/i, '').trim(); return; } if (typeof value !== 'object') return; Object.entries(value).forEach(([childKey, childValue]) => inspect(childValue, childKey, depth + 1)); }; [localStorage, sessionStorage].forEach(storage => { try { for (let index = 0; index < storage.length; index += 1) { const key = storage.key(index) || ''; const raw = storage.getItem(key) || ''; inspect(raw, key); if (/^[{\[]/.test(raw)) { try { inspect(JSON.parse(raw), key); } catch (error) {} } } } catch (error) {} }); return result; } // 提取接口返回的验证码 function findXunleiCaptchaToken(value, depth = 0) { if (depth > 4 || value == null) return ''; if (typeof value !== 'object') return ''; for (const [key, child] of Object.entries(value)) { if (/captcha[_-]?token/i.test(key) && typeof child === 'string' && child) return child; const token = findXunleiCaptchaToken(child, depth + 1); if (token) return token; } return ''; } // 发送迅雷 GET 请求 function requestXunleiGet(url, headers, label) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, headers, responseType: 'json', timeout: 30000, onload(response) { const data = response.response || (() => { try { return JSON.parse(response.responseText); } catch (error) { return null; } })(); if (response.status >= 200 && response.status < 300) resolve(data || {}); else { const error = new Error(data?.message || data?.msg || data?.error_description || data?.error || data?.error_details?.map(item => item?.detail || item?.message).filter(Boolean).join(' ') || `${label}请求失败:${response.status}`); error.status = response.status; reject(error); } }, onerror() { reject(new Error(`${label}网络请求失败,请稍后重试`)); }, ontimeout() { reject(new Error(`${label}请求超时,请稍后重试`)); } }); }); } // 尝试刷新迅雷页面凭证 async function refreshXunleiCredentials() { const deviceId = xunleiContext.deviceId || getXunleiStoredValue(/device|guid|(^|_)did$/i); const token = xunleiContext.token || getXunleiStoredValue(/bearer|access[_-]?token|auth[_-]?token|user[_-]?token/i); const clientId = xunleiContext.clientId || getXunleiStoredValue(/client[_-]?id/i) || XUNLEI_CLIENT_ID; if (!deviceId) return false; xunleiContext.deviceId = deviceId; if (token) xunleiContext.token = token; xunleiContext.clientId = clientId; const headers = { 'x-client-id': clientId, Referer: 'https://pan.xunlei.com/', 'x-device-id': deviceId, 'x-guid': deviceId }; if (token) headers.Authorization = `Bearer ${token}`; try { const response = await requestXunleiGet(`${XUNLEI_API_BASE}/drive/v1/captcha/init?client_id=${encodeURIComponent(clientId)}&device_id=${encodeURIComponent(deviceId)}&action=share`, headers, '迅雷云盘'); const captchaToken = findXunleiCaptchaToken(response); if (captchaToken) xunleiContext.captchaToken = captchaToken; } catch (error) {} return Boolean(xunleiContext.deviceId && xunleiContext.captchaToken); } // 判断是否需要刷新迅雷凭证 function isXunleiCredentialError(error) { return /captcha|device.?id|凭证|登录态|authorization|token|unauth|401|403/i.test(String(error?.message || error || '')); } // 判断是否可以重试迅雷请求 function isXunleiRetryableError(error) { return isXunleiCredentialError(error) || /网络|超时|429|5\d\d/i.test(String(error?.message || error || '')); } // 执行带凭证刷新的迅雷请求 async function requestXunleiWithRetry(request) { let lastError = null; let refreshed = false; for (let attempt = 1; attempt <= 3; attempt += 1) { try { return await request(); } catch (error) { lastError = error; const credentialError = isXunleiCredentialError(error); if (credentialError && !refreshed) { refreshed = true; if (xunleiRetryNotifier) xunleiRetryNotifier({ phase: 'refreshing' }); await refreshXunleiCredentials(); continue; } if (attempt < 3 && isXunleiRetryableError(error)) { if (xunleiRetryNotifier) xunleiRetryNotifier({ phase: 'retrying', attempt: attempt + 1 }); await delay(400 * attempt); continue; } break; } } if (isXunleiCredentialError(lastError)) throw new Error('迅雷云盘凭证刷新失败,请刷新页面后重试'); throw lastError || new Error('迅雷云盘请求失败'); } // 获取迅雷接口请求头 function getXunleiHeaders() { const token = xunleiContext.token || getXunleiStoredValue(/bearer|access[_-]?token|auth[_-]?token|user[_-]?token/i); const deviceId = xunleiContext.deviceId || getXunleiStoredValue(/device|guid|(^|_)did$/i); const captchaToken = xunleiContext.captchaToken; const clientId = xunleiContext.clientId || getXunleiStoredValue(/client[_-]?id/i) || XUNLEI_CLIENT_ID; if (!token) throw new Error('未检测到迅雷云盘登录态,请登录后重试'); if (!deviceId || !captchaToken) throw new Error('正在获取迅雷云盘凭证,请刷新页面后重试'); xunleiContext.token = token; xunleiContext.deviceId = deviceId; xunleiContext.clientId = clientId; const headers = { 'Content-Type': 'application/json', 'x-client-id': clientId, Referer: 'https://pan.xunlei.com/', Authorization: `Bearer ${token}` }; if (deviceId) { headers['x-device-id'] = deviceId; headers['x-guid'] = deviceId; } if (captchaToken) headers['x-captcha-token'] = captchaToken; return { headers, deviceId }; } // 请求迅雷云盘接口 function requestXunleiApi(url) { return requestXunleiWithRetry(() => { const context = getXunleiHeaders(); const requestUrl = new URL(url); requestUrl.searchParams.set('device_id', context.deviceId); requestUrl.searchParams.set('did', context.deviceId); return requestXunleiGet(requestUrl.toString(), context.headers, '迅雷云盘'); }); } // 判断是否处于迅雷分享页面 function isXunleiSharePage() { return isXunleiPan && /^\/s\//.test(window.location.pathname); } // 获取当前迅雷分享上下文 function getXunleiShareContext() { const shareId = window.location.pathname.match(/^\/s\/([^/?#]+)/)?.[1] || ''; const passCodeToken = xunleiContext.shareToken || new URLSearchParams(window.location.search).get('pwd') || ''; if (!shareId || !passCodeToken) throw new Error('无法读取迅雷分享凭证,请刷新分享页面后重试'); return { shareId, passCodeToken }; } // 请求迅雷分享接口 function requestXunleiShareApi(url) { return requestXunleiWithRetry(() => { const deviceId = xunleiContext.deviceId || getXunleiStoredValue(/device|guid|(^|_)did$/i); const captchaToken = xunleiContext.captchaToken; if (!deviceId || !captchaToken) throw new Error('正在获取迅雷分享凭证,请刷新页面后重试'); xunleiContext.deviceId = deviceId; const requestUrl = new URL(url); requestUrl.searchParams.set('device_id', deviceId); requestUrl.searchParams.set('did', deviceId); const headers = { 'Content-Type': 'application/json', 'x-client-id': xunleiContext.clientId || XUNLEI_CLIENT_ID, Referer: 'https://pan.xunlei.com/', 'x-device-id': deviceId, 'x-guid': deviceId, 'x-captcha-token': captchaToken }; return requestXunleiGet(requestUrl.toString(), headers, '迅雷分享'); }); } // 标准化迅雷文件对象 function normalizeXunleiFile(file, path = '') { const name = String(file?.name ?? file?.fileName ?? ''); return { ...file, id: String(file?.id ?? file?.fileId ?? ''), name, parentId: String(file?.parent_id ?? file?.parentId ?? ''), kind: String(file?.kind ?? ''), size: Number(file?.size ?? file?.fileSize ?? 0) || 0, path, hash: String(file?.hash ?? file?.gcid ?? ''), md5: String(file?.md5_checksum ?? file?.md5 ?? file?.etag ?? '') }; } // 获取迅雷目录内容 async function getXunleiFiles(parentId) { const files = []; let pageToken = ''; const filters = encodeURIComponent(JSON.stringify({ phase: { eq: 'PHASE_TYPE_COMPLETE' }, trashed: { eq: false } })); do { const response = await requestXunleiApi(`${XUNLEI_API_BASE}/drive/v1/files?parent_id=${encodeURIComponent(parentId)}&usage=DISPLAY&filters=${filters}&with_audit=true&limit=100&page_token=${encodeURIComponent(pageToken)}`); files.push(...(response?.files || []).map(file => normalizeXunleiFile(file))); pageToken = String(response?.next_page_token || ''); } while (pageToken); return files; } // 获取迅雷分享目录内容 async function getXunleiShareFiles(parentId, context) { const files = []; let pageToken = ''; do { const response = await requestXunleiShareApi(`${XUNLEI_API_BASE}/drive/v1/share/detail?share_id=${encodeURIComponent(context.shareId)}&parent_id=${encodeURIComponent(parentId)}&pass_code_token=${encodeURIComponent(context.passCodeToken)}&limit=100&page_token=${encodeURIComponent(pageToken)}&with_audit=true&thumbnail_size=SIZE_LARGE&usage=CONSUME`); files.push(...(response?.files || response?.data?.files || []).map(file => normalizeXunleiFile(file))); pageToken = String(response?.next_page_token || response?.data?.next_page_token || ''); } while (pageToken); return files; } // 获取迅雷文件详情补充哈希 async function getXunleiFileDetail(file) { const response = await requestXunleiApi(`${XUNLEI_API_BASE}/drive/v1/files/${encodeURIComponent(file.id)}?space=&usage=CONSUME`); return normalizeXunleiFile(response?.file || response?.data || response, file.path); } // 获取迅雷分享文件详情 async function getXunleiShareFileDetail(file, context) { const response = await requestXunleiShareApi(`${XUNLEI_API_BASE}/drive/v1/share/file_info?share_id=${encodeURIComponent(context.shareId)}&file_id=${encodeURIComponent(file.id)}&pass_code_token=${encodeURIComponent(context.passCodeToken)}&usage=CONSUME`); return normalizeXunleiFile(response?.file_info || response?.file || response?.data || response, file.path); } // 读取迅雷页面选择的资源 async function getSelectedXunleiResources() { if (isXunleiSharePage()) { const context = getXunleiShareContext(); const files = await getXunleiShareFiles('', context); const selectedRows = [...document.querySelectorAll("li[class*='SourceListItem__item']")].filter(row => row.querySelector("input[type='checkbox']:checked")); if (!selectedRows.length) return []; return files.filter(file => selectedRows.some(row => row.textContent.includes(file.name))); } const items = [...document.querySelectorAll("[class*='SourceListItem__item']")]; const store = items.map(item => item.__vue__?.$store).find(Boolean); const selected = items.map(item => item.__vue__?.$props?.selected).find(value => Array.isArray(value) && value.length) || []; const all = store?.state?.drive?.all || {}; return selected.map(item => typeof item === 'object' ? item : all[item]).filter(Boolean).map(file => normalizeXunleiFile(file)); } // 递归收集迅雷所选资源 async function collectXunleiExportFiles(resources, onProgress) { const files = []; const visited = new Set(); const walk = async (resource, path = []) => { if (!resource?.id || visited.has(resource.id)) return; visited.add(resource.id); if (resource.kind !== 'drive#folder') { files.push({ ...resource, path }); if (onProgress) onProgress({ phase: 'scanning', count: files.length }); return; } const children = await getXunleiFiles(resource.id); for (const child of children) await walk(child, [...path, resource.name]); }; for (const resource of resources) await walk(resource); return files; } // 递归收集迅雷分享所选资源 async function collectXunleiShareExportFiles(resources, context, onProgress) { const files = []; const visited = new Set(); const walk = async (resource, path = []) => { if (!resource?.id || visited.has(resource.id)) return; visited.add(resource.id); if (resource.kind !== 'drive#folder') { files.push({ ...resource, path }); if (onProgress) onProgress({ phase: 'scanning', count: files.length }); return; } const children = await getXunleiShareFiles(resource.id, context); for (const child of children) await walk(child, [...path, resource.name]); }; for (const resource of resources) await walk(resource); return files; } // 构建迅雷秒传导出数据 async function buildXunleiRapidExportData(resources, onProgress) { const shareContext = isXunleiSharePage() ? getXunleiShareContext() : null; const sourceFiles = shareContext ? await collectXunleiShareExportFiles(resources, shareContext, onProgress) : await collectXunleiExportFiles(resources, onProgress); const skipped = []; const files = []; for (let index = 0; index < sourceFiles.length; index += 1) { const sourceFile = sourceFiles[index]; let file = sourceFile; let hash = getGuangyaRapidHash(file); if (onProgress) onProgress({ phase: 'checking', index: index + 1, total: sourceFiles.length }); if (!hash.gcid && !hash.md5) { try { file = { ...file, ...(shareContext ? await getXunleiShareFileDetail(file, shareContext) : await getXunleiFileDetail(file)) }; hash = getGuangyaRapidHash(file); } catch (error) {} } if (!hash.gcid && !hash.md5) { skipped.push(file.name); continue; } const md5 = hash.md5 || hash.gcid.slice(0, 32).toLowerCase(); const entry = { path: [...file.path, file.name].filter(Boolean).join('/'), size: Math.max(0, Number(file.size) || 0), sourceXunlei: true, fileId: file.id, parentId: file.parentId }; if (hash.gcid) entry.gcid = hash.gcid; if (md5) entry.md5 = md5; if (hash.cid) entry.cid = hash.cid; if (hash.wholeCid) entry.wholeCid = hash.wholeCid; files.push(entry); } if (!files.length) throw new Error('选中的文件缺少秒传信息,暂无法导出'); return { data: { version: '1.0', source: 'xunlei', createdAt: new Date().toISOString(), files }, total: sourceFiles.length, skipped }; } // 获取当前目录 ID function getCurrentGuangyaDirectoryId() { const segments = window.location.hash.replace(/^#\/home\/all\/?/, '').split('/').filter(Boolean); return segments.length ? segments[segments.length - 1].split('-')[0] : ''; } // 请求光鸭开放接口 function requestGuangyaApi(url, body) { const token = getGuangyaAuthToken(); if (!token) return Promise.reject(new Error('未登录,请先登录光鸭云盘')); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url, headers: { 'Content-Type': 'application/json', Authorization: token }, data: JSON.stringify(body), responseType: 'json', onload(response) { const data = response.response || (() => { try { return JSON.parse(response.responseText); } catch (error) { return null; } })(); if (response.status >= 200 && response.status < 300) resolve(data); else reject(new Error(data?.msg || `请求失败:${response.status}`)); }, onerror() { reject(new Error('网络请求失败,请稍后重试')); }, }); }); } // 请求秒传接口 function requestGuangyaRapidApi(url, body) { const token = getGuangyaAuthToken(); if (!token) return Promise.reject(new Error('未登录,请先登录光鸭云盘')); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url, headers: { 'Content-Type': 'application/json;charset=utf-8', Authorization: token, dt: '4' }, data: JSON.stringify(body), responseType: 'json', timeout: 20000, onload(response) { const data = response.response || (() => { try { return JSON.parse(response.responseText); } catch (error) { return null; } })(); if (response.status >= 200 && response.status < 300) resolve(data || {}); else reject(new Error(data?.msg || `请求失败:${response.status}`)); }, onerror() { reject(new Error('网络请求失败,请稍后重试')); }, ontimeout() { reject(new Error('请求超时,请稍后重试')); }, }); }); } // 读取秒传任务编号 function findRapidTaskId(value) { if (!value || typeof value !== 'object') return ''; if (value.taskId || value.task_id) return String(value.taskId || value.task_id); for (const child of Object.values(value)) { const id = findRapidTaskId(child); if (id) return id; } return ''; } // 提取标准 MD5 function getRapidMd5(value) { const match = String(value || '').match(/[a-f\d]{32}/i); return match ? match[0].toLowerCase() : ''; } // 提取标准 GCID function getRapidGcid(value) { const match = String(value || '').match(/[a-f\d]{40}/i); return match ? match[0].toUpperCase() : ''; } // 规范导入文件路径 function normalizeRapidImportEntry(entry, index) { if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`第 ${index + 1} 项不是文件对象`); const rawPath = String(entry.path || entry.name || '').trim().replace(/\\/g, '/'); if (!rawPath) throw new Error(`第 ${index + 1} 项缺少 path`); if (/\/$/.test(rawPath)) throw new Error(`第 ${index + 1} 项的 path 不能是目录`); const parts = rawPath.replace(/^\/+/, '').split('/').map(part => part.trim()).filter(Boolean); if (!parts.length || parts.some(part => part === '.' || part === '..' || /[\0\r\n]/.test(part))) throw new Error(`第 ${index + 1} 项的 path 无效`); const size = Number(entry.size ?? entry.fileSize ?? entry.file_size ?? 0); if (!Number.isFinite(size) || size < 0) throw new Error(`第 ${index + 1} 项的 size 无效`); const md5 = getRapidMd5(entry.md5 || entry.etag); const gcid = getRapidGcid(entry.gcid); if (!md5 && !gcid) throw new Error(`第 ${index + 1} 项缺少有效的 MD5 或 GCID`); return { path: parts.join('/'), name: parts.at(-1), directories: parts.slice(0, -1), size, md5, gcid, cid: getRapidGcid(entry.cid || entry.wholeCid || entry.tripleCid) }; } // 解析秒传 JSON function parseRapidImportJson(rawJson) { let value; try { value = JSON.parse(String(rawJson || '')); } catch (error) { throw new Error('JSON 解析失败,请检查内容格式'); } if (!value || typeof value !== 'object' || Array.isArray(value) || !Array.isArray(value.files)) throw new Error('JSON 顶层需包含 files 数组'); if (!value.files.length) throw new Error('files 数组为空,没有可导入的文件'); const files = []; const invalid = []; value.files.forEach((entry, index) => { try { files.push({ ...normalizeRapidImportEntry(entry, index), sourceIndex: index }); } catch (error) { invalid.push(error.message); } }); if (!files.length) throw new Error(invalid[0] || '没有可导入的有效文件'); return { files, invalid }; } // 创建并缓存导入目录 async function ensureRapidDirectoryPath(rootId, directories, cache) { let parentId = String(rootId || ''); let fullPath = ''; for (const directory of directories) { fullPath = fullPath ? `${fullPath}/${directory}` : directory; if (cache.has(fullPath)) { parentId = cache.get(fullPath); continue; } parentId = String(await ensureMovieDirectory(parentId, directory)); cache.set(fullPath, parentId); } return parentId; } // 等待秒传任务完成 async function waitRapidImportTask(taskId) { for (let attempt = 0; attempt < 60; attempt += 1) { const response = await requestGuangyaRapidApi('https://api.guangyapan.com/userres/v1/file/get_info_by_task_id', { taskId: String(taskId) }); const data = response?.data || {}; if (data.fileId || data.file_id || data.id) return; const status = String(data.taskStatus ?? data.task_status ?? data.status ?? data.state ?? '').toUpperCase(); if ([3, 4].includes(Number(status)) || /FAIL|ERROR|CANCEL|失败|错误|取消/.test(status)) throw new Error(response?.msg || '秒传任务失败'); await delay(500); } throw new Error('秒传任务超时'); } // 执行单个秒传文件导入 async function importRapidFile(file, parentId) { const tokenUrl = 'https://api.guangyapan.com/userres/v1/get_res_center_token'; const checkUrl = 'https://api.guangyapan.com/userres/v1/check_can_flash_upload'; const fileSize = Number(file.size); const gcid = file.gcid; const validCid = file.cid && file.cid !== gcid ? file.cid : ''; const candidates = []; const addCandidate = (capacity, res) => { const key = `${capacity}:${JSON.stringify(res)}`; if (!candidates.some(candidate => candidate.key === key)) candidates.push({ key, capacity, res }); }; if (gcid) { if (validCid) addCandidate(2, { gcid, cid: validCid, ...(file.md5 ? { md5: file.md5 } : {}), fileSize }); if (file.md5) addCandidate(2, { gcid, md5: file.md5, fileSize }); addCandidate(2, { gcid, fileSize }); } else { addCandidate(1, { md5: file.md5, fileSize }); } let tokenResponse = null; let taskId = ''; let tokenError = null; for (const candidate of candidates) { try { const response = await requestGuangyaRapidApi(tokenUrl, { capacity: candidate.capacity, res: candidate.res, name: file.name, parentId: String(parentId || '') }); if (Number(response?.code) === 156) return; const currentTaskId = findRapidTaskId(response?.data || response); if (currentTaskId) { tokenResponse = response; taskId = currentTaskId; break; } tokenResponse = response; } catch (error) { tokenError = error; } } if (!gcid) throw new Error(tokenResponse?.msg || tokenError?.message || '资源未命中,无法秒传'); if (!taskId) throw new Error(tokenResponse?.msg || tokenError?.message || '未能创建秒传任务'); const checkCandidates = []; if (validCid) checkCandidates.push({ taskId, gcid, cid: validCid, ...(file.md5 ? { md5: file.md5 } : {}), fileSize }); if (file.md5) checkCandidates.push({ taskId, gcid, md5: file.md5, fileSize }); checkCandidates.push({ taskId, gcid, fileSize }); let checkResponse = null; let pollTaskId = ''; let checkError = null; for (const body of checkCandidates) { try { const response = await requestGuangyaRapidApi(checkUrl, body); checkResponse = response; const data = response?.data || {}; if (data.canFlashUpload === true || data.fileId || data.file_id) { pollTaskId = findRapidTaskId(data) || taskId; break; } const nextTaskId = findRapidTaskId(data); if (nextTaskId) { pollTaskId = nextTaskId; break; } } catch (error) { checkError = error; } } if (!pollTaskId) throw new Error(checkResponse?.msg || checkError?.message || '资源未命中,无法秒传'); await waitRapidImportTask(pollTaskId); } // 并发导入秒传文件 async function runRapidImport(rawJson, onProgress) { const parsed = parseRapidImportJson(rawJson); const rootId = getCurrentGuangyaDirectoryId(); const directoryCache = new Map([['', String(rootId || '')]]); const summary = { total: parsed.files.length, success: 0, failed: 0, invalid: parsed.invalid, failures: [] }; for (let index = 0; index < parsed.files.length; index += 1) { const file = parsed.files[index]; try { if (onProgress) onProgress({ phase: 'preparing', index: index + 1, ...summary, file }); file.parentId = await ensureRapidDirectoryPath(rootId, file.directories, directoryCache); if (onProgress) onProgress({ phase: 'prepared', index: index + 1, ...summary, file }); } catch (error) { summary.failed += 1; summary.failures.push({ path: file.path, reason: error?.message || '导入失败' }); if (onProgress) onProgress({ phase: 'failed', index: index + 1, ...summary, file, error }); } } const queue = parsed.files.filter(file => file.parentId !== undefined); let nextIndex = 0; const worker = async () => { while (true) { const file = queue[nextIndex++]; if (!file) return; const index = file.sourceIndex + 1; try { if (onProgress) onProgress({ phase: 'processing', index, ...summary, file }); await importRapidFile(file, file.parentId); summary.success += 1; if (onProgress) onProgress({ phase: 'success', index, ...summary, file }); } catch (error) { summary.failed += 1; summary.failures.push({ path: file.path, reason: error?.message || '导入失败' }); if (onProgress) onProgress({ phase: 'failed', index, ...summary, file, error }); } if (nextIndex < queue.length) await delay(100); } }; await Promise.all(Array.from({ length: Math.min(3, queue.length) }, worker)); return summary; } // 查找异步任务编号 function findGuangyaTaskId(value) { if (!value || typeof value !== 'object') return ''; if (value.taskId || value.task_id) return String(value.taskId || value.task_id); for (const child of Object.values(value)) { const id = findGuangyaTaskId(child); if (id) return id; } return ''; } // 等待光鸭异步任务完成 async function waitGuangyaTask(response, label) { const taskId = findGuangyaTaskId(response); if (!taskId) return; for (let attempt = 0; attempt < 40; attempt += 1) { const taskResponse = await requestGuangyaApi('https://api.guangyapan.com/nd.bizuserres.s/v1/get_task_status', { taskId }); const task = taskResponse?.data || taskResponse; const status = String(task?.taskStatus ?? task?.task_status ?? task?.status ?? task?.state ?? '').toUpperCase(); if (Number(status) === 2 || /SUCCESS|SUCCEED|DONE|FINISH|COMPLET|成功|完成/.test(status)) return; if ([3, 4].includes(Number(status)) || /FAIL|ERROR|CANCEL|失败|错误|取消/.test(status)) throw new Error(label + '失败'); await delay(500); } throw new Error(label + '超时'); } // 获取目录下的全部文件 async function getGuangyaFiles(parentId) { const files = []; let page = 0; while (true) { const response = await requestGuangyaApi( 'https://api.guangyapan.com/nd.bizuserres.s/v1/file/get_file_list', { page, parentId, pageSize: 1000, orderBy: 1, sortType: 1 } ); const list = response?.data?.list || []; files.push(...list.map(file => ({ ...file, id: String(file.fileId ?? file.id ?? ''), name: String(file.fileName ?? file.name ?? ''), resType: Number(file.resType), fileType: file.fileType, size: Number(file.fileSize ?? file.size ?? file.fileSizeByte ?? file.file_size ?? 0), parentId: String(file.parentId ?? parentId ?? '') }))); if (list.length === 0 || files.length >= (response?.data?.total || 0)) break; page += 1; } return files; } // 提取可用于秒传的文件哈希 function getGuangyaRapidHash(file) { const values = [file?.gcid, file?.md5, file?.etag, file?.hash, file?.fileHash, file?.fileMd5, file?.cid, file?.tripleCid, file?.wholeCid, file?.whole_cid, file?.sha1, file?.fileSha1].map(value => String(value || '').trim()); const gcid = values.find(value => /^[a-f\d]{40}$/i.test(value)) || ''; const md5 = values.find(value => /^[a-f\d]{32}$/i.test(value)) || ''; const cid = [file?.cid, file?.tripleCid, file?.triple_cid, file?.sampleCid, file?.sampleFileCid, file?.chunkCid, file?.chunkHash].map(value => String(value || '').trim()).find(value => /^[a-f\d]{40}$/i.test(value) && value.toLowerCase() !== gcid.toLowerCase()) || ''; const wholeCid = [file?.wholeCid, file?.whole_cid, file?.wholeSha1, file?.whole_sha1, file?.fileSha1, file?.file_sha1, file?.sha1].map(value => String(value || '').trim()).find(value => /^[a-f\d]{40}$/i.test(value) && value.toLowerCase() !== gcid.toLowerCase() && value.toLowerCase() !== cid.toLowerCase()) || ''; return { md5: md5.toLowerCase(), gcid: gcid.toUpperCase(), cid: cid.toUpperCase(), wholeCid: wholeCid.toUpperCase() }; } // 递归收集所选资源中的全部文件 async function collectGuangyaExportFiles(resources, onProgress) { const files = []; const visited = new Set(); const walk = async (resource, path = []) => { const id = String(resource?.id || ''); if (!id || visited.has(id)) return; visited.add(id); if (resource.resType === 1) { files.push({ ...resource, path }); if (onProgress) onProgress({ phase: 'scanning', count: files.length }); return; } const children = await getGuangyaFiles(id); for (const child of children) await walk(child, [...path, resource.name]); }; for (const resource of resources) await walk(resource); return files; } // 构建光鸭秒传导出数据 async function buildGuangyaRapidExportData(resources, onProgress) { const sourceFiles = await collectGuangyaExportFiles(resources, onProgress); const skipped = []; const files = []; sourceFiles.forEach((file, index) => { if (onProgress) onProgress({ phase: 'building', index: index + 1, total: sourceFiles.length }); const hash = getGuangyaRapidHash(file); if (!hash.md5 && !hash.gcid) { skipped.push(file.name); return; } const md5 = hash.md5 || hash.gcid.slice(0, 32).toLowerCase(); const entry = { path: [...file.path, file.name].filter(Boolean).join('/'), size: Math.max(0, Number(file.size) || 0) }; if (md5) entry.md5 = md5; if (hash.gcid) entry.gcid = hash.gcid; if (hash.cid) entry.cid = hash.cid; if (hash.wholeCid) entry.wholeCid = hash.wholeCid; files.push(entry); }); if (!files.length) throw new Error('选中的文件缺少秒传信息,暂无法导出'); return { data: { version: '1.0', source: 'guangya', createdAt: new Date().toISOString(), files }, total: sourceFiles.length, skipped }; } const guangyaSelection = { selectedFiles: new Map(), fileCache: new Map(), filesByName: new Map(), directoryId: null, cachePromise: null, isUpdating: false, }; // 合并短时间内的重复调用 function debounce(callback, wait = 250) { let timer = null; return (...args) => { window.clearTimeout(timer); timer = window.setTimeout(() => callback(...args), wait); }; } // 确保当前目录文件已缓存 async function ensureGuangyaFileCache() { const directoryId = getCurrentGuangyaDirectoryId(); if (guangyaSelection.directoryId === directoryId && guangyaSelection.fileCache.size > 0) return; if (guangyaSelection.directoryId !== directoryId) { guangyaSelection.directoryId = directoryId; guangyaSelection.selectedFiles.clear(); guangyaSelection.fileCache.clear(); guangyaSelection.filesByName.clear(); } if (guangyaSelection.cachePromise) return guangyaSelection.cachePromise; guangyaSelection.cachePromise = getGuangyaFiles(directoryId) .then((files) => { guangyaSelection.fileCache = new Map(files.map(file => [file.id, file])); guangyaSelection.filesByName.clear(); files.forEach((file) => { const entries = guangyaSelection.filesByName.get(file.name) || []; entries.push(file.id); guangyaSelection.filesByName.set(file.name, entries); }); }) .finally(() => { guangyaSelection.cachePromise = null; }); return guangyaSelection.cachePromise; } // 从缓存中按名称查找文件 function getCachedFileByName(fileName, preferUnselected = true) { const ids = guangyaSelection.filesByName.get(fileName) || []; const matchedId = preferUnselected ? ids.find(id => !guangyaSelection.selectedFiles.has(id)) || ids[0] : ids[0]; return matchedId ? guangyaSelection.fileCache.get(matchedId) : null; } // 将文件加入选择集合 async function selectGuangyaFile(fileName) { await ensureGuangyaFileCache(); const file = getCachedFileByName(fileName); if (file) guangyaSelection.selectedFiles.set(file.id, file); } // 将文件移出选择集合 function unselectGuangyaFile(fileName) { const ids = guangyaSelection.filesByName.get(fileName) || []; const selectedId = ids.find(id => guangyaSelection.selectedFiles.has(id)); if (selectedId) guangyaSelection.selectedFiles.delete(selectedId); } // 同步页面可见的选中项 async function syncVisibleGuangyaSelection() { const selectedNames = Array.from(document.querySelectorAll('.swangpan-file-list-table__row[data-state="selected"]')) .map(row => row.querySelector('[title]')?.getAttribute('title')) .filter(Boolean); if (!selectedNames.length) return; await ensureGuangyaFileCache(); selectedNames.forEach((fileName) => { const file = getCachedFileByName(fileName); if (file) guangyaSelection.selectedFiles.set(file.id, file); }); } // 绑定文件选择事件 function bindGuangyaSelectionEvents() { const headerCheckbox = document.querySelector('.swangpan-file-list-table__header .swangpan-checkbox__input'); if (headerCheckbox && !headerCheckbox.dataset.gyhBound) { headerCheckbox.dataset.gyhBound = '1'; headerCheckbox.addEventListener('change', async (event) => { if (guangyaSelection.isUpdating) return; guangyaSelection.isUpdating = true; try { if (event.target.checked) { await ensureGuangyaFileCache(); guangyaSelection.selectedFiles = new Map(guangyaSelection.fileCache); } else { guangyaSelection.selectedFiles.clear(); } } finally { guangyaSelection.isUpdating = false; } }); } const fileListBody = document.querySelector('.swangpan-file-list-table__body'); if (fileListBody && !fileListBody.dataset.gyhBound) { fileListBody.dataset.gyhBound = '1'; fileListBody.addEventListener('click', (event) => { if (guangyaSelection.isUpdating) return; window.setTimeout(async () => { const row = event.target.closest('.swangpan-file-list-table__row'); if (!row) return; const fileName = row.querySelector('[title]')?.getAttribute('title'); if (!fileName) return; guangyaSelection.isUpdating = true; try { if (row.getAttribute('data-state') === 'selected') { await selectGuangyaFile(fileName); } else { unselectGuangyaFile(fileName); } } finally { guangyaSelection.isUpdating = false; } }, 0); }); } } // 初始化选择状态监听 function initializeGuangyaSelection() { let lastDirectoryId = getCurrentGuangyaDirectoryId(); const rebind = debounce(() => { const currentDirectoryId = getCurrentGuangyaDirectoryId(); if (currentDirectoryId !== lastDirectoryId) { guangyaSelection.directoryId = null; guangyaSelection.selectedFiles.clear(); lastDirectoryId = currentDirectoryId; } bindGuangyaSelectionEvents(); syncVisibleGuangyaSelection().catch(() => {}); ensureGuangyaFileCache().catch(() => {}); }); bindGuangyaSelectionEvents(); syncVisibleGuangyaSelection().catch(() => {}); ensureGuangyaFileCache().catch(() => {}); new MutationObserver(rebind).observe(document.body, { childList: true, subtree: true }); } // 返回当前已选文件 async function getSelectedGuangyaFiles() { await syncVisibleGuangyaSelection(); return [...guangyaSelection.selectedFiles.values()].filter(file => file.resType === 1); } // 返回当前已选资源 async function getSelectedGuangyaResources() { await syncVisibleGuangyaSelection(); return [...guangyaSelection.selectedFiles.values()]; } // 重命名单个光鸭文件 async function renameGuangyaFile(fileId, newName) { const response = await requestGuangyaApi( 'https://api.guangyapan.com/nd.bizuserres.s/v1/file/rename', { fileId, newName } ); if (response?.msg !== 'success') throw new Error(response?.msg || '重命名失败'); await waitGuangyaTask(response, '重命名'); } // 转义预览中的文本内容 function escapeHtml(value) { return String(value).replace(/[&<>"]/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', })[character]); } // 判断文件是否为视频 function isVideoFile(file) { return file.fileType === 2 || /\.(mp4|mkv|avi|mov|wmv|flv|webm|m4v|ts|m2ts|mpg|mpeg)$/i.test(file.name); } let renameOverlay = null; // 关闭重命名弹窗并按需刷新 function closeRenameDesigner() { if (!renameOverlay) return; const shouldRefresh = renameOverlay._gyhShouldRefresh === true; if (renameOverlay._gyhOnKeydown) { document.removeEventListener('keydown', renameOverlay._gyhOnKeydown); } renameOverlay.remove(); renameOverlay = null; if (shouldRefresh) window.location.reload(); } // 渲染当前方式的规则输入项 function renderRenameFields(container, mode) { const fieldsByMode = { sequence: [ ['前缀', '例如:第'], ['起始编号', '例如:01'], ['后缀', '例如:集'], ], append: [ ['前缀', '追加到文件名前'], ['后缀', '追加到文件名后'], ['扩展名', '保留原扩展名'], ], replace: [ ['查找内容', '输入要替换的文字'], ['替换为', '输入新的文字'], ['匹配选项', '忽略大小写'], ], regex: [ ['正则表达式', '例如:\\s+'], ['替换为', '输入替换内容'], ['匹配选项', '全局匹配'], ], format: [ ['新格式名', '例如:mp4'], ['命名规则', '保留原文件名'], ['预览方式', '仅修改扩展名'], ], }; container.innerHTML = fieldsByMode[mode].map(([label, placeholder]) => ` `).join(''); } // 根据规则生成重命名预览 function buildRenamePlan(files, mode, inputs) { const values = Array.from(inputs).map(input => input.value || ''); let regex = null; if (mode === 'regex' && values[0]) { try { regex = new RegExp(values[0], 'g'); } catch (error) { return { error: '正则表达式格式不正确', items: [] }; } } const items = files.map((file, index) => { const lastDot = file.name.lastIndexOf('.'); const baseName = lastDot > 0 ? file.name.slice(0, lastDot) : file.name; const extension = lastDot > 0 ? file.name.slice(lastDot) : ''; let newName = file.name; if (mode === 'sequence') { const startText = values[1] || ''; const parsedStart = Number.parseInt(startText, 10); const start = Number.isNaN(parsedStart) || parsedStart === 0 ? 1 : parsedStart; newName = `${values[0]}${String(start + index).padStart(startText.length, '0')}${values[2]}${extension}`; } else if (mode === 'append') { newName = `${values[0]}${baseName}${values[1]}${extension}`; } else if (mode === 'replace' && values[0]) { const ignoreCase = values[2].includes('忽略'); newName = ignoreCase ? file.name.replace(new RegExp(values[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), values[1]) : file.name.split(values[0]).join(values[1]); } else if (mode === 'regex' && regex) { newName = file.name.replace(regex, values[1]); } else if (mode === 'format' && values[0]) { newName = `${baseName}.${values[0].replace(/^\./, '')}`; } return { ...file, newName }; }); return { error: null, items }; } // 渲染文件名称预览行 function renderRenamePreview(container, items) { container.innerHTML = ` ${items.map((file, index) => `
正在准备导出内容
正在读取所选文件
正在递归读取文件夹
生成可用于秒传导入的 JSON 文件
选择文件或直接填写秒传内容
配置后才能开始影视识别
匹配 TMDB 后预览,确认后再整理
查找后选择正确的影片再确认
匹配 TMDB 后预览,确认后再整理
查找后选择正确的剧集再确认
' + escapeHtml(group.folderName || group.folder.name) + '
设置规则并预览文件的新名称