支持分片弹幕接口 /api/v2/comment 请求时支持定义 segmentflag=true 参数,用于请求弹幕分片列表 /api/v2/segmentcomment 通过comment接口返回体中的Segment类JSON数据获取单独一个分片的弹幕数据 增加一键导出forward弹幕插件,使用命令 node build-forward-widget.js 修复youku弹幕里color是字符串导致返回的格式错误的问题
362 lines
8.0 KiB
JavaScript
362 lines
8.0 KiB
JavaScript
// 本插件从项目 https://github.com/huangxd-/danmu_api.git 一键导出,配置参数使用说明请参考该项目README
|
|
|
|
class URL {
|
|
constructor(url, base) {
|
|
if (base) {
|
|
// 如果提供了基础URL,拼接相对路径
|
|
if (url.startsWith('/')) {
|
|
// 处理绝对路径形式
|
|
const baseWithoutPath = base.replace(/\/[^\/]*$/, '');
|
|
this._url = baseWithoutPath + url;
|
|
} else if (url.startsWith('./') || !url.startsWith('http')) {
|
|
// 处理相对路径
|
|
const baseWithoutPath = base.replace(/\/[^\/]*$/, '');
|
|
this._url = baseWithoutFile + '/' + url.replace('./', '');
|
|
} else {
|
|
this._url = url;
|
|
}
|
|
} else {
|
|
this._url = url;
|
|
}
|
|
|
|
// 解析URL组件
|
|
this.parseURL(this._url);
|
|
}
|
|
|
|
parseURL(url) {
|
|
// 基础URL解析逻辑
|
|
const match = url.match(/^([^:]+):\/\/([^\/]+)(.*)$/);
|
|
if (match) {
|
|
this.protocol = match[1] + ':';
|
|
this.hostname = match[2];
|
|
const pathAndQuery = match[3] || '';
|
|
const queryIndex = pathAndQuery.indexOf('?');
|
|
|
|
if (queryIndex !== -1) {
|
|
this.pathname = pathAndQuery.substring(0, queryIndex);
|
|
this.search = pathAndQuery.substring(queryIndex);
|
|
} else {
|
|
this.pathname = pathAndQuery;
|
|
this.search = '';
|
|
}
|
|
} else {
|
|
this.protocol = '';
|
|
this.hostname = '';
|
|
this.pathname = url;
|
|
this.search = '';
|
|
}
|
|
}
|
|
|
|
toString() {
|
|
return this._url;
|
|
}
|
|
|
|
static createObjectURL(obj) {
|
|
// 简单的模拟实现
|
|
return 'blob:' + Date.now();
|
|
}
|
|
|
|
static revokeObjectURL(url) {
|
|
// 简单的模拟实现
|
|
}
|
|
|
|
get href() {
|
|
return this._url;
|
|
}
|
|
|
|
get origin() {
|
|
return this.protocol + '//' + this.hostname;
|
|
}
|
|
|
|
get host() {
|
|
return this.hostname;
|
|
}
|
|
|
|
get searchParams() {
|
|
// 创建一个简单的SearchParams实现
|
|
const paramsString = this.search.substring(1); // 移除开头的?
|
|
const params = new (function() {
|
|
const entries = {};
|
|
if (paramsString) {
|
|
paramsString.split('&').forEach(pair => {
|
|
const [key, value] = pair.split('=');
|
|
if (key) {
|
|
entries[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
}
|
|
});
|
|
}
|
|
|
|
this.get = (name) => entries[name] || null;
|
|
this.set = (name, value) => { entries[name] = value.toString(); };
|
|
this.toString = () => Object.keys(entries).map(key =>
|
|
encodeURIComponent(key) + '=' + encodeURIComponent(entries[key])
|
|
).join('&');
|
|
})();
|
|
return params;
|
|
}
|
|
}
|
|
|
|
class AbortController {
|
|
constructor() {
|
|
this.signal = new AbortSignal();
|
|
}
|
|
|
|
abort() {
|
|
this.signal.abort();
|
|
}
|
|
}
|
|
|
|
class AbortSignal {
|
|
constructor() {
|
|
this.aborted = false;
|
|
this.onabort = null;
|
|
this.listeners = [];
|
|
}
|
|
|
|
abort() {
|
|
if (this.aborted) return;
|
|
|
|
this.aborted = true;
|
|
|
|
// 触发所有监听器
|
|
this.listeners.forEach(listener => {
|
|
try {
|
|
if (typeof listener === 'function') {
|
|
listener({ type: 'abort' });
|
|
} else if (listener && typeof listener.handleEvent === 'function') {
|
|
listener.handleEvent({ type: 'abort' });
|
|
}
|
|
} catch (e) {
|
|
// 忽略监听器中的错误
|
|
}
|
|
});
|
|
|
|
// 触发onabort回调
|
|
if (this.onabort) {
|
|
try {
|
|
this.onabort({ type: 'abort' });
|
|
} catch (e) {
|
|
// 忽略onabort回调中的错误
|
|
}
|
|
}
|
|
}
|
|
|
|
addEventListener(type, listener) {
|
|
if (type === 'abort') {
|
|
this.listeners.push(listener);
|
|
// 如果已经中止,立即触发监听器
|
|
if (this.aborted) {
|
|
try {
|
|
if (typeof listener === 'function') {
|
|
listener({ type: 'abort' });
|
|
} else if (listener && typeof listener.handleEvent === 'function') {
|
|
listener.handleEvent({ type: 'abort' });
|
|
}
|
|
} catch (e) {
|
|
// 忽略监听器中的错误
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
removeEventListener(type, listener) {
|
|
if (type === 'abort') {
|
|
const index = this.listeners.indexOf(listener);
|
|
if (index !== -1) {
|
|
this.listeners.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatchEvent(event) {
|
|
if (event.type === 'abort') {
|
|
this.abort();
|
|
}
|
|
}
|
|
}
|
|
|
|
const { setTimeout: customSetTimeout, clearTimeout: customClearTimeout } = (function() {
|
|
let timerId = 0;
|
|
const timers = new Map();
|
|
|
|
const setTimeoutFn = function(callback, delay = 0) {
|
|
const id = ++timerId;
|
|
|
|
if (typeof Promise !== 'undefined') {
|
|
Promise.resolve().then(() => {
|
|
if (timers.has(id)) {
|
|
try {
|
|
callback();
|
|
} catch (e) {
|
|
console.error('setTimeout error:', e);
|
|
} finally {
|
|
timers.delete(id);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
// 同步执行
|
|
try {
|
|
callback();
|
|
} catch (e) {
|
|
console.error('setTimeout error:', e);
|
|
}
|
|
}
|
|
|
|
timers.set(id, { callback, delay, timestamp: Date.now() });
|
|
return id;
|
|
};
|
|
|
|
const clearTimeoutFn = function(id) {
|
|
return timers.delete(id);
|
|
};
|
|
|
|
return {
|
|
setTimeout: setTimeoutFn,
|
|
clearTimeout: clearTimeoutFn
|
|
};
|
|
})();
|
|
|
|
const setTimeout = customSetTimeout;
|
|
const clearTimeout = customClearTimeout;
|
|
|
|
class Headers {
|
|
constructor(init = {}) {
|
|
this._headers = {};
|
|
if (init instanceof Headers) {
|
|
// 从另一个Headers实例初始化
|
|
for (const [key, value] of init.entries()) {
|
|
this.set(key, value);
|
|
}
|
|
} else if (Array.isArray(init)) {
|
|
// 从键值对数组初始化
|
|
for (const [key, value] of init) {
|
|
this.set(key, value);
|
|
}
|
|
} else if (init && typeof init === 'object') {
|
|
// 从对象初始化
|
|
for (const [key, value] of Object.entries(init)) {
|
|
this.set(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
append(name, value) {
|
|
name = name.toLowerCase();
|
|
if (this._headers[name]) {
|
|
this._headers[name] = this._headers[name] + ', ' + value;
|
|
} else {
|
|
this._headers[name] = value;
|
|
}
|
|
}
|
|
|
|
delete(name) {
|
|
delete this._headers[name.toLowerCase()];
|
|
}
|
|
|
|
get(name) {
|
|
return this._headers[name.toLowerCase()] || null;
|
|
}
|
|
|
|
has(name) {
|
|
return name.toLowerCase() in this._headers;
|
|
}
|
|
|
|
set(name, value) {
|
|
this._headers[name.toLowerCase()] = String(value);
|
|
}
|
|
|
|
forEach(callback, thisArg) {
|
|
for (const [name, value] of Object.entries(this._headers)) {
|
|
callback.call(thisArg, value, name, this);
|
|
}
|
|
}
|
|
|
|
*entries() {
|
|
for (const [name, value] of Object.entries(this._headers)) {
|
|
yield [name, value];
|
|
}
|
|
}
|
|
|
|
*keys() {
|
|
for (const name of Object.keys(this._headers)) {
|
|
yield name;
|
|
}
|
|
}
|
|
|
|
*values() {
|
|
for (const value of Object.values(this._headers)) {
|
|
yield value;
|
|
}
|
|
}
|
|
|
|
[Symbol.iterator]() {
|
|
return this.entries();
|
|
}
|
|
|
|
toJSON() {
|
|
return { ...this._headers };
|
|
}
|
|
}
|
|
|
|
class Response {
|
|
constructor(body, init = {}) {
|
|
this.status = init.status || 200;
|
|
this.statusText = init.statusText || 'OK';
|
|
this.headers = new Headers(init.headers || {});
|
|
this.type = 'default';
|
|
this.url = '';
|
|
this.redirected = false;
|
|
|
|
this._bodyUsed = false;
|
|
if (body !== undefined && body !== null) {
|
|
this._body = body;
|
|
} else {
|
|
this._body = '';
|
|
}
|
|
}
|
|
|
|
get ok() {
|
|
return this.status >= 200 && this.status < 300;
|
|
}
|
|
|
|
get bodyUsed() {
|
|
return this._bodyUsed;
|
|
}
|
|
|
|
_checkBodyUsed() {
|
|
if (this._bodyUsed) {
|
|
throw new TypeError('body stream already read');
|
|
}
|
|
this._bodyUsed = true;
|
|
}
|
|
|
|
async json() {
|
|
this._checkBodyUsed();
|
|
if (typeof this._body === 'string') {
|
|
return JSON.parse(this._body);
|
|
}
|
|
return this._body;
|
|
}
|
|
|
|
async text() {
|
|
this._checkBodyUsed();
|
|
if (typeof this._body === 'string') {
|
|
return this._body;
|
|
}
|
|
return String(this._body);
|
|
}
|
|
|
|
clone() {
|
|
if (this._bodyUsed) {
|
|
throw new TypeError('cannot clone a disturbed response');
|
|
}
|
|
const cloned = new Response(this._body, {
|
|
status: this.status,
|
|
statusText: this.statusText,
|
|
headers: this.headers
|
|
});
|
|
return cloned;
|
|
}
|
|
}
|