Download videos from iwara.tv
// ==UserScript==
// @name Iwara Download Tool
// @description Download videos from iwara.tv
// @name:ja Iwara バッチダウンローダー
// @name:zh-CN Iwara 批量下载工具
// @description:ja Iwara 動画バッチをダウンロード
// @description:zh-CN 批量下载 Iwara 视频
// @icon https://www.google.com/s2/favicons?sz=64&domain=iwara.tv
// @namespace https://github.com/dawn-lc/
// @author dawn-lc
// @license Apache-2.0
// @copyright 2026, Dawnlc (https://dawnlc.me/)
// @supportURL https://github.com/IwaraEnhance/IwaraDownloadTool/issues
// @connect iwara.tv
// @connect *.iwara.*
// @connect localhost
// @connect 127.0.0.1
// @connect *
// @include *://*iwara*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_download
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_info
// @grant unsafeWindow
// @grant window.close
// @run-at document-start
// @noframes
// @version 3.3.132
// ==/UserScript==
"use strict";
(() => {
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res, err) => function __init() {
if (err) throw err[0];
try {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
} catch (e) {
throw err = [e], e;
}
};
var __commonJS = (cb, mod) => function __require() {
try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var originalFetch, originalHistoryPushState, originalHistoryReplaceState, originalNodeAppendChild, originalNodeRemoveChild, originalElementRemove, originalAddEventListener, originalRemoveEventListener, originalStorageSetItem, originalStorageRemoveItem, originalStorageClear, originalConsole;
var init_hijack = __esm({
"src/core/hijack.ts"() {
"use strict";
originalFetch = unsafeWindow.fetch;
originalHistoryPushState = unsafeWindow.history.pushState;
originalHistoryReplaceState = unsafeWindow.history.replaceState;
originalNodeAppendChild = unsafeWindow.Node.prototype.appendChild;
originalNodeRemoveChild = unsafeWindow.Node.prototype.removeChild;
originalElementRemove = unsafeWindow.Element.prototype.remove;
originalAddEventListener = unsafeWindow.EventTarget.prototype.addEventListener;
originalRemoveEventListener = unsafeWindow.EventTarget.prototype.removeEventListener;
originalStorageSetItem = unsafeWindow.Storage.prototype.setItem;
originalStorageRemoveItem = unsafeWindow.Storage.prototype.removeItem;
originalStorageClear = unsafeWindow.Storage.prototype.clear;
originalConsole = {
log: unsafeWindow.console.log.bind(unsafeWindow.console),
info: unsafeWindow.console.info.bind(unsafeWindow.console),
warn: unsafeWindow.console.warn.bind(unsafeWindow.console),
error: unsafeWindow.console.error.bind(unsafeWindow.console),
debug: unsafeWindow.console.debug.bind(unsafeWindow.console),
trace: unsafeWindow.console.trace.bind(unsafeWindow.console),
dir: unsafeWindow.console.dir.bind(unsafeWindow.console),
table: unsafeWindow.console.table?.bind(unsafeWindow.console)
};
}
});
var GM_KEY_IS_DEBUG, GM_KEY_IS_FIRST_RUN, GM_KEY_VERSION, GM_KEY_SELECT_LIST, LS_KEY_TOKEN, LS_KEY_ACCESS_TOKEN, LS_KEY_RATING;
var init_constants = __esm({
"src/core/constants.ts"() {
"use strict";
GM_KEY_IS_DEBUG = "isDebug";
GM_KEY_IS_FIRST_RUN = "isFirstRun";
GM_KEY_VERSION = "version";
GM_KEY_SELECT_LIST = "selectList";
LS_KEY_TOKEN = "token";
LS_KEY_ACCESS_TOKEN = "accessToken";
LS_KEY_RATING = "rating";
}
});
function createLogger(tag) {
const prefix = `[${tag}]`;
return {
debug: (...args) => {
GM_getValue(GM_KEY_IS_DEBUG) && originalConsole.debug(prefix, ...args);
},
info: (...args) => {
originalConsole.info(prefix, ...args);
},
warn: (...args) => {
originalConsole.warn(prefix, ...args);
},
error: (...args) => {
originalConsole.error(prefix, ...args);
}
};
}
var init_log = __esm({
"src/core/log.ts"() {
"use strict";
init_hijack();
init_constants();
}
});
function isConvertibleToNumber(obj, includeInfinity = false) {
if (isNullOrUndefined(obj)) {
return false;
}
if (isString(obj)) {
return obj.isConvertibleToNumber(includeInfinity);
}
if (isNumber(obj)) {
return isNaN(obj) ? false : includeInfinity ? true : isFinite(obj);
}
return false;
}
function throttle(fn, delay2, { leading = true, trailing = true } = {}) {
let lastCall = 0;
let timer = null;
const throttled = function(...args) {
const now = Date.now();
if (!lastCall && !leading) {
lastCall = now;
}
const remaining = delay2 - (now - lastCall);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastCall = now;
fn.apply(this, args);
} else if (trailing && !timer) {
timer = setTimeout(() => {
lastCall = leading ? Date.now() : 0;
timer = null;
fn.apply(this, args);
}, remaining);
}
};
throttled.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastCall = 0;
};
return throttled;
}
function debounce(fn, delay2, { immediate = false } = {}) {
let timer = null;
const debounced = function(...args) {
const callNow = immediate && !timer;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
timer = null;
if (!immediate) {
fn.apply(this, args);
}
}, delay2);
if (callNow) {
fn.apply(this, args);
}
};
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
}
function delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
function hasProperty(element, property) {
return property in element;
}
function hasOwnProperty(element, prop) {
return prop in element;
}
function hasFunction(obj, method) {
return isObject(obj) && method in obj && typeof obj[method] === "function";
}
function UUID() {
return isNullOrUndefined(crypto) ? Array.from({ length: 8 }, () => ((1 + Math.random()) * 65536 | 0).toString(16).substring(1)).join("") : crypto.randomUUID().replaceAll("-", "");
}
function stringify(data) {
switch (typeof data) {
case "undefined":
return "undefined";
case "boolean":
return data ? "true" : "false";
case "number":
return String(data);
case "string":
return data;
case "symbol":
return data.toString();
case "function":
return data.toString();
case "object":
if (isNull(data)) {
return "null";
}
if (data instanceof Error) {
return data.toString();
}
if (data instanceof Date) {
return data.toISOString();
}
return JSON.stringify(data, null, 2);
default:
return "unknown";
}
}
function prune(data) {
if (isElement(data) || isNode(data)) {
return data;
}
if (Array.isArray(data)) {
return data.map((item) => prune(item)).filter(isNotEmpty);
}
if (isObject(data)) {
const result = Object.fromEntries(
Object.entries(data).filter(([, v2]) => isNotEmpty(v2)).map(([k2, v2]) => [k2, prune(v2)]).filter(([, v2]) => isNotEmpty(v2))
);
return result;
}
return data;
}
var log, ConvertibleNumber, PositiveInteger, NegativeInteger, PositiveFloat, NegativeFloat, emojiBase, emojiRegex, isNull, isUndefined, isNullOrUndefined, isObject, isString, isNumber, isArray, isElement, isNode, isNotEmpty, isVideoInfo, hasValidID, isInitVideoInfo, isFullVideoInfo, isPartialVideoInfo, isCacheVideoInfo, isFailVideoInfo, assertVideoInfoType;
var init_env = __esm({
"src/core/env.ts"() {
"use strict";
init_log();
log = createLogger("Env");
ConvertibleNumber = Symbol("ConvertibleNumber");
PositiveInteger = Symbol("PositiveInteger");
NegativeInteger = Symbol("NegativeInteger");
PositiveFloat = Symbol("PositiveFloat");
NegativeFloat = Symbol("NegativeFloat");
emojiBase = String.raw`\p{Emoji}(?:\p{EMod}|[\u{E0020}-\u{E007E}]+\u{E007F}|\uFE0F?\u20E3?)`;
emojiRegex = new RegExp(String.raw`\p{RI}{2}|(?)${emojiBase}(?:\u200D${emojiBase})*`, "gu");
isNull = (obj) => obj === null;
isUndefined = (obj) => typeof obj === "undefined";
isNullOrUndefined = (obj) => isUndefined(obj) || isNull(obj);
isObject = (obj) => !isNullOrUndefined(obj) && typeof obj === "object" && !Array.isArray(obj);
isString = (obj) => !isNullOrUndefined(obj) && typeof obj === "string";
isNumber = (obj) => !isNullOrUndefined(obj) && typeof obj === "number";
isArray = (obj) => Array.isArray(obj);
isElement = (obj) => !isNullOrUndefined(obj) && obj instanceof Element;
isNode = (obj) => !isNullOrUndefined(obj) && obj instanceof Node;
isNotEmpty = (obj) => {
if (isNullOrUndefined(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;
};
isVideoInfo = (obj) => {
if (obj === null || typeof obj !== "object") return false;
const info = obj;
return isInitVideoInfo(info) || isFullVideoInfo(info) || isPartialVideoInfo(info) || isCacheVideoInfo(info) || isFailVideoInfo(info);
};
hasValidID = (info) => isString(info.ID) && isNotEmpty(info.ID);
isInitVideoInfo = (info) => !isNullOrUndefined(info) && info.Type === "init" && hasValidID(info);
isFullVideoInfo = (info) => !isNullOrUndefined(info) && info.Type === "full" && hasValidID(info) && isNumber(info.UploadTime) && isString(info.Title) && isNotEmpty(info.Title) && isString(info.FileName) && isNotEmpty(info.FileName) && isNumber(info.Size) && isArray(info.Tags) && typeof info.Liked === "boolean" && typeof info.Following === "boolean" && typeof info.Friend === "boolean" && isString(info.Author) && isNotEmpty(info.Author) && isString(info.AuthorID) && isNotEmpty(info.AuthorID) && typeof info.Private === "boolean" && typeof info.Unlisted === "boolean" && isString(info.DownloadQuality) && typeof info.External === "boolean" && isString(info.DownloadUrl) && isNotEmpty(info.DownloadUrl) && isObject(info.RAW);
isPartialVideoInfo = (info) => !isNullOrUndefined(info) && info.Type === "partial" && hasValidID(info) && isNumber(info.UploadTime) && isString(info.Title) && isNotEmpty(info.Title) && isArray(info.Tags) && typeof info.Liked === "boolean" && isString(info.Author) && isNotEmpty(info.Author) && isString(info.AuthorID) && isNotEmpty(info.AuthorID) && typeof info.Private === "boolean" && typeof info.Unlisted === "boolean" && typeof info.External === "boolean" && isObject(info.RAW);
isCacheVideoInfo = (info) => !isNullOrUndefined(info) && info.Type === "cache" && hasValidID(info) && isObject(info.RAW);
isFailVideoInfo = (info) => !isNullOrUndefined(info) && info.Type === "fail" && hasValidID(info);
assertVideoInfoType = (info) => {
switch (info.Type) {
case "init":
return info;
case "full":
return info;
case "partial":
return info;
case "cache":
return info;
case "fail":
return info;
default:
throw new Error(`未知的 VideoInfo 类型: ${info.Type}`);
}
};
Number.isConvertibleNumber = (value, includeInfinity = false) => {
if (isNullOrUndefined(value)) {
return false;
}
if (isString(value)) {
return value.isConvertibleToNumber(includeInfinity);
}
if (isNumber(value)) {
return isNaN(value) ? false : includeInfinity ? true : isFinite(value);
}
return false;
};
Number.isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
Number.isNegativeInteger = (value) => typeof value === "number" && Number.isInteger(value) && value < 0;
Number.isPositiveFloat = (value) => typeof value === "number" && !Number.isInteger(value) && value > 0;
Number.isNegativeFloat = (value) => typeof value === "number" && !Number.isInteger(value) && value < 0;
Number.toPositiveInteger = (value) => {
if (!Number.isPositiveInteger(value)) {
throw new Error("值必须为正整数");
}
return value;
};
Number.toNegativeInteger = (value) => {
if (!Number.isNegativeInteger(value)) {
throw new Error("值必须为负整数");
}
return value;
};
Number.toPositiveFloat = (value) => {
if (!Number.isPositiveFloat(value)) {
throw new Error("值必须为正浮点数");
}
return value;
};
Number.toNegativeFloat = (value) => {
if (!Number.isNegativeFloat(value)) {
throw new Error("值必须为负浮点数");
}
return value;
};
Array.prototype.any = function() {
return this.filter((i) => !isNullOrUndefined(i)).length > 0;
};
Array.prototype.unique = function(prop) {
if (isNullOrUndefined(prop)) {
const seen = new Set();
return this.filter((item) => {
if (seen.has(item)) return false;
seen.add(item);
return true;
});
} else {
const seen = new Map();
const nanSymbol = Symbol();
return this.filter((item) => {
const rawKey = item[prop];
const key = isNumber(rawKey) && Number.isNaN(rawKey) ? nanSymbol : rawKey;
if (seen.has(key)) return false;
seen.set(key, true);
return true;
});
}
};
Array.prototype.union = function(that, prop) {
return [...this, ...that].unique(prop);
};
Array.prototype.intersect = function(that, prop) {
return this.filter((item) => that.some((t) => isNullOrUndefined(prop) ? t === item : t[prop] === item[prop])).unique(prop);
};
Array.prototype.difference = function(that, prop) {
return this.filter((item) => !that.some((t) => isNullOrUndefined(prop) ? t === item : t[prop] === item[prop])).unique(prop);
};
Array.prototype.complement = function(that, prop) {
return this.union(that, prop).difference(this.intersect(that, prop), prop);
};
String.prototype.isEmpty = function() {
return !isNullOrUndefined(this) && this.length === 0;
};
String.prototype.isConvertibleToNumber = function(includeInfinity = false) {
const trimmed = this.trim();
if (trimmed === "") return false;
return Number.isConvertibleNumber(Number(trimmed), includeInfinity);
};
String.prototype.reversed = function() {
const segmenter = new Intl.Segmenter(navigator.language, { granularity: "grapheme" });
return [...segmenter.segment(this.toString())].reverse().join("");
};
String.prototype.among = function(start, end, greedy = false, reverse = false) {
if (this.isEmpty() || start.isEmpty() || end.isEmpty()) return "";
if (!reverse) {
const startIndex = this.indexOf(start);
if (startIndex === -1) return "";
const adjustedStartIndex = startIndex + start.length;
const endIndex = greedy ? this.lastIndexOf(end) : this.indexOf(end, adjustedStartIndex);
if (endIndex === -1 || endIndex < adjustedStartIndex) return "";
return this.slice(adjustedStartIndex, endIndex);
} else {
const endIndex = this.lastIndexOf(end);
if (endIndex === -1) return "";
const adjustedEndIndex = endIndex - end.length;
const startIndex = greedy ? this.indexOf(start) : this.lastIndexOf(start, adjustedEndIndex);
if (startIndex === -1 || startIndex + start.length > adjustedEndIndex) return "";
return this.slice(startIndex + start.length, endIndex);
}
};
String.prototype.splitLimit = function(separator, limit) {
if (this.isEmpty() || isNullOrUndefined(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.replaceEmojis = function(replace) {
return this.replaceAll(emojiRegex, replace ?? "");
};
String.prototype.toURL = function() {
try {
return new URL(this.toString());
} catch (error) {
if (error instanceof TypeError && this.toString().startsWith("//")) {
return new URL(unsafeWindow.location.protocol + this.toString());
}
throw error;
}
};
String.prototype.replaceVariable = function(replacements, prefix = "%#", suffix = "#%") {
function escapeRegex2(str) {
return str.replace(/[\.\*\+\?\^\$\{\}\(\)\|\[\]\\]/g, "\\$&");
}
let current = this.toString();
prefix = escapeRegex2(prefix);
suffix = escapeRegex2(suffix);
const seen = new Set();
const patterns = Object.keys(replacements).map((key) => ({
value: replacements[key],
placeholderRegex: new RegExp(`${prefix}${escapeRegex2(key)}(?=(?::.*?${suffix}|${suffix}))(?::(?<fmt>.*?))?${suffix}`, "gs")
}));
while (true) {
if (seen.has(current)) {
log.warn("检测到循环替换!", `终止于: ${current}`);
break;
}
seen.add(current);
let next = current;
for (const { value, placeholderRegex } of patterns) {
next = next.replace(placeholderRegex, (match, fmt) => {
if (!isNullOrUndefined(fmt) && !fmt.isEmpty() && hasFunction(value, "format")) {
return stringify(value.format(fmt));
}
return stringify(value instanceof Date ? value.format("YYYY-MM-DD") : value);
});
}
if (current === next) break;
current = next;
}
return current;
};
}
});
function C() {
return w || (w = [IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction]);
}
function x() {
return b || (b = [IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey]);
}
function V(e) {
const n = new Promise((t, r) => {
const i = () => {
e.removeEventListener("success", c), e.removeEventListener("error", o);
}, c = () => {
t(a(e.result)), i();
}, o = () => {
r(e.error), i();
};
e.addEventListener("success", c), e.addEventListener("error", o);
});
return f.set(n, e), n;
}
function S(e) {
if (I.has(e)) return;
const n = new Promise((t, r) => {
const i = () => {
e.removeEventListener("complete", c), e.removeEventListener("error", o), e.removeEventListener("abort", o);
}, c = () => {
t(), i();
}, o = () => {
r(e.error || new DOMException("AbortError", "AbortError")), i();
};
e.addEventListener("complete", c), e.addEventListener("error", o), e.addEventListener("abort", o);
});
I.set(e, n);
}
function E(e) {
y = e(y);
}
function T(e) {
return x().includes(e) ? function(...n) {
return e.apply(h(this), n), a(this.request);
} : function(...n) {
return a(e.apply(h(this), n));
};
}
function j(e) {
return typeof e == "function" ? T(e) : (e instanceof IDBTransaction && S(e), D(e, C()) ? new Proxy(e, y) : e);
}
function a(e) {
if (e instanceof IDBRequest) return V(e);
if (l.has(e)) return l.get(e);
const n = j(e);
return n !== e && (l.set(e, n), f.set(n, e)), n;
}
function A(e, n, { blocked: t, upgrade: r, blocking: i, terminated: c } = {}) {
const o = indexedDB.open(e, n), d = a(o);
return r && o.addEventListener("upgradeneeded", (s) => {
r(a(o.result), s.oldVersion, s.newVersion, a(o.transaction), s);
}), t && o.addEventListener("blocked", (s) => t(s.oldVersion, s.newVersion, s)), d.then((s) => {
c && s.addEventListener("close", () => c()), i && s.addEventListener("versionchange", (u) => i(u.oldVersion, u.newVersion, u));
}).catch(() => {
}), d;
}
function O(e, { blocked: n } = {}) {
const t = indexedDB.deleteDatabase(e);
return n && t.addEventListener("blocked", (r) => n(r.oldVersion, r)), a(t).then(() => {
});
}
function M(e, n) {
if (!(e instanceof IDBDatabase && !(n in e) && typeof n == "string")) return;
if (m.get(n)) return m.get(n);
const t = n.replace(/FromIndex$/, ""), r = n !== t, i = W.includes(t);
if (!(t in (r ? IDBIndex : IDBObjectStore).prototype) || !(i || v.includes(t))) return;
const c = async function(o, ...d) {
const s = this.transaction(o, i ? "readwrite" : "readonly");
let u = s.store;
return r && (u = u.index(d.shift())), (await Promise.all([u[t](...d), i && s.done]))[0];
};
return m.set(n, c), c;
}
async function* k(...e) {
let n = this;
if (n instanceof IDBCursor || (n = await n.openCursor(...e)), !n) return;
n = n;
const t = new Proxy(n, F);
for (L.set(t, n), f.set(t, h(n)); n; ) yield t, n = await (B.get(t) || n.continue()), B.delete(t);
}
function g(e, n) {
return n === Symbol.asyncIterator && D(e, [IDBIndex, IDBObjectStore, IDBCursor]) || n === "iterate" && D(e, [IDBIndex, IDBObjectStore]);
}
var D, w, b, I, l, f, y, h, v, W, m, p, P, B, L, F;
var init_build = __esm({
"node_modules/idb/build/index.js"() {
"use strict";
D = (e, n) => n.some((t) => e instanceof t);
I = new WeakMap(), l = new WeakMap(), f = new WeakMap();
y = { get(e, n, t) {
if (e instanceof IDBTransaction) {
if (n === "done") return I.get(e);
if (n === "store") return t.objectStoreNames[1] ? void 0 : t.objectStore(t.objectStoreNames[0]);
}
return a(e[n]);
}, set(e, n, t) {
return e[n] = t, true;
}, has(e, n) {
return e instanceof IDBTransaction && (n === "done" || n === "store") ? true : n in e;
} };
h = (e) => f.get(e);
v = ["get", "getKey", "getAll", "getAllKeys", "count"], W = ["put", "add", "delete", "clear"], m = new Map();
E((e) => ({ ...e, get: (n, t, r) => M(n, t) || e.get(n, t, r), has: (n, t) => !!M(n, t) || e.has(n, t) }));
p = ["continue", "continuePrimaryKey", "advance"], P = {}, B = new WeakMap(), L = new WeakMap(), F = { get(e, n) {
if (!p.includes(n)) return e[n];
let t = P[n];
return t || (t = P[n] = function(...r) {
B.set(this, L.get(this)[n](...r));
}), t;
} };
E((e) => ({ ...e, get(n, t, r) {
return g(n, t) ? k : e.get(n, t, r);
}, has(n, t) {
return g(n, t) || e.has(n, t);
} }));
}
});
function upgradeDatabase(db2, oldVersion) {
if (!db2.objectStoreNames.contains("follows")) {
const followsStore = db2.createObjectStore("follows", { keyPath: "id" });
followsStore.createIndex("id", "id", { unique: true });
followsStore.createIndex("username", "username", { unique: true });
followsStore.createIndex("name", "name");
followsStore.createIndex("friend", "friend");
followsStore.createIndex("following", "following");
followsStore.createIndex("followedBy", "followedBy");
}
if (!db2.objectStoreNames.contains("friends")) {
const friendsStore = db2.createObjectStore("friends", { keyPath: "id" });
friendsStore.createIndex("id", "id", { unique: true });
friendsStore.createIndex("username", "username", { unique: true });
friendsStore.createIndex("name", "name");
friendsStore.createIndex("friend", "friend");
friendsStore.createIndex("following", "following");
friendsStore.createIndex("followedBy", "followedBy");
}
if (!db2.objectStoreNames.contains("videos")) {
const videosStore = db2.createObjectStore("videos", { keyPath: "ID" });
videosStore.createIndex("ID", "ID", { unique: true });
videosStore.createIndex("UploadTime", "UploadTime");
videosStore.createIndex("Private", "Private");
videosStore.createIndex("Unlisted", "Unlisted");
videosStore.createIndex("Type", "Type");
}
if (!db2.objectStoreNames.contains("idmap")) {
const idmapStore = db2.createObjectStore("idmap", { keyPath: "ID" });
idmapStore.createIndex("ID", "ID", { unique: true });
}
if (oldVersion < 21 && db2.objectStoreNames.contains("caches")) {
db2.deleteObjectStore("caches");
}
if (oldVersion < 22 && db2.objectStoreNames.contains("pairs")) {
db2.deleteObjectStore("pairs");
}
}
var DB_NAME, DB_VERSION;
var init_dbMigration = __esm({
"src/core/dbMigration.ts"() {
"use strict";
DB_NAME = "IwaraDownloadTool";
DB_VERSION = 22;
}
});
var db_exports = {};
__export(db_exports, {
Database: () => Database,
db: () => db
});
var log3, Database, db;
var init_db = __esm({
"src/core/db.ts"() {
"use strict";
init_env();
init_env();
init_build();
init_log();
init_dbMigration();
log3 = createLogger("db");
Database = class _Database {
static instance;
dbPromise;
constructor() {
this.dbPromise = A(DB_NAME, DB_VERSION, {
upgrade: (db2, oldVersion) => upgradeDatabase(db2, oldVersion)
});
}
async getDB() {
return this.dbPromise;
}
async follows() {
const db2 = await this.getDB();
return db2.transaction("follows", "readwrite").objectStore("follows");
}
async friends() {
const db2 = await this.getDB();
return db2.transaction("friends", "readwrite").objectStore("friends");
}
async videos() {
const db2 = await this.getDB();
return db2.transaction("videos", "readwrite").objectStore("videos");
}
async idmap() {
const db2 = await this.getDB();
return db2.transaction("idmap", "readwrite").objectStore("idmap");
}
async getFollows() {
const store = await this.follows();
return store.getAll();
}
async getFriends() {
const store = await this.friends();
return store.getAll();
}
async getVideos() {
const store = await this.videos();
return store.getAll();
}
async getIdmap() {
const store = await this.idmap();
return store.getAll();
}
async getFollowByUsername(username) {
const db2 = await this.getDB();
const tx = db2.transaction("follows", "readonly");
const index = tx.store.index("username");
return index.get(username);
}
async getFollowById(id) {
const db2 = await this.getDB();
return db2.get("follows", id);
}
async getVideoById(id) {
const db2 = await this.getDB();
return db2.get("videos", id);
}
async getVideosByIds(ids) {
const db2 = await this.getDB();
const tx = db2.transaction("videos", "readonly");
const store = tx.store;
const results = [];
for (const id of ids) {
const video = await store.get(id);
if (video) {
results.push(video);
}
}
return results;
}
async countVideos(predicate) {
const db2 = await this.getDB();
const tx = db2.transaction("videos", "readonly");
const store = tx.store;
let cursor = await store.openCursor();
let count = 0;
while (cursor) {
if (!predicate || predicate(cursor.value)) count++;
cursor = await cursor.continue();
}
return count;
}
async *iterateVideosBatched(batchSize = 500, predicate, sortBy, direction = "next") {
let lastIndexKey = void 0;
let lastPrimaryKey = void 0;
let hasMore = true;
while (hasMore) {
const db2 = await this.getDB();
const tx = db2.transaction("videos", "readonly");
const store = tx.store;
const source = sortBy ? store.index(sortBy) : store;
let cursor;
if (lastIndexKey !== void 0) {
if (direction === "prev") {
const range = IDBKeyRange.upperBound(lastIndexKey, false);
cursor = await source.openCursor(range, direction);
while (cursor && cursor.key === lastIndexKey && cursor.primaryKey >= lastPrimaryKey) {
cursor = await cursor.continue();
}
} else {
const range = IDBKeyRange.lowerBound(lastIndexKey, false);
cursor = await source.openCursor(range, direction);
while (cursor && cursor.key === lastIndexKey && cursor.primaryKey <= lastPrimaryKey) {
cursor = await cursor.continue();
}
}
} else {
cursor = await source.openCursor(null, direction);
}
const batch = [];
while (cursor && batch.length < batchSize) {
const video = cursor.value;
if (!predicate || predicate(video)) {
batch.push(video);
}
lastIndexKey = cursor.key;
lastPrimaryKey = cursor.primaryKey;
cursor = await cursor.continue();
}
hasMore = cursor !== null;
yield batch;
}
}
async *iterateVideoKeysBatched(batchSize = 5e3) {
let lastKey = void 0;
let hasMore = true;
while (hasMore) {
const db2 = await this.getDB();
const tx = db2.transaction("videos", "readonly");
const store = tx.store;
let cursor;
if (lastKey !== void 0) {
const range = IDBKeyRange.lowerBound(lastKey, true);
cursor = await store.openKeyCursor(range, "next");
} else {
cursor = await store.openKeyCursor(null, "next");
}
const batch = [];
while (cursor && batch.length < batchSize) {
batch.push(cursor.primaryKey);
lastKey = cursor.primaryKey;
cursor = await cursor.continue();
}
hasMore = cursor !== null;
yield batch;
}
}
async *iterateTableBatched(table, batchSize = 500) {
let lastKey = void 0;
let hasMore = true;
while (hasMore) {
const db2 = await this.getDB();
const tx = db2.transaction(table, "readonly");
const store = tx.store;
let cursor;
if (lastKey !== void 0) {
const range = IDBKeyRange.lowerBound(lastKey, true);
cursor = await store.openCursor(range, "next");
} else {
cursor = await store.openCursor(null, "next");
}
const batch = [];
while (cursor && batch.length < batchSize) {
batch.push(cursor.value);
lastKey = cursor.primaryKey;
cursor = await cursor.continue();
}
hasMore = cursor !== null;
yield batch;
}
}
async putVideo(video) {
const db2 = await this.getDB();
await db2.put("videos", video);
}
async bulkPutVideos(videos) {
const db2 = await this.getDB();
const tx = db2.transaction("videos", "readwrite");
const store = tx.store;
for (const video of videos) {
await store.put(video);
}
await tx.done;
}
async putFollow(user) {
const db2 = await this.getDB();
await db2.put("follows", user);
}
async putFriend(user) {
const db2 = await this.getDB();
await db2.put("friends", user);
}
async deleteFollow(id) {
const db2 = await this.getDB();
await db2.delete("follows", id);
}
async deleteFriend(id) {
const db2 = await this.getDB();
await db2.delete("friends", id);
}
async getFilteredVideos(startTime, endTime) {
if (isNullOrUndefined(startTime) || isNullOrUndefined(endTime)) return [];
const db2 = await this.getDB();
const tx = db2.transaction("videos", "readonly");
const store = tx.store;
const index = store.index("UploadTime");
const allVideos = [];
let cursor = await index.openCursor(IDBKeyRange.bound(startTime, endTime, true, true));
while (cursor) {
const video = cursor.value;
if ((video.Type === "partial" || video.Type === "full") && (video.Private || video.Unlisted) && !isNullOrUndefined(video.RAW)) {
allVideos.push(video);
}
cursor = await cursor.continue();
}
return allVideos;
}
async getMediaCenterIdMap(videoId) {
const db2 = await this.getDB();
const entry = await db2.get("idmap", videoId);
return entry?.href;
}
async putMediaCenterIdMap(videoId, mediaCenterId) {
const db2 = await this.getDB();
await db2.put("idmap", { ID: videoId, href: mediaCenterId });
}
async bulkPutMediaCenterIdMaps(entries) {
const db2 = await this.getDB();
const tx = db2.transaction("idmap", "readwrite");
const store = tx.store;
for (const { videoId, mediaCenterId } of entries) {
await store.put({ ID: videoId, href: mediaCenterId });
}
await tx.done;
}
async getAllMediaCenterIdMaps() {
const db2 = await this.getDB();
const all = await db2.getAll("idmap");
const map = new Map();
for (const entry of all) {
map.set(entry.ID, entry.href);
}
return map;
}
async deleteMediaCenterIdMap(videoId) {
const db2 = await this.getDB();
await db2.delete("idmap", videoId);
}
async clearAllMediaCenterIdMaps() {
const db2 = await this.getDB();
const tx = db2.transaction("idmap", "readwrite");
const store = tx.store;
let cursor = await store.openCursor();
while (cursor) {
await cursor.delete();
cursor = await cursor.continue();
}
await tx.done;
}
async downloadTextFile(filename, content, mimeType, mode = "auto", gmTimeout = 30 * 1e3) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const triggerBrowserDownload = () => {
const a2 = document.createElement("a");
a2.href = url;
a2.download = filename;
document.body.appendChild(a2);
a2.click();
a2.remove();
setTimeout(() => URL.revokeObjectURL(url), 60 * 1e3);
};
const isFirefox = () => /Firefox/i.test(navigator.userAgent);
const useGm = (mode === "gm" || mode === "auto" && !isFirefox()) && typeof GM_download === "function";
if (useGm) {
try {
await new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(new Error(`下载 ${filename} 超时(GM_download 未响应)`));
}, gmTimeout);
GM_download({
url,
name: filename,
saveAs: false,
onload: () => {
if (settled) return;
settled = true;
clearTimeout(timer);
URL.revokeObjectURL(url);
resolve();
},
onerror: (err) => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(new Error(`下载 ${filename} 失败: ${err.error}${err.details ? ` - ${err.details}` : ""}`));
},
ontimeout: () => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(new Error(`下载 ${filename} 超时`));
}
});
});
return;
} catch (error) {
if (mode === "auto") {
log3.warn(`GM_download 不可用,回退浏览器原生下载: ${error.message}`);
triggerBrowserDownload();
return;
}
URL.revokeObjectURL(url);
throw error;
}
}
triggerBrowserDownload();
}
async exportToJsonFiles(table, options = {}) {
const { batchSize = 500, prefix = table, pretty = true, onProgress, downloadMode = "auto", gmTimeout } = options;
const db2 = await this.getDB();
const total = await db2.count(table);
const totalBatches = Math.ceil(total / batchSize);
let fileCount = 0;
for await (const batch of this.iterateTableBatched(table, batchSize)) {
fileCount++;
const content = JSON.stringify(batch, null, pretty ? 2 : void 0);
const filename = `${prefix}_${String(fileCount).padStart(3, "0")}.json`;
await this.downloadTextFile(filename, content, "application/json;charset=utf-8", downloadMode, gmTimeout);
onProgress?.(fileCount, totalBatches);
}
return fileCount;
}
async exportAllToJsonFiles(options = {}) {
const tables = ["videos", "follows", "friends", "idmap"];
const result = {};
for (const table of tables) {
result[table] = await this.exportToJsonFiles(table, options);
}
return result;
}
static getInstance() {
if (isNullOrUndefined(_Database.instance)) {
_Database.instance = new _Database();
}
return _Database.instance;
}
static destroyInstance() {
_Database.instance = void 0;
}
async delete() {
const db2 = await this.getDB();
db2.close();
await O(DB_NAME);
}
};
db = Database.getInstance();
}
});
var require_dayjs_min = __commonJS({
"node_modules/dayjs/dayjs.min.js"(exports, module) {
"use strict";
(function(T2, g2) {
typeof exports == "object" && typeof module < "u" ? module.exports = g2() : typeof define == "function" && define.amd ? define(g2) : (T2 = typeof globalThis < "u" ? globalThis : T2 || self).dayjs = g2();
})(exports, (function() {
"use strict";
var T2 = 1e3, g2 = 6e4, U = 36e5, A2 = "millisecond", w2 = "second", O2 = "minute", b2 = "hour", M2 = "day", L2 = "week", m2 = "month", F2 = "quarter", y2 = "year", _ = "date", J = "Invalid Date", V2 = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, q = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, B2 = { name: "en", weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), ordinal: function(i) {
var n = ["th", "st", "nd", "rd"], t = i % 100;
return "[" + i + (n[(t - 20) % 10] || n[t] || n[0]) + "]";
} }, I2 = function(i, n, t) {
var r = String(i);
return !r || r.length >= n ? i : "" + Array(n + 1 - r.length).join(t) + i;
}, E2 = { s: I2, z: function(i) {
var n = -i.utcOffset(), t = Math.abs(n), r = Math.floor(t / 60), e = t % 60;
return (n <= 0 ? "+" : "-") + I2(r, 2, "0") + ":" + I2(e, 2, "0");
}, m: function i(n, t) {
if (n.date() < t.date()) return -i(t, n);
var r = 12 * (t.year() - n.year()) + (t.month() - n.month()), e = n.clone().add(r, m2), s = t - e < 0, u = n.clone().add(r + (s ? -1 : 1), m2);
return +(-(r + (t - e) / (s ? e - u : u - e)) || 0);
}, a: function(i) {
return i < 0 ? Math.ceil(i) || 0 : Math.floor(i);
}, p: function(i) {
return { M: m2, y: y2, w: L2, d: M2, D: _, h: b2, m: O2, s: w2, ms: A2, Q: F2 }[i] || String(i || "").toLowerCase().replace(/s$/, "");
}, u: function(i) {
return i === void 0;
} }, Y = "en", D2 = {};
D2[Y] = B2;
var Z = "$isDayjsObject", N = function(i) {
return i instanceof C2 || !(!i || !i[Z]);
}, W2 = function i(n, t, r) {
var e;
if (!n) return Y;
if (typeof n == "string") {
var s = n.toLowerCase();
D2[s] && (e = s), t && (D2[s] = t, e = s);
var u = n.split("-");
if (!e && u.length > 1) return i(u[0]);
} else {
var o = n.name;
D2[o] = n, e = o;
}
return !r && e && (Y = e), e || !r && Y;
}, f2 = function(i, n) {
if (N(i)) return i.clone();
var t = typeof n == "object" ? n : {};
return t.date = i, t.args = arguments, new C2(t);
}, a2 = E2;
a2.l = W2, a2.i = N, a2.w = function(i, n) {
return f2(i, { locale: n.$L, utc: n.$u, x: n.$x, $offset: n.$offset });
};
var C2 = (function() {
function i(t) {
this.$L = W2(t.locale, null, true), this.parse(t), this.$x = this.$x || t.x || {}, this[Z] = true;
}
var n = i.prototype;
return n.parse = function(t) {
this.$d = (function(r) {
var e = r.date, s = r.utc;
if (e === null) return new Date(NaN);
if (a2.u(e)) return new Date();
if (e instanceof Date) return new Date(e);
if (typeof e == "string" && !/Z$/i.test(e)) {
var u = e.match(V2);
if (u) {
var o = u[2] - 1 || 0, c = (u[7] || "0").substring(0, 3);
return s ? new Date(Date.UTC(u[1], o, u[3] || 1, u[4] || 0, u[5] || 0, u[6] || 0, c)) : new Date(u[1], o, u[3] || 1, u[4] || 0, u[5] || 0, u[6] || 0, c);
}
}
return new Date(e);
})(t), this.init();
}, n.init = function() {
var t = this.$d;
this.$y = t.getFullYear(), this.$M = t.getMonth(), this.$D = t.getDate(), this.$W = t.getDay(), this.$H = t.getHours(), this.$m = t.getMinutes(), this.$s = t.getSeconds(), this.$ms = t.getMilliseconds();
}, n.$utils = function() {
return a2;
}, n.isValid = function() {
return this.$d.toString() !== J;
}, n.isSame = function(t, r) {
var e = f2(t);
return this.startOf(r) <= e && e <= this.endOf(r);
}, n.isAfter = function(t, r) {
return f2(t) < this.startOf(r);
}, n.isBefore = function(t, r) {
return this.endOf(r) < f2(t);
}, n.$g = function(t, r, e) {
return a2.u(t) ? this[r] : this.set(e, t);
}, n.unix = function() {
return Math.floor(this.valueOf() / 1e3);
}, n.valueOf = function() {
return this.$d.getTime();
}, n.startOf = function(t, r) {
var e = this, s = !!a2.u(r) || r, u = a2.p(t), o = function(S2, $) {
var v2 = a2.w(e.$u ? Date.UTC(e.$y, $, S2) : new Date(e.$y, $, S2), e);
return s ? v2 : v2.endOf(M2);
}, c = function(S2, $) {
return a2.w(e.toDate()[S2].apply(e.toDate("s"), (s ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice($)), e);
}, h2 = this.$W, d = this.$M, l2 = this.$D, k2 = "set" + (this.$u ? "UTC" : "");
switch (u) {
case y2:
return s ? o(1, 0) : o(31, 11);
case m2:
return s ? o(1, d) : o(0, d + 1);
case L2:
var p2 = this.$locale().weekStart || 0, H = (h2 < p2 ? h2 + 7 : h2) - p2;
return o(s ? l2 - H : l2 + (6 - H), d);
case M2:
case _:
return c(k2 + "Hours", 0);
case b2:
return c(k2 + "Minutes", 1);
case O2:
return c(k2 + "Seconds", 2);
case w2:
return c(k2 + "Milliseconds", 3);
default:
return this.clone();
}
}, n.endOf = function(t) {
return this.startOf(t, false);
}, n.$set = function(t, r) {
var e, s = a2.p(t), u = "set" + (this.$u ? "UTC" : ""), o = (e = {}, e[M2] = u + "Date", e[_] = u + "Date", e[m2] = u + "Month", e[y2] = u + "FullYear", e[b2] = u + "Hours", e[O2] = u + "Minutes", e[w2] = u + "Seconds", e[A2] = u + "Milliseconds", e)[s], c = s === M2 ? this.$D + (r - this.$W) : r;
if (s === m2 || s === y2) {
var h2 = this.clone().set(_, 1);
h2.$d[o](c), h2.init(), this.$d = h2.set(_, Math.min(this.$D, h2.daysInMonth())).$d;
} else o && this.$d[o](c);
return this.init(), this;
}, n.set = function(t, r) {
return this.clone().$set(t, r);
}, n.get = function(t) {
return this[a2.p(t)]();
}, n.add = function(t, r) {
var e, s = this;
t = Number(t);
var u = a2.p(r), o = function(d) {
var l2 = f2(s);
return a2.w(l2.date(l2.date() + Math.round(d * t)), s);
};
if (u === m2) return this.set(m2, this.$M + t);
if (u === y2) return this.set(y2, this.$y + t);
if (u === M2) return o(1);
if (u === L2) return o(7);
var c = (e = {}, e[O2] = g2, e[b2] = U, e[w2] = T2, e)[u] || 1, h2 = this.$d.getTime() + t * c;
return a2.w(h2, this);
}, n.subtract = function(t, r) {
return this.add(-1 * t, r);
}, n.format = function(t) {
var r = this, e = this.$locale();
if (!this.isValid()) return e.invalidDate || J;
var s = t || "YYYY-MM-DDTHH:mm:ssZ", u = a2.z(this), o = this.$H, c = this.$m, h2 = this.$M, d = e.weekdays, l2 = e.months, k2 = e.meridiem, p2 = function($, v2, x2, j2) {
return $ && ($[v2] || $(r, s)) || x2[v2].slice(0, j2);
}, H = function($) {
return a2.s(o % 12 || 12, $, "0");
}, S2 = k2 || function($, v2, x2) {
var j2 = $ < 12 ? "AM" : "PM";
return x2 ? j2.toLowerCase() : j2;
};
return s.replace(q, (function($, v2) {
return v2 || (function(x2) {
switch (x2) {
case "YY":
return String(r.$y).slice(-2);
case "YYYY":
return a2.s(r.$y, 4, "0");
case "M":
return h2 + 1;
case "MM":
return a2.s(h2 + 1, 2, "0");
case "MMM":
return p2(e.monthsShort, h2, l2, 3);
case "MMMM":
return p2(l2, h2);
case "D":
return r.$D;
case "DD":
return a2.s(r.$D, 2, "0");
case "d":
return String(r.$W);
case "dd":
return p2(e.weekdaysMin, r.$W, d, 2);
case "ddd":
return p2(e.weekdaysShort, r.$W, d, 3);
case "dddd":
return d[r.$W];
case "H":
return String(o);
case "HH":
return a2.s(o, 2, "0");
case "h":
return H(1);
case "hh":
return H(2);
case "a":
return S2(o, c, true);
case "A":
return S2(o, c, false);
case "m":
return String(c);
case "mm":
return a2.s(c, 2, "0");
case "s":
return String(r.$s);
case "ss":
return a2.s(r.$s, 2, "0");
case "SSS":
return a2.s(r.$ms, 3, "0");
case "Z":
return u;
}
return null;
})($) || u.replace(":", "");
}));
}, n.utcOffset = function() {
return 15 * -Math.round(this.$d.getTimezoneOffset() / 15);
}, n.diff = function(t, r, e) {
var s, u = this, o = a2.p(r), c = f2(t), h2 = (c.utcOffset() - this.utcOffset()) * g2, d = this - c, l2 = function() {
return a2.m(u, c);
};
switch (o) {
case y2:
s = l2() / 12;
break;
case m2:
s = l2();
break;
case F2:
s = l2() / 3;
break;
case L2:
s = (d - h2) / 6048e5;
break;
case M2:
s = (d - h2) / 864e5;
break;
case b2:
s = d / U;
break;
case O2:
s = d / g2;
break;
case w2:
s = d / T2;
break;
default:
s = d;
}
return e ? s : a2.a(s);
}, n.daysInMonth = function() {
return this.endOf(m2).$D;
}, n.$locale = function() {
return D2[this.$L];
}, n.locale = function(t, r) {
if (!t) return this.$L;
var e = this.clone(), s = W2(t, r, true);
return s && (e.$L = s), e;
}, n.clone = function() {
return a2.w(this.$d, this);
}, n.toDate = function() {
return new Date(this.valueOf());
}, n.toJSON = function() {
return this.isValid() ? this.toISOString() : null;
}, n.toISOString = function() {
return this.$d.toISOString();
}, n.toString = function() {
return this.$d.toUTCString();
}, i;
})(), z = C2.prototype;
return f2.prototype = z, [["$ms", A2], ["$s", w2], ["$m", O2], ["$H", b2], ["$W", M2], ["$M", m2], ["$y", y2], ["$D", _]].forEach((function(i) {
z[i[1]] = function(n) {
return this.$g(n, i[0], i[1]);
};
})), f2.extend = function(i, n) {
return i.$i || (i(n, C2, f2), i.$i = true), f2;
}, f2.locale = W2, f2.isDayjs = N, f2.unix = function(i) {
return f2(1e3 * i);
}, f2.en = D2[Y], f2.Ls = D2, f2.p = {}, f2;
}));
}
});
var site_default = {
officialDomains: [
"iwara.tv",
"iwara.zip",
"iwara.shop",
"iwara.ai"
],
supportedDomains: [
"iwara.tv",
"iwara.ai"
],
phishingKeywords: [
"iwara"
],
apiEndpoint: "api.iwara.tv",
syncStartPage: 6e3,
maxFindPages: 64,
pageLimit: 50
};
if (unsafeWindow.IwaraDownloadTool) {
throw `Script is already running`;
}
unsafeWindow.IwaraDownloadTool = true;
var domain = window.location.hostname;
var isOfficial = site_default.officialDomains.some((d) => domain === d || domain.endsWith("." + d));
if (!isOfficial && site_default.phishingKeywords.some((k2) => domain.includes(k2))) {
XMLHttpRequest.prototype.open = function() {
throw new Error("Blocked");
};
unsafeWindow.fetch = () => Promise.reject();
unsafeWindow.WebSocket = function() {
throw new Error("Blocked");
};
const i18n = {
zh: "警告:当前网站不是 Iwara 官方网站,可能存在钓鱼或仿冒风险,请勿输入账号、密码或其他敏感信息。是否继续访问?",
en: "Warning: This website is not an official Iwara website and may be a phishing or impersonation site. Do not enter your account, password, or any sensitive information. Continue anyway?",
ja: "警告: 現在のサイトは Iwara の公式サイトではなく、フィッシングサイトまたは偽装サイトの可能性があります。アカウント、パスワード、その他の機密情報を入力しないでください。続行しますか?"
};
const lang = navigator.language?.replace("-", "_").toLowerCase().split("_")[0];
if (!confirm(i18n[lang] || i18n.en)) {
unsafeWindow.document.documentElement?.remove();
unsafeWindow.stop();
unsafeWindow.close();
unsafeWindow.location.href = "about:blank";
throw new Error("Blocked");
} else {
throw "Not official";
}
}
var rainbow_default = "@keyframes rainbow-horizontal{0%{background-position:0% 0%}to{background-position:200% 0%}}@keyframes rainbow-vertical{0%{background-position:0% 0%}to{background-position:0% 200%}}.rainbow-text{background-image:linear-gradient(to right,#ff4040,#ffff40,#40ff40,#40ffff,#4040ff,#ff40ff,#ff4040);background-size:200% 100%;background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:rainbow-horizontal .8s infinite linear;font-weight:700}";
var menu_default = '#pluginMenu{z-index:2147483644;position:fixed;top:50%;right:0;padding:10px 26px;background-color:var(--body-dark);border:1px solid var(--text);border-radius:5px;box-shadow:0 0 10px var(--text);transform:translate(calc(100% - 26px)) translateY(-50%);transition:transform .3s ease-in-out}#pluginMenu:not(.expanded){overflow:visible}#pluginMenu:not(.expanded):before{content:"";mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIj48cGF0aCBkPSJNNTYwLTI0MCAzMjAtNDgwbDI0MC0yNDAgNTYgNTYtMTg0IDE4NCAxODQgMTg0LTU2IDU2WiIvPjwvc3ZnPg==) no-repeat center;-webkit-mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIj48cGF0aCBkPSJNNTYwLTI0MCAzMjAtNDgwbDI0MC0yNDAgNTYgNTYtMTg0IDE4NCAxODQgMTg0LTU2IDU2WiIvPjwvc3ZnPg==) no-repeat center;background-color:var(--text);width:24px;height:24px;position:absolute;left:0;top:50%;line-height:0;transform:translateY(-50%)}#pluginMenu.expanded{transform:translate(0) translateY(-50%)}#pluginMenu ul{list-style:none;margin:0;padding:0}#pluginMenu li{padding:5px 10px;cursor:pointer;text-align:center;user-select:none;color:var(--text)}#pluginMenu li:hover{background-color:var(--primary-dark);border-radius:3px}@media(max-width:720px){#pluginMenu{padding:8px 18px;transform:translate(calc(100% - 18px)) translateY(-50%)}#pluginMenu:not(.expanded):before{width:20px;height:20px}#pluginMenu.expanded{transform:translate(0) translateY(-50%)}#pluginMenu li{padding:12px 16px;font-size:15px;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center;white-space:nowrap}}@media(max-width:480px){#pluginMenu{padding:6px 14px;transform:translate(calc(100% - 14px)) translateY(-50%)}#pluginMenu:not(.expanded):before{width:18px;height:18px}#pluginMenu li{padding:14px 18px;font-size:14px}}@media(pointer:coarse){#pluginMenu{padding:12px 34px;transform:translate(calc(100% - 34px)) translateY(-50%)}#pluginMenu:not(.expanded):before{width:28px;height:28px;left:3px}#pluginMenu.expanded{transform:translate(0) translateY(-50%)}#pluginMenu li{padding:14px 22px;font-size:15px;min-width:48px;min-height:48px;display:flex;align-items:center;justify-content:center;white-space:nowrap}#pluginMenu li:nth-child(odd){background-color:var(--body-dark)}#pluginMenu li:nth-child(2n){background-color:var(--body)}#pluginMenu li:active{background-color:var(--primary-dark)!important;border-radius:3px}}';
var config_default = '#pluginConfig{color:var(--text);position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000000bf;z-index:2147483646;display:flex;flex-direction:column;align-items:center;justify-content:center;animation:configFadeIn .15s ease}@keyframes configFadeIn{0%{opacity:0}to{opacity:1}}#pluginConfig .main{background-color:var(--body);padding:0 24px 24px;margin:10px;overflow-y:auto;width:480px;height:clamp(360px,70vh,680px);max-height:90%;border-radius:14px;border:1px solid color-mix(in srgb,var(--text) 14%,transparent);box-shadow:0 12px 48px #00000073;scrollbar-width:thin;scrollbar-color:var(--muted) transparent;animation:configPanelIn .2s ease}@keyframes configPanelIn{0%{opacity:0;transform:translateY(-14px) scale(.98)}to{opacity:1;transform:none}}@media(max-width:640px){#pluginConfig .main{width:100%;height:80%;margin:0;border-radius:0}}#pluginConfig .main h2{margin:0 0 6px;padding:20px 0 8px;text-align:center;font-size:20px;font-weight:700;letter-spacing:.5px;color:var(--primary)}#pluginConfig .buttonList{display:flex;flex-direction:row;justify-content:center;gap:16px;margin-top:16px;padding-top:14px;border-top:1px solid color-mix(in srgb,var(--text) 10%,transparent)}#pluginConfig .buttonList button{background-color:var(--primary);margin:0;padding:10px 32px;color:var(--primary-text);font-size:15px;font-weight:600;border:none;border-radius:8px;cursor:pointer;transition:filter .15s,transform .1s}#pluginConfig .buttonList button:hover{filter:brightness(1.12)}#pluginConfig .buttonList button:active{transform:scale(.97)}#pluginConfig .buttonList button[disabled]{background-color:var(--muted);cursor:not-allowed;filter:none;transform:none}#pluginConfig p{display:flex;flex-direction:column;margin:0}#pluginConfig fieldset{border:1px solid color-mix(in srgb,var(--text) 10%,transparent);border-radius:10px;margin:12px 0 0;padding:12px 14px 14px;display:flex;flex-direction:column;background:color-mix(in srgb,var(--body) 94%,var(--text))}#pluginConfig fieldset>.fieldTitle{margin:0 0 6px;font-size:12px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--primary)}#pluginConfig fieldset.downloadType{flex-direction:row;justify-content:space-between;flex-wrap:wrap;gap:6px;margin-top:10px;padding:6px 0 4px;border:none;background:none}#pluginConfig fieldset.downloadType>.fieldTitle{width:100%;margin-bottom:6px}#pluginConfig fieldset.downloadType>label{display:inline-flex;align-items:center;gap:6px;margin:0;padding:6px 14px;font-size:13px;border-radius:999px;border:1px solid color-mix(in srgb,var(--text) 18%,transparent);cursor:pointer;transition:background-color .15s,border-color .15s,color .15s}#pluginConfig fieldset.downloadType>label:hover{border-color:var(--primary)}#pluginConfig fieldset.downloadType>label:has(input:checked){background:color-mix(in srgb,var(--primary) 16%,transparent);border-color:var(--primary);color:var(--primary);font-weight:600}#pluginConfig fieldset.downloadType input[type=radio]{accent-color:var(--primary);margin:0}#pluginConfig p label{display:flex;flex-direction:column;margin:5px 0 0}#pluginConfig .inputRadioLine{display:flex;align-items:center;flex-direction:row;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid color-mix(in srgb,var(--text) 6%,transparent)}#pluginConfig .inputRadioLine:last-child{border-bottom:none}#pluginConfig .inputRadioLine label{font-size:14px;cursor:pointer;user-select:none}#pluginConfig input[type=text],#pluginConfig input[type=password],#pluginConfig input[type=number]{outline:none;border:1px solid color-mix(in srgb,var(--text) 15%,transparent);border-radius:8px;line-height:1.2;height:34px;padding:0 10px;box-sizing:border-box;width:100%;background-color:color-mix(in srgb,var(--body) 92%,var(--text));color:var(--text);font-size:14px;transition:border-color .15s,box-shadow .15s}#pluginConfig input[type=text]:focus,#pluginConfig input[type=password]:focus,#pluginConfig input[type=number]:focus{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 22%,transparent)}#pluginConfig input[type=checkbox].switch{outline:none;appearance:none;-webkit-appearance:none;-moz-appearance:none;position:relative;flex-shrink:0;width:42px;height:22px;background:var(--muted);border-radius:999px;cursor:pointer;transition:background-color .2s}#pluginConfig input[type=checkbox].switch:after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--white);box-shadow:0 1px 3px #00000059;transition:transform .2s}#pluginConfig input[type=checkbox].switch:checked{background:var(--success)}#pluginConfig input[type=checkbox].switch:checked:after{transform:translate(20px)}#pluginConfig .tabs{display:flex;flex-direction:row;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;gap:4px;border-bottom:1px solid color-mix(in srgb,var(--text) 14%,transparent);margin-top:8px;position:sticky;top:0;z-index:2;background:var(--body);padding-top:4px}#pluginConfig .tabs::-webkit-scrollbar{display:none}#pluginConfig .tabs button{background:none;color:var(--muted);margin:0;padding:10px 12px;font-size:14px;font-weight:600;white-space:nowrap;flex-shrink:0;border:none;border-bottom:2px solid transparent;border-radius:0;cursor:pointer;transition:color .15s,border-color .15s}#pluginConfig .tabs button:hover{color:var(--text)}#pluginConfig .tabs button.active{color:var(--primary);border-bottom-color:var(--primary)}#pluginConfig .tabPanels{margin-top:8px}#pluginConfig label.fieldLine{display:flex;flex-direction:column;padding:8px 0;border-bottom:1px solid color-mix(in srgb,var(--text) 6%,transparent)}#pluginConfig label.fieldLine:last-child{border-bottom:none}#pluginConfig label.fieldLine span{font-size:12px;color:var(--muted);margin:0 0 4px 2px;letter-spacing:.3px}#pluginConfig .fieldLine.conditionsField{display:flex;flex-direction:column;padding:8px 0;border-bottom:1px solid color-mix(in srgb,var(--text) 6%,transparent)}#pluginConfig .fieldLine.conditionsField:last-child{border-bottom:none}#pluginConfig .fieldLine.conditionsField>span{font-size:12px;color:var(--muted);margin:0 0 6px 2px;letter-spacing:.3px}#pluginConfig .conditionsList{display:flex;flex-direction:row;flex-wrap:wrap;gap:6px}#pluginConfig .conditionsList .conditionsItem{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;font-size:12px;border:1px solid color-mix(in srgb,var(--text) 20%,transparent);border-radius:14px;cursor:pointer;transition:border-color .15s,background .15s}#pluginConfig .conditionsList .conditionsItem:hover{border-color:var(--primary)}#pluginConfig .conditionsList .conditionsItem:has(input:checked){background:color-mix(in srgb,var(--primary) 18%,transparent);border-color:var(--primary);color:var(--primary)}#pluginConfig .conditionsList .conditionsItem input[type=checkbox]{width:14px;height:14px;margin:0;cursor:pointer}#pluginConfig .languageLine{display:flex;flex-direction:row;align-items:center;gap:8px;font-size:14px;margin-top:10px;padding:8px 12px;background:color-mix(in srgb,var(--body) 92%,var(--text));border:1px solid color-mix(in srgb,var(--text) 12%,transparent);border-radius:10px;transition:border-color .15s,box-shadow .15s}#pluginConfig .languageLine:before{content:"\\1f310";font-size:16px;line-height:1}#pluginConfig .languageLine input{width:80px;background:transparent;border:none;height:24px;padding:0 2px;font-size:14px;box-shadow:none}#pluginConfig .languageLine:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 22%,transparent)}';
var overlay_default = '#pluginOverlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000000bf;z-index:2147483645;display:flex;flex-direction:column;align-items:center;justify-content:center;animation:overlayFadeIn .15s ease}@keyframes overlayFadeIn{0%{opacity:0}to{opacity:1}}#pluginOverlay .main{color:var(--text);font-size:16px;line-height:1.7;width:520px;max-width:calc(100% - 40px);max-height:70vh;background-color:var(--body);padding:28px 32px;margin:10px;overflow-y:auto;border-radius:14px;border:1px solid color-mix(in srgb,var(--text) 14%,transparent);box-shadow:0 12px 48px #00000073;scrollbar-width:thin;scrollbar-color:var(--muted) transparent;animation:overlayPanelIn .2s ease}@keyframes overlayPanelIn{0%{opacity:0;transform:translateY(-14px) scale(.98)}to{opacity:1;transform:none}}#pluginOverlay .main p{margin:0 0 12px}#pluginOverlay .main p:last-child{margin-bottom:0}@media(max-width:640px){#pluginOverlay .main{width:100%;max-width:none}}#pluginOverlay .checkbox-container{display:flex;align-items:center;margin:16px 0 12px}#pluginOverlay .checkbox-label{color:var(--text);font-size:16px;font-weight:600;margin-left:10px;display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none}#pluginOverlay .checkbox{appearance:none;-webkit-appearance:none;-moz-appearance:none;width:22px;height:22px;margin:0;padding:0;border:2px solid color-mix(in srgb,var(--text) 30%,transparent);border-radius:6px;background:color-mix(in srgb,var(--body) 92%,var(--text));cursor:pointer;position:relative;transition:background-color .15s,border-color .15s}#pluginOverlay .checkbox:hover{border-color:var(--primary)}#pluginOverlay .checkbox:checked{background:var(--primary);border-color:var(--primary)}#pluginOverlay .checkbox:checked:after{content:"";position:absolute;left:6px;top:2px;width:5px;height:10px;border:solid var(--primary-text);border-width:0 2px 2px 0;transform:rotate(45deg)}#pluginOverlay button{padding:10px 40px;color:var(--primary-text);font-size:15px;font-weight:600;border:none;border-radius:8px;cursor:pointer;background-color:var(--primary);transition:filter .15s,transform .1s}#pluginOverlay button:hover:not([disabled]){filter:brightness(1.12)}#pluginOverlay button:active:not([disabled]){transform:scale(.97)}#pluginOverlay button[disabled]{background-color:var(--muted);cursor:not-allowed;filter:none;transform:none}';
var videoCard_default = ".fixed-bottom-right{position:fixed;bottom:0;right:0;background-color:var(--body);color:var(--text);border-top:1px solid var(--primary);border-left:1px solid var(--primary);border-top-left-radius:6px;padding:2px 5px;margin:0;user-select:none;z-index:100}.downloaded{bottom:46px;right:2px;border-radius:2px;position:absolute;padding:3px 5px;background-color:#000c;pointer-events:none}.follow{bottom:24px;right:2px;border-radius:2px;position:absolute;padding:3px 5px;background-color:#000c;pointer-events:none}.selectButton{accent-color:var(--primary);position:absolute;width:38px;height:38px;right:0;cursor:pointer;z-index:100;top:22px}.deleteButton{accent-color:var(--danger);position:absolute;width:38px;height:38px;left:0;cursor:pointer;z-index:100;border:none;padding:0;margin:3px;top:22px}";
var toast_default = ".toast h3{margin:0 0 10px}.toast p{margin:0}.toast .toast-buttons{display:flex;gap:8px;margin-top:10px}.toast .toast-buttons button{flex:1;padding:6px 0;border:none;border-radius:6px;font-size:13px;line-height:1.4;cursor:pointer;color:#fff;background:#ffffff47}.toast .toast-buttons button:hover{filter:brightness(1.15)}.toast .toast-buttons button.aria2TrackRebuildRetry{background:#ffffff47}.toast .toast-buttons button.aria2TrackRebuildCancel{background:#00000047}.offscreen-container{position:absolute;visibility:hidden;pointer-events:none;max-width:480px}.toast-container{isolation:isolate;position:fixed;z-index:2147483647;display:flex;flex-direction:column;box-sizing:border-box;transition:transform calc(.6 * var(--toast-rate) * 1s) ease,opacity calc(.6 * var(--toast-rate) * 1s) ease}.toast-container.toast-top{top:0}.toast-container.toast-bottom{bottom:0}.toast-container.toast-left{left:0;align-items:flex-start}.toast-container.toast-center{left:50%;transform:translate(-50%);align-items:center}.toast-container.toast-right{right:0;align-items:flex-end}#toast-container-top-left .toast{margin:10px 0 0 10px;transform-origin:left center}#toast-container-top-center .toast{margin:10px 0 0;transform-origin:top}#toast-container-top-right .toast{margin:10px 10px 0 0;transform-origin:right center}#toast-container-bottom-left .toast{margin:0 0 10px 10px;transform-origin:left center}#toast-container-bottom-center .toast{margin:0 0 10px;transform-origin:bottom}#toast-container-bottom-right .toast{margin:0 10px 10px 0;transform-origin:right center}.toast{--toast-rate: 1;--toast-translate: 0;--toast-scale: 1;position:relative;user-select:none;-webkit-user-select:none;transition:transform calc(.4s * var(--toast-rate)) cubic-bezier(.34,1.56,.64,1),opacity calc(.3s * var(--toast-rate)) ease,max-height calc(.3s * var(--toast-rate)) ease;transform:translate3d(0,var(--toast-translate),0) scale(var(--toast-scale));max-width:480px;max-height:0px;opacity:0;will-change:transform,opacity;backface-visibility:hidden;contain:content;border-radius:6px;box-shadow:0 4px 8px #00000040}.toast-close{position:absolute;color:var(--text);top:5px;right:5px;cursor:pointer;font-size:18px;font-weight:700;line-height:18px;z-index:2147483648;transform-origin:center center}.toast-content{border-radius:6px;padding:14px 18px;max-width:100%;box-sizing:border-box;background:var(--primary);color:var(--primary-text);cursor:default;white-space:normal;word-break:break-all;overflow:hidden;position:relative}.toast.clickable .toast-content{cursor:pointer}.toast-progress{position:absolute;bottom:0;left:0;right:0;height:4px;background:#fffc;transform:scaleX(1);animation:toast-progress var(--toast-duration, 2s) linear forwards;will-change:transform;backface-visibility:hidden}@keyframes toast-progress{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.toast:hover{z-index:2147483648}.toast-container.toast-left .toast .toast-content .toast-progress{transform-origin:left}.toast-container.toast-center .toast .toast-content .toast-progress{transform-origin:center}.toast-container.toast-right .toast .toast-content .toast-progress{transform-origin:right}.toast.show{opacity:1;max-height:var(--toast-height)}.toast.hide{opacity:0;max-height:0px}";
var friendRequests_default = ".text--h1:has(>#pluginFriendsApproveHost),h1:has(>#pluginFriendsApproveHost),h2:has(>#pluginFriendsApproveHost){display:flex!important;align-items:center;justify-content:space-between}#pluginFriendsApproveHost{flex-shrink:0;margin:0}.friendsApproveButton{appearance:none;border:1px solid color-mix(in srgb,var(--primary, #4f8cff) 55%,transparent);background:color-mix(in srgb,var(--primary, #4f8cff) 16%,transparent);color:var(--text, inherit);border-radius:8px;padding:6px 16px;font-size:14px;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .1s ease}.friendsApproveButton:hover:not(:disabled){background:color-mix(in srgb,var(--primary, #4f8cff) 30%,transparent);border-color:var(--primary, #4f8cff)}.friendsApproveButton:active:not(:disabled){transform:scale(.97)}.friendsApproveButton:disabled{opacity:.5;cursor:not-allowed}.friendsApprove{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:2147483644;display:flex;flex-direction:column;gap:8px;min-width:280px;max-width:min(480px,90vw);padding:12px 16px;background:var(--body, #1b1b1f);color:var(--text, inherit);border:1px solid color-mix(in srgb,var(--text, #fff) 14%,transparent);border-radius:12px;box-shadow:0 8px 32px #0006;font-size:14px}.friendsApprove .friendsApproveText{text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.friendsApprove .friendsApproveTrack{position:relative;height:8px;border-radius:999px;background:color-mix(in srgb,var(--text, #fff) 12%,transparent);overflow:hidden}.friendsApprove .friendsApproveBar{height:100%;width:0%;border-radius:999px;background:linear-gradient(90deg,var(--primary, #4f8cff),color-mix(in srgb,var(--primary, #4f8cff) 60%,#fff));transition:width .25s ease}";
var beautify_default = ".page-profile__content *>.col-6.col-sm-4.col-lg-3,.moreFromUser__item,.moreLikeThis__item{margin-bottom:2%}@media(max-width:720px){.page-videoList__item{padding:0 6px 0 2px!important}}.videoTeaser{display:flex;flex-direction:column;height:100%;border-radius:6px;border:2px solid color-mix(in srgb,var(--text) 20%,transparent);overflow:hidden;transition:background-color .2s ease,border-color .2s ease;background-color:var(--body-alt)}.videoTeaser:hover{background-color:color-mix(in srgb,var(--body-alt) 90%,var(--text))}.videoTeaser__content{display:flex;flex-direction:column;flex:1;padding:2%}.videoTeaser__title{padding-left:2%;padding-right:2%}.videoTeaser__bottom{margin-top:auto!important;height:auto!important;padding-bottom:2%;padding-left:2%;padding-right:2%}";
var widescreen_default = ".container,.container-fluid{max-width:85%!important}.videoTeaser__title,.videoTeaser__byline{line-clamp:1!important;-webkit-line-clamp:1!important}@media(max-width:720px){.container,.container-fluid{max-width:100%!important}.videoTeaser__title,.videoTeaser__byline{line-clamp:2!important;-webkit-line-clamp:2!important}}";
var en_default = {
appName: "Iwara Download Tool",
language: "Language: ",
downloadPriority: "Download Quality: ",
downloadPath: "Download Path: ",
downloadProxy: "Download Proxy: ",
downloadProxyUsername: "Download Proxy Username: ",
downloadProxyPassword: "Download Proxy Password: ",
aria2Path: "Aria2 RPC: ",
aria2Token: "Aria2 Token: ",
iwaradlPath: "iwaradl RPC: ",
iwaradlToken: "iwaradl Token: ",
mediaCenterApi: "MediaCenter API: ",
mediaCenterApiKey: "MediaCenter API Key: ",
mediaCenterInfo: "→ MediaCenter ←",
downloadGroup: "Download",
generalTab: "General",
friendsTab: "Friends",
advancedTab: "Advanced",
downloadTab: "Download",
aria2Tab: "Aria2",
iwaradlTab: "iwaradl",
mediaCenterTab: "MediaCenter",
aria2Group: "Aria2",
proxyGroup: "Proxy",
mediaCenterGroup: "MediaCenter",
iwaradlGroup: "iwaradl",
priorityGroup: "Quality",
pathNormalizeGroup: "Path Normalization",
selectionGroup: "Selection",
visibilityGroup: "Visibility",
interfaceGroup: "Interface",
friendRequestGroup: "Friend Requests",
scriptGroup: "Script",
pathNormalize: "Path Normalize (NFKC)",
pathReplaceEmojis: "Replace Emoji",
pathFoldMarks: "Fold Combining Marks",
pathSanitize: "Sanitize Filename Characters",
pathTruncate: "Truncate Title/Alias Length",
pathTitleMaxLength: "Title Max Length: ",
pathAliasMaxLength: "Alias Max Length: ",
experimentalFeatures: "Experimental Features",
enableUnsafeMode: "Unsafe Mode (Use at your own risk)",
enableBeautify: "Beautify",
enableWidescreen: "Widescreen Adaptation",
save: "Save",
reset: "Reset",
ok: "OK",
delete: "Delete",
deleteSucceed: "Deletion successful!",
isDebug: "Debug Mode",
downloadType: "Download Type",
browserDownload: "Browser Download",
iwaradlDownload: "iwaradl Download",
autoFollow: "Automatically follow the selected video author",
autoLike: "Automatically like the selected videos",
addUnlistedAndPrivate: "Force display unlisted and private videos (requires following the author)",
filterUnlistedAndPrivate: "Filter unlisted and private videos on subscriptions page (mutually exclusive with force display)",
autoCollapseMenu: "Auto Collapse Menu",
friendRequestApprove: "One-click approve friend requests",
friendRequestApprovalMode: "Approval mode: ",
approvalMode_any: "Approve if any condition matches",
approvalMode_all: "Approve only if all conditions match",
friendRequestApprovalConditions: "Approval conditions (combined per mode above): ",
promoteScriptAuthor: "Automatically follow and friend the script author",
approvalCondition_always: "Always approve",
approvalCondition_followedBy: "They follow me",
approvalCondition_following: "I follow them",
approvalCondition_premium: "They are a Premium user",
approvalCondition_recentlyActive: "Active within 30 days",
approvalCondition_established: "Account older than 30 days",
approvalCondition_active: "Account in good standing",
approvalCondition_commentedProfile: "Commented on my profile",
approvalCondition_commentedForumThread: "Commented on the pinned forum thread",
friendApproveEvidenceThreadId: "Evidence thread ID (last segment of forum thread URL)",
friendApproveAll: "Approve all requests",
friendApproveFetching: "Fetching friend requests...",
friendApproveChecking: "Checking approval conditions",
friendApproveRunning: "Approving",
friendApproveDone: "Approval finished, succeeded",
friendApproveNone: "No friend requests received",
friendApproveError: "Approval failed!",
friendApproveReload: "Reload page",
friendApproveReloadHint: "reload to refresh the list",
notLoggedIn: "Not logged in or session expired!",
checkDownloadLink: "Check third-party cloud storage download links",
checkPriority: "Check download quality",
autoDownloadMetadata: "Auto-download metadata",
filterLikedVideos: "Filter liked videos on timeline",
videoMetadata: "Video Metadata",
autoInjectCheckbox: "Automatically inject selection box",
autoCopySaveFileName: "Automatically copy the filename generated by rules",
configurationIncompatible: "Initialization or configuration file incompatible, please reconfigure!",
browserDownloadNotEnabled: "Download feature not enabled!",
browserDownloadNotWhitelisted: "Requested file extension not whitelisted!",
browserDownloadNotPermitted: "Download feature enabled, but permission not granted!",
browserDownloadNotSupported: "Current browser/version does not support download functionality!",
browserDownloadNotSucceeded: "Download did not start or failed!",
browserDownloadUnknownError: "Unknown error, possibly due to invalid download parameters. Please check if the filename is valid!",
browserDownloadTimeout: "Download timed out. Please check your network connection!",
variable: "View available variables",
iwaradlLink: "→ iwaradl Official Website ←",
settings: "Open Settings",
downloadThis: "Download current video",
manualDownload: "Manual Download",
reverseSelect: "Reverse selection on this page",
deselectThis: "Deselect on this page",
deselectAll: "Deselect all",
selectThis: "Select all on this page",
selected: "Selected",
downloadSelected: "Download selected",
injectCheckbox: "Toggle selection box",
configError: "There is an error in the script configuration. Please modify.",
alreadyKnowHowToUse: "I already know how to use it!!!",
notice: [
"Loading Complete"
],
useHelpForBase: [
"Please read the usage guide carefully!",
{
nodeType: "br"
},
"Click the gray sidebar on the webpage to expand the script menu, then click the functions in the menu according to your needs.",
{
nodeType: "br"
},
{
nodeType: "br"
},
'Click "Settings" in the menu to open the config panel. Options are organized into tabs: General (download behavior / selection / visibility / interface), Download (path template & normalization), Aria2/iwaradl (downloader & proxy), MediaCenter, and Advanced (experimental & debugging).',
{
nodeType: "br"
},
'For first-time use, it is recommended to check the download path and path normalization (NFKC, Emoji, illegal characters, length truncation) on the "Download" tab.'
],
useHelpForInjectCheckbox: "Open any page with video cards, and the script will inject checkboxes on the video cards. Click the checkbox or hover over the video card and press space to select this video.",
useHelpForCheckDownloadLink: 'Enabling "%#checkDownloadLink#%" will check the video description and comments before downloading. If third-party cloud storage links are found, a prompt will appear allowing you to visit the video page.',
useHelpForManualDownload: [
'To use manual download, provide the video ID. For batch manual download, use "|" to separate video IDs.',
{
nodeType: "br"
},
"Example: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L"
],
useHelpForBugreport: [
"To report bugs or usage issues, please visit: ",
{
nodeType: "a",
childs: "Github",
attributes: {
href: "https://github.com/IwaraEnhance/IwaraDownloadTool/"
}
}
],
tryRestartingDownload: "→ Click here to restart download ←",
tryReparseDownload: "Reparse",
openVideoLink: "Open link",
copySucceed: "Copy succeeded!",
pushTaskSucceed: "Task pushed successfully!",
exportConfig: "Export Configuration",
importConfig: "Import Configuration",
exportConfigSucceed: "Configuration exported to clipboard!",
importConfigFail: "Configuration import failed!",
connectionTest: "Connection Test",
settingsCheck: "Settings Check",
createTask: "Create Task",
downloadPathError: "Download path error!",
browserDownloadModeError: "Please enable the browser API download mode in the script manager!",
downloadQualityError: "Specified quality download URL not found!",
findedDownloadLink: "Possible third-party cloud storage link found!",
allCompleted: "All parsing completed!",
following: "Following",
downloaded: "Downloaded",
parsingProgress: "Parsing Progress: ",
taskInProgress: "Parsing in progress, please wait...",
waitingForLock: "Waiting for another page's task to finish...",
manualDownloadTips: 'For individual downloads, input the video ID here. For batch downloads, separate video IDs with "|". Example: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L',
noAvailableVideoSource: "No available video sources",
videoSourceNotAvailable: "Video source URL unavailable",
getVideoSourceFailed: "Failed to get video source",
downloadFailed: "Download failed!",
pushTaskFailed: "Failed to push download task!",
parsingFailed: "Failed to parse video information!",
autoFollowFailed: "Failed to auto-follow the video author!",
autoLikeFailed: "Failed to auto-like the video!",
duplicateTaskAnalysisError: "An error occurred while analyzing duplicate download tasks!",
aria2TrackComplete: "Download complete",
aria2TrackRestart: "Download failed, auto-restarted",
aria2TrackRestartSlow: "Download speed too slow, auto-restarted",
aria2TrackRestartPaused: "Task paused, auto-restarted",
aria2TrackRebuildGiveUp: "Failed after 8 retries. Retry or give up?",
aria2TrackRebuildRetry: "Retry",
aria2TrackRebuildCancel: "Cancel",
aria2TrackRebuildGiveUpDone: "Retry abandoned, task removed from queue"
};
var ja_default = {
appName: "Iwara バッチダウンロードツール",
language: "言語: ",
downloadPriority: "ダウンロード画質: ",
downloadPath: "ダウンロード先: ",
downloadProxy: "ダウンロードプロキシ: ",
downloadProxyUsername: "ダウンロードプロキシユーザー名: ",
downloadProxyPassword: "ダウンロードプロキシパスワード: ",
aria2Path: "Aria2 RPC: ",
aria2Token: "Aria2 トークン: ",
iwaradlPath: "iwaradl RPC: ",
iwaradlToken: "iwaradl トークン: ",
mediaCenterApi: "MediaCenter API: ",
mediaCenterApiKey: "MediaCenter API キー: ",
mediaCenterInfo: "→ MediaCenter ←",
downloadGroup: "ダウンロード",
generalTab: "一般",
friendsTab: "フレンド申請",
advancedTab: "詳細",
downloadTab: "ダウンロード",
aria2Tab: "Aria2",
iwaradlTab: "iwaradl",
mediaCenterTab: "MediaCenter",
aria2Group: "Aria2",
proxyGroup: "プロキシ",
mediaCenterGroup: "MediaCenter",
iwaradlGroup: "iwaradl",
priorityGroup: "画質",
pathNormalizeGroup: "パス正規化",
selectionGroup: "選択",
visibilityGroup: "公開設定",
interfaceGroup: "インターフェース",
friendRequestGroup: "フレンドリクエスト",
scriptGroup: "スクリプト",
pathNormalize: "パス正規化 (NFKC)",
pathReplaceEmojis: "絵文字を置換",
pathFoldMarks: "結合記号を畳む",
pathSanitize: "不正なファイル名文字を除去",
pathTruncate: "タイトル/別名の長さを切り詰める",
pathTitleMaxLength: "タイトル最大長: ",
pathAliasMaxLength: "別名最大長: ",
experimentalFeatures: "実験的機能",
enableUnsafeMode: "アンセーフモード(自己責任で使用)",
enableBeautify: "美化",
enableWidescreen: "ワイドスクリーン適応",
save: "保存",
reset: "リセット",
ok: "OK",
delete: "削除",
deleteSucceed: "削除成功!",
isDebug: "デバッグモード",
downloadType: "ダウンロード方式",
browserDownload: "ブラウザダウンロード",
iwaradlDownload: "iwaradlダウンロード",
autoFollow: "選択した動画の作者を自動フォロー",
autoLike: "選択した動画を自動いいね",
addUnlistedAndPrivate: "非公開・限定公開動画を強制表示(作者のフォローが必要)",
filterUnlistedAndPrivate: "購読ページの非公開・限定公開動画をフィルタリング(強制表示とは排他)",
autoCollapseMenu: "メニューを自動で折りたたむ",
friendRequestApprove: "フレンドリクエストを一括承認",
friendRequestApprovalMode: "承認モード:",
approvalMode_any: "いずれかの条件を満たせば承認",
approvalMode_all: "すべての条件を満たした場合のみ承認",
friendRequestApprovalConditions: "承認条件(上のモードで組み合わせ): ",
promoteScriptAuthor: "スクリプト作者を自動でフォロー・フレンド申請する",
approvalCondition_always: "無条件で承認",
approvalCondition_followedBy: "相手が自分をフォロー中",
approvalCondition_following: "自分が相手をフォロー中",
approvalCondition_premium: "相手はプレミアムユーザー",
approvalCondition_recentlyActive: "相手は30日以内にアクティブ",
approvalCondition_established: "相手は登録から30日以上経過",
approvalCondition_active: "相手のアカウントは正常状態",
approvalCondition_commentedProfile: "相手はプロフィールにコメントした",
approvalCondition_commentedForumThread: "相手は指定フォーラム投稿にコメントした",
friendApproveEvidenceThreadId: "証拠スレッド ID(フォーラム URL の末尾セグメント)",
friendApproveAll: "すべてのリクエストを承認",
friendApproveFetching: "フレンドリクエストを取得中...",
friendApproveChecking: "承認条件を確認中",
friendApproveRunning: "承認中",
friendApproveDone: "承認完了、成功",
friendApproveNone: "フレンドリクエストはありません",
friendApproveError: "一括承認でエラーが発生しました!",
friendApproveReload: "ページを再読み込み",
friendApproveReloadHint: "リストを更新するには再読み込み",
notLoggedIn: "未ログインまたはセッションが期限切れです!",
checkDownloadLink: "サードパーティクラウドストレージのダウンロードリンクをチェック",
checkPriority: "ダウンロード画質チェック",
autoDownloadMetadata: "動画メタデータを自動ダウンロード",
filterLikedVideos: "タイムライン上のいいね済み動画をフィルタリング",
videoMetadata: "動画メタデータ",
autoInjectCheckbox: "選択ボックスを自動注入",
autoCopySaveFileName: "ルールに基づいて生成されたファイル名を自動コピー",
configurationIncompatible: "初期化または設定ファイルが互換性がありません。再設定してください!",
browserDownloadNotEnabled: "ダウンロード機能が有効になっていません!",
browserDownloadNotWhitelisted: "要求されたファイル拡張子がホワイトリストに登録されていません!",
browserDownloadNotPermitted: "ダウンロード機能は有効ですが、権限が付与されていません!",
browserDownloadNotSupported: "現在のブラウザ/バージョンはダウンロード機能をサポートしていません!",
browserDownloadNotSucceeded: "ダウンロードが開始されなかったか失敗しました!",
browserDownloadUnknownError: "不明なエラー。ダウンロード時に提供されたパラメータに問題がある可能性があります。ファイル名が有効か確認してください!",
browserDownloadTimeout: "ダウンロードがタイムアウトしました。ネットワーク環境を確認してください!",
variable: "→ 利用可能な変数を表示 ←",
iwaradlLink: "→ iwaradl 公式サイト ←",
settings: "設定を開く",
downloadThis: "現在の動画をダウンロード",
manualDownload: "手動ダウンロード",
reverseSelect: "このページで選択を反転",
deselectThis: "このページの選択を解除",
deselectAll: "すべての選択を解除",
selectThis: "このページをすべて選択",
selected: "選択済み",
downloadSelected: "選択したものをダウンロード",
injectCheckbox: "選択ボックスを切り替え",
configError: "スクリプト設定にエラーがあります。修正してください。",
alreadyKnowHowToUse: "使用方法を理解しました!!!(このページは一度だけ表示されます)",
notice: [
"読み込み完了"
],
useHelpForBase: [
"使用ガイドをよくお読みください!",
{
nodeType: "br"
},
"ウェブページのサイドにある灰色のサイドバーをクリックしてスクリプトメニューを展開し、必要に応じてメニューの機能をクリックしてください。",
{
nodeType: "br"
},
{
nodeType: "br"
},
"メニューの「設定」をクリックすると設定パネルが開きます。設定はタブで分類されています:一般(ダウンロード動作/選択/公開設定/インターフェース)、ダウンロード(パステンプレートと正規化)、Aria2/iwaradl(ダウンローダーとプロキシ)、MediaCenter、詳細(実験的機能とデバッグ)。",
{
nodeType: "br"
},
"初回使用時は、「ダウンロード」タブのダウンロード先とパス正規化(NFKC、絵文字、不正文字、長さ切り詰め)が要件に合っているか確認することをお勧めします。"
],
useHelpForInjectCheckbox: "動画カードがある任意のページを開くと、スクリプトは動画カードにチェックボックスを注入します。チェックボックスをクリックするか、動画カードにマウスをホバーしてスペースキーを押すと、この動画が選択されます。",
useHelpForCheckDownloadLink: '"%#checkDownloadLink#%"機能を有効にすると、動画をダウンロードする前に動画の説明とコメントをチェックします。サードパーティクラウドストレージのダウンロードリンクが見つかった場合、プロンプトが表示され、動画ページを開くことができます。',
useHelpForManualDownload: [
"手動ダウンロード機能を使用するには、動画IDを提供する必要があります。バッチ手動ダウンロードの場合は、「|」で区切った動画IDを提供してください。",
{
nodeType: "br"
},
"例: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L"
],
useHelpForBugreport: [
"バグや使用上の問題を報告する場合は、以下にアクセスしてください: ",
{
nodeType: "a",
childs: "Github",
attributes: {
href: "https://github.com/IwaraEnhance/IwaraDownloadTool/"
}
}
],
tryRestartingDownload: "→ ここをクリックしてダウンロードを再開 ←",
tryReparseDownload: "再解析",
openVideoLink: "リンクを開く",
copySucceed: "コピー成功!",
pushTaskSucceed: "ダウンロードタスクのプッシュ成功!",
exportConfig: "設定をエクスポート",
importConfig: "設定をインポート",
exportConfigSucceed: "設定がクリップボードにエクスポートされました!",
importConfigFail: "設定のインポートに失敗しました!",
connectionTest: "接続テスト",
settingsCheck: "設定チェック",
createTask: "タスク作成",
downloadPathError: "ダウンロードパスエラー!",
browserDownloadModeError: "スクリプトマネージャーのブラウザAPIダウンロードモードを有効にしてください!",
downloadQualityError: "指定された画質のダウンロードURLが見つかりません!",
findedDownloadLink: "サードパーティクラウドストレージのダウンロードリンクを発見!",
allCompleted: "すべての解析完了!",
following: "フォロー中",
downloaded: "ダウンロード済み",
parsingProgress: "解析進捗: ",
taskInProgress: "解析中です。しばらくお待ちください…",
waitingForLock: "他のページのタスク完了を待っています…",
manualDownloadTips: "個別ダウンロードはここに動画IDを直接入力してください。バッチダウンロードは「|」で区切った動画IDを提供してください。例: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L",
noAvailableVideoSource: "利用可能な動画ソースがありません",
videoSourceNotAvailable: "動画ソースURLが利用できません",
getVideoSourceFailed: "動画ソースの取得に失敗しました",
downloadFailed: "ダウンロード失敗!",
pushTaskFailed: "ダウンロードタスクのプッシュに失敗!",
parsingFailed: "動画情報の解析に失敗!",
autoFollowFailed: "動画作者の自動フォローに失敗!",
autoLikeFailed: "動画の自動いいねに失敗!",
duplicateTaskAnalysisError: "重複したダウンロードタスクの分析中にエラーが発生しました!",
aria2TrackComplete: "ダウンロード完了",
aria2TrackRestart: "ダウンロードに失敗しました。自動再開しました",
aria2TrackRestartSlow: "ダウンロード速度が遅すぎるため、自動再開しました",
aria2TrackRestartPaused: "タスクが一時停止したため、自動再開しました",
aria2TrackRebuildGiveUp: "8回リトライしても失敗しました。再試行しますか?",
aria2TrackRebuildRetry: "再試行",
aria2TrackRebuildCancel: "キャンセル",
aria2TrackRebuildGiveUpDone: "再試行を中止し、タスクをキューから削除しました"
};
var zh_cn_default = {
appName: "Iwara 批量下载工具",
language: "语言: ",
downloadPriority: "下载画质: ",
downloadPath: "下载到: ",
downloadProxy: "下载代理: ",
downloadProxyUsername: "下载代理用户名: ",
downloadProxyPassword: "下载代理密码: ",
aria2Path: "Aria2 RPC: ",
aria2Token: "Aria2 密钥: ",
iwaradlPath: "iwaradl RPC: ",
iwaradlToken: "iwaradl 密钥: ",
mediaCenterApi: "MediaCenter API: ",
mediaCenterApiKey: "MediaCenter API 密钥: ",
mediaCenterInfo: "→ MediaCenter项目 ←",
downloadGroup: "下载",
generalTab: "常规",
friendsTab: "好友请求",
advancedTab: "高级",
downloadTab: "下载",
aria2Tab: "Aria2",
iwaradlTab: "iwaradl",
mediaCenterTab: "MediaCenter",
aria2Group: "Aria2",
proxyGroup: "代理",
mediaCenterGroup: "MediaCenter",
iwaradlGroup: "iwaradl",
priorityGroup: "画质",
pathNormalizeGroup: "路径规范化",
selectionGroup: "选择",
visibilityGroup: "可见性",
interfaceGroup: "界面",
friendRequestGroup: "好友请求",
scriptGroup: "脚本",
pathNormalize: "路径规范化 (NFKC)",
pathReplaceEmojis: "替换 Emoji",
pathFoldMarks: "折叠组合变音标记",
pathSanitize: "清理非法文件名字符",
pathTruncate: "截断标题/别名长度",
pathTitleMaxLength: "标题最大长度: ",
pathAliasMaxLength: "别名最大长度: ",
experimentalFeatures: "实验性功能",
enableUnsafeMode: "激进模式(风险自行承担)",
enableBeautify: "美化",
enableWidescreen: "宽屏适配",
save: "保存",
reset: "重置",
ok: "确定",
delete: "删除",
deleteSucceed: "删除成功!",
isDebug: "调试模式",
downloadType: "下载方式",
browserDownload: "浏览器下载",
iwaradlDownload: "iwaradl下载",
autoFollow: "自动关注选中的视频作者",
autoLike: "自动点赞选中的视频",
addUnlistedAndPrivate: "不公开和私有视频强制显示(需关注作者)",
filterUnlistedAndPrivate: "过滤订阅页中的不公开和私有视频(与强制显示互斥)",
autoCollapseMenu: "菜单自动收起",
friendRequestApprove: "一键审批好友请求",
friendRequestApprovalMode: "审批模式:",
approvalMode_any: "任一条件满足即批准",
approvalMode_all: "所有条件满足才批准",
friendRequestApprovalConditions: "审批条件(按上方模式组合): ",
promoteScriptAuthor: "启动时关注并添加脚本作者为好友",
approvalCondition_always: "无条件批准",
approvalCondition_followedBy: "对方已关注我",
approvalCondition_following: "我已关注对方",
approvalCondition_premium: "对方是 Premium 用户",
approvalCondition_recentlyActive: "对方 30 天内活跃过",
approvalCondition_established: "对方注册满 30 天",
approvalCondition_active: "对方账号状态正常",
approvalCondition_commentedProfile: "对方在我的个人主页评论过",
approvalCondition_commentedForumThread: "对方在指定论坛帖下评论过",
friendApproveEvidenceThreadId: "证据帖 ID(论坛帖 URL 末段)",
friendApproveAll: "一键审批全部请求",
friendApproveFetching: "正在获取好友请求...",
friendApproveChecking: "正在校验审批条件",
friendApproveRunning: "正在批准",
friendApproveDone: "批准完成,成功",
friendApproveNone: "没有收到好友请求",
friendApproveError: "一键审批出错!",
friendApproveReload: "刷新页面",
friendApproveReloadHint: "刷新页面以更新列表",
notLoggedIn: "未登录或登录态已失效!",
checkDownloadLink: "第三方网盘下载地址检查",
checkPriority: "下载画质检查",
autoDownloadMetadata: "自动下载视频元数据",
filterLikedVideos: "过滤时间线上的已喜欢视频",
videoMetadata: "视频元数据",
autoInjectCheckbox: "自动注入选择框",
autoCopySaveFileName: "自动复制根据规则生成的文件名",
configurationIncompatible: "初始化或配置文件不兼容,请重新配置!",
browserDownloadNotEnabled: "未启用下载功能!",
browserDownloadNotWhitelisted: "请求的文件扩展名未列入白名单!",
browserDownloadNotPermitted: "下载功能已启用,但未授予下载权限!",
browserDownloadNotSupported: "目前浏览器/版本不支持下载功能!",
browserDownloadNotSucceeded: "下载未开始或失败!",
browserDownloadUnknownError: "未知错误,有可能是下载时提供的参数存在问题,请检查文件名是否合法!",
browserDownloadTimeout: "下载超时,请检查网络环境是否正常!",
variable: "→ 查看可用变量 ←",
iwaradlLink: "→ iwaradl 官网 ←",
settings: "打开设置",
downloadThis: "下载当前视频",
manualDownload: "手动下载",
reverseSelect: "本页反向选中",
deselectThis: "取消本页选中",
deselectAll: "取消所有选中",
selectThis: "本页全部选中",
downloadSelected: "下载所选",
selected: "已选中",
injectCheckbox: "开关选择框",
configError: "脚本配置中存在错误,请修改。",
alreadyKnowHowToUse: "我已知晓如何使用!!!(此页面仅显示一次)",
notice: [
"加载完成"
],
useHelpForBase: [
"请认真阅读使用指南!",
{
nodeType: "br"
},
"点击网页侧边的灰色侧栏展开脚本菜单,根据需求点击菜单中的功能。",
{
nodeType: "br"
},
{
nodeType: "br"
},
"点击菜单中的“设置”可打开配置面板,配置项按标签页分类:常规(下载行为/选择/可见性/界面)、下载(路径模板与规范化)、Aria2/iwaradl(下载器与代理)、MediaCenter、高级(实验性与调试)。",
{
nodeType: "br"
},
"首次使用建议先检查“下载”标签页中的下载路径与路径规范化(NFKC、Emoji、非法字符、长度截断)是否符合需求。"
],
useHelpForInjectCheckbox: "打开任意存在视频卡片的页面,脚本会在视频卡片上注入复选框,点击复选框或鼠标悬浮在视频卡片上按空格将会勾选此视频。",
useHelpForCheckDownloadLink: "开启“%#checkDownloadLink#%”功能会在下载视频前会检查视频简介以及评论,如果在其中发现疑似第三方网盘下载链接,将会弹出提示,您可以点击提示打开视频页面。",
useHelpForManualDownload: [
"使用手动下载功能需要提供视频ID,如需批量手动下载请提供使用“|”分割的视频ID。",
{
nodeType: "br"
},
"例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L"
],
useHelpForBugreport: [
"反馈遇到的BUG、使用问题等请前往: ",
{
nodeType: "a",
childs: "Github",
attributes: {
href: "https://github.com/IwaraEnhance/IwaraDownloadTool/"
}
}
],
tryRestartingDownload: "→ 点击此处重新下载 ←",
tryReparseDownload: "重新解析",
openVideoLink: "打开链接",
copySucceed: "复制成功!",
pushTaskSucceed: "推送下载任务成功",
pushTaskFail: "推送下载任务失败",
exportConfig: "导出配置",
importConfig: "导入配置",
exportConfigSucceed: "配置已导出至剪切板!",
importConfigFail: "配置导入失败!",
connectionTest: "连接测试",
settingsCheck: "配置检查",
createTask: "创建任务",
downloadPathError: "下载路径错误!",
browserDownloadModeError: "请启用脚本管理器的浏览器API下载模式!",
downloadQualityError: "未找到指定的画质下载地址!",
findedDownloadLink: "发现疑似第三方网盘下载地址!",
allCompleted: "全部解析完成!",
following: "已关注",
downloaded: "已下载",
parsingProgress: "解析进度: ",
taskInProgress: "解析任务进行中,请稍候…",
waitingForLock: "正在等待其他页面的任务完成…",
manualDownloadTips: "单独下载请直接在此处输入视频ID, 批量下载请提供使用“|”分割的视频ID, 例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L",
noAvailableVideoSource: "没有可供下载的视频源",
videoSourceNotAvailable: "视频源地址不可用",
getVideoSourceFailed: "获取视频源失败",
downloadFailed: "下载失败!",
pushTaskFailed: "推送下载任务失败!",
parsingFailed: "视频信息解析失败!",
autoFollowFailed: "自动关注视频作者失败!",
autoLikeFailed: "自动点赞视频失败!",
duplicateTaskAnalysisError: "分析重复的的下载任务时出现错误!",
aria2TrackComplete: "下载完成",
aria2TrackRestart: "下载失败,已自动重启",
aria2TrackRestartSlow: "下载速度过慢,已自动重启",
aria2TrackRestartPaused: "任务已暂停,已自动重启",
aria2TrackRebuildGiveUp: "已连续重试 8 次仍未成功,是否继续重试?",
aria2TrackRebuildRetry: "重试",
aria2TrackRebuildCancel: "取消",
aria2TrackRebuildGiveUpDone: "已放弃重试,任务已从队列移除"
};
var i18nList = {
en: en_default,
ja: ja_default,
zh: zh_cn_default
};
var DownloadType = ((DownloadType2) => {
DownloadType2[DownloadType2["Aria2"] = 0] = "Aria2";
DownloadType2[DownloadType2["Iwaradl"] = 1] = "Iwaradl";
DownloadType2[DownloadType2["Browser"] = 2] = "Browser";
DownloadType2[DownloadType2["Others"] = 3] = "Others";
return DownloadType2;
})(DownloadType || {});
var PageType = ((PageType4) => {
PageType4["Video"] = "video";
PageType4["Image"] = "image";
PageType4["VideoList"] = "videoList";
PageType4["ImageList"] = "imageList";
PageType4["Forum"] = "forum";
PageType4["ForumSection"] = "forumSection";
PageType4["ForumThread"] = "forumThread";
PageType4["Page"] = "page";
PageType4["Home"] = "home";
PageType4["Profile"] = "profile";
PageType4["Subscriptions"] = "subscriptions";
PageType4["Playlist"] = "playlist";
PageType4["Favorites"] = "favorites";
PageType4["History"] = "history";
PageType4["Search"] = "search";
PageType4["Account"] = "account";
PageType4["Post"] = "post";
PageType4["Friends"] = "friends";
PageType4["Messages"] = "messages";
PageType4["Notifications"] = "notifications";
PageType4["Auth"] = "auth";
PageType4["Product"] = "product";
PageType4["Create"] = "create";
PageType4["Rule"] = "rule";
PageType4["Admin"] = "admin";
return PageType4;
})(PageType || {});
var ToastType = ((ToastType3) => {
ToastType3[ToastType3["Log"] = 0] = "Log";
ToastType3[ToastType3["Info"] = 1] = "Info";
ToastType3[ToastType3["Warn"] = 2] = "Warn";
ToastType3[ToastType3["Error"] = 3] = "Error";
return ToastType3;
})(ToastType || {});
var MessageType = ((MessageType2) => {
MessageType2[MessageType2["Close"] = 0] = "Close";
MessageType2[MessageType2["Request"] = 1] = "Request";
MessageType2[MessageType2["Receive"] = 2] = "Receive";
MessageType2[MessageType2["Set"] = 3] = "Set";
MessageType2[MessageType2["Del"] = 4] = "Del";
return MessageType2;
})(MessageType || {});
var VersionState = ((VersionState2) => {
VersionState2[VersionState2["Low"] = 0] = "Low";
VersionState2[VersionState2["Equal"] = 1] = "Equal";
VersionState2[VersionState2["High"] = 2] = "High";
return VersionState2;
})(VersionState || {});
init_env();
init_env();
init_log();
var log2 = createLogger("Config");
var DEFAULT_CONFIG = {
language: "zh",
autoFollow: false,
autoLike: false,
autoCopySaveFileName: false,
autoDownloadMetadata: false,
enableUnsafeMode: false,
enableBeautify: false,
enableWidescreen: false,
experimentalFeatures: false,
autoInjectCheckbox: true,
checkDownloadLink: false,
filterLikedVideos: false,
checkPriority: true,
addUnlistedAndPrivate: false,
filterUnlistedAndPrivate: false,
autoCollapseMenu: true,
friendRequestApprove: false,
friendRequestApprovalConditions: ["always"],
friendRequestApprovalMode: "any",
friendApproveEvidenceThreadId: "",
promoteScriptAuthor: true,
downloadPriority: "Source",
downloadType: 3,
downloadPath: "/Iwara/%#AUTHOR#%/%#TITLE#%[%#ID#%].mp4",
downloadProxy: "",
downloadProxyUsername: "",
downloadProxyPassword: "",
aria2Path: "http://127.0.0.1:6800/jsonrpc",
aria2Token: "",
iwaradlPath: "http://127.0.0.1:23456/api/tasks",
iwaradlToken: "",
mediaCenterApi: "http://127.0.0.1:3000",
mediaCenterApiKey: "",
pathNormalize: true,
pathReplaceEmojis: true,
pathFoldMarks: true,
pathSanitize: true,
pathTruncate: true,
pathTitleMaxLength: 72,
pathAliasMaxLength: 64,
priority: {
Source: 100,
"540": 99,
"360": 98,
preview: 1
}
};
var Config = class _Config {
static instance;
configChange;
authorization;
language = DEFAULT_CONFIG.language;
autoFollow = DEFAULT_CONFIG.autoFollow;
autoLike = DEFAULT_CONFIG.autoLike;
autoDownloadMetadata = DEFAULT_CONFIG.autoDownloadMetadata;
addUnlistedAndPrivate = DEFAULT_CONFIG.addUnlistedAndPrivate;
filterUnlistedAndPrivate = DEFAULT_CONFIG.filterUnlistedAndPrivate;
autoCollapseMenu = DEFAULT_CONFIG.autoCollapseMenu;
friendRequestApprove = DEFAULT_CONFIG.friendRequestApprove;
friendRequestApprovalConditions = DEFAULT_CONFIG.friendRequestApprovalConditions;
friendRequestApprovalMode = DEFAULT_CONFIG.friendRequestApprovalMode;
friendApproveEvidenceThreadId = DEFAULT_CONFIG.friendApproveEvidenceThreadId;
promoteScriptAuthor = DEFAULT_CONFIG.promoteScriptAuthor;
enableUnsafeMode = DEFAULT_CONFIG.enableUnsafeMode;
enableBeautify = DEFAULT_CONFIG.enableBeautify;
enableWidescreen = DEFAULT_CONFIG.enableWidescreen;
experimentalFeatures = DEFAULT_CONFIG.experimentalFeatures;
autoInjectCheckbox = DEFAULT_CONFIG.autoInjectCheckbox;
autoCopySaveFileName = DEFAULT_CONFIG.autoCopySaveFileName;
filterLikedVideos = DEFAULT_CONFIG.filterLikedVideos;
checkDownloadLink = DEFAULT_CONFIG.checkDownloadLink;
checkPriority = DEFAULT_CONFIG.checkPriority;
downloadPriority = DEFAULT_CONFIG.downloadPriority;
downloadType = DEFAULT_CONFIG.downloadType;
downloadPath = DEFAULT_CONFIG.downloadPath;
downloadProxy = DEFAULT_CONFIG.downloadProxy;
downloadProxyUsername = DEFAULT_CONFIG.downloadProxyUsername;
downloadProxyPassword = DEFAULT_CONFIG.downloadProxyPassword;
aria2Path = DEFAULT_CONFIG.aria2Path;
aria2Token = DEFAULT_CONFIG.aria2Token;
iwaradlPath = DEFAULT_CONFIG.iwaradlPath;
iwaradlToken = DEFAULT_CONFIG.iwaradlToken;
mediaCenterApi = DEFAULT_CONFIG.mediaCenterApi;
mediaCenterApiKey = DEFAULT_CONFIG.mediaCenterApiKey;
pathNormalize = DEFAULT_CONFIG.pathNormalize;
pathReplaceEmojis = DEFAULT_CONFIG.pathReplaceEmojis;
pathFoldMarks = DEFAULT_CONFIG.pathFoldMarks;
pathSanitize = DEFAULT_CONFIG.pathSanitize;
pathTruncate = DEFAULT_CONFIG.pathTruncate;
pathTitleMaxLength = DEFAULT_CONFIG.pathTitleMaxLength;
pathAliasMaxLength = DEFAULT_CONFIG.pathAliasMaxLength;
priority = DEFAULT_CONFIG.priority;
constructor(importConfig2) {
let body = new Proxy(this, {
get: function(target, property) {
if (property === "configChange") {
return target.configChange;
}
let value = GM_getValue(property, target[property]);
if (property === "language") {
return _Config.getLanguage(value);
}
return value;
},
set: function(target, property, value) {
if (property === "configChange") {
target.configChange = value;
return true;
}
GM_setValue(property, value);
if (!isNullOrUndefined(target.configChange)) target.configChange(property);
return true;
}
});
for (const key of Object.keys(DEFAULT_CONFIG)) {
GM_addValueChangeListener(key, (name, old_value, new_value, remote) => {
if (remote && !isNullOrUndefined(body.configChange)) body.configChange(name);
});
}
if (!isNullOrUndefined(importConfig2)) {
Object.assign(body, importConfig2);
}
return body;
}
static getInstance() {
if (isNullOrUndefined(_Config.instance)) _Config.instance = new _Config();
return _Config.instance;
}
static destroyInstance() {
_Config.instance = void 0;
}
static initInstance(importConfig2) {
_Config.instance = new _Config(importConfig2 ?? DEFAULT_CONFIG);
}
static resolveLanguage(lang) {
const normalized = lang.replace("-", "_").toLowerCase();
if (i18nList[normalized]) return normalized;
const main2 = normalized.split("_")[0];
if (i18nList[main2]) return main2;
return void 0;
}
static getLanguage(value) {
const candidates = [value, navigator.language, ...navigator.languages ?? []];
for (const lang of candidates) {
if (!lang) continue;
const resolved = _Config.resolveLanguage(lang);
if (resolved) return resolved;
}
return DEFAULT_CONFIG.language;
}
};
var config = Config.getInstance();
var Version = class _Version {
major;
minor;
patch;
preRelease;
buildMetadata;
constructor(versionString) {
if (!versionString || typeof versionString !== "string") {
throw new Error("Invalid version string");
}
const dashIndex = versionString.indexOf("-");
const plusIndex = versionString.indexOf("+");
let version, preRelease, buildMetadata;
if (dashIndex === -1 && plusIndex === -1) {
version = versionString;
preRelease = "";
buildMetadata = "";
} else if (plusIndex !== -1 && (dashIndex === -1 || plusIndex < dashIndex)) {
version = versionString.substring(0, plusIndex);
preRelease = "";
buildMetadata = versionString.substring(plusIndex + 1);
} else {
version = versionString.substring(0, dashIndex);
const rest = versionString.substring(dashIndex + 1);
const buildSep = rest.indexOf("+");
if (buildSep !== -1) {
preRelease = rest.substring(0, buildSep);
buildMetadata = rest.substring(buildSep + 1);
} else {
preRelease = rest;
buildMetadata = "";
}
}
const versionParts = version.split(".").map(Number);
if (versionParts.some(isNaN)) {
throw new Error("Version string contains invalid numbers");
}
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 || "";
}
static compareValues(a2, b2) {
if (a2 < b2) return 0;
if (a2 > b2) return 2;
return 1;
}
compare(other) {
let state = _Version.compareValues(this.major, other.major);
if (state !== 1) return state;
state = _Version.compareValues(this.minor, other.minor);
if (state !== 1) return state;
state = _Version.compareValues(this.patch, other.patch);
if (state !== 1) return state;
if (this.preRelease.length === 0 && other.preRelease.length > 0) {
return 2;
}
if (this.preRelease.length > 0 && other.preRelease.length === 0) {
return 0;
}
const maxLen = Math.max(this.preRelease.length, other.preRelease.length);
for (let i = 0; i < maxLen; i++) {
if (i >= this.preRelease.length) return 0;
if (i >= other.preRelease.length) return 2;
const pre1 = this.preRelease[i];
const pre2 = other.preRelease[i];
const isNum1 = !isNaN(+pre1);
const isNum2 = !isNaN(+pre2);
if (isNum1 && !isNum2) return 0;
if (!isNum1 && isNum2) return 2;
const val1 = isNum1 ? +pre1 : pre1;
const val2 = isNum2 ? +pre2 : pre2;
if (val1 < val2) return 0;
if (val1 > val2) return 2;
}
return 1;
}
baseCompare(other) {
let state = _Version.compareValues(this.major, other.major);
if (state !== 1) return state;
state = _Version.compareValues(this.minor, other.minor);
if (state !== 1) return state;
return _Version.compareValues(this.patch, other.patch);
}
toString() {
const version = `${this.major}.${this.minor}.${this.patch}`;
const preRelease = this.preRelease.length ? `-${this.preRelease.join(".")}` : "";
const buildMetadata = this.buildMetadata ? `+${this.buildMetadata}` : "";
return `${version}${preRelease}${buildMetadata}`;
}
};
init_log();
init_env();
init_constants();
var log4 = createLogger("Migration");
var migrations = [
{ from: "3.3.0", name: "reset-to-first-run", run: () => GM_setValue(GM_KEY_IS_FIRST_RUN, true) },
{
from: "3.3.31",
name: "clear-selectlist-and-db",
run: async ({ selectList: selectList2 }) => {
selectList2.clear();
GM_deleteValue(GM_KEY_SELECT_LIST);
const { db: db2 } = await Promise.resolve().then(() => (init_db(), db_exports));
await db2.delete();
}
},
{
from: "3.3.129",
name: "aria2track-queue-split",
silent: true,
run: () => {
const rawQueue = GM_getValue("Aria2TrackQueue", []);
if (!Array.isArray(rawQueue) || rawQueue.length === 0) return;
const MIGRATION_LOCK_KEY = "GMLock:aria2TrackManager";
const MIGRATION_LOCK_OWNER = "migration-queue-split";
GM_setValue(MIGRATION_LOCK_KEY, { owner: MIGRATION_LOCK_OWNER, expires: Date.now() + 3e4 });
const rawDone = GM_getValue("Aria2TrackDone", []);
const migrated = Array.isArray(rawDone) ? rawDone.filter((e) => Array.isArray(e) && isString(e[0]) && isConvertibleToNumber(e[1])) : typeof rawDone === "object" && rawDone !== null ? Object.entries(rawDone).filter((e) => isConvertibleToNumber(e[1])) : [];
const doneKeys = new Set(migrated.map(([k2]) => k2));
const liveQueue = [];
let doneCount = 0;
for (const [key, value] of rawQueue) {
if (!isString(key) || !value || !isString(value.videoId)) {
liveQueue.push([key, value]);
continue;
}
const isDone = !isNullOrUndefined(value.pushedAt) || !isNullOrUndefined(value.pushDone) || !isNullOrUndefined(value.completedAt) && !value.pushFailedAt;
if (isDone) {
const doneAt = [value.pushedAt, value.completedAt, value.addedAt].find(
(t) => isConvertibleToNumber(t)
);
if (!isNullOrUndefined(doneAt) && !doneKeys.has(key)) {
migrated.push([key, doneAt]);
doneKeys.add(key);
doneCount++;
}
continue;
}
const { completedAt: _c, pushDone: _p, pushedAt: _pd, ...live } = value;
liveQueue.push([key, live]);
}
if (doneCount > 0) GM_setValue("Aria2TrackDone", migrated);
GM_setValue("Aria2TrackQueue", liveQueue);
GM_deleteValue(MIGRATION_LOCK_KEY);
log4.info(`aria2track-queue-split: 终态迁移 ${doneCount} 条 → Aria2TrackDone(entries 形态),活任务 ${liveQueue.length} 条留队`);
}
}
];
async function runMigrations(deps) {
const installedVersion = GM_getValue(GM_KEY_VERSION, "");
if (installedVersion.isEmpty()) return "none";
const installed2 = new Version(installedVersion);
const pending = migrations.filter(({ from }) => installed2.baseCompare(new Version(from)) === 0);
if (pending.length === 0) return "none";
if (pending.some((m2) => !m2.silent)) deps.notifyIncompatible?.();
for (const { from, name, run } of pending) {
try {
log4.info(`migration: ${name} (installed < ${from})`);
await run(deps);
} catch (error) {
log4.error(`migration failed: ${name}`, error);
return "failed";
}
}
GM_setValue(GM_KEY_VERSION, GM_info.script.version);
return "reload";
}
var resolver = (text) => text;
function setPlaceholderResolver(fn) {
resolver = fn;
}
function resolvePlaceholders(text) {
return resolver(text);
}
function hasPlaceholderResolver() {
return resolver !== identity;
}
function identity(text) {
return text;
}
init_env();
init_hijack();
init_env();
var import_dayjs = __toESM(require_dayjs_min(), 1);
Date.prototype.format = function(format) {
return (0, import_dayjs.default)(this).format(format);
};
Date.prototype.add = function({ years = 0, months = 0, days = 0, hours = 0, minutes = 0, seconds = 0, ms = 0 } = {}) {
return (0, import_dayjs.default)(this).add(years, "year").add(months, "month").add(days, "day").add(hours, "hour").add(minutes, "minute").add(seconds, "second").add(ms, "millisecond").toDate();
};
Date.prototype.sub = function({ years = 0, months = 0, days = 0, hours = 0, minutes = 0, seconds = 0, ms = 0 } = {}) {
return (0, import_dayjs.default)(this).subtract(years, "year").subtract(months, "month").subtract(days, "day").subtract(hours, "hour").subtract(minutes, "minute").subtract(seconds, "second").subtract(ms, "millisecond").toDate();
};
function gmFetch(url, init) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: init.method,
url,
headers: init.headers || {},
data: init.body || void 0,
onload: (res) => resolve(new Response(res.responseText, { status: res.status, statusText: res.statusText })),
onerror: (err) => reject(new Error(err?.error ?? err?.statusText ?? "GM_xmlhttpRequest network error")),
ontimeout: () => reject(new Error("Request timeout"))
});
});
}
var unlimitedFetch = async (input, init = {}, retryOptions) => {
const { force = false, retry = false, maxRetries = 3, retryDelay = 3e3, successStatus = [200, 201], failStatus = [403, 404], onRetry, onFail } = retryOptions ?? {};
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
const useGM = force || new URL(url).hostname !== unsafeWindow.location.hostname;
const execFetch = () => useGM ? gmFetch(url, init) : originalFetch(input, init);
if (!retry) return execFetch();
const okStatus = Array.isArray(successStatus) ? successStatus : [successStatus];
const failCodes = Array.isArray(failStatus) ? failStatus : [failStatus];
let response = await execFetch();
for (let attempt = 1; attempt < maxRetries; attempt++) {
if (okStatus.includes(response.status)) return response;
if (failCodes.includes(response.status)) break;
await onRetry?.(response);
await delay(retryDelay);
response = await execFetch();
}
await onFail?.(response);
return response;
};
var findElement = (element, condition) => {
while (!isNullOrUndefined(element) && !element.matches(condition)) {
if (isNullOrUndefined(element.parentElement)) return void 0;
element = element.parentElement;
}
return element.querySelectorAll(condition).length > 1 ? void 0 : element;
};
var renderNode = (renderCode) => {
let code = prune(renderCode);
if (isNullOrUndefined(code)) throw new Error("RenderCode null");
if (typeof code === "string") {
return document.createTextNode(resolvePlaceholders(code));
}
if (renderCode instanceof Node) {
return code;
}
if (typeof renderCode !== "object" || !renderCode.nodeType) {
throw new Error("Invalid arguments");
}
const { nodeType, attributes, events, className, childs } = renderCode;
const node = document.createElement(nodeType);
if (!isNullOrUndefined(events) && Object.keys(events).length > 0) {
Object.entries(events).forEach(([eventName, eventHandler]) => originalAddEventListener.call(node, eventName, eventHandler));
}
if (!isNullOrUndefined(attributes) && Object.keys(attributes).length > 0) {
Object.entries(attributes).forEach(([key, value]) => {
node.setAttribute(key, value);
node[key] = value;
});
}
if (!isNullOrUndefined(className) && className.length > 0) {
const classes = typeof className === "string" ? className.split(/\s+/).filter(Boolean) : className;
node.classList.add(...classes);
}
if (!isNullOrUndefined(childs)) {
node.append(...(isArray(childs) ? childs : [childs]).filter((child) => !isNullOrUndefined(child)).map(renderNode));
}
return node;
};
init_env();
var sink;
function setReportSink(s) {
sink = s;
}
function report(level, options) {
if (!isNullOrUndefined(sink)) {
sink[level](options);
return;
}
const text = typeof options.body === "string" ? options.body : options.body.textContent ?? "";
const line = `[report:${level}]${options.title ? ` (${options.title})` : ""} ${text}`;
if (level === "error") console.error(line);
else console.warn(line);
}
function progress(options) {
if (!isNullOrUndefined(sink?.progress)) return sink.progress(options);
const text = typeof options.body === "string" ? options.body : options.body.textContent ?? "";
console.warn(`[report:progress] ${text}`);
return { update: () => void 0, dismiss: () => void 0 };
}
function buildToastParamsFromReport(options) {
const params = {
text: typeof options.body === "string" ? options.body : void 0,
node: typeof options.body === "string" ? void 0 : options.body
};
if (options.duration !== void 0) params.duration = options.duration;
if (options.close !== void 0) params.close = options.close;
if (options.gravity !== void 0) params.gravity = options.gravity;
if (options.position !== void 0) params.position = options.position;
if (options.progressRatio !== void 0) params.progressRatio = options.progressRatio;
return params;
}
function toastNode(body, title) {
return renderNode({
nodeType: "div",
childs: [
!isNullOrUndefined(title) && !title.isEmpty() ? {
nodeType: "h3",
childs: `%#appName#% - ${title}`
} : {
nodeType: "h3",
childs: "%#appName#%"
},
{
nodeType: "p",
childs: body
}
]
});
}
var PAGE_TYPE_ROUTES = [
[["forum", ":section", ":id", ":slug?"], "forumThread"],
[["forum", ":id"], "forumSection"],
[["forum"], "forum"],
[["video", ":id", ":slug?"], "video"],
[["v", ":id"], "video"],
[["videos"], "videoList"],
[["image", ":id", ":slug?"], "image"],
[["i", ":id"], "image"],
[["images"], "imageList"],
[["playlist", ":id", ":item?"], "playlist"],
[["favorites", ":type?"], "favorites"],
[["subscriptions", ":type?"], "subscriptions"],
[["history", ":type?"], "history"],
[["profile", ":id", ":section?"], "profile"],
[["p", ":id"], "profile"],
[["search"], "search"],
[["account", ":page?", ":subPage?"], "account"],
[["dashboard", ":page?"], "account"],
[["user", "voucher"], "account"],
[["post", ":id", ":action?"], "post"],
[["friends", ":page?"], "friends"],
[["notifications"], "notifications"],
[["messages", ":conversationId?"], "messages"],
[["login"], "auth"],
[["register"], "auth"],
[["activate"], "auth"],
[["password-reset"], "auth"],
[["forgot-password"], "auth"],
[["verify-email"], "auth"],
[["auth", ":service", "callback"], "auth"],
[["products"], "product"],
[["product", ":id", ":action?"], "product"],
[["create", ":type?"], "create"],
[["rules"], "rule"],
[["rule", ":id", ":action?"], "rule"],
[["faq"], "rule"],
[["changelog"], "rule"],
[["flags"], "rule"],
[["admin", ":a?", ":b?", ":c?", ":d?"], "admin"],
[[":userId", "messages", ":conversationId?"], "messages"],
[[], "home"]
];
function matchPathPattern(segments, pattern) {
if (pattern.length === 0) return segments.length === 0;
let required = pattern.length;
while (required > 0 && pattern[required - 1].endsWith("?")) required--;
if (segments.length < required || segments.length > pattern.length) return false;
for (let i = 0; i < segments.length; i++) {
const p2 = pattern[i];
if (!p2.startsWith(":") && p2 !== segments[i]) return false;
}
return true;
}
function getPageTypeFromPath(path) {
const segments = path.split("/").filter(Boolean);
for (const [pattern, type] of PAGE_TYPE_ROUTES) {
if (matchPathPattern(segments, pattern)) return type;
}
return "page";
}
function currentPath() {
const hashPath = unsafeWindow.location.hash.trimHead("#").trimHead("!").split("?")[0];
return hashPath || unsafeWindow.location.pathname;
}
function getPageType() {
return getPageTypeFromPath(currentPath());
}
function getPageParams() {
const segments = currentPath().split("/").filter(Boolean);
for (const [pattern] of PAGE_TYPE_ROUTES) {
if (!matchPathPattern(segments, pattern)) continue;
const params = {};
for (let i = 0; i < pattern.length; i++) {
const seg = pattern[i];
if (seg.startsWith(":")) params[seg] = segments[i] ?? "";
}
return params;
}
return {};
}
var listeners = new Map();
function listenersOf(name) {
let set = listeners.get(name);
if (!set) {
set = new Set();
listeners.set(name, set);
}
return set;
}
function on(name, cb) {
const set = listenersOf(name);
set.add(cb);
return () => set.delete(cb);
}
function off(name, cb) {
listeners.get(name)?.delete(cb);
}
function emit(name, payload) {
const set = listeners.get(name);
if (!set || set.size === 0) return;
for (const cb of [...set]) {
try {
;
cb(payload);
} catch (error) {
originalConsoleError(`[Events] subscriber error on '${String(name)}':`, error);
}
}
}
function clearBus() {
listeners.clear();
}
function originalConsoleError(...args) {
console.error(...args);
}
init_constants();
init_hijack();
var installed = false;
var nodeAddedCbs = [];
var nodeRemovedCbs = [];
var historyChangeCbs = [];
var tokenChangeCbs = [];
function subscribe(list, cb) {
list.push(cb);
return () => {
const idx = list.indexOf(cb);
if (idx >= 0) list.splice(idx, 1);
};
}
function installPlatformHooks(options = { watchNodeAppend: true }) {
if (installed) return;
installed = true;
unsafeWindow.EventTarget.prototype.addEventListener = function(type, listener, options2) {
originalAddEventListener.call(this, type, listener, options2);
};
if (options.watchNodeAppend) {
Node.prototype.appendChild = function(node) {
if (node instanceof HTMLElement && node.classList.contains("videoTeaser")) {
nodeAddedCbs.forEach((cb) => cb(node));
}
return originalNodeAppendChild.call(this, node);
};
}
Node.prototype.removeChild = function(child) {
nodeRemovedCbs.forEach((cb) => cb(child));
return originalNodeRemoveChild.apply(this, [child]);
};
Element.prototype.remove = function() {
nodeRemovedCbs.forEach((cb) => cb(this));
return originalElementRemove.apply(this);
};
unsafeWindow.history.pushState = function(...args) {
originalHistoryPushState.apply(this, args);
emit("page:change", { type: getPageType(), path: unsafeWindow.location.pathname });
historyChangeCbs.forEach((cb) => cb());
};
unsafeWindow.history.replaceState = function(...args) {
originalHistoryReplaceState.apply(this, args);
emit("page:change", { type: getPageType(), path: unsafeWindow.location.pathname });
historyChangeCbs.forEach((cb) => cb());
};
unsafeWindow.Storage.prototype.setItem = function(key, value) {
originalStorageSetItem.call(this, key, value);
if (key === LS_KEY_TOKEN) {
emit("auth:token-changed", void 0);
tokenChangeCbs.forEach((cb) => cb());
}
};
unsafeWindow.Storage.prototype.removeItem = function(key) {
originalStorageRemoveItem.call(this, key);
if (key === LS_KEY_TOKEN) {
emit("auth:token-changed", void 0);
tokenChangeCbs.forEach((cb) => cb());
}
};
unsafeWindow.Storage.prototype.clear = function() {
originalStorageClear.call(this);
emit("auth:token-changed", void 0);
tokenChangeCbs.forEach((cb) => cb());
};
}
function onNodeAdded(cb) {
return subscribe(nodeAddedCbs, cb);
}
function onNodeRemoved(cb) {
return subscribe(nodeRemovedCbs, cb);
}
function onHistoryChange(cb) {
return subscribe(historyChangeCbs, cb);
}
function onTokenChange(cb) {
return subscribe(tokenChangeCbs, cb);
}
init_constants();
var hostname = unsafeWindow.location.hostname;
var domain2 = site_default.supportedDomains.find((d) => hostname === d || hostname.endsWith("." + d)) ?? "";
if (!domain2) {
throw "Not target";
}
switch (GM_info.scriptHandler) {
case "Tampermonkey":
case "ScriptCat":
break;
case "Via":
throw `Not support ${GM_info.scriptHandler} (内置油猴引擎不完整,跨页同步不可用)`;
default:
throw `Not support ${GM_info.scriptHandler}`;
}
var apiEndpoint = site_default.apiEndpoint;
function apiUrl(path, query) {
const url = new URL(path.startsWith("/") ? path : `/${path}`, `https://${apiEndpoint}`);
if (query !== void 0) url.search = query.toString();
return url;
}
var rating = () => localStorage.getItem(LS_KEY_RATING) ?? "all";
var Dictionary = class extends Map {
constructor(data = []) {
super();
for (const [key, value] of data) {
super.set(key, value);
}
}
toArray() {
return Array.from(this);
}
keysArray() {
return Array.from(this.keys());
}
valuesArray() {
return Array.from(this.values());
}
};
init_env();
init_env();
var GMSyncDictionary = class _GMSyncDictionary extends Dictionary {
onSet;
onDel;
onSync;
name;
listenerId = null;
static BATCH_THRESHOLD = 10;
constructor(name, initial = [], validator = isVideoInfo) {
let stored = initial.any() ? initial : GM_getValue(name, initial);
try {
super(stored.filter(([_, value]) => validator(value)));
} catch (error) {
super();
}
this.name = name;
this.saveToStorage();
this.setupValueChangeListener();
}
setupValueChangeListener() {
if (this.listenerId !== null) {
GM_removeValueChangeListener(this.listenerId);
}
this.listenerId = GM_addValueChangeListener(this.name, (key, oldValue, newValue, remote) => {
if (key === this.name && remote) {
this.handleRemoteChange(newValue);
}
});
}
handleRemoteChange(newValue) {
if (isNullOrUndefined(newValue)) {
super.clear();
this.onSync?.();
return;
}
const currentKeys = new Set(this.keys());
const addedOrUpdated = [];
const deleted = [];
for (const [key, value] of newValue) {
if (!currentKeys.has(key)) {
addedOrUpdated.push([key, value]);
} else {
const currentValue = this.get(key);
if (currentValue !== value) {
addedOrUpdated.push([key, value]);
}
currentKeys.delete(key);
}
}
for (const key of currentKeys) {
deleted.push(key);
}
const totalChanges = addedOrUpdated.length + deleted.length;
if (totalChanges > _GMSyncDictionary.BATCH_THRESHOLD) {
super.clear();
for (const [key, value] of newValue) {
super.set(key, value);
}
this.onSync?.();
} else {
for (const [key, value] of addedOrUpdated) {
super.set(key, value);
this.onSet?.(key, value);
}
for (const key of deleted) {
super.delete(key);
this.onDel?.(key);
}
}
}
saveToStorage() {
GM_setValue(this.name, this.toArray());
}
set(key, value) {
super.set(key, value);
this.saveToStorage();
this.onSet?.(key, value);
return this;
}
delete(key) {
const result = super.delete(key);
if (result) {
this.saveToStorage();
this.onDel?.(key);
}
return result;
}
clear() {
super.clear();
this.saveToStorage();
this.onSync?.();
}
get(key) {
return super.get(key);
}
has(key) {
return super.has(key);
}
get size() {
return super.size;
}
destroy() {
if (this.listenerId !== null) {
GM_removeValueChangeListener(this.listenerId);
this.listenerId = null;
}
}
};
init_constants();
var selectList = new GMSyncDictionary(GM_KEY_SELECT_LIST);
var pageSelectButtons = new Dictionary();
var selection = selectList;
function getSelectButton(id) {
return pageSelectButtons.has(id) ? pageSelectButtons.get(id) : unsafeWindow.document.querySelector(`input.selectButton[videoid="${id}"]`);
}
function updateButtonState(videoID) {
const selectButton = getSelectButton(videoID);
if (selectButton) selectButton.checked = selectList.has(videoID);
}
selectList.onSet = (key) => emit("selection:changed", { size: selectList.size, videoId: key });
selectList.onDel = (key) => emit("selection:changed", { size: selectList.size, videoId: key });
selectList.onSync = () => emit("selection:changed", { size: selectList.size });
init_env();
init_hijack();
var batchActions = new Map();
function registerBatchAction(action) {
if (batchActions.has(action.id)) console.warn(`[Selection] BatchAction id conflict: ${action.id} (overwritten)`);
batchActions.set(action.id, action);
}
function getBatchActions() {
return [...batchActions.values()];
}
function selectAll(checked) {
unsafeWindow.document.querySelectorAll(".selectButton").forEach((element) => {
const button = element;
button.checked !== checked && button.click();
});
}
function toggleSelect() {
unsafeWindow.document.querySelectorAll(".selectButton").forEach((element) => {
;
element.click();
});
}
function deselectAll() {
for (const id of selectList.keys()) {
selectList.delete(id);
}
}
var mouseTarget = null;
function installSelectionShortcuts() {
originalAddEventListener("mouseover", (event) => {
mouseTarget = event.target instanceof Element ? event.target : null;
});
originalAddEventListener("keydown", (event) => {
const keyboardEvent = event;
if (keyboardEvent.code === "Space" && !isNullOrUndefined(mouseTarget)) {
let element = findElement(mouseTarget, ".videoTeaser");
let button = element && (element.matches(".selectButton") ? element : element.querySelector(".selectButton"));
button && button.click();
button && keyboardEvent.preventDefault();
}
});
}
init_env();
init_env();
var VARIABLE_PREFIX = "%#";
var VARIABLE_SUFFIX = "#%";
function escapeRegex(str) {
return str.replace(/[\.\*\+\?\^\$\{\}\(\)\|\[\]\\]/g, "\\$&");
}
var Path = class _Path {
fullPath;
directory;
fullName;
type;
extension;
baseName;
ast;
normalized;
constructor(input, validate = true) {
if (input === "") {
throw new Error("路径不能为空");
}
if (input.startsWith("\\\\")) {
throw new Error("不接受UNC路径");
}
const ast = _Path.parse(input);
this.type = ast.type;
this.ast = ast;
if (validate) _Path.validate(ast);
const normalized = _Path.normalize(ast);
this.normalized = normalized;
this.fullPath = _Path.render(normalized);
this.directory = _Path.extractDirectory(normalized);
this.fullName = _Path.extractFileName(normalized);
const { baseName, extension } = _Path.extractBaseAndExtension(this.fullName);
this.baseName = baseName;
this.extension = extension;
}
static tokenize(input) {
const tokens = [];
let start = 0;
for (let i = 0; i <= input.length; i++) {
const ch = input[i];
if (ch === "\\" || ch === "/" || i === input.length) {
if (i > start) {
tokens.push({ kind: "text", value: input.substring(start, i) });
}
if (ch === "\\" || ch === "/") {
tokens.push({ kind: "sep", char: ch });
}
start = i + 1;
}
}
return tokens;
}
static parse(input) {
const tokens = _Path.tokenize(input);
const first = tokens[0];
const second = tokens[1];
const isWindows = first.kind === "text" && /^[A-Za-z]:$/.test(first.value) && second.kind === "sep";
const isUnix = !isWindows && first.kind === "sep" && first.char === "/";
const type = isWindows ? "Windows" : isUnix ? "Unix" : "Relative";
let root = null;
let index = 0;
if (isWindows && first.kind === "text") {
root = first.value;
index = 2;
} else if (isUnix) {
root = "/";
index = 1;
}
const segments = [];
for (; index < tokens.length; index++) {
const token = tokens[index];
if (token.kind === "sep") continue;
segments.push({
kind: token.value === "." ? "dot" : token.value === ".." ? "dotdot" : "name",
parts: _Path.parseParts(token.value)
});
}
return { type, root, segments };
}
static parseParts(text, prefix = VARIABLE_PREFIX, suffix = VARIABLE_SUFFIX) {
const parts = [];
let lastIndex = 0;
const varRe = new RegExp(`${escapeRegex(prefix)}(.*?)${escapeRegex(suffix)}`, "g");
let match;
while ((match = varRe.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push({ kind: "literal", value: text.substring(lastIndex, match.index) });
}
const content = match[1];
const colon = content.indexOf(":");
if (colon === -1) {
parts.push({ kind: "variable", name: content, format: null });
} else {
parts.push({
kind: "variable",
name: content.substring(0, colon),
format: content.substring(colon + 1)
});
}
lastIndex = varRe.lastIndex;
}
if (lastIndex < text.length) {
parts.push({ kind: "literal", value: text.substring(lastIndex) });
}
return parts;
}
static validate(ast) {
if (ast.type === "Windows") {
if (!ast.root || !/^[A-Za-z]:$/.test(ast.root)) {
throw new Error("无效的Windows路径格式");
}
for (const segment of ast.segments) {
_Path.checkSegmentChars(segment);
}
} else if (ast.type === "Unix") {
_Path.checkNullChars(ast);
} else {
for (const segment of ast.segments) {
_Path.checkSegmentChars(segment);
}
_Path.checkNullChars(ast);
}
}
static checkSegmentChars(segment) {
const invalidChars = /[<>:"|?*]/;
for (const part of segment.parts) {
if (part.kind === "literal") {
if (invalidChars.test(part.value)) {
throw new Error(`路径段 "${_Path.renderSegment(segment)}" 含有非法字符`);
}
} else if (part.format !== null && invalidChars.test(part.format)) {
throw new Error(`路径变量格式化参数 "${part.format}" 含有非法字符`);
}
}
}
static checkNullChars(ast) {
for (const segment of ast.segments) {
for (const part of segment.parts) {
if (part.kind === "literal" && part.value.indexOf("\0") !== -1) {
throw new Error("路径中包含非法空字符");
}
}
}
}
static normalize(ast) {
const isAbsolute = ast.type !== "Relative";
const stack = [];
for (const segment of ast.segments) {
if (segment.kind === "dot") continue;
if (segment.kind === "dotdot") {
const top = stack[stack.length - 1];
if (top && top.kind !== "dotdot") {
stack.pop();
} else if (isAbsolute) {
throw new Error("绝对路径不能越界");
} else {
stack.push(segment);
}
} else {
stack.push(segment);
}
}
return { type: ast.type, root: ast.root, segments: stack };
}
static render(ast) {
const sep = ast.type === "Windows" ? "\\" : "/";
const body = ast.segments.map(_Path.renderSegment).join(sep);
if (ast.type === "Windows") {
return ast.root ? ast.root + sep + body : body;
}
if (ast.type === "Unix") {
return "/" + body;
}
return body;
}
static renderSegment(segment) {
let text = "";
for (const part of segment.parts) {
if (part.kind === "literal") {
text += part.value;
} else {
text += `${VARIABLE_PREFIX}${part.name}${part.format !== null ? ":" + part.format : ""}${VARIABLE_SUFFIX}`;
}
}
return text;
}
static extractDirectory(ast) {
const sep = ast.type === "Windows" ? "\\" : "/";
const count = ast.segments.length;
if (count === 0) {
if (ast.type === "Windows") return (ast.root ?? "") + sep;
if (ast.type === "Unix") return "/";
return "";
}
if (count === 1) {
if (ast.type === "Windows") return ast.root ?? "";
return "";
}
const body = ast.segments.slice(0, -1).map(_Path.renderSegment).join(sep);
if (ast.type === "Windows") return (ast.root ?? "") + sep + body;
if (ast.type === "Unix") return "/" + body;
return body;
}
static extractFileName(ast) {
const last = ast.segments[ast.segments.length - 1];
return last ? _Path.renderSegment(last) : "";
}
static extractBaseAndExtension(fileName) {
const lastDot = fileName.lastIndexOf(".");
if (lastDot <= 0) {
return { baseName: fileName, extension: "" };
}
return {
baseName: fileName.substring(0, lastDot),
extension: fileName.substring(lastDot + 1)
};
}
resolve(replacements) {
const sep = this.type === "Windows" ? "\\" : "/";
const body = this.normalized.segments.map((segment) => segment.parts.map((part) => part.kind === "literal" ? part.value : _Path.resolveValue(part.name, part.format, replacements)).join("")).join(sep);
const rendered = this.type === "Windows" ? (this.normalized.root ?? "") + sep + body : this.type === "Unix" ? "/" + body : body;
return rendered.replaceVariable(replacements);
}
static resolveValue(name, format, replacements) {
const value = replacements[name];
if (isNullOrUndefined(value)) {
return `${VARIABLE_PREFIX}${name}${format !== null ? ":" + format : ""}${VARIABLE_SUFFIX}`;
}
if (format !== null && !format.isEmpty() && hasFunction(value, "format")) {
return stringify(value.format(format));
}
return stringify(value instanceof Date ? value.format("YYYY-MM-DD") : value);
}
};
init_env();
init_env();
function buildDownloadUrl(videoInfo, localPath) {
const url = videoInfo.DownloadUrl.toURL();
url.searchParams.set("videoid", videoInfo.ID);
url.searchParams.set("download", localPath.fullName);
return url;
}
function getDownloadPath(videoInfo) {
const sanitize = (source, maxLength) => {
let name = source;
if (config.pathNormalize) name = name.normalize("NFKC");
if (config.pathReplaceEmojis) name = name.replaceEmojis("_");
if (config.pathFoldMarks) name = name.replaceAll(new RegExp("(\\P{Mark})(\\p{Mark}+)", "gu"), "_");
if (config.pathSanitize) name = name.replace(/^\.|[\\\\/:*?\"<>|]/gim, "_");
if (config.pathTruncate) name = name.truncate(maxLength);
return name;
};
return analyzeLocalPath(
config.downloadPath.trim().replaceVariable({
NowTime: new Date(),
UploadTime: new Date(videoInfo.UploadTime),
AUTHOR: videoInfo.Author,
ID: videoInfo.ID,
TITLE: sanitize(videoInfo.Title, config.pathTitleMaxLength),
ALIAS: sanitize(videoInfo.Alias, config.pathAliasMaxLength),
QUALITY: videoInfo.DownloadQuality
})
);
}
function analyzeLocalPath(path) {
try {
return new Path(path);
} catch (error) {
report("error", {
title: "%#settingsCheck#%",
body: toastNode(["%#downloadPathError#%", { nodeType: "br" }, stringify(error)], "%#settingsCheck#%"),
position: "center",
onClick: (host) => host?.hide()
});
throw new Error(`%#downloadPathError#% ["${path}"]`);
}
}
init_env();
init_env();
init_log();
var log5 = createLogger("EnvCheck");
function reportCheckFail(body, title = "%#settingsCheck#%") {
report("error", {
title,
body: toastNode(body, title),
position: "center",
close: true
});
}
async function EnvCheck() {
try {
if (GM_info.scriptHandler !== "ScriptCat" && GM_info.downloadMode !== "browser") {
log5.debug(GM_info);
throw new Error("%#browserDownloadModeError#%");
}
} catch (error) {
reportCheckFail(["%#configError#%", { nodeType: "br" }, stringify(error)]);
return false;
}
return true;
}
async function localPathCheck() {
try {
let pathTest = analyzeLocalPath(
config.downloadPath.replaceVariable({
NowTime: new Date(),
UploadTime: new Date(),
AUTHOR: "test",
ID: "test",
TITLE: "test",
ALIAS: "test",
QUALITY: "test"
})
);
if (isNullOrUndefined(pathTest)) throw "analyzeLocalPath error";
if (pathTest.fullPath.isEmpty()) throw "analyzeLocalPath isEmpty";
} catch (error) {
reportCheckFail([`%#downloadPathError#%`, { nodeType: "br" }, stringify(error)]);
return false;
}
return true;
}
async function aria2Check() {
try {
let res = await (await unlimitedFetch(config.aria2Path, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json"
},
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) {
reportCheckFail([`Aria2 RPC %#connectionTest#%`, { nodeType: "br" }, stringify(error)]);
return false;
}
return true;
}
async function iwaradlCheck() {
try {
let res = await (await unlimitedFetch(config.iwaradlPath, {
method: "GET",
headers: {
accept: "application/json",
"content-type": "application/json",
authorization: `Bearer ${config.iwaradlToken}`
}
})).json();
if (!isArray(res)) {
throw new Error(`后端未启动或无响应`);
}
} catch (error) {
reportCheckFail([`iwaradl RPC %#connectionTest#%`, { nodeType: "br" }, stringify(error)]);
return false;
}
return true;
}
async function check() {
if (await localPathCheck()) {
switch (config.downloadType) {
case 0:
return await aria2Check();
case 1:
return await iwaradlCheck();
case 2:
return await EnvCheck();
default:
break;
}
return true;
} else {
return false;
}
}
init_env();
var X_VERSION_SECRET = "mSvL05GfEmeEmsEYfGCnVpEjYgTJraJN";
async function getXVersion(urlString) {
let url = urlString.toURL();
const data = new TextEncoder().encode([url.pathname.split("/").pop(), url.searchParams.get("expires"), X_VERSION_SECRET].join("_"));
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
return Array.from(new Uint8Array(hashBuffer)).map((b2) => b2.toString(16).padStart(2, "0")).join("");
}
init_env();
init_env();
init_log();
init_constants();
var log6 = createLogger("Auth");
function isLoggedIn() {
const refreshToken3 = unsafeWindow.localStorage.getItem(LS_KEY_TOKEN);
if (isNullOrUndefined(refreshToken3) || refreshToken3.isEmpty()) return false;
try {
const payload = getPlayload(`Bearer ${refreshToken3}`);
if (payload.type && payload.type !== "refresh_token") return false;
if (typeof payload.exp === "number" && payload.exp * 1e3 <= Date.now()) return false;
} catch {
return false;
}
return true;
}
var LOGIN_VERIFY_TTL = 60 * 1e3;
var lastVerifyTime = 0;
var lastVerifyResult = false;
async function verifyLogin(force = false) {
const now = Date.now();
if (!force && now - lastVerifyTime < LOGIN_VERIFY_TTL) return lastVerifyResult;
const cache = (result) => {
lastVerifyResult = result;
lastVerifyTime = Date.now();
};
if (!isLoggedIn()) {
cache(false);
return false;
}
let res;
try {
res = await unlimitedFetch(apiUrl("/user"), {
method: "GET",
headers: await getAuth()
});
if (!res.ok) {
await refreshToken().catch(() => void 0);
res = await unlimitedFetch(apiUrl("/user"), {
method: "GET",
headers: await getAuth()
});
}
} catch (error) {
log6.warn("Failed to verify login:", error);
cache(false);
return false;
}
if (!res.ok) {
log6.warn("Login credential rejected by server, clearing local credentials");
localStorage.removeItem(LS_KEY_TOKEN);
localStorage.removeItem(LS_KEY_ACCESS_TOKEN);
config.authorization = "";
}
cache(res.ok);
return res.ok;
}
async function refreshToken() {
const { authorization } = config;
if (!isLoggedIn()) throw new Error(`Refresh token failed: Not logged in`);
const refreshToken3 = localStorage.getItem(LS_KEY_TOKEN) ?? authorization;
if (isNullOrUndefined(refreshToken3) || refreshToken3.isEmpty()) {
throw new Error(`Refresh token failed: no refresh token`);
}
const oldAccessToken = localStorage.getItem(LS_KEY_ACCESS_TOKEN);
try {
const res = await unlimitedFetch(apiUrl("/user/token"), {
method: "POST",
headers: {
Authorization: `Bearer ${refreshToken3}`
}
});
if (!res.ok) {
throw new Error(`Refresh token failed with status: ${res.status}`);
}
const { accessToken } = await res.json();
if (!accessToken) {
throw new Error(`No access token in response`);
}
if (!oldAccessToken || oldAccessToken !== accessToken) {
localStorage.setItem(LS_KEY_ACCESS_TOKEN, accessToken);
}
return accessToken;
} catch (error) {
log6.warn("Failed to refresh token:", error);
if (!oldAccessToken?.trim()) {
throw new Error(`Refresh token failed and no valid access token available`);
}
return oldAccessToken;
}
}
async function getAuth(url) {
return prune({
Referer: `${window.location.origin}/`,
Accept: "application/json",
Cooike: unsafeWindow.document.cookie,
Authorization: isLoggedIn() ? `Bearer ${localStorage.getItem(LS_KEY_ACCESS_TOKEN) ?? await refreshToken()}` : void 0,
"X-Version": !isNullOrUndefined(url) && !url.isEmpty() ? await getXVersion(url) : void 0,
"X-Site": unsafeWindow.location.hostname
});
}
function getPlayload(authorization) {
return JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(authorization.split(" ").pop().split(".")[1]))));
}
init_env();
init_env();
init_log();
init_db();
var log7 = createLogger("Video");
async function getCommentData(id, commentID, page = 0) {
const query = new URLSearchParams({ page: String(page) });
if (!isNullOrUndefined(commentID) && !commentID.isEmpty()) query.set("parent", commentID);
return await (await unlimitedFetch(apiUrl(`/video/${id}/comments`, query), { headers: await getAuth() })).json();
}
async function getCommentDatas(id, commentID) {
let comments = [];
let base = await getCommentData(id, commentID);
comments.push(...base.results);
for (let page = 1; page < Math.ceil(base.count / base.limit); page++) {
comments.push(...(await getCommentData(id, commentID, page)).results);
}
let replies = [];
for (let index = 0; index < comments.length; index++) {
const comment = comments[index];
if (comment.numReplies > 0) {
replies.push(...await getCommentDatas(id, comment.id));
}
}
comments.push(...replies);
return comments;
}
async function parseVideoInfo(info) {
let ID = info.ID;
let Type = info.Type;
let RAW = info.RAW;
try {
switch (info.Type) {
case "cache":
RAW = info.RAW;
ID = RAW.id;
Type = "partial";
break;
case "init":
case "fail":
case "partial":
case "full":
log7.debug("try parse full source");
let sourceResult = await (await unlimitedFetch(
apiUrl(`/video/${info.ID}`),
{
headers: await getAuth()
},
{
retry: true,
maxRetries: 3,
failStatus: [403, 404],
retryDelay: 1e3,
onRetry: async () => {
await refreshToken();
},
onFail: async (response) => {
log7.debug(`${response.url} Fail, response: ${await response.clone().text()}`);
}
}
)).json();
if (isNullOrUndefined(sourceResult.id)) {
Type = "fail";
const Msg = sourceResult.message ?? stringify(sourceResult);
log7.warn(`parse video failed [${ID}]: ${Msg}`);
return {
ID,
Type,
RAW,
Msg
};
}
RAW = sourceResult;
ID = RAW.id;
Type = "full";
break;
default:
Type = "fail";
return {
ID,
Type,
RAW,
Msg: "Unknown type"
};
}
} catch (error) {
log7.warn(`parse video error [${ID}]:`, error);
Type = "fail";
return {
ID,
Type,
RAW,
Msg: stringify(error)
};
}
let FileName;
let Size;
let External;
let ExternalUrl;
let Description;
let DownloadQuality;
let DownloadUrl;
let Comments;
let UploadTime;
let Title;
let Tags;
let Liked;
let Alias;
let Author;
let AuthorID;
let Private;
let Unlisted;
let Following;
let Friend;
UploadTime = new Date(RAW.createdAt ?? 0).getTime();
Title = RAW.title;
Tags = RAW.tags;
Liked = RAW.liked;
Alias = RAW.user.name;
Author = RAW.user.username;
AuthorID = RAW.user.id;
Private = RAW.private;
Unlisted = RAW.unlisted;
External = !isNullOrUndefined(RAW.embedUrl) && !RAW.embedUrl.isEmpty();
ExternalUrl = RAW.embedUrl ?? void 0;
if (External) {
Type = "fail";
return {
Type,
RAW,
ID,
Alias,
Author,
AuthorID,
Private,
UploadTime,
Title,
Tags,
Liked,
External,
ExternalUrl,
Description,
Unlisted,
Msg: "external Video"
};
}
try {
switch (Type) {
case "full":
Following = RAW.user.following;
Friend = RAW.user.friend;
if (Following) {
await db.putFollow(RAW.user);
} else {
await db.deleteFollow(AuthorID);
}
if (Friend) {
await db.putFriend(RAW.user);
} else {
await db.deleteFriend(AuthorID);
}
Description = RAW.body ?? void 0;
FileName = RAW.file.name;
Size = RAW.file.size;
const sourceResponse = await unlimitedFetch(RAW.fileUrl, { headers: await getAuth(RAW.fileUrl) });
const VideoFileSource = (await sourceResponse.json()).sort((a2, b2) => (!isNullOrUndefined(config.priority[b2.name]) ? config.priority[b2.name] : 0) - (!isNullOrUndefined(config.priority[a2.name]) ? config.priority[a2.name] : 0));
if (isNullOrUndefined(VideoFileSource) || !(VideoFileSource instanceof Array) || VideoFileSource.length < 1) throw new Error("No available video source");
DownloadQuality = config.checkPriority ? config.downloadPriority : VideoFileSource[0].name;
let fileList = VideoFileSource.filter((x2) => x2.name === DownloadQuality);
if (!fileList.any()) throw new Error("No video source matches the priority");
let Source = fileList[Math.floor(Math.random() * fileList.length)].src.download;
if (isNullOrUndefined(Source) || Source.isEmpty()) throw new Error("Video source not available");
DownloadUrl = decodeURIComponent(`https:${Source}`);
log7.debug("try parse all comment");
Comments = JSON.stringify(await getCommentDatas(ID)).normalize("NFKC");
return {
Type,
RAW,
ID,
Alias,
Author,
AuthorID,
Private,
UploadTime,
Title,
Tags,
Liked,
External,
FileName,
DownloadQuality,
ExternalUrl,
Description,
Comments,
DownloadUrl,
Size,
Following,
Unlisted,
Friend
};
case "partial":
return {
Type,
RAW,
ID,
Alias,
Author,
AuthorID,
UploadTime,
Title,
Tags,
Liked,
External,
ExternalUrl,
Unlisted,
Private
};
default:
Type = "fail";
return {
Type,
RAW,
ID,
Alias,
Author,
AuthorID,
Private,
UploadTime,
Title,
Tags,
Liked,
External,
ExternalUrl,
Description,
Unlisted,
Msg: "Unknown type"
};
}
} catch (error) {
Type = "fail";
const Msg = stringify(error);
log7.warn(`parse video fields error [${ID}]:`, error);
return {
Type,
RAW,
ID,
Alias,
Author,
AuthorID,
Private,
UploadTime,
Title,
Tags,
Liked,
External,
ExternalUrl,
Description,
Unlisted,
Msg
};
}
}
function getVideoInfoCompleteness(info) {
switch (info.Type) {
case "full":
return 5;
case "partial":
return 4;
case "fail":
return 3;
case "cache":
return 2;
case "init":
return 1;
}
}
function getMoreCompleteVideoInfo(a2, b2) {
if (a2.ID !== b2.ID) throw new Error(`VideoInfo ID mismatch: "${a2.ID}" vs "${b2.ID}"`);
const completenessA = getVideoInfoCompleteness(a2);
const completenessB = getVideoInfoCompleteness(b2);
return completenessB > completenessA ? b2 : a2;
}
init_hijack();
init_log();
init_db();
init_constants();
init_env();
var log8 = createLogger("Fetch");
function handleAuthorizationHeader(init) {
if (!init || !init.headers) return;
let authorization = null;
if (init.headers instanceof Headers) {
authorization = init.headers.has("Authorization") ? init.headers.get("Authorization") : null;
} else if (Array.isArray(init.headers)) {
const index = init.headers.findIndex(([key]) => key.toLowerCase() === "authorization");
if (index >= 0) authorization = init.headers[index][1];
} else if (typeof init.headers === "object") {
for (const key in init.headers) {
if (key.toLowerCase() === "authorization") {
authorization = init.headers[key];
break;
}
}
}
if (!authorization) return;
const payload = getPlayload(authorization);
const token = authorization.split(" ").pop();
if (payload["type"] === "refresh_token" && !isUndefined(token)) {
localStorage.setItem(LS_KEY_TOKEN, token);
config.authorization = token;
log8.debug("refresh_token: 凭证已隐藏");
}
}
async function handleUserTokenResponse(response) {
const cloneResponse = response.clone();
if (!cloneResponse.ok) return;
const { accessToken } = await cloneResponse.json();
const token = localStorage.getItem(LS_KEY_ACCESS_TOKEN);
if (isNull(token) || token !== accessToken) {
localStorage.setItem(LS_KEY_ACCESS_TOKEN, accessToken);
}
}
async function handleVideosResponse(response, url) {
const cloneResponse = response.clone();
if (!cloneResponse.ok) return response;
const cloneBody = await cloneResponse.json();
const rawVideos = cloneBody.results;
const parsePromises = rawVideos.map((info) => parseVideoInfo({ Type: "cache", ID: info.id, RAW: info }));
const settled = await Promise.allSettled(parsePromises);
const list = settled.filter((i) => i.status === "fulfilled").map((i) => i.value).filter((i) => i.Type === "partial" || i.Type === "full");
const ids = list.map((v2) => v2.ID);
const existing = await db.getVideosByIds(ids);
const fullVideos = existing.filter((v2) => v2.Type === "full");
const toUpdate = list.difference(fullVideos, "ID");
if (toUpdate.any()) {
await db.bulkPutVideos(toUpdate);
}
if (config.filterLikedVideos) {
cloneBody.results = rawVideos.filter((i) => !i.liked);
cloneBody.limit = cloneBody.results.length;
cloneBody.count = cloneBody.limit * (cloneBody.page + 1) + 1;
}
if (config.filterUnlistedAndPrivate && url.searchParams.has("subscribed")) {
cloneBody.results = cloneBody.results.filter((i) => !i.private && !i.unlisted);
cloneBody.limit = cloneBody.results.length;
cloneBody.count = cloneBody.limit * (cloneBody.page + 1) + 1;
}
let preResponse = new Response(JSON.stringify(cloneBody), {
status: cloneResponse.status,
statusText: cloneResponse.statusText,
headers: Object.fromEntries(cloneResponse.headers.entries())
});
if (!config.addUnlistedAndPrivate || config.filterUnlistedAndPrivate) return preResponse;
if (url.searchParams.has("user")) return preResponse;
if (url.searchParams.has("subscribed")) return preResponse;
if (url.searchParams.has("sort") && url.searchParams.get("sort") !== "date") return preResponse;
const sortedList = list.sort((a2, b2) => a2.UploadTime - b2.UploadTime);
if (sortedList.length === 0) return preResponse;
const minTime = sortedList[0].UploadTime;
const maxTime = sortedList[sortedList.length - 1].UploadTime;
const startTime = new Date(minTime).sub({ hours: 4 }).getTime();
const endTime = new Date(maxTime).add({ hours: 4 }).getTime();
const cacheVideos = (await db.getFilteredVideos(startTime, endTime)).filter((i) => i.Type === "partial" || i.Type === "full").sort((a2, b2) => b2.UploadTime - a2.UploadTime).map((i) => i.RAW);
cloneBody.results.push(...cacheVideos);
cloneBody.limit = cloneBody.results.length;
cloneBody.count = cloneBody.limit * (cloneBody.page + 1) + 1;
preResponse = new Response(JSON.stringify(cloneBody), {
status: cloneResponse.status,
statusText: cloneResponse.statusText,
headers: Object.fromEntries(cloneResponse.headers.entries())
});
return preResponse;
}
function createInterceptedFetch() {
return async function(input, init) {
log8.debug(`Fetch ${input}`);
const url = (input instanceof Request ? input.url : input instanceof URL ? input.href : input).toURL();
if (!isUndefined(init) && init.headers) {
handleAuthorizationHeader(init);
}
return new Promise(
(resolve, reject) => originalFetch(input, init).then(async (response) => {
if (!url.pathname.isEmpty()) {
const path = url.pathname.toLowerCase().split("/").slice(1);
if (url.hostname === "apiq.iwara.tv" || url.hostname === "api.iwara.tv") {
switch (path[0]) {
case "user":
if (path[1] === "token") await handleUserTokenResponse(response);
break;
case "videos":
return resolve(await handleVideosResponse(response, url));
default:
break;
}
}
}
return resolve(response);
}).catch((err) => reject(err))
);
};
}
init_env();
var activeToasts = new Dictionary();
var toastTimeouts = new Map();
var toastContainers = new Map();
var offscreenContainer = document.createElement("div");
offscreenContainer.classList.add("offscreen-container");
var camelToKebab = (str) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
var MIN_PROGRESS_DURATION = 3e3;
var getContainer = (gravity, position) => {
const containerId = `toast-container-${gravity}-${position}`;
let container = toastContainers.get(containerId);
if (isNullOrUndefined(container)) {
container = document.createElement("div");
container.id = containerId;
container.classList.add("toast-container", `toast-${gravity}`, `toast-${position}`);
document.body.appendChild(container);
toastContainers.set(containerId, container);
}
return container;
};
var addTimeout = (toast, callback) => {
if (isNullOrUndefined(toast.options.duration)) return;
delTimeout(toast);
const duration = toast.options.duration;
const timeoutId = window.setTimeout(() => {
callback();
delTimeout(toast);
}, duration);
toastTimeouts.set(toast, timeoutId);
if (!toast.showProgress || !isNullOrUndefined(toast.ratioProgress)) return;
if (isNullOrUndefined(toast.progress)) return;
const progress2 = toast.progress;
progress2.style.setProperty("--toast-duration", `${duration}ms`);
progress2.style.animation = "none";
void progress2.offsetWidth;
progress2.style.animation = "";
progress2.style.animationPlayState = "running";
};
var delTimeout = (toast) => {
const timeoutId = toastTimeouts.get(toast);
if (!isNullOrUndefined(timeoutId)) {
clearTimeout(timeoutId);
toastTimeouts.delete(toast);
}
if (!toast.showProgress || !isNullOrUndefined(toast.ratioProgress)) return;
if (!isNullOrUndefined(toast.progress)) {
toast.progress.style.animationPlayState = "paused";
}
};
var Toast = class _Toast {
static defaults = {
gravity: "top",
position: "left",
stopOnFocus: true,
oldestFirst: true,
showProgress: false,
rate: 1
};
id;
options;
root;
element;
gravity;
position;
oldestFirst;
stopOnFocus;
showProgress;
ratioProgress;
progressRatioValue;
content;
progress;
mouseOverHandler;
mouseLeaveHandler;
closeButtonHandler;
animationEndHandler;
clickHandler;
closeButton;
hidden = false;
constructor(options) {
this.id = options.id ?? UUID();
this.options = {
..._Toast.defaults,
...options,
id: this.id
};
if (this.options.text && this.options.node) delete this.options.text;
if (this.options.buttons && this.options.buttons.length > 0) delete this.options.onClick;
this.root = getContainer(this.options.gravity, this.options.position);
this.gravity = this.options.gravity;
this.position = this.options.position;
this.stopOnFocus = this.options.stopOnFocus;
this.oldestFirst = this.options.oldestFirst;
this.showProgress = options.showProgress ?? (!isNullOrUndefined(this.options.duration) && this.options.duration > 0 && this.options.duration >= MIN_PROGRESS_DURATION);
this.element = document.createElement("div");
if (this.options.rate !== 1) {
this.element.style.setProperty("--toast-rate", String(this.options.rate));
}
this.applyBaseStyles().addCloseButton().createContent().ensureCloseMethod().bindEvents();
activeToasts.set(this.id, this);
}
applyBaseStyles() {
this.element.classList.add("toast");
if (this.options.className) {
const classes = Array.isArray(this.options.className) ? this.options.className : [this.options.className];
classes.forEach((cls) => this.element.classList.add(cls));
}
return this;
}
createContent() {
this.content = document.createElement("div");
this.content.classList.add("toast-content");
if (this.options.text) {
this.content.textContent = this.options.text;
}
if (this.options.node) {
this.content.appendChild(this.options.node);
}
if (this.options.style) {
this.applyStyles(this.content, this.options.style);
}
if (this.showProgress) {
this.progress = document.createElement("div");
this.progress.classList.add("toast-progress");
this.content.appendChild(this.progress);
}
if (this.options.progressRatio !== void 0) {
this.ratioProgress = document.createElement("div");
this.ratioProgress.classList.add("toast-progress");
this.ratioProgress.style.animation = "none";
this.content.appendChild(this.ratioProgress);
this.setProgressRatio(this.options.progressRatio);
}
if (this.options.buttons && this.options.buttons.length > 0) {
const buttonsRow = document.createElement("div");
buttonsRow.classList.add("toast-buttons");
for (const btn of this.options.buttons) {
const button = document.createElement("button");
button.textContent = btn.text;
if (btn.className) button.className = btn.className;
button.addEventListener("click", (e) => {
e.stopPropagation();
btn.onClick(this);
});
buttonsRow.appendChild(button);
}
this.content.appendChild(buttonsRow);
}
this.element.appendChild(this.content);
return this;
}
setProgressRatio(ratio) {
if (isNullOrUndefined(this.ratioProgress)) return this;
this.progressRatioValue = Math.max(0, Math.min(1, ratio));
this.ratioProgress.style.transform = `scaleX(${this.progressRatioValue})`;
return this;
}
setText(text) {
if (this.options.node !== void 0 || isNullOrUndefined(this.content)) return this;
this.options.text = text;
const first = this.content.firstChild;
if (first && first.nodeType === Node.TEXT_NODE) {
first.textContent = text;
} else {
this.content.insertBefore(document.createTextNode(text), this.content.firstChild);
}
return this;
}
addCloseButton() {
if (this.options.close) {
this.closeButton = document.createElement("span");
this.closeButton.className = "toast-close";
this.closeButton.textContent = "🗙";
this.closeButtonHandler = () => this.hide("close-button");
this.closeButton.addEventListener("click", this.closeButtonHandler);
this.element.appendChild(this.closeButton);
}
return this;
}
setToastRect() {
if (!this.element.classList.contains("show")) offscreenContainer.appendChild(this.element);
this.element.style.removeProperty("--toast-height");
this.element.style.removeProperty("--toast-width");
this.element.style.setProperty("max-height", "none", "important");
this.element.style.setProperty("max-width", `${this.root.getBoundingClientRect().width}px`, "important");
const { height, width } = this.element.getBoundingClientRect();
this.element.style.setProperty("--toast-height", `${height}px`);
this.element.style.setProperty("--toast-width", `${width}px`);
this.element.style.removeProperty("max-height");
this.element.style.removeProperty("max-width");
if (!this.element.classList.contains("show")) offscreenContainer.removeChild(this.element);
return this;
}
ensureCloseMethod() {
if (isNullOrUndefined(this.options.duration) && isNullOrUndefined(this.options.close) && isNullOrUndefined(this.options.onClick) && isNullOrUndefined(this.options.buttons)) {
this.options.onClick = () => this.hide("other");
}
return this;
}
bindEvents() {
if (this.stopOnFocus && !isNullOrUndefined(this.options.duration) && this.options.duration > 0) {
this.mouseOverHandler = () => delTimeout(this);
this.mouseLeaveHandler = () => addTimeout(this, () => this.hide("timeout"));
this.element.addEventListener("mouseover", this.mouseOverHandler);
this.element.addEventListener("mouseleave", this.mouseLeaveHandler);
}
if (!isNullOrUndefined(this.options.onClick)) {
this.clickHandler = this.options.onClick.bind(this);
this.element.addEventListener("click", this.clickHandler);
this.element.classList.add("clickable");
}
return this;
}
applyStyles(element, styles) {
for (const key in styles) {
const value = styles[key];
const property = camelToKebab(key);
if (isNullOrUndefined(value)) {
element.style.removeProperty(property);
continue;
}
const important = value.includes("!important");
const cleanValue = value.replace(/\s*!important\s*/, "").trim();
element.style.setProperty(property, cleanValue, important ? "important" : "");
}
}
toggleAnimationState(animation) {
if (!this.element.classList.replace(animation ? "hide" : "show", animation ? "show" : "hide")) {
this.element.classList.add(animation ? "show" : "hide");
}
return this;
}
insertToastElement() {
if (this.oldestFirst) {
this.root.insertBefore(this.element, this.root.firstChild);
} else {
this.root.appendChild(this.element);
}
return this;
}
setupAutoHide() {
if (!isNullOrUndefined(this.options.duration) && this.options.duration > 0) {
addTimeout(this, () => this.hide("timeout"));
}
return this;
}
show() {
this.setToastRect().insertToastElement();
void this.element.offsetWidth;
this.toggleAnimationState(true).setupAutoHide();
return this;
}
showToast() {
return this.show();
}
removeEventListeners() {
if (this.mouseOverHandler) {
this.element.removeEventListener("mouseover", this.mouseOverHandler);
}
if (this.mouseLeaveHandler) {
this.element.removeEventListener("mouseleave", this.mouseLeaveHandler);
}
if (this.clickHandler) {
this.element.removeEventListener("click", this.clickHandler);
}
if (this.options.close && this.closeButton && this.closeButtonHandler) {
this.closeButton.removeEventListener("click", this.closeButtonHandler);
}
return this;
}
hide(reason = "other") {
if (this.hidden || !this.element) return;
this.hidden = true;
delTimeout(this);
activeToasts.delete(this.id);
let closed = false;
const finalize = () => {
if (closed) return;
closed = true;
this.element.remove();
this.options.onClose?.call(
this,
new CustomEvent("toast-close", {
detail: { reason }
})
);
};
this.animationEndHandler = (e) => {
if (e.propertyName !== "max-height") return;
this.element.removeEventListener("transitionend", this.animationEndHandler);
finalize();
};
this.element.addEventListener("transitionend", this.animationEndHandler);
const durations = getComputedStyle(this.element).transitionDuration;
const maxMs = durations.split(",").map((s) => parseFloat(s)).filter(Number.isFinite).reduce((max, sec) => Math.max(max, sec), 0) * 1e3;
window.setTimeout(finalize, Math.max(maxMs, 300) + 300);
this.removeEventListeners().toggleAnimationState(false);
}
hideToast() {
this.hide("other");
}
};
function createToast(options) {
return new Toast(options);
}
globalThis.Toast = createToast;
globalThis.Toastify = createToast;
(document.body ?? document.documentElement).appendChild(offscreenContainer);
window.addEventListener(
"resize",
debounce(() => {
for (const [_, toast] of activeToasts) {
toast.setToastRect();
}
}, 100)
);
init_env();
init_env();
init_log();
var log9 = createLogger("Toast");
function reportSinkAdapter() {
return {
info(options) {
showToastFromReport(1, options);
},
warn(options) {
showToastFromReport(2, options);
},
error(options) {
showToastFromReport(3, options);
},
progress(options) {
const toast = showToastFromReport(1, { ...options, duration: -1, close: false });
return {
update(next) {
if (typeof next.body === "string") toast.setText(resolvePlaceholders(next.body));
if (next.progressRatio !== void 0) toast.setProgressRatio(next.progressRatio);
},
dismiss: () => toast.hide()
};
}
};
}
function showToastFromReport(type, options) {
const params = buildToastParamsFromReport(options);
if (options.onClick !== void 0) {
params.onClick = function() {
this.hide();
options.onClick?.(this);
};
}
const toast = newToast(type, params);
options.onDismiss?.(() => toast.hide());
toast.show();
return toast;
}
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 = {
[2]: log9.warn,
[3]: log9.error,
[0]: log9.info,
[1]: log9.info
}[type] || log9.info;
if (isNullOrUndefined(params)) params = {};
if (!isNullOrUndefined(params.id) && activeToasts.has(params.id)) activeToasts.get(params.id)?.hide();
switch (type) {
case 1:
params = Object.assign(
{
duration: 2e3,
style: {
background: "linear-gradient(-30deg, rgb(0, 108, 215), rgb(0, 180, 255))"
}
},
params
);
break;
case 2:
params = Object.assign(
{
duration: -1,
style: {
background: "linear-gradient(-30deg, rgb(119, 76, 0), rgb(255, 165, 0))"
}
},
params
);
break;
case 3:
params = Object.assign(
{
duration: -1,
style: {
background: "linear-gradient(-30deg, rgb(108, 0, 0), rgb(215, 0, 0))"
}
},
params
);
break;
default:
break;
}
if (!isNullOrUndefined(params.text)) {
params.text = params.text.replaceVariable(i18nList[config.language]).toString();
}
if (params.buttons && params.buttons.length > 0) {
params.buttons = params.buttons.map((b2) => ({ ...b2, text: b2.text.replaceVariable(i18nList[config.language]).toString() }));
}
logFunc((!isNullOrUndefined(params.text) ? params.text : !isNullOrUndefined(params.node) ? getTextNode(params.node) : "undefined").replaceVariable(i18nList[config.language]));
return new Toast(params);
}
init_env();
init_hijack();
init_log();
var log10 = createLogger("Menu");
var menu = class {
observer;
pageType;
interface;
interfacePage;
isTouchDevice;
openSettings;
actions = {};
providers = {};
constructor(actions2) {
this.openSettings = actions2?.openSettings;
let body = new Proxy(this, {
set: (target, prop, value) => {
if (prop === "pageType") {
if (isNullOrUndefined(value) || target.pageType === value) return true;
const ok = Reflect.set(target, prop, value);
this.pageChange();
log10.debug(`Page change to ${this.pageType}`);
return ok;
}
return Reflect.set(target, prop, value);
}
});
body.interfacePage = renderNode({
nodeType: "ul"
});
body.interface = renderNode({
nodeType: "div",
attributes: {
id: "pluginMenu"
},
childs: body.interfacePage
});
body.isTouchDevice = unsafeWindow.matchMedia("(pointer: coarse)").matches || (unsafeWindow.navigator.maxTouchPoints ?? 0) > 0;
if (config.autoCollapseMenu) {
if (body.isTouchDevice) {
originalAddEventListener.call(body.interface, "click", (event) => {
if (event.target === body.interface) {
body.interface.classList.toggle("expanded");
}
});
originalAddEventListener.call(unsafeWindow.document, "click", (event) => {
if (body.interface.classList.contains("expanded") && !body.interface.contains(event.target)) {
body.interface.classList.remove("expanded");
}
});
} else {
let mouseoutTimer = null;
originalAddEventListener.call(body.interface, "mouseover", (event) => {
if (mouseoutTimer !== null) {
clearTimeout(mouseoutTimer);
mouseoutTimer = null;
}
body.interface.classList.add("expanded");
});
originalAddEventListener.call(body.interface, "mouseout", (event) => {
const e = event;
const relatedTarget = e.relatedTarget;
if (relatedTarget && body.interface.contains(relatedTarget)) {
return;
}
mouseoutTimer = setTimeout(() => {
body.interface.classList.remove("expanded");
mouseoutTimer = null;
}, 300);
});
originalAddEventListener.call(body.interface, "click", (event) => {
if (event.target === body.interface) {
body.interface.classList.toggle("expanded");
}
});
}
} else {
body.interface.classList.add("expanded");
}
body.observer = new MutationObserver(() => body.pageType = getPageType());
body.pageType = "page";
return body;
}
setup(actions2, providers) {
this.actions = actions2;
this.providers = providers;
}
button(name, click) {
const self2 = this;
return renderNode({
nodeType: "li",
childs: `%#${name}#%`,
events: {
click: (event) => {
if (!isNullOrUndefined(click)) click(name, event);
if (self2.isTouchDevice && config.autoCollapseMenu) {
setTimeout(() => self2.interface.classList.remove("expanded"), 150);
}
event.stopPropagation();
return false;
}
}
});
}
appendAll(items) {
items.forEach((i) => originalNodeAppendChild.call(this.interfacePage, i));
}
async pageChange() {
while (this.interfacePage.hasChildNodes()) {
this.interfacePage.removeChild(this.interfacePage.firstChild);
}
const baseButtons = [
this.button("manualDownload", () => this.actions.manualDownload?.()),
this.button("exportConfig", () => this.actions.exportConfig?.()),
this.button("importConfig", () => this.actions.importConfig?.()),
this.button("settings", () => this.openSettings?.())
];
let injectCheckboxButton = this.button("injectCheckbox", () => this.providers.toggleInjectCheckbox?.());
const selectionButtons = (this.providers.selectionButtons ?? []).map((s) => this.button(s.id, () => s.click()));
const actionButtons = (this.providers.actionButtons ?? []).map((a2) => this.button(a2.labelKey ?? a2.id, () => a2.click()));
let selectButtons = [injectCheckboxButton, ...selectionButtons, ...actionButtons];
let downloadThisButton = this.button("downloadThis", () => this.actions.downloadThis?.());
switch (this.pageType) {
case "video":
this.appendAll([downloadThisButton, ...selectButtons, ...baseButtons]);
break;
case "search":
case "profile":
case "home":
case "videoList":
case "subscriptions":
case "playlist":
case "favorites":
case "history":
case "account":
this.appendAll([...selectButtons, ...baseButtons]);
break;
case "page":
case "forum":
case "image":
case "imageList":
case "forumSection":
case "forumThread":
case "post":
case "friends":
case "messages":
case "notifications":
case "auth":
case "product":
case "create":
case "rule":
case "admin":
default:
this.appendAll(baseButtons);
break;
}
if (config.addUnlistedAndPrivate && !config.filterUnlistedAndPrivate && this.pageType === "videoList") {
this.actions.parseUnlistedAndPrivate?.();
} else {
log10.debug("Conditions not met: addUnlistedAndPrivate or pageType mismatch.");
}
}
inject() {
try {
this.observer.observe(unsafeWindow.document.getElementById("app"), { childList: true, subtree: true });
if (!unsafeWindow.document.querySelector("#pluginMenu")) {
originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
this.pageType = getPageType();
}
} catch (error) {
originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
}
}
};
init_log();
init_env();
var log11 = createLogger("ApprovalConditions");
var conditionAlways = {
id: "always",
test: async () => true
};
var conditionFollowedBy = {
id: "followedBy",
test: async ({ user }) => user.followedBy === true
};
var conditionFollowing = {
id: "following",
test: async ({ user }) => user.following === true
};
var conditionPremium = {
id: "premium",
test: async ({ user }) => user.premium === true
};
var conditionRecentlyActive = {
id: "recentlyActive",
test: async ({ user }) => {
if (!user.seenAt) return false;
return Date.now() - new Date(user.seenAt).getTime() <= 30 * 24 * 60 * 60 * 1e3;
}
};
var conditionEstablished = {
id: "established",
test: async ({ user }) => {
if (!user.createdAt) return false;
return Date.now() - new Date(user.createdAt).getTime() >= 30 * 24 * 60 * 60 * 1e3;
}
};
var conditionActive = {
id: "active",
test: async ({ user }) => user.status === void 0 || user.status === "active"
};
function hasCommentIn(cache, sourceKey, userId) {
return cache !== void 0 && (cache.get(sourceKey)?.get(userId)?.length ?? 0) > 0;
}
var conditionCommentedOnProfile = {
id: "commentedProfile",
test: async ({ user, commentsByUser }) => hasCommentIn(commentsByUser, "profile", user.id)
};
var conditionCommentedOnForumThread = {
id: "commentedForumThread",
test: async ({ user, commentsByUser }) => {
const threadId = config.friendApproveEvidenceThreadId;
if (isNullOrUndefined(threadId) || threadId.isEmpty()) return false;
return hasCommentIn(commentsByUser, `forum:${threadId}`, user.id);
}
};
var BUILTIN_APPROVAL_CONDITIONS = [
conditionAlways,
conditionFollowedBy,
conditionFollowing,
conditionPremium,
conditionRecentlyActive,
conditionEstablished,
conditionActive,
conditionCommentedOnProfile,
conditionCommentedOnForumThread
];
function getApprovalCondition(id) {
return BUILTIN_APPROVAL_CONDITIONS.find((c) => c.id === id);
}
function getEnabledApprovalConditionIds() {
const stored = config.friendRequestApprovalConditions;
if (!Array.isArray(stored) || stored.length === 0) return ["always"];
return stored;
}
function setEnabledApprovalConditionIds(ids) {
config.friendRequestApprovalConditions = ids;
}
function getApprovalMode() {
return config.friendRequestApprovalMode === "all" ? "all" : "any";
}
function setApprovalMode(mode) {
config.friendRequestApprovalMode = mode;
}
async function checkApprovalConditions(ids, ctx, mode = "any") {
for (const id of ids) {
const condition = getApprovalCondition(id);
if (!condition) {
log11.warn(`未注册的审批条件: ${id},跳过`);
continue;
}
let result = false;
try {
result = await condition.test(ctx);
} catch (error) {
log11.warn(`审批条件 ${id} 判定异常:`, error);
}
if (mode === "any" && result) return true;
if (mode === "all" && !result) return false;
}
return mode === "all";
}
init_hijack();
init_constants();
var TABS = [
{ id: "general", visible: () => true },
{ id: "download", visible: () => true },
{ id: "aria2", visible: (target) => target.downloadType === 0 },
{ id: "iwaradl", visible: (target) => target.downloadType === 1 },
{ id: "mediaCenter", visible: (target) => target.downloadType === 0 && target.experimentalFeatures },
{ id: "friends", visible: () => true },
{ id: "advanced", visible: () => true }
];
var CONFIG_FIELDS = [
{ name: "checkPriority", type: "switch", tabs: ["general"], group: "download", rerender: true },
{ name: "checkDownloadLink", type: "switch", tabs: ["general"], group: "download" },
{ name: "autoDownloadMetadata", type: "switch", tabs: ["general"], group: "download" },
{ name: "autoCopySaveFileName", type: "switch", tabs: ["general"], group: "download" },
{ name: "autoInjectCheckbox", type: "switch", tabs: ["general"], group: "selection" },
{ name: "autoFollow", type: "switch", tabs: ["general"], group: "selection" },
{ name: "autoLike", type: "switch", tabs: ["general"], group: "selection" },
{ name: "filterLikedVideos", type: "switch", tabs: ["general"], group: "selection" },
{
name: "addUnlistedAndPrivate",
type: "switch",
tabs: ["general"],
group: "visibility",
onSet: (target, e) => {
const checked = e.target.checked;
target.addUnlistedAndPrivate = checked;
if (checked) target.filterUnlistedAndPrivate = false;
}
},
{
name: "filterUnlistedAndPrivate",
type: "switch",
tabs: ["general"],
group: "visibility",
onSet: (target, e) => {
const checked = e.target.checked;
target.filterUnlistedAndPrivate = checked;
if (checked) target.addUnlistedAndPrivate = false;
}
},
{ name: "autoCollapseMenu", type: "switch", tabs: ["general"], group: "interface" },
{ name: "enableWidescreen", type: "switch", tabs: ["general"], group: "interface" },
{ name: "enableBeautify", type: "switch", tabs: ["general"], group: "interface" },
{ name: "friendRequestApprove", type: "switch", tabs: ["friends"], group: "friendRequest", rerender: true },
{ name: "friendRequestApprovalMode", type: "approvalMode", tabs: ["friends"], group: "friendRequest", dependsOn: "friendRequestApprove" },
{ name: "friendRequestApprovalConditions", type: "conditions", tabs: ["friends"], group: "friendRequest", dependsOn: "friendRequestApprove" },
{ name: "friendApproveEvidenceThreadId", type: "text", tabs: ["friends"], group: "friendRequest", dependsOn: "friendRequestApprove" },
{ name: "promoteScriptAuthor", type: "switch", tabs: ["advanced"] },
{ name: "experimentalFeatures", type: "switch", tabs: ["advanced"], rerender: true },
{ name: "enableUnsafeMode", type: "switch", tabs: ["advanced"] },
{
name: "isDebug",
type: "switch",
tabs: ["advanced"],
defaultValue: false,
get: (name, defaultValue) => GM_getValue(name, defaultValue),
onSet: (target, e) => {
GM_setValue(GM_KEY_IS_DEBUG, e.target.checked);
unsafeWindow.location.reload();
}
},
{ name: "downloadPriority", type: "text", tabs: ["download"], group: "download", visible: (target) => target.checkPriority },
{ name: "downloadPath", type: "text", tabs: ["download"], group: "download", help: { text: "%#variable#%", href: "https://github.com/IwaraEnhance/IwaraDownloadTool/wiki/路径可用变量" } },
{ name: "pathNormalize", type: "switch", tabs: ["download"], group: "pathNormalize" },
{ name: "pathReplaceEmojis", type: "switch", tabs: ["download"], group: "pathNormalize" },
{ name: "pathFoldMarks", type: "switch", tabs: ["download"], group: "pathNormalize" },
{ name: "pathSanitize", type: "switch", tabs: ["download"], group: "pathNormalize" },
{ name: "pathTruncate", type: "switch", tabs: ["download"], group: "pathNormalize" },
{ name: "pathTitleMaxLength", type: "number", tabs: ["download"], group: "pathNormalize", dependsOn: "pathTruncate" },
{ name: "pathAliasMaxLength", type: "number", tabs: ["download"], group: "pathNormalize", dependsOn: "pathTruncate" },
{ name: "aria2Path", type: "text", tabs: ["aria2"], group: "aria2" },
{ name: "aria2Token", type: "password", tabs: ["aria2"], group: "aria2" },
{ name: "downloadProxy", type: "text", tabs: ["aria2", "iwaradl"], group: "proxy" },
{ name: "downloadProxyUsername", type: "text", tabs: ["aria2", "iwaradl"], group: "proxy" },
{ name: "downloadProxyPassword", type: "password", tabs: ["aria2", "iwaradl"], group: "proxy" },
{ name: "mediaCenterApi", type: "text", tabs: ["mediaCenter"], group: "mediaCenter", help: { text: "%#mediaCenterInfo#%", href: "https://github.com/dawn-lc/MediaCenter" } },
{ name: "mediaCenterApiKey", type: "password", tabs: ["mediaCenter"], group: "mediaCenter" },
{ name: "iwaradlPath", type: "text", tabs: ["iwaradl"], group: "iwaradl", help: { text: "%#iwaradlLink#%", href: "https://github.com/Izumiko/iwaradl" } },
{ name: "iwaradlToken", type: "password", tabs: ["iwaradl"], group: "iwaradl" }
];
var configEdit = class {
target;
interface;
tabButtons;
tabPanels;
activeTab = "general";
verifyBeforeSave;
constructor(config3, verifyBeforeSave) {
this.verifyBeforeSave = verifyBeforeSave;
this.target = config3;
this.target.configChange = (item) => {
this.configChange.call(this, item);
};
this.tabButtons = renderNode({
nodeType: "div",
className: "tabs"
});
this.tabPanels = renderNode({
nodeType: "div",
className: "tabPanels",
childs: TABS.map((tab) => ({
nodeType: "div",
className: "tabPanel",
attributes: {
"data-tab": tab.id
}
}))
});
let save = renderNode({
nodeType: "button",
childs: "%#save#%",
attributes: {
title: i18nList[config3.language].save
},
events: {
click: async () => {
save.disabled = !save.disabled;
if (this.verifyBeforeSave ? await this.verifyBeforeSave() : true) {
unsafeWindow.location.reload();
}
save.disabled = !save.disabled;
}
}
});
let reset = renderNode({
nodeType: "button",
childs: "%#reset#%",
attributes: {
title: i18nList[config3.language].reset
},
events: {
click: () => {
GM_setValue(GM_KEY_IS_FIRST_RUN, true);
unsafeWindow.location.reload();
}
}
});
this.interface = renderNode({
nodeType: "div",
attributes: {
id: "pluginConfig"
},
childs: [
{
nodeType: "div",
className: "main",
childs: [
{
nodeType: "h2",
childs: "%#appName#%"
},
this.tabButtons,
this.tabPanels
]
},
{
nodeType: "p",
className: "buttonList",
childs: [reset, save]
}
]
});
}
renderField(field) {
if (field.type === "switch") {
return this.switchButton(field.name, field.get, field.onSet ? (name, e) => field.onSet(this.target, e) : void 0, field.defaultValue);
}
if (field.type === "conditions") {
return this.approvalConditionsField(field.name, field.dependsOn);
}
if (field.type === "approvalMode") {
return this.approvalModeField(field.name, field.dependsOn);
}
const help = field.help ? renderNode({
nodeType: "a",
childs: field.help.text,
className: "rainbow-text",
attributes: { style: "float: inline-end;", href: field.help.href }
}) : void 0;
return this.inputComponent(field.name, field.type, help, void 0, void 0, field.dependsOn);
}
switchButton(name, get, set, defaultValue) {
return renderNode({
nodeType: "p",
className: "inputRadioLine",
childs: [
{
nodeType: "label",
childs: `%#${name}#%`,
attributes: {
for: name
}
},
{
nodeType: "input",
className: "switch",
attributes: {
type: "checkbox",
name,
checked: get !== void 0 ? get(name, defaultValue) : this.target[name] ?? defaultValue ?? false
},
events: {
change: (e) => {
if (set !== void 0) {
set(name, e);
return;
} else {
this.target[name] = e.target.checked;
}
}
}
}
]
});
}
inputComponent(name, type, help, get, set, dependsOn) {
return renderNode({
nodeType: "label",
className: "fieldLine",
attributes: dependsOn ? { "data-depends-on": dependsOn } : void 0,
childs: [
{
nodeType: "span",
childs: [`%#${name}#%`, help]
},
{
nodeType: "input",
attributes: {
name,
type: type ?? "text",
value: get !== void 0 ? get(name) : this.target[name]
},
events: {
change: (e) => {
if (set !== void 0) {
set(name, e);
return;
} else {
const input = e.target;
this.target[name] = input.type === "number" ? Number(input.value) : input.value;
}
}
}
}
]
});
}
approvalConditionsField(name, dependsOn) {
const enabled = getEnabledApprovalConditionIds();
return renderNode({
nodeType: "div",
className: ["fieldLine", "conditionsField"],
attributes: dependsOn ? { "data-depends-on": dependsOn } : void 0,
childs: [
{
nodeType: "span",
childs: `%#${name}#%`
},
{
nodeType: "div",
className: "conditionsList",
childs: BUILTIN_APPROVAL_CONDITIONS.map(
(condition) => renderNode({
nodeType: "label",
className: "conditionsItem",
childs: [
{
nodeType: "input",
attributes: {
type: "checkbox",
name,
value: condition.id,
checked: enabled.includes(condition.id)
},
events: {
change: () => {
const checked = Array.from(this.interface.querySelectorAll(`.conditionsList input[name=${name}]:checked`)).map((i) => i.value);
setEnabledApprovalConditionIds(checked.length > 0 ? checked : ["always"]);
}
}
},
`%#approvalCondition_${condition.id}#%`
]
})
)
}
]
});
}
approvalModeField(name, dependsOn) {
const current = getApprovalMode();
const modes = ["any", "all"];
return renderNode({
nodeType: "div",
className: ["fieldLine", "conditionsField"],
attributes: dependsOn ? { "data-depends-on": dependsOn } : void 0,
childs: [
{
nodeType: "span",
childs: `%#${name}#%`
},
{
nodeType: "div",
className: "conditionsList",
childs: modes.map(
(mode) => renderNode({
nodeType: "label",
className: "conditionsItem",
childs: [
{
nodeType: "input",
attributes: {
type: "radio",
name,
value: mode,
checked: current === mode
},
events: {
change: (e) => {
const value = e.target.value;
if (value === "all") setApprovalMode("all");
else setApprovalMode("any");
}
}
},
`%#approvalMode_${mode}#%`
]
})
)
}
]
});
}
downloadTypeSelect() {
return renderNode({
nodeType: "fieldset",
className: "downloadType",
childs: [
{
nodeType: "div",
className: "fieldTitle",
childs: "%#downloadType#%"
},
...Object.keys(DownloadType).filter((i) => isNaN(Number(i))).map(
(type, index) => renderNode({
nodeType: "label",
childs: [
{
nodeType: "input",
attributes: {
type: "radio",
name: "downloadType",
value: index,
checked: index === Number(this.target.downloadType)
},
events: {
change: (e) => {
this.target.downloadType = Number(e.target.value);
}
}
},
type
]
})
)
]
});
}
updateVisibility() {
this.interface.querySelectorAll("[data-depends-on]").forEach((element) => {
const dependsOn = element.dataset.dependsOn;
if (dependsOn) element.style.display = this.target[dependsOn] ? "" : "none";
});
}
configChange(item) {
if (item === "downloadType") {
this.interface.querySelectorAll("[name=downloadType]").forEach((radio) => {
radio.checked = Number(radio.value) === Number(this.target.downloadType);
});
this.renderAllTabs();
return;
}
if (CONFIG_FIELDS.find((field) => field.name === item)?.rerender) {
this.renderAllTabs();
return;
}
const customFieldType = CONFIG_FIELDS.find((field) => field.name === item)?.type;
if (customFieldType === "conditions" || customFieldType === "approvalMode") return;
const element = this.interface.querySelector(`[name=${item}]`);
if (element) {
if (element.type === "checkbox") element.checked = this.target[item];
else element.value = this.target[item];
}
this.updateVisibility();
}
tabChange(tab) {
this.activeTab = tab;
this.tabButtons.querySelectorAll("button.tab").forEach((button) => {
button.classList.toggle("active", button.dataset.tab === tab);
});
this.tabPanels.querySelectorAll(".tabPanel").forEach((panel2) => {
panel2.style.display = panel2.dataset.tab === tab ? "" : "none";
});
this.scrollActiveTabIntoView(true);
this.updateVisibility();
}
scrollActiveTabIntoView(smooth = false) {
const button = this.tabButtons.querySelector(`button.tab.active`);
if (!button) return;
const max = this.tabButtons.scrollWidth - this.tabButtons.clientWidth;
if (max <= 0) return;
const target = button.offsetLeft + button.offsetWidth / 2 - this.tabButtons.clientWidth / 2;
const left = Math.max(0, Math.min(max, target));
if (smooth) this.tabButtons.scrollTo({ left, behavior: "smooth" });
else this.tabButtons.scrollLeft = left;
}
renderAllTabs() {
if (!TABS.find((tab) => tab.id === this.activeTab && tab.visible(this.target))) {
this.activeTab = "general";
}
while (this.tabButtons.hasChildNodes()) {
this.tabButtons.removeChild(this.tabButtons.firstChild);
}
for (const tab of TABS) {
if (!tab.visible(this.target)) continue;
originalNodeAppendChild.call(
this.tabButtons,
renderNode({
nodeType: "button",
className: this.activeTab === tab.id ? ["tab", "active"] : "tab",
attributes: {
"data-tab": tab.id,
type: "button"
},
childs: `%#${tab.id}Tab#%`,
events: {
click: () => {
this.tabChange(tab.id);
}
}
})
);
}
for (const tab of TABS) {
const panel2 = this.tabPanels.querySelector(`.tabPanel[data-tab=${tab.id}]`);
while (panel2.hasChildNodes()) {
panel2.removeChild(panel2.firstChild);
}
if (!tab.visible(this.target)) {
panel2.style.display = "none";
continue;
}
if (tab.id === "general") {
originalNodeAppendChild.call(
panel2,
renderNode({
nodeType: "label",
className: "languageLine",
childs: [
"%#language#% ",
{
nodeType: "input",
className: "inputRadioLine",
attributes: {
name: "language",
type: "text",
value: this.target.language
},
events: {
change: (event) => {
this.target.language = event.target.value;
}
}
}
]
})
);
originalNodeAppendChild.call(panel2, this.downloadTypeSelect());
}
const groups = new Map();
for (const field of CONFIG_FIELDS) {
if (!field.tabs.includes(tab.id)) continue;
if (field.visible && !field.visible(this.target)) continue;
const key = field.group ?? "__flat__";
const list = groups.get(key) ?? [];
list.push(field);
groups.set(key, list);
}
for (const [group, fields] of groups) {
if (group === "__flat__") {
fields.forEach((field) => originalNodeAppendChild.call(panel2, this.renderField(field)));
} else {
originalNodeAppendChild.call(
panel2,
renderNode({
nodeType: "fieldset",
childs: [
{
nodeType: "div",
className: "fieldTitle",
childs: `%#${group}Group#%`
},
...fields.map((field) => this.renderField(field))
]
})
);
}
}
panel2.style.display = this.activeTab === tab.id ? "" : "none";
}
this.scrollActiveTabIntoView();
this.updateVisibility();
}
inject() {
if (!unsafeWindow.document.querySelector("#pluginConfig")) {
originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
this.renderAllTabs();
}
}
};
function currentVideoId() {
return getPageParams()[":id"];
}
function currentPlaylistId() {
const params = getPageParams();
return params[":id"] && unsafeWindow.location.pathname.startsWith("/playlist") ? params[":id"] : void 0;
}
init_db();
init_env();
init_hijack();
var callbacks;
function setCheckboxCallbacks(cb) {
callbacks = cb;
}
function uninjectCheckbox(element) {
if (element instanceof HTMLElement) {
if (element instanceof HTMLInputElement && element.classList.contains("selectButton")) {
element.hasAttribute("videoID") && pageSelectButtons.delete(element.getAttribute("videoID"));
}
if (element.querySelector("input.selectButton")) {
element.querySelectorAll(".selectButton").forEach((i) => i.hasAttribute("videoID") && pageSelectButtons.delete(i.getAttribute("videoID")));
}
}
}
async function injectCheckbox(element) {
const thumbnail = element.querySelector("a.videoTeaser__thumbnail");
if (isNullOrUndefined(thumbnail)) return;
let ID = thumbnail.href.toURL().pathname.split("/")[2];
if (isNullOrUndefined(ID)) return;
let info = await db.getVideoById(ID);
const hasFullInfo = info?.Type === "full" || info?.Type === "partial";
const authorLink = element.querySelector("a.username");
let Title = hasFullInfo ? info?.Title : info?.RAW?.title ?? element.querySelector(".videoTeaser__title")?.getAttribute("title") ?? void 0;
let Alias = hasFullInfo ? info?.Alias : info?.RAW?.user.name ?? authorLink?.getAttribute("title") ?? void 0;
let Author = hasFullInfo ? info?.Author : info?.RAW?.user.username ?? authorLink?.href.toURL().pathname.split("/").pop();
let UploadTime = hasFullInfo ? info?.UploadTime : new Date(info?.RAW?.updatedAt ?? 0).getTime();
let button = renderNode({
nodeType: "input",
attributes: {
type: "checkbox",
videoID: ID,
checked: selectList.has(ID) ? true : void 0,
videoName: Title,
videoAlias: Alias,
videoAuthor: Author,
videoUploadTime: UploadTime
},
className: "selectButton",
events: {
click: (event) => {
;
event.target.checked ? selectList.set(ID, {
Type: "init",
ID,
Title,
Alias,
Author,
UploadTime
}) : selectList.delete(ID);
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
}
});
let item = thumbnail.parentElement;
item?.style.setProperty("position", "relative");
pageSelectButtons.set(ID, button);
originalNodeAppendChild.call(item, button);
if (!isNullOrUndefined(Author)) {
const AuthorInfo = await db.getFollowByUsername(Author);
if (AuthorInfo?.following && thumbnail.querySelector(".follow") === null) {
originalNodeAppendChild.call(
thumbnail,
renderNode({
nodeType: "div",
className: "follow",
childs: {
nodeType: "div",
className: ["text", "text--white", "text--tiny", "text--bold"],
childs: "%#following#%"
}
})
);
}
}
if (!config.mediaCenterApi.isEmpty() && !config.mediaCenterApiKey.isEmpty()) {
const mediaCenterId = await db.getMediaCenterIdMap(ID);
if (!isNullOrUndefined(mediaCenterId) && !mediaCenterId.isEmpty() && thumbnail.querySelector(".downloaded") === null) {
originalNodeAppendChild.call(
thumbnail,
renderNode({
nodeType: "div",
className: "downloaded",
childs: {
nodeType: "div",
className: ["text", "text--white", "text--tiny", "text--bold"],
childs: "%#downloaded#%"
}
})
);
}
}
if (getPageType() === "playlist" && callbacks) {
const playlistId = currentPlaylistId();
if (playlistId && await callbacks.canDeletePlaylistItem(playlistId)) {
let deletePlaylistItme = renderNode({
nodeType: "button",
attributes: {
videoID: ID
},
childs: "%#delete#%",
className: "deleteButton",
events: {
click: async (event) => {
if (await callbacks.deletePlaylistItem(playlistId, ID)) {
newToast(1, { text: `${Title} %#deleteSucceed#%`, close: true }).show();
deletePlaylistItme.remove();
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
}
});
originalNodeAppendChild.call(item, deletePlaylistItme);
}
}
}
init_constants();
init_hijack();
var DEBUG_SWITCH_THRESHOLD = 5;
var waterMark = class {
debugSwitchCount = 0;
selected = renderNode({
nodeType: "span",
childs: ` %#selected#% ${selectList.size} `
});
debugFlag = renderNode({
nodeType: "span",
childs: `${GM_getValue(GM_KEY_IS_DEBUG) ? `${i18nList[config.language].isDebug} ${GM_info.scriptHandler}` : ""}`
});
body = renderNode({
nodeType: "p",
className: "fixed-bottom-right",
childs: [`%#appName#% ${GM_getValue(GM_KEY_VERSION)} `, this.selected, this.debugFlag],
events: {
click: (e) => {
if (GM_getValue(GM_KEY_IS_DEBUG)) return;
if (this.debugSwitchCount < DEBUG_SWITCH_THRESHOLD) {
this.debugSwitchCount++;
return;
} else {
GM_setValue(GM_KEY_IS_DEBUG, true);
this.debugFlag.textContent = `${GM_getValue(GM_KEY_IS_DEBUG) ? i18nList[config.language].isDebug : ""}`;
unsafeWindow.location.reload();
}
}
}
});
inject() {
originalNodeAppendChild.call(unsafeWindow.document.body, this.body);
}
};
var propertyProbe;
var runtimeVerdict;
function isWebLockSupported() {
if (propertyProbe !== void 0) return propertyProbe;
try {
const nav = unsafeWindow.navigator;
propertyProbe = !!nav && !!nav.locks && typeof nav.locks.request === "function";
} catch {
propertyProbe = false;
}
return propertyProbe;
}
async function verifyWebLockSupported() {
if (runtimeVerdict !== void 0) return runtimeVerdict;
if (!isWebLockSupported()) {
runtimeVerdict = false;
return false;
}
try {
const probe = new WebLock("GMLock-verify");
runtimeVerdict = await probe.peekAsync(`__e2e_weblock_verify_${Date.now()}`);
} catch {
runtimeVerdict = false;
}
return runtimeVerdict;
}
function webLockVerdict() {
return runtimeVerdict ?? isWebLockSupported();
}
var WebLock = class {
pendingResolve = new Map();
constructor(_owner) {
}
async acquireAsync(name, _ttl) {
if (!isWebLockSupported()) return false;
const locks = unsafeWindow.navigator.locks;
let settleAcquire;
const acquireGate = new Promise((resolve) => {
settleAcquire = resolve;
});
try {
void locks.request(
name,
{ ifAvailable: true },
(lock) => {
if (!lock) {
settleAcquire(false);
return;
}
settleAcquire(true);
return new Promise((resolve) => this.pendingResolve.set(name, resolve));
}
).catch(() => {
settleAcquire(false);
this.pendingResolve.delete(name);
});
} catch {
settleAcquire(false);
this.pendingResolve.delete(name);
}
const result = await acquireGate;
if (!result) this.pendingResolve.delete(name);
return result;
}
async isHeld(name) {
if (!isWebLockSupported()) return false;
const snapshot = await unsafeWindow.navigator.locks.query();
return (snapshot.held ?? []).some((l2) => l2.name === name);
}
async peekAsync(name) {
if (!isWebLockSupported()) return false;
const locks = unsafeWindow.navigator.locks;
let granted = false;
try {
await locks.request(
name,
{ ifAvailable: true },
(lock) => {
if (!lock) return;
granted = true;
}
);
} catch {
granted = false;
}
return granted;
}
release(name) {
const resolve = this.pendingResolve.get(name);
if (resolve) {
this.pendingResolve.delete(name);
resolve();
}
}
};
var GMLockTTL = ((GMLockTTL3) => {
GMLockTTL3[GMLockTTL3["ShortTask"] = 6e4] = "ShortTask";
GMLockTTL3[GMLockTTL3["BatchTask"] = 6e4] = "BatchTask";
GMLockTTL3[GMLockTTL3["BatchTaskSlow"] = 12e4] = "BatchTaskSlow";
GMLockTTL3[GMLockTTL3["DaemonTask"] = 75e3] = "DaemonTask";
return GMLockTTL3;
})(GMLockTTL || {});
var GMLock = class _GMLock {
static PREFIX = "GMLock:";
owner;
heartbeats = new Map();
static webLock = new WebLock("GMLock");
static useWebLock = new Map();
static webAcquireState = new Map();
held = new Map();
constructor(owner) {
this.owner = owner;
}
static key(name) {
return _GMLock.PREFIX + name;
}
backend(name) {
if (!_GMLock.useWebLock.has(name)) _GMLock.useWebLock.set(name, webLockVerdict());
return _GMLock.useWebLock.get(name) ? "web" : "gm";
}
static reverifyPromise;
reverifyBackend() {
_GMLock.reverifyPromise ??= verifyWebLockSupported().then((ok) => {
if (!ok) _GMLock.useWebLock.clear();
});
return _GMLock.reverifyPromise;
}
async acquireAsync(name, ttl) {
await this.reverifyBackend();
await this.acquireReady(name);
if (!this.acquire(name, ttl)) return false;
return this.isHeld(name);
}
static webKey(name) {
return _GMLock.key(name);
}
async acquireReady(name) {
if (this.backend(name) !== "web") return;
const key = _GMLock.webKey(name);
const granted = await _GMLock.webLock.peekAsync(key);
_GMLock.webAcquireState.set(key, granted ? "granted" : "denied");
}
acquire(name, ttl) {
const key = _GMLock.key(name);
const now = Date.now();
const shadowWrite = () => GM_setValue(key, { owner: this.owner, expires: now + ttl });
if (this.backend(name) === "web") {
const shadow = GM_getValue(key);
if (shadow && shadow.owner !== this.owner && shadow.expires > now && this.held.has(name) === false) return false;
const state = _GMLock.webAcquireState.get(key);
if (state === "granted") {
if (this.held.has(name)) {
shadowWrite();
return true;
}
this.held.set(name, true);
_GMLock.webAcquireState.delete(key);
shadowWrite();
void _GMLock.webLock.acquireAsync(key).then((granted) => {
if (!granted) this.held.delete(name);
});
return true;
}
if (state === "denied") void this.acquireReady(name);
else if (state === void 0) void this.acquireReady(name);
}
const current = GM_getValue(key);
if (current && current.owner !== this.owner && current.expires > now) return false;
GM_setValue(key, { owner: this.owner, expires: now + ttl });
return true;
}
renew(name, ttl) {
const key = _GMLock.key(name);
if (this.backend(name) === "web") {
if (!this.held.has(name)) return false;
GM_setValue(key, { owner: this.owner, expires: Date.now() + ttl });
return true;
}
const current = GM_getValue(key);
if (current?.owner !== this.owner) return false;
GM_setValue(key, { owner: this.owner, expires: Date.now() + ttl });
return true;
}
isHeld(name) {
const key = _GMLock.key(name);
if (this.backend(name) === "web") {
const shadow = GM_getValue(key);
const shadowValid = !!shadow && shadow.owner === this.owner && shadow.expires > Date.now();
if (this.held.has(name)) return shadowValid;
return shadowValid;
}
const value = GM_getValue(key);
return !!value && value.owner === this.owner && value.expires > Date.now();
}
startHeartbeatFor(name, ttl, onLost, interval) {
this.startHeartbeat(name, ttl, interval ?? ttl / 3, onLost);
}
startHeartbeat(name, ttl, interval, onLost) {
this.stopHeartbeat(name);
const timer = setInterval(() => {
if (!this.renew(name, ttl)) {
this.stopHeartbeat(name);
onLost?.(name);
}
}, interval);
this.heartbeats.set(name, timer);
}
stopHeartbeat(name) {
const timer = this.heartbeats.get(name);
if (timer !== void 0) {
clearInterval(timer);
this.heartbeats.delete(name);
}
}
acquireWithHeartbeat(name, ttl, interval = ttl / 3, onLost) {
if (!this.acquire(name, ttl)) return false;
this.startHeartbeat(name, ttl, interval, onLost);
return true;
}
async acquireWaitWithHeartbeat(name, ttl, interval = ttl / 3, onLost, timeout = Infinity) {
await this.reverifyBackend();
await this.acquireReady(name);
if (!await this.acquireWait(name, ttl, timeout)) return false;
this.startHeartbeat(name, ttl, interval, onLost);
return true;
}
release(name) {
this.stopHeartbeat(name);
const key = _GMLock.key(name);
if (this.backend(name) === "web") {
const isWebHeld = this.held.has(name);
if (isWebHeld) {
this.held.delete(name);
_GMLock.webLock.release(key);
}
if (GM_getValue(key)?.owner === this.owner) {
_GMLock.webLock.release(key);
GM_deleteValue(key);
}
return;
}
if (GM_getValue(key)?.owner === this.owner) {
GM_deleteValue(key);
}
}
async acquireWait(name, ttl, timeout = Infinity) {
const deadline = Date.now() + timeout;
for (; ; ) {
if (this.acquire(name, ttl)) return true;
if (Date.now() >= deadline) return false;
const key = _GMLock.key(name);
const current = GM_getValue(key);
const remain = current ? Math.max(0, current.expires - Date.now()) : 0;
const waitMs = Math.max(15, Math.min(remain + 5, deadline - Date.now()));
await new Promise((resolve) => {
let listenerId;
let timer;
let settled = false;
const done = () => {
if (settled) return;
settled = true;
if (listenerId !== void 0) GM_removeValueChangeListener(listenerId);
if (timer !== void 0) clearTimeout(timer);
resolve();
};
listenerId = GM_addValueChangeListener(key, () => done());
timer = setTimeout(done, waitMs);
});
}
}
static pruneExpired() {
const now = Date.now();
for (const key of GM_listValues()) {
if (!key.startsWith(_GMLock.PREFIX)) continue;
const value = GM_getValue(key);
const valid = typeof value?.owner === "string" && typeof value.expires === "number" && Number.isFinite(value.expires);
if (valid && value.expires > now) continue;
if (JSON.stringify(GM_getValue(key)) === JSON.stringify(value)) GM_deleteValue(key);
}
}
};
async function followUser(userId) {
try {
return (await unlimitedFetch(apiUrl(`/user/${userId}/followers`), { method: "POST", headers: await getAuth() })).ok;
} catch {
return false;
}
}
async function likeVideo(videoId) {
try {
return (await unlimitedFetch(apiUrl(`/video/${videoId}/like`), { method: "POST", headers: await getAuth() })).ok;
} catch {
return false;
}
}
async function addFriend(userId) {
try {
return (await unlimitedFetch(apiUrl(`/user/${userId}/friends`), { method: "POST", headers: await getAuth() })).ok;
} catch {
return false;
}
}
async function removeFriend(userId) {
try {
return (await unlimitedFetch(apiUrl(`/user/${userId}/friends`), { method: "DELETE", headers: await getAuth() })).ok;
} catch {
return false;
}
}
async function fetchAllFriendRequests(meId, maxPages = 32) {
const received = [];
for (let page = 0; page < maxPages; page++) {
const res = await unlimitedFetch(apiUrl(`/user/${meId}/friends/requests`, new URLSearchParams({ page: String(page) })), {
method: "GET",
headers: await getAuth()
});
if (!res.ok) throw new Error(`fetchFriendRequests failed: HTTP ${res.status}`);
const data = await res.json();
received.push(...(data.results ?? []).filter((e) => e.user.id !== meId));
const limit = data.limit > 0 ? data.limit : (data.results ?? []).length;
if ((data.results ?? []).length < limit || data.results.length === 0) break;
}
return received;
}
init_env();
function createProgressTracker(initialText) {
let handle;
const ensure = (text, ratio) => {
if (handle !== void 0) {
if (text !== void 0) handle.update({ body: text });
if (ratio !== void 0) handle.update({ progressRatio: ratio });
return handle;
}
const first = { body: text ?? initialText, duration: -1, close: false };
if (ratio !== void 0) first.progressRatio = ratio;
handle = progress(first);
return handle;
};
return {
start: () => void ensure(),
update: (text) => {
if (text.isEmpty()) return;
ensure(text);
},
reportRatio: (ratio) => ensure(void 0, ratio),
close: () => {
handle?.dismiss();
handle = void 0;
}
};
}
async function runBatchTask(options) {
const lock = new GMLock(UUID());
const tracker = createProgressTracker("");
let acquired = await lock.acquireAsync(options.lockName, options.ttl);
if (acquired) {
lock.startHeartbeatFor(options.lockName, options.ttl, onLostHeartbeat(options));
} else {
if (options.onConflict === "exit") {
report("warn", { body: "%#taskInProgress#%", duration: 3e3, close: true });
return void 0;
}
const waiting = createProgressTracker("%#waitingForLock#%");
waiting.start();
try {
acquired = await lock.acquireWaitWithHeartbeat(options.lockName, options.ttl);
} finally {
waiting.close();
}
if (!acquired) return void 0;
}
try {
if (!lock.isHeld(options.lockName)) {
taskConflict();
return void 0;
}
return await options.iterate({
isHeld: () => lock.isHeld(options.lockName),
progress: (text) => tracker.update(text),
progressRatio: (done, total) => tracker.reportRatio(total > 0 ? done / total : 0),
progressEnd: () => tracker.close()
});
} finally {
tracker.close();
lock.release(options.lockName);
}
}
function onLostHeartbeat(options) {
return () => {
void options;
};
}
function taskConflict() {
report("warn", { body: "%#taskInProgress#%", duration: 3e3, close: true });
}
var downloadLinks_default = [
"iwara.zip",
"pan.baidu",
"/s/",
"mega.nz",
"drive.google.com",
"aliyundrive",
"uploadgig",
"katfile",
"storex",
"subyshare",
"rapidgator",
"filebe",
"filespace",
"mexa.sh",
"mexashare",
"mx-sh.net",
"icerbox",
"alfafile",
"1drv.ms",
"onedrive.",
"gofile.io",
"workupload.com",
"pixeldrain.",
"dailyuploads.net",
"katfile.com",
"fikper.com",
"frdl.io",
"rg.to",
"gigafile.nu",
"mediafire.com"
];
init_env();
var DOWNLOAD_LINK_PATTERNS = downloadLinks_default;
function checkIsHaveDownloadLink(comment) {
if (!config.checkDownloadLink || isNullOrUndefined(comment) || comment.isEmpty()) {
return false;
}
return DOWNLOAD_LINK_PATTERNS.filter((i) => comment.toLowerCase().includes(i)).any();
}
init_env();
init_env();
init_log();
var log12 = createLogger("Aria2");
var enqueueTrackTaskHook;
function setAria2TrackEnqueueHook(hook) {
enqueueTrackTaskHook = hook;
}
async function aria2API(method, params) {
return await (await unlimitedFetch(config.aria2Path, {
headers: {
accept: "application/json",
"content-type": "application/json"
},
body: JSON.stringify(
{
jsonrpc: "2.0",
method,
id: UUID(),
params: [`token:${config.aria2Token}`, ...params]
},
(_, v2) => typeof v2 === "boolean" ? String(v2) : v2
),
method: "POST"
})).json();
}
function aria2TaskExtractVideoID(task) {
try {
if (isNullOrUndefined(task.files) || task.files.length !== 1) return;
const file = task.files[0];
if (isNullOrUndefined(file)) return;
if (file.uris.length < 1) return;
let downloadUrl = file.uris[0].uri.toURL();
if (isNullOrUndefined(downloadUrl)) return;
let videoID;
if (downloadUrl.searchParams.has("videoid")) videoID = downloadUrl.searchParams.get("videoid");
if (!isNullOrUndefined(videoID) && !videoID.isEmpty()) return videoID;
if (isNullOrUndefined(file.path) || file.path.isEmpty()) return;
let path = analyzeLocalPath(file.path);
if (isNullOrUndefined(path.fullName) || path.fullName.isEmpty()) return;
videoID = path.fullName.toLowerCase().among("[", "].mp4", false, true);
if (videoID.isEmpty()) return;
return videoID;
} catch (error) {
log12.debug(`check aria2 task file fail! ${stringify(task)}`);
return;
}
}
async function aria2Download(videoInfo, overwrite = void 0) {
const localPath = getDownloadPath(videoInfo);
const downloadUrl = buildDownloadUrl(videoInfo, localPath);
const downloadParams = prune({
"force-save": true,
"allow-overwrite": true,
"all-proxy": config.downloadProxy,
"all-proxy-passwd": !config.downloadProxy.isEmpty() ? config.downloadProxyPassword : void 0,
"all-proxy-user": !config.downloadProxy.isEmpty() ? config.downloadProxyUsername : void 0,
out: localPath.fullName,
dir: localPath.directory,
referer: window.location.hostname,
header: ["Cookie:" + unsafeWindow.document.cookie]
});
try {
let res = await aria2API("aria2.addUri", [[downloadUrl.href], downloadParams]);
if (res.result.isEmpty()) throw `aria2 下载失败:${stringify(res)}`;
report("info", {
body: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskSucceed#%`),
gravity: "bottom"
});
enqueueTrackTaskHook?.(videoInfo.ID, res.result, downloadParams);
} catch (error) {
report("info", {
body: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskFail#%`),
gravity: "bottom"
});
}
}
init_env();
init_env();
init_log();
var log13 = createLogger("Download");
var retryDownloadHook;
function setRetryDownloadHook(hook) {
retryDownloadHook = hook;
}
function iwaradlDownload(videoInfo) {
;
(async function(videoInfo2) {
try {
let proxyURL;
if (!config.downloadProxy.isEmpty()) {
proxyURL = new URL(config.downloadProxy);
proxyURL.username = config.downloadProxyUsername;
proxyURL.password = config.downloadProxyPassword;
}
let downloadPathTemplate = new Path(config.downloadPath, false);
let response = await unlimitedFetch(config.iwaradlPath, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
authorization: `Bearer ${config.iwaradlToken}`
},
body: JSON.stringify(
prune({
urls: [`https://www.${domain2}/video/${videoInfo2.ID}`],
options: {
proxy_url: proxyURL ? proxyURL.href : void 0,
cookies: unsafeWindow.document.cookie,
download_dir: downloadPathTemplate.directory,
filename_template: downloadPathTemplate.fullName
}
})
)
});
if (response.ok) {
log13.info(`${videoInfo2.Title} %#pushTaskSucceed#%`);
report("info", {
body: toastNode(`${videoInfo2.Title}[${videoInfo2.ID}] %#pushTaskSucceed#%`)
});
}
} catch (error) {
report("error", {
title: "%#iwaradlDownload#%",
body: toastNode([`${videoInfo2.Title}[${videoInfo2.ID}] %#pushTaskFailed#% `, { nodeType: "br" }, stringify(error)], "%#iwaradlDownload#%")
});
}
})(videoInfo);
}
function othersDownload(videoInfo) {
const DownloadUrl = buildDownloadUrl(videoInfo, getDownloadPath(videoInfo));
GM_openInTab(DownloadUrl.href, { active: false, insert: true, setParent: true });
}
function browserDownloadErrorParse(error) {
let errorInfo = stringify(error);
if (!(error instanceof Error)) {
errorInfo = {
not_enabled: `%#browserDownloadNotEnabled#%`,
not_whitelisted: `%#browserDownloadNotWhitelisted#%`,
not_permitted: `%#browserDownloadNotPermitted#%`,
not_supported: `%#browserDownloadNotSupported#%`,
not_succeeded: `%#browserDownloadNotSucceeded#% ${isNullOrUndefined(error.details) ? "UnknownError" : error.details}`
}[error.error] || `%#browserDownloadUnknownError#%`;
}
return errorInfo;
}
function browserDownload(videoInfo) {
;
(async function(videoInfo2) {
function reportError(error) {
report("error", {
title: "%#browserDownload#%",
body: toastNode([`${videoInfo2.Title}[${videoInfo2.ID}] %#downloadFailed#%`, { nodeType: "br" }, browserDownloadErrorParse(error), { nodeType: "br" }, `%#tryRestartingDownload#%`], "%#browserDownload#%"),
close: true,
onClick: async (host) => {
;
host?.hide();
await retryDownloadHook?.(videoInfo2);
}
});
}
GM_download({
url: videoInfo2.DownloadUrl,
saveAs: false,
name: getDownloadPath(videoInfo2).fullPath,
onerror: (err) => reportError(err),
ontimeout: () => reportError(new Error("%#browserDownloadTimeout#%"))
});
})(videoInfo);
}
function generateMatadataURL(videoInfo) {
const metadataContent = generateMetadataContent(videoInfo);
const blob = new Blob([metadataContent], { type: "text/plain" });
return URL.createObjectURL(blob);
}
function getMatadataPath(videoInfo) {
const videoPath = getDownloadPath(videoInfo);
return `${videoPath.directory}/${videoPath.baseName}.json`;
}
function generateMetadataContent(videoInfo) {
const metadata = Object.assign({}, videoInfo, {
DownloadPath: getDownloadPath(videoInfo).fullPath,
MetaDataVersion: GM_info.script.version
});
return JSON.stringify(
metadata,
(key, value) => {
if (value instanceof Date) {
return value.toISOString();
}
return value;
},
2
);
}
function browserDownloadMetadata(videoInfo) {
const url = generateMatadataURL(videoInfo);
function reportError(error) {
report("error", {
title: "%#browserDownload#%",
body: toastNode([`${videoInfo.Title}[${videoInfo.ID}] %#videoMetadata#% %#downloadFailed#%`, { nodeType: "br" }, browserDownloadErrorParse(error)], "%#browserDownload#%"),
close: true
});
}
GM_download({
url,
saveAs: false,
name: getMatadataPath(videoInfo),
onerror: (err) => reportError(err),
ontimeout: () => reportError(new Error("%#browserDownloadTimeout#%")),
onload: () => URL.revokeObjectURL(url)
});
}
function othersDownloadMetadata(videoInfo) {
const url = generateMatadataURL(videoInfo);
const metadataFile = analyzeLocalPath(getMatadataPath(videoInfo)).fullName;
const downloadHandle = renderNode({
nodeType: "a",
attributes: {
href: url,
download: metadataFile
}
});
downloadHandle.click();
downloadHandle.remove();
URL.revokeObjectURL(url);
}
init_env();
init_env();
init_log();
init_db();
var log14 = createLogger("DownloadQueue");
var getPageTypeProbe = () => "page";
function setPageTypeProbe(probe) {
getPageTypeProbe = probe;
}
async function addDownloadTask() {
if (unsafeWindow.document.querySelector("#pluginOverlay")) return;
let textArea = renderNode({
nodeType: "textarea",
attributes: {
placeholder: i18nList[config.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 (!isNullOrUndefined(textArea.value) && !textArea.value.isEmpty()) {
let list = [];
try {
list = textArea.value.split("|").map((ID) => [
ID.trim(),
{
Type: "init",
ID: ID.trim()
}
]);
} catch (error) {
throw new Error("解析结果不是符合预期的列表");
}
if (list.length > 0) {
analyzeDownloadTask(new Dictionary(list));
}
}
body.remove();
}
},
childs: i18nList[config.language].ok
}
]
});
unsafeWindow.document.body.appendChild(body);
}
async function downloadTaskUnique(taskList) {
try {
let stoped = prune(
(await aria2API("aria2.tellStopped", [0, 4096, ["gid", "status", "files", "errorCode", "bittorrent"]])).result.filter((task) => isNullOrUndefined(task.bittorrent)).map((task) => {
let ID = aria2TaskExtractVideoID(task);
if (!isNullOrUndefined(ID) && !ID.isEmpty()) {
return {
id: ID,
data: task
};
}
})
);
let active = prune(
(await aria2API("aria2.tellActive", [["gid", "status", "files", "downloadSpeed", "bittorrent"]])).result.filter((task) => isNullOrUndefined(task.bittorrent)).map((task) => {
let ID = aria2TaskExtractVideoID(task);
if (!isNullOrUndefined(ID) && !ID.isEmpty()) {
return {
id: ID,
data: task
};
}
})
);
let downloadCompleted = stoped.filter((task) => task.data.status === "complete").unique("id");
let startedAndCompleted = [...active, ...downloadCompleted].map((i) => i.id);
for (let key of taskList.keysArray().intersect(startedAndCompleted)) {
taskList.delete(key);
}
} catch (error) {
newToast(3, {
node: toastNode([`%#duplicateTaskAnalysisError#%`, { nodeType: "br" }, stringify(error)])
}).show();
}
}
var analyzeDownloadTaskRunning = false;
var ANALYZE_DOWNLOAD_LOCK = "analyzeDownloadTask";
async function analyzeDownloadTask(taskList = selectList) {
if (analyzeDownloadTaskRunning) {
taskConflict();
return;
}
log14.debug(`[时序] analyze start: size=${taskList.size}, isSnapshot=${taskList !== selectList}`);
analyzeDownloadTaskRunning = true;
try {
await runBatchTask({
lockName: ANALYZE_DOWNLOAD_LOCK,
ttl: 6e4,
onConflict: "wait",
iterate: async (ctl) => {
const size = taskList.size;
let done = 0;
const progressText = () => `${i18nList[config.language].parsingProgress}[${taskList.size}/${size}] `;
ctl.progress(progressText());
if (config.experimentalFeatures && config.downloadType === 0) {
await downloadTaskUnique(taskList);
ctl.progress(progressText());
}
for (const [id, info] of taskList) {
if (!ctl.isHeld()) {
taskConflict();
return;
}
await pushDownloadTask(await parseVideoInfo(info));
taskList.delete(id);
done++;
ctl.progress(progressText());
ctl.progressRatio(1 - taskList.size / size, 1);
!config.enableUnsafeMode && await delay(3e3);
}
log14.debug(`[时序] analyze end, taskList.remain=${taskList.size}, selectList.size=${selectList.size}`);
newToast(1, {
text: `%#allCompleted#%`,
duration: -1,
close: true,
onClick() {
this.hide();
}
}).show();
}
});
} finally {
analyzeDownloadTaskRunning = false;
}
}
var pushingVideoIds = new Set();
var PUSH_LOCK_PREFIX = "pushDownloadTask:";
var pushLock = new GMLock(UUID());
async function pushDownloadTask(videoInfo) {
if (pushingVideoIds.has(videoInfo.ID)) {
log14.debug(`Skip duplicate push: ${videoInfo.ID}`);
return;
}
const lockName = `${PUSH_LOCK_PREFIX}${videoInfo.ID}`;
if (!await pushLock.acquireAsync(lockName, 6e4)) {
log14.debug(`Skip duplicate push (another page): ${videoInfo.ID}`);
return;
}
pushingVideoIds.add(videoInfo.ID);
try {
await pushDownloadTaskInner(videoInfo);
} finally {
pushingVideoIds.delete(videoInfo.ID);
pushLock.release(lockName);
}
}
async function pushDownloadTaskInner(videoInfo) {
switch (videoInfo.Type) {
case "partial":
const partialCache = await db.getVideoById(videoInfo.ID);
if (!isNullOrUndefined(partialCache) && partialCache.Type !== "full") await db.putVideo(videoInfo);
case "cache":
case "init":
return await pushDownloadTaskInner(await parseVideoInfo(videoInfo));
case "fail":
const cache = await db.getVideoById(videoInfo.ID);
const externalUrl = videoInfo.External && !isNullOrUndefined(videoInfo.ExternalUrl) && !videoInfo.ExternalUrl.isEmpty() ? videoInfo.ExternalUrl : void 0;
newToast(3, {
close: true,
node: toastNode([`${videoInfo.Title ?? videoInfo.RAW?.title ?? cache?.RAW?.title}[${videoInfo.ID}] %#parsingFailed#%`, { nodeType: "br" }, videoInfo.Msg], "%#createTask#%"),
buttons: [
isNullOrUndefined(externalUrl) ? {
text: "%#tryReparseDownload#%",
onClick: async (t) => {
t.hide();
await pushDownloadTask(await parseVideoInfo({ Type: "init", ID: videoInfo.ID, RAW: videoInfo.RAW ?? cache?.RAW }));
}
} : {
text: "%#openVideoLink#%",
onClick: (t) => {
t.hide();
GM_openInTab(externalUrl, { active: false, insert: true, setParent: true });
}
}
]
}).show();
break;
case "full":
await db.putVideo(videoInfo);
const authorInfo = await db.getFollowById(videoInfo.AuthorID);
if (config.autoFollow && (!authorInfo?.following || !videoInfo.Following)) {
if (!await followUser(videoInfo.AuthorID).catch(() => false)) {
newToast(2, {
text: `${videoInfo.Alias} %#autoFollowFailed#%`,
close: true,
onClick() {
this.hide();
}
}).show();
}
}
if (config.autoLike && !videoInfo.Liked) {
if (!await likeVideo(videoInfo.ID).catch(() => false)) {
newToast(2, {
text: `${videoInfo.Alias} %#autoLikeFailed#%`,
close: true,
onClick() {
this.hide();
}
}).show();
}
}
if (getPageTypeProbe() !== "video" && config.checkDownloadLink && checkIsHaveDownloadLink(`${videoInfo.Description} ${videoInfo.Comments}`)) {
const toastBody = toastNode([`${videoInfo.Title}[${videoInfo.ID}] %#findedDownloadLink#%`], "%#createTask#%");
newToast(2, {
node: toastBody,
close: true,
buttons: [
{
text: "%#openVideoLink#%",
onClick: (t) => {
GM_openInTab(`https://www.${domain2}/video/${videoInfo.ID}`, { active: false, insert: true, setParent: true });
if (config.autoCopySaveFileName) {
GM_setClipboard(getDownloadPath(videoInfo).fullName, "text");
toastBody.appendChild(
renderNode({
nodeType: "p",
childs: "%#copySucceed#%"
})
);
} else {
t.hide();
}
}
}
]
}).show();
return;
}
if (config.checkPriority && videoInfo.DownloadQuality !== config.downloadPriority) {
newToast(2, {
node: toastNode([`${videoInfo.Title.truncate(64)}[${videoInfo.ID}] %#downloadQualityError#%`, { nodeType: "br" }, `%#tryReparseDownload#%`], "%#createTask#%"),
async onClick() {
this.hide();
await pushDownloadTask(await parseVideoInfo(videoInfo));
}
}).show();
return;
}
switch (config.downloadType) {
case 0:
aria2Download(videoInfo, true);
break;
case 1:
iwaradlDownload(videoInfo);
break;
case 2:
browserDownload(videoInfo);
break;
default:
othersDownload(videoInfo);
break;
}
if (config.autoDownloadMetadata) {
switch (config.downloadType) {
case 3:
othersDownloadMetadata(videoInfo);
break;
case 2:
browserDownloadMetadata(videoInfo);
break;
default:
break;
}
log14.debug("Download task pushed:", videoInfo);
}
selectList.delete(videoInfo.ID);
break;
default:
log14.debug("Unknown type:", videoInfo);
break;
}
}
registerBatchAction({
id: "downloadSelected",
labelKey: "downloadSelected",
run: () => analyzeDownloadTask()
});
async function fetchVideoPage(page) {
const response = await unlimitedFetch(
apiUrl("/videos", new URLSearchParams({ sort: "date", page: String(page), limit: String(site_default.pageLimit) })),
{ headers: await getAuth() },
{
retry: true,
maxRetries: 3,
retryDelay: 3e3,
failStatus: [403, 404, 429],
onRetry: async () => {
await refreshToken();
}
}
);
return await response.json();
}
async function fetchSubscribedVideos(page, retryDelay = 1e3) {
const response = await unlimitedFetch(
apiUrl("/videos", new URLSearchParams({ subscribed: "true", limit: String(site_default.pageLimit), rating: rating(), page: String(page) })),
{ headers: await getAuth() },
{
retry: true,
retryDelay,
onRetry: async () => {
await refreshToken();
}
}
);
return await response.json();
}
init_env();
init_env();
init_log();
init_db();
var log15 = createLogger("SyncPages");
var SYNC_PAGES_LOCK = "syncAllVideosPages";
var MONTH_MS = 30 * 24 * 60 * 60 * 1e3;
var PARSE_UNLISTED_LOCK = "parseUnlistedAndPrivate";
var parseUnlistedRunning = false;
async function fetchAndCachePage(page) {
let pageData;
try {
pageData = await fetchVideoPage(page);
} catch {
return false;
}
const rawVideos = pageData.results;
if (pageData.page * pageData.limit >= pageData.count) return "last";
if (isNullOrUndefined(rawVideos) || rawVideos.length === 0) return true;
const list = [];
let idx = 0;
const concurrency = 6;
const nextParse = async () => {
if (idx >= rawVideos.length) return;
const i = idx++;
try {
const info = await parseVideoInfo({ Type: "cache", ID: rawVideos[i].id, RAW: rawVideos[i] });
if (info.Type === "partial" || info.Type === "full") {
list.push(info);
}
} catch {
} finally {
if (idx < rawVideos.length) await delay(100);
await nextParse();
}
};
await Promise.allSettled(Array.from({ length: Math.min(concurrency, rawVideos.length) }, () => nextParse()));
if (list.length > 0) {
const ids = list.map((v2) => v2.ID);
const existing = await db.getVideosByIds(ids);
const fullVideos = existing.filter((v2) => v2.Type === "full");
const toUpdate = list.difference(fullVideos, "ID");
if (toUpdate.any()) {
await db.bulkPutVideos(toUpdate);
log15.info(`update: ${toUpdate.length} ${toUpdate[0].Title}`);
}
}
return true;
}
async function syncAllVideosPages() {
if (!await verifyLogin()) {
newToast(2, {
node: toastNode(`请先登录 iwara`, "页面遍历"),
duration: 3e3
}).show();
return;
}
return runBatchTask({
lockName: SYNC_PAGES_LOCK,
ttl: 6e4,
onConflict: "exit",
iterate: async (ctl) => {
ctl.progress(`正在遍历视频页面...`);
let succeeded = 0;
const failedPages = [];
let page = site_default.syncStartPage;
while (true) {
if (!ctl.isHeld()) {
taskConflict();
return;
}
try {
const result = await fetchAndCachePage(page);
if (result === "last") {
succeeded++;
ctl.progress(`正在遍历视频页面... 已是最后一页 (${page}),提前结束`);
break;
}
if (result === false) {
log15.warn(`页面 ${page} 失败`);
failedPages.push(page);
await delay(5e3 + Math.random() * 1e3);
page++;
continue;
}
succeeded++;
ctl.progress(`正在遍历视频页面... 第 ${page} 页 (失败: ${failedPages.length})`);
} catch (error) {
log15.warn(`页面 ${page} 异常:`, stringify(error));
failedPages.push(page);
}
await delay(500 + Math.random() * 1e3);
page++;
}
let retryFailed = [];
if (failedPages.length > 0) {
ctl.progress(`正在重试 ${failedPages.length} 个失败页面...`);
for (const retryPage of failedPages) {
if (!ctl.isHeld()) {
taskConflict();
return;
}
try {
const result = await fetchAndCachePage(retryPage);
if (result === false) {
log15.warn(`重试页面 ${retryPage} 仍失败`);
retryFailed.push(retryPage);
continue;
}
succeeded++;
ctl.progress(`正在重试失败页面... ${retryPage} 成功 (剩余 ${failedPages.length - retryFailed.length - (failedPages.indexOf(retryPage) + 1 - retryFailed.length)} 个待重试)`);
} catch (error) {
log15.warn(`重试页面 ${retryPage} 异常:`, stringify(error));
retryFailed.push(retryPage);
}
await delay(500 + Math.random() * 1e3);
}
}
ctl.progressEnd();
newToast(1, {
text: `页面遍历完成!成功: ${succeeded} 页${retryFailed.length > 0 ? `,重试后仍失败: ${retryFailed.length} 页` : ",无失败"}`,
close: true,
onClick() {
this.hide();
}
}).show();
if (retryFailed.length > 0) {
log15.warn(`始终失败的页码: ${retryFailed.join(", ")}`);
}
}
});
}
async function parseUnlistedAndPrivate() {
if (parseUnlistedRunning) return;
parseUnlistedRunning = true;
try {
await runBatchTask({
lockName: PARSE_UNLISTED_LOCK,
ttl: 12e4,
onConflict: "exit",
iterate: async (ctl) => {
if (!await verifyLogin()) return;
const lastMonthTimestamp = Date.now() - MONTH_MS;
const thisMonthUnlistedAndPrivateVideos = await db.getFilteredVideos(lastMonthTimestamp, Infinity);
const parseUnlistedAndPrivateVideos = [];
const MAX_FIND_PAGES = site_default.maxFindPages;
let pageCount = 0;
log15.debug(`Starting fetch loop. MAX_PAGES=${MAX_FIND_PAGES}`);
while (pageCount < MAX_FIND_PAGES) {
if (!ctl.isHeld()) return;
log15.debug(`Fetching page ${pageCount}.`);
const data = (await fetchSubscribedVideos(pageCount)).results;
log15.debug(`Page ${pageCount} returned ${data.length} videos.`);
data.forEach((info) => info.user.following = true);
const videoPromises = data.map(
(info) => parseVideoInfo({
Type: "cache",
ID: info.id,
RAW: info
})
);
log15.debug("Initializing VideoInfo promises.");
const videoInfos = await Promise.all(videoPromises);
parseUnlistedAndPrivateVideos.push(...videoInfos);
const foundPrivate = videoInfos.filter((i) => i.Type === "partial" && (i.Private || i.Unlisted)).any();
log15.debug("All VideoInfo objects initialized.");
if (foundPrivate && thisMonthUnlistedAndPrivateVideos.intersect(videoInfos, "ID").any()) {
log15.debug(`Found private video on page ${pageCount}.`);
break;
}
log15.debug(`Latest private video not found on page ${pageCount}, continuing.`);
pageCount++;
log15.debug(`Incremented page to ${pageCount}, delaying next fetch.`);
await delay(100);
}
log15.debug("Fetch loop ended. Start updating the database");
const existingVideos = await db.getVideosByIds(parseUnlistedAndPrivateVideos.map((v2) => v2.ID));
const toUpdate = parseUnlistedAndPrivateVideos.difference(
existingVideos.filter((v2) => v2.Type === "full"),
"ID"
);
if (toUpdate.any()) {
log15.debug(`Need to update ${toUpdate.length} pieces of data.`);
await db.bulkPutVideos(toUpdate);
log15.debug(`Update Completed.`);
} else {
log15.debug(`No need to update data.`);
}
}
});
} finally {
parseUnlistedRunning = false;
}
}
init_env();
init_env();
async function importConfig() {
if (unsafeWindow.document.querySelector("#pluginOverlay")) return;
let textArea = renderNode({
nodeType: "textarea",
attributes: {
placeholder: i18nList[config.language].importConfig,
style: "margin-bottom: 10px;",
rows: "16",
cols: "96"
}
});
let body = renderNode({
nodeType: "div",
attributes: {
id: "pluginOverlay"
},
childs: [
textArea,
{
nodeType: "button",
events: {
click: (e) => {
if (!isNullOrUndefined(textArea.value) && !textArea.value.isEmpty()) {
try {
let tempConfig = JSON.parse(textArea.value);
if (!tempConfig || typeof tempConfig !== "object") {
throw "配置校验失败";
}
Config.initInstance(tempConfig);
unsafeWindow.location.reload();
} catch (error) {
newToast(3, {
node: renderNode({
nodeType: "p",
childs: ["%#importConfigFail#%", stringify(error)]
})
}).show();
}
}
body.remove();
}
},
childs: i18nList[config.language].ok
}
]
});
unsafeWindow.document.body.appendChild(body);
}
init_env();
init_log();
var log16 = createLogger("Users");
var localUserPromise = null;
function getLocalUser(options = {}) {
if (options.refresh || localUserPromise === null) {
localUserPromise = (async () => {
try {
if (options.refresh ? !await verifyLogin() : !isLoggedIn()) return null;
const res = await unlimitedFetch(apiUrl("/user"), {
method: "GET",
headers: await getAuth()
});
if (!res.ok) return null;
return (await res.json()).user ?? null;
} catch (error) {
log16.warn("Failed to get local user:", error);
return null;
}
})();
}
return localUserPromise;
}
async function isLocalUser(ownerId) {
const localUser = await getLocalUser();
return !isNullOrUndefined(localUser) && ownerId !== "" && localUser.id === ownerId;
}
init_log();
var log17 = createLogger("Playlists");
var playlistOwnerPromise = null;
function getPlaylistOwnerId(playlistId) {
if (playlistOwnerPromise?.playlistId !== playlistId) {
playlistOwnerPromise = {
playlistId,
promise: (async () => {
try {
const res = await unlimitedFetch(apiUrl(`/playlist/${playlistId}`), {
method: "GET",
headers: await getAuth()
});
if (!res.ok) return "";
return (await res.json()).playlist?.user?.id ?? "";
} catch (error) {
log17.warn("Failed to get playlist owner:", error);
return "";
}
})()
};
}
return playlistOwnerPromise.promise;
}
async function removePlaylistVideo(playlistId, videoId) {
try {
return (await unlimitedFetch(apiUrl(`/playlist/${playlistId}/${videoId}`), {
method: "DELETE",
headers: await getAuth()
})).ok;
} catch (error) {
log17.warn(`Failed to remove video ${videoId} from playlist ${playlistId}:`, error);
return false;
}
}
init_env();
function installCheckboxCallbacks() {
const callbacks2 = {
canDeletePlaylistItem: async (playlistId) => isLocalUser(await getPlaylistOwnerId(playlistId)),
deletePlaylistItem: (playlistId, videoId) => removePlaylistVideo(playlistId, videoId)
};
setCheckboxCallbacks(callbacks2);
}
function setupMenuActions(instance) {
instance.setup(
{
manualDownload: () => addDownloadTask(),
exportConfig: () => {
GM_setClipboard(stringify(config));
newToast(1, {
node: toastNode(i18nList[config.language].exportConfigSucceed),
duration: 3e3,
gravity: "bottom",
position: "center",
onClick() {
this.hide();
}
}).show();
},
importConfig: () => importConfig(),
downloadThis: () => {
const ID = currentVideoId();
if (!isNullOrUndefined(ID)) void pushDownloadTask({ Type: "init", ID });
},
parseUnlistedAndPrivate: () => {
void parseUnlistedAndPrivate();
}
},
{
toggleInjectCheckbox: () => {
if (unsafeWindow.document.querySelector(".selectButton")) {
unsafeWindow.document.querySelectorAll(".selectButton").forEach((element) => {
element.remove();
});
} else {
unsafeWindow.document.querySelectorAll(".videoTeaser").forEach((element) => {
injectCheckbox(element);
});
}
},
selectionButtons: [
{ id: "deselectAll", click: () => deselectAll() },
{ id: "reverseSelect", click: () => toggleSelect() },
{ id: "selectThis", click: () => selectAll(true) },
{ id: "deselectThis", click: () => selectAll(false) }
],
actionButtons: getBatchActions().map((action) => ({
id: action.id,
labelKey: action.labelKey ?? action.id,
click: () => {
void action.run();
}
}))
}
);
}
init_env();
function isMediaCenterConfigured() {
return !config.mediaCenterApi.isEmpty() && !config.mediaCenterApiKey.isEmpty();
}
function mediaCenterApiBase() {
return config.mediaCenterApi.replace(/\/+$/, "");
}
function mediaCenterAuthHeaders() {
return {
accept: "application/json",
"content-type": "application/json",
authorization: `Bearer ${config.mediaCenterApiKey}`
};
}
async function fetchMediaList() {
const response = await unlimitedFetch(`${mediaCenterApiBase()}/api/media?limit=0&sortBy=createdAt&sortOrder=desc`, {
headers: mediaCenterAuthHeaders()
});
if (!response.ok) {
throw new Error(`拉取 MediaCenter 列表失败: ${response.status} ${await response.text()}`);
}
const result = await response.json();
return isNullOrUndefined(result.items) ? [] : result.items;
}
async function mediaCenterRecordExists(mediaCenterId) {
const checkRes = await unlimitedFetch(`${mediaCenterApiBase()}/api/media/${mediaCenterId}`, {
method: "GET",
headers: mediaCenterAuthHeaders()
});
return checkRes.ok;
}
async function createMedia(body) {
const createRes = await unlimitedFetch(`${mediaCenterApiBase()}/api/media`, {
method: "POST",
headers: mediaCenterAuthHeaders(),
body: JSON.stringify(body)
});
if (createRes.status === 409) {
const conflict = await createRes.json();
return { ok: false, conflict: { existingId: conflict.existingId }, error: conflict.error };
}
if (!createRes.ok) {
return { ok: false, status: createRes.status, error: await createRes.text() };
}
const createResult = await createRes.json();
if (createResult.error) return { ok: false, error: createResult.error };
if (!createResult.id || createResult.id.isEmpty()) return { ok: false, error: "no id returned" };
return { ok: true, id: createResult.id };
}
async function updateMedia(mediaCenterId, body) {
const updateRes = await unlimitedFetch(`${mediaCenterApiBase()}/api/media/${mediaCenterId}`, {
method: "PUT",
headers: mediaCenterAuthHeaders(),
body: JSON.stringify(body)
});
if (!updateRes.ok) {
return { ok: false, status: updateRes.status, error: await updateRes.text() };
}
const updateResult = await updateRes.json();
if (updateResult.error) return { ok: false, error: updateResult.error };
if (!updateResult.media) return { ok: false, error: "no media returned" };
return { ok: true };
}
init_env();
init_env();
init_log();
init_db();
var log18 = createLogger("MediaCenter");
var mediaLog = createLogger("MediaCenter");
var mediaCenterIdMapValidateCache = new Map();
var MEDIA_CENTER_ID_MAP_VALIDATE_TTL = 6 * 60 * 60 * 1e3;
async function verifyMediaCenterMapping(videoId, mediaCenterId) {
if (!isMediaCenterConfigured()) return mediaCenterId;
const cacheKey = `${config.mediaCenterApi}|${mediaCenterId}`;
const cached = mediaCenterIdMapValidateCache.get(cacheKey);
if (cached && Date.now() - cached.checkedAt < MEDIA_CENTER_ID_MAP_VALIDATE_TTL) {
return cached.valid ? mediaCenterId : void 0;
}
try {
const exists = await mediaCenterRecordExists(mediaCenterId);
if (exists) {
mediaCenterIdMapValidateCache.set(cacheKey, { valid: true, checkedAt: Date.now() });
return mediaCenterId;
}
mediaLog.warn(`映射失效: ${videoId} → ${mediaCenterId} 在服务端不存在,删除本地映射`);
await db.deleteMediaCenterIdMap(videoId);
mediaCenterIdMapValidateCache.delete(cacheKey);
return void 0;
} catch (e) {
mediaLog.warn(`验证映射 ${videoId} → ${mediaCenterId} 异常,保守视为有效: ${stringify(e)}`);
return mediaCenterId;
}
}
async function pushToMediaCenter(videoInfo, mediaCenterId = void 0) {
if (!isMediaCenterConfigured()) return false;
const downloadPath = getDownloadPath(videoInfo);
try {
if (!isNullOrUndefined(mediaCenterId) && !mediaCenterId.isEmpty()) {
mediaCenterId = await verifyMediaCenterMapping(videoInfo.ID, mediaCenterId);
}
if (isNullOrUndefined(mediaCenterId) || mediaCenterId.isEmpty()) {
const created = await createMedia(prune({
filePath: downloadPath.fullPath,
fileHash: videoInfo.ID
}));
if (!created.ok) {
if (created.conflict?.existingId && !created.conflict.existingId.isEmpty()) {
mediaCenterId = created.conflict.existingId;
await db.putMediaCenterIdMap(videoInfo.ID, mediaCenterId);
mediaLog.debug(`createMedia 409, reuse existing ${videoInfo.ID} → ${mediaCenterId}`);
} else {
mediaLog.warn(`createMedia failed for ${videoInfo.ID}:`, created.error ?? created.status);
return false;
}
} else {
mediaCenterId = created.id;
await db.putMediaCenterIdMap(videoInfo.ID, mediaCenterId);
}
}
const updateBody = prune({
filePath: downloadPath.fullPath,
fileHash: videoInfo.ID,
title: videoInfo.Title,
description: videoInfo.Description ?? "",
source: "iwara",
author: videoInfo.Author,
altNames: videoInfo.Alias.isEmpty() ? void 0 : [videoInfo.Alias],
tags: (videoInfo.Tags ?? []).map((t) => t.id),
duration: videoInfo.RAW?.file?.duration,
sourceMeta: videoInfo.RAW ? JSON.stringify(videoInfo.RAW) : void 0,
createdAt: new Date(videoInfo.UploadTime).toISOString()
});
const updated = await updateMedia(mediaCenterId, updateBody);
if (!updated.ok) {
if (updated.status === 404) {
mediaLog.warn(`updateMedia 404 for ${videoInfo.ID}: 服务端记录不存在,删除本地映射 ${mediaCenterId}`);
await db.deleteMediaCenterIdMap(videoInfo.ID);
} else {
mediaLog.warn(`updateMedia failed for ${videoInfo.ID}: ${updated.status ?? ""} ${updated.error ?? ""}`);
}
return false;
}
mediaLog.debug("metadata pushed:", videoInfo.ID, "→", mediaCenterId);
return true;
} catch (error) {
mediaLog.warn(`Push metadata error for ${videoInfo.ID}:`, stringify(error));
}
return false;
}
async function syncCachedToMediaCenter() {
if (!isMediaCenterConfigured()) {
newToast(2, {
node: toastNode(`请先配置 MediaCenter API 地址和密钥`, "MediaCenter 同步"),
duration: 3e3
}).show();
return;
}
const apiBase = mediaCenterApiBase();
const authHeaders = mediaCenterAuthHeaders();
const total = await db.countVideos();
if (total === 0) {
newToast(1, {
text: `没有找到缓存的视频数据`,
duration: 3e3
}).show();
return;
}
const phaseStartTime = Date.now();
let stepStartTime = phaseStartTime;
const matchedMap = await db.getAllMediaCenterIdMaps();
log18.debug(`步骤1/4 加载本地映射缓存: ${((Date.now() - stepStartTime) / 1e3).toFixed(1)}s`);
stepStartTime = Date.now();
const listProgress = createProgressTracker(`正在从 MediaCenter 拉取视频列表...`);
listProgress.start();
try {
const items = await fetchMediaList();
log18.debug(`步骤2/4 拉取 MediaCenter 列表: ${((Date.now() - stepStartTime) / 1e3).toFixed(1)}s(${items.length} 条)`);
stepStartTime = Date.now();
listProgress.update(`MediaCenter 列表拉取完成,共 ${items.length} 条记录,正在遍历本地数据库建立映射...`);
const entriesToSave = [];
let processedCount = 0;
const hashToId = new Map();
const tokenToId = new Map();
const titleContains = [];
for (const m2 of items) {
if (!isNullOrUndefined(m2.fileHash) && !m2.fileHash.isEmpty()) {
hashToId.set(m2.fileHash, m2.id);
}
if (!isNullOrUndefined(m2.title) && !m2.title.isEmpty()) {
const lowerTitle = m2.title.toLowerCase();
titleContains.push({ id: m2.id, lowerTitle });
for (const token of lowerTitle.split(/[^a-z0-9]+/).filter((t) => t.length >= 3)) {
if (!tokenToId.has(token)) tokenToId.set(token, m2.id);
}
}
}
const keyIterator = db.iterateVideoKeysBatched(10240);
let idx2 = 0;
let currentBatch = [];
let keyBatchIter = keyIterator[Symbol.asyncIterator]();
let prefetchDone = false;
let prefetchPromise = null;
const batchQueue = [];
const prefetchNextBatch = async () => {
const t0 = Date.now();
const { value, done } = await keyBatchIter.next();
log18.debug(`批次获取: ${((Date.now() - t0) / 1e3).toFixed(1)}s(${value?.length ?? 0} 条)`);
if (done) {
prefetchDone = true;
} else {
batchQueue.push(value);
}
};
const ensureBatch = async () => {
if (batchQueue.length > 0) return true;
if (prefetchDone) return false;
if (!prefetchPromise) {
prefetchPromise = prefetchNextBatch().finally(() => {
prefetchPromise = null;
});
}
await prefetchPromise;
return batchQueue.length > 0;
};
prefetchPromise = prefetchNextBatch().finally(() => {
prefetchPromise = null;
});
const nextVideo = async () => {
if (idx2 >= currentBatch.length) {
if (!await ensureBatch()) return;
currentBatch = batchQueue.shift();
idx2 = 0;
if (batchQueue.length < 2 && !prefetchDone && !prefetchPromise) {
prefetchPromise = prefetchNextBatch().finally(() => {
prefetchPromise = null;
});
}
}
const videoId = currentBatch[idx2++];
processedCount++;
if (Date.now() - stepStartTime >= 500 || processedCount === 1 || processedCount === total) {
listProgress.update(`正在匹配映射... [${processedCount}/${total}] 已映射: ${matchedMap.size}`);
}
if (matchedMap.has(videoId)) return nextVideo();
if (hashToId.has(videoId)) {
const mcId = hashToId.get(videoId);
matchedMap.set(videoId, mcId);
entriesToSave.push({ videoId, mediaCenterId: mcId });
return nextVideo();
}
const videoIdLower = videoId.toLowerCase();
if (tokenToId.has(videoIdLower)) {
const mcId = tokenToId.get(videoIdLower);
matchedMap.set(videoId, mcId);
entriesToSave.push({ videoId, mediaCenterId: mcId });
return nextVideo();
}
for (const tc of titleContains) {
if (tc.lowerTitle.indexOf(videoIdLower) !== -1) {
matchedMap.set(videoId, tc.id);
entriesToSave.push({ videoId, mediaCenterId: tc.id });
break;
}
}
return nextVideo();
};
await Promise.allSettled(Array.from({ length: Math.min(64, total) }, () => nextVideo()));
log18.debug(`步骤3/4 遍历数据库匹配映射: ${((Date.now() - stepStartTime) / 1e3).toFixed(1)}s(${processedCount} 条,映射 ${entriesToSave.length} 条)`);
stepStartTime = Date.now();
if (entriesToSave.length > 0) {
await db.bulkPutMediaCenterIdMaps(entriesToSave);
}
log18.debug(`步骤4/4 保存映射到本地: ${((Date.now() - stepStartTime) / 1e3).toFixed(1)}s`);
const totalTime = (Date.now() - phaseStartTime) / 1e3;
listProgress.update(`映射建立完成,共 ${matchedMap.size} 条映射(新增 ${entriesToSave.length} 条),总耗时 ${totalTime.toFixed(1)}s`);
} catch (error) {
log18.error("拉取列表失败:", stringify(error));
listProgress.close();
newToast(3, {
node: toastNode([`MediaCenter 列表拉取失败,请检查 API 地址和密钥`, { nodeType: "br" }, stringify(error)], "MediaCenter 同步"),
duration: 1e4,
close: true,
onClick() {
this.hide();
}
}).show();
return;
}
listProgress.close();
if (matchedMap.size === 0) {
newToast(1, {
text: `MediaCenter 中未找到包含 fileHash 的视频记录,无法同步`,
duration: 3e3
}).show();
return;
}
let updated = 0;
let skipped = 0;
let updateErrors = 0;
const updateProgress = createProgressTracker(`MediaCenter 更新中... [0/${matchedMap.size}]`);
updateProgress.start();
const matchedIds = [...matchedMap.keys()];
const concurrency = 6;
let idx = 0;
const nextUpdate = async () => {
if (idx >= matchedIds.length) return;
const i = idx++;
const videoId = matchedIds[i];
const mediaCenterId = matchedMap.get(videoId);
try {
let video = await db.getVideoById(videoId) ?? { Type: "init", ID: videoId };
if (video.Type !== "full") {
const pvideo = await parseVideoInfo(video);
video = getMoreCompleteVideoInfo(video, pvideo);
}
if (video.Type === "cache" || video.Type === "init") {
skipped++;
return;
}
db.putVideo(video);
if (await pushToMediaCenter(video, mediaCenterId)) {
updated++;
} else {
updateErrors++;
}
} catch (error) {
log18.warn(`同步异常 ${videoId}:`, stringify(error));
updateErrors++;
} finally {
updateProgress.update(`MediaCenter 更新中... [${updated + skipped + updateErrors}/${matchedMap.size}] 更新: ${updated} 跳过: ${skipped} 错误: ${updateErrors}`);
if (idx < matchedIds.length) await delay(100);
await nextUpdate();
}
};
await Promise.allSettled(Array.from({ length: Math.min(concurrency, matchedIds.length) }, () => nextUpdate()));
updateProgress.close();
newToast(1, {
text: `MediaCenter 同步完成!已更新: ${updated}, 跳过: ${skipped}, 错误: ${updateErrors}`,
duration: 5e3,
close: true,
onClick() {
this.hide();
}
}).show();
}
init_env();
init_db();
init_hijack();
init_log();
init_constants();
var log19 = createLogger("Aria2Track");
var mediaCenterPushHook;
function setMediaCenterPushHook(hook) {
mediaCenterPushHook = hook;
}
var ARIA2_TRACK_QUEUE_KEY = "Aria2TrackQueue";
var ARIA2_TRACK_DONE_KEY = "Aria2TrackDone";
var ARIA2_TRACK_MANAGER_LOCK = "aria2TrackManager";
var ARIA2_TRACK_ELECTION_INTERVAL = 4e3;
var ARIA2_TRACK_SCAN_INTERVAL = 6e4;
var ARIA2_TRACK_POLL_INTERVAL_ACTIVE = 1e3 * 4;
var ARIA2_TRACK_POLL_INTERVAL_IDLE = 1e3 * 8;
var ARIA2_TRACK_POLL_INTERVAL_BACKOFF = 1e3 * 32;
function aria2TrackPollInterval(status) {
switch (status) {
case "active":
return ARIA2_TRACK_POLL_INTERVAL_ACTIVE;
case "waiting":
case "paused":
return ARIA2_TRACK_POLL_INTERVAL_IDLE;
default:
return ARIA2_TRACK_POLL_INTERVAL_BACKOFF;
}
}
function aria2TrackGidPriority(status) {
switch (status) {
case "active":
return 3;
case "waiting":
return 2;
case "paused":
case "complete":
return 1;
default:
return 0;
}
}
var ARIA2_SLOW_SPEED_THRESHOLD = 16 * 1024;
var ARIA2_TRACK_SLOW_START_EXEMPT_POLLS = 16;
var ARIA2_TRACK_MAX_REBUILD_ATTEMPTS = 8;
var ARIA2_TRACK_PUSH_RETRY_INTERVAL = 5 * 60 * 1e3;
var ARIA2_TRACK_DONE_RETENTION = 30 * 24 * 60 * 60 * 1e3;
var mediaCenterIdMapValidateCache2 = new Map();
var MEDIA_CENTER_ID_MAP_VALIDATE_TTL2 = 6 * 60 * 60 * 1e3;
var aria2TrackOwner = UUID();
var aria2TrackLock = new GMLock(aria2TrackOwner);
var aria2TrackManagerLoopRunning = false;
var aria2TrackWorkers = new Set();
var aria2TrackManagerEpoch = 0;
var aria2TrackQueue = new GMSyncDictionary(ARIA2_TRACK_QUEUE_KEY, [], (value) => isString(value?.videoId) && isString(value?.gid));
var aria2TrackDone = new GMSyncDictionary(ARIA2_TRACK_DONE_KEY, [], (value) => isConvertibleToNumber(value));
aria2TrackQueue.onSet = () => {
if (aria2TrackLock.isHeld(ARIA2_TRACK_MANAGER_LOCK)) syncAria2TrackWorkers(aria2TrackManagerEpoch);
};
aria2TrackQueue.onDel = () => {
if (aria2TrackLock.isHeld(ARIA2_TRACK_MANAGER_LOCK)) syncAria2TrackWorkers(aria2TrackManagerEpoch);
};
aria2TrackQueue.onSync = () => {
if (aria2TrackLock.isHeld(ARIA2_TRACK_MANAGER_LOCK)) syncAria2TrackWorkers(aria2TrackManagerEpoch);
};
function buildDownloadUrl2(videoInfo) {
const localPath = getDownloadPath(videoInfo);
const url = videoInfo.DownloadUrl.toURL();
url.searchParams.set("videoid", videoInfo.ID);
url.searchParams.set("download", localPath.fullName);
return url;
}
function enqueueAria2TrackTask(videoId, gid, downloadParams, alreadyComplete = false) {
aria2TrackQueue.set(videoId, { videoId, gid, downloadParams, addedAt: Date.now(), alreadyComplete });
}
function hasAria2TrackTask(videoId) {
return aria2TrackQueue.has(videoId);
}
function hasAria2TrackDone(videoId) {
return !isNullOrUndefined(aria2TrackDone.get(videoId));
}
function finishAria2TrackTask(videoId, doneAt) {
aria2TrackDone.set(videoId, doneAt);
aria2TrackQueue.delete(videoId);
}
function removeAria2TrackTask(videoId) {
aria2TrackQueue.delete(videoId);
}
function updateAria2TrackTaskGid(videoId, gid) {
const task = aria2TrackQueue.get(videoId);
if (task) aria2TrackQueue.set(videoId, { ...task, gid });
}
function updateAria2TrackTask(videoId, patch) {
const task = aria2TrackQueue.get(videoId);
if (task) aria2TrackQueue.set(videoId, { ...task, ...patch });
}
function isMediaCenterPushEnabled() {
return config.experimentalFeatures && !config.mediaCenterApi.isEmpty() && !config.mediaCenterApiKey.isEmpty();
}
function getAria2Progress(status) {
if (!isConvertibleToNumber(status.completedLength) || !isConvertibleToNumber(status.totalLength)) return 0;
const total = Number(status.totalLength);
if (total <= 0) return 0;
return Math.min(1, Number(status.completedLength) / total);
}
function trackStatusToast(videoId, title, type, i18nKey) {
newToast(type, {
id: `aria2Track-${videoId}`,
duration: 5e3,
node: toastNode(`${title}[${videoId}] %#${i18nKey}#%`)
}).show();
}
function formatAria2Speed(speed) {
if (!isConvertibleToNumber(speed)) return "未知";
const n = Number(speed);
if (n < 1024) return `${n} B/s`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB/s`;
return `${(n / 1024 / 1024).toFixed(2)} MiB/s`;
}
function askAria2RebuildContinue(videoId, title) {
return new Promise((resolve) => {
let settled = false;
const settle = (value) => {
if (settled) return;
settled = true;
resolve(value);
};
const toast = newToast(3, {
id: `aria2Track-retry-${videoId}`,
duration: -1,
close: true,
node: toastNode("%#aria2TrackRebuildGiveUp#%", title),
buttons: [
{
text: "%#aria2TrackRebuildRetry#%",
className: "aria2TrackRebuildRetry",
onClick: (t) => {
settle(true);
t.hide("other");
}
},
{
text: "%#aria2TrackRebuildCancel#%",
className: "aria2TrackRebuildCancel",
onClick: (t) => {
settle(false);
t.hide("close-button");
}
}
]
});
toast.show();
setTimeout(() => {
settle(true);
toast.hide("other");
}, 5 * 60 * 1e3);
});
}
async function processAria2TrackTask(task, epoch) {
let info = await db.getVideoById(task.videoId);
if (info?.Type !== "full") {
try {
const parsed = await parseVideoInfo({ Type: "init", ID: task.videoId, RAW: info?.RAW });
info = getMoreCompleteVideoInfo(info ?? { Type: "init", ID: task.videoId }, parsed);
} catch (e) {
log19.warn(`${task.videoId} 初始化解析异常,降级继续处理: ${stringify(e)}`);
}
}
let videoInfo = info && info.Type === "full" ? info : void 0;
let currentGid = task.gid;
let consecutiveFailures = 0;
let rebuildFailures = 0;
let lastStatus;
let activePollCount = 0;
log19.debug(`${task.videoId} 开始处理 (gid=${task.gid}, title=${videoInfo?.Title ?? task.videoId})`);
while (true) {
if (!aria2TrackLock.isHeld(ARIA2_TRACK_MANAGER_LOCK)) return;
if (!hasAria2TrackTask(task.videoId)) return;
if (epoch !== aria2TrackManagerEpoch) return;
const latestTask = aria2TrackQueue.get(task.videoId);
if (latestTask && latestTask.gid !== currentGid) {
log19.debug(`${task.videoId} 队列 gid 更新 ${currentGid} → ${latestTask.gid},跟随`);
currentGid = latestTask.gid;
}
try {
const statusRes = await aria2API("aria2.tellStatus", [currentGid, ["gid", "status", "files", "downloadSpeed", "errorCode", "completedLength", "totalLength"]]);
const status = statusRes.result;
if (!status?.status) {
lastStatus = void 0;
consecutiveFailures++;
log19.debug(`${task.videoId} (gid=${currentGid}) 无状态响应(第${consecutiveFailures}次),退避轮询`);
if (consecutiveFailures > 5) {
log19.warn(`${task.videoId} (gid=${currentGid}) gid 在 aria2 中不可达/不存在,移出队列(周期扫描会以最新 gid 重新纳入)`);
removeAria2TrackTask(task.videoId);
return;
}
await delay(aria2TrackPollInterval(void 0));
continue;
}
consecutiveFailures = 0;
const progress2 = getAria2Progress(status);
if (lastStatus !== status.status) {
log19.debug(`${task.videoId} 状态: ${lastStatus ?? "初始"} → ${status.status} (gid=${currentGid}, 进度=${(progress2 * 100).toFixed(1)}%, 速度=${formatAria2Speed(status.downloadSpeed)})`);
}
lastStatus = status.status;
if (status.status !== "active") activePollCount = 0;
switch (status.status) {
case "complete": {
const now = Date.now();
const queued = aria2TrackQueue.get(task.videoId);
if (queued?.pushFailedAt) {
if (now - queued.pushFailedAt < ARIA2_TRACK_PUSH_RETRY_INTERVAL) {
log19.debug(`${task.videoId} 推送失败退避中,等待重试`);
break;
}
} else {
log19.debug(`${task.videoId} 下载完成`);
if (!queued?.alreadyComplete) {
trackStatusToast(task.videoId, videoInfo?.Title ?? task.videoId, 1, "aria2TrackComplete");
}
}
if (isMediaCenterPushEnabled() && videoInfo) {
const pushed = await mediaCenterPushHook?.(videoInfo).then(() => true).catch(() => false) ?? false;
if (pushed) {
finishAria2TrackTask(task.videoId, now);
return;
}
log19.warn(`${task.videoId} MediaCenter 推送失败,${Math.round(ARIA2_TRACK_PUSH_RETRY_INTERVAL / 1e3)}s 后重试`);
updateAria2TrackTask(task.videoId, { pushFailedAt: now });
break;
}
finishAria2TrackTask(task.videoId, now);
return;
}
case "error":
case "removed":
log19.warn(`${task.videoId} 任务 ${status.status},重新创建下载`);
if (epoch !== aria2TrackManagerEpoch) return;
const attempt = await nextRebuildAttempt(task.videoId, videoInfo?.Title ?? task.videoId, rebuildFailures);
if (!attempt.ok) {
removeAria2TrackTask(task.videoId);
return;
}
rebuildFailures = attempt.failures;
const rebuilt = await rebuildAria2Task(task.videoId, task, status);
if (rebuilt.gid) {
currentGid = rebuilt.gid;
updateAria2TrackTaskGid(task.videoId, currentGid);
if (rebuilt.videoInfo) videoInfo = rebuilt.videoInfo;
activePollCount = 0;
rebuildFailures = 0;
trackStatusToast(task.videoId, videoInfo?.Title ?? task.videoId, 2, "aria2TrackRestart");
}
break;
case "active":
activePollCount++;
if (isConvertibleToNumber(status.downloadSpeed) && Number(status.downloadSpeed) <= ARIA2_SLOW_SPEED_THRESHOLD) {
if (activePollCount > ARIA2_TRACK_SLOW_START_EXEMPT_POLLS) {
const speed = Number(status.downloadSpeed);
if (speed === 0 || getAria2Progress(status) <= 0.98) {
log19.debug(`${task.videoId} 速度过慢,重启`);
if (epoch !== aria2TrackManagerEpoch) return;
const freshActive = await restartAria2Task(currentGid, task.videoId, status);
if (freshActive) {
videoInfo = freshActive;
activePollCount = 0;
trackStatusToast(task.videoId, videoInfo?.Title ?? task.videoId, 2, "aria2TrackRestartSlow");
} else {
log19.warn(`${task.videoId} 重启失败,降级为无条件重建`);
if (epoch !== aria2TrackManagerEpoch) return;
const attempt2 = await nextRebuildAttempt(task.videoId, videoInfo?.Title ?? task.videoId, rebuildFailures);
if (!attempt2.ok) {
removeAria2TrackTask(task.videoId);
return;
}
rebuildFailures = attempt2.failures;
const rebuilt2 = await rebuildAria2Task(task.videoId, task, status);
if (rebuilt2.gid) {
currentGid = rebuilt2.gid;
updateAria2TrackTaskGid(task.videoId, currentGid);
if (rebuilt2.videoInfo) videoInfo = rebuilt2.videoInfo;
activePollCount = 0;
rebuildFailures = 0;
trackStatusToast(task.videoId, videoInfo?.Title ?? task.videoId, 2, "aria2TrackRestart");
}
}
}
} else {
log19.debug(`${task.videoId} 速度过慢(${formatAria2Speed(status.downloadSpeed)}),慢启动豁免期 (${activePollCount}/${ARIA2_TRACK_SLOW_START_EXEMPT_POLLS}),暂不重启`);
}
}
break;
case "waiting":
break;
case "paused":
if (await isAria2QueueFull()) {
log19.debug(`${task.videoId} 队列已满,暂不重启`);
} else {
log19.debug(`${task.videoId} 已暂停,重启`);
if (epoch !== aria2TrackManagerEpoch) return;
const freshPaused = await restartAria2Task(currentGid, task.videoId, status);
if (freshPaused) {
videoInfo = freshPaused;
activePollCount = 0;
trackStatusToast(task.videoId, videoInfo?.Title ?? task.videoId, 2, "aria2TrackRestartPaused");
} else {
log19.warn(`${task.videoId} 重启失败,降级为无条件重建`);
if (epoch !== aria2TrackManagerEpoch) return;
const attempt2 = await nextRebuildAttempt(task.videoId, videoInfo?.Title ?? task.videoId, rebuildFailures);
if (!attempt2.ok) {
removeAria2TrackTask(task.videoId);
return;
}
rebuildFailures = attempt2.failures;
const rebuilt2 = await rebuildAria2Task(task.videoId, task, status);
if (rebuilt2.gid) {
currentGid = rebuilt2.gid;
updateAria2TrackTaskGid(task.videoId, currentGid);
if (rebuilt2.videoInfo) videoInfo = rebuilt2.videoInfo;
activePollCount = 0;
rebuildFailures = 0;
trackStatusToast(task.videoId, videoInfo?.Title ?? task.videoId, 2, "aria2TrackRestart");
}
}
}
break;
}
} catch (error) {
log19.warn(`追踪异常 ${currentGid}:`, stringify(error));
consecutiveFailures++;
if (consecutiveFailures > 5) {
log19.warn(`任务 ${currentGid} 持续异常,从队列移除`);
removeAria2TrackTask(task.videoId);
return;
}
}
await delay(aria2TrackPollInterval(lastStatus));
}
}
function syncAria2TrackWorkers(epoch) {
for (const task of aria2TrackQueue.valuesArray()) {
if (aria2TrackWorkers.has(task.videoId)) continue;
aria2TrackWorkers.add(task.videoId);
log19.debug(`管理器开始处理 ${task.videoId} (gid=${task.gid})`);
processAria2TrackTask(task, epoch).finally(() => {
aria2TrackWorkers.delete(task.videoId);
});
}
}
async function tryBecomeAria2TrackManager() {
if (aria2TrackManagerLoopRunning) return;
aria2TrackManagerLoopRunning = true;
try {
await aria2TrackLock.acquireReady(ARIA2_TRACK_MANAGER_LOCK);
if (!aria2TrackLock.acquireWithHeartbeat(ARIA2_TRACK_MANAGER_LOCK, 75e3)) return;
log19.debug("本页成为管理器,接管整个队列");
await startAria2TrackManagerLoop();
} finally {
aria2TrackManagerLoopRunning = false;
}
}
async function startAria2TrackManagerLoop() {
const epoch = ++aria2TrackManagerEpoch;
aria2TrackWorkers.clear();
syncAria2TrackWorkers(epoch);
while (aria2TrackLock.isHeld(ARIA2_TRACK_MANAGER_LOCK)) {
await delay(75e3 / 3);
syncAria2TrackWorkers(epoch);
}
log19.debug("管理器锁过期/被抢占,停止处理队列");
}
function aria2TrackUriExpiry(uri) {
try {
const url = uri.toURL();
if (isNullOrUndefined(url)) return -Infinity;
const expires = url.searchParams.get("expires");
if (isNullOrUndefined(expires)) return -Infinity;
const exp = Number(expires);
return Number.isFinite(exp) ? exp : -Infinity;
} catch {
return -Infinity;
}
}
function aria2TrackTaskExpiry(task) {
let best = -Infinity;
for (const file of task.files ?? []) {
for (const uri of file.uris ?? []) {
const exp = aria2TrackUriExpiry(uri.uri);
if (exp > best) best = exp;
}
}
return best;
}
function formatAria2TrackExpiry(exp) {
if (!Number.isFinite(exp)) return "未知";
const remain = Math.max(0, Math.floor((exp * 1e3 - Date.now()) / 6e4));
if (remain < 60) return `${remain} 分钟`;
if (remain < 60 * 24) return `${Math.floor(remain / 60)} 小时 ${remain % 60} 分钟`;
return `${Math.floor(remain / 60 / 24)} 天 ${Math.floor(remain / 60 % 24)} 小时`;
}
async function removeRedundantAria2Task(task) {
if (task.status === "active" || task.status === "waiting" || task.status === "paused") {
try {
await aria2API("aria2.remove", [task.gid]);
} catch (e) {
log19.debug(`停止冗余任务 ${task.gid} 失败(可能已被移除),继续清理结果: ${stringify(e)}`);
}
}
try {
await aria2API("aria2.removeDownloadResult", [task.gid]);
} catch (e) {
log19.debug(`清理冗余任务结果 ${task.gid} 失败: ${stringify(e)}`);
}
}
async function scanAria2TasksAndEnqueue() {
try {
const [actRes, stopRes] = await Promise.all([aria2API("aria2.tellActive", [["gid", "status", "files", "bittorrent"]]), aria2API("aria2.tellStopped", [0, 4096, ["gid", "status", "files", "bittorrent"]])]);
const activeRes = actRes;
const stoppedRes = stopRes;
const allTasks = [...activeRes.result ?? [], ...stoppedRes.result ?? []].filter((t) => isNullOrUndefined(t.bittorrent));
const taskGroups = new Map();
for (const task of allTasks) {
const videoId = aria2TaskExtractVideoID(task);
if (isNullOrUndefined(videoId) || videoId.isEmpty()) {
continue;
}
let group = taskGroups.get(videoId);
if (!group) {
group = [];
taskGroups.set(videoId, group);
}
group.push(task);
}
let enqueued = 0;
let stopped = 0;
const now = Date.now();
for (const [videoId, doneAt] of aria2TrackDone.entries()) {
if (now - doneAt > ARIA2_TRACK_DONE_RETENTION) aria2TrackDone.delete(videoId);
}
for (const [videoId, group] of taskGroups) {
let task = group[0];
if (group.length > 1) {
const queued = aria2TrackQueue.get(videoId);
const sorted = [...group].sort((a2, b2) => {
const expA = aria2TrackTaskExpiry(a2);
const expB = aria2TrackTaskExpiry(b2);
if (expA !== expB) return expA > expB ? -1 : 1;
const priA = aria2TrackGidPriority(a2.status);
const priB = aria2TrackGidPriority(b2.status);
if (priA !== priB) return priA > priB ? -1 : 1;
if (a2.gid === queued?.gid) return -1;
if (b2.gid === queued?.gid) return 1;
return 0;
});
task = sorted[0];
for (const loser of sorted.slice(1)) {
await removeRedundantAria2Task(loser);
const queuedTask = aria2TrackQueue.get(videoId);
if (queuedTask && queuedTask.gid === loser.gid) removeAria2TrackTask(videoId);
stopped++;
log19.info(`扫描 ${videoId}: 停止冗余任务 ${loser.gid}(${loser.status}, 过期${formatAria2TrackExpiry(aria2TrackTaskExpiry(loser))}),保留 ${task.gid}(${task.status}, 过期${formatAria2TrackExpiry(aria2TrackTaskExpiry(task))})`);
}
}
if (hasAria2TrackDone(videoId) && task.status !== "active" && task.status !== "waiting") {
log19.debug(`扫描 ${videoId}: 30 天内已完成,跳过残留任务 ${task.gid}(${task.status})`);
continue;
}
if (hasAria2TrackTask(videoId)) {
const queued = aria2TrackQueue.get(videoId);
if (queued) {
if (queued.gid !== task.gid) {
try {
const queuedStatusRes = await aria2API("aria2.tellStatus", [queued.gid, ["status"]]);
const queuedStatus = queuedStatusRes.result?.status;
if (aria2TrackGidPriority(task.status) > aria2TrackGidPriority(queuedStatus)) {
log19.debug(`扫描 ${videoId}: 队列 gid ${queued.gid}(${queuedStatus}) → 更新为 ${task.gid}(${task.status}),跟随真实下载`);
updateAria2TrackTaskGid(videoId, task.gid);
continue;
}
} catch {
}
}
}
continue;
}
let info = await db.getVideoById(videoId);
if (!info || info.Type === "cache" || info.Type === "init" || info.Type === "fail") {
info = await parseVideoInfo({ Type: "init", ID: videoId });
} else if (info.Type === "partial") {
info = await parseVideoInfo(info);
}
let localPath;
if (info.Type === "full") {
localPath = getDownloadPath(info);
} else {
log19.warn(`入队 ${videoId} 解析失败 (${info.Type}),仍按现有路径入队待重建`);
try {
localPath = analyzeLocalPath(task.files?.[0]?.path ?? "");
} catch {
localPath = analyzeLocalPath(`[${videoId}].mp4`);
}
}
const downloadParams = prune({
"force-save": true,
"allow-overwrite": true,
"all-proxy": config.downloadProxy,
"all-proxy-passwd": !config.downloadProxy.isEmpty() ? config.downloadProxyPassword : void 0,
"all-proxy-user": !config.downloadProxy.isEmpty() ? config.downloadProxyUsername : void 0,
out: localPath.fullName,
dir: localPath.directory,
referer: window.location.hostname,
header: ["Cookie:" + unsafeWindow.document.cookie]
});
log19.debug(`现有任务 ${videoId} (gid=${task.gid}) 已纳入队列`);
enqueueAria2TrackTask(videoId, task.gid, downloadParams, task.status === "complete");
enqueued++;
}
log19.info(`扫描完成: aria2 共 ${allTasks.length} 个任务,新纳入队列 ${enqueued} 个,停止冗余任务 ${stopped} 个`);
} catch (error) {
log19.warn("扫描 aria2 任务失败:", stringify(error));
}
}
async function isAria2QueueFull() {
try {
const [statRes, optRes] = await Promise.all([aria2API("aria2.getGlobalStat", []), aria2API("aria2.getGlobalOption", [])]);
const numActive = parseInt(statRes.result?.numActive ?? "0", 10);
const maxConcurrent = parseInt(optRes.result?.["max-concurrent-downloads"] ?? "5", 10);
return numActive >= maxConcurrent;
} catch {
return false;
}
}
async function nextRebuildAttempt(videoId, title, failures) {
failures++;
if (failures > ARIA2_TRACK_MAX_REBUILD_ATTEMPTS) {
const shouldRetry = await askAria2RebuildContinue(videoId, title);
if (!shouldRetry) {
log19.warn(`${videoId} 用户放弃重试,从队列移除`);
return { ok: false, failures };
}
failures = 0;
log19.info(`${videoId} 用户选择继续重试,重置失败计数`);
}
return { ok: true, failures };
}
async function rebuildAria2Task(videoId, task, status) {
try {
const oldUris = (status.files?.[0]?.uris ?? []).map((u) => u.uri);
let urls;
let freshVideoInfo;
try {
const freshInfo = await parseVideoInfo({ Type: "init", ID: videoId });
if (freshInfo.Type === "full") {
freshVideoInfo = freshInfo;
urls = [buildDownloadUrl2(freshInfo).href];
} else {
log19.warn(`${videoId} 重新解析失败 (${freshInfo.Type}),回退用现有链接重建`);
urls = oldUris;
}
} catch (e) {
log19.warn(`${videoId} 重新解析异常,回退用现有链接重建: ${stringify(e)}`);
urls = oldUris;
}
if (status.status !== "removed") {
try {
let gid = task.gid;
if (status.status === "error") {
const restartRes = await aria2API("aria2.restart", [task.gid]);
if (restartRes?.result?.isEmpty()) {
log19.warn(`${videoId} aria2.restart 返回空 gid,重建失败,下轮重试`);
return {};
}
gid = restartRes.result;
try {
await aria2API("aria2.removeDownloadResult", [task.gid]);
} catch {
}
await aria2API("aria2.changeUri", [gid, 0, oldUris, urls]);
log19.debug(`${videoId} 原地重启成功 (旧 gid=${task.gid} → 新 gid=${gid}, 旧地址 ${oldUris.length} 个 → 新地址 ${urls.length} 个)`);
return { gid, videoInfo: freshVideoInfo };
}
await aria2API("aria2.changeUri", [gid, 0, oldUris, urls]);
await aria2API("aria2.unpause", [gid]);
log19.debug(`${videoId} 原地替换新地址成功 (gid=${gid}, 旧地址 ${oldUris.length} 个 → 新地址 ${urls.length} 个),无需重建`);
return { gid, videoInfo: freshVideoInfo };
} catch (e) {
log19.warn(`${videoId} 原地恢复失败(error 任务 restart 异常或链接已失效),降级为 addUri 重建: ${stringify(e)}`);
}
}
const newRes = await aria2API("aria2.addUri", [urls, task.downloadParams]);
if (newRes?.result?.isEmpty()) {
log19.warn(`${videoId} addUri 返回空 gid,重建失败,下轮重试`);
return {};
}
if (status.status === "error" || status.status === "removed") {
try {
await aria2API("aria2.removeDownloadResult", [task.gid]);
log19.info(`${videoId} 已清理旧任务结果 (gid=${task.gid})`);
} catch {
}
}
log19.debug(`${videoId} 任务重建成功,新 gid=${newRes.result}`);
return { gid: newRes.result, videoInfo: freshVideoInfo };
} catch (error) {
log19.warn(`${videoId} 重建异常,下轮重试: ${stringify(error)}`);
return {};
}
}
async function restartAria2Task(gid, videoId, task) {
try {
const freshInfo = await parseVideoInfo({ Type: "init", ID: videoId });
if (freshInfo.Type !== "full") {
log19.warn(`restartAria2Task ${videoId} 重新解析失败 (${freshInfo.Type})`);
return void 0;
}
const oldUris = (task.files?.[0]?.uris ?? []).map((u) => u.uri);
const newUris = [buildDownloadUrl2(freshInfo).href];
await aria2API("aria2.changeUri", [gid, 0, oldUris, newUris]);
await aria2API("aria2.unpause", [gid]);
return freshInfo;
} catch (error) {
log19.warn(`restartAria2Task 失败 ${gid}:`, stringify(error));
return void 0;
}
}
async function trackExistingAria2Tasks() {
if (config.downloadType !== 0) return;
await scanAria2TasksAndEnqueue();
await tryBecomeAria2TrackManager();
setInterval(() => {
tryBecomeAria2TrackManager();
}, ARIA2_TRACK_ELECTION_INTERVAL);
setInterval(() => {
scanAria2TasksAndEnqueue();
}, ARIA2_TRACK_SCAN_INTERVAL);
setInterval(() => {
GMLock.pruneExpired();
}, ARIA2_TRACK_SCAN_INTERVAL);
originalAddEventListener.call(unsafeWindow, "pagehide", () => {
aria2TrackLock.release(ARIA2_TRACK_MANAGER_LOCK);
});
}
if (GM_getValue(GM_KEY_IS_DEBUG)) {
unsafeWindow.testAria2RebuildAsk = async (videoId = "debugVideo", title = "Debug 视频") => {
try {
const retry = await askAria2RebuildContinue(videoId, title);
log19.info(`testAria2RebuildAsk 结果: ${videoId} → 用户${retry ? "选择重试" : "选择放弃"}`);
newToast(retry ? 1 : 2, {
id: `aria2Track-test-${videoId}`,
duration: 5e3,
node: toastNode(`${title}[${videoId}] 用户${retry ? "选择重试" : "选择放弃"}`, "测试结果")
}).show();
return retry;
} catch (e) {
log19.error(`testAria2RebuildAsk 异常: ${stringify(e)}`);
return void 0;
}
};
}
init_env();
init_log();
var log20 = createLogger("InjectionWatcher");
var rules = [];
var lastInjectAt = new Map();
var observer;
function registerInjectionRule(rule) {
if (rules.some((r) => r.id === rule.id)) return;
rules.push(rule);
startObserver();
queueMicrotask(dispatch);
}
function startObserver() {
if (!isNullOrUndefined(observer)) return;
observer = new MutationObserver(dispatch);
observer.observe(unsafeWindow.document, { childList: true, subtree: true });
log20.debug("注入调度器已启动");
}
function dispatch() {
for (const rule of rules) {
try {
if (rule.enabled && !rule.enabled()) continue;
if (!rule.isTargetPage()) continue;
if (!isNullOrUndefined(unsafeWindow.document.querySelector(rule.markerSelector))) continue;
if (rule.isReady && !rule.isReady()) continue;
const cooldown = rule.cooldown ?? 500;
if (Date.now() - (lastInjectAt.get(rule.id) ?? 0) < cooldown) continue;
lastInjectAt.set(rule.id, Date.now());
Promise.resolve(rule.inject()).catch((error) => log20.warn(`规则 ${rule.id} 注入失败:`, error));
} catch (error) {
log20.warn(`规则 ${rule.id} 调度异常:`, error);
}
}
}
async function fetchCommentPage(type, id, page, parent) {
const query = new URLSearchParams({ page: String(page) });
if (parent !== void 0) query.set("parent", parent);
const res = await unlimitedFetch(apiUrl(`/${type}/${id}/comments`, query), { headers: await getAuth() });
if (!res.ok) throw new Error(`fetch comments failed: HTTP ${res.status}`);
return await res.json();
}
async function fetchAllComments(type, id, parent) {
const comments = [];
const base = await fetchCommentPage(type, id, 0, parent);
comments.push(...base.results);
for (let page = 1; page < Math.ceil(base.count / base.limit); page++) {
comments.push(...(await fetchCommentPage(type, id, page, parent)).results);
}
const replies = [];
for (const comment of comments) {
if (parent === void 0 && comment.numReplies > 0) {
replies.push(...await fetchAllComments(type, id, comment.id));
}
}
return [...comments, ...replies];
}
init_env();
init_env();
init_log();
init_hijack();
var log21 = createLogger("FriendRequests");
var panel;
var panelBar;
var panelText;
var running = false;
function updatePanel(done, total, text) {
if (isNullOrUndefined(panel) || isNullOrUndefined(panelBar) || isNullOrUndefined(panelText)) {
panelBar = renderNode({ nodeType: "div", className: "friendsApproveBar" });
panel = renderNode({
nodeType: "div",
className: "friendsApprove",
childs: [
{ nodeType: "span", className: "friendsApproveText", childs: resolvePlaceholders(text) },
{ nodeType: "div", className: "friendsApproveTrack", childs: panelBar }
],
attributes: { id: "pluginFriendsApprovePanel" }
});
panelText = panel.querySelector(".friendsApproveText");
panel.style.display = "none";
originalNodeAppendChild.call(unsafeWindow.document.body, panel);
} else {
panelText.textContent = resolvePlaceholders(text);
}
panel.style.display = "";
panelBar.style.width = total > 0 ? `${Math.round(done / total * 100)}%` : "0%";
}
function hidePanel() {
if (!isNullOrUndefined(panel)) panel.style.display = "none";
}
async function gatherCommentEvidence(conditionIds, me) {
const cache = new Map();
const load = async (sourceKey, type, id) => {
try {
const comments = await fetchAllComments(type, id);
const byUser = new Map();
for (const c of comments) {
if (isNullOrUndefined(c.user?.id)) continue;
const list = byUser.get(c.user.id) ?? [];
list.push(c.body ?? "");
byUser.set(c.user.id, list);
}
cache.set(sourceKey, byUser);
} catch (error) {
log21.warn(`评论证据抓取失败 (${sourceKey}):`, error);
}
};
const threadId = config.friendApproveEvidenceThreadId;
await Promise.all([
...conditionIds.includes("commentedProfile") ? [load("profile", "profile", me.id)] : [],
...conditionIds.includes("commentedForumThread") && !isNullOrUndefined(threadId) && !threadId.isEmpty() ? [load(`forum:${threadId}`, "forumThread", threadId)] : []
]);
return cache;
}
function hasCommentEvidenceSources(enabledIds) {
return enabledIds.includes("commentedProfile") || enabledIds.includes("commentedForumThread");
}
async function runApproveAll() {
if (running) return;
running = true;
try {
const me = await getLocalUser({ refresh: true });
if (isNullOrUndefined(me)) {
updatePanel(0, 1, "%#notLoggedIn#%");
return;
}
updatePanel(0, 1, "%#friendApproveFetching#%");
const entries = await fetchAllFriendRequests(me.id);
if (entries.length === 0) {
updatePanel(1, 1, "%#friendApproveNone#%");
await delay(2500);
hidePanel();
return;
}
const conditionIds = getEnabledApprovalConditionIds();
const mode = getApprovalMode();
const commentEvidence = hasCommentEvidenceSources(conditionIds) ? await gatherCommentEvidence(conditionIds, me) : void 0;
const qualified = [];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
updatePanel(i, entries.length, `%#friendApproveChecking#% ${i + 1}/${entries.length}`);
const peer = entry.user.id === me.id ? entry.target : entry.user;
if (await checkApprovalConditions(conditionIds, { user: peer, entry, commentsByUser: commentEvidence }, mode)) qualified.push(entry);
}
let approved = 0;
for (let i = 0; i < qualified.length; i++) {
updatePanel(i, qualified.length, `%#friendApproveRunning#% ${i + 1}/${qualified.length}`);
if (await addFriend(qualified[i].user.id)) {
approved++;
} else {
log21.warn(`批准好友请求失败 ${qualified[i].user.id}`);
}
await delay(500);
}
updatePanel(qualified.length, qualified.length, `%#friendApproveDone#% ${approved}/${qualified.length}`);
log21.info(`一键批准完成: ${approved}/${qualified.length} (共 ${entries.length} 条请求)`);
newToast(1, {
duration: -1,
node: toastNode(`%#friendApproveDone#% ${approved}/${qualified.length} (%#friendApproveReloadHint#%)`),
buttons: [{ text: "%#friendApproveReload#%", onClick: (t) => {
t.hide();
unsafeWindow.location.reload();
} }, { text: "%#ok#%", onClick: (t) => t.hide() }]
}).show();
await delay(3e3);
hidePanel();
} catch (error) {
log21.error("一键批准异常:", error);
updatePanel(0, 1, `%#friendApproveError#%`);
newToast(3, {
node: toastNode(`%#friendApproveError#%`),
buttons: [{ text: "%#ok#%", onClick: (t) => t.hide() }]
}).show();
} finally {
running = false;
}
}
function isFriendRequestsPage() {
return getPageTypeFromPath(unsafeWindow.location.hash.trimHead("#").trimHead("!").split("?")[0] || unsafeWindow.location.pathname) === "friends" && unsafeWindow.location.pathname.replace(/\/+$/, "").endsWith("/requests");
}
async function injectFriendApproveButton() {
if (!isFriendRequestsPage()) return;
if (!isNullOrUndefined(unsafeWindow.document.querySelector("#pluginFriendsApprove"))) return;
if (!await verifyLogin()) return;
if (isNullOrUndefined(unsafeWindow.document.querySelector(".friendRequest, .friend"))) return;
const button = renderNode({
nodeType: "button",
className: "friendsApproveButton",
attributes: { id: "pluginFriendsApprove", type: "button" },
childs: "%#friendApproveAll#%",
events: {
click: async (event) => {
event.preventDefault();
event.stopPropagation();
const target = event.target;
target.disabled = true;
await runApproveAll();
target.disabled = false;
return false;
}
}
});
const host = renderNode({
nodeType: "div",
attributes: { id: "pluginFriendsApproveHost" },
childs: button
});
const list = unsafeWindow.document.querySelector(".friendRequests, .row.friendRequests, .friendRequest, .friend");
const container = list?.closest(".container-fluid");
const heading = container?.querySelector(".text--h1") ?? container?.querySelector("h1, h2");
if (heading) {
heading.appendChild(host);
} else if (list && !isNullOrUndefined(list.parentElement)) {
list.parentElement.insertBefore(host, list);
} else {
originalNodeAppendChild.call(container ?? unsafeWindow.document.body, host);
}
log21.debug("一键审批按钮已注入");
}
registerInjectionRule({
id: "friendApproveButton",
enabled: () => config.friendRequestApprove === true,
isTargetPage: isFriendRequestsPage,
isReady: () => !isNullOrUndefined(unsafeWindow.document.querySelector(".friendRequest, .friend")),
markerSelector: "#pluginFriendsApproveHost",
inject: injectFriendApproveButton
});
init_env();
init_env();
init_log();
init_constants();
init_hijack();
var log22 = createLogger("Onboarding");
var actions;
function setOnboardingActions(a2) {
actions = a2;
}
function showGuideOverlay(confirm2) {
let confirmButton = renderNode({
nodeType: "button",
attributes: {
disabled: true,
title: i18nList[config.language].ok
},
childs: "%#ok#%",
events: {
click: () => {
unsafeWindow.document.querySelector("#pluginOverlay")?.remove();
if (confirm2) {
confirm2();
} else {
actions?.openSettings();
}
}
}
});
originalNodeAppendChild.call(
unsafeWindow.document.body,
renderNode({
nodeType: "div",
attributes: {
id: "pluginOverlay"
},
childs: [
{
nodeType: "div",
className: "main",
childs: [
{ nodeType: "p", childs: i18nList[config.language].useHelpForBase },
{ nodeType: "p", childs: "%#useHelpForInjectCheckbox#%" },
{ nodeType: "p", childs: "%#useHelpForCheckDownloadLink#%" },
{ nodeType: "p", childs: i18nList[config.language].useHelpForManualDownload },
{ nodeType: "p", childs: "%#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
]
})
);
}
function firstRun(openSettings) {
GM_listValues().forEach((i) => GM_deleteValue(i));
Config.destroyInstance();
setOnboardingActions({ openSettings });
showGuideOverlay(() => {
GM_setValue(GM_KEY_IS_FIRST_RUN, false);
GM_setValue(GM_KEY_VERSION, GM_info.script.version);
openSettings();
});
}
function showNoticeToast() {
newToast(1, {
node: toastNode(i18nList[config.language].notice),
duration: 1e4,
gravity: "bottom",
position: "center",
onClick() {
this.hide();
}
}).show();
}
function isAccountActive(user) {
return user.status === void 0 || user.status === "active";
}
async function promoteAuthor() {
if (!config.promoteScriptAuthor) return;
try {
const localUser = await getLocalUser({ refresh: true });
const authorProfileRes = await unlimitedFetch(apiUrl("/profile/dawn"), {
method: "GET",
headers: await getAuth()
});
if (isNullOrUndefined(localUser) || !authorProfileRes.ok) {
log22.warn("登录态验证请求失败:", !authorProfileRes.ok ? authorProfileRes.status : "localUser null");
return;
}
const authorProfile = await authorProfileRes.json();
const author = authorProfile.user;
if (localUser.id !== author.id) {
if (!author.following) followUser(author.id).catch(() => void 0);
if (!author.friend) addFriend(author.id).catch(() => void 0);
}
} catch (error) {
log22.warn("验证登录态时出错:", error);
}
}
init_env();
init_db();
init_constants();
init_log();
var log23 = createLogger("DebugExpose");
function exposeDebugTools() {
if (!GM_getValue(GM_KEY_IS_DEBUG)) return false;
Object.assign(unsafeWindow, {
// @ts-ignore
syncCachedToMediaCenter,
// @ts-ignore
syncAllVideosPages,
// @ts-ignore
exportAllToJsonFiles: db.exportAllToJsonFiles.bind(db),
// @ts-ignore
exportToJsonFiles: db.exportToJsonFiles.bind(db),
// @ts-ignore
debugGMFetch: unlimitedFetch
});
log23.debug(stringify(GM_info));
return true;
}
init_env();
init_hijack();
init_constants();
unsafeWindow.fetch = createInterceptedFetch();
setPlaceholderResolver((text) => text.replaceVariable(i18nList[config.language]));
setReportSink(reportSinkAdapter());
if (exposeDebugTools()) debugger;
var pluginMenu = new menu({ openSettings: () => editConfig.inject() });
var editConfig = new configEdit(config, () => check());
var watermark = new waterMark();
setupMenuActions(pluginMenu);
installCheckboxCallbacks();
setOnboardingActions({ openSettings: () => editConfig.inject() });
setPageTypeProbe(() => pluginMenu.pageType);
on("auth:token-changed", () => pluginMenu.pageChange());
on("selection:changed", ({ size, videoId }) => {
watermark.selected.textContent = ` ${i18nList[config.language].selected} ${size} `;
if (videoId) {
const selectButton = getSelectButton(videoId);
if (selectButton) selectButton.checked = selectList.has(videoId);
} else {
pageSelectButtons.forEach((_, key) => updateButtonState(key));
}
});
function pageChange() {
pluginMenu.pageType = getPageType();
if (config.friendRequestApprove) injectFriendApproveButton();
}
if (GM_getValue(GM_KEY_IS_DEBUG)) {
unsafeWindow.testFirstRun = () => firstRun(() => editConfig.inject());
unsafeWindow.testGuideOverlay = showGuideOverlay;
}
async function main() {
;
[rainbow_default, menu_default, config_default, overlay_default, videoCard_default, toast_default, friendRequests_default].forEach((css) => GM_addStyle(css));
const migration = await runMigrations({
selectList,
notifyIncompatible: () => alert(i18nList[config.language].configurationIncompatible)
});
if (migration === "failed") return;
if (migration === "reload") {
unsafeWindow.location.reload();
return;
}
if (GM_getValue(GM_KEY_IS_FIRST_RUN, true)) {
firstRun(() => editConfig.inject());
return;
}
GM_setValue(GM_KEY_VERSION, GM_info.script.version);
watermark.inject();
config.enableBeautify && GM_addStyle(beautify_default);
config.enableWidescreen && GM_addStyle(widescreen_default);
if (!await check()) {
newToast(1, {
text: `%#configError#%`,
duration: 60 * 1e3
}).show();
editConfig.inject();
return;
}
installPlatformHooks({ watchNodeAppend: config.autoInjectCheckbox });
onNodeAdded((node) => injectCheckbox(node));
onNodeRemoved((node) => uninjectCheckbox(node));
onHistoryChange(() => pageChange());
installSelectionShortcuts();
registerInjectionRule({
id: "pluginMenuShell",
isTargetPage: () => true,
isReady: () => !isNullOrUndefined(unsafeWindow.document.getElementById("app")),
markerSelector: "#pluginMenu",
inject: () => {
pluginMenu.inject();
pageChange();
}
});
setMediaCenterPushHook(async (videoInfo) => {
if (!await pushToMediaCenter(videoInfo)) throw new Error("MediaCenter push failed");
});
setAria2TrackEnqueueHook((videoId, gid, downloadParams) => enqueueAria2TrackTask(videoId, gid, downloadParams));
setRetryDownloadHook((videoInfo) => pushDownloadTask(videoInfo));
if (await verifyLogin(true)) {
await promoteAuthor();
}
showNoticeToast();
trackExistingAria2Tasks();
}
;
(unsafeWindow.document.body ? Promise.resolve() : new Promise((resolve) => originalAddEventListener.call(unsafeWindow.document, "DOMContentLoaded", resolve))).then(main);
})();