// ==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://www.iwara.tv/logo.png
// @namespace https://github.com/dawn-lc/
// @author dawn-lc
// @license Apache-2.0
// @copyright 2024, Dawnlc (https://dawnlc.me/)
// @source https://github.com/dawn-lc/IwaraDownloadTool
// @supportURL https://github.com/dawn-lc/IwaraDownloadTool/issues
// @connect iwara.tv
// @connect *.iwara.tv
// @connect mmdfans.net
// @connect *.mmdfans.net
// @connect localhost
// @connect 127.0.0.1
// @connect *
// @match *://*.iwara.tv/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_addValueChangeListener
// @grant GM_addStyle
// @grant GM_addElement
// @grant GM_getResourceText
// @grant GM_download
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_info
// @grant unsafeWindow
// @run-at document-start
// @require https://cdn.staticfile.net/dexie/3.2.4/dexie.min.js
// @require https://cdn.staticfile.net/toastify-js/1.12.0/toastify.min.js
// @require https://cdn.staticfile.net/moment.js/2.30.1/moment.min.js
// @require https://cdn.staticfile.net/moment.js/2.30.1/moment-with-locales.min.js
// @resource toastify-css https://cdn.staticfile.net/toastify-js/1.12.0/toastify.min.css
// @version 3.2.58
// ==/UserScript==
(function () {
const originalFetch = unsafeWindow.fetch;
const originalNodeAppendChild = unsafeWindow.Node.prototype.appendChild;
const originalAddEventListener = unsafeWindow.EventTarget.prototype.addEventListener;
const isNull = (obj) => typeof obj === 'undefined' || obj === null;
const isObject = (obj) => !isNull(obj) && typeof obj === 'object' && !Array.isArray(obj);
const isString = (obj) => !isNull(obj) && typeof obj === 'string';
const isNumber = (obj) => !isNull(obj) && typeof obj === 'number';
const isElement = (obj) => !isNull(obj) && obj instanceof Element;
const isNode = (obj) => !isNull(obj) && obj instanceof Node;
const isDate = (obj) => !isNull(obj) && obj instanceof Date;
const isStringTupleArray = (obj) => Array.isArray(obj) && obj.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string');
const hasFunction = (obj, method) => {
return !method.isEmpty() && !isNull(obj) ? method in obj && typeof obj[method] === 'function' : false;
};
const getString = (obj) => {
obj = obj instanceof Error ? String(obj) : obj;
obj = obj instanceof Date ? obj.format('YYYY-MM-DD') : obj;
return typeof obj === 'object' ? JSON.stringify(obj, null, 2) : String(obj);
};
Array.prototype.any = function () {
return this.prune().length > 0;
};
Array.prototype.prune = function () {
return this.filter(i => i !== null && typeof i !== 'undefined');
};
Array.prototype.unique = function (prop) {
return this.filter((item, index, self) => index === self.findIndex((t) => (prop ? t[prop] === item[prop] : t === item)));
};
Array.prototype.union = function (that, prop) {
return [...this, ...that].unique(prop);
};
Array.prototype.intersect = function (that, prop) {
return this.filter((item) => that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
};
Array.prototype.difference = function (that, prop) {
return this.filter((item) => !that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
};
Array.prototype.complement = function (that, prop) {
return this.union(that, prop).difference(this.intersect(that, prop), prop);
};
String.prototype.isEmpty = function () {
return !isNull(this) && this.length === 0;
};
String.prototype.among = function (start, end) {
if (this.isEmpty() || start.isEmpty() || end.isEmpty()) {
throw new Error('Empty');
}
let body = !this.split(start).pop().isEmpty() ? this.split(start).pop() : '';
return !body.split(end).shift().isEmpty() ? body.split(end).shift() : '';
};
String.prototype.splitLimit = function (separator, limit) {
if (this.isEmpty() || isNull(separator)) {
throw new Error('Empty');
}
let body = this.split(separator);
return limit ? body.slice(0, limit).concat(body.slice(limit).join(separator)) : body;
};
String.prototype.truncate = function (maxLength) {
return this.length > maxLength ? this.substring(0, maxLength) : this.toString();
};
String.prototype.trimHead = function (prefix) {
return this.startsWith(prefix) ? this.slice(prefix.length) : this.toString();
};
String.prototype.trimTail = function (suffix) {
return this.endsWith(suffix) ? this.slice(0, -suffix.length) : this.toString();
};
String.prototype.toURL = function () {
let URLString = this;
if (URLString.split('//')[0].isEmpty()) {
URLString = `${unsafeWindow.location.protocol}${URLString}`;
}
return new URL(URLString.toString());
};
Array.prototype.append = function (arr) {
this.push(...arr);
};
Date.prototype.format = function (format) {
return moment(this).locale(language()).format(format);
};
String.prototype.replaceVariable = function (replacements, count = 0) {
let replaceString = this.toString();
try {
replaceString = Object.entries(replacements).reduce((str, [key, value]) => {
if (str.includes(`%#${key}:`)) {
let format = str.among(`%#${key}:`, '#%').toString();
return str.replaceAll(`%#${key}:${format}#%`, getString(hasFunction(value, 'format') ? value.format(format) : value));
}
else {
return str.replaceAll(`%#${key}#%`, getString(value));
}
}, replaceString);
count++;
return Object.keys(replacements).map((key) => this.includes(`%#${key}#%`)).includes(true) && count < 128 ? replaceString.replaceVariable(replacements, count) : replaceString;
}
catch (error) {
GM_getValue('isDebug') && console.log(`replace variable error: ${getString(error)}`);
return replaceString;
}
};
function prune(obj) {
if (Array.isArray(obj)) {
return obj.filter(isNotEmpty).map(prune);
}
if (isElement(obj) || isNode(obj)) {
return obj;
}
if (isObject(obj)) {
return Object.fromEntries(Object.entries(obj)
.filter(([key, value]) => isNotEmpty(value))
.map(([key, value]) => [key, prune(value)]));
}
return isNotEmpty(obj) ? obj : undefined;
}
function isNotEmpty(obj) {
if (isNull(obj)) {
return false;
}
if (Array.isArray(obj)) {
return obj.some(isNotEmpty);
}
if (isString(obj)) {
return !obj.isEmpty();
}
if (isNumber(obj)) {
return !Number.isNaN(obj);
}
if (isElement(obj) || isNode(obj)) {
return true;
}
if (isObject(obj)) {
return Object.values(obj).some(isNotEmpty);
}
return true;
}
if (!isNull((unsafeWindow.IwaraDownloadTool))) {
return;
}
unsafeWindow.IwaraDownloadTool = true;
const fetch = (input, init, force) => {
if (init && init.headers && isStringTupleArray(init.headers))
throw new Error("init headers Error");
if (init && init.method && !(init.method === 'GET' || init.method === 'HEAD' || init.method === 'POST'))
throw new Error("init method Error");
return force || (typeof input === 'string' ? input : input.url).toURL().hostname !== unsafeWindow.location.hostname ? new Promise((resolve, reject) => {
GM_xmlhttpRequest(prune({
method: (init && init.method) || 'GET',
url: typeof input === 'string' ? input : input.url,
headers: (init && init.headers) || {},
data: ((init && init.body) || null),
onload: function (response) {
resolve(new Response(response.responseText, {
status: response.status,
statusText: response.statusText,
}));
},
onerror: function (error) {
reject(error);
}
}));
}) : originalFetch(input, init);
};
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 language = function () {
let env = (!isNull(config) ? config.language : (navigator.language ?? navigator.languages[0] ?? 'en')).replace('-', '_');
let main = env.split('_').shift() ?? 'en';
return (!isNull(i18n[env]) ? env : !isNull(i18n[main]) ? main : 'en');
};
const renderNode = function (renderCode) {
renderCode = prune(renderCode);
if (isNull(renderCode))
throw new Error("RenderCode null");
if (typeof renderCode === 'string') {
return document.createTextNode(renderCode.replaceVariable(i18n[language()]).toString());
}
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);
(!isNull(attributes) && Object.keys(attributes).any()) && Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, value));
(!isNull(events) && Object.keys(events).any()) && Object.entries(events).forEach(([eventName, eventHandler]) => originalAddEventListener.call(node, eventName, eventHandler));
(!isNull(className) && className.length > 0) && node.classList.add(...[].concat(className));
!isNull(childs) && node.append(...[].concat(childs).map(renderNode));
return node;
};
const findElement = function (element, condition) {
while (element && !element.matches(condition)) {
element = element.parentElement;
}
return element;
};
if (GM_getValue('isDebug')) {
console.log(getString(GM_info));
debugger;
}
const Channel = new BroadcastChannel('IwaraDownloadTool');
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 PageType;
(function (PageType) {
PageType["Video"] = "video";
PageType["Image"] = "image";
PageType["VideoList"] = "videoList";
PageType["ImageList"] = "imageList";
PageType["Forum"] = "forum";
PageType["ForumSection"] = "forumSection";
PageType["ForumThread"] = "forumThread";
PageType["Page"] = "page";
PageType["Home"] = "home";
PageType["Profile"] = "profile";
PageType["Subscriptions"] = "subscriptions";
PageType["Playlist"] = "playlist";
PageType["Favorites"] = "favorites";
PageType["Search"] = "search";
PageType["Account"] = "account";
})(PageType || (PageType = {}));
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 = {}));
let MessageType;
(function (MessageType) {
MessageType[MessageType["Set"] = 0] = "Set";
MessageType[MessageType["Del"] = 1] = "Del";
})(MessageType || (MessageType = {}));
let VersionState;
(function (VersionState) {
VersionState[VersionState["Low"] = 0] = "Low";
VersionState[VersionState["Equal"] = 1] = "Equal";
VersionState[VersionState["High"] = 2] = "High";
})(VersionState || (VersionState = {}));
class Version {
constructor(versionString) {
const [version, preRelease, buildMetadata] = versionString.split(/[-+]/);
const versionParts = version.split('.').map(Number);
this.major = versionParts[0] || 0;
this.minor = versionParts.length > 1 ? versionParts[1] : 0;
this.patch = versionParts.length > 2 ? versionParts[2] : 0;
this.preRelease = preRelease ? preRelease.split('.') : [];
this.buildMetadata = buildMetadata;
}
compare(other) {
const compareSegment = (a, b) => {
if (a < b) {
return VersionState.Low;
}
else if (a > b) {
return VersionState.High;
}
return VersionState.Equal;
};
let state = compareSegment(this.major, other.major);
if (state !== VersionState.Equal)
return state;
state = compareSegment(this.minor, other.minor);
if (state !== VersionState.Equal)
return state;
state = compareSegment(this.patch, other.patch);
if (state !== VersionState.Equal)
return state;
for (let i = 0; i < Math.max(this.preRelease.length, other.preRelease.length); i++) {
const pre1 = this.preRelease[i];
const pre2 = other.preRelease[i];
if (pre1 === undefined && pre2 !== undefined) {
return VersionState.High;
}
else if (pre1 !== undefined && pre2 === undefined) {
return VersionState.Low;
}
if (pre1 !== undefined && pre2 !== undefined) {
state = compareSegment(isNaN(+pre1) ? pre1 : +pre1, isNaN(+pre2) ? pre2 : +pre2);
if (state !== VersionState.Equal)
return state;
}
}
return VersionState.Equal;
}
}
class SyncDictionary {
constructor(id, data = []) {
this.id = id;
this.dictionary = new Dictionary(data);
GM_getValue(this.id, []).map(i => this.dictionary.set(i.key, i.value));
Channel.onmessage = (event) => {
const message = event.data;
if (message.id === this.id) {
switch (message.type) {
case MessageType.Set:
this.dictionary.set(message.data.key, message.data.value);
let selectButtonA = unsafeWindow.document.querySelector(`input.selectButton[videoid*="${message.data.key}"i]`);
if (!isNull(selectButtonA))
selectButtonA.checked = true;
break;
case MessageType.Del:
this.dictionary.del(message.data.key);
let selectButtonB = unsafeWindow.document.querySelector(`input.selectButton[videoid=*"${message.data.key}"i]`);
if (!isNull(selectButtonB))
selectButtonB.checked = false;
break;
default:
break;
}
}
};
Channel.onmessageerror = (event) => {
GM_getValue('isDebug') && console.log(`Channel message error: ${getString(event)}`);
};
}
set(key, value) {
this.dictionary.set(key, value);
Channel.postMessage({ id: this.id, type: MessageType.Set, data: { key: key, value: value } });
GM_setValue(this.id, this.dictionary.toArray());
}
get(key) {
return this.dictionary.get(key);
}
has(key) {
return this.dictionary.has(key);
}
del(key) {
this.dictionary.del(key);
Channel.postMessage({ id: this.id, type: MessageType.Del, data: { key: key } });
GM_setValue(this.id, this.dictionary.toArray());
}
get size() {
return this.dictionary.size;
}
keys() {
return this.dictionary.keys();
}
values() {
return this.dictionary.values();
}
toArray() {
return this.dictionary.toArray();
}
}
class Dictionary {
constructor(data = []) {
this.items = new Object();
data.map(i => this.set(i.key, i.value));
}
set(key, value) {
this.items[key] = value;
}
del(key) {
if (this.has(key)) {
delete this.items[key];
}
else if (this.has(key.toLowerCase())) {
delete this.items[key.toLowerCase()];
}
}
get(key) {
return this.has(key) ? this.items[key] : this.has(key.toLowerCase()) ? this.items[key.toLowerCase()] : undefined;
}
has(key) {
return this.items.hasOwnProperty(key) || this.items.hasOwnProperty(key.toLowerCase());
}
get size() {
return Object.keys(this.items).length;
}
keys() {
return Object.keys(this.items);
}
values() {
return Object.values(this.items);
}
toArray() {
return this.keys().map(k => { return { key: k, value: this.items[k] }; });
}
}
class I18N {
constructor() {
this.zh_CN = this['zh'];
this.zh = {
appName: 'Iwara 批量下载工具',
language: '语言: ',
downloadPriority: '下载画质: ',
downloadPath: '下载到: ',
downloadProxy: '下载代理: ',
aria2Path: 'Aria2 RPC: ',
aria2Token: 'Aria2 密钥: ',
iwaraDownloaderPath: 'IwaraDownloader RPC: ',
iwaraDownloaderToken: 'IwaraDownloader 密钥: ',
rename: '重命名',
save: '保存',
ok: '确定',
on: '开启',
off: '关闭',
isDebug: '调试模式',
downloadType: '下载方式',
browserDownload: '浏览器下载',
iwaraDownloaderDownload: 'IwaraDownloader下载',
autoFollow: '自动关注选中的视频作者',
autoLike: '自动点赞选中的视频',
checkDownloadLink: '第三方网盘下载地址检查',
checkPriority: '下载画质检查',
autoInjectCheckbox: '自动注入选择框',
configurationIncompatible: '检测到不兼容的配置文件,请重新配置!',
browserDownloadNotEnabled: `未启用下载功能!`,
browserDownloadNotWhitelisted: `请求的文件扩展名未列入白名单!`,
browserDownloadNotPermitted: `下载功能已启用,但未授予下载权限!`,
browserDownloadNotSupported: `目前浏览器/版本不支持下载功能!`,
browserDownloadNotSucceeded: `下载未开始或失败!`,
browserDownloadUnknownError: `未知错误,有可能是下载时提供的参数存在问题,请检查文件名是否合法!`,
browserDownloadTimeout: `下载超时,请检查网络环境是否正常!`,
variable: '查看可用变量',
downloadTime: '下载时间 ',
uploadTime: '发布时间 ',
example: '示例: ',
result: '结果: ',
loadingCompleted: '加载完成',
settings: '打开设置',
downloadThis: '下载当前',
manualDownload: '手动下载',
reverseSelect: '反向选中',
aria2TaskCheck: 'Aria2任务重启',
deselect: '取消选中',
selectAll: '全部选中',
downloadSelected: '下载所选',
downloadingSelected: '正在下载所选, 请稍后...',
injectCheckbox: '开关选择',
configError: '脚本配置中存在错误,请修改。',
alreadyKnowHowToUse: '我已知晓如何使用!!!',
notice: [
{ nodeType: 'br' },
'新增下载画质选择功能,开启后输入画质名称脚本会检查是否存在指定的画质是否可以下载,关闭后将默认下载最高画质。',
{ nodeType: 'br' },
'新增失效视频自动查找MMDfans缓存功能,功能默认开启,将在遇到无法解析的视频时尝试寻找MMDfans缓存'
],
useHelpForBase: `请认真阅读使用指南!`,
useHelpForInjectCheckbox: `开启“%#autoInjectCheckbox#%”以获得更好的体验!或等待加载出视频卡片后, 点击侧边栏中[%#injectCheckbox#%]开启下载选择框`,
useHelpForCheckDownloadLink: '开启“%#checkDownloadLink#%”功能会在下载视频前会检查视频简介以及评论,如果在其中发现疑似第三方网盘下载链接,将会弹出提示,您可以点击提示打开视频页面。',
useHelpForManualDownload: [
'使用手动下载功能需要提供视频ID, 如需批量手动下载请提供使用“|”分割的视频ID。',
{ nodeType: 'br' },
'例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L',
{ nodeType: 'br' },
'或提供符合以下格式对象的数组json字符串',
{ nodeType: 'br' },
'{ key: string, value: { Title?: string, Alias?: string, Author?: string } }',
{ nodeType: 'br' },
'例如: ',
{ nodeType: 'br' },
'[{ key: "AeGUIRO2D5vQ6F", value: { Title: "237知更鸟", Alias: "骑着牛儿追织女", Author: "user1528210" } },{ key: "qQsUMJa19LcK3L", value: { Title: "Mika Automotive Extradimensional", Alias: "Temptation’s_Symphony", Author: "temptations_symphony" } }]'
],
useHelpForBugreport: [
'反馈遇到的BUG、使用问题等请前往: ',
{
nodeType: 'a',
childs: 'Github',
attributes: {
href: 'https://github.com/dawn-lc/IwaraDownloadTool/'
}
}
],
tryRestartingDownload: '→ 点击此处重新下载 ←',
tryReparseDownload: '→ 点击此处重新解析 ←',
cdnCacheFinded: '→ 进入 MMD Fans 缓存页面 ←',
openVideoLink: '→ 进入视频页面 ←',
pushTaskSucceed: '推送下载任务成功!',
connectionTest: '连接测试',
settingsCheck: '配置检查',
createTask: '创建任务',
downloadPathError: '下载路径错误!',
browserDownloadModeError: '请启用脚本管理器的浏览器API下载模式!',
downloadQualityError: '未找到指定的画质下载地址!',
findedDownloadLink: '发现疑似第三方网盘下载地址!',
allCompleted: '全部解析完成!',
parsingProgress: '解析进度: ',
manualDownloadTips: '单独下载请直接在此处输入视频ID, 批量下载请提供使用“|”分割的视频ID, 例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L\r\n或提供符合以下格式对象的数组json字符串\r\n{ key: string, value: { Title?: string, Alias?: string, Author?: string } }\r\n例如: \r\n[{ key: "AeGUIRO2D5vQ6F", value: { Title: "237知更鸟", Alias: "骑着牛儿追织女", Author: "user1528210" } },{ key: "qQsUMJa19LcK3L", value: { Title: "Mika Automotive Extradimensional", Alias: "Temptation’s_Symphony", Author: "temptations_symphony" } }]',
externalVideo: `非本站视频`,
noAvailableVideoSource: '没有可供下载的视频源',
videoSourceNotAvailable: '视频源地址不可用',
getVideoSourceFailed: '获取视频源失败',
downloadFailed: '下载失败!',
downloadThisFailed: '未找到可供下载的视频!',
pushTaskFailed: '推送下载任务失败!',
parsingFailed: '视频信息解析失败!',
autoFollowFailed: '自动关注视频作者失败!',
autoLikeFailed: '自动点赞视频失败!',
};
this.en = {
appName: 'Iwara Download Tool',
language: 'Language:',
downloadPath: 'Download to:',
downloadProxy: 'Download proxy:',
rename: 'Rename:',
save: 'Save',
ok: 'OK',
on: 'On',
off: 'Off',
switchDebug: 'Debug mode:',
downloadType: 'Download type:',
configurationIncompatible: 'An incompatible configuration file was detected, please reconfigure!',
browserDownload: 'Browser download',
iwaraDownloaderDownload: 'iwaraDownloader download',
checkDownloadLink: 'High-quality download link check:',
downloadThis: 'Download this video',
autoInjectCheckbox: 'Auto inject selection',
variable: 'Available variables:',
downloadTime: 'Download time ',
uploadTime: 'Upload time ',
example: 'Example:',
result: 'Result:',
loadingCompleted: 'Loading completed',
settings: 'Open settings',
manualDownload: 'Manual download',
reverseSelect: 'Reverse select',
deselect: 'Deselect',
selectAll: 'Select all',
downloadSelected: 'Download selected',
downloadingSelected: 'Downloading selected, please wait...',
injectCheckbox: 'Switch selection',
configError: 'There is an error in the script configuration, please modify it.',
alreadyKnowHowToUse: 'I\'m already aware of how to use it!!!',
useHelpForInjectCheckbox: "After the video card is loaded, click [%#injectCheckbox#%] in the sidebar to enable the download checkbox",
useHelpForCheckDownloadLink: "Before downloading the video, the video introduction and comments will be checked. If a suspected third-party download link is found in them, a prompt will pop up. You can click the prompt to open the video page.",
useHelpForManualDownload: "Manual download requires you to provide a video ID! \r\nIf you need to batch download, please use '|' to separate IDs. For example:A|B|C...",
useHelpForBugreport: [
'Report bugs: ',
{
nodeType: 'a',
childs: 'Guthub',
attributes: {
href: 'https://github.com/dawn-lc/IwaraDownloadTool/issues/new/choose'
}
}
],
downloadFailed: 'Download failed!',
tryRestartingDownload: '→ Click here to restrat ←',
tryReparseDownload: '→ Click here to reparse ←',
openVideoLink: '→ Enter video page ←',
pushTaskFailed: 'Failed to push download task!',
pushTaskSucceed: 'Pushed download task successfully!',
connectionTest: 'Connection test',
settingsCheck: 'Configuration check',
parsingFailed: 'Video information parsing failed!',
createTask: 'Create task',
downloadPathError: 'Download path error!',
browserDownloadModeError: "Please enable the browser API download mode of the script manager!",
downloadQualityError: "No original painting download address!",
findedDownloadLink: "Found suspected high-quality download link!",
allCompleted: "All parsing completed!",
parsingProgress: "Parsing progress:",
manualDownloadTips: "Please enter the video ID you want to download! \r\nIf you need to batch download, please use '|' to separate IDs. For example:A|B|C...",
externalVideo: `Non-site video`,
getVideoSourceFailed: `Failed to get video source`,
noAvailableVideoSource: `No available video source`,
videoSourceNotAvailable: `Video source address not available`,
};
}
}
class Config {
constructor() {
this.language = language();
this.autoFollow = false;
this.autoLike = false;
this.autoInjectCheckbox = true;
this.checkDownloadLink = true;
this.checkPriority = true;
this.downloadPriority = 'Source';
this.downloadType = DownloadType.Others;
this.downloadPath = '/Iwara/%#AUTHOR#%/%#TITLE#%[%#ID#%].mp4';
this.downloadProxy = '';
this.aria2Path = 'http://127.0.0.1:6800/jsonrpc';
this.aria2Token = '';
this.iwaraDownloaderPath = 'http://127.0.0.1:6800/jsonrpc';
this.iwaraDownloaderToken = '';
this.priority = {
'Source': 100,
'540': 99,
'360': 98,
'preview': 1
};
let body = new Proxy(this, {
get: function (target, property) {
if (property === 'configChange') {
return target.configChange;
}
let value = GM_getValue(property, target[property]);
GM_getValue('isDebug') && console.log(`get: ${property} ${getString(value)}`);
return value;
},
set: function (target, property, value) {
if (property === 'configChange') {
target.configChange = value;
return true;
}
GM_setValue(property, value);
GM_getValue('isDebug') && console.log(`set: ${property} ${getString(value)}`);
target.configChange(property);
return true;
}
});
GM_listValues().forEach((value) => {
GM_addValueChangeListener(value, (name, old_value, new_value, remote) => {
GM_getValue('isDebug') && console.log(`$Is Remote: ${remote} Change Value: ${name}`);
if (remote && !isNull(body.configChange))
body.configChange(name);
});
});
return body;
}
async check() {
if (await localPathCheck()) {
switch (this.downloadType) {
case DownloadType.Aria2:
return await aria2Check();
case DownloadType.IwaraDownloader:
return await iwaraDownloaderCheck();
case DownloadType.Browser:
return await EnvCheck();
default:
break;
}
return true;
}
else {
return false;
}
}
}
class configEdit {
constructor(config) {
this.target = config;
this.target.configChange = (item) => { this.configChange.call(this, item); };
this.interfacePage = renderNode({
nodeType: 'p'
});
let save = renderNode({
nodeType: 'button',
className: 'closeButton',
childs: '%#save#%',
attributes: {
title: i18n[language()].save
},
events: {
click: async () => {
save.disabled = !save.disabled;
if (await this.target.check()) {
unsafeWindow.location.reload();
}
save.disabled = !save.disabled;
}
}
});
this.interface = renderNode({
nodeType: 'div',
attributes: {
id: 'pluginConfig'
},
childs: [
{
nodeType: 'div',
className: 'main',
childs: [
{
nodeType: 'h2',
childs: '%#appName#%'
},
{
nodeType: 'label',
childs: [
'%#language#% ',
{
nodeType: 'input',
className: 'inputRadioLine',
attributes: Object.assign({
name: 'language',
type: 'text',
value: this.target.language
}),
events: {
change: (event) => {
this.target.language = event.target.value;
}
}
}
]
},
this.downloadTypeSelect(),
this.interfacePage,
this.switchButton('checkPriority'),
this.switchButton('checkDownloadLink'),
this.switchButton('autoFollow'),
this.switchButton('autoLike'),
this.switchButton('autoInjectCheckbox'),
this.switchButton('isDebug', GM_getValue, (name, e) => { GM_setValue(name, e.target.checked); }, false),
]
},
save
]
});
}
switchButton(name, get, set, defaultValue) {
let button = renderNode({
nodeType: 'p',
className: 'inputRadioLine',
childs: [
{
nodeType: 'label',
childs: `%#${name}#%`,
attributes: {
for: name
}
}, {
nodeType: 'input',
className: 'switch',
attributes: {
type: 'checkbox',
name: name,
},
events: {
change: (e) => {
if (set !== undefined) {
set(name, e);
return;
}
else {
this.target[name] = e.target.checked;
}
}
}
}
]
});
button.querySelector(`[name='${name}']`).checked = get !== undefined ? get(name, defaultValue) : this.target[name] ?? defaultValue ?? false;
return button;
}
inputComponent(name, type, get, set) {
return {
nodeType: 'label',
childs: [
`%#${name}#% `,
{
nodeType: 'input',
attributes: Object.assign({
name: name,
type: type ?? 'text',
value: get !== undefined ? get(name) : this.target[name]
}),
events: {
change: (e) => {
if (set !== undefined) {
set(name, e);
return;
}
else {
this.target[name] = e.target.value;
}
}
}
}
]
};
}
downloadTypeSelect() {
let select = renderNode({
nodeType: 'p',
className: 'inputRadioLine',
childs: [
`%#downloadType#%`,
{
nodeType: 'select',
childs: Object.keys(DownloadType).filter((i) => isNaN(Number(i))).map((i) => renderNode({
nodeType: 'option',
childs: i
})),
attributes: {
name: 'downloadType'
},
events: {
change: (e) => {
this.target.downloadType = e.target.selectedIndex;
}
}
}
]
});
select.selectedIndex = Number(this.target.downloadType);
return select;
}
configChange(item) {
switch (item) {
case 'downloadType':
this.interface.querySelector(`[name=${item}]`).selectedIndex = Number(this.target.downloadType);
this.pageChange();
break;
case 'checkPriority':
this.pageChange();
break;
default:
let element = this.interface.querySelector(`[name=${item}]`);
if (element) {
switch (element.type) {
case 'radio':
element.value = this.target[item];
break;
case 'checkbox':
element.checked = this.target[item];
break;
case 'text':
case 'password':
element.value = this.target[item];
break;
default:
break;
}
}
break;
}
}
pageChange() {
while (this.interfacePage.hasChildNodes()) {
this.interfacePage.removeChild(this.interfacePage.firstChild);
}
let variableInfo = renderNode({
nodeType: 'a',
childs: '%#variable#%',
attributes: {
href: 'https://github.com/dawn-lc/IwaraDownloadTool#路径可用变量'
}
});
let downloadConfigInput = [
variableInfo,
renderNode(this.inputComponent('downloadPath')),
renderNode(this.inputComponent('downloadProxy'))
];
let aria2ConfigInput = [
renderNode(this.inputComponent('aria2Path')),
renderNode(this.inputComponent('aria2Token', 'password'))
];
let iwaraDownloaderConfigInput = [
renderNode(this.inputComponent('iwaraDownloaderPath')),
renderNode(this.inputComponent('iwaraDownloaderToken', 'password'))
];
let BrowserConfigInput = [
variableInfo,
renderNode(this.inputComponent('downloadPath'))
];
switch (this.target.downloadType) {
case DownloadType.Aria2:
downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
aria2ConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
break;
case DownloadType.IwaraDownloader:
downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
iwaraDownloaderConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
break;
default:
BrowserConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
break;
}
if (this.target.checkPriority) {
originalNodeAppendChild.call(this.interfacePage, renderNode(this.inputComponent('downloadPriority')));
}
}
inject() {
if (!unsafeWindow.document.querySelector('#pluginConfig')) {
originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
this.configChange('downloadType');
}
}
}
class VideoInfo {
constructor(info) {
if (!isNull(info)) {
if (!isNull(info.Title) && !info.Title.isEmpty())
this.Title = info.Title;
if (!isNull(info.Alias) && !info.Alias.isEmpty())
this.Alias = info.Alias;
if (!isNull(info.Author) && !info.Author.isEmpty())
this.Author = info.Author;
}
return this;
}
async init(ID, InfoSource) {
try {
this.ID = ID;
if (isNull(InfoSource)) {
config.authorization = `Bearer ${await refreshToken()}`;
}
let VideoInfoSource = InfoSource ?? await (await fetch(`https://api.iwara.tv/video/${this.ID}`, {
headers: await getAuth()
})).json();
if (VideoInfoSource.id === undefined) {
let cache = await db.videos.where('ID').equals(this.ID).toArray();
if (cache.any()) {
Object.assign(this, cache.pop());
}
let cdnCache = await db.caches.where('ID').equals(this.ID).toArray();
if (!cdnCache.any()) {
let query = prune({
author: this.Alias ?? this.Author,
title: this.Title
});
for (const key in query) {
for (let i of [...new DOMParser().parseFromString(await (await fetch(`https://mmdfans.net/?query=${encodeURIComponent(`${key}:${query[key]}`)}`)).text(), "text/xml").querySelectorAll('.mdui-col > a')]) {
let imgID = i.querySelector('.mdui-grid-tile > img')?.src?.toURL()?.pathname?.split('/')?.pop()?.trimTail('.jpg');
await db.caches.put(i.href, imgID);
}
}
}
cdnCache = await db.caches.where('ID').equals(this.ID).toArray();
if (cdnCache.any()) {
let toast = newToast(ToastType.Warn, {
node: toastNode([
`${this.Title}[${this.ID}] %#parsingFailed#%`,
{ nodeType: 'br' },
`%#cdnCacheFinded#%`
], '%#createTask#%'),
onClick() {
GM_openInTab(cdnCache.pop(), { active: false, insert: true, setParent: true });
toast.hideToast();
},
});
toast.showToast();
let button = unsafeWindow.document.querySelector(`.selectButton[videoid="${this.ID}"]`);
button && button.checked && button.click();
selectList.del(this.ID);
this.State = false;
return this;
}
throw new Error(i18n[language()].parsingFailed.toString());
}
this.ID = VideoInfoSource.id;
this.Title = ((VideoInfoSource.title ?? this.Title).normalize('NFKC').replace(/^\.|[\\\\/:*?\"<>|]/img, '_')).truncate(128);
this.External = !isNull(VideoInfoSource.embedUrl) && !VideoInfoSource.embedUrl.isEmpty();
this.AuthorID = VideoInfoSource.user.id;
this.Following = VideoInfoSource.user.following;
this.Liked = VideoInfoSource.liked;
this.Friend = VideoInfoSource.user.friend;
this.Private = VideoInfoSource.private;
this.Alias = VideoInfoSource.user.name;
this.Author = VideoInfoSource.user.username;
this.UploadTime = new Date(VideoInfoSource.createdAt);
this.Tags = VideoInfoSource.tags;
this.Comments = `${VideoInfoSource.body}\n`;
this.ExternalUrl = VideoInfoSource.embedUrl;
await db.videos.put(this);
if (!isNull(InfoSource)) {
return this;
}
if (this.External) {
throw new Error(i18n[language()].externalVideo.toString());
}
const getCommentData = async (commentID = null, page = 0) => {
return await (await fetch(`https://api.iwara.tv/video/${this.ID}/comments?page=${page}${!isNull(commentID) && !commentID.isEmpty() ? '&parent=' + commentID : ''}`, { headers: await getAuth() })).json();
};
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.prune();
};
this.Comments += `${(await getCommentDatas()).map(i => i.body).join('\n')}`.normalize('NFKC');
this.FileName = VideoInfoSource.file.name;
this.Size = VideoInfoSource.file.size;
let VideoFileSource = (await (await fetch(VideoInfoSource.fileUrl, { headers: await getAuth(VideoInfoSource.fileUrl) })).json()).sort((a, b) => (!isNull(config.priority[b.name]) ? config.priority[b.name] : 0) - (!isNull(config.priority[a.name]) ? config.priority[a.name] : 0));
if (isNull(VideoFileSource) || !(VideoFileSource instanceof Array) || VideoFileSource.length < 1) {
throw new Error(i18n[language()].getVideoSourceFailed.toString());
}
this.DownloadQuality = config.checkPriority ? config.downloadPriority : VideoFileSource[0].name;
let fileList = VideoFileSource.filter(x => x.name === this.DownloadQuality);
if (!fileList.any())
throw new Error(i18n[language()].noAvailableVideoSource.toString());
let Source = fileList[Math.floor(Math.random() * fileList.length)].src.download;
if (isNull(Source) || Source.isEmpty())
throw new Error(i18n[language()].videoSourceNotAvailable.toString());
this.DownloadUrl = decodeURIComponent(`https:${Source}`);
this.State = true;
await db.videos.put(this);
return this;
}
catch (error) {
let data = this;
let toast = newToast(ToastType.Error, {
node: toastNode([
`${this.Title}[${this.ID}] %#parsingFailed#%`,
{ nodeType: 'br' },
`${getString(error)}`,
{ nodeType: 'br' },
this.External ? `%#openVideoLink#%` : `%#tryReparseDownload#%`
], '%#createTask#%'),
async onClick() {
toast.hideToast();
if (data.External) {
GM_openInTab(data.ExternalUrl, { active: false, insert: true, setParent: true });
}
else {
pustDownloadTask(await new VideoInfo(data).init(data.ID));
}
},
});
toast.showToast();
let button = unsafeWindow.document.querySelector(`.selectButton[videoid*="${this.ID}"i]`);
button && button.checked && button.click();
selectList.del(this.ID);
this.State = false;
return this;
}
}
}
class Database extends Dexie {
constructor() {
super("VideoDatabase");
this.version(2).stores({
videos: 'ID',
caches: 'ID'
});
this.videos = this.table("videos");
this.caches = this.table("caches");
}
}
class menu {
constructor() {
this.interfacePage = renderNode({
nodeType: 'ul'
});
this.interface = renderNode({
nodeType: 'div',
attributes: {
id: 'pluginMenu'
},
childs: this.interfacePage
});
}
button(name, click) {
return renderNode(prune({
nodeType: 'li',
childs: `%#${name}#%`,
events: {
click: (event) => {
click(name, event);
event.stopPropagation();
return false;
}
}
}));
}
pageChange(pageType) {
while (this.interfacePage.hasChildNodes()) {
this.interfacePage.removeChild(this.interfacePage.firstChild);
}
let manualDownloadButton = this.button('manualDownload', (name, event) => {
addDownloadTask();
});
let settingsButton = this.button('settings', (name, event) => {
editConfig.inject();
});
let baseButtons = [manualDownloadButton, settingsButton];
let injectCheckboxButton = this.button('injectCheckbox', (name, event) => {
if (unsafeWindow.document.querySelector('.selectButton')) {
unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
element.remove();
});
}
else {
unsafeWindow.document.querySelectorAll(`.videoTeaser`).forEach((element) => {
injectCheckbox(element, compatible);
});
}
});
let selectAllButton = this.button('selectAll', (name, event) => {
unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
let button = element;
!button.checked && button.click();
});
});
let reverseSelectButton = this.button('reverseSelect', (name, event) => {
unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
element.click();
});
});
let deselectButton = this.button('deselect', (name, event) => {
unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
let button = element;
button.checked && button.click();
});
});
let downloadSelectedButton = this.button('downloadSelected', (name, event) => {
analyzeDownloadTask();
newToast(ToastType.Info, {
text: `%#${name}#%`,
close: true
}).showToast();
});
let selectButtons = [injectCheckboxButton, selectAllButton, reverseSelectButton, deselectButton, downloadSelectedButton];
let downloadThisButton = this.button('downloadThis', async (name, event) => {
let ID = unsafeWindow.location.href.trim().split('//').pop().split('/')[2];
let Title = unsafeWindow.document.querySelector('.page-video__details')?.childNodes[0]?.textContent;
let videoInfo = await (new VideoInfo(prune({ Title: Title, }))).init(ID);
videoInfo.State && await pustDownloadTask(videoInfo);
});
let aria2TaskCheckButton = this.button('aria2TaskCheck', (name, event) => {
aria2TaskCheck();
});
GM_getValue('isDebug') && originalNodeAppendChild.call(this.interfacePage, aria2TaskCheckButton);
switch (pageType) {
case PageType.Video:
originalNodeAppendChild.call(this.interfacePage, downloadThisButton);
selectButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
break;
case PageType.Search:
case PageType.Profile:
case PageType.Home:
case PageType.VideoList:
case PageType.Subscriptions:
case PageType.Playlist:
case PageType.Favorites:
case PageType.Account:
selectButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
break;
case PageType.Page:
case PageType.Forum:
case PageType.Image:
case PageType.ImageList:
case PageType.ForumSection:
case PageType.ForumThread:
default:
baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
break;
}
}
inject() {
if (!unsafeWindow.document.querySelector('#pluginMenu')) {
new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type !== 'childList' || mutation.addedNodes.length < 1) {
continue;
}
let pages = [...mutation.addedNodes].filter(i => isElement(i)).filter(i => i.classList.contains('page'));
if (pages.length < 1) {
continue;
}
if (unsafeWindow.location.pathname.toLowerCase().split('/').pop() === 'search') {
this.pageChange(PageType.Search);
continue;
}
let page = pages.find(i => i.classList.length > 1);
if (!page) {
continue;
}
this.pageChange(page.classList[1].split('-').pop());
}
}).observe(unsafeWindow.document.getElementById('app'), { childList: true, subtree: true });
originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
this.pageChange(PageType.Page);
}
}
}
GM_addStyle(GM_getResourceText('toastify-css'));
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: 2147483644;
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(2%, -50%);
}
#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;
}
#pluginConfig {
color: var(--text);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
z-index: 2147483646;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#pluginConfig .main {
background-color: var(--body);
padding: 24px;
margin: 10px;
overflow-y: auto;
width: 400px;
}
@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 button {
background-color: blue;
}
#pluginConfig button[disabled] {
background-color: darkgray;
cursor: not-allowed;
}
#pluginConfig p {
display: flex;
flex-direction: column;
}
#pluginConfig p label{
display: flex;
flex-direction: column;
margin: 5px;
}
#pluginConfig .inputRadioLine {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
}
#pluginConfig input[type="text"], #pluginConfig input[type="password"] {
outline: none;
border-top: none;
border-right: none;
border-left: none;
border-image: initial;
border-bottom: 1px solid var(--muted);
line-height: 1;
height: 30px;
box-sizing: border-box;
width: 100%;
background-color: var(--body);
color: var(--text);
}
#pluginConfig input[type='checkbox'].switch{
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
position: relative;
width: 40px;
height: 20px;
background: #ccc;
border-radius: 10px;
transition: border-color .2s, background-color .2s;
}
#pluginConfig input[type='checkbox'].switch::after {
content: '';
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 50%;
background: #fff;
box-shadow: 0, 0, 2px, #999;
transition: .2s;
top: 2px;
position: absolute;
right: 55%;
}
#pluginConfig input[type='checkbox'].switch:checked {
background: rgb(19, 206, 102);
}
#pluginConfig input[type='checkbox'].switch:checked::after {
content: '';
position: absolute;
right: 2px;
top: 2px;
}
#pluginOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
z-index: 2147483645;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#pluginOverlay .main {
color: white;
font-size: 24px;
width: 60%;
background-color: rgba(64, 64, 64, 0.75);
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 {
width: 32px;
height: 32px;
margin: 0 4px 0 0;
padding: 0;
}
#pluginOverlay .checkbox-container {
display: flex;
align-items: center;
margin: 0 0 10px 0;
}
#pluginOverlay .checkbox-label {
color: white;
font-size: 32px;
font-weight: bold;
margin-left: 10px;
display: flex;
align-items: center;
}
.selectButton {
position: absolute;
width: 38px;
height: 38px;
bottom: 24px;
right: 0px;
cursor:pointer;
}
.selectButtonCompatible {
width: 32px;
height: 32px;
bottom: 0px;
right: 4px;
transform: translate(-50%, -50%);
margin: 0;
padding: 0;
cursor:pointer;
}
.toastify h3 {
margin: 0 0 10px 0;
}
.toastify p {
margin: 0 ;
}
`);
var mouseTarget = null;
var compatible = navigator.userAgent.toLowerCase().includes('firefox');
var i18n = new I18N();
var config = new Config();
var db = new Database();
var selectList = new SyncDictionary('selectList');
var editConfig = new configEdit(config);
var pluginMenu = new menu();
function getPlayload(authorization) {
return JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(authorization.split(' ').pop().split('.')[1]))));
}
const modifyFetch = async (input, init) => {
GM_getValue('isDebug') && console.log(`Fetch ${input}`);
let url = (input instanceof Request ? input.url : input instanceof URL ? input.href : input).toURL();
if (url.hostname.includes('sentry.io'))
return undefined;
if (!isNull(init) && !isNull(init.headers) && !isStringTupleArray(init.headers)) {
let authorization = null;
if (init.headers instanceof Headers) {
authorization = init.headers.has('Authorization') ? init.headers.get('Authorization') : null;
}
else {
for (const key in init.headers) {
if (key.toLowerCase() === "authorization") {
authorization = init.headers[key];
break;
}
}
}
if (!isNull(authorization) && authorization !== config.authorization) {
let playload = getPlayload(authorization);
if (playload['type'] === 'refresh_token') {
GM_getValue('isDebug') && console.log(`refresh_token: ${authorization.split(' ').pop()}`);
isNull(localStorage.getItem('token')) && localStorage.setItem('token', authorization.split(' ').pop());
}
if (playload['type'] === 'access_token') {
config.authorization = `Bearer ${authorization.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()}`);
}
}
}
return new Promise((resolve, reject) => {
originalFetch(input, init).then(async (response) => {
if (url.hostname === 'api.iwara.tv' && !url.pathname.isEmpty()) {
let path = url.pathname.split('/').slice(1);
switch (path[0]) {
case 'videos':
(await response.clone().json()).results.forEach(info => new VideoInfo().init(info.id, info));
break;
default:
break;
}
}
resolve(response);
}).catch((err) => {
reject(err);
});
});
};
unsafeWindow.fetch = modifyFetch;
unsafeWindow.EventTarget.prototype.addEventListener = function (type, listener, options) {
originalAddEventListener.call(this, type, listener, options);
};
async function refreshToken() {
let refresh = config.authorization;
try {
refresh = (await (await fetch(`https://api.iwara.tv/user/token`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})).json())['accessToken'];
}
catch (error) {
console.warn(`Refresh token error: ${getString(error)}`);
}
return refresh;
}
async function getXVersion(urlString) {
let url = urlString.toURL();
const data = new TextEncoder().encode(`${url.pathname.split("/").pop()}_${url.searchParams.get('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('');
}
async function getAuth(url) {
return Object.assign({
'Cooike': unsafeWindow.document.cookie,
'Authorization': config.authorization
}, !isNull(url) && !url.isEmpty() ? { 'X-Version': await getXVersion(url) } : {});
}
async function addDownloadTask() {
let textArea = renderNode({
nodeType: "textarea",
attributes: {
placeholder: i18n[language()].manualDownloadTips,
style: 'margin-bottom: 10px;',
rows: "16",
cols: "96"
}
});
let body = renderNode({
nodeType: "div",
attributes: {
id: "pluginOverlay"
},
childs: [
textArea,
{
nodeType: "button",
events: {
click: (e) => {
if (!isNull(textArea.value) && !textArea.value.isEmpty()) {
try {
analyzeDownloadTask(new Dictionary(JSON.parse(textArea.value)));
}
catch (error) {
let IDList = new Dictionary();
textArea.value.split('|').map(ID => IDList.set(ID, {}));
analyzeDownloadTask(IDList);
}
}
body.remove();
}
},
childs: "确认"
}
]
});
unsafeWindow.document.body.appendChild(body);
}
async function analyzeDownloadTask(list = selectList) {
let size = list.size;
let node = renderNode({
nodeType: 'p',
childs: `%#parsingProgress#%[${list.size}/${size}]`
});
let start = newToast(ToastType.Info, {
node: node,
duration: -1
});
start.showToast();
if (GM_getValue('isDebug') && config.downloadType === DownloadType.Aria2) {
let completed = (await aria2API('aria2.tellStopped', [0, 2048, [
'gid',
'status',
'files',
'errorCode',
'bittorrent'
]])).result.filter((task) => isNull(task.bittorrent) && (task.status === 'complete' || task.errorCode === '13')).map((task) => aria2TaskExtractVideoID(task)).filter(Boolean);
for (let key of list.keys().intersect(completed)) {
let button = unsafeWindow.document.querySelector(`.selectButton[videoid="${key}"]`);
button && button.checked && button.click();
list.del(key);
node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.size}/${size}]`;
}
}
for (let key of list.keys()) {
let button = unsafeWindow.document.querySelector(`.selectButton[videoid="${key}"]`);
let videoInfo = await (new VideoInfo(list.get(key))).init(key);
videoInfo.State && await pustDownloadTask(videoInfo);
button && button.checked && button.click();
list.del(key);
node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.size}/${size}]`;
}
start.hideToast();
if (size != 1) {
let completed = newToast(ToastType.Info, {
text: `%#allCompleted#%`,
duration: -1,
close: true,
onClick() {
completed.hideToast();
}
});
completed.showToast();
}
}
function checkIsHaveDownloadLink(comment) {
if (!config.checkDownloadLink || isNull(comment) || comment.isEmpty()) {
return false;
}
return [
'iwara.zip',
'pan.baidu',
'/s/',
'mega.nz',
'drive.google.com',
'aliyundrive',
'uploadgig',
'katfile',
'storex',
'subyshare',
'rapidgator',
'filebe',
'filespace',
'mexa.sh',
'mexashare',
'mx-sh.net',
'uploaded.',
'icerbox',
'alfafile',
'1drv.ms',
'onedrive.',
'gofile.io',
'workupload.com',
'pixeldrain.',
'gigafile.nu'
].filter(i => comment.toLowerCase().includes(i)).any();
}
function toastNode(body, title) {
return renderNode({
nodeType: 'div',
childs: [
!isNull(title) && !title.isEmpty() ? {
nodeType: 'h3',
childs: `%#appName#% - ${title}`
} : {
nodeType: 'h3',
childs: '%#appName#%'
},
{
nodeType: 'p',
childs: body
}
]
});
}
function getTextNode(node) {
return node.nodeType === Node.TEXT_NODE
? node.textContent || ''
: node.nodeType === Node.ELEMENT_NODE
? Array.from(node.childNodes)
.map(getTextNode)
.join('')
: '';
}
function newToast(type, params) {
const logFunc = {
[ToastType.Warn]: console.warn,
[ToastType.Error]: console.error,
[ToastType.Log]: console.log,
[ToastType.Info]: console.info,
}[type] || console.log;
params = Object.assign({
newWindow: true,
gravity: 'top',
position: 'left',
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))'
}
}, !isNull(params) && params);
if (!isNull(params.text)) {
params.text = params.text.replaceVariable(i18n[language()]).toString();
}
logFunc((!isNull(params.text) ? params.text : !isNull(params.node) ? getTextNode(params.node) : 'undefined').replaceVariable(i18n[language()]));
return Toastify(params);
}
async function pustDownloadTask(videoInfo) {
if (!videoInfo.State) {
return;
}
if (config.autoFollow && !videoInfo.Following) {
if ((await fetch(`https://api.iwara.tv/user/${videoInfo.AuthorID}/followers`, {
method: 'POST',
headers: await getAuth()
})).status !== 201)
newToast(ToastType.Warn, { text: `${videoInfo.Alias} %#autoFollowFailed#%`, close: true }).showToast();
}
if (config.autoLike && !videoInfo.Liked) {
if ((await fetch(`https://api.iwara.tv/video/${videoInfo.ID}/like`, {
method: 'POST',
headers: await getAuth()
})).status !== 201)
newToast(ToastType.Warn, { text: `${videoInfo.Title} %#autoLikeFailed#%`, close: true }).showToast();
}
if (config.checkDownloadLink && checkIsHaveDownloadLink(videoInfo.Comments)) {
let toast = newToast(ToastType.Warn, {
node: toastNode([
`${videoInfo.Title}[${videoInfo.ID}] %#findedDownloadLink#%`,
{ nodeType: 'br' },
`%#openVideoLink#%`
], '%#createTask#%'),
onClick() {
GM_openInTab(`https://www.iwara.tv/video/${videoInfo.ID}`, { active: false, insert: true, setParent: true });
toast.hideToast();
}
});
toast.showToast();
return;
}
if (config.checkPriority && videoInfo.DownloadQuality !== config.downloadPriority) {
let toast = newToast(ToastType.Warn, {
node: toastNode([
`${videoInfo.Title}[${videoInfo.ID}] %#downloadQualityError#%`,
{ nodeType: 'br' },
`%#tryReparseDownload#%`
], '%#createTask#%'),
async onClick() {
toast.hideToast();
await pustDownloadTask(await new VideoInfo(videoInfo).init(videoInfo.ID));
}
});
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.replaceAll('//', '/').replaceAll('\\\\', '/').match(/^([a-zA-Z]:)?[\/\\]?([^\/\\]+[\/\\])*([^\/\\]+\.\w+)$/);
if (isNull(matchPath))
throw new Error(`%#downloadPathError#%["${path}"]`);
try {
return {
fullPath: matchPath[0],
drive: matchPath[1] || '',
filename: matchPath[3]
};
}
catch (error) {
throw new Error(`%#downloadPathError#% ["${matchPath.join(',')}"]`);
}
}
async function EnvCheck() {
try {
if (GM_info.downloadMode !== 'browser') {
GM_getValue('isDebug') && console.log(GM_info);
throw new Error('%#browserDownloadModeError#%');
}
}
catch (error) {
let toast = newToast(ToastType.Error, {
node: toastNode([
`%#configError#%`,
{ nodeType: 'br' },
getString(error)
], '%#settingsCheck#%'),
position: 'center',
onClick() {
toast.hideToast();
}
});
toast.showToast();
return false;
}
return true;
}
async function localPathCheck() {
try {
let pathTest = analyzeLocalPath(config.downloadPath);
for (const key in pathTest) {
if (!Object.prototype.hasOwnProperty.call(pathTest, key) || pathTest[key]) {
}
}
}
catch (error) {
let toast = newToast(ToastType.Error, {
node: toastNode([
`%#downloadPathError#%`,
{ nodeType: 'br' },
getString(error)
], '%#settingsCheck#%'),
position: 'center',
onClick() {
toast.hideToast();
}
});
toast.showToast();
return false;
}
return true;
}
async function aria2Check() {
try {
let res = await (await fetch(config.aria2Path, {
method: 'POST',
body: JSON.stringify({
'jsonrpc': '2.0',
'method': 'aria2.tellActive',
'id': UUID(),
'params': ['token:' + config.aria2Token]
})
})).json();
if (res.error) {
throw new Error(res.error.message);
}
}
catch (error) {
let toast = newToast(ToastType.Error, {
node: toastNode([
`Aria2 RPC %#connectionTest#%`,
{ nodeType: 'br' },
getString(error)
], '%#settingsCheck#%'),
position: 'center',
onClick() {
toast.hideToast();
}
});
toast.showToast();
return false;
}
return true;
}
async function iwaraDownloaderCheck() {
try {
let res = await (await fetch(config.iwaraDownloaderPath, {
method: 'POST',
body: JSON.stringify(prune({
'ver': GM_getValue('version', '0.0.0').split('.').map(i => Number(i)),
'code': 'State',
'token': config.iwaraDownloaderToken
}))
})).json();
if (res.code !== 0) {
throw new Error(res.msg);
}
}
catch (error) {
let toast = newToast(ToastType.Error, {
node: toastNode([
`IwaraDownloader RPC %#connectionTest#%`,
{ nodeType: 'br' },
getString(error)
], '%#settingsCheck#%'),
position: 'center',
onClick() {
toast.hideToast();
}
});
toast.showToast();
return false;
}
return true;
}
function aria2Download(videoInfo) {
(async function (id, author, name, uploadTime, info, tag, quality, alias, downloadUrl) {
let localPath = analyzeLocalPath(config.downloadPath.replaceVariable({
NowTime: new Date(),
UploadTime: uploadTime,
AUTHOR: author,
ID: id,
TITLE: name,
ALIAS: alias,
QUALITY: quality
}).trim());
let res = await aria2API('aria2.addUri', [
[downloadUrl],
prune({
'all-proxy': config.downloadProxy,
'out': localPath.filename,
'dir': localPath.fullPath.replace(localPath.filename, ''),
'referer': window.location.hostname,
'header': [
'Cookie:' + unsafeWindow.document.cookie
]
})
]);
console.log(`Aria2 ${name} ${JSON.stringify(res)}`);
newToast(ToastType.Info, {
node: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskSucceed#%`)
}).showToast();
}(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl));
}
function iwaraDownloaderDownload(videoInfo) {
(async function (videoInfo) {
let r = await (await fetch(config.iwaraDownloaderPath, {
method: 'POST',
body: JSON.stringify(prune({
'ver': GM_getValue('version', '0.0.0').split('.').map(i => Number(i)),
'code': 'add',
'token': config.iwaraDownloaderToken,
'data': {
'info': {
'name': videoInfo.Title,
'url': videoInfo.DownloadUrl,
'size': videoInfo.Size,
'source': videoInfo.ID,
'alias': videoInfo.Alias,
'author': videoInfo.Author,
'uploadTime': videoInfo.UploadTime,
'comments': videoInfo.Comments,
'tags': videoInfo.Tags,
'path': config.downloadPath.replaceVariable({
NowTime: new Date(),
UploadTime: videoInfo.UploadTime,
AUTHOR: videoInfo.Author,
ID: videoInfo.ID,
TITLE: videoInfo.Title,
ALIAS: videoInfo.Alias,
QUALITY: videoInfo.DownloadQuality
})
},
'option': {
'proxy': config.downloadProxy,
'cookies': unsafeWindow.document.cookie
}
}
}))
})).json();
if (r.code === 0) {
console.log(`${videoInfo.Title} %#pushTaskSucceed#% ${r}`);
newToast(ToastType.Info, {
node: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskSucceed#%`)
}).showToast();
}
else {
let toast = newToast(ToastType.Error, {
node: toastNode([
`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskFailed#% `,
{ nodeType: 'br' },
r.msg
], '%#iwaraDownloaderDownload#%'),
onClick() {
toast.hideToast();
}
});
toast.showToast();
}
}(videoInfo));
}
function othersDownload(videoInfo) {
(async function (ID, Author, Name, UploadTime, DownloadQuality, Alias, DownloadUrl) {
let filename = analyzeLocalPath(config.downloadPath.replaceVariable({
NowTime: new Date(),
UploadTime: UploadTime,
AUTHOR: Author,
ID: ID,
TITLE: Name,
ALIAS: Alias,
QUALITY: DownloadQuality
}).trim()).filename;
DownloadUrl.searchParams.set('download', filename);
GM_openInTab(DownloadUrl.href, { active: false, insert: true, setParent: true });
}(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl.toURL()));
}
function browserDownload(videoInfo) {
(async function (ID, Author, Name, UploadTime, Info, Tag, DownloadQuality, Alias, DownloadUrl) {
function browserDownloadError(error) {
let errorInfo = getString(Error);
if (!(error instanceof Error)) {
errorInfo = {
'not_enabled': `%#browserDownloadNotEnabled#%`,
'not_whitelisted': `%#browserDownloadNotWhitelisted#%`,
'not_permitted': `%#browserDownloadNotPermitted#%`,
'not_supported': `%#browserDownloadNotSupported#%`,
'not_succeeded': `%#browserDownloadNotSucceeded#% ${error.details ?? getString(error.details)}`
}[error.error] || `%#browserDownloadUnknownError#%`;
}
let toast = newToast(ToastType.Error, {
node: toastNode([
`${Name}[${ID}] %#downloadFailed#%`,
{ nodeType: 'br' },
errorInfo,
{ nodeType: 'br' },
`%#tryRestartingDownload#%`
], '%#browserDownload#%'),
async onClick() {
toast.hideToast();
await pustDownloadTask(videoInfo);
}
});
toast.showToast();
}
GM_download({
url: DownloadUrl,
saveAs: false,
name: config.downloadPath.replaceVariable({
NowTime: new Date(),
UploadTime: UploadTime,
AUTHOR: Author,
ID: ID,
TITLE: Name,
ALIAS: Alias,
QUALITY: DownloadQuality
}).trim(),
onerror: (err) => browserDownloadError(err),
ontimeout: () => browserDownloadError(new Error('%#browserDownloadTimeout#%'))
});
}(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl));
}
async function aria2API(method, params) {
return await (await fetch(config.aria2Path, {
headers: {
'accept': 'application/json',
'content-type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: method,
id: UUID(),
params: [`token:${config.aria2Token}`, ...params]
}),
method: 'POST'
})).json();
}
function aria2TaskExtractVideoID(task) {
if (isNull(task.files)) {
GM_getValue('isDebug') && console.log(`check aria2 task files fail! ${JSON.stringify(task)}`);
return null;
}
for (let index = 0; index < task.files.length; index++) {
const file = task.files[index];
if (isNull(file)) {
GM_getValue('isDebug') && console.log(`check aria2 task file fail! ${JSON.stringify(task.files)}`);
continue;
}
let videoID = analyzeLocalPath(file?.path)?.filename?.match(/\[([^\[\]]*)\](?=[^\[]*$)/g)?.pop()?.trimHead('[')?.trimTail(']');
if (isNull(videoID) || videoID.isEmpty()) {
GM_getValue('isDebug') && console.log(`check aria2 task videoID fail! ${JSON.stringify(file.path)}`);
continue;
}
return videoID;
}
return null;
}
async function aria2TaskCheck() {
let completed = (await aria2API('aria2.tellStopped', [0, 2048, [
'gid',
'status',
'files',
'errorCode',
'bittorrent'
]])).result.filter((task) => isNull(task.bittorrent) && (task.status === 'complete' || task.errorCode === '13')).map((task) => aria2TaskExtractVideoID(task)).filter(Boolean).map((i) => i.toLowerCase());
let active = await aria2API('aria2.tellActive', [[
'gid',
'downloadSpeed',
'files',
'bittorrent'
]]);
let needRestart = active.result.filter((i) => isNull(i.bittorrent) && !Number.isNaN(i.downloadSpeed) && Number(i.downloadSpeed) <= 1024);
for (let index = 0; index < needRestart.length; index++) {
const task = needRestart[index];
let videoID = aria2TaskExtractVideoID(task);
if (!isNull(videoID) && !videoID.isEmpty()) {
if (!completed.includes(videoID.toLowerCase())) {
let cache = (await db.videos.where('ID').equals(videoID).toArray()).pop();
let videoInfo = await (new VideoInfo(cache)).init(videoID);
videoInfo.State && await pustDownloadTask(videoInfo);
}
await aria2API('aria2.forceRemove', [task.gid]);
}
}
}
function injectCheckbox(element, compatible) {
let ID = element.querySelector('a.videoTeaser__thumbnail').href.toURL().pathname.split('/')[2];
let Name = element.querySelector('.videoTeaser__title')?.getAttribute('title').trim();
let Alias = element.querySelector('a.username')?.getAttribute('title');
let Author = element.querySelector('a.username')?.href.toURL().pathname.split('/').pop();
let node = compatible ? element : element.querySelector('.videoTeaser__thumbnail');
originalNodeAppendChild.call(node, renderNode({
nodeType: 'input',
attributes: Object.assign(selectList.has(ID) ? { checked: true } : {}, {
type: 'checkbox',
videoID: ID,
videoName: Name,
videoAlias: Alias,
videoAuthor: Author
}),
className: compatible ? ['selectButton', 'selectButtonCompatible'] : 'selectButton',
events: {
click: (event) => {
event.target.checked ? selectList.set(ID, {
Title: Name,
Alias: Alias,
Author: Author
}) : selectList.del(ID);
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
}
}));
}
function firstRun() {
console.log('First run config reset!');
GM_listValues().forEach(i => GM_deleteValue(i));
config = new Config();
editConfig = new configEdit(config);
let confirmButton = renderNode({
nodeType: 'button',
attributes: {
disabled: true,
title: i18n[language()].ok
},
childs: '%#ok#%',
events: {
click: () => {
GM_setValue('isFirstRun', false);
GM_setValue('version', GM_info.script.version);
unsafeWindow.document.querySelector('#pluginOverlay').remove();
editConfig.inject();
}
}
});
originalNodeAppendChild.call(unsafeWindow.document.body, renderNode({
nodeType: 'div',
attributes: {
id: 'pluginOverlay'
},
childs: [
{
nodeType: 'div',
className: 'main',
childs: [
{ nodeType: 'p', childs: '%#useHelpForBase#%' },
{ nodeType: 'p', childs: '%#useHelpForInjectCheckbox#%' },
{ nodeType: 'p', childs: '%#useHelpForCheckDownloadLink#%' },
{ nodeType: 'p', childs: i18n[language()].useHelpForManualDownload },
{ nodeType: 'p', childs: i18n[language()].useHelpForBugreport }
]
},
{
nodeType: 'div',
className: 'checkbox-container',
childs: {
nodeType: 'label',
className: ['checkbox-label', 'rainbow-text'],
childs: [{
nodeType: 'input',
className: 'checkbox',
attributes: {
type: 'checkbox',
name: 'agree-checkbox'
},
events: {
change: (event) => {
confirmButton.disabled = !event.target.checked;
}
}
}, '%#alreadyKnowHowToUse#%']
}
},
confirmButton
]
}));
}
async function main() {
if (GM_getValue('isFirstRun', true)) {
firstRun();
return;
}
if (!await config.check()) {
newToast(ToastType.Info, {
text: `%#configError#%`,
duration: 60 * 1000,
}).showToast();
editConfig.inject();
return;
}
GM_setValue('version', GM_info.script.version);
if (config.autoInjectCheckbox) {
Node.prototype.appendChild = function (node) {
if (node instanceof HTMLElement && node.classList.contains('videoTeaser')) {
injectCheckbox(node, compatible);
}
return originalNodeAppendChild.call(this, node);
};
}
new MutationObserver((m, o) => {
if (m.some(m => m.type === 'childList' && unsafeWindow.document.getElementById('app'))) {
pluginMenu.inject();
o.disconnect();
}
}).observe(unsafeWindow.document.body, { childList: true, subtree: true });
originalAddEventListener('mouseover', (event) => {
mouseTarget = event.target instanceof Element ? event.target : null;
});
unsafeWindow.document.addEventListener('keydown', function (e) {
if (e.code === 'Space' && !isNull(mouseTarget)) {
let element = findElement(mouseTarget, '.videoTeaser');
let button = element && (element.matches('.selectButton') ? element : element.querySelector('.selectButton'));
button && button.click();
button && e.preventDefault();
}
});
newToast(ToastType.Info, {
node: toastNode([
`加载完成`,
{ nodeType: 'br' },
`公告: `,
...i18n[language()].notice
]),
duration: 10000,
gravity: 'bottom',
position: 'center'
}).showToast();
}
if (new Version(GM_getValue('version', '0.0.0')).compare(new Version('3.2.5')) === VersionState.Low) {
GM_setValue('isFirstRun', true);
alert(i18n[language()].configurationIncompatible);
}
(unsafeWindow.document.body ? Promise.resolve() : new Promise(resolve => originalAddEventListener.call(unsafeWindow.document, "DOMContentLoaded", resolve))).then(main);
})();