Files
JHS-enhance/src/plugins/req115.js
T
2026-08-30 03:08:17 +08:00

528 lines
17 KiB
JavaScript

import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
class Req {
static DEFAULT_GET_RESPONSE_TYPE = "document";
static DEFAULT_POST_RESPONSE_TYPE = "json";
static DEFAULT_TIMEOUT = 30000;
static DEFAULT_METHOD = "GET";
static _isRawBody = (v) => v instanceof FormData || v instanceof Blob || v instanceof ArrayBuffer || v instanceof URLSearchParams;
static _isPlainObject = (v) => Object.prototype.toString.call(v) === "[object Object]";
static _serializeToFormData(formData, data, parentKey = "") {
Object.entries(data).forEach(([key, val]) => {
const fullKey = parentKey ? `${parentKey}[${key}]` : key;
if (val === undefined) return;
if (val !== null && typeof val === "object" && !(val instanceof Blob || val instanceof Date)) {
this._serializeToFormData(formData, val, fullKey);
} else {
formData.append(fullKey, val instanceof Date ? val.toISOString() : val);
}
});
}
static request(config) {
let options;
if (typeof config === "string") {
options = {url: config};
} else if (config !== null && typeof config === "object") {
options = {...config};
} else {
return Promise.reject(new TypeError("Request config must be a string or an object"));
}
if (!options.url) return Promise.reject(new Error("Request URL is required"));
options.method = (options.method || this.DEFAULT_METHOD).toUpperCase();
options.timeout ||= this.DEFAULT_TIMEOUT;
const {params, method, data, signal} = options;
try {
const urlInstance = new URL(options.url, location.origin);
if (params) {
const cleanParams = this._isPlainObject(params) ? Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null)) : params;
new URLSearchParams(cleanParams).forEach((v, k) => urlInstance.searchParams.set(k, v));
}
options.url = urlInstance.toString();
} catch {
return Promise.reject(new Error(`Invalid Request URL: ${options.url}`));
}
if (method === "POST") {
options.responseType ??= this.DEFAULT_POST_RESPONSE_TYPE;
if (this._isPlainObject(data) && !this._isRawBody(data)) {
const formData = new FormData();
this._serializeToFormData(formData, data);
options.data = formData;
}
} else if (method === "GET") {
options.responseType ??= options.url.split(/[?#]/)[0].endsWith(".json") ? "json" : this.DEFAULT_GET_RESPONSE_TYPE;
}
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(new DOMException("Aborted", "AbortError"));
const {url: targetUrl} = options;
let xhrHandler;
const handleAbort = () => xhrHandler?.abort();
const finalize = (settleFn, ...args) => {
signal?.removeEventListener("abort", handleAbort);
settleFn(...args);
};
xhrHandler = GM_xmlhttpRequest({
onabort: () => reject(new DOMException("Aborted", "AbortError")),
ontimeout: () => finalize(reject, new Error(`Timeout: ${targetUrl}`)),
onerror: () => finalize(reject, new Error(`Network Error: ${targetUrl}`)),
onload: ({
status,
finalUrl,
response
}) => (status >= 400 ? finalize(reject, new Error(`HTTP ${status}: ${targetUrl}`)) : finalize(resolve, method === "HEAD" ? finalUrl : response)),
...options,
params: undefined,
});
signal?.addEventListener("abort", handleAbort, {once: true});
});
}
static async tasks(initial, steps, signal) {
let ctx = initial;
for (const step of steps) {
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
if (typeof ctx === "string" || ctx?.url) ctx = await this.request(typeof ctx === "string" ? {
url: ctx,
signal
} : {...ctx, signal});
ctx = await step(ctx);
}
return ctx;
}
}
class Drive115 extends Req {
static DEFAULT_GET_RESPONSE_TYPE = "json";
static files(cid = "0", params = {}) {
return this.request({
url: "https://webapi.115.com/files",
params: {cid, ...params},
});
}
static filesSearch(search_value, params = {}) {
return this.request({
url: "https://webapi.115.com/files/search",
params: {search_value, ...params},
});
}
static lixianTaskLists(page = 1) {
return this.request({
url: "https://115.com/web/lixian/",
params: {ct: "lixian", ac: "task_lists", page},
});
}
static labelList() {
return this.request({
url: "https://webapi.115.com/label/list",
params: {keyword: "", limit: 11150},
});
}
/**
* @connect 115vod.com
*/
static filesVideo(pickcode) {
return this.request({
url: "https://115vod.com/webapi/files/video",
params: {pickcode, local: 1},
});
}
static post(details) {
return this.request({method: "POST", ...details});
}
static filesAdd(cname, pid) {
return this.post({
url: "https://webapi.115.com/files/add",
data: {cname, pid},
});
}
static lixianAddTaskUrl(url, wp_path_id) {
return this.post({
url: "https://115.com/web/lixian/",
params: {ct: "lixian", ac: "add_task_url"},
data: {url, wp_path_id},
});
}
/**
* Bulk delete offline tasks and source files
* @param {string[]} hash Array of info_hashes
*/
static lixianTaskDel(hash) {
return this.post({
url: "https://115.com/web/lixian/",
params: {ct: "lixian", ac: "task_del"},
data: {hash},
});
}
/**
* Bulk delete files
* @param {string[]} fid Array of file IDs
* @param {string} pid Parent folder ID
*/
static rbDelete(fid, pid = "") {
return this.post({
url: "https://webapi.115.com/rb/delete",
data: {fid, pid, ignore_warn: 1},
});
}
/**
* Batch move files
* @param {string[]} fid Array of file IDs
* @param {string} pid Destination folder ID
*/
static filesMove(fid, pid) {
return this.post({
url: "https://webapi.115.com/files/move",
data: {fid, pid, move_proid: ""},
});
}
/**
* Bulk label files
* @param {string} file_ids fid1,fid2,fid3...
* @param {string} file_label label_id1,label_id2,label_id3...
*/
static filesBatchLabel(file_ids, file_label, action = "add") {
return this.post({
url: "https://webapi.115.com/files/batch_label",
data: {file_ids, file_label, action},
});
}
/**
* Bulk rename files
* @param {object} files_new_name { [fid]: rename }
*/
static filesBatchRename(files_new_name) {
return this.post({
url: "https://webapi.115.com/files/batch_rename",
data: {files_new_name},
});
}
static sampleInitUpload({filename, filesize, cid}) {
return this.post({
url: "https://uplb.115.com/3.0/sampleinitupload.php",
data: {filename, filesize, target: `U_1_${cid}`},
});
}
/**
* @connect aliyuncs.com
*/
static upload({
host: url,
filename: name,
object: key,
policy,
accessid: OSSAccessKeyId,
callback,
signature,
file,
}) {
return this.post({
url,
data: {
name,
key,
policy,
OSSAccessKeyId,
success_action_status: "200",
callback,
signature,
file,
},
});
}
static filesEdit(fid, fid_cover) {
return this.post({
url: "https://webapi.115.com/files/edit",
data: {fid, fid_cover},
});
}
}
export class Req115 extends Drive115 {
static async filesAll(cid, params = {}) {
const res = await this.files(cid, params);
const {count, page_size, data} = res;
return count > page_size && data.length ? this.files(cid, {...params, limit: count}) : res;
}
static filesAllVideos(cid, params = {}) {
return this.filesAll(cid, {...params, type: 4});
}
static filesAllSRTs(cid, params = {}) {
return this.filesAll(cid, {...params, suffix: "srt"});
}
static async filesSearchAll(search_value, params = {}) {
const res = await this.filesSearch(search_value, params);
const {count, page_size, data} = res;
return count > page_size && data.length ? this.filesSearch(search_value, {...params, limit: count}) : res;
}
static filesSearchAllVideos(search_value, params = {}) {
return this.filesSearchAll(search_value, {...params, type: 4});
}
static filesSearchAllFolders(search_value, params = {}) {
return this.filesSearchAll(search_value, {...params, fc: 1});
}
static async handleDir(routes) {
if (routes.length === 1 && /^\d{5,}$/.test(routes[0])) return routes[0];
let cid;
const routesStr = routes.join("/");
const cachedCid = localStorage.getItem(routesStr);
if (cachedCid) {
const res = await this.files(cachedCid);
if (res?.path?.length) {
const path = res.path.slice(1).map((p) => p.name);
if (path.join("/") === routesStr) cid = cachedCid;
}
}
if (!cid) {
cid = "0";
for (const route of routes) {
const {data} = await this.filesSearchAllFolders(route, {cid});
let folder = data.find((folder) => folder.n === route);
if (!folder) folder = await this.filesAdd(route, cid);
cid = folder?.cid;
if (!cid) break;
}
}
const month = new Date().getMonth().toString();
if (localStorage.getItem("115_CD") !== month) {
localStorage.clear();
localStorage.setItem("115_CD", month);
}
if (cid) localStorage.setItem(routesStr, cid);
return cid;
}
static async handleVerify(info_hash, {regex, codes}, {max, filter}) {
const sleep = () => {
return new Promise((r) => {
setTimeout(r, 1000);
});
};
let file_id = "";
let videos = [];
for (let index = 0; index < max; index++) {
if (index) await sleep();
const {tasks} = await this.lixianTaskLists();
const task = tasks.find((task) => task.info_hash === info_hash);
if (!task || task.status === -1) break;
file_id = task.file_id;
if (file_id) break;
}
if (!file_id) return {file_id, videos};
for (let index = 0; index < max; index++) {
if (index) await sleep();
const {data} = await this.filesAllVideos(file_id);
videos = data.filter((item) => regex.test(item.n));
if (videos.length) break;
}
if (!videos.length) {
const {tasks} = await this.lixianTaskLists();
const task = tasks.find((task) => task.info_hash === info_hash);
if (task.status === 2) {
const {data} = await this.filesAllVideos(file_id);
codes = codes.map((code) => code.toUpperCase());
videos = data.filter((item) => {
const name = item.n.toUpperCase();
return codes.some((code) => name.includes(code));
});
}
}
return {videos: videos.filter(filter), file_id};
}
static async handleClean(keepFiles, cid) {
const needMove = keepFiles.filter((file) => file.cid !== cid).map((file) => file.fid);
if (needMove.length) await this.filesMove(needMove, cid);
const {data} = await this.filesAll(cid);
const needRemove = data
.filter((item) => !keepFiles.some((file) => file.fid === item.fid))
.map((item) => item.fid ?? item.cid);
if (needRemove.length) return this.rbDelete(needRemove, cid);
}
static async handleTags(files, tags) {
const {data} = await this.labelList();
if (!data?.list?.length) return;
const {list} = data;
const labels = [];
tags.forEach((tag) => {
const item = list.find((item) => item.name === tag);
if (item) labels.push(item.id);
});
if (labels.length) return this.filesBatchLabel(files.map((it) => it.fid).toString(), labels.toString());
}
static handleRename(files, cid, {rename, renameTxt, zh, crack}) {
rename = rename.replaceAll("$zh", zh ? renameTxt.zh : "");
rename = rename.replaceAll("$crack", crack ? renameTxt.crack : "");
rename = rename.split("$sep").filter(Boolean).join(renameTxt.sep);
rename = rename.trim();
const renameObj = {[cid]: rename};
if (files.length === 1) {
const {fid, ico} = files[0];
renameObj[fid] = `${rename}.${ico}`;
return this.filesBatchRename(renameObj);
}
const icoMap = files.reduce((acc, {ico, ...item}) => {
acc[ico] ??= [];
acc[ico].push(item);
return acc;
}, {});
const noTxt = renameTxt.no;
for (const [ico, items] of Object.entries(icoMap)) {
if (items.length === 1) {
renameObj[items[0].fid] = `${rename}.${ico}`;
continue;
}
items
.toSorted((a, b) => a.n.localeCompare(b.n))
.forEach(({fid}, idx) => {
const no = noTxt.replaceAll(`\${no}`, `${idx + 1}`.padStart(2, "0"));
renameObj[fid] = `${rename}${no}.${ico}`;
});
}
return this.filesBatchRename(renameObj);
}
static async handleCover(url, cid, filename) {
const file = await this.request({url, timeout: 60000, responseType: "blob"});
if (!file) return;
const res = await this.sampleInitUpload({cid, filename, filesize: file.size});
if (res?.host) return this.upload({...res, filename, file});
}
static async handleOffline(
{dir, regex, codes, verifyOptions, code, rename, renameTxt, tags, clean, cover},
magnets,
) {
const res = {status: "error", msg: `获取目录失败: ${dir.join("/")}`};
const cid = await this.handleDir(dir);
if (!cid) return res;
for (let index = 0, {length} = magnets; index < length; index++) {
const {url, zh, crack} = magnets[index];
const {state, error_msg, errcode, info_hash} = await this.lixianAddTaskUrl(url, cid);
if (!state) {
res.msg = error_msg;
res.status = "error";
res.currIdx = index;
if (errcode === 10008) continue;
if (errcode === 911) res.status = "warn";
break;
}
const {videos, file_id} = await this.handleVerify(info_hash, {regex, codes}, verifyOptions);
if (!videos.length) {
if (verifyOptions.clean) this.lixianTaskDel([info_hash]);
if (file_id && clean) this.rbDelete([file_id], cid);
res.msg = `${code} 离线验证失败`;
res.status = "error";
continue;
}
const {data: srts = []} = await this.filesAllSRTs(file_id);
const files = [...videos, ...srts];
if (clean) await this.handleClean(files, file_id);
if (tags.length) this.handleTags(videos, tags);
if (rename) this.handleRename(files, file_id, {rename, renameTxt, zh: zh || srts.length, crack});
if (cover) {
try {
const {data} = await this.handleCover(cover, file_id, `${code}.cover.jpg`);
if (data?.file_id) this.filesEdit(file_id, data.file_id);
} catch (err) {
console.warn("[Req115.handleCover]", err?.message);
}
}
res.msg = `${code} 离线任务成功`;
res.status = "success";
break;
}
return res;
}
static async queueMutation(name, fn) {
return fn();
}
}
window.Req115 = Req115;