From 42da93314da0e66a39c5760382bc63e14885f581 Mon Sep 17 00:00:00 2001 From: Virgil Clyne Date: Thu, 21 Mar 2024 11:12:39 +0800 Subject: [PATCH] build: rollup.js Update DualSubs.Netflix.beta.sgmodule Create Netflix.request.beta.js Create Netflix.response.beta.js Create rollup.debug.config.js Create detectFormat.mjs Create certificate.cer --- .gitignore | 144 +- .gitmodules | 6 + example/certificate.cer | 27 + js/Netflix.request.beta.js | 1161 +++++++++++++++++ js/Netflix.response.beta.js | 1141 ++++++++++++++++ modules/DualSubs.Netflix.beta.sgmodule | 18 +- .../archive/DualSubs.Netflix.beta.sgmodule | 21 + .../DualSubs.Netflix.test.sgmodule | 0 package-lock.json | 620 +++++++++ package.json | 29 + rollup.config.js | 9 + rollup.debug.config.js | 23 + rollup.default.config.js | 15 + src/ENV | 1 + src/Netflix.request.beta.js | 156 +++ src/Netflix.response.beta.js | 130 ++ src/URI | 1 + src/database/Default.json | 25 + src/database/Netflix.json | 10 + src/database/index.mjs | 7 + src/function/detectFormat.mjs | 77 ++ src/function/setENV.mjs | 22 + 22 files changed, 3637 insertions(+), 6 deletions(-) create mode 100644 example/certificate.cer create mode 100644 js/Netflix.request.beta.js create mode 100644 js/Netflix.response.beta.js create mode 100644 modules/archive/DualSubs.Netflix.beta.sgmodule rename modules/{ => archive}/DualSubs.Netflix.test.sgmodule (100%) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 rollup.debug.config.js create mode 100644 rollup.default.config.js create mode 160000 src/ENV create mode 100644 src/Netflix.request.beta.js create mode 100644 src/Netflix.response.beta.js create mode 160000 src/URI create mode 100644 src/database/Default.json create mode 100644 src/database/Netflix.json create mode 100644 src/database/index.mjs create mode 100644 src/function/detectFormat.mjs create mode 100644 src/function/setENV.mjs diff --git a/.gitignore b/.gitignore index 9bea433..3502ef7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,144 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node -.DS_Store +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node diff --git a/.gitmodules b/.gitmodules index cc89551..9b6b98b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "BoxJs"] path = BoxJs url = https://github.com/DualSubs/BoxJs.git +[submodule "src/ENV"] + path = src/ENV + url = https://github.com/NanoCat-Me/ENV.git +[submodule "src/URI"] + path = src/URI + url = https://github.com/NanoCat-Me/URI.git diff --git a/example/certificate.cer b/example/certificate.cer new file mode 100644 index 0000000..9aef6d0 --- /dev/null +++ b/example/certificate.cer @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIE2jCCA8KgAwIBAgIIBRGnbPd8z1YwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MTMwMQYDVQQDDCpBcHBsZSBLZXkgU2VydmljZXMg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTMwMzI3MjEyNjU2WhcNMTUwMzI4 +MjEyNjU2WjBjMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLTmV0ZmxpeC5jb20xDDAK +BgNVBAsMA0VEUzEwMC4GA1UEAwwnRlBTIEFwcGxpY2F0aW9uIENlcnRpZmljYXRl +ICgyMDEzIHYxLjApMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfaIdDptTh +ILsQcAbDMvT5FpK4JNn/BnHAY++rS9OFfhg5R4pV7CI+UMZeC64TFJJZciq6dX4/ +Vh7JDDULooAeZxlOLqJB4v+KDMpFS6VsRPweeMRSCE5rQffF5HoRKx682Kw4Ltv2 +PTxE3M16ktYCOxq+/7fxevMt3uII+2V0tQIDAQABo4IB+DCCAfQwHQYDVR0OBBYE +FDuQUJCSl+l2UeybrEfNbUR1JcwSMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU +Y+RHVMuFcVlGLIOszEQxZGcDLL4wgeIGA1UdIASB2jCB1zCB1AYJKoZIhvdjZAUB +MIHGMIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNh +dGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBh +cHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwg +Y2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0 +YXRlbWVudHMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuYXBwbGUuY29t +L2tleXNlcnZpY2VzLmNybDAOBgNVHQ8BAf8EBAMCBSAwLQYLKoZIhvdjZAYNAQMB +Af8EGwGcLBpLUU8iNtuBsGfgldUUE/I42u6RKyl8uzBJBgsqhkiG92NkBg0BBAEB +/wQ3AV+LX+Xo3O4lI5WzFXfxVrna5jJD1GHioNsMHMKUv97Kx9dCozZVRhmiGdTR +EdjOptDoUjj2ODANBgkqhkiG9w0BAQUFAAOCAQEAmkGc6tT450ENeFTTmvhyTHfn +tjWyEpEvsvoubGpqPnbPXhYsaz6U1RuoLkf5q4BkaXVE0yekfKiPa5lOSIYOebyW +gDkWBuJDPrQFw8QYreq5T/rteSNQnJS1lAbg5vyLzexLMH7kq47OlCAnUlrI20mv +GM71RuU6HlKJIlWIVlId5JZQF2ae0/A6BVZWh35+bQu+iPI1PXjrTVYqtmrV6N+v +V8UaHRdKV6rCD648iJebynWZj4Gbgzqw7AX4RE6UwiX0Rgz9ZMM5Vzfgrgk8KxOm +suaP8Kgqf5KWeH/LDa+ocftU7zGz1jO5L999JptFIatsdPyZXnA3xM+QjzBW8w== +-----END CERTIFICATE----- diff --git a/js/Netflix.request.beta.js b/js/Netflix.request.beta.js new file mode 100644 index 0000000..d6f9b1b --- /dev/null +++ b/js/Netflix.request.beta.js @@ -0,0 +1,1161 @@ +/* README: https://github.com/DualSubs */ +/* https://www.lodashjs.com */ +class Lodash { + static name = "Lodash"; + static version = "1.2.2"; + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) }; + + static get(object = {}, path = "", defaultValue = undefined) { + // translate array case to dot case, then split with . + // a[0].b -> a.0.b -> ['a', '0', 'b'] + if (!Array.isArray(path)) path = this.toPath(path); + + const result = path.reduce((previousValue, currentValue) => { + return Object(previousValue)[currentValue]; // null undefined get attribute will throwError, Object() can return a object + }, object); + return (result === undefined) ? defaultValue : result; + } + + static set(object = {}, path = "", value) { + if (!Array.isArray(path)) path = this.toPath(path); + path + .slice(0, -1) + .reduce( + (previousValue, currentValue, currentIndex) => + (Object(previousValue[currentValue]) === previousValue[currentValue]) + ? previousValue[currentValue] + : previousValue[currentValue] = (/^\d+$/.test(path[currentIndex + 1]) ? [] : {}), + object + )[path[path.length - 1]] = value; + return object + } + + static unset(object = {}, path = "") { + if (!Array.isArray(path)) path = this.toPath(path); + let result = path.reduce((previousValue, currentValue, currentIndex) => { + if (currentIndex === path.length - 1) { + delete previousValue[currentValue]; + return true + } + return Object(previousValue)[currentValue] + }, object); + return result + } + + static toPath(value) { + return value.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean); + } + + static escape(string) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return string.replace(/[&<>"']/g, m => map[m]) + }; + + static unescape(string) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + }; + return string.replace(/&|<|>|"|'/g, m => map[m]) + } + +} + +/* https://developer.mozilla.org/zh-CN/docs/Web/API/Storage/setItem */ +class $Storage { + static name = "$Storage"; + static version = "1.0.9"; + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) }; + static data = null + static dataFile = 'box.dat' + static #nameRegex = /^@(?[^.]+)(?:\.(?.*))?$/; + + static #platform() { + if ('undefined' !== typeof $environment && $environment['surge-version']) + return 'Surge' + if ('undefined' !== typeof $environment && $environment['stash-version']) + return 'Stash' + if ('undefined' !== typeof module && !!module.exports) return 'Node.js' + if ('undefined' !== typeof $task) return 'Quantumult X' + if ('undefined' !== typeof $loon) return 'Loon' + if ('undefined' !== typeof $rocket) return 'Shadowrocket' + if ('undefined' !== typeof Egern) return 'Egern' + } + + static getItem(keyName = new String, defaultValue = null) { + let keyValue = defaultValue; + // 如果以 @ + switch (keyName.startsWith('@')) { + case true: + const { key, path } = keyName.match(this.#nameRegex)?.groups; + //console.log(`1: ${key}, ${path}`); + keyName = key; + let value = this.getItem(keyName, {}); + //console.log(`2: ${JSON.stringify(value)}`) + if (typeof value !== "object") value = {}; + //console.log(`3: ${JSON.stringify(value)}`) + keyValue = Lodash.get(value, path); + //console.log(`4: ${JSON.stringify(keyValue)}`) + try { + keyValue = JSON.parse(keyValue); + } catch (e) { + // do nothing + } //console.log(`5: ${JSON.stringify(keyValue)}`) + break; + default: + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + keyValue = $persistentStore.read(keyName); + break; + case 'Quantumult X': + keyValue = $prefs.valueForKey(keyName); + break; + case 'Node.js': + this.data = this.#loaddata(this.dataFile); + keyValue = this.data?.[keyName]; + break; + default: + keyValue = this.data?.[keyName] || null; + break; + } try { + keyValue = JSON.parse(keyValue); + } catch (e) { + // do nothing + } break; + } return keyValue ?? defaultValue; + }; + + static setItem(keyName = new String, keyValue = new String) { + let result = false; + //console.log(`0: ${typeof keyValue}`); + switch (typeof keyValue) { + case "object": + keyValue = JSON.stringify(keyValue); + break; + default: + keyValue = String(keyValue); + break; + } switch (keyName.startsWith('@')) { + case true: + const { key, path } = keyName.match(this.#nameRegex)?.groups; + //console.log(`1: ${key}, ${path}`); + keyName = key; + let value = this.getItem(keyName, {}); + //console.log(`2: ${JSON.stringify(value)}`) + if (typeof value !== "object") value = {}; + //console.log(`3: ${JSON.stringify(value)}`) + Lodash.set(value, path, keyValue); + //console.log(`4: ${JSON.stringify(value)}`) + result = this.setItem(keyName, value); + //console.log(`5: ${result}`) + break; + default: + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + result = $persistentStore.write(keyValue, keyName); + break; + case 'Quantumult X': + result =$prefs.setValueForKey(keyValue, keyName); + break; + case 'Node.js': + this.data = this.#loaddata(this.dataFile); + this.data[keyName] = keyValue; + this.#writedata(this.dataFile); + result = true; + break; + default: + result = this.data?.[keyName] || null; + break; + } break; + } return result; + }; + + static removeItem(keyName){ + let result = false; + switch (keyName.startsWith('@')) { + case true: + const { key, path } = keyName.match(this.#nameRegex)?.groups; + keyName = key; + let value = this.getItem(keyName); + if (typeof value !== "object") value = {}; + keyValue = Lodash.unset(value, path); + result = this.setItem(keyName, value); + break; + default: + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + result = false; + break; + case 'Quantumult X': + result = $prefs.removeValueForKey(keyName); + break; + case 'Node.js': + result = false; + break; + default: + result = false; + break; + } break; + } return result; + } + + static clear() { + let result = false; + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + result = false; + break; + case 'Quantumult X': + result = $prefs.removeAllValues(); + break; + case 'Node.js': + result = false; + break; + default: + result = false; + break; + } return result; + } + + static #loaddata(dataFile) { + if (this.isNode()) { + this.fs = this.fs ? this.fs : require('fs'); + this.path = this.path ? this.path : require('path'); + const curDirDataFilePath = this.path.resolve(dataFile); + const rootDirDataFilePath = this.path.resolve( + process.cwd(), + dataFile + ); + const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath); + const isRootDirDataFile = + !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath); + if (isCurDirDataFile || isRootDirDataFile) { + const datPath = isCurDirDataFile + ? curDirDataFilePath + : rootDirDataFilePath; + try { + return JSON.parse(this.fs.readFileSync(datPath)) + } catch (e) { + return {} + } + } else return {} + } else return {} + } + + static #writedata(dataFile = this.dataFile) { + if (this.isNode()) { + this.fs = this.fs ? this.fs : require('fs'); + this.path = this.path ? this.path : require('path'); + const curDirDataFilePath = this.path.resolve(dataFile); + const rootDirDataFilePath = this.path.resolve( + process.cwd(), + dataFile + ); + const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath); + const isRootDirDataFile = + !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath); + const jsondata = JSON.stringify(this.data); + if (isCurDirDataFile) { + this.fs.writeFileSync(curDirDataFilePath, jsondata); + } else if (isRootDirDataFile) { + this.fs.writeFileSync(rootDirDataFilePath, jsondata); + } else { + this.fs.writeFileSync(curDirDataFilePath, jsondata); + } + } + }; + +} + +class ENV { + static name = "ENV" + static version = '1.8.2' + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) } + + constructor(name, opts) { + console.log(`\n🟧 ${ENV.name} v${ENV.version}\n`); + this.name = name; + this.logs = []; + this.isMute = false; + this.isMuteLog = false; + this.logSeparator = '\n'; + this.encoding = 'utf-8'; + this.startTime = new Date().getTime(); + Object.assign(this, opts); + this.log(`\n🚩 开始!\n${name}\n`); + } + + environment() { + switch (this.platform()) { + case 'Surge': + $environment.app = 'Surge'; + return $environment + case 'Stash': + $environment.app = 'Stash'; + return $environment + case 'Egern': + $environment.app = 'Egern'; + return $environment + case 'Loon': + let environment = $loon.split(' '); + return { + "device": environment[0], + "ios": environment[1], + "loon-version": environment[2], + "app": "Loon" + }; + case 'Quantumult X': + return { + "app": "Quantumult X" + }; + case 'Node.js': + process.env.app = 'Node.js'; + return process.env + default: + return {} + } + } + + platform() { + if ('undefined' !== typeof $environment && $environment['surge-version']) + return 'Surge' + if ('undefined' !== typeof $environment && $environment['stash-version']) + return 'Stash' + if ('undefined' !== typeof module && !!module.exports) return 'Node.js' + if ('undefined' !== typeof $task) return 'Quantumult X' + if ('undefined' !== typeof $loon) return 'Loon' + if ('undefined' !== typeof $rocket) return 'Shadowrocket' + if ('undefined' !== typeof Egern) return 'Egern' + } + + isNode() { + return 'Node.js' === this.platform() + } + + isQuanX() { + return 'Quantumult X' === this.platform() + } + + isSurge() { + return 'Surge' === this.platform() + } + + isLoon() { + return 'Loon' === this.platform() + } + + isShadowrocket() { + return 'Shadowrocket' === this.platform() + } + + isStash() { + return 'Stash' === this.platform() + } + + isEgern() { + return 'Egern' === this.platform() + } + + async getScript(url) { + return await this.fetch(url).then(response => response.body); + } + + async runScript(script, runOpts) { + let httpapi = $Storage.getItem('@chavy_boxjs_userCfgs.httpapi'); + httpapi = httpapi?.replace?.(/\n/g, '')?.trim(); + let httpapi_timeout = $Storage.getItem('@chavy_boxjs_userCfgs.httpapi_timeout'); + httpapi_timeout = (httpapi_timeout * 1) ?? 20; + httpapi_timeout = runOpts?.timeout ?? httpapi_timeout; + const [password, address] = httpapi.split('@'); + const request = { + url: `http://${address}/v1/scripting/evaluate`, + body: { + script_text: script, + mock_type: 'cron', + timeout: httpapi_timeout + }, + headers: { 'X-Key': password, 'Accept': '*/*' }, + timeout: httpapi_timeout + }; + await this.fetch(request).then(response => response.body, error => this.logErr(error)); + } + + initGotEnv(opts) { + this.got = this.got ? this.got : require('got'); + this.cktough = this.cktough ? this.cktough : require('tough-cookie'); + this.ckjar = this.ckjar ? this.ckjar : new this.cktough.CookieJar(); + if (opts) { + opts.headers = opts.headers ? opts.headers : {}; + if (undefined === opts.headers.Cookie && undefined === opts.cookieJar) { + opts.cookieJar = this.ckjar; + } + } + } + + async fetch(request = {} || "", option = {}) { + // 初始化参数 + switch (request.constructor) { + case Object: + request = { ...request, ...option }; + break; + case String: + request = { "url": request, ...option }; + break; + } // 自动判断请求方法 + if (!request.method) { + request.method = "GET"; + if (request.body ?? request.bodyBytes) request.method = "POST"; + } // 移除请求头中的部分参数, 让其自动生成 + delete request.headers?.Host; + delete request.headers?.[":authority"]; + delete request.headers?.['Content-Length']; + delete request.headers?.['content-length']; + // 定义请求方法(小写) + const method = request.method.toLocaleLowerCase(); + // 判断平台 + switch (this.platform()) { + case 'Loon': + case 'Surge': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + default: + // 转换请求参数 + if (request.timeout) { + request.timeout = parseInt(request.timeout, 10); + if (this.isSurge()) ; else request.timeout = request.timeout * 1000; + } if (request.policy) { + if (this.isLoon()) request.node = request.policy; + if (this.isStash()) Lodash.set(request, "headers.X-Stash-Selected-Proxy", encodeURI(request.policy)); + if (this.isShadowrocket()) Lodash.set(request, "headers.X-Surge-Proxy", request.policy); + } if (typeof request.redirection === "boolean") request["auto-redirect"] = request.redirection; + // 转换请求体 + if (request.bodyBytes && !request.body) { + request.body = request.bodyBytes; + delete request.bodyBytes; + } // 发送请求 + return await new Promise((resolve, reject) => { + $httpClient[method](request, (error, response, body) => { + if (error) reject(error); + else { + response.ok = /^2\d\d$/.test(response.status); + response.statusCode = response.status; + if (body) { + response.body = body; + if (request["binary-mode"] == true) response.bodyBytes = body; + } resolve(response); + } + }); + }); + case 'Quantumult X': + // 转换请求参数 + if (request.policy) Lodash.set(request, "opts.policy", request.policy); + if (typeof request["auto-redirect"] === "boolean") Lodash.set(request, "opts.redirection", request["auto-redirect"]); + // 转换请求体 + if (request.body instanceof ArrayBuffer) { + request.bodyBytes = request.body; + delete request.body; + } else if (ArrayBuffer.isView(request.body)) { + request.bodyBytes = request.body.buffer.slice(request.body.byteOffset, request.body.byteLength + request.body.byteOffset); + delete object.body; + } else if (request.body) delete request.bodyBytes; + // 发送请求 + return await $task.fetch(request).then( + response => { + response.ok = /^2\d\d$/.test(response.statusCode); + response.status = response.statusCode; + return response; + }, + reason => Promise.reject(reason.error)); + case 'Node.js': + let iconv = require('iconv-lite'); + this.initGotEnv(request); + const { url, ...option } = request; + return await this.got[method](url, option) + .on('redirect', (response, nextOpts) => { + try { + if (response.headers['set-cookie']) { + const ck = response.headers['set-cookie'] + .map(this.cktough.Cookie.parse) + .toString(); + if (ck) { + this.ckjar.setCookieSync(ck, null); + } + nextOpts.cookieJar = this.ckjar; + } + } catch (e) { + this.logErr(e); + } + // this.ckjar.setCookieSync(response.headers['set-cookie'].map(Cookie.parse).toString()) + }) + .then( + response => { + response.statusCode = response.status; + response.body = iconv.decode(response.rawBody, this.encoding); + response.bodyBytes = response.rawBody; + return response; + }, + error => Promise.reject(error.message)); + } }; + + /** + * + * 示例:$.time('yyyy-MM-dd qq HH:mm:ss.S') + * :$.time('yyyyMMddHHmmssS') + * y:年 M:月 d:日 q:季 H:时 m:分 s:秒 S:毫秒 + * 其中y可选0-4位占位符、S可选0-1位占位符,其余可选0-2位占位符 + * @param {string} format 格式化参数 + * @param {number} ts 可选: 根据指定时间戳返回格式化日期 + * + */ + time(format, ts = null) { + const date = ts ? new Date(ts) : new Date(); + let o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'H+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + 'q+': Math.floor((date.getMonth() + 3) / 3), + 'S': date.getMilliseconds() + }; + if (/(y+)/.test(format)) + format = format.replace( + RegExp.$1, + (date.getFullYear() + '').substr(4 - RegExp.$1.length) + ); + for (let k in o) + if (new RegExp('(' + k + ')').test(format)) + format = format.replace( + RegExp.$1, + RegExp.$1.length == 1 + ? o[k] + : ('00' + o[k]).substr(('' + o[k]).length) + ); + return format + } + + /** + * 系统通知 + * + * > 通知参数: 同时支持 QuanX 和 Loon 两种格式, EnvJs根据运行环境自动转换, Surge 环境不支持多媒体通知 + * + * 示例: + * $.msg(title, subt, desc, 'twitter://') + * $.msg(title, subt, desc, { 'open-url': 'twitter://', 'media-url': 'https://github.githubassets.com/images/modules/open_graph/github-mark.png' }) + * $.msg(title, subt, desc, { 'open-url': 'https://bing.com', 'media-url': 'https://github.githubassets.com/images/modules/open_graph/github-mark.png' }) + * + * @param {*} title 标题 + * @param {*} subt 副标题 + * @param {*} desc 通知详情 + * @param {*} opts 通知参数 + * + */ + msg(title = name, subt = '', desc = '', opts) { + const toEnvOpts = (rawopts) => { + switch (typeof rawopts) { + case undefined: + return rawopts + case 'string': + switch (this.platform()) { + case 'Surge': + case 'Stash': + case 'Egern': + default: + return { url: rawopts } + case 'Loon': + case 'Shadowrocket': + return rawopts + case 'Quantumult X': + return { 'open-url': rawopts } + case 'Node.js': + return undefined + } + case 'object': + switch (this.platform()) { + case 'Surge': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + default: { + let openUrl = + rawopts.url || rawopts.openUrl || rawopts['open-url']; + return { url: openUrl } + } + case 'Loon': { + let openUrl = + rawopts.openUrl || rawopts.url || rawopts['open-url']; + let mediaUrl = rawopts.mediaUrl || rawopts['media-url']; + return { openUrl, mediaUrl } + } + case 'Quantumult X': { + let openUrl = + rawopts['open-url'] || rawopts.url || rawopts.openUrl; + let mediaUrl = rawopts['media-url'] || rawopts.mediaUrl; + let updatePasteboard = + rawopts['update-pasteboard'] || rawopts.updatePasteboard; + return { + 'open-url': openUrl, + 'media-url': mediaUrl, + 'update-pasteboard': updatePasteboard + } + } + case 'Node.js': + return undefined + } + default: + return undefined + } + }; + if (!this.isMute) { + switch (this.platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + default: + $notification.post(title, subt, desc, toEnvOpts(opts)); + break + case 'Quantumult X': + $notify(title, subt, desc, toEnvOpts(opts)); + break + case 'Node.js': + break + } + } + if (!this.isMuteLog) { + let logs = ['', '==============📣系统通知📣==============']; + logs.push(title); + subt ? logs.push(subt) : ''; + desc ? logs.push(desc) : ''; + console.log(logs.join('\n')); + this.logs = this.logs.concat(logs); + } + } + + log(...logs) { + if (logs.length > 0) { + this.logs = [...this.logs, ...logs]; + } + console.log(logs.join(this.logSeparator)); + } + + logErr(error) { + switch (this.platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + case 'Quantumult X': + default: + this.log('', `❗️ ${this.name}, 错误!`, error); + break + case 'Node.js': + this.log('', `❗️${this.name}, 错误!`, error.stack); + break + } + } + + wait(time) { + return new Promise((resolve) => setTimeout(resolve, time)) + } + + done(object = {}) { + const endTime = new Date().getTime(); + const costTime = (endTime - this.startTime) / 1000; + this.log("", `🚩 ${this.name}, 结束! 🕛 ${costTime} 秒`, ""); + switch (this.platform()) { + case 'Surge': + if (object.policy) Lodash.set(object, "headers.X-Surge-Policy", object.policy); + $done(object); + break; + case 'Loon': + if (object.policy) object.node = object.policy; + $done(object); + break; + case 'Stash': + if (object.policy) Lodash.set(object, "headers.X-Stash-Selected-Proxy", encodeURI(object.policy)); + $done(object); + break; + case 'Egern': + $done(object); + break; + case 'Shadowrocket': + default: + $done(object); + break; + case 'Quantumult X': + if (object.policy) Lodash.set(object, "opts.policy", object.policy); + // 移除不可写字段 + delete object["auto-redirect"]; + delete object["auto-cookie"]; + delete object["binary-mode"]; + delete object.charset; + delete object.host; + delete object.insecure; + delete object.method; // 1.4.x 不可写 + delete object.opt; // $task.fetch() 参数, 不可写 + delete object.path; // 可写, 但会与 url 冲突 + delete object.policy; + delete object["policy-descriptor"]; + delete object.scheme; + delete object.sessionIndex; + delete object.statusCode; + delete object.timeout; + if (object.body instanceof ArrayBuffer) { + object.bodyBytes = object.body; + delete object.body; + } else if (ArrayBuffer.isView(object.body)) { + object.bodyBytes = object.body.buffer.slice(object.body.byteOffset, object.body.byteLength + object.body.byteOffset); + delete object.body; + } else if (object.body) delete object.bodyBytes; + $done(object); + break; + case 'Node.js': + process.exit(1); + break; + } + } +} + +class URI { + static name = "URI"; + static version = "1.2.7"; + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) }; + static #json = { scheme: "", host: "", path: "", query: {} }; + + static parse(url) { + const URLRegex = /(?:(?.+):\/\/(?[^/]+))?\/?(?[^?]+)?\??(?[^?]+)?/; + let json = url.match(URLRegex)?.groups ?? null; + if (json?.path) json.paths = json.path.split("/"); else json.path = ""; + //if (json?.paths?.at(-1)?.includes(".")) json.format = json.paths.at(-1).split(".").at(-1); + if (json?.paths) { + const fileName = json.paths[json.paths.length - 1]; + if (fileName?.includes(".")) { + const list = fileName.split("."); + json.format = list[list.length - 1]; + } + } + if (json?.query) json.query = Object.fromEntries(json.query.split("&").map((param) => param.split("="))); + return json + }; + + static stringify(json = this.#json) { + let url = ""; + if (json?.scheme && json?.host) url += json.scheme + "://" + json.host; + if (json?.path) url += (json?.host) ? "/" + json.path : json.path; + if (json?.query) url += "?" + Object.entries(json.query).map(param => param.join("=")).join("&"); + return url + }; +} + +var Settings$1 = { + Switch: true, + Type: "Translate", + Types: [ + "Official", + "Translate" + ], + Languages: [ + "EN", + "ZH" + ], + CacheSize: 50 +}; +var Configs$1 = { + breakLine: { + "text/xml": " ", + "application/xml": " ", + "text/vtt": "\n", + "application/vtt": "\n", + "text/json": "\n", + "application/json": "\n" + } +}; +var Default = { + Settings: Settings$1, + Configs: Configs$1 +}; + +var Default$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + Configs: Configs$1, + Settings: Settings$1, + default: Default +}); + +var Settings = { + Switch: true, + Types: [ + "Translate", + "External" + ], + Languages: [ + "AUTO", + "ZH" + ] +}; +var Configs = { + Languages: { + AR: "ar", + CS: "cs", + DA: "da", + DE: "de", + EN: "en", + "EN-GB": "en-GB", + "EN-US": "en-US", + "EN-US SDH": "en-US SDH", + ES: "es", + "ES-419": "es-419", + "ES-ES": "es-ES", + FI: "fi", + FR: "fr", + HE: "he", + HR: "hr", + HU: "hu", + ID: "id", + IT: "it", + JA: "ja", + KO: "ko", + MS: "ms", + NB: "nb", + NL: "nl", + PL: "pl", + PT: "pt", + "PT-PT": "pt-PT", + "PT-BR": "pt-BR", + RO: "ro", + RU: "ru", + SV: "sv", + TH: "th", + TR: "tr", + UK: "uk", + VI: "vi", + IS: "is", + ZH: "zh", + "ZH-HANS": "zh-Hans", + "ZH-HK": "zh-HK", + "ZH-HANT": "zh-Hant" + } +}; +var Netflix = { + Settings: Settings, + Configs: Configs +}; + +var Netflix$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + Configs: Configs, + Settings: Settings, + default: Netflix +}); + +var Database$1 = Database = { + "Default": Default$1, + "Netflix": Netflix$1, +}; + +/** + * Get Storage Variables + * @link https://github.com/NanoCat-Me/ENV/blob/main/getStorage.mjs + * @author VirgilClyne + * @param {String} key - Persistent Store Key + * @param {Array} names - Platform Names + * @param {Object} database - Default Database + * @return {Object} { Settings, Caches, Configs } + */ +function getStorage(key, names, database) { + //console.log(`☑️ ${this.name}, Get Environment Variables`, ""); + /***************** BoxJs *****************/ + // 包装为局部变量,用完释放内存 + // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 + let BoxJs = $Storage.getItem(key, database); + //console.log(`🚧 ${this.name}, Get Environment Variables`, `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs)}`, ""); + /***************** Argument *****************/ + let Argument = {}; + if (typeof $argument !== "undefined") { + if (Boolean($argument)) { + //console.log(`🎉 ${this.name}, $Argument`); + let arg = Object.fromEntries($argument.split("&").map((item) => item.split("=").map(i => i.replace(/\"/g, '')))); + //console.log(JSON.stringify(arg)); + for (let item in arg) Lodash.set(Argument, item, arg[item]); + //console.log(JSON.stringify(Argument)); + } //console.log(`✅ ${this.name}, Get Environment Variables`, `Argument类型: ${typeof Argument}`, `Argument内容: ${JSON.stringify(Argument)}`, ""); + } /***************** Store *****************/ + const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; + if (!Array.isArray(names)) names = [names]; + //console.log(`🚧 ${this.name}, Get Environment Variables`, `names类型: ${typeof names}`, `names内容: ${JSON.stringify(names)}`, ""); + for (let name of names) { + Store.Settings = { ...Store.Settings, ...database?.[name]?.Settings, ...Argument, ...BoxJs?.[name]?.Settings }; + Store.Configs = { ...Store.Configs, ...database?.[name]?.Configs }; + if (BoxJs?.[name]?.Caches && typeof BoxJs?.[name]?.Caches === "string") BoxJs[name].Caches = JSON.parse(BoxJs?.[name]?.Caches); + Store.Caches = { ...Store.Caches, ...BoxJs?.[name]?.Caches }; + } //console.log(`🚧 ${this.name}, Get Environment Variables`, `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`, ""); + traverseObject(Store.Settings, (key, value) => { + //console.log(`🚧 ${this.name}, traverseObject`, `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`, ""); + if (value === "true" || value === "false") value = JSON.parse(value); // 字符串转Boolean + else if (typeof value === "string") { + if (value.includes(",")) value = value.split(",").map(item => string2number(item)); // 字符串转数组转数字 + else value = string2number(value); // 字符串转数字 + } return value; + }); + //console.log(`✅ ${this.name}, Get Environment Variables`, `Store: ${typeof Store.Caches}`, `Store内容: ${JSON.stringify(Store)}`, ""); + return Store; + + /***************** function *****************/ + function traverseObject(o, c) { for (var t in o) { var n = o[t]; o[t] = "object" == typeof n && null !== n ? traverseObject(n, c) : c(t, n); } return o } + function string2number(string) { if (string && !isNaN(string)) string = parseInt(string, 10); return string } +} + +/** + * Set Environment Variables + * @author VirgilClyne + * @param {String} name - Persistent Store Key + * @param {Array} platforms - Platform Names + * @param {Object} database - Default DataBase + * @return {Object} { Settings, Caches, Configs } + */ +function setENV(name, platforms, database) { + console.log(`☑️ Set Environment Variables`, ""); + let { Settings, Caches, Configs } = getStorage(name, platforms, database); + /***************** Settings *****************/ + if (!Array.isArray(Settings?.Types)) Settings.Types = (Settings.Types) ? [Settings.Types] : []; // 只有一个选项时,无逗号分隔 + console.log(`✅ Set Environment Variables, Settings: ${typeof Settings}, Settings内容: ${JSON.stringify(Settings)}`, ""); + /***************** Caches *****************/ + //console.log(`✅ Set Environment Variables, Caches: ${typeof Caches}, Caches内容: ${JSON.stringify(Caches)}`, ""); + /***************** Configs *****************/ + return { Settings, Caches, Configs }; +} + +/** + * detect Format + * @author VirgilClyne + * @param {Object} url - Parsed URL + * @param {String} body - response body + * @return {String} format - format + */ +function detectFormat(url, body, format = undefined) { + console.log(`☑️ detectFormat, format: ${url.format ?? url.query?.fmt ?? url.query?.format}`, ""); + switch (url.format ?? url.query?.fmt ?? url.query?.format) { + case "txt": + format = "text/plain"; + break; + case "xml": + case "srv3": + case "ttml": + case "ttml2": + case "imsc": + format = "text/xml"; + break; + case "vtt": + case "webvtt": + format = "text/vtt"; + break; + case "json": + case "json3": + format = "application/json"; + break; + case "m3u": + case "m3u8": + format = "application/x-mpegurl"; + break; + case "plist": + format = "application/plist"; + break; + case undefined: + const HEADER = body?.substring?.(0, 6).trim?.(); + //console.log(`🚧 detectFormat, HEADER: ${HEADER}`, ""); + //console.log(`🚧 detectFormat, HEADER?.substring?.(0, 1): ${HEADER?.substring?.(0, 1)}`, ""); + switch (HEADER) { + case " { + const { Settings, Caches, Configs } = setENV("DualSubs", "Netflix", Database$1); + $.log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); + switch (Settings.Switch) { + case true: + default: + // 创建空数据 + let body = {}; + // 格式判断 + switch (FORMAT) { + case undefined: // 视为无body + break; + case "application/x-www-form-urlencoded": + case "text/plain": + default: + break; + case "application/x-mpegURL": + case "application/x-mpegurl": + case "application/vnd.apple.mpegurl": + case "audio/mpegurl": + //body = M3U8.parse($request.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$request.body = M3U8.stringify(body); + break; + case "text/xml": + case "text/html": + case "text/plist": + case "application/xml": + case "application/plist": + case "application/x-plist": + //body = XML.parse($request.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + break; + case "text/vtt": + case "application/vtt": + //body = VTT.parse($request.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$request.body = VTT.stringify(body); + break; + case "text/json": + case "application/json": + if ($request.body.includes("}{")) body = JSON.parse(`[${$request.body.replaceAll('}{','},{')}]`); + else body = JSON.parse($request.body ?? "{}"); + $.log(`🚧 body: ${JSON.stringify(body, null, 2)}`, ""); + // 主机判断 + switch (HOST) { + case "www.netflix.com": + case "logs.netflix.com": + // 路径判断 + switch (PATH) { + case "msl/playapi/cadmium/licensedmanifest": + case "msl/playapi/cadmium/manifest/1": + case "msl/playapi/cadmium/event/1": + case "msl/playapi/cadmium/logblob/1": + case "log/cadmium/logblob/1": + case "nq/msl_v1/cadmium/pbo_manifests/%5E1.0.0/router": + case "nq/msl_v1/cadmium/pbo_licenses/%5E1.0.0/router": + body.forEach(item => { + if (item?.errordata){ + const errordata = atob(item.errordata); + throw new Error(`${errordata.internalcode}: ${errordata.errormsg}`); + } if (item?.entityauthdata) { + const authdata = item.entityauthdata; + $.log(`🚧 authdata: ${JSON.stringify(authdata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.authdata`, authdata); + } if (item?.headerdata){ + const headerdata = JSON.parse(atob(item.headerdata)); + $.log(`🚧 headerdata: ${JSON.stringify(headerdata, null, 2)}`, ""); + if (headerdata.keyrequestdata) { + const keyrequestdata = headerdata.keyrequestdata; + $.log(`🚧 keyrequestdata: ${JSON.stringify(keyrequestdata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.keyrequestdata`, keyrequestdata); + } if (headerdata.sender) { + const sender = headerdata.sender; + $.log(`🚧 sender: ${JSON.stringify(sender, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.sender`, sender); + } if (headerdata.ciphertext) ; } if (item?.mastertoken?.tokendata){ + const tokendata = JSON.parse(atob(item.mastertoken.tokendata)); + $.log(`🚧 tokendata: ${JSON.stringify(tokendata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.tokendata`, tokendata); + } }); + break; + } break; + } if (Array.isArray(body)) $request.body = body.map(item => JSON.stringify(item)).join(""); + else $request.body = JSON.stringify(body); + break; + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + case "application/grpc": + case "application/grpc+proto": + case "application/octet-stream": + break; + } break; + case false: + break; + }})() +.catch((e) => $.logErr(e)) +.finally(() => { + switch ($response) { + default: // 有构造回复数据,返回构造的回复数据 + if ($.isQuanX()) { + if (!$response.status) $response.status = "HTTP/1.1 200 OK"; + delete $response.headers?.["Content-Length"]; + delete $response.headers?.["content-length"]; + delete $response.headers?.["Transfer-Encoding"]; + $.done($response); + } else $.done({ response: $response }); + break; + case undefined: // 无构造回复数据,发送修改的请求数据 + //$.log(`🚧 finally`, `$request: ${JSON.stringify($request, null, 2)}`, ""); + $.done($request); + break; + }}); diff --git a/js/Netflix.response.beta.js b/js/Netflix.response.beta.js new file mode 100644 index 0000000..7208e69 --- /dev/null +++ b/js/Netflix.response.beta.js @@ -0,0 +1,1141 @@ +/* README: https://github.com/DualSubs */ +/* https://www.lodashjs.com */ +class Lodash { + static name = "Lodash"; + static version = "1.2.2"; + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) }; + + static get(object = {}, path = "", defaultValue = undefined) { + // translate array case to dot case, then split with . + // a[0].b -> a.0.b -> ['a', '0', 'b'] + if (!Array.isArray(path)) path = this.toPath(path); + + const result = path.reduce((previousValue, currentValue) => { + return Object(previousValue)[currentValue]; // null undefined get attribute will throwError, Object() can return a object + }, object); + return (result === undefined) ? defaultValue : result; + } + + static set(object = {}, path = "", value) { + if (!Array.isArray(path)) path = this.toPath(path); + path + .slice(0, -1) + .reduce( + (previousValue, currentValue, currentIndex) => + (Object(previousValue[currentValue]) === previousValue[currentValue]) + ? previousValue[currentValue] + : previousValue[currentValue] = (/^\d+$/.test(path[currentIndex + 1]) ? [] : {}), + object + )[path[path.length - 1]] = value; + return object + } + + static unset(object = {}, path = "") { + if (!Array.isArray(path)) path = this.toPath(path); + let result = path.reduce((previousValue, currentValue, currentIndex) => { + if (currentIndex === path.length - 1) { + delete previousValue[currentValue]; + return true + } + return Object(previousValue)[currentValue] + }, object); + return result + } + + static toPath(value) { + return value.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean); + } + + static escape(string) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return string.replace(/[&<>"']/g, m => map[m]) + }; + + static unescape(string) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + }; + return string.replace(/&|<|>|"|'/g, m => map[m]) + } + +} + +/* https://developer.mozilla.org/zh-CN/docs/Web/API/Storage/setItem */ +class $Storage { + static name = "$Storage"; + static version = "1.0.9"; + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) }; + static data = null + static dataFile = 'box.dat' + static #nameRegex = /^@(?[^.]+)(?:\.(?.*))?$/; + + static #platform() { + if ('undefined' !== typeof $environment && $environment['surge-version']) + return 'Surge' + if ('undefined' !== typeof $environment && $environment['stash-version']) + return 'Stash' + if ('undefined' !== typeof module && !!module.exports) return 'Node.js' + if ('undefined' !== typeof $task) return 'Quantumult X' + if ('undefined' !== typeof $loon) return 'Loon' + if ('undefined' !== typeof $rocket) return 'Shadowrocket' + if ('undefined' !== typeof Egern) return 'Egern' + } + + static getItem(keyName = new String, defaultValue = null) { + let keyValue = defaultValue; + // 如果以 @ + switch (keyName.startsWith('@')) { + case true: + const { key, path } = keyName.match(this.#nameRegex)?.groups; + //console.log(`1: ${key}, ${path}`); + keyName = key; + let value = this.getItem(keyName, {}); + //console.log(`2: ${JSON.stringify(value)}`) + if (typeof value !== "object") value = {}; + //console.log(`3: ${JSON.stringify(value)}`) + keyValue = Lodash.get(value, path); + //console.log(`4: ${JSON.stringify(keyValue)}`) + try { + keyValue = JSON.parse(keyValue); + } catch (e) { + // do nothing + } //console.log(`5: ${JSON.stringify(keyValue)}`) + break; + default: + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + keyValue = $persistentStore.read(keyName); + break; + case 'Quantumult X': + keyValue = $prefs.valueForKey(keyName); + break; + case 'Node.js': + this.data = this.#loaddata(this.dataFile); + keyValue = this.data?.[keyName]; + break; + default: + keyValue = this.data?.[keyName] || null; + break; + } try { + keyValue = JSON.parse(keyValue); + } catch (e) { + // do nothing + } break; + } return keyValue ?? defaultValue; + }; + + static setItem(keyName = new String, keyValue = new String) { + let result = false; + //console.log(`0: ${typeof keyValue}`); + switch (typeof keyValue) { + case "object": + keyValue = JSON.stringify(keyValue); + break; + default: + keyValue = String(keyValue); + break; + } switch (keyName.startsWith('@')) { + case true: + const { key, path } = keyName.match(this.#nameRegex)?.groups; + //console.log(`1: ${key}, ${path}`); + keyName = key; + let value = this.getItem(keyName, {}); + //console.log(`2: ${JSON.stringify(value)}`) + if (typeof value !== "object") value = {}; + //console.log(`3: ${JSON.stringify(value)}`) + Lodash.set(value, path, keyValue); + //console.log(`4: ${JSON.stringify(value)}`) + result = this.setItem(keyName, value); + //console.log(`5: ${result}`) + break; + default: + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + result = $persistentStore.write(keyValue, keyName); + break; + case 'Quantumult X': + result =$prefs.setValueForKey(keyValue, keyName); + break; + case 'Node.js': + this.data = this.#loaddata(this.dataFile); + this.data[keyName] = keyValue; + this.#writedata(this.dataFile); + result = true; + break; + default: + result = this.data?.[keyName] || null; + break; + } break; + } return result; + }; + + static removeItem(keyName){ + let result = false; + switch (keyName.startsWith('@')) { + case true: + const { key, path } = keyName.match(this.#nameRegex)?.groups; + keyName = key; + let value = this.getItem(keyName); + if (typeof value !== "object") value = {}; + keyValue = Lodash.unset(value, path); + result = this.setItem(keyName, value); + break; + default: + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + result = false; + break; + case 'Quantumult X': + result = $prefs.removeValueForKey(keyName); + break; + case 'Node.js': + result = false; + break; + default: + result = false; + break; + } break; + } return result; + } + + static clear() { + let result = false; + switch (this.#platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + result = false; + break; + case 'Quantumult X': + result = $prefs.removeAllValues(); + break; + case 'Node.js': + result = false; + break; + default: + result = false; + break; + } return result; + } + + static #loaddata(dataFile) { + if (this.isNode()) { + this.fs = this.fs ? this.fs : require('fs'); + this.path = this.path ? this.path : require('path'); + const curDirDataFilePath = this.path.resolve(dataFile); + const rootDirDataFilePath = this.path.resolve( + process.cwd(), + dataFile + ); + const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath); + const isRootDirDataFile = + !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath); + if (isCurDirDataFile || isRootDirDataFile) { + const datPath = isCurDirDataFile + ? curDirDataFilePath + : rootDirDataFilePath; + try { + return JSON.parse(this.fs.readFileSync(datPath)) + } catch (e) { + return {} + } + } else return {} + } else return {} + } + + static #writedata(dataFile = this.dataFile) { + if (this.isNode()) { + this.fs = this.fs ? this.fs : require('fs'); + this.path = this.path ? this.path : require('path'); + const curDirDataFilePath = this.path.resolve(dataFile); + const rootDirDataFilePath = this.path.resolve( + process.cwd(), + dataFile + ); + const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath); + const isRootDirDataFile = + !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath); + const jsondata = JSON.stringify(this.data); + if (isCurDirDataFile) { + this.fs.writeFileSync(curDirDataFilePath, jsondata); + } else if (isRootDirDataFile) { + this.fs.writeFileSync(rootDirDataFilePath, jsondata); + } else { + this.fs.writeFileSync(curDirDataFilePath, jsondata); + } + } + }; + +} + +class ENV { + static name = "ENV" + static version = '1.8.2' + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) } + + constructor(name, opts) { + console.log(`\n🟧 ${ENV.name} v${ENV.version}\n`); + this.name = name; + this.logs = []; + this.isMute = false; + this.isMuteLog = false; + this.logSeparator = '\n'; + this.encoding = 'utf-8'; + this.startTime = new Date().getTime(); + Object.assign(this, opts); + this.log(`\n🚩 开始!\n${name}\n`); + } + + environment() { + switch (this.platform()) { + case 'Surge': + $environment.app = 'Surge'; + return $environment + case 'Stash': + $environment.app = 'Stash'; + return $environment + case 'Egern': + $environment.app = 'Egern'; + return $environment + case 'Loon': + let environment = $loon.split(' '); + return { + "device": environment[0], + "ios": environment[1], + "loon-version": environment[2], + "app": "Loon" + }; + case 'Quantumult X': + return { + "app": "Quantumult X" + }; + case 'Node.js': + process.env.app = 'Node.js'; + return process.env + default: + return {} + } + } + + platform() { + if ('undefined' !== typeof $environment && $environment['surge-version']) + return 'Surge' + if ('undefined' !== typeof $environment && $environment['stash-version']) + return 'Stash' + if ('undefined' !== typeof module && !!module.exports) return 'Node.js' + if ('undefined' !== typeof $task) return 'Quantumult X' + if ('undefined' !== typeof $loon) return 'Loon' + if ('undefined' !== typeof $rocket) return 'Shadowrocket' + if ('undefined' !== typeof Egern) return 'Egern' + } + + isNode() { + return 'Node.js' === this.platform() + } + + isQuanX() { + return 'Quantumult X' === this.platform() + } + + isSurge() { + return 'Surge' === this.platform() + } + + isLoon() { + return 'Loon' === this.platform() + } + + isShadowrocket() { + return 'Shadowrocket' === this.platform() + } + + isStash() { + return 'Stash' === this.platform() + } + + isEgern() { + return 'Egern' === this.platform() + } + + async getScript(url) { + return await this.fetch(url).then(response => response.body); + } + + async runScript(script, runOpts) { + let httpapi = $Storage.getItem('@chavy_boxjs_userCfgs.httpapi'); + httpapi = httpapi?.replace?.(/\n/g, '')?.trim(); + let httpapi_timeout = $Storage.getItem('@chavy_boxjs_userCfgs.httpapi_timeout'); + httpapi_timeout = (httpapi_timeout * 1) ?? 20; + httpapi_timeout = runOpts?.timeout ?? httpapi_timeout; + const [password, address] = httpapi.split('@'); + const request = { + url: `http://${address}/v1/scripting/evaluate`, + body: { + script_text: script, + mock_type: 'cron', + timeout: httpapi_timeout + }, + headers: { 'X-Key': password, 'Accept': '*/*' }, + timeout: httpapi_timeout + }; + await this.fetch(request).then(response => response.body, error => this.logErr(error)); + } + + initGotEnv(opts) { + this.got = this.got ? this.got : require('got'); + this.cktough = this.cktough ? this.cktough : require('tough-cookie'); + this.ckjar = this.ckjar ? this.ckjar : new this.cktough.CookieJar(); + if (opts) { + opts.headers = opts.headers ? opts.headers : {}; + if (undefined === opts.headers.Cookie && undefined === opts.cookieJar) { + opts.cookieJar = this.ckjar; + } + } + } + + async fetch(request = {} || "", option = {}) { + // 初始化参数 + switch (request.constructor) { + case Object: + request = { ...request, ...option }; + break; + case String: + request = { "url": request, ...option }; + break; + } // 自动判断请求方法 + if (!request.method) { + request.method = "GET"; + if (request.body ?? request.bodyBytes) request.method = "POST"; + } // 移除请求头中的部分参数, 让其自动生成 + delete request.headers?.Host; + delete request.headers?.[":authority"]; + delete request.headers?.['Content-Length']; + delete request.headers?.['content-length']; + // 定义请求方法(小写) + const method = request.method.toLocaleLowerCase(); + // 判断平台 + switch (this.platform()) { + case 'Loon': + case 'Surge': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + default: + // 转换请求参数 + if (request.timeout) { + request.timeout = parseInt(request.timeout, 10); + if (this.isSurge()) ; else request.timeout = request.timeout * 1000; + } if (request.policy) { + if (this.isLoon()) request.node = request.policy; + if (this.isStash()) Lodash.set(request, "headers.X-Stash-Selected-Proxy", encodeURI(request.policy)); + if (this.isShadowrocket()) Lodash.set(request, "headers.X-Surge-Proxy", request.policy); + } if (typeof request.redirection === "boolean") request["auto-redirect"] = request.redirection; + // 转换请求体 + if (request.bodyBytes && !request.body) { + request.body = request.bodyBytes; + delete request.bodyBytes; + } // 发送请求 + return await new Promise((resolve, reject) => { + $httpClient[method](request, (error, response, body) => { + if (error) reject(error); + else { + response.ok = /^2\d\d$/.test(response.status); + response.statusCode = response.status; + if (body) { + response.body = body; + if (request["binary-mode"] == true) response.bodyBytes = body; + } resolve(response); + } + }); + }); + case 'Quantumult X': + // 转换请求参数 + if (request.policy) Lodash.set(request, "opts.policy", request.policy); + if (typeof request["auto-redirect"] === "boolean") Lodash.set(request, "opts.redirection", request["auto-redirect"]); + // 转换请求体 + if (request.body instanceof ArrayBuffer) { + request.bodyBytes = request.body; + delete request.body; + } else if (ArrayBuffer.isView(request.body)) { + request.bodyBytes = request.body.buffer.slice(request.body.byteOffset, request.body.byteLength + request.body.byteOffset); + delete object.body; + } else if (request.body) delete request.bodyBytes; + // 发送请求 + return await $task.fetch(request).then( + response => { + response.ok = /^2\d\d$/.test(response.statusCode); + response.status = response.statusCode; + return response; + }, + reason => Promise.reject(reason.error)); + case 'Node.js': + let iconv = require('iconv-lite'); + this.initGotEnv(request); + const { url, ...option } = request; + return await this.got[method](url, option) + .on('redirect', (response, nextOpts) => { + try { + if (response.headers['set-cookie']) { + const ck = response.headers['set-cookie'] + .map(this.cktough.Cookie.parse) + .toString(); + if (ck) { + this.ckjar.setCookieSync(ck, null); + } + nextOpts.cookieJar = this.ckjar; + } + } catch (e) { + this.logErr(e); + } + // this.ckjar.setCookieSync(response.headers['set-cookie'].map(Cookie.parse).toString()) + }) + .then( + response => { + response.statusCode = response.status; + response.body = iconv.decode(response.rawBody, this.encoding); + response.bodyBytes = response.rawBody; + return response; + }, + error => Promise.reject(error.message)); + } }; + + /** + * + * 示例:$.time('yyyy-MM-dd qq HH:mm:ss.S') + * :$.time('yyyyMMddHHmmssS') + * y:年 M:月 d:日 q:季 H:时 m:分 s:秒 S:毫秒 + * 其中y可选0-4位占位符、S可选0-1位占位符,其余可选0-2位占位符 + * @param {string} format 格式化参数 + * @param {number} ts 可选: 根据指定时间戳返回格式化日期 + * + */ + time(format, ts = null) { + const date = ts ? new Date(ts) : new Date(); + let o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'H+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + 'q+': Math.floor((date.getMonth() + 3) / 3), + 'S': date.getMilliseconds() + }; + if (/(y+)/.test(format)) + format = format.replace( + RegExp.$1, + (date.getFullYear() + '').substr(4 - RegExp.$1.length) + ); + for (let k in o) + if (new RegExp('(' + k + ')').test(format)) + format = format.replace( + RegExp.$1, + RegExp.$1.length == 1 + ? o[k] + : ('00' + o[k]).substr(('' + o[k]).length) + ); + return format + } + + /** + * 系统通知 + * + * > 通知参数: 同时支持 QuanX 和 Loon 两种格式, EnvJs根据运行环境自动转换, Surge 环境不支持多媒体通知 + * + * 示例: + * $.msg(title, subt, desc, 'twitter://') + * $.msg(title, subt, desc, { 'open-url': 'twitter://', 'media-url': 'https://github.githubassets.com/images/modules/open_graph/github-mark.png' }) + * $.msg(title, subt, desc, { 'open-url': 'https://bing.com', 'media-url': 'https://github.githubassets.com/images/modules/open_graph/github-mark.png' }) + * + * @param {*} title 标题 + * @param {*} subt 副标题 + * @param {*} desc 通知详情 + * @param {*} opts 通知参数 + * + */ + msg(title = name, subt = '', desc = '', opts) { + const toEnvOpts = (rawopts) => { + switch (typeof rawopts) { + case undefined: + return rawopts + case 'string': + switch (this.platform()) { + case 'Surge': + case 'Stash': + case 'Egern': + default: + return { url: rawopts } + case 'Loon': + case 'Shadowrocket': + return rawopts + case 'Quantumult X': + return { 'open-url': rawopts } + case 'Node.js': + return undefined + } + case 'object': + switch (this.platform()) { + case 'Surge': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + default: { + let openUrl = + rawopts.url || rawopts.openUrl || rawopts['open-url']; + return { url: openUrl } + } + case 'Loon': { + let openUrl = + rawopts.openUrl || rawopts.url || rawopts['open-url']; + let mediaUrl = rawopts.mediaUrl || rawopts['media-url']; + return { openUrl, mediaUrl } + } + case 'Quantumult X': { + let openUrl = + rawopts['open-url'] || rawopts.url || rawopts.openUrl; + let mediaUrl = rawopts['media-url'] || rawopts.mediaUrl; + let updatePasteboard = + rawopts['update-pasteboard'] || rawopts.updatePasteboard; + return { + 'open-url': openUrl, + 'media-url': mediaUrl, + 'update-pasteboard': updatePasteboard + } + } + case 'Node.js': + return undefined + } + default: + return undefined + } + }; + if (!this.isMute) { + switch (this.platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + default: + $notification.post(title, subt, desc, toEnvOpts(opts)); + break + case 'Quantumult X': + $notify(title, subt, desc, toEnvOpts(opts)); + break + case 'Node.js': + break + } + } + if (!this.isMuteLog) { + let logs = ['', '==============📣系统通知📣==============']; + logs.push(title); + subt ? logs.push(subt) : ''; + desc ? logs.push(desc) : ''; + console.log(logs.join('\n')); + this.logs = this.logs.concat(logs); + } + } + + log(...logs) { + if (logs.length > 0) { + this.logs = [...this.logs, ...logs]; + } + console.log(logs.join(this.logSeparator)); + } + + logErr(error) { + switch (this.platform()) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + case 'Quantumult X': + default: + this.log('', `❗️ ${this.name}, 错误!`, error); + break + case 'Node.js': + this.log('', `❗️${this.name}, 错误!`, error.stack); + break + } + } + + wait(time) { + return new Promise((resolve) => setTimeout(resolve, time)) + } + + done(object = {}) { + const endTime = new Date().getTime(); + const costTime = (endTime - this.startTime) / 1000; + this.log("", `🚩 ${this.name}, 结束! 🕛 ${costTime} 秒`, ""); + switch (this.platform()) { + case 'Surge': + if (object.policy) Lodash.set(object, "headers.X-Surge-Policy", object.policy); + $done(object); + break; + case 'Loon': + if (object.policy) object.node = object.policy; + $done(object); + break; + case 'Stash': + if (object.policy) Lodash.set(object, "headers.X-Stash-Selected-Proxy", encodeURI(object.policy)); + $done(object); + break; + case 'Egern': + $done(object); + break; + case 'Shadowrocket': + default: + $done(object); + break; + case 'Quantumult X': + if (object.policy) Lodash.set(object, "opts.policy", object.policy); + // 移除不可写字段 + delete object["auto-redirect"]; + delete object["auto-cookie"]; + delete object["binary-mode"]; + delete object.charset; + delete object.host; + delete object.insecure; + delete object.method; // 1.4.x 不可写 + delete object.opt; // $task.fetch() 参数, 不可写 + delete object.path; // 可写, 但会与 url 冲突 + delete object.policy; + delete object["policy-descriptor"]; + delete object.scheme; + delete object.sessionIndex; + delete object.statusCode; + delete object.timeout; + if (object.body instanceof ArrayBuffer) { + object.bodyBytes = object.body; + delete object.body; + } else if (ArrayBuffer.isView(object.body)) { + object.bodyBytes = object.body.buffer.slice(object.body.byteOffset, object.body.byteLength + object.body.byteOffset); + delete object.body; + } else if (object.body) delete object.bodyBytes; + $done(object); + break; + case 'Node.js': + process.exit(1); + break; + } + } +} + +class URI { + static name = "URI"; + static version = "1.2.7"; + static about() { return console.log(`\n🟧 ${this.name} v${this.version}\n`) }; + static #json = { scheme: "", host: "", path: "", query: {} }; + + static parse(url) { + const URLRegex = /(?:(?.+):\/\/(?[^/]+))?\/?(?[^?]+)?\??(?[^?]+)?/; + let json = url.match(URLRegex)?.groups ?? null; + if (json?.path) json.paths = json.path.split("/"); else json.path = ""; + //if (json?.paths?.at(-1)?.includes(".")) json.format = json.paths.at(-1).split(".").at(-1); + if (json?.paths) { + const fileName = json.paths[json.paths.length - 1]; + if (fileName?.includes(".")) { + const list = fileName.split("."); + json.format = list[list.length - 1]; + } + } + if (json?.query) json.query = Object.fromEntries(json.query.split("&").map((param) => param.split("="))); + return json + }; + + static stringify(json = this.#json) { + let url = ""; + if (json?.scheme && json?.host) url += json.scheme + "://" + json.host; + if (json?.path) url += (json?.host) ? "/" + json.path : json.path; + if (json?.query) url += "?" + Object.entries(json.query).map(param => param.join("=")).join("&"); + return url + }; +} + +var Settings$1 = { + Switch: true, + Type: "Translate", + Types: [ + "Official", + "Translate" + ], + Languages: [ + "EN", + "ZH" + ], + CacheSize: 50 +}; +var Configs$1 = { + breakLine: { + "text/xml": " ", + "application/xml": " ", + "text/vtt": "\n", + "application/vtt": "\n", + "text/json": "\n", + "application/json": "\n" + } +}; +var Default = { + Settings: Settings$1, + Configs: Configs$1 +}; + +var Default$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + Configs: Configs$1, + Settings: Settings$1, + default: Default +}); + +var Settings = { + Switch: true, + Types: [ + "Translate", + "External" + ], + Languages: [ + "AUTO", + "ZH" + ] +}; +var Configs = { + Languages: { + AR: "ar", + CS: "cs", + DA: "da", + DE: "de", + EN: "en", + "EN-GB": "en-GB", + "EN-US": "en-US", + "EN-US SDH": "en-US SDH", + ES: "es", + "ES-419": "es-419", + "ES-ES": "es-ES", + FI: "fi", + FR: "fr", + HE: "he", + HR: "hr", + HU: "hu", + ID: "id", + IT: "it", + JA: "ja", + KO: "ko", + MS: "ms", + NB: "nb", + NL: "nl", + PL: "pl", + PT: "pt", + "PT-PT": "pt-PT", + "PT-BR": "pt-BR", + RO: "ro", + RU: "ru", + SV: "sv", + TH: "th", + TR: "tr", + UK: "uk", + VI: "vi", + IS: "is", + ZH: "zh", + "ZH-HANS": "zh-Hans", + "ZH-HK": "zh-HK", + "ZH-HANT": "zh-Hant" + } +}; +var Netflix = { + Settings: Settings, + Configs: Configs +}; + +var Netflix$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + Configs: Configs, + Settings: Settings, + default: Netflix +}); + +var Database$1 = Database = { + "Default": Default$1, + "Netflix": Netflix$1, +}; + +/** + * Get Storage Variables + * @link https://github.com/NanoCat-Me/ENV/blob/main/getStorage.mjs + * @author VirgilClyne + * @param {String} key - Persistent Store Key + * @param {Array} names - Platform Names + * @param {Object} database - Default Database + * @return {Object} { Settings, Caches, Configs } + */ +function getStorage(key, names, database) { + //console.log(`☑️ ${this.name}, Get Environment Variables`, ""); + /***************** BoxJs *****************/ + // 包装为局部变量,用完释放内存 + // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 + let BoxJs = $Storage.getItem(key, database); + //console.log(`🚧 ${this.name}, Get Environment Variables`, `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs)}`, ""); + /***************** Argument *****************/ + let Argument = {}; + if (typeof $argument !== "undefined") { + if (Boolean($argument)) { + //console.log(`🎉 ${this.name}, $Argument`); + let arg = Object.fromEntries($argument.split("&").map((item) => item.split("=").map(i => i.replace(/\"/g, '')))); + //console.log(JSON.stringify(arg)); + for (let item in arg) Lodash.set(Argument, item, arg[item]); + //console.log(JSON.stringify(Argument)); + } //console.log(`✅ ${this.name}, Get Environment Variables`, `Argument类型: ${typeof Argument}`, `Argument内容: ${JSON.stringify(Argument)}`, ""); + } /***************** Store *****************/ + const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; + if (!Array.isArray(names)) names = [names]; + //console.log(`🚧 ${this.name}, Get Environment Variables`, `names类型: ${typeof names}`, `names内容: ${JSON.stringify(names)}`, ""); + for (let name of names) { + Store.Settings = { ...Store.Settings, ...database?.[name]?.Settings, ...Argument, ...BoxJs?.[name]?.Settings }; + Store.Configs = { ...Store.Configs, ...database?.[name]?.Configs }; + if (BoxJs?.[name]?.Caches && typeof BoxJs?.[name]?.Caches === "string") BoxJs[name].Caches = JSON.parse(BoxJs?.[name]?.Caches); + Store.Caches = { ...Store.Caches, ...BoxJs?.[name]?.Caches }; + } //console.log(`🚧 ${this.name}, Get Environment Variables`, `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`, ""); + traverseObject(Store.Settings, (key, value) => { + //console.log(`🚧 ${this.name}, traverseObject`, `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`, ""); + if (value === "true" || value === "false") value = JSON.parse(value); // 字符串转Boolean + else if (typeof value === "string") { + if (value.includes(",")) value = value.split(",").map(item => string2number(item)); // 字符串转数组转数字 + else value = string2number(value); // 字符串转数字 + } return value; + }); + //console.log(`✅ ${this.name}, Get Environment Variables`, `Store: ${typeof Store.Caches}`, `Store内容: ${JSON.stringify(Store)}`, ""); + return Store; + + /***************** function *****************/ + function traverseObject(o, c) { for (var t in o) { var n = o[t]; o[t] = "object" == typeof n && null !== n ? traverseObject(n, c) : c(t, n); } return o } + function string2number(string) { if (string && !isNaN(string)) string = parseInt(string, 10); return string } +} + +/** + * Set Environment Variables + * @author VirgilClyne + * @param {String} name - Persistent Store Key + * @param {Array} platforms - Platform Names + * @param {Object} database - Default DataBase + * @return {Object} { Settings, Caches, Configs } + */ +function setENV(name, platforms, database) { + console.log(`☑️ Set Environment Variables`, ""); + let { Settings, Caches, Configs } = getStorage(name, platforms, database); + /***************** Settings *****************/ + if (!Array.isArray(Settings?.Types)) Settings.Types = (Settings.Types) ? [Settings.Types] : []; // 只有一个选项时,无逗号分隔 + console.log(`✅ Set Environment Variables, Settings: ${typeof Settings}, Settings内容: ${JSON.stringify(Settings)}`, ""); + /***************** Caches *****************/ + //console.log(`✅ Set Environment Variables, Caches: ${typeof Caches}, Caches内容: ${JSON.stringify(Caches)}`, ""); + /***************** Configs *****************/ + return { Settings, Caches, Configs }; +} + +/** + * detect Format + * @author VirgilClyne + * @param {Object} url - Parsed URL + * @param {String} body - response body + * @return {String} format - format + */ +function detectFormat(url, body, format = undefined) { + console.log(`☑️ detectFormat, format: ${url.format ?? url.query?.fmt ?? url.query?.format}`, ""); + switch (url.format ?? url.query?.fmt ?? url.query?.format) { + case "txt": + format = "text/plain"; + break; + case "xml": + case "srv3": + case "ttml": + case "ttml2": + case "imsc": + format = "text/xml"; + break; + case "vtt": + case "webvtt": + format = "text/vtt"; + break; + case "json": + case "json3": + format = "application/json"; + break; + case "m3u": + case "m3u8": + format = "application/x-mpegurl"; + break; + case "plist": + format = "application/plist"; + break; + case undefined: + const HEADER = body?.substring?.(0, 6).trim?.(); + //console.log(`🚧 detectFormat, HEADER: ${HEADER}`, ""); + //console.log(`🚧 detectFormat, HEADER?.substring?.(0, 1): ${HEADER?.substring?.(0, 1)}`, ""); + switch (HEADER) { + case " { + const { Settings, Caches, Configs } = setENV("DualSubs", "Netflix", Database$1); + $.log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); + switch (Settings.Switch) { + case true: + default: + // 创建空数据 + let body = {}; + // 格式判断 + switch (FORMAT) { + case undefined: // 视为无body + break; + case "application/x-www-form-urlencoded": + case "text/plain": + default: + break; + case "application/x-mpegURL": + case "application/x-mpegurl": + case "application/vnd.apple.mpegurl": + case "audio/mpegurl": + //body = M3U8.parse($response.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$response.body = M3U8.stringify(body); + break; + case "text/xml": + case "text/html": + case "text/plist": + case "application/xml": + case "application/plist": + case "application/x-plist": + //body = XML.parse($response.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + break; + case "text/vtt": + case "application/vtt": + //body = VTT.parse($response.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$response.body = VTT.stringify(body); + break; + case "text/json": + case "application/json": + if ($response.body.includes("}{")) body = JSON.parse(`[${$response.body.replaceAll('}{','},{')}]`); + else body = JSON.parse($response.body ?? "{}"); + $.log(`🚧 body: ${JSON.stringify(body, null, 2)}`, ""); + // 主机判断 + switch (HOST) { + case "www.netflix.com": + case "logs.netflix.com": + // 路径判断 + switch (PATH) { + case "msl/playapi/cadmium/licensedmanifest": + case "msl/playapi/cadmium/manifest/1": + case "msl/playapi/cadmium/event/1": + case "msl/playapi/cadmium/logblob/1": + case "log/cadmium/logblob/1": + case "nq/msl_v1/cadmium/pbo_manifests/%5E1.0.0/router": + case "nq/msl_v1/cadmium/pbo_licenses/%5E1.0.0/router": + body.forEach(item => { + if (item?.errordata){ + const errordata = atob(item.errordata); + throw new Error(`${errordata.internalcode}: ${errordata.errormsg}`); + } if (item?.entityauthdata) { + const authdata = item.entityauthdata; + $.log(`🚧 authdata: ${JSON.stringify(authdata, null, 2)}`, ""); + //$Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.authdata`, authdata); + } if (item?.headerdata){ + const headerdata = JSON.parse(atob(item.headerdata)); + $.log(`🚧 headerdata: ${JSON.stringify(headerdata, null, 2)}`, ""); + if (headerdata.keyresponsedata) { + const keyresponsedata = headerdata.keyresponsedata; + $.log(`🚧 keyresponsedata: ${JSON.stringify(keyresponsedata, null, 2)}`, ""); + $.log(`🚧 hmacKeyEncStr: ${keyresponsedata.keydata.hmackey}`, ""); + $.log(`🚧 encKeyEncStr: ${keyresponsedata.keydata.encryptionkey}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.keyresponsedata`, keyresponsedata); + } } if (item?.mastertoken?.tokendata){ + const tokendata = JSON.parse(atob(item.mastertoken.tokendata)); + $.log(`🚧 tokendata: ${JSON.stringify(tokendata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.tokendata`, tokendata); + } }); + break; + } break; + } if (Array.isArray(body)) $response.body = body.map(item => JSON.stringify(item)).join(""); + else $response.body = JSON.stringify(body); + break; + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + case "application/grpc": + case "application/grpc+proto": + case "application/octet-stream": + break; + } break; + case false: + break; + }})() + .catch((e) => $.logErr(e)) + .finally(() => $.done($response)); diff --git a/modules/DualSubs.Netflix.beta.sgmodule b/modules/DualSubs.Netflix.beta.sgmodule index fd26a91..a13853d 100644 --- a/modules/DualSubs.Netflix.beta.sgmodule +++ b/modules/DualSubs.Netflix.beta.sgmodule @@ -9,13 +9,21 @@ #!arguments=Switch:true,Languages[0]:AUTO,Languages[1]:ZH,ShowOnly:false #!arguments-desc=Switch: 功能开关\n └ 是否启用此平台修改功能\nLanguages[0]: 主字幕语言(源语言)\n └ 当“主语言”字幕存在时,将生成“主语言/副语言(翻译)”与“主语言(外挂)”的字幕或字幕选项\nLanguages[1]: 副字幕语言(目标语言)\n └ 当“副语言”字幕存在时,将生成“副语言/主语言(官方)”的字幕或字幕选项\nShowOnly: 只显示翻译后字幕 -[Rule] -URL-REGEX,^https?:\/\/(ios|tvos|win10)(-anycast)?\.prod\.(ftl|http1|cloud)\.netflix\.com\/msl\/playapi\/(ios|cadmium)\/(manifest|logblob|event)$,REJECT-DROP - [Script] -DualSubs.Netflix.Manifest.response.json = type=http-response, pattern=^https?:\/\/(ios|tvos|win10)\.prod\.(ftl|http1|cloud)\.netflix\.com\/playapi\/(ios|cadmium)\/manifest$, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/DualSubs.Netflix.Manifest.response.beta.js +# Message Security Layer (MSL) v1 +DualSubs.Netflix.msl_v1.cadmium.request = type=http-request, pattern=^https?:\/\/www\.netflix\.com\/nq\/msl_v1\/cadmium\/(pbo_manifests|pbo_licenses)\/%5E1\.0\.0\/router, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.request.beta.js, argument= +DualSubs.Netflix.msl_v1.cadmium.response = type=http-response, pattern=^https?:\/\/www\.netflix\.com\/nq\/msl_v1\/cadmium\/(pbo_manifests|pbo_licenses)\/%5E1\.0\.0\/router, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.response.beta.js, argument= +DualSubs.Netflix.msl.playapi.cadmium.request = type=http-request, pattern=^https?:\/\/www\.netflix\.com\/msl\/playapi\/cadmium\/(manifest|event|logblob)\/1, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.request.beta.js, argument= +DualSubs.Netflix.msl.playapi.cadmium.response = type=http-response, pattern=^https?:\/\/www\.netflix\.com\/msl\/playapi\/cadmium\/(manifest|event|logblob)\/1, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.response.beta.js, argument= +DualSubs.Netflix.msl.playapi.cadmium.request = type=http-request, pattern=^https?:\/\/logs\.netflix\.com\/log\/cadmium\/logblob\/1, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.request.beta.js, argument= +DualSubs.Netflix.msl.playapi.cadmium.response = type=http-response, pattern=^https?:\/\/logs\.netflix\.com\/log\/cadmium\/logblob\/1, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.response.beta.js, argument= +# DualSubs.Netflix.iosplatform.request = type=http-request, pattern=^https?:\/\/ios\.prod\.(ftl|http1)\.netflix\.com\/nq\/iosplatform\/(pbo_manifest|pbo_license)\/~1\.0\.0\/router, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.request.beta.js, argument= +# DualSubs.Netflix.iosplatform.response = type=http-response, pattern=^https?:\/\/ios\.prod\.(ftl|http1)\.netflix\.com\/nq\/iosplatform\/(pbo_manifest|pbo_license)\/~1\.0\.0\/router, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.response.beta.js, argument= +# DualSubs.Netflix.msl.app.request = type=http-request, pattern=^https?:\/\/(.+)\.prod\.(ftl|http1)\.netflix\.com\/nq(\/msl|msl_v1)?(\/cadmium)?(\/iosplatform)?\/pbo_manifest\/~1\.0\.0\/router, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.request.beta.js, argument= +# DualSubs.Netflix.msl.app.response = type=http-response, pattern=^https?:\/\/(.+)\.prod\.(ftl|http1)\.netflix\.com\/nq(\/msl|msl_v1)?(\/cadmium)?(\/iosplatform)?\/pbo_manifest\/~1\.0\.0\/router, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/Netflix.response.beta.js, argument= +# DualSubs Subtitles Translate DualSubs.Netflix.Translate.Subtitles.response = type=http-response, pattern=^https?:\/\/(.+)\.oca\.nflxvideo\.net\/\?o=\d+&v=\d+&e=\d+&t=.+, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Universal/beta/js/Translate.response.beta.js, argument=Switch={{{Switch}}}&Languages[0]={{{Languages[0]}}}&Languages[1]={{{Languages[1]}}}&ShowOnly={{{ShowOnly}}} [MITM] -hostname = %APPEND% anycast.ftl.netflix.com, oca-api.netflix.com, *.prod.cloud.netflix.com, *.prod.ftl.netflix.com, *.prod.http1.netflix.com, *.oca.nflxvideo.net +hostname = %APPEND% *.netflix.com, *.oca.nflxvideo.net tcp-connection = true diff --git a/modules/archive/DualSubs.Netflix.beta.sgmodule b/modules/archive/DualSubs.Netflix.beta.sgmodule new file mode 100644 index 0000000..40fc31c --- /dev/null +++ b/modules/archive/DualSubs.Netflix.beta.sgmodule @@ -0,0 +1,21 @@ +#!name=🍿️ DualSubs: 🇳 Netflix β +#!desc=(BETA) Netflix字幕增强及双语模块 +#!openUrl=http://boxjs.com/#/app/DualSubs.Netflix.beta +#!author=VirgilClyne +#!homepage=https://github.com/DualSubs +#!manual=https://github.com/DualSubs/Netflix/wiki/🍿-DualSubs:-🇳-Netflix +#!icon=https://github.com/DualSubs/Netflix/raw/main/database/icon_rounded.png +#!category=🍿️ DualSubs +#!arguments=Switch:true,Languages[0]:AUTO,Languages[1]:ZH,ShowOnly:false +#!arguments-desc=Switch: 功能开关\n └ 是否启用此平台修改功能\nLanguages[0]: 主字幕语言(源语言)\n └ 当“主语言”字幕存在时,将生成“主语言/副语言(翻译)”与“主语言(外挂)”的字幕或字幕选项\nLanguages[1]: 副字幕语言(目标语言)\n └ 当“副语言”字幕存在时,将生成“副语言/主语言(官方)”的字幕或字幕选项\nShowOnly: 只显示翻译后字幕 + +[Rule] +URL-REGEX,^https?:\/\/(ios|tvos|win10)(-anycast)?\.prod\.(ftl|http1|cloud)\.netflix\.com\/msl\/playapi\/(ios|cadmium)\/(manifest|logblob|event)$,REJECT-DROP + +[Script] +DualSubs.Netflix.Manifest.response.json = type=http-response, pattern=^https?:\/\/(ios|tvos|win10)\.prod\.(ftl|http1|cloud)\.netflix\.com\/playapi\/(ios|cadmium)\/manifest$, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Netflix/beta/js/DualSubs.Netflix.Manifest.response.beta.js +DualSubs.Netflix.Translate.Subtitles.response = type=http-response, pattern=^https?:\/\/(.+)\.oca\.nflxvideo\.net\/\?o=\d+&v=\d+&e=\d+&t=.+, requires-body=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Universal/beta/js/Translate.response.beta.js, argument=Switch={{{Switch}}}&Languages[0]={{{Languages[0]}}}&Languages[1]={{{Languages[1]}}}&ShowOnly={{{ShowOnly}}} + +[MITM] +hostname = %APPEND% *.netflix.com, *.oca.nflxvideo.net +tcp-connection = true diff --git a/modules/DualSubs.Netflix.test.sgmodule b/modules/archive/DualSubs.Netflix.test.sgmodule similarity index 100% rename from modules/DualSubs.Netflix.test.sgmodule rename to modules/archive/DualSubs.Netflix.test.sgmodule diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..67e6bbd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,620 @@ +{ + "name": "dualsubs-netflix", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dualsubs-netflix", + "version": "1.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^4.9.6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", + "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rollup": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/smob": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", + "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3bff055 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "dualsubs-netflix", + "version": "1.0.0", + "description": "DualSubs: Netflix", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/DualSubs/Netflix.git" + }, + "directories": { + "example": "example" + }, + "scripts": { + "build": "rollup -c", + "test": "rollup --config --configDebug" + }, + "browserslist": [ + "iOS >= 15" + ], + "keywords": [], + "author": "VirgilClyne", + "license": "Apache-2.0", + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^4.9.6" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..9abc81d --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,9 @@ +import defaultConfig from './rollup.default.config.js'; +import debugConfig from './rollup.debug.config.js'; + +export default commandLineArgs => { + if (commandLineArgs.configDebug === true) { + return debugConfig; + } + return defaultConfig; +}; diff --git a/rollup.debug.config.js b/rollup.debug.config.js new file mode 100644 index 0000000..487979f --- /dev/null +++ b/rollup.debug.config.js @@ -0,0 +1,23 @@ +import json from '@rollup/plugin-json'; +import commonjs from "@rollup/plugin-commonjs"; + +export default [ + { + input: 'src/Netflix.request.beta.js', + output: { + file: 'js/Netflix.request.beta.js', + //format: 'es', + banner: '/* README: https://github.com/DualSubs */', + }, + plugins: [json(), commonjs()] + }, + { + input: 'src/Netflix.response.beta.js', + output: { + file: 'js/Netflix.response.beta.js', + //format: 'es', + banner: '/* README: https://github.com/DualSubs */', + }, + plugins: [json(), commonjs()] + }, +]; diff --git a/rollup.default.config.js b/rollup.default.config.js new file mode 100644 index 0000000..4fb0c7b --- /dev/null +++ b/rollup.default.config.js @@ -0,0 +1,15 @@ +import json from '@rollup/plugin-json'; +import commonjs from "@rollup/plugin-commonjs"; +import terser from '@rollup/plugin-terser'; + +export default [ + { + input: 'src/Netflix.response.js', + output: { + file: 'js/Netflix.response.js', + format: 'es', + banner: '/* README: https://github.com/DualSubs */', + }, + plugins: [json(), commonjs(), terser()] + }, +]; diff --git a/src/ENV b/src/ENV new file mode 160000 index 0000000..0c591b6 --- /dev/null +++ b/src/ENV @@ -0,0 +1 @@ +Subproject commit 0c591b60a5b1b1446a0a97cd424fed5eb67dff2e diff --git a/src/Netflix.request.beta.js b/src/Netflix.request.beta.js new file mode 100644 index 0000000..0858ef6 --- /dev/null +++ b/src/Netflix.request.beta.js @@ -0,0 +1,156 @@ +import _ from './ENV/Lodash.mjs' +import $Storage from './ENV/$Storage.mjs' +import ENV from "./ENV/ENV.mjs"; +import URI from "./URI/URI.mjs"; + +import Database from "./database/index.mjs"; +import setENV from "./function/setENV.mjs"; +import detectFormat from "./function/detectFormat.mjs"; + +const $ = new ENV("🍿️ DualSubs: 🇳 Netflix v0.1.0(3) request.beta"); + +// 构造回复数据 +let $response = undefined; + +/***************** Processing *****************/ +// 解构URL +const URL = URI.parse($request.url); +$.log(`⚠ URL: ${JSON.stringify(URL)}`, ""); +// 获取连接参数 +const METHOD = $request.method, HOST = URL.host, PATH = URL.path, PATHs = URL.paths; +$.log(`⚠ METHOD: ${METHOD}`, ""); +// 解析格式 +let FORMAT = ($request.headers?.["Content-Type"] ?? $request.headers?.["content-type"])?.split(";")?.[0]; +if (FORMAT === "application/octet-stream" || FORMAT === "text/plain") FORMAT = detectFormat(URL, $request.body, FORMAT); +$.log(`⚠ FORMAT: ${FORMAT}`, ""); +(async () => { + const { Settings, Caches, Configs } = setENV("DualSubs", "Netflix", Database); + $.log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); + switch (Settings.Switch) { + case true: + default: + // 创建空数据 + let body = {}; + // 格式判断 + switch (FORMAT) { + case undefined: // 视为无body + break; + case "application/x-www-form-urlencoded": + case "text/plain": + default: + break; + case "application/x-mpegURL": + case "application/x-mpegurl": + case "application/vnd.apple.mpegurl": + case "audio/mpegurl": + //body = M3U8.parse($request.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$request.body = M3U8.stringify(body); + break; + case "text/xml": + case "text/html": + case "text/plist": + case "application/xml": + case "application/plist": + case "application/x-plist": + //body = XML.parse($request.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + break; + case "text/vtt": + case "application/vtt": + //body = VTT.parse($request.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$request.body = VTT.stringify(body); + break; + case "text/json": + case "application/json": + if ($request.body.includes("}{")) body = JSON.parse(`[${$request.body.replaceAll('}{','},{')}]`); + else body = JSON.parse($request.body ?? "{}"); + $.log(`🚧 body: ${JSON.stringify(body, null, 2)}`, ""); + // 主机判断 + switch (HOST) { + case "www.netflix.com": + case "logs.netflix.com": + // 路径判断 + switch (PATH) { + case "msl/playapi/cadmium/licensedmanifest": + case "msl/playapi/cadmium/manifest/1": + case "msl/playapi/cadmium/event/1": + case "msl/playapi/cadmium/logblob/1": + case "log/cadmium/logblob/1": + case "nq/msl_v1/cadmium/pbo_manifests/%5E1.0.0/router": + case "nq/msl_v1/cadmium/pbo_licenses/%5E1.0.0/router": + body.forEach(item => { + if (item?.errordata){ + const errordata = atob(item.errordata); + throw new Error(`${errordata.internalcode}: ${errordata.errormsg}`); + }; + if (item?.entityauthdata) { + const authdata = item.entityauthdata; + $.log(`🚧 authdata: ${JSON.stringify(authdata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.authdata`, authdata); + }; + if (item?.headerdata){ + const headerdata = JSON.parse(atob(item.headerdata)); + $.log(`🚧 headerdata: ${JSON.stringify(headerdata, null, 2)}`, ""); + if (headerdata.keyrequestdata) { + const keyrequestdata = headerdata.keyrequestdata; + $.log(`🚧 keyrequestdata: ${JSON.stringify(keyrequestdata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.keyrequestdata`, keyrequestdata); + }; + if (headerdata.sender) { + const sender = headerdata.sender; + $.log(`🚧 sender: ${JSON.stringify(sender, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.sender`, sender); + }; + if (headerdata.ciphertext) {}; + }; + if (item?.mastertoken?.tokendata){ + const tokendata = JSON.parse(atob(item.mastertoken.tokendata)); + $.log(`🚧 tokendata: ${JSON.stringify(tokendata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.tokendata`, tokendata); + }; + }); + break; + }; + break; + default: // 其他主机 + break; + }; + if (Array.isArray(body)) $request.body = body.map(item => JSON.stringify(item)).join(""); + else $request.body = JSON.stringify(body); + break; + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + case "application/grpc": + case "application/grpc+proto": + case "application/octet-stream": + break; + }; + break; + case false: + break; + }; +})() +.catch((e) => $.logErr(e)) +.finally(() => { + switch ($response) { + default: // 有构造回复数据,返回构造的回复数据 + //$.log(`🚧 finally`, `echo $response: ${JSON.stringify($response, null, 2)}`, ""); + if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; + if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; + if ($.isQuanX()) { + if (!$response.status) $response.status = "HTTP/1.1 200 OK"; + delete $response.headers?.["Content-Length"]; + delete $response.headers?.["content-length"]; + delete $response.headers?.["Transfer-Encoding"]; + $.done($response); + } else $.done({ response: $response }); + break; + case undefined: // 无构造回复数据,发送修改的请求数据 + //$.log(`🚧 finally`, `$request: ${JSON.stringify($request, null, 2)}`, ""); + $.done($request); + break; + }; +}) diff --git a/src/Netflix.response.beta.js b/src/Netflix.response.beta.js new file mode 100644 index 0000000..fe4e640 --- /dev/null +++ b/src/Netflix.response.beta.js @@ -0,0 +1,130 @@ +import _ from './ENV/Lodash.mjs' +import $Storage from './ENV/$Storage.mjs' +import ENV from "./ENV/ENV.mjs"; +import URI from "./URI/URI.mjs"; + +import Database from "./database/index.mjs"; +import setENV from "./function/setENV.mjs"; +import detectFormat from "./function/detectFormat.mjs"; + +const $ = new ENV("🍿️ DualSubs: 🇳 Netflix v0.1.0(2) response.beta"); + +/***************** Processing *****************/ +// 解构URL +const URL = URI.parse($request.url); +$.log(`⚠ URL: ${JSON.stringify(URL)}`, ""); +// 获取连接参数 +const METHOD = $request.method, HOST = URL.host, PATH = URL.path, PATHs = URL.paths; +$.log(`⚠ METHOD: ${METHOD}`, ""); +// 解析格式 +let FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; +if (FORMAT === "application/octet-stream" || FORMAT === "text/plain" || FORMAT === undefined) FORMAT = detectFormat(URL, $response.body, FORMAT); +$.log(`⚠ FORMAT: ${FORMAT}`, ""); +(async () => { + const { Settings, Caches, Configs } = setENV("DualSubs", "Netflix", Database); + $.log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); + switch (Settings.Switch) { + case true: + default: + // 创建空数据 + let body = {}; + // 格式判断 + switch (FORMAT) { + case undefined: // 视为无body + break; + case "application/x-www-form-urlencoded": + case "text/plain": + default: + break; + case "application/x-mpegURL": + case "application/x-mpegurl": + case "application/vnd.apple.mpegurl": + case "audio/mpegurl": + //body = M3U8.parse($response.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$response.body = M3U8.stringify(body); + break; + case "text/xml": + case "text/html": + case "text/plist": + case "application/xml": + case "application/plist": + case "application/x-plist": + //body = XML.parse($response.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + break; + case "text/vtt": + case "application/vtt": + //body = VTT.parse($response.body); + //$.log(`🚧 body: ${JSON.stringify(body)}`, ""); + //$response.body = VTT.stringify(body); + break; + case "text/json": + case "application/json": + if ($response.body.includes("}{")) body = JSON.parse(`[${$response.body.replaceAll('}{','},{')}]`); + else body = JSON.parse($response.body ?? "{}"); + $.log(`🚧 body: ${JSON.stringify(body, null, 2)}`, ""); + // 主机判断 + switch (HOST) { + case "www.netflix.com": + case "logs.netflix.com": + // 路径判断 + switch (PATH) { + case "msl/playapi/cadmium/licensedmanifest": + case "msl/playapi/cadmium/manifest/1": + case "msl/playapi/cadmium/event/1": + case "msl/playapi/cadmium/logblob/1": + case "log/cadmium/logblob/1": + case "nq/msl_v1/cadmium/pbo_manifests/%5E1.0.0/router": + case "nq/msl_v1/cadmium/pbo_licenses/%5E1.0.0/router": + body.forEach(item => { + if (item?.errordata){ + const errordata = atob(item.errordata); + throw new Error(`${errordata.internalcode}: ${errordata.errormsg}`); + }; + if (item?.entityauthdata){ + const authdata = item.entityauthdata; + $.log(`🚧 authdata: ${JSON.stringify(authdata, null, 2)}`, ""); + //$Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.authdata`, authdata); + }; + if (item?.headerdata){ + const headerdata = JSON.parse(atob(item.headerdata)); + $.log(`🚧 headerdata: ${JSON.stringify(headerdata, null, 2)}`, ""); + if (headerdata.keyresponsedata) { + const keyresponsedata = headerdata.keyresponsedata; + $.log(`🚧 keyresponsedata: ${JSON.stringify(keyresponsedata, null, 2)}`, ""); + $.log(`🚧 hmacKeyEncStr: ${keyresponsedata.keydata.hmackey}`, ""); + $.log(`🚧 encKeyEncStr: ${keyresponsedata.keydata.encryptionkey}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.keyresponsedata`, keyresponsedata); + }; + }; + if (item?.mastertoken?.tokendata){ + const tokendata = JSON.parse(atob(item.mastertoken.tokendata)); + $.log(`🚧 tokendata: ${JSON.stringify(tokendata, null, 2)}`, ""); + $Storage.setItem(`@DualSubs.${"Netflix"}.Caches.MSL.tokendata`, tokendata); + }; + }); + break; + }; + break; + default: // 其他主机 + break; + }; + if (Array.isArray(body)) $response.body = body.map(item => JSON.stringify(item)).join(""); + else $response.body = JSON.stringify(body); + break; + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + case "application/grpc": + case "application/grpc+proto": + case "application/octet-stream": + break; + }; + break; + case false: + break; + }; +})() + .catch((e) => $.logErr(e)) + .finally(() => $.done($response)) diff --git a/src/URI b/src/URI new file mode 160000 index 0000000..f6ff83d --- /dev/null +++ b/src/URI @@ -0,0 +1 @@ +Subproject commit f6ff83d07e09f90351c1e8c614218069271c5311 diff --git a/src/database/Default.json b/src/database/Default.json new file mode 100644 index 0000000..eb1f3b6 --- /dev/null +++ b/src/database/Default.json @@ -0,0 +1,25 @@ +{ + "Settings": { + "Switch": true, + "Type": "Translate", + "Types": [ + "Official", + "Translate" + ], + "Languages": [ + "EN", + "ZH" + ], + "CacheSize": 50 + }, + "Configs":{ + "breakLine":{ + "text/xml":" ", + "application/xml":" ", + "text/vtt":"\n", + "application/vtt":"\n", + "text/json":"\n", + "application/json":"\n" + } + } +} diff --git a/src/database/Netflix.json b/src/database/Netflix.json new file mode 100644 index 0000000..0d5189c --- /dev/null +++ b/src/database/Netflix.json @@ -0,0 +1,10 @@ +{ + "Settings": { + "Switch": true, + "Types": ["Translate", "External"], + "Languages": ["AUTO", "ZH"] + }, + "Configs": { + "Languages":{"AR":"ar","CS":"cs","DA":"da","DE":"de","EN":"en","EN-GB":"en-GB","EN-US":"en-US","EN-US SDH":"en-US SDH","ES":"es","ES-419":"es-419","ES-ES":"es-ES","FI":"fi","FR":"fr","HE":"he","HR":"hr","HU":"hu","ID":"id","IT":"it","JA":"ja","KO":"ko","MS":"ms","NB":"nb","NL":"nl","PL":"pl","PT":"pt","PT-PT":"pt-PT","PT-BR":"pt-BR","RO":"ro","RU":"ru","SV":"sv","TH":"th","TR":"tr","UK":"uk","VI":"vi","IS":"is","ZH":"zh","ZH-HANS":"zh-Hans","ZH-HK":"zh-HK","ZH-HANT":"zh-Hant"} + } +} diff --git a/src/database/index.mjs b/src/database/index.mjs new file mode 100644 index 0000000..0d4751b --- /dev/null +++ b/src/database/index.mjs @@ -0,0 +1,7 @@ +import * as Default from "./Default.json"; +import * as Netflix from "./Netflix.json"; + +export default Database = { + "Default": Default, + "Netflix": Netflix, +}; diff --git a/src/function/detectFormat.mjs b/src/function/detectFormat.mjs new file mode 100644 index 0000000..6a526b9 --- /dev/null +++ b/src/function/detectFormat.mjs @@ -0,0 +1,77 @@ +/** + * detect Format + * @author VirgilClyne + * @param {Object} url - Parsed URL + * @param {String} body - response body + * @return {String} format - format + */ +export default function detectFormat(url, body, format = undefined) { + console.log(`☑️ detectFormat, format: ${url.format ?? url.query?.fmt ?? url.query?.format}`, ""); + switch (url.format ?? url.query?.fmt ?? url.query?.format) { + case "txt": + format = "text/plain"; + break; + case "xml": + case "srv3": + case "ttml": + case "ttml2": + case "imsc": + format = "text/xml"; + break; + case "vtt": + case "webvtt": + format = "text/vtt"; + break; + case "json": + case "json3": + format = "application/json"; + break; + case "m3u": + case "m3u8": + format = "application/x-mpegurl"; + break; + case "plist": + format = "application/plist"; + break; + case undefined: + const HEADER = body?.substring?.(0, 6).trim?.(); + //console.log(`🚧 detectFormat, HEADER: ${HEADER}`, ""); + //console.log(`🚧 detectFormat, HEADER?.substring?.(0, 1): ${HEADER?.substring?.(0, 1)}`, ""); + switch (HEADER) { + case "