// ==UserScript== // @name 115Aria // @namespace http://tampermonkey.net/ // @version 0.4 // @description 115.com OpenList直链发送到aria2 RPC // @author jiemo // @match *://115.com/* // @match *://*.115.com/* // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect * // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; const STORE_PREFIX = 'aria115_'; const SETTINGS_KEY = 'aria115_settings_v1'; const DEFAULT_OPENLIST_HOST = 'https://abc.com'; const DEFAULT_OPENLIST_MOUNT_PATH = '115'; const BREADCRUMB_CLASS = 'flex items-center text-xs overflow-x-auto gap-3 flex-wrap pr-6'; const DEFAULT_DIR = '/Users/Administrator/Downloads'; const CONTAINER_ID = 'aria115-container'; const MODAL_ID = 'aria115-settings-modal'; const STYLE_ID = 'aria115-style'; const FOLDER_SCAN_LIMIT = 5000; const DEFAULT_SETTINGS = { version: 1, openlistHost: DEFAULT_OPENLIST_HOST, openlistMountPath: DEFAULT_OPENLIST_MOUNT_PATH, activeRpcId: 'local-win', activePath: DEFAULT_DIR, rpcConfigs: [ { id: 'local-win', name: '本地Win', endpoint: 'http://127.0.0.1:6800', token: '', path: '/jsonrpc' }, { id: 'remote-linux', name: '远程Linux', endpoint: 'https://your-linux-server.example.com:443', token: '', path: '/jsonrpc' } ], downloadPaths: [ DEFAULT_DIR, '/Users/Administrator/Desktop', '/root/downloads' ] }; const BAD_NAME_TEXT = new Set([ '下载', '分享', '删除', '移动', '复制', '重命名', '更多', '选择', '全选', '文件名', '大小', '时间', '拖拽移动', '置顶', 'download', 'share', 'delete', 'move', 'copy', 'rename', 'more', 'select' ]); function clone(value) { return JSON.parse(JSON.stringify(value)); } function normalizeText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function normalizePath(value) { return String(value || '').trim(); } function stripSlashes(value) { return String(value || '').replace(/^\/+|\/+$/g, ''); } function parseOpenlistHost(value) { const raw = normalizePath(value); if (!raw) return null; const urlText = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`; try { const url = new URL(urlText); if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; return url.origin; } catch (err) { return null; } } function normalizeOpenlistHost(value) { return parseOpenlistHost(value) || DEFAULT_OPENLIST_HOST; } function normalizeOpenlistMountPath(value) { const raw = stripSlashes(normalizePath(value) || DEFAULT_OPENLIST_MOUNT_PATH).replace(/^d\//i, ''); return raw || DEFAULT_OPENLIST_MOUNT_PATH; } function safeDecode(value) { try { return decodeURIComponent(value); } catch (err) { return value; } } function buildOlistPrefix(settings) { const source = settings || DEFAULT_SETTINGS; const host = normalizeOpenlistHost(source.openlistHost); const mountPath = normalizeOpenlistMountPath(source.openlistMountPath); return `${host}/d/${encodePath(mountPath)}/`; } function parseStoredValue(raw, fallbackValue) { if (raw === undefined || raw === null || raw === '') return fallbackValue; if (typeof raw !== 'string') return raw; try { return JSON.parse(raw); } catch (err) { return raw; } } function readStorage(key, fallbackValue) { try { if (typeof GM_getValue === 'function') { const gmValue = GM_getValue(key); if (gmValue !== undefined && gmValue !== null && gmValue !== '') { return parseStoredValue(gmValue, fallbackValue); } } } catch (err) { console.warn('[115Aria] GM_getValue failed:', err); } try { const prefixedValue = localStorage.getItem(STORE_PREFIX + key); if (prefixedValue !== undefined && prefixedValue !== null && prefixedValue !== '') { return parseStoredValue(prefixedValue, fallbackValue); } return fallbackValue; } catch (err) { console.warn('[115Aria] localStorage read failed:', err); return fallbackValue; } } function writeStorage(key, value) { const data = JSON.stringify(value); try { if (typeof GM_setValue === 'function') { GM_setValue(key, data); return true; } } catch (err) { console.warn('[115Aria] GM_setValue failed:', err); } try { localStorage.setItem(STORE_PREFIX + key, data); return true; } catch (err) { console.warn('[115Aria] localStorage write failed:', err); return false; } } function makeId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; } function splitEndpoint(endpoint) { const raw = String(endpoint || '').trim(); if (!raw) return null; try { const url = new URL(raw); if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; return { domain: `${url.protocol}//${url.hostname}`, port: String(url.port || (url.protocol === 'https:' ? '443' : '80')), endpoint: `${url.protocol}//${url.hostname}:${url.port || (url.protocol === 'https:' ? '443' : '80')}` }; } catch (err) { return null; } } function sanitizeRpcConfig(input, index) { const source = input && typeof input === 'object' ? input : {}; const parsed = splitEndpoint(source.endpoint || `${source.domain || ''}${source.port ? `:${source.port}` : ''}`); const fallback = DEFAULT_SETTINGS.rpcConfigs[index] || DEFAULT_SETTINGS.rpcConfigs[0]; return { id: String(source.id || fallback.id || makeId('rpc')), name: String(source.name || fallback.name || `RPC-${index + 1}`).trim(), endpoint: parsed ? parsed.endpoint : fallback.endpoint, token: String(source.token || ''), path: normalizePath(source.path || fallback.path || '/jsonrpc') || '/jsonrpc' }; } function sanitizeSettings(rawInput) { const source = rawInput && typeof rawInput === 'object' ? rawInput : {}; let rpcConfigs = Array.isArray(source.rpcConfigs) ? source.rpcConfigs : DEFAULT_SETTINGS.rpcConfigs; rpcConfigs = rpcConfigs.map(sanitizeRpcConfig).filter((item) => splitEndpoint(item.endpoint)); if (rpcConfigs.length === 0) rpcConfigs = clone(DEFAULT_SETTINGS.rpcConfigs); let downloadPaths = Array.isArray(source.downloadPaths) ? source.downloadPaths : DEFAULT_SETTINGS.downloadPaths; downloadPaths = downloadPaths.map(normalizePath).filter(Boolean); downloadPaths = Array.from(new Set(downloadPaths)); if (downloadPaths.length === 0) downloadPaths = clone(DEFAULT_SETTINGS.downloadPaths); const activeRpcId = rpcConfigs.some((item) => item.id === source.activeRpcId) ? source.activeRpcId : rpcConfigs[0].id; const activePath = downloadPaths.includes(source.activePath) ? source.activePath : downloadPaths[0]; return { version: 1, openlistHost: normalizeOpenlistHost(source.openlistHost), openlistMountPath: normalizeOpenlistMountPath(source.openlistMountPath), activeRpcId, activePath, rpcConfigs, downloadPaths }; } function loadSettings() { const settings = sanitizeSettings(readStorage(SETTINGS_KEY)); saveSettings(settings); return settings; } function saveSettings(settings) { writeStorage(SETTINGS_KEY, settings); } function getActiveRpc(settings) { return settings.rpcConfigs.find((item) => item.id === settings.activeRpcId) || settings.rpcConfigs[0]; } function buildRpcUrl(rpc) { const endpoint = splitEndpoint(rpc.endpoint); if (!endpoint) throw new Error('RPC服务器未配置。'); const url = new URL(endpoint.endpoint); url.pathname = normalizePath(rpc.path || '/jsonrpc') || '/jsonrpc'; url.search = ''; url.hash = ''; return url.toString(); } function parseResponseJson(response) { if (response.response && typeof response.response === 'object') return response.response; const text = response.responseText || ''; return text ? JSON.parse(text) : null; } function gmRequestJson(options) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest不可用。')); return; } GM_xmlhttpRequest({ method: options.method || 'GET', url: options.url, headers: options.headers || {}, data: options.data, responseType: 'json', timeout: options.timeout || 30000, onload(response) { if (response.status >= 400) { reject(new Error(`HTTP ${response.status}: ${response.responseText || response.statusText}`)); return; } try { resolve(parseResponseJson(response)); } catch (err) { reject(err); } }, onerror() { reject(new Error('网络请求失败。')); }, ontimeout() { reject(new Error('网络请求超时。')); } }); }); } async function postJson(url, payload) { const data = JSON.stringify(payload); const headers = { 'Content-Type': 'application/json' }; if (typeof GM_xmlhttpRequest === 'function') { return gmRequestJson({ method: 'POST', url, headers, data }); } const response = await fetch(url, { method: 'POST', headers, body: data }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } async function getJson(url) { try { const response = await fetch(url, { credentials: 'include' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (err) { if (typeof GM_xmlhttpRequest !== 'function') throw err; return gmRequestJson({ method: 'GET', url }); } } async function sendToAria2(rpc, dir, url) { const params = []; const token = normalizePath(rpc.token); if (token) params.push(`token:${token}`); params.push([url]); params.push(dir ? { dir } : {}); const result = await postJson(buildRpcUrl(rpc), { jsonrpc: '2.0', id: `aria115-${Date.now()}-${Math.random().toString(16).slice(2)}`, method: 'aria2.addUri', params }); if (result && result.error) { const message = result.error.message || JSON.stringify(result.error); if (/Unauthorized/i.test(message)) { throw new Error('RPC认证失败,请在“设置”里填写正确的 aria2 token。'); } throw new Error(message); } return result ? result.result : null; } async function testAria2(rpc) { const params = []; const token = normalizePath(rpc.token); if (token) params.push(`token:${token}`); const result = await postJson(buildRpcUrl(rpc), { jsonrpc: '2.0', id: `aria115-test-${Date.now()}-${Math.random().toString(16).slice(2)}`, method: 'aria2.getVersion', params }); if (result && result.error) { const message = result.error.message || JSON.stringify(result.error); if (/Unauthorized/i.test(message)) { throw new Error('RPC认证失败,请检查 aria2 token。'); } throw new Error(message); } return result && result.result ? result.result.version || 'OK' : 'OK'; } function encodePath(path) { return stripSlashes(path) .split('/') .filter(Boolean) .map((part) => encodeURIComponent(part)) .join('/'); } function buildOlistUrl(filePath, source) { const prefix = buildOlistPrefix(source || DEFAULT_SETTINGS); return prefix + encodePath(filePath); } function getUrlParam(name) { const sources = [location.search, location.hash, location.href]; const matcher = new RegExp(`[?&#]${name}=([^&#]+)`); for (const source of sources) { const match = String(source || '').match(matcher); if (match) return safeDecode(match[1]); } return ''; } function getCurrentCid() { return getUrlParam('cid') || '0'; } function buildFilesApiUrl(cid, offset, limit) { const url = new URL('/api/proxy/115', location.origin); const params = new URLSearchParams({ domain: 'webapi', path: '/files', aid: '1', cid: cid || '0', offset: String(offset || 0), limit: String(limit || 1150), type: '0', show_dir: '1', fc_mix: '0', natsort: '1', count_folders: '1', record_open_time: '1', format: 'json', o: 'user_ptime', asc: '0' }); url.search = params.toString(); return url.toString(); } function getApiItems(payload) { if (!payload || typeof payload !== 'object') return []; if (Array.isArray(payload.data)) return payload.data; if (payload.data && Array.isArray(payload.data.list)) return payload.data.list; if (payload.data && Array.isArray(payload.data.data)) return payload.data.data; if (payload.data && Array.isArray(payload.data.files)) return payload.data.files; if (Array.isArray(payload.list)) return payload.list; if (Array.isArray(payload.files)) return payload.files; if (Array.isArray(payload.items)) return payload.items; return []; } function getApiItemName(item) { if (!item || typeof item !== 'object') return ''; return normalizeText(item.n || item.name || item.file_name || item.filename || item.title || ''); } function getApiItemCid(item) { if (!item || typeof item !== 'object') return ''; const value = item.cid || item.folder_id || item.category_id || item.id; return value === undefined || value === null ? '' : String(value); } function isApiFolder(item) { if (!item || typeof item !== 'object') return false; if (item.fid || item.file_id || item.pick_code || item.pickcode || item.pc) return false; if (String(item.is_dir || item.isdir || item.isFolder || item.fc || '') === '1') return true; if (String(item.type || '').toLowerCase() === 'dir') return true; if (String(item.type || '').toLowerCase() === 'folder') return true; if (item.cid || item.folder_id || item.category_id) return true; return false; } async function fetchFolderItems(cid) { const result = []; const limit = 1150; let offset = 0; while (true) { const payload = await getJson(buildFilesApiUrl(cid, offset, limit)); const items = getApiItems(payload); result.push(...items); if (items.length < limit) break; offset += items.length; if (offset > FOLDER_SCAN_LIMIT) break; } return result; } async function collectFilesFromFolder(folderCid, folderParts, output) { if (output.length >= FOLDER_SCAN_LIMIT) return; const items = await fetchFolderItems(folderCid); for (const item of items) { if (output.length >= FOLDER_SCAN_LIMIT) return; const name = getApiItemName(item); if (!name) continue; if (isApiFolder(item)) { const childCid = getApiItemCid(item); if (childCid) await collectFilesFromFolder(childCid, folderParts.concat(name), output); } else { output.push(folderParts.concat(name).map(stripSlashes).filter(Boolean).join('/')); } } } function escapeCssIdent(value) { if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value); return String(value).replace(/([^a-zA-Z0-9_-])/g, '\\$1'); } function exactClassSelector(className) { return className.split(/\s+/).filter(Boolean).map((item) => `.${escapeCssIdent(item)}`).join(''); } function cleanBreadcrumbPart(value) { return normalizeText(value) .replace(/^根目录\s*/, '') .replace(/^[/\\>›»]+|[/\\>›»]+$/g, '') .trim(); } function get115FolderParts() { const selector = `div${exactClassSelector(BREADCRUMB_CLASS)}`; const nodes = Array.from(document.querySelectorAll(selector)); const target = nodes.find((node) => Array.from(node.querySelectorAll('button')).some((button) => normalizeText(button.getAttribute('title') || button.textContent) === '根目录')) || nodes.find((node) => normalizeText(node.innerText || node.textContent).includes('根目录')) || nodes[0]; if (!target) return []; const buttonParts = Array.from(target.querySelectorAll('button')) .map((node) => cleanBreadcrumbPart(node.getAttribute('title') || node.innerText || node.textContent)) .filter((text) => text && text !== '根目录'); if (buttonParts.length > 0) return buttonParts; return normalizeText(target.innerText || target.textContent) .split(/[\n/>›»]+/) .map(cleanBreadcrumbPart) .filter((text) => text && text !== '根目录'); } function isUsefulName(value) { const name = normalizeText(value); if (!name || BAD_NAME_TEXT.has(name.toLowerCase())) return false; if (/^\d+(\.\d+)?\s*(B|KB|MB|GB|TB)$/i.test(name)) return false; if (/^\d{4}[-/]\d{1,2}[-/]\d{1,2}/.test(name)) return false; return true; } function readAttrDeep(element, names) { for (const name of names) { const value = element.getAttribute && element.getAttribute(name); if (value && isUsefulName(value)) return normalizeText(value); } const selector = names.map((name) => `[${name}]`).join(','); const child = selector ? element.querySelector(selector) : null; if (!child) return ''; for (const name of names) { const value = child.getAttribute(name); if (value && isUsefulName(value)) return normalizeText(value); } return ''; } function readNameFromElement(element) { const attrName = readAttrDeep(element, ['data-name', 'data-filename', 'data-file-name', 'file_name', 'filename']); if (attrName) return attrName; const nameNode = element.querySelector('.file-name-responsive[title], [class*="file-name"][title], [class*="filename"][title], [class*="name"][title], [class*="Name"][title]'); if (nameNode) { const value = normalizeText(nameNode.getAttribute('title') || nameNode.innerText || nameNode.textContent); if (isUsefulName(value)) return value; } const imgNode = element.querySelector('img[title], img[alt]'); if (imgNode) { const value = normalizeText(imgNode.getAttribute('title') || imgNode.getAttribute('alt')); if (isUsefulName(value)) return value; } const titleName = readAttrDeep(element, ['title', 'aria-label']); if (titleName) return titleName; return ''; } function isFolderElement(element) { const text = normalizeText(element.innerText || element.textContent); if (text.includes('文件夹')) return true; const icon = element.querySelector('img[src*="folder"], img[src*="dir"], img[alt*="文件夹"], img[title*="文件夹"], i[class*="folder"], i[class*="Folder"], i[class*="dir"], i[class*="Dir"]'); return Boolean(icon); } function hasFileLikeExtension(name) { const cleanName = stripSlashes(name).split('/').pop() || ''; return /\.[a-z0-9]{1,10}$/i.test(cleanName); } function normalizeSelectedElement(element) { return element.closest('[data-fid], [data-file-id], [data-pickcode], [data-pick-code], [data-id], tr, li, [role="row"], [class*="file"], [class*="File"], [class*="row"], [class*="Row"], [class*="item"], [class*="Item"]') || element; } function getSelectedEntriesFromDom() { const selected = new Set(); const selectors = [ 'tr.selected', 'tr[aria-selected="true"]', 'li.selected', 'li[aria-selected="true"]', '[role="row"].selected', '[role="row"][aria-selected="true"]', '.file-item.selected', '.file-item[aria-selected="true"]', '.list-item.selected', '.list-item[aria-selected="true"]', '[data-fid].selected', '[data-file-id].selected', '[data-pickcode].selected', '[data-pick-code].selected', '.file-list-item.bg-blue-100', '.file-list-item .bg-blue-100', '[class*="selected"][data-fid]', '[class*="selected"][data-file-id]', '[class*="selected"][data-id]' ]; selectors.forEach((selector) => { document.querySelectorAll(selector).forEach((item) => selected.add(normalizeSelectedElement(item))); }); document.querySelectorAll('input[type="checkbox"]:checked').forEach((item) => { selected.add(normalizeSelectedElement(item)); }); return Array.from(selected) .map((element) => ({ element, name: readNameFromElement(element), isFolder: isFolderElement(element) })) .filter((item) => isUsefulName(item.name)); } function getSelectedFileNamesFromDom() { return getSelectedEntriesFromDom().map((item) => item.name); } function buildRelativePath(folderParts, fileName) { const name = stripSlashes(fileName); if (!name) return ''; if (name.includes('/')) return name; return folderParts.concat(name).map(stripSlashes).filter(Boolean).join('/'); } function dedupe(list) { return Array.from(new Set(list.filter(Boolean))); } function promptManualPaths(folderParts) { const folderPath = folderParts.join('/') || '/'; const input = window.prompt(`未能识别115选中文件。\n当前目录: ${folderPath}\n请输入115文件路径或当前目录下文件名,每行一个:`, ''); if (!input) return []; return input .split(/\r?\n/) .map(normalizeText) .filter(Boolean) .map((line) => buildRelativePath(folderParts, line)); } function getCurrentFolderPath() { return get115FolderParts().join('/') || '根目录'; } function collectDetectedFilePaths() { const folderParts = get115FolderParts(); const fileNames = getSelectedEntriesFromDom() .filter((item) => !item.isFolder) .map((item) => item.name); return dedupe(fileNames.map((name) => buildRelativePath(folderParts, name))); } function matchApiItemByName(name, items, preferredFolder) { const targetName = normalizeText(name); const sameName = items.filter((item) => normalizeText(getApiItemName(item)) === targetName); if (sameName.length === 0) return null; if (preferredFolder) return sameName.find(isApiFolder) || sameName[0]; return sameName.find((item) => !isApiFolder(item)) || sameName[0]; } async function collectSelectedFilePaths() { const folderParts = get115FolderParts(); const selected = getSelectedEntriesFromDom(); if (selected.length === 0) { return dedupe(promptManualPaths(folderParts)); } let currentItems = []; let apiError = null; try { currentItems = await fetchFolderItems(getCurrentCid()); } catch (err) { apiError = err; } const output = []; for (const selectedItem of selected) { const matched = currentItems.length > 0 ? matchApiItemByName(selectedItem.name, currentItems, selectedItem.isFolder) : null; if (matched && isApiFolder(matched)) { const folderCid = getApiItemCid(matched); if (!folderCid) throw new Error(`文件夹 ${selectedItem.name} 未取到 cid,无法递归。`); await collectFilesFromFolder(folderCid, folderParts.concat(selectedItem.name), output); continue; } if ((selectedItem.isFolder || !hasFileLikeExtension(selectedItem.name)) && !matched) { throw new Error(`文件夹 ${selectedItem.name} 未能从115目录接口匹配到 cid,无法递归。${apiError ? ` ${apiError.message || apiError}` : ''}`); } output.push(buildRelativePath(folderParts, selectedItem.name)); } return dedupe(output); } function ensureStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` #${CONTAINER_ID} { position: fixed; top: 76px; right: 22px; z-index: 999998; display: flex; flex-direction: column; align-items: center; gap: 8px; width: min(760px, calc(100vw - 44px)); padding: 10px; border-radius: 18px; background: linear-gradient(135deg, rgba(250, 253, 255, 0.96), rgba(236, 245, 255, 0.92)); border: 1px solid rgba(39, 119, 248, 0.22); box-shadow: 0 18px 44px rgba(15, 23, 42, 0.18); backdrop-filter: blur(14px); color: #172033; font-size: 13px; } .aria115-top { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .aria115-brand { display: flex; align-items: center; gap: 10px; min-width: 180px; font-weight: 800; color: #1d4ed8; } .aria115-logo { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 10px; color: #fff; background: linear-gradient(135deg, #2777f8, #74b8ff); box-shadow: 0 8px 18px rgba(39, 119, 248, 0.28); font-weight: 900; } .aria115-status { flex: 1; min-width: 0; color: #50627c; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; } .aria115-controls { width: 100%; display: grid; grid-template-columns: auto minmax(120px, 1fr) minmax(180px, 1.4fr) auto auto auto; gap: 8px; align-items: center; } .aria115-select { height: 34px; min-width: 0; border: 1px solid #c7d6e8; border-radius: 10px; padding: 0 10px; background: #fff; color: #172033; outline: none; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); } .aria115-select:focus { border-color: #2777f8; box-shadow: 0 0 0 3px rgba(39, 119, 248, 0.12); } .aria115-btn { height: 34px; border: 1px solid #c7d6e8; border-radius: 10px; padding: 0 12px; background: rgba(255, 255, 255, 0.94); color: #24415f; cursor: pointer; font-size: 13px; transition: all .16s ease; white-space: nowrap; } .aria115-btn:hover { border-color: #2777f8; color: #0d67e6; transform: translateY(-1px); } .aria115-primary { background: linear-gradient(135deg, #2777f8, #0d67e6); border-color: #2777f8; color: #fff; box-shadow: 0 8px 18px rgba(39, 119, 248, 0.24); } .aria115-primary:hover { border-color: #0d67e6; color: #fff; } .aria115-primary:disabled { cursor: wait; opacity: 0.72; } #${MODAL_ID} { position: fixed; inset: 0; z-index: 999999; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(15, 23, 42, 0.45); } .aria115-panel { width: 980px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; background: #fff; border-radius: 18px; border: 1px solid #d4e2f4; box-shadow: 0 20px 40px rgba(15, 23, 42, 0.18); color: #213547; overflow: hidden; } .aria115-head, .aria115-footer { padding: 16px 18px; background: #f9fbff; } .aria115-head { border-bottom: 1px solid #e6edf6; } .aria115-head h3 { margin: 0; font-size: 18px; color: #17324d; } .aria115-head p { margin: 6px 0 0; color: #6b7f96; font-size: 12px; } .aria115-body { padding: 16px 18px; display: grid; gap: 14px; overflow: auto; } .aria115-section { border: 1px solid #e3edf8; border-radius: 14px; background: #fbfdff; padding: 12px; } .aria115-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; } .aria115-label { font-weight: 800; font-size: 14px; color: #27486b; } .aria115-tip { margin-top: 4px; color: #6b7280; font-size: 12px; } .aria115-input { width: 100%; height: 34px; box-sizing: border-box; border: 1px solid #c7d6e8; border-radius: 10px; padding: 0 10px; outline: none; background: #fff; color: #1f2937; font-size: 13px; } .aria115-input:focus { border-color: #2777f8; box-shadow: 0 0 0 3px rgba(39, 119, 248, 0.12); } .aria115-rpc-row, .aria115-path-row { display: grid; gap: 8px; margin-top: 8px; padding: 8px; border: 1px solid #e6edf6; border-radius: 12px; background: #fff; } .aria115-olist-row { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 10px; } .aria115-rpc-row { grid-template-columns: 120px minmax(210px, 1.4fr) minmax(120px, 1fr) 100px auto; } .aria115-path-row { grid-template-columns: 1fr auto; } .aria115-field-title { margin: 0 0 5px; color: #64748b; font-size: 11px; } .aria115-error { display: none; padding: 10px; border-radius: 8px; background: #fff4f2; border: 1px solid #ffb3ab; color: #b42318; font-size: 12px; } .aria115-footer { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid #e6edf6; } @media (max-width: 860px) { #${CONTAINER_ID} { left: 8px; right: 8px; top: 64px; width: auto; } .aria115-top { align-items: flex-start; flex-direction: column; } .aria115-status { width: 100%; text-align: left; } .aria115-controls { grid-template-columns: 1fr 1fr; } .aria115-olist-row, .aria115-rpc-row, .aria115-path-row { grid-template-columns: 1fr; } } `; document.head.appendChild(style); } function createButton(text, className) { const button = document.createElement('button'); button.type = 'button'; button.className = `aria115-btn ${className || ''}`.trim(); button.textContent = text; return button; } function createInput(value, placeholder, type) { const input = document.createElement('input'); input.className = 'aria115-input'; input.type = type || 'text'; input.value = value || ''; input.placeholder = placeholder || ''; if (input.type === 'password') input.autocomplete = 'new-password'; return input; } function createField(title, input) { const wrap = document.createElement('label'); const label = document.createElement('div'); label.className = 'aria115-field-title'; label.textContent = title; wrap.appendChild(label); wrap.appendChild(input); return wrap; } function renderSelectOptions(select, values, selectedValue, getText) { select.innerHTML = ''; values.forEach((item) => { const option = document.createElement('option'); option.value = typeof item === 'string' ? item : item.id; option.text = getText ? getText(item) : option.value; select.appendChild(option); }); select.value = selectedValue; } function openSettingsModal(settings, onSave) { const old = document.getElementById(MODAL_ID); if (old) old.remove(); let draft = clone(settings); const overlay = document.createElement('div'); overlay.id = MODAL_ID; const panel = document.createElement('div'); panel.className = 'aria115-panel'; const head = document.createElement('div'); head.className = 'aria115-head'; head.innerHTML = '

115Aria 设置

'; const body = document.createElement('div'); body.className = 'aria115-body'; const olistSection = document.createElement('div'); olistSection.className = 'aria115-section'; const olistHead = document.createElement('div'); olistHead.className = 'aria115-section-head'; const olistLabel = document.createElement('div'); olistLabel.className = 'aria115-label'; olistLabel.textContent = 'OpenList 直链配置'; olistHead.appendChild(olistLabel); const olistRow = document.createElement('div'); olistRow.className = 'aria115-olist-row'; const openlistHostInput = createInput(draft.openlistHost || DEFAULT_OPENLIST_HOST, ''); const openlistMountInput = createInput(draft.openlistMountPath || DEFAULT_OPENLIST_MOUNT_PATH, '例如 115'); const olistTip = document.createElement('div'); olistTip.className = 'aria115-tip'; olistTip.textContent = '需openlist关闭签名。填写openlist主机,填写挂载路径 如:媒体/115。'; olistRow.appendChild(createField('OpenList 主机', openlistHostInput)); olistRow.appendChild(createField('115 挂载路径', openlistMountInput)); olistSection.appendChild(olistHead); olistSection.appendChild(olistRow); olistSection.appendChild(olistTip); const rpcSection = document.createElement('div'); rpcSection.className = 'aria115-section'; const rpcHead = document.createElement('div'); rpcHead.className = 'aria115-section-head'; const rpcLabel = document.createElement('div'); rpcLabel.className = 'aria115-label'; rpcLabel.textContent = 'RPC 服务器'; const rpcAdd = createButton('+ 新增RPC'); const rpcWrap = document.createElement('div'); rpcHead.appendChild(rpcLabel); rpcHead.appendChild(rpcAdd); rpcSection.appendChild(rpcHead); rpcSection.appendChild(rpcWrap); const pathSection = document.createElement('div'); pathSection.className = 'aria115-section'; const pathHead = document.createElement('div'); pathHead.className = 'aria115-section-head'; const pathLabel = document.createElement('div'); pathLabel.className = 'aria115-label'; pathLabel.textContent = '下载路径'; const pathAdd = createButton('+ 新增路径'); const pathWrap = document.createElement('div'); pathHead.appendChild(pathLabel); pathHead.appendChild(pathAdd); pathSection.appendChild(pathHead); pathSection.appendChild(pathWrap); const errorBox = document.createElement('div'); errorBox.className = 'aria115-error'; const footer = document.createElement('div'); footer.className = 'aria115-footer'; const reset = createButton('恢复默认'); const cancel = createButton('取消'); const save = createButton('保存', 'aria115-primary'); body.appendChild(olistSection); body.appendChild(rpcSection); body.appendChild(pathSection); body.appendChild(errorBox); footer.appendChild(reset); footer.appendChild(cancel); footer.appendChild(save); panel.appendChild(head); panel.appendChild(body); panel.appendChild(footer); overlay.appendChild(panel); document.body.appendChild(overlay); function showError(message) { errorBox.textContent = message; errorBox.style.display = 'block'; } function renderRpcRows() { rpcWrap.innerHTML = ''; draft.rpcConfigs.forEach((rpc, index) => { const row = document.createElement('div'); row.className = 'aria115-rpc-row'; const name = createInput(rpc.name, '名称'); const endpoint = createInput(rpc.endpoint, 'http://127.0.0.1:6800'); const token = createInput('', rpc.token ? '已保存,留空不修改' : 'Token 可留空', 'password'); const rpcPath = createInput(rpc.path || '/jsonrpc', '/jsonrpc'); const del = createButton('删除'); name.oninput = () => { draft.rpcConfigs[index].name = name.value; }; endpoint.oninput = () => { draft.rpcConfigs[index].endpoint = endpoint.value; }; token.oninput = () => { draft.rpcConfigs[index]._tokenEdited = true; draft.rpcConfigs[index]._tokenValue = token.value; }; rpcPath.oninput = () => { draft.rpcConfigs[index].path = rpcPath.value; }; del.onclick = () => { const removed = draft.rpcConfigs[index]; draft.rpcConfigs.splice(index, 1); if (removed && removed.id === draft.activeRpcId) { draft.activeRpcId = draft.rpcConfigs[0] ? draft.rpcConfigs[0].id : ''; } renderRpcRows(); }; row.appendChild(createField('名称', name)); row.appendChild(createField('地址', endpoint)); row.appendChild(createField('Token', token)); row.appendChild(createField('路径', rpcPath)); row.appendChild(del); rpcWrap.appendChild(row); }); } function renderPathRows() { pathWrap.innerHTML = ''; draft.downloadPaths.forEach((path, index) => { const row = document.createElement('div'); row.className = 'aria115-path-row'; const input = createInput(path, '/Users/Administrator/Downloads'); const del = createButton('删除'); input.oninput = () => { draft.downloadPaths[index] = input.value; }; del.onclick = () => { draft.downloadPaths.splice(index, 1); if (!draft.downloadPaths.includes(draft.activePath)) { draft.activePath = draft.downloadPaths[0] || ''; } renderPathRows(); }; row.appendChild(input); row.appendChild(del); pathWrap.appendChild(row); }); } reset.onclick = () => { draft = clone(DEFAULT_SETTINGS); openlistHostInput.value = draft.openlistHost; openlistMountInput.value = draft.openlistMountPath; errorBox.style.display = 'none'; renderRpcRows(); renderPathRows(); }; cancel.onclick = () => overlay.remove(); overlay.addEventListener('click', (event) => { if (event.target === overlay) overlay.remove(); }); save.onclick = () => { const errors = []; draft.openlistHost = normalizeOpenlistHost(openlistHostInput.value); draft.openlistMountPath = normalizeOpenlistMountPath(openlistMountInput.value); draft.rpcConfigs = draft.rpcConfigs.map((rpc, index) => ({ id: rpc.id || makeId(`rpc${index}`), name: normalizeText(rpc.name) || `RPC-${index + 1}`, endpoint: normalizePath(rpc.endpoint), token: rpc._tokenEdited ? String(rpc._tokenValue || '') : String(rpc.token || ''), path: normalizePath(rpc.path || '/jsonrpc') || '/jsonrpc' })); draft.downloadPaths = Array.from(new Set(draft.downloadPaths.map(normalizePath).filter(Boolean))); if (!parseOpenlistHost(openlistHostInput.value)) { errors.push('OpenList 主机必须是 http/https 地址。'); } if (!normalizePath(openlistMountInput.value)) { errors.push('115 挂载路径不能为空。'); } if (draft.rpcConfigs.length === 0) { errors.push('至少需要 1 个 RPC 配置。'); } draft.rpcConfigs.forEach((rpc, index) => { const parsed = splitEndpoint(rpc.endpoint); if (!parsed) errors.push(`RPC #${index + 1} 地址格式错误。`); else rpc.endpoint = parsed.endpoint; if (!rpc.path.startsWith('/')) errors.push(`RPC #${index + 1} JSON-RPC 路径需以 / 开头。`); }); if (draft.downloadPaths.length === 0) { errors.push('至少需要 1 个下载路径。'); } if (errors.length > 0) { showError(errors.join(' ')); return; } if (!draft.rpcConfigs.some((rpc) => rpc.id === draft.activeRpcId)) { draft.activeRpcId = draft.rpcConfigs[0].id; } if (!draft.downloadPaths.includes(draft.activePath)) { draft.activePath = draft.downloadPaths[0]; } onSave(sanitizeSettings(draft)); overlay.remove(); }; rpcAdd.onclick = () => { draft.rpcConfigs.push({ id: makeId('rpc'), name: '新RPC', endpoint: 'http://127.0.0.1:6800', token: '', path: '/jsonrpc' }); renderRpcRows(); }; pathAdd.onclick = () => { draft.downloadPaths.push(DEFAULT_DIR); renderPathRows(); }; renderRpcRows(); renderPathRows(); } function mountUI() { if (!document.body) { window.setTimeout(mountUI, 200); return; } ensureStyle(); let settings = loadSettings(); let summaryTimer = 0; let container = document.getElementById(CONTAINER_ID); if (container) container.remove(); container = document.createElement('div'); container.id = CONTAINER_ID; const top = document.createElement('div'); top.className = 'aria115-top'; const brand = document.createElement('div'); brand.className = 'aria115-brand'; brand.innerHTML = '115Aria'; const status = document.createElement('div'); status.className = 'aria115-status'; const controls = document.createElement('div'); controls.className = 'aria115-controls'; const settingsButton = createButton('设置'); const rpcSelect = document.createElement('select'); rpcSelect.className = 'aria115-select'; const pathSelect = document.createElement('select'); pathSelect.className = 'aria115-select'; const previewButton = createButton('预览'); const testButton = createButton('测试RPC'); const sendButton = createButton('发送RPC', 'aria115-primary'); function updateSummary() { const folderPath = getCurrentFolderPath(); const selectedCount = getSelectedEntriesFromDom().length; const prefix = buildOlistPrefix(settings); status.textContent = `目录: ${folderPath} · 已选 ${selectedCount} 项 · ${prefix}`; status.title = status.textContent; } function refresh() { renderSelectOptions(rpcSelect, settings.rpcConfigs, settings.activeRpcId, (item) => item.name || item.endpoint); renderSelectOptions(pathSelect, settings.downloadPaths, settings.activePath); saveSettings(settings); updateSummary(); } settingsButton.onclick = () => { openSettingsModal(settings, (next) => { settings = next; refresh(); }); }; rpcSelect.onchange = () => { settings.activeRpcId = rpcSelect.value; saveSettings(settings); updateSummary(); }; pathSelect.onchange = () => { settings.activePath = pathSelect.value; saveSettings(settings); updateSummary(); }; previewButton.onclick = async () => { if (previewButton.disabled) return; previewButton.disabled = true; previewButton.textContent = '扫描中...'; try { const filePaths = await collectSelectedFilePaths(); if (filePaths.length === 0) { window.alert('[115Aria] 当前没有识别到可发送文件。'); return; } const urls = filePaths.map((filePath) => buildOlistUrl(filePath, settings)); window.alert(urls.slice(0, 20).join('\n') + (urls.length > 20 ? `\n... 还有 ${urls.length - 20} 个` : '')); } catch (err) { window.alert(`[115Aria] ${err.message || err}`); } finally { previewButton.disabled = false; previewButton.textContent = '预览'; updateSummary(); } }; testButton.onclick = async () => { if (testButton.disabled) return; testButton.disabled = true; testButton.textContent = '测试中...'; try { const version = await testAria2(getActiveRpc(settings)); window.alert(`RPC连接正常,aria2 ${version}`); } catch (err) { window.alert(`[115Aria] ${err.message || err}`); } finally { testButton.disabled = false; testButton.textContent = '测试RPC'; } }; sendButton.onclick = async () => { if (sendButton.disabled) return; sendButton.disabled = true; sendButton.textContent = '识别中...'; try { const filePaths = await collectSelectedFilePaths(); if (filePaths.length === 0) throw new Error('没有可发送的115文件路径。'); const rpc = getActiveRpc(settings); const errors = []; let success = 0; for (let index = 0; index < filePaths.length; index += 1) { const filePath = filePaths[index]; sendButton.textContent = `发送 ${index + 1}/${filePaths.length}`; try { await sendToAria2(rpc, settings.activePath, buildOlistUrl(filePath, settings)); success += 1; } catch (err) { errors.push(`${filePath}: ${err.message || err}`); } } if (errors.length > 0) { window.alert(`已发送 ${success}/${filePaths.length} 个任务,失败 ${errors.length} 个:\n${errors.slice(0, 5).join('\n')}`); } else { sendButton.textContent = `已发送${success}个`; window.setTimeout(() => { sendButton.textContent = '发送RPC'; }, 1600); } updateSummary(); } catch (err) { window.alert(`[115Aria] ${err.message || err}`); } finally { sendButton.disabled = false; if (!/^已发送/.test(sendButton.textContent)) sendButton.textContent = '发送RPC'; } }; top.appendChild(brand); top.appendChild(status); controls.appendChild(settingsButton); controls.appendChild(rpcSelect); controls.appendChild(pathSelect); controls.appendChild(previewButton); controls.appendChild(testButton); controls.appendChild(sendButton); container.appendChild(top); container.appendChild(controls); document.body.appendChild(container); refresh(); summaryTimer = window.setInterval(() => { if (!document.body.contains(container)) { window.clearInterval(summaryTimer); return; } updateSummary(); }, 1200); } unsafeWindow.Aria115 = { buildOlistUrl, collectSelectedFilePaths, get115FolderParts, loadSettings, saveSettings, sendToAria2 }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', mountUI, { once: true }); } else { mountUI(); } })();