您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
批量下载 Iwara 视频
当前为
// ==UserScript== // @name Iwara Download Tool // @description Download videos from iwara.tv // @name:ja Iwara バッチダウンローダー // @description:ja Iwara 動画バッチをダウンロード // @name:zh-CN Iwara 批量下载工具 // @description:zh-CN 批量下载 Iwara 视频 // @icon https://i.harem-battle.club/images/2023/03/21/wMQ.png // @namespace https://github.com/dawn-lc/user.js // @version 3.0.380 // @author dawn-lc // @license Apache-2.0 // @copyright 2023, Dawnlc (https://dawnlc.me/) // @source https://github.com/dawn-lc/user.js // @supportURL https://github.com/dawn-lc/user.js/issues // @connect iwara.tv // @connect www.iwara.tv // @connect api.iwara.tv // @connect cdn.staticfile.org // @connect 127.* // @connect localhost // @connect 10.* // @connect 172.16.* // @connect 192.168.* // @connect * // @match *://*.iwara.tv/* // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM_cookie // @grant GM_info // @grant unsafeWindow // @run-at document-start // @require https://cdn.staticfile.org/toastify-js/1.12.0/toastify.min.js // ==/UserScript== (async function () { if (GM_getValue('isDebug')) { debugger; } const originalAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (type, listener, options) { originalAddEventListener.call(this, type, listener, options); }; Object.prototype.getObjectString = function () { return String(this).includes('[object') || String(this).includes('[native code]') ? JSON.stringify(this) : String(this); }; String.prototype.isEmpty = function () { return this.trim().length == 0; }; String.prototype.truncate = function (maxLength) { return this.length > maxLength ? this.substring(0, maxLength) : this.toString(); }; String.prototype.replaceVariable = function (replacements) { return Object.entries(replacements).reduce((str, [key, value]) => str.split(`%#${key}#%`).join(String(value)), this); }; String.prototype.replaceNowTime = function () { return this.replaceVariable({ Y: new Date().getFullYear(), M: new Date().getMonth() + 1, D: new Date().getDate(), h: new Date().getHours(), m: new Date().getMinutes(), s: new Date().getSeconds() }); }; String.prototype.replaceUploadTime = function (time) { return this.replaceVariable({ UploadYear: time.getFullYear(), UploadMonth: time.getMonth() + 1, UploadDate: time.getDate(), UploadHours: time.getHours(), UploadMinutes: time.getMinutes(), UploadSeconds: time.getSeconds() }); }; String.prototype.toURL = function () { return new URL(this.toString()); }; Array.prototype.append = function (arr) { this.push(...arr); }; Array.prototype.any = function () { return this.length > 0; }; let DownloadType; (function (DownloadType) { DownloadType[DownloadType["Aria2"] = 0] = "Aria2"; DownloadType[DownloadType["IwaraDownloader"] = 1] = "IwaraDownloader"; DownloadType[DownloadType["Browser"] = 2] = "Browser"; DownloadType[DownloadType["Others"] = 3] = "Others"; })(DownloadType || (DownloadType = {})); let ToastType; (function (ToastType) { ToastType[ToastType["Log"] = 0] = "Log"; ToastType[ToastType["Info"] = 1] = "Info"; ToastType[ToastType["Warn"] = 2] = "Warn"; ToastType[ToastType["Error"] = 3] = "Error"; })(ToastType || (ToastType = {})); class Dictionary { items; constructor(data = []) { this.items = {}; data.map(i => this.set(i.key, i.value)); } set(key, value) { this.items[key] = value; } get(key) { return this.has(key) ? this.items[key] : undefined; } has(key) { return this.items.hasOwnProperty(key); } remove(key) { if (this.has(key)) { delete this.items[key]; return true; } return false; } get size() { return Object.keys(this.items).length; } keys() { return Object.keys(this.items); } values() { return Object.values(this.items); } clear() { this.items = {}; } forEach(callback) { for (let key in this.items) { if (this.has(key)) { callback(key, this.items[key]); } } } } class Config { cookies; checkDownloadLink; downloadType; downloadPath; downloadProxy; aria2Path; aria2Token; iwaraDownloaderPath; iwaraDownloaderToken; authorization; priority; constructor() { //初始化 this.checkDownloadLink = GM_getValue('checkDownloadLink', true); this.downloadType = GM_getValue('downloadType', DownloadType.Browser); this.downloadPath = GM_getValue('downloadPath', '/Iwara/%#AUTHOR#%/%#TITLE#%[%#ID#%].mp4'); this.downloadProxy = GM_getValue('downloadProxy', ''); this.aria2Path = GM_getValue('aria2Path', 'http://127.0.0.1:6800/jsonrpc'); this.aria2Token = GM_getValue('aria2Token', ''); this.iwaraDownloaderPath = GM_getValue('iwaraDownloaderPath', 'http://127.0.0.1:6800/jsonrpc'); this.iwaraDownloaderToken = GM_getValue('iwaraDownloaderToken', ''); this.priority = GM_getValue('priority', { 'Source': 100, '540': 2, '360': 1 }); //代理本页面的更改 let body = new Proxy(this, { get: function (target, property) { GM_getValue('isDebug') && console.log(`get ${property.toString()}`); return target[property]; }, set: function (target, property, value) { if (target[property] !== value && GM_getValue('isFirstRun', true) !== true) { let setr = Reflect.set(target, property, value); GM_getValue('isDebug') && console.log(`set ${property.toString()} ${value} ${setr}`); GM_getValue(property.toString()) !== value && GM_setValue(property.toString(), value); target.configChange(property.toString()); return setr; } else { return true; } } }); //同步其他页面脚本的更改 GM_listValues().forEach((value) => { GM_addValueChangeListener(value, (name, old_value, new_value, remote) => { if (remote && body[name] !== new_value && old_value !== new_value && !GM_getValue('isFirstRun', true)) { body[name] = new_value; } }); }); GM_cookie('list', { domain: 'iwara.tv', httpOnly: true }, (list, error) => { if (error) { console.log(error); body.cookies = []; } else { body.cookies = list; } }); return body; } downloadTypeItem(type) { return { nodeType: 'label', className: 'inputRadio', childs: [ DownloadType[type], { nodeType: 'input', attributes: Object.assign({ name: 'DownloadType', type: 'radio', value: type }, config.downloadType == type ? { checked: true } : {}), events: { change: () => { config.downloadType = type; } } } ] }; } configChange(item) { switch (item) { case 'downloadType': let page = document.querySelector('#pluginConfigPage'); while (page.hasChildNodes()) { page.removeChild(page.firstChild); } let downloadConfigInput = [ renderNode({ nodeType: 'label', childs: [ '下载到:', { nodeType: 'input', attributes: Object.assign({ name: 'DownloadPath', type: 'Text', value: config.downloadPath }), events: { change: (event) => { config.downloadPath = event.target.value; } } } ] }), renderNode({ nodeType: 'label', childs: [ '下载代理:', { nodeType: 'input', attributes: Object.assign({ name: 'DownloadProxy', type: 'Text', value: config.downloadProxy }), events: { change: (event) => { config.downloadProxy = event.target.value; } } } ] }) ]; let aria2ConfigInput = [ renderNode({ nodeType: 'label', childs: [ 'Aria2 RPC:', { nodeType: 'input', attributes: Object.assign({ name: 'Aria2Path', type: 'Text', value: config.aria2Path }), events: { change: (event) => { config.aria2Path = event.target.value; } } } ] }), renderNode({ nodeType: 'label', childs: [ 'Aria2 Token:', { nodeType: 'input', attributes: Object.assign({ name: 'Aria2Token', type: 'Text', value: config.aria2Token }), events: { change: (event) => { config.aria2Token = event.target.value; } } } ] }) ]; let iwaraDownloaderConfigInput = [ renderNode({ nodeType: 'label', childs: [ 'IwaraDownloader RPC:', { nodeType: 'input', attributes: Object.assign({ name: 'IwaraDownloaderPath', type: 'Text', value: config.iwaraDownloaderPath }), events: { change: (event) => { config.iwaraDownloaderPath = event.target.value; } } } ] }), renderNode({ nodeType: 'label', childs: [ 'IwaraDownloader Token:', { nodeType: 'input', attributes: Object.assign({ name: 'IwaraDownloaderToken', type: 'Text', value: config.iwaraDownloaderToken }), events: { change: (event) => { config.downloadProxy = event.target.value; } } } ] }) ]; let BrowserConfigInput = [ renderNode({ nodeType: 'label', childs: [ '重命名:', { nodeType: 'input', attributes: Object.assign({ name: 'DownloadPath', type: 'Text', value: config.downloadPath }), events: { change: (event) => { config.downloadPath = event.target.value; } } } ] }) ]; switch (config.downloadType) { case DownloadType.Aria2: downloadConfigInput.map(i => page.appendChild(i)); aria2ConfigInput.map(i => page.appendChild(i)); break; case DownloadType.IwaraDownloader: downloadConfigInput.map(i => page.appendChild(i)); iwaraDownloaderConfigInput.map(i => page.appendChild(i)); break; case DownloadType.Browser: BrowserConfigInput.map(i => page.appendChild(i)); break; default: break; } break; default: break; } } edit() { if (!document.querySelector('#pluginConfig')) { let editor = renderNode({ nodeType: 'div', attributes: { id: 'pluginConfig' }, childs: [ { nodeType: 'div', className: 'main', childs: [ { nodeType: 'h2', childs: 'Iwara 批量下载工具' }, { nodeType: 'p', className: 'inputRadioLine', childs: [ '下载方式:', ...Object.keys(DownloadType).map(i => !Object.is(Number(i), NaN) ? this.downloadTypeItem(Number(i)) : undefined).filter(Boolean) ] }, { nodeType: 'p', className: 'inputRadioLine', childs: [ '画质检查:', { nodeType: 'label', className: 'inputRadio', childs: [ "开启", { nodeType: 'input', attributes: Object.assign({ name: 'CheckDownloadLink', type: 'radio' }, config.checkDownloadLink ? { checked: true } : {}), events: { change: () => { config.checkDownloadLink = true; } } } ] }, { nodeType: 'label', className: 'inputRadio', childs: [ "关闭", { nodeType: 'input', attributes: Object.assign({ name: 'CheckDownloadLink', type: 'radio' }, config.checkDownloadLink ? {} : { checked: true }), events: { change: () => { config.checkDownloadLink = false; } } } ] } ] }, { nodeType: 'p', attributes: { id: 'pluginConfigPage' } } ] }, { nodeType: 'button', className: 'closeButton', childs: '保存', events: { click: async () => { switch (config.downloadType) { case DownloadType.Aria2: (await aria2Check()) && editor.remove(); break; case DownloadType.IwaraDownloader: (await iwaraDownloaderCheck()) && editor.remove(); break; case DownloadType.Browser: (await EnvCheck()) && editor.remove(); break; default: editor.remove(); break; } } } } ] }); document.body.appendChild(editor); this.configChange('downloadType'); } } } class VideoInfo { ID; UploadTime; Name; FileName; Size; Tags; Alias; Author; Private; VideoInfoSource; VideoFileSource; External; State; Comments; getDownloadQuality; getDownloadUrl; constructor(Name) { this.Name = Name; return this; } async init(ID) { try { config.authorization = `Bearer ${await refreshToken()}`; this.ID = ID.toLocaleLowerCase(); this.VideoInfoSource = JSON.parse(await get(`https://api.iwara.tv/video/${this.ID}`.toURL(), window.location.href, await getAuth())); if (this.VideoInfoSource.id === undefined) { throw new Error('获取视频信息失败'); } this.Name = ((this.VideoInfoSource.title ?? this.Name).replace(/^\.|[\\\\/:*?\"<>|.]/img, '_')).truncate(100); this.External = notNull(this.VideoInfoSource.embedUrl) && !this.VideoInfoSource.embedUrl.isEmpty(); if (this.External) { throw new Error(`非本站视频 ${this.VideoInfoSource.embedUrl}`); } this.Private = this.VideoInfoSource.private; this.Alias = this.VideoInfoSource.user.name.replace(/^\.|[\\\\/:*?\"<>|.]/img, '_'); this.Author = this.VideoInfoSource.user.username.replace(/^\.|[\\\\/:*?\"<>|.]/img, '_'); this.UploadTime = new Date(this.VideoInfoSource.createdAt); this.Tags = this.VideoInfoSource.tags.map((i) => i.id); this.FileName = this.VideoInfoSource.file.name.replace(/^\.|[\\\\/:*?\"<>|.]/img, '_'); this.Size = this.VideoInfoSource.file.size; this.VideoFileSource = JSON.parse(await get(this.VideoInfoSource.fileUrl.toURL(), window.location.href, await getAuth(this.VideoInfoSource.fileUrl))); if (isNull(this.VideoFileSource) || !(this.VideoFileSource instanceof Array) || this.VideoFileSource.length < 1) { throw new Error('获取视频源失败'); } this.getDownloadQuality = () => { return this.VideoFileSource.sort((a, b) => config.priority[b.name] - config.priority[a.name])[0].name; }; this.getDownloadUrl = () => { let fileList = this.VideoFileSource.filter(x => x.name == this.getDownloadQuality()); return decodeURIComponent('https:' + fileList[Math.floor(Math.random() * fileList.length)].src.download); }; const getCommentData = async (commentID = null, page = 0) => { return JSON.parse(await get(`https://api.iwara.tv/video/${this.ID}/comments?page=${page}${notNull(commentID) && !commentID.isEmpty() ? '&parent=' + commentID : ''}`.toURL(), window.location.href, await getAuth())); }; const getCommentDatas = async (commentID = null) => { let comments = []; let base = await getCommentData(commentID); comments.append(base.results); for (let page = 1; page < ceilDiv(base.count, base.limit); page++) { comments.append((await getCommentData(commentID, page)).results); } let replies = []; for (let index = 0; index < comments.length; index++) { const comment = comments[index]; if (comment.numReplies > 0) { replies.append(await getCommentDatas(comment.id)); } } comments.append(replies); return comments.filter(Boolean); }; this.Comments = this.VideoInfoSource.body + (await getCommentDatas()).map(i => i.body).join('\n'); this.State = true; return this; } catch (error) { let data = this; let toast = newToast(ToastType.Error, { node: toastNode([ `在解析 ${this.Name}[${this.ID}] 的过程中出现问题! `, { nodeType: 'br' }, `错误信息: ${error.getObjectString()}`, { nodeType: 'br' }, `→ 点击此处重新解析 ←` ], '解析模块'), onClick() { analyzeDownloadTask(new Dictionary([{ key: data.ID, value: data.Name }])); toast.hideToast(); }, }); toast.showToast(); console.error(`--------- ${this.Name}[${this.ID}] Error ---------`); console.log(this.VideoInfoSource); console.log(this.VideoFileSource); console.error(error); console.error(`--------- ${this.Name}[${this.ID}] Error ---------`); let button = document.querySelector(`.selectButton[videoid="${this.ID}"]`); button && button.checked && button.click(); videoList.remove(this.ID); this.State = false; return this; } } } let config = new Config(); let videoList = new Dictionary(); const originFetch = fetch; const modifyFetch = async (url, options) => { GM_getValue('isDebug') && console.log(`Fetch ${url}`); if (options !== undefined && options.headers !== undefined) { for (const key in options.headers) { if (key.toLocaleLowerCase() == "authorization") { if (config.authorization !== options.headers[key]) { let playload = JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(options.headers[key].split(' ').pop().split('.')[1])))); if (playload['type'] === 'refresh_token') { GM_getValue('isDebug') && console.log(`refresh_token: ${options.headers[key].split(' ').pop()}`); break; } if (playload['type'] === 'access_token') { config.authorization = `Bearer ${options.headers[key].split(' ').pop()}`; GM_getValue('isDebug') && console.log(JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(config.authorization.split('.')[1]))))); GM_getValue('isDebug') && console.log(`access_token: ${config.authorization.split(' ').pop()}`); break; } } /* if (playload['type'] === 'refresh_token') { let fetchResponse = await originFetch(url, options) let token = (await fetchResponse.json())['accessToken'] GM_getValue('isDebug') && console.log(JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(token.split('.')[1]))))) config.authorization = `Bearer ${token}` return new Promise<Response>((resolve, reject) => { resolve(new Proxy(fetchResponse, { get: function (target: any, prop: any, receiver: any) { if (typeof Reflect.get(target, prop) === 'function') { if (Reflect.get(target, prop + 'proxy') === undefined) { target[prop + 'proxy'] = new Proxy(Reflect.get(target, prop), { apply: (target, thisArg, argumentsList) => { console.log('fetchfunction', target.name, Response, argumentsList) return Reflect.apply(target, Response, argumentsList); } }); } return Reflect.get(target, prop + 'proxy') } return Reflect.get(target, prop); }, set(target: any, prop: any, value: any) { return Reflect.set(target, prop, value); } } )) }) } */ } } } return originFetch(url, options); }; window.fetch = modifyFetch; window.unsafeWindow.fetch = modifyFetch; const delay = async function (ms) { return new Promise(resolve => setTimeout(resolve, ms)); }; const UUID = function () { return Array.from({ length: 8 }, () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)).join(''); }; const ceilDiv = function (dividend, divisor) { return Math.floor(dividend / divisor) + (dividend % divisor > 0 ? 1 : 0); }; const random = function (min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }; const isNull = function (obj) { return obj === undefined || obj === null; }; const notNull = function (obj) { return obj !== undefined && obj !== null; }; const renderNode = function (renderCode) { if (typeof renderCode === "string") { return document.createTextNode(renderCode); } if (renderCode instanceof Node) { return renderCode; } if (typeof renderCode !== "object" || !renderCode.nodeType) { throw new Error('Invalid arguments'); } const { nodeType, attributes, events, className, childs } = renderCode; const node = document.createElement(nodeType); (notNull(attributes) && Object.keys(attributes).length !== 0) && Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, value)); (notNull(events) && Object.keys(events).length > 0) && Object.entries(events).forEach(([eventName, eventHandler]) => originalAddEventListener.call(node, eventName, eventHandler)); (notNull(className) && className.length > 0) && node.classList.add(...[].concat(className)); notNull(childs) && node.append(...[].concat(childs).map(renderNode)); return node; }; async function get(url, referrer = window.location.href, headers = {}) { if (url.hostname !== window.location.hostname) { let data = await new Promise(async (resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url.href, headers: Object.assign({ 'Accept': 'application/json, text/plain, */*' }, headers), onload: function (response) { resolve(response); }, onerror: function (error) { reject(error); } }); }); return data.responseText; } return (await originFetch(url.href, { 'headers': Object.assign({ 'accept': 'application/json, text/plain, */*' }, headers), 'referrer': referrer, 'method': 'GET', "mode": "cors", "credentials": "include" })).text(); } async function post(url, body, referrer = window.location.hostname, headers = {}) { if (typeof body !== 'string') body = JSON.stringify(body); if (url.hostname !== window.location.hostname) { let data = await new Promise(async (resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: url.href, headers: Object.assign({ 'Accept': 'application/json', 'Content-Type': 'application/json' }, headers), data: body, onload: function (response) { resolve(response); }, onerror: function (error) { reject(error); } }); }); return data.responseText; } return (await originFetch(url.href, { 'headers': Object.assign({ 'accept': 'application/json, text/plain, */*' }, headers), 'referrer': referrer, 'body': body, 'method': 'POST', "mode": "cors", "credentials": "include" })).text(); } function parseSearchParams(searchParams, initialObject = {}) { return [...searchParams.entries()].reduce((acc, [key, value]) => ({ ...acc, [key]: value }), initialObject); } async function refreshToken() { let refresh = JSON.parse(await post(`https://api.iwara.tv/user/token`.toURL(), {}, window.location.href, { 'Authorization': `Bearer ${localStorage.getItem('token')}` })); return refresh['accessToken']; } async function getXVersion(urlString) { let url = new URL(urlString); let params = parseSearchParams(url.searchParams); const data = new TextEncoder().encode(`${url.pathname.split("/").pop()}_${params['expires']}_5nFp9kmbNnHdAFhaqMvt`); const hashBuffer = await crypto.subtle.digest("SHA-1", data); return Array.from(new Uint8Array(hashBuffer)) .map(b => b.toString(16).padStart(2, "0")) .join(""); } // 检查是否是首次运行脚本 if (GM_getValue('isFirstRun', true)) { GM_listValues().forEach(i => GM_deleteValue(i)); config = new Config(); let confirmButton = renderNode({ nodeType: 'button', attributes: { disabled: true }, childs: '确定', events: { click: () => { GM_setValue('isFirstRun', false); document.querySelector('#pluginOverlay').remove(); window.unsafeWindow.location.reload(); } } }); document.body.appendChild(renderNode({ nodeType: 'div', attributes: { id: 'pluginOverlay' }, childs: [ { nodeType: 'div', className: 'main', childs: [ { nodeType: 'h2', childs: [ '请使用', { nodeType: 'a', attributes: { href: 'https://docs.scriptcat.org/' }, childs: 'ScriptCat' }, ' 或 ', { nodeType: 'a', attributes: { href: 'https://www.tampermonkey.net/index.php?#download_gcal' }, childs: 'Tampermonkey Beta' }, '载入本脚本。' ] }, { nodeType: 'p', childs: '路径变量:%#Y#% (当前时间[年]) | %#M#% (当前时间[月]) | %#D#% (当前时间[日]) | %#h#% (当前时间[时]) | %#m#% (当前时间[分]) | %#s#% (当前时间[秒])' }, { nodeType: 'p', childs: '%#TITLE#% (标题) | %#ID#% (ID) | %#AUTHOR#% (作者)' }, { nodeType: 'p', childs: '%#UploadYear#% (发布时间[年]) | %#UploadMonth#% (发布时间[月]) | %#UploadDate#% (发布时间[日]) | %#UploadHours#% (发布时间[时]) | %#UploadMinutes#% (发布时间[分]) | %#UploadSeconds#% (发布时间[秒])' }, { nodeType: 'p', childs: '例: %#Y#%-%#M#%-%#D#%_%#TITLE#%[%#ID#%].MP4' }, { nodeType: 'p', childs: '结果: ' + '%#Y#%-%#M#%-%#D#%_%#TITLE#%[%#ID#%].MP4'.replaceNowTime().replace('%#TITLE#%', '演示标题').replace('%#ID#%', '演示ID'), }, { nodeType: 'p', childs: '打开i站后等待加载出视频后, 点击侧边栏中“开关选择”开启下载复选框' }, { nodeType: 'p', childs: '插件下载视频前会检查视频简介,如果在简介中发现疑似第三方下载链接,将会在控制台提示,您可以手动打开视频页面选择。' }, { nodeType: 'p', childs: '手动下载需要您提供视频ID!' } ] }, { nodeType: 'div', className: 'checkbox-container', childs: [ { nodeType: 'input', className: 'checkbox', attributes: { type: 'checkbox', name: 'agree-checkbox' }, events: { change: (event) => { confirmButton.disabled = !event.target.checked; } } }, { nodeType: 'label', className: 'checkbox-label', attributes: { for: 'agree-checkbox' }, childs: { nodeType: 'h1', className: 'rainbow-text', childs: '我已知晓如何使用!!!' }, }, ], }, confirmButton ] })); } async function getAuth(url) { return Object.assign({ 'Cooike': config.cookies.map((i) => `${i.name}:${i.value}`).join('; '), 'Authorization': config.authorization }, notNull(url) && !url.isEmpty() ? { 'X-Version': await getXVersion(url) } : {}); } async function addDownloadTask() { let data = prompt('请输入需要下载的视频ID! \r\n若需要批量下载请用 "|" 分割ID, 例如: AAAAAAAAAA|BBBBBBBBBBBB|CCCCCCCCCCCC...', ''); if (notNull(data) && !(data.isEmpty())) { let IDList = new Dictionary(); data.toLowerCase().split('|').map(ID => ID.match(/((?<=(\[)).*?(?=(\])))/g)?.pop() ?? ID.match(/((?<=(\_)).*?(?=(\_)))/g)?.pop() ?? ID).filter(Boolean).map(ID => IDList.set(ID, '手动解析')); analyzeDownloadTask(IDList); } } async function analyzeDownloadTask(list = videoList) { for (const key in list.items) { await delay(random(10, 100)); //脚本太快了,延迟一下防止被屏蔽 let videoInfo = await (new VideoInfo(list[key])).init(key); if (videoInfo.State) { await pustDownloadTask(videoInfo); let button = document.querySelector(`.selectButton[videoid="${key}"]`); button && button.checked && button.click(); list.remove(key); } } } function checkIsHaveDownloadLink(comment) { if (!config.checkDownloadLink || isNull(comment) || comment.isEmpty()) { return false; } const downloadLinkCharacteristics = [ 'pan\.baidu', 'mega\.nz', 'drive\.google\.com', 'aliyundrive', 'uploadgig', 'katfile', 'storex', 'subyshare', 'rapidgator', 'filebe', 'filespace', 'mexa\.sh', 'mexashare', 'mx-sh\.net', 'uploaded\.', 'icerbox', 'alfafile', 'drv\.ms', 'onedrive', 'pixeldrain\.com', 'gigafile\.nu' ]; for (let index = 0; index < downloadLinkCharacteristics.length; index++) { if (comment.toLowerCase().includes(downloadLinkCharacteristics[index])) { return true; } } return false; } function toastNode(body, title) { return renderNode({ nodeType: 'div', childs: [ notNull(title) && !title.isEmpty() ? { nodeType: 'h3', childs: `Iwara 批量下载工具 - ${title}` } : { nodeType: 'h3', childs: 'Iwara 批量下载工具' }, { nodeType: 'p', childs: body } ] }); } function newToast(type, params) { return Toastify(Object.assign({ newWindow: true, gravity: "top", position: "right", stopOnFocus: true }, type === ToastType.Warn && { duration: -1, style: { background: "linear-gradient(-30deg, rgb(119 76 0), rgb(255 165 0))" } }, type === ToastType.Error && { duration: -1, style: { background: "linear-gradient(-30deg, rgb(108 0 0), rgb(215 0 0))" } }, notNull(params) && params)); } async function pustDownloadTask(videoInfo) { if (config.checkDownloadLink && checkIsHaveDownloadLink(videoInfo.Comments)) { let toast = newToast(ToastType.Warn, { node: toastNode([`在创建 ${videoInfo.Name}[${videoInfo.ID}] 下载任务过程中发现疑似高画质下载连接! `, { nodeType: 'br' }, `点击此处,进入视频页面`], '创建任务'), onClick() { GM_openInTab(`https://www.iwara.tv/video/${videoInfo.ID}`, { active: true, insert: true, setParent: true }); toast.hideToast(); } }); toast.showToast(); return; } if (config.checkDownloadLink && videoInfo.getDownloadQuality() != 'Source') { let toast = newToast(ToastType.Warn, { node: toastNode([`在创建 ${videoInfo.Name}[${videoInfo.ID}] 下载任务过程中发现无原画下载地址! `, { nodeType: 'br' }, `→ 点击此处,进入视频页面 ←`], '创建任务'), onClick() { GM_openInTab(`https://www.iwara.tv/video/${videoInfo.ID}`, { active: true, insert: true, setParent: true }); toast.hideToast(); } }); toast.showToast(); return; } switch (config.downloadType) { case DownloadType.Aria2: aria2Download(videoInfo); break; case DownloadType.IwaraDownloader: iwaraDownloaderDownload(videoInfo); break; case DownloadType.Browser: browserDownload(videoInfo); break; default: othersDownload(videoInfo); break; } } function analyzeLocalPath(path) { let matchPath = path.match(/^([a-zA-Z]:)?[\/\\]?([^\/\\]+[\/\\])*([^\/\\]+\.\w+)$/) || ''; return { fullPath: matchPath[0], drive: matchPath[1] || '', directories: matchPath[2].split(/[\/\\]/), filename: matchPath[3], match: matchPath !== null }; } async function EnvCheck() { try { if (GM_info.downloadMode !== "browser") { GM_getValue('isDebug') && console.log(GM_info); throw new Error("请启用脚本管理器的浏览器API下载模式!"); } } catch (error) { let toast = newToast(ToastType.Error, { node: toastNode([ `无法保存配置, 请检查配置是否正确。`, { nodeType: 'br' }, `错误信息: ${error.getObjectString()}` ], '配置检查'), position: "center", onClick() { toast.hideToast(); } }); toast.showToast(); console.error(error); return false; } return true; } async function aria2Check() { try { let res = JSON.parse(await post(config.aria2Path.toURL(), { 'jsonrpc': '2.0', 'method': 'aria2.tellActive', 'id': UUID(), 'params': ['token:' + config.aria2Token] })); if (res.error) { throw new Error(res.error.message); } } catch (error) { let toast = newToast(ToastType.Error, { node: toastNode([ `Aria2 RPC 连接测试`, { nodeType: 'br' }, `错误信息: ${error.getObjectString()}` ], '配置检查'), position: "center", onClick() { toast.hideToast(); } }); toast.showToast(); console.error(error); return false; } return true; } async function iwaraDownloaderCheck() { let errorObj = {}; try { let res = JSON.parse(await post(config.iwaraDownloaderPath.toURL(), Object.assign({ 'ver': 1, 'code': 'State' }, config.iwaraDownloaderToken.isEmpty() ? {} : { 'token': config.iwaraDownloaderToken }))); if (res.code !== 0) { let err = new Error(res.msg); errorObj = { message: err.message, stack: err.stack }; throw err; } } catch (error) { let toast = newToast(ToastType.Error, { node: toastNode([ `IwaraDownloader RPC 连接测试`, { nodeType: 'br' }, `错误信息: ${error.getObjectString()}` ], '配置检查'), position: "center", onClick() { toast.hideToast(); } }); toast.showToast(); console.error(error); return false; } return true; } function aria2Download(videoInfo) { (async function (id, author, name, uploadTime, info, tag, downloadUrl) { let localPath = analyzeLocalPath(config.downloadPath.replaceNowTime().replaceUploadTime(uploadTime).replaceVariable({ AUTHOR: author, ID: id, TITLE: name }).trim()); let json = JSON.stringify({ 'jsonrpc': '2.0', 'method': 'aria2.addUri', 'id': UUID(), 'params': [ 'token:' + config.aria2Token, [downloadUrl], Object.assign(config.downloadProxy.isEmpty() ? {} : { 'all-proxy': config.downloadProxy }, config.downloadPath.isEmpty() ? {} : { 'out': localPath.filename, 'dir': localPath.fullPath.replace(localPath.filename, '') }, { 'referer': 'https://ecchi.iwara.tv/', 'header': [ 'Cookie:' + config.cookies.map((i) => `${i.name}:${i.value}`).join('; ') //,'Authorization:' + config.authorization ] }) ] }); console.log(`${name} 已推送到Aria2 ${await post(config.aria2Path.toURL(), json)}`); newToast(ToastType.Info, { node: toastNode(`${videoInfo.Name}[${videoInfo.ID}] 已推送到Aria2`) }).showToast(); }(videoInfo.ID, videoInfo.Author, videoInfo.Name, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.getDownloadUrl())); } function iwaraDownloaderDownload(videoInfo) { (async function (ID, Author, Name, UploadTime, Info, Tag, DownloadUrl, Size) { let r = JSON.parse(await post(config.iwaraDownloaderPath.toURL(), Object.assign({ 'ver': 1, 'code': 'add', 'data': Object.assign({ 'source': ID, 'author': Author, 'name': Name, 'downloadTime': new Date(), 'uploadTime': UploadTime, 'downloadUrl': DownloadUrl, 'downloadCookies': config.cookies, 'authorization': config.authorization, 'size': Size, 'info': Info, 'tag': Tag }, config.downloadPath.isEmpty() ? {} : { 'path': config.downloadPath.replaceNowTime().replaceUploadTime(UploadTime).replaceVariable({ AUTHOR: Author, ID: ID, TITLE: Name }) }) }, config.iwaraDownloaderToken.isEmpty() ? {} : { 'token': config.iwaraDownloaderToken }))); if (r.code == 0) { console.log(`${Name} 已推送到IwaraDownloader ${r}`); newToast(ToastType.Info, { node: toastNode(`${Name}[${ID}] 已推送到IwaraDownloader`) }).showToast(); } else { let toast = newToast(ToastType.Error, { node: toastNode([ `在推送 ${Name}[${ID}] 下载任务到IwaraDownloader过程中出现错误! `, { nodeType: 'br' }, `错误信息: ${r.msg}` ], '推送下载任务'), position: "center", onClick() { toast.hideToast(); } }); toast.showToast(); console.log(`${Name} 推送失败 ${r}`); } }(videoInfo.ID, videoInfo.Author, videoInfo.Name, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.getDownloadUrl(), videoInfo.Size)); } function othersDownload(videoInfo) { (async function (ID, Author, Name, UploadTime, Info, Tag, DownloadUrl) { GM_openInTab(DownloadUrl, { active: true, insert: true, setParent: true }); }(videoInfo.ID, videoInfo.Author, videoInfo.Name, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.getDownloadUrl())); } function browserDownload(videoInfo) { (async function (ID, Author, Name, UploadTime, Info, Tag, DownloadUrl) { function browserDownloadError(error) { let toast = newToast(ToastType.Error, { node: toastNode([ `在下载 ${Name}[${ID}] 的过程中出现问题! `, { nodeType: 'br' }, `错误信息: ${error.getObjectString()}`, { nodeType: 'br' }, `→ 点击此处重新下载 ←` ], '下载任务'), position: "center", onClick() { analyzeDownloadTask(new Dictionary([{ key: ID, value: Name }])); toast.hideToast(); } }); toast.showToast(); } let localPath = analyzeLocalPath(config.downloadPath.replaceNowTime().replaceUploadTime(UploadTime).replaceVariable({ AUTHOR: Author, ID: ID, TITLE: Name }).trim()); GM_download({ url: DownloadUrl, saveAs: false, name: localPath.filename, onerror: (err) => browserDownloadError(err), ontimeout: () => browserDownloadError(new Error('Timeout')) }); }(videoInfo.ID, videoInfo.Author, videoInfo.Name, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.getDownloadUrl())); } GM_addStyle(await get('https://cdn.staticfile.org/toastify-js/1.12.0/toastify.min.css'.toURL())); GM_addStyle(` .rainbow-text { background-image: linear-gradient(to right, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 600% 100%; animation: rainbow 0.5s infinite linear; } @keyframes rainbow { 0% { background-position: 0% 0%; } 100% { background-position: 100% 0%; } } #pluginMenu { z-index: 4096; color: white; position: fixed; top: 50%; right: 0px; padding: 10px; background-color: #565656; border: 1px solid #ccc; border-radius: 5px; box-shadow: 0 0 10px #ccc; transform: translate(85%, -50%); transition: transform 0.5s cubic-bezier(0,1,.60,1); } #pluginMenu ul { list-style: none; margin: 0; padding: 0; } #pluginMenu li { padding: 5px 10px; cursor: pointer; text-align: center; user-select: none; } #pluginMenu li:hover { background-color: #000000cc; border-radius: 3px; } #pluginMenu:hover { transform: translate(0%, -50%); transition-delay: 0.5s; } #pluginMenu:not(:hover) { transition-delay: 0s; } #pluginMenu.moving-out { transform: translate(0%, -50%); } #pluginMenu.moving-in { transform: translate(85%, -50%); } /* 以下为兼容性处理 */ #pluginMenu:not(.moving-out):not(.moving-in) { transition-delay: 0s; } #pluginMenu:hover, #pluginMenu:hover ~ #pluginMenu { transition-delay: 0s; } #pluginMenu:hover { transition-duration: 0.5s; } #pluginMenu:not(:hover).moving-in { transition-delay: 0.5s; } #pluginConfig { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(128, 128, 128, 0.8); z-index: 8192; display: flex; flex-direction: column; align-items: center; justify-content: center; } #pluginConfig .main { color: white; background-color: rgb(64,64,64,0.7); padding: 24px; margin: 10px; overflow-y: auto; } @media (max-width: 640px) { #pluginConfig .main { width: 100%; } } #pluginConfig button { background-color: blue; padding: 10px 20px; color: white; font-size: 18px; border: none; border-radius: 4px; cursor: pointer; } #pluginConfig p { display: flex; flex-direction: column; } #pluginConfig p label{ display: flex; } #pluginConfig p label input{ flex-grow: 1; margin-left: 10px; } #pluginConfig .inputRadioLine { display: flex; align-items: center; flex-direction: row; margin-right: 10px; } #pluginConfig .inputRadio { display: flex; align-items: center; flex-direction: row-reverse; margin-right: 10px; } #pluginOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(128, 128, 128, 0.8); z-index: 8192; display: flex; flex-direction: column; align-items: center; justify-content: center; } #pluginOverlay .main { color: white; font-size: 24px; width: 60%; background-color: rgb(64,64,64,0.7); padding: 24px; margin: 10px; overflow-y: auto; } @media (max-width: 640px) { #pluginOverlay .main { width: 100%; } } #pluginOverlay button { padding: 10px 20px; color: white; font-size: 18px; border: none; border-radius: 4px; cursor: pointer; } #pluginOverlay button { background-color: blue; } #pluginOverlay button[disabled] { background-color: darkgray; cursor: not-allowed; } #pluginOverlay .checkbox-container { display: flex; align-items: center; } #pluginOverlay .checkbox-label { color: white; font-size: 18px; margin-left: 10px; } .selectButton { position: absolute; width: 38px; height: 38px; bottom: 24px; right: 0px; } .selectButtonFirefox { position: absolute; width: 32px; height: 32px; bottom: 0px; right: 4px; transform: translate(-50%, -50%); margin: 0; padding: 0; } .toastify h3 { margin: 0 0 10px 0; } .toastify p { margin: 0 ; } `); document.body.appendChild(renderNode({ nodeType: "div", attributes: { id: "pluginMenu" }, childs: { nodeType: "ul", childs: [ { nodeType: "li", childs: "开关选择", events: { click: () => { let isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); GM_getValue('isDebug') && console.log(isFirefox); if (!document.querySelector('.selectButton')) { let videoNodes = document.querySelectorAll(`.videoTeaser`); console.log("开始注入复选框 预计注入" + videoNodes.length + "个复选框"); videoNodes.forEach((element) => { let ID = element.querySelector('.videoTeaser__thumbnail').getAttribute('href').trim().split('/')[2]; let Name = element.querySelector('.videoTeaser__title').getAttribute('title').trim(); let node = isFirefox ? element : element.querySelector('.videoTeaser__thumbnail'); node.appendChild(renderNode({ nodeType: "input", attributes: Object.assign(videoList.has(ID) ? { checked: true } : {}, { type: "checkbox", videoID: ID, videoName: Name }), className: isFirefox ? ['selectButton', 'selectButtonFirefox'] : 'selectButton', events: { click: (event) => { let target = event.target; target.checked ? videoList.set(ID, Name) : videoList.remove(ID); event.stopPropagation(); event.stopImmediatePropagation(); return false; } } })); }); } else { console.log("移除复选框"); document.querySelectorAll('.selectButton').forEach((element) => { videoList.remove(element.getAttribute('videoid')); element.remove(); }); } } } }, { nodeType: "li", childs: "下载所选", events: { click: (event) => { analyzeDownloadTask(); newToast(ToastType.Info, { text: `正在下载所选, 请稍后...`, close: true }).showToast(); event.stopPropagation(); return false; } } }, { nodeType: "li", childs: "全部选中", events: { click: (event) => { document.querySelectorAll('.selectButton').forEach((element) => { let button = element; !button.checked && button.click(); }); event.stopPropagation(); return false; } } }, { nodeType: "li", childs: "取消全选", events: { click: (event) => { document.querySelectorAll('.selectButton').forEach((element) => { let button = element; button.checked && button.click(); }); event.stopPropagation(); return false; } } }, { nodeType: "li", childs: "反向选中", events: { click: (event) => { document.querySelectorAll('.selectButton').forEach((element) => { element.click(); }); event.stopPropagation(); return false; } } }, { nodeType: "li", childs: "手动下载", events: { click: (event) => { addDownloadTask(); event.stopPropagation(); return false; } } }, { nodeType: "li", childs: "打开设置", events: { click: (event) => { config.edit(); event.stopPropagation(); return false; } } } ] } })); newToast(ToastType.Info, { text: `Iwara 批量下载工具加载完成`, duration: 10000, close: true }).showToast(); })(); //# sourceMappingURL=IwaraDownloadTool.user.js.map