diff --git a/.gitmodules b/.gitmodules index 6ec9c63..8fd781e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "src/utils"] - path = src/utils - url = https://github.com/NanoCat-Me/utils.git [submodule "src/protobuf"] path = src/protobuf url = https://github.com/DualSubs/protobuf.git diff --git a/arguments-builder.config.ts b/arguments-builder.config.ts index d2ba06a..998ad40 100644 --- a/arguments-builder.config.ts +++ b/arguments-builder.config.ts @@ -32,14 +32,6 @@ export default defineConfig({ }, }, args: [ - { - key: "Switch", - name: "总功能开关", - defaultValue: true, - type: "boolean", - description: "是否启用此APP修改", - exclude: ["surge", "loon"], - }, { key: "Type", name: "[字幕] 启用类型", diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..37d3d39 --- /dev/null +++ b/biome.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "files": { + "ignore": [ + "**/*.bundle.js" + ], + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 320 + }, + "javascript": { + "formatter": { + "arrowParentheses": "asNeeded", + "bracketSameLine": true, + "quoteStyle": "double" + } + }, + "json": { + "parser": { + "allowComments": true, + "allowTrailingCommas": true + } + }, + "linter": { + "enabled": true, + "rules": { + "complexity": { + "noForEach": "off", + "noStaticOnlyClass": "off", + "noUselessSwitchCase": "off", + "useArrowFunction": "info", + "useFlatMap": "off", + "useLiteralKeys": "info" + }, + "correctness": { + "noInnerDeclarations": "info", + "noSelfAssign": "off", + "noSwitchDeclarations": "info", + "noUnsafeOptionalChaining": "info" + }, + "performance": { + "noDelete": "info" + }, + "recommended": true, + "style": { + "noNegationElse": "off", + "noParameterAssign": "off", + "noUselessElse": "off", + "noVar": "info", + "useDefaultParameterLast": "info", + "useForOf": "error", + "useNodejsImportProtocol": "error", + "useNumberNamespace": "error", + "useSingleVarDeclarator": "off" + }, + "suspicious": { + "noAssignInExpressions": "info", + "noDoubleEquals": "info", + "noFallthroughSwitchClause": "info", + "noGlobalIsNan": "off", + "useDefaultSwitchClauseLast": "off" + } + } + }, + "organizeImports": { + "enabled": true + }, + "vcs": { + "clientKind": "git", + "enabled": true, + "useIgnoreFile": true + } +} diff --git a/rspack.config.js b/rspack.config.js index 1bd6ec4..0779330 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -15,6 +15,10 @@ export default defineConfig({ new NodePolyfillPlugin({ //additionalAliases: ['console'], }), + new rspack.BannerPlugin({ + banner: `console.log('Date: ${new Date().toLocaleString("zh-CN", { timeZone: "PRC" })}');`, + raw: true, + }), new rspack.BannerPlugin({ banner: `console.log('Version: ${pkg.version}');`, raw: true, @@ -24,13 +28,14 @@ export default defineConfig({ raw: true, }), new rspack.BannerPlugin({ - banner: "console.log('🍿️ DualSubs: ▶️ YouTube');", + banner: `console.log('${pkg.displayName}');`, raw: true, }), new rspack.BannerPlugin({ - banner: "https://DualSubs.github.io", + banner: pkg.homepage, }), ], + devtool: false, performance: false, module: { rules: [ diff --git a/rspack.dev.config.js b/rspack.dev.config.js index 0d14c1d..58702b1 100644 --- a/rspack.dev.config.js +++ b/rspack.dev.config.js @@ -15,6 +15,10 @@ export default defineConfig({ new NodePolyfillPlugin({ //additionalAliases: ['console'], }), + new rspack.BannerPlugin({ + banner: `console.log('Date: ${new Date().toLocaleString("zh-CN", { timeZone: "PRC" })}');`, + raw: true, + }), new rspack.BannerPlugin({ banner: `console.log('Version: ${pkg.version}');`, raw: true, @@ -24,13 +28,14 @@ export default defineConfig({ raw: true, }), new rspack.BannerPlugin({ - banner: "console.log('🍿️ DualSubs: ▶️ YouTube β');", + banner: `console.log('${pkg.displayName}');`, raw: true, }), new rspack.BannerPlugin({ - banner: "https://DualSubs.github.io", + banner: pkg.homepage, }), ], + devtool: false, performance: false, module: { rules: [ diff --git a/src/function/database.mjs b/src/function/database.mjs index 6e69147..a55a66b 100644 --- a/src/function/database.mjs +++ b/src/function/database.mjs @@ -1,7 +1,6 @@ export default { Default: { Settings: { - Switch: true, Type: "Translate", Types: ["Official", "Translate"], Languages: ["EN", "ZH"], @@ -20,7 +19,6 @@ export default { }, YouTube: { Settings: { - Switch: true, Type: "Official", Types: ["Translate", "External"], Languages: ["AUTO", "ZH"], diff --git a/src/function/setCache.mjs b/src/function/setCache.mjs index 5e31ac1..0db4888 100644 --- a/src/function/setCache.mjs +++ b/src/function/setCache.mjs @@ -1,4 +1,5 @@ -import { log } from "@nsnanocat/util"; +import { Console } from "@nsnanocat/util"; + /** * Set Cache * @author VirgilClyne @@ -7,9 +8,9 @@ import { log } from "@nsnanocat/util"; * @return {Boolean} isSaved */ export default function setCache(cache, cacheSize = 100) { - log(`☑️ Set Cache, cacheSize: ${cacheSize}`, ""); + Console.log("☑️ Set Cache", `cacheSize: ${cacheSize}`); cache = Array.from(cache || []); // Map转Array cache = cache.slice(-cacheSize); // 限制缓存大小 - log(`✅ Set Cache`, ""); + Console.log("✅ Set Cache"); return cache; }; diff --git a/src/function/setCaptions.mjs b/src/function/setCaptions.mjs index 887abb7..e005e3d 100644 --- a/src/function/setCaptions.mjs +++ b/src/function/setCaptions.mjs @@ -1,10 +1,10 @@ -import { log } from "@nsnanocat/util"; +import { Console } from "@nsnanocat/util"; export default function setCaptions(captions, translationLanguages) { - log(`☑️ Set Captions`); + Console.log("☑️ Set Captions"); // 有播放器字幕列表渲染器 if (captions?.playerCaptionsTracklistRenderer) { - log(`⚠ Tracklist`); + Console.info("Tracklist"); if (captions?.playerCaptionsTracklistRenderer?.captionTracks) { // 改字幕可用性 captions.playerCaptionsTracklistRenderer.captionTracks = captions?.playerCaptionsTracklistRenderer.captionTracks.map(caption => { @@ -38,6 +38,6 @@ export default function setCaptions(captions, translationLanguages) { captions.playerCaptionsTracklistRenderer.defaultCaptionTrackIndex = 0; }; }; - log(`✅ Set Captions, `); + Console.log("✅ Set Captions"); return captions; }; diff --git a/src/function/setENV.mjs b/src/function/setENV.mjs index 6122d5a..0b964d6 100644 --- a/src/function/setENV.mjs +++ b/src/function/setENV.mjs @@ -1,4 +1,4 @@ -import { getStorage, Lodash as _, log } from "@nsnanocat/util"; +import { Console, getStorage, Lodash as _ } from "@nsnanocat/util"; /** * Set Environment Variables @@ -10,13 +10,13 @@ import { getStorage, Lodash as _, log } from "@nsnanocat/util"; * @return {Object} { Settings, Caches, Configs } */ export default function setENV(name, platforms, database) { - log(`☑️ Set Environment Variables`, ""); - let { Settings, Caches, Configs } = getStorage(name, platforms, database); + Console.log("☑️ Set Environment Variables"); + const { Settings, Caches, Configs } = getStorage(name, platforms, database); /***************** Settings *****************/ if (!Array.isArray(Settings?.Types)) Settings.Types = (Settings.Types) ? [Settings.Types] : []; // 只有一个选项时,无逗号分隔 - log(`✅ Set Environment Variables, Settings: ${typeof Settings}, Settings内容: ${JSON.stringify(Settings)}`, ""); + Console.debug(`typeof Settings: ${typeof Settings}`, `Settings: ${JSON.stringify(Settings)}`); /***************** Caches *****************/ - //log(`✅ Set Environment Variables, Caches: ${typeof Caches}, Caches内容: ${JSON.stringify(Caches)}`, ""); + //Console.debug(`typeof Caches: ${typeof Caches}`, `Caches: ${JSON.stringify(Caches)}`); if (typeof Caches?.Playlists !== "object" || Array.isArray(Caches?.Playlists)) Caches.Playlists = {}; // 创建Playlists缓存 Caches.Playlists.Master = new Map(JSON.parse(Caches?.Playlists?.Master || "[]")); // Strings转Array转Map Caches.Playlists.Subtitle = new Map(JSON.parse(Caches?.Playlists?.Subtitle || "[]")); // Strings转Array转Map @@ -24,5 +24,6 @@ export default function setENV(name, platforms, database) { if (typeof Caches?.Metadatas !== "object" || Array.isArray(Caches?.Metadatas)) Caches.Metadatas = {}; // 创建Playlists缓存 if (typeof Caches?.Metadatas?.Tracks !== "object") Caches.Metadatas.Tracks = new Map(JSON.parse(Caches?.Metadatas?.Tracks || "[]")); // Strings转Array转Map /***************** Configs *****************/ + Console.log("✅ Set Environment Variables"); return { Settings, Caches, Configs }; }; diff --git a/src/request.dev.js b/src/request.dev.js index 25016b2..6fa23b0 100644 --- a/src/request.dev.js +++ b/src/request.dev.js @@ -1,4 +1,5 @@ -import { $platform, Lodash as _, Storage, fetch, notification, log, logError, wait, done, getScript, runScript } from "@nsnanocat/util"; +import { $app, Console, done, fetch, Lodash as _, notification, Storage, wait } from "@nsnanocat/util"; +import { URL } from "@nsnanocat/url"; import database from "./function/database.mjs"; import setENV from "./function/setENV.mjs"; import setCache from "./function/setCache.mjs"; @@ -10,281 +11,283 @@ let $response = undefined; /***************** Processing *****************/ // 解构URL const url = new URL($request.url); -log(`⚠ url: ${url.toJSON()}`, ""); +Console.info(`url: ${url.toJSON()}`); // 获取连接参数 -const METHOD = $request.method, HOST = url.hostname, PATH = url.pathname; -log(`⚠ METHOD: ${METHOD}, HOST: ${HOST}, PATH: ${PATH}` , ""); +const PATHs = url.pathname.split("/").filter(Boolean); +Console.info(`PATHs: ${PATHs}`); // 解析格式 const FORMAT = ($request.headers?.["Content-Type"] ?? $request.headers?.["content-type"])?.split(";")?.[0]; -log(`⚠ FORMAT: ${FORMAT}`, ""); +Console.info(`FORMAT: ${FORMAT}`); !(async () => { /** * 设置 * @type {{Settings: import('./types').Settings}} */ const { Settings, Caches, Configs } = setENV("DualSubs", "YouTube", database); - log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); - switch (Settings.Switch) { - case true: - default: - // 获取字幕类型与语言 - const Type = url.searchParams.get("subtype") ?? Settings.Type, Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; - log(`⚠ Type: ${Type}, Languages: ${Languages}`, ""); - // 创建空数据 - let body = {}; - // 方法判断 - switch (METHOD) { - case "POST": - case "PUT": - case "PATCH": - case "DELETE": - // 格式判断 - 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)}`, ""); - //$request.body = XML.stringify(body); + // 获取字幕类型与语言 + const Type = url.searchParams.get("subtype") ?? Settings.Type, + Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; + Console.info(`Type: ${Type}`, `Languages: ${Languages}`); + // 创建空数据 + let body = {}; + // 方法判断 + switch ($request.method) { + case "POST": + case "PUT": + case "PATCH": + // biome-ignore lint/suspicious/noFallthroughSwitchClause: + case "DELETE": + // 格式判断 + 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); + //Console.debug(`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); + //Console.debug(`body: ${JSON.stringify(body)}`); + //$request.body = XML.stringify(body); + break; + case "text/vtt": + case "application/vtt": + //body = VTT.parse($request.body); + //Console.debug(`body: ${JSON.stringify(body)}`); + //$request.body = VTT.stringify(body); + break; + case "text/json": + case "application/json": + body = JSON.parse($request.body ?? "{}"); + switch (url.pathname) { + case "/youtubei/v1/player": + // 找功能 + if (body?.playbackContext) { + // 有播放设置 + Console.info("playbackContext"); + if (body?.playbackContext.contentPlaybackContext) { + // 有播放设置内容 + body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 + } + } break; - case "text/vtt": - case "application/vtt": - //body = VTT.parse($request.body); - //log(`🚧 body: ${JSON.stringify(body)}`, ""); - //$request.body = VTT.stringify(body); + case "/youtubei/v1/browse": + if (body?.browseId?.startsWith?.("MPLYt_")) url.searchParams.set("subtype", "Translate"); break; - case "text/json": - case "application/json": - body = JSON.parse($request.body ?? "{}"); - switch (PATH) { + } + $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": { + //Console.debug(`调试信息`, `$request: ${JSON.stringify($request, null, 2)}`); + let rawBody = $app === "Quantumult X" ? new Uint8Array($request.bodyBytes ?? []) : ($request.body ?? new Uint8Array()); + //Console.debug(`调试信息`, `isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`); + switch (FORMAT) { + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + switch (url.pathname) { case "/youtubei/v1/player": + body = PlayerRequest.fromBinary(rawBody); + Console.debug(`data: ${JSON.stringify(body)}`); // 找功能 - if (body?.playbackContext) { // 有播放设置 - log(`⚠ playbackContext`, ""); - if (body?.playbackContext.contentPlaybackContext) { // 有播放设置内容 - body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 - }; - }; + if (body?.playbackContext) { + // 有播放设置 + Console.info("playbackContext"); + if (body?.playbackContext.contentPlaybackContext) { + // 有播放设置内容 + //body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 + body.playbackContext.contentPlaybackContext.id4 = 1; // + body.playbackContext.contentPlaybackContext.id6 = 1; // + body.playbackContext.contentPlaybackContext.id8 = 1; // + body.playbackContext.contentPlaybackContext.id9 = 1; // + } + } + Console.debug(`data: ${JSON.stringify(body)}`); + rawBody = PlayerRequest.toBinary(body); break; case "/youtubei/v1/browse": - if (body?.browseId?.startsWith?.("MPLYt_")) url.searchParams.set("subtype", "Translate"); - break; - }; - $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": - //log(`🚧 调试信息`, `$request: ${JSON.stringify($request, null, 2)}`, ""); - let rawBody = ($platform === "Quantumult X") ? new Uint8Array($request.bodyBytes ?? []) : $request.body ?? new Uint8Array(); - //log(`🚧 调试信息`, `isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`, ""); - switch (FORMAT) { - case "application/protobuf": - case "application/x-protobuf": - case "application/vnd.google.protobuf": - switch (PATH) { - case "/youtubei/v1/player": - body = PlayerRequest.fromBinary(rawBody); - log(`🚧 调试信息`, `data: ${JSON.stringify(body)}`, ""); - // 找功能 - if (body?.playbackContext) { // 有播放设置 - log(`⚠ playbackContext`, ""); - if (body?.playbackContext.contentPlaybackContext) { // 有播放设置内容 - //body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 - body.playbackContext.contentPlaybackContext.id4 = 1; // - body.playbackContext.contentPlaybackContext.id6 = 1; // - body.playbackContext.contentPlaybackContext.id8 = 1; // - body.playbackContext.contentPlaybackContext.id9 = 1; // - }; - }; - log(`🚧 调试信息`, `data: ${JSON.stringify(body)}`, ""); - rawBody = PlayerRequest.toBinary(body); - break; - case "/youtubei/v1/browse": - body = BrowseRequest.fromBinary(rawBody); - log(`🚧 调试信息`, `data: ${JSON.stringify(body)}`, ""); - if (body?.browseId?.startsWith?.("MPLYt_")) { - /* + body = BrowseRequest.fromBinary(rawBody); + Console.debug(`data: ${JSON.stringify(body)}`); + if (body?.browseId?.startsWith?.("MPLYt_")) { + /* if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); */ - const detectStutus = fetch($request); - //const detectTrack = fetch(_request); - await Promise.allSettled([detectStutus]).then(results => { - /* + const detectStutus = fetch($request); + //const detectTrack = fetch(_request); + await Promise.allSettled([detectStutus]).then(results => { + /* results.forEach((result, i) => { - log(`🚧 调试信息`, `result[${i}]: ${JSON.stringify(result)}`, ""); + Console.debug(`调试信息`, `result[${i}]: ${JSON.stringify(result)}`); }); */ - switch (results[0].status) { - case "fulfilled": - let response = results[0].value; - switch (response?.headers?.["content-encoding"] ?? response?.headers?.["Content-Encoding"]) { - case "identity": - if (parseInt(response?.headers?.["content-length"] ?? response?.headers?.["Content-Length"], 10) > 4000) { - if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); - else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); - } else { - if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); - }; - break; - case "gzip": - break; - case "br": - if (parseInt(response?.headers?.["content-length"] ?? response?.headers?.["Content-Length"], 10) > 2000) { - if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); - else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); - } else { - if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); - }; - break; - case "deflate": - break; - default: - break; - }; + switch (results[0].status) { + case "fulfilled": { + const response = results[0].value; + switch (response?.headers?.["content-encoding"] ?? response?.headers?.["Content-Encoding"]) { + case "identity": + if (Number.parseInt(response?.headers?.["content-length"] ?? response?.headers?.["Content-Length"], 10) > 4000) { + if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); + else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); + } else { + if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); + } break; - case "rejected": - log(`🚧 调试信息`, `detectStutus.reason: ${JSON.stringify(results[0].reason)}`, ""); - if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); + case "gzip": break; - }; - }); - }; - rawBody = BrowseRequest.toBinary(body); - break; - }; - break; - case "application/grpc": - case "application/grpc+proto": + case "br": + if (Number.parseInt(response?.headers?.["content-length"] ?? response?.headers?.["Content-Length"], 10) > 2000) { + if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); + else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); + } else { + if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); + } + break; + case "deflate": + break; + default: + break; + } + break; + } + case "rejected": + Console.debug(`detectStutus.reason: ${JSON.stringify(results[0].reason)}`); + if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); + break; + } + }); + } + rawBody = BrowseRequest.toBinary(body); break; - }; - // 写入二进制数据 - $request.body = rawBody; + } break; - }; - //break; // 不中断,继续处理URL - case "GET": - // 主机判断 - switch (HOST) { - case "www.youtube.com": - case "m.youtube.com": - // 路径判断 - switch (PATH) { - case "/api/timedtext": - const v = url.searchParams.get("v"); - const kind = url.searchParams.get("kind"); - const lang = url.searchParams.get("lang"); - const tlang = url.searchParams.get("tlang"); - log(`⚠ v: ${v}, kind: ${kind}, lang: ${lang}, tlang: ${tlang}`, ""); - if (!tlang) { - log(`⚠ 翻译语言:未指定`, ""); - // 保存原文语言 - if (v && lang) { - Caches.Playlists.Subtitle.set(v, lang); - Caches.Playlists.Subtitle = setCache(Caches?.Playlists.Subtitle, Settings.CacheSize); - Storage.setItem(`@DualSubs.${"Composite"}.Caches.Playlists.Subtitle`, Caches.Playlists.Subtitle); - }; - // 自动翻译字幕 - switch (Settings.AutoCC) { - case true: - default: - log(`⚠ 自动翻译字幕:开启`, ""); - if (Caches.tlang) { - if (Caches.tlang !== lang) url.searchParams.set("tlang", Caches.tlang); - } - break; - case false: - log(`⚠ 自动翻译字幕:关闭`, ""); - break; - }; - }; - if (url.searchParams.get("tlang")) { - log(`⚠ 翻译语言:已指定`, ""); - // 保存目标语言 - Caches.tlang = url.searchParams.get("tlang"); - Storage.setItem(`@DualSubs.${"YouTube"}.Caches.tlang`, Caches.tlang); - // 字幕类型判断 - switch (Settings.Type) { - case "Composite": - case "Official": - default: - log(`⚠ 官方字幕:合成器`, ""); - if (lang?.split?.(/[-_]/)?.[0] === url.searchParams.get("tlang")?.split?.(/[-_]/)?.[0]) Settings.ShowOnly = true; - if (!Settings.ShowOnly) url.searchParams.set("subtype", "Official"); // 官方字幕 - break; - case "Translate": - log(`⚠ 翻译字幕:翻译器`, ""); - url.searchParams.delete("tlang"); - url.searchParams.set("subtype", "Translate"); // 翻译字幕 - /* + case "application/grpc": + case "application/grpc+proto": + break; + } + // 写入二进制数据 + $request.body = rawBody; + break; + } + } + //break; // 不中断,继续处理URL + // biome-ignore lint/suspicious/noFallthroughSwitchClause: + case "GET": + // 主机判断 + switch (url.hostname) { + case "www.youtube.com": + case "m.youtube.com": + // 路径判断 + switch (url.pathname) { + case "/api/timedtext": { + const v = url.searchParams.get("v"); + const kind = url.searchParams.get("kind"); + const lang = url.searchParams.get("lang"); + const tlang = url.searchParams.get("tlang"); + Console.info(`v: ${v}`, `kind: ${kind}`, `lang: ${lang}`, `tlang: ${tlang}`); + if (!tlang) { + Console.info("翻译语言:未指定"); + // 保存原文语言 + if (v && lang) { + Caches.Playlists.Subtitle.set(v, lang); + Caches.Playlists.Subtitle = setCache(Caches?.Playlists.Subtitle, Settings.CacheSize); + Storage.setItem(`@DualSubs.${"Composite"}.Caches.Playlists.Subtitle`, Caches.Playlists.Subtitle); + } + // 自动翻译字幕 + switch (Settings.AutoCC) { + case true: + default: + Console.info("自动翻译字幕:开启"); + if (Caches.tlang) { + if (Caches.tlang !== lang) url.searchParams.set("tlang", Caches.tlang); + } + break; + case false: + Console.info("自动翻译字幕:关闭"); + break; + } + } + if (url.searchParams.get("tlang")) { + Console.info("翻译语言:已指定"); + // 保存目标语言 + Caches.tlang = url.searchParams.get("tlang"); + Storage.setItem(`@DualSubs.${"YouTube"}.Caches.tlang`, Caches.tlang); + // 字幕类型判断 + switch (Settings.Type) { + case "Composite": + case "Official": + default: + Console.info("官方字幕:合成器"); + if (lang?.split?.(/[-_]/)?.[0] === url.searchParams.get("tlang")?.split?.(/[-_]/)?.[0]) Settings.ShowOnly = true; + if (!Settings.ShowOnly) url.searchParams.set("subtype", "Official"); // 官方字幕 + break; + case "Translate": + Console.info("翻译字幕:翻译器"); + url.searchParams.delete("tlang"); + url.searchParams.set("subtype", "Translate"); // 翻译字幕 + /* switch (URL.query?.kind) { // 类型判断 case "asr": - log(`⚠ 自动生成(听译)字幕`, ""); - log(`⚠ 仅支持官方字幕`, ""); + Console.info(`自动生成(听译)字幕`); + Console.info(`仅支持官方字幕`); if (!Settings.ShowOnly) url.searchParams.set("subtype", "Official"); // 官方字幕 break; case "captions": default: - log(`⚠ 普通字幕`, ""); + Console.info(`普通字幕`); url.searchParams.delete("tlang"); url.searchParams.set("subtype", "Translate"); // 翻译字幕 }; */ - break; - case "External": - log(`⚠ 外部字幕:外部源`, ""); - url.searchParams.delete("tlang"); - url.searchParams.set("subtype", "External"); // 外挂字幕 - break; - }; - }; - break; - }; + break; + case "External": + Console.info("外部字幕:外部源"); + url.searchParams.delete("tlang"); + url.searchParams.set("subtype", "External"); // 外挂字幕 + break; + } + } break; - }; - case "HEAD": - case "OPTIONS": - break; - case "CONNECT": - case "TRACE": + } + } break; - }; - $request.url = url.toString(); - log(`🚧 调试信息`, `$request.url: ${$request.url}`, ""); + } + case "HEAD": + case "OPTIONS": break; - case false: + case "CONNECT": + case "TRACE": break; - }; + } + $request.url = url.toString(); + Console.debug(`$request.url: ${$request.url}`); })() - .catch((e) => logError(e)) + .catch(e => Console.error(e)) .finally(() => { switch ($response) { default: // 有构造回复数据,返回构造的回复数据 - //log(`🚧 finally`, `echo $response: ${JSON.stringify($response, null, 2)}`, ""); + //Console.debug(`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"; - switch ($platform) { + switch ($app) { default: done({ response: $response }); break; @@ -295,11 +298,11 @@ log(`⚠ FORMAT: ${FORMAT}`, ""); delete $response.headers?.["Transfer-Encoding"]; done($response); break; - }; + } break; case undefined: // 无构造回复数据,发送修改的请求数据 - //log(`🚧 finally`, `$request: ${JSON.stringify($request, null, 2)}`, ""); + //Console.debug(`finally`, `$request: ${JSON.stringify($request, null, 2)}`); done($request); break; - }; - }) + } + }); diff --git a/src/request.js b/src/request.js index cbbf0b3..78c8c5c 100644 --- a/src/request.js +++ b/src/request.js @@ -1,4 +1,5 @@ -import { $platform, Lodash as _, Storage, fetch, notification, log, logError, wait, done, getScript, runScript } from "@nsnanocat/util"; +import { $app, Console, done, fetch, Lodash as _, notification, Storage, wait } from "@nsnanocat/util"; +import { URL } from "@nsnanocat/url"; import database from "./function/database.mjs"; import setENV from "./function/setENV.mjs"; import setCache from "./function/setCache.mjs"; @@ -6,225 +7,227 @@ import { PlayerRequest } from "./protobuf/google/protos/youtube/api/innertube/Pl import { BrowseRequest } from "./protobuf/google/protos/youtube/api/innertube/BrowseRequest.js"; // 构造回复数据 let $response = undefined; +Console.debug = () => {}; /***************** Processing *****************/ // 解构URL const url = new URL($request.url); -log(`⚠ url: ${url.toJSON()}`, ""); +Console.info(`url: ${url.toJSON()}`); // 获取连接参数 -const METHOD = $request.method, HOST = url.hostname, PATH = url.pathname; -log(`⚠ METHOD: ${METHOD}, HOST: ${HOST}, PATH: ${PATH}` , ""); +const PATHs = url.pathname.split("/").filter(Boolean); +Console.info(`PATHs: ${PATHs}`); // 解析格式 const FORMAT = ($request.headers?.["Content-Type"] ?? $request.headers?.["content-type"])?.split(";")?.[0]; -log(`⚠ FORMAT: ${FORMAT}`, ""); +Console.info(`FORMAT: ${FORMAT}`); !(async () => { /** * 设置 * @type {{Settings: import('./types').Settings}} */ const { Settings, Caches, Configs } = setENV("DualSubs", "YouTube", database); - log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); - switch (Settings.Switch) { - case true: - default: - // 获取字幕类型与语言 - const Type = url.searchParams.get("subtype") ?? Settings.Type, Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; - log(`⚠ Type: ${Type}, Languages: ${Languages}`, ""); - // 创建空数据 - let body = {}; - // 方法判断 - switch (METHOD) { - case "POST": - case "PUT": - case "PATCH": - case "DELETE": - // 格式判断 - 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": - break; - case "text/xml": - case "text/html": - case "text/plist": - case "application/xml": - case "application/plist": - case "application/x-plist": + // 获取字幕类型与语言 + const Type = url.searchParams.get("subtype") ?? Settings.Type, + Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; + Console.info(`Type: ${Type}`, `Languages: ${Languages}`); + // 创建空数据 + let body = {}; + // 方法判断 + switch ($request.method) { + case "POST": + case "PUT": + case "PATCH": + // biome-ignore lint/suspicious/noFallthroughSwitchClause: + case "DELETE": + // 格式判断 + 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": + break; + case "text/xml": + case "text/html": + case "text/plist": + case "application/xml": + case "application/plist": + case "application/x-plist": + break; + case "text/vtt": + case "application/vtt": + break; + case "text/json": + case "application/json": + body = JSON.parse($request.body ?? "{}"); + switch (url.pathname) { + case "/youtubei/v1/player": + // 找功能 + if (body?.playbackContext) { + // 有播放设置 + Console.info("playbackContext"); + if (body?.playbackContext.contentPlaybackContext) { + // 有播放设置内容 + body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 + } + } break; - case "text/vtt": - case "application/vtt": + case "/youtubei/v1/browse": + if (body?.browseId?.startsWith?.("MPLYt_")) url.searchParams.set("subtype", "Translate"); break; - case "text/json": - case "application/json": - body = JSON.parse($request.body ?? "{}"); - switch (PATH) { + } + $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": { + let rawBody = $app === "Quantumult X" ? new Uint8Array($request.bodyBytes ?? []) : ($request.body ?? new Uint8Array()); + switch (FORMAT) { + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + switch (url.pathname) { case "/youtubei/v1/player": + body = PlayerRequest.fromBinary(rawBody); // 找功能 - if (body?.playbackContext) { // 有播放设置 - log(`⚠ playbackContext`, ""); - if (body?.playbackContext.contentPlaybackContext) { // 有播放设置内容 - body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 - }; - }; + if (body?.playbackContext) { + // 有播放设置 + Console.info("playbackContext"); + if (body?.playbackContext.contentPlaybackContext) { + // 有播放设置内容 + //body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 + body.playbackContext.contentPlaybackContext.id4 = 1; // + body.playbackContext.contentPlaybackContext.id6 = 1; // + body.playbackContext.contentPlaybackContext.id8 = 1; // + body.playbackContext.contentPlaybackContext.id9 = 1; // + } + } + rawBody = PlayerRequest.toBinary(body); break; case "/youtubei/v1/browse": - if (body?.browseId?.startsWith?.("MPLYt_")) url.searchParams.set("subtype", "Translate"); + body = BrowseRequest.fromBinary(rawBody); + if (body?.browseId?.startsWith?.("MPLYt_")) { + if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); + else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); + } + rawBody = BrowseRequest.toBinary(body); break; - }; - $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": - let rawBody = ($platform === "Quantumult X") ? new Uint8Array($request.bodyBytes ?? []) : $request.body ?? new Uint8Array(); - switch (FORMAT) { - case "application/protobuf": - case "application/x-protobuf": - case "application/vnd.google.protobuf": - switch (PATH) { - case "/youtubei/v1/player": - body = PlayerRequest.fromBinary(rawBody); - // 找功能 - if (body?.playbackContext) { // 有播放设置 - log(`⚠ playbackContext`, ""); - if (body?.playbackContext.contentPlaybackContext) { // 有播放设置内容 - //body.playbackContext.contentPlaybackContext.autoCaptionsDefaultOn = true; // 默认开启自动字幕 - body.playbackContext.contentPlaybackContext.id4 = 1; // - body.playbackContext.contentPlaybackContext.id6 = 1; // - body.playbackContext.contentPlaybackContext.id8 = 1; // - body.playbackContext.contentPlaybackContext.id9 = 1; // - }; - }; - rawBody = PlayerRequest.toBinary(body); - break; - case "/youtubei/v1/browse": - body = BrowseRequest.fromBinary(rawBody); - if (body?.browseId?.startsWith?.("MPLYt_")) { - if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); - else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); - }; - rawBody = BrowseRequest.toBinary(body); - break; - }; - break; - case "application/grpc": - case "application/grpc+proto": - break; - }; - // 写入二进制数据 - $request.body = rawBody; break; - }; - //break; // 不中断,继续处理URL - case "GET": - // 主机判断 - switch (HOST) { - case "www.youtube.com": - case "m.youtube.com": - // 路径判断 - switch (PATH) { - case "/api/timedtext": - const v = url.searchParams.get("v"); - const kind = url.searchParams.get("kind"); - const lang = url.searchParams.get("lang"); - const tlang = url.searchParams.get("tlang"); - log(`⚠ v: ${v}, kind: ${kind}, lang: ${lang}, tlang: ${tlang}`, ""); - if (!tlang) { - log(`⚠ 翻译语言:未指定`, ""); - // 保存原文语言 - if (v && lang) { - Caches.Playlists.Subtitle.set(v, lang); - Caches.Playlists.Subtitle = setCache(Caches?.Playlists.Subtitle, Settings.CacheSize); - Storage.setItem(`@DualSubs.${"Composite"}.Caches.Playlists.Subtitle`, Caches.Playlists.Subtitle); - }; - // 自动翻译字幕 - switch (Settings.AutoCC) { - case true: - default: - log(`⚠ 自动翻译字幕:开启`, ""); - if (Caches.tlang) { - if (Caches.tlang !== lang) url.searchParams.set("tlang", Caches.tlang); - } - break; - case false: - log(`⚠ 自动翻译字幕:关闭`, ""); - break; - }; - }; - if (url.searchParams.get("tlang")) { - log(`⚠ 翻译语言:已指定`, ""); - // 保存目标语言 - Caches.tlang = url.searchParams.get("tlang"); - Storage.setItem(`@DualSubs.${"YouTube"}.Caches.tlang`, Caches.tlang); - // 字幕类型判断 - switch (Settings.Type) { - case "Composite": - case "Official": - default: - log(`⚠ 官方字幕:合成器`, ""); - if (lang?.split?.(/[-_]/)?.[0] === url.searchParams.get("tlang")?.split?.(/[-_]/)?.[0]) Settings.ShowOnly = true; - if (!Settings.ShowOnly) url.searchParams.set("subtype", "Official"); // 官方字幕 - break; - case "Translate": - log(`⚠ 翻译字幕:翻译器`, ""); - url.searchParams.delete("tlang"); - url.searchParams.set("subtype", "Translate"); // 翻译字幕 - break; - case "External": - log(`⚠ 外部字幕:外部源`, ""); - url.searchParams.delete("tlang"); - url.searchParams.set("subtype", "External"); // 外挂字幕 - break; - }; - }; - break; - }; - break; - }; - case "HEAD": - case "OPTIONS": + } + // 写入二进制数据 + $request.body = rawBody; break; - case "CONNECT": - case "TRACE": + } + } + //break; // 不中断,继续处理URL + // biome-ignore lint/suspicious/noFallthroughSwitchClause: + case "GET": + // 主机判断 + switch (url.hostname) { + case "www.youtube.com": + case "m.youtube.com": + // 路径判断 + switch (url.pathname) { + case "/api/timedtext": { + const v = url.searchParams.get("v"); + const kind = url.searchParams.get("kind"); + const lang = url.searchParams.get("lang"); + const tlang = url.searchParams.get("tlang"); + Console.info(`v: ${v}`, `kind: ${kind}`, `lang: ${lang}`, `tlang: ${tlang}`); + if (!tlang) { + Console.info("翻译语言:未指定"); + // 保存原文语言 + if (v && lang) { + Caches.Playlists.Subtitle.set(v, lang); + Caches.Playlists.Subtitle = setCache(Caches?.Playlists.Subtitle, Settings.CacheSize); + Storage.setItem(`@DualSubs.${"Composite"}.Caches.Playlists.Subtitle`, Caches.Playlists.Subtitle); + } + // 自动翻译字幕 + switch (Settings.AutoCC) { + case true: + default: + Console.info("自动翻译字幕:开启"); + if (Caches.tlang) { + if (Caches.tlang !== lang) url.searchParams.set("tlang", Caches.tlang); + } + break; + case false: + Console.info("自动翻译字幕:关闭"); + break; + } + } + if (url.searchParams.get("tlang")) { + Console.info("翻译语言:已指定"); + // 保存目标语言 + Caches.tlang = url.searchParams.get("tlang"); + Storage.setItem(`@DualSubs.${"YouTube"}.Caches.tlang`, Caches.tlang); + // 字幕类型判断 + switch (Settings.Type) { + case "Composite": + case "Official": + default: + Console.info("官方字幕:合成器"); + if (lang?.split?.(/[-_]/)?.[0] === url.searchParams.get("tlang")?.split?.(/[-_]/)?.[0]) Settings.ShowOnly = true; + if (!Settings.ShowOnly) url.searchParams.set("subtype", "Official"); // 官方字幕 + break; + case "Translate": + Console.info("翻译字幕:翻译器"); + url.searchParams.delete("tlang"); + url.searchParams.set("subtype", "Translate"); // 翻译字幕 + break; + case "External": + Console.info("外部字幕:外部源"); + url.searchParams.delete("tlang"); + url.searchParams.set("subtype", "External"); // 外挂字幕 + break; + } + } + break; + } + } break; - }; - $request.url = url.toString(); - log(`🚧 调试信息`, `$request.url: ${$request.url}`, ""); + } + case "HEAD": + case "OPTIONS": break; - case false: + case "CONNECT": + case "TRACE": break; - }; + } + $request.url = url.toString(); + Console.debug(`$request.url: ${$request.url}`); })() - .catch((e) => logError(e)) + .catch(e => Console.error(e)) .finally(() => { switch ($response) { default: // 有构造回复数据,返回构造的回复数据 - if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; - if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; - switch ($platform) { - default: - done({ response: $response }); - break; - case "Quantumult X": - 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); - break; - }; + if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; + if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; + switch ($app) { + default: + done({ response: $response }); + break; + case "Quantumult X": + 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); + break; + } break; case undefined: // 无构造回复数据,发送修改的请求数据 done($request); break; - }; - }) + } + }); diff --git a/src/response.dev.js b/src/response.dev.js index 4703aab..7544f3b 100644 --- a/src/response.dev.js +++ b/src/response.dev.js @@ -1,197 +1,158 @@ -import { $platform, Lodash as _, Storage, fetch, notification, log, logError, wait, done, getScript, runScript } from "@nsnanocat/util"; +import { $app, Console, done, fetch, Lodash as _, notification, Storage, wait } from "@nsnanocat/util"; +import { URL } from "@nsnanocat/url"; import database from "./function/database.mjs"; import setENV from "./function/setENV.mjs"; import setCache from "./function/setCache.mjs"; +import setCaptions from "./function/setCaptions.mjs"; import { GetWatchResponse } from "./protobuf/google/protos/youtube/api/innertube/GetWatchResponse.js"; import { PlayerResponse } from "./protobuf/google/protos/youtube/api/innertube/PlayerResponse.js"; import { WireType, UnknownFieldHandler, reflectionMergePartial, MESSAGE_TYPE, MessageType, BinaryReader, isJsonObject, typeofJsonValue, jsonWriteOptions } from "@protobuf-ts/runtime"; /***************** Processing *****************/ // 解构URL const url = new URL($request.url); -log(`⚠ url: ${url.toJSON()}`, ""); +Console.info(`url: ${url.toJSON()}`); // 获取连接参数 -const METHOD = $request.method, HOST = url.hostname, PATH = url.pathname; -log(`⚠ METHOD: ${METHOD}, HOST: ${HOST}, PATH: ${PATH}`, ""); +const PATHs = url.pathname.split("/").filter(Boolean); +Console.info(`PATHs: ${PATHs}`); // 解析格式 const FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; -log(`⚠ FORMAT: ${FORMAT}`, ""); +Console.info(`FORMAT: ${FORMAT}`); !(async () => { /** * 设置 * @type {{Settings: import('./types').Settings}} */ const { Settings, Caches, Configs } = setENV("DualSubs", "YouTube", database); - log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); - switch (Settings.Switch) { - case true: + // 获取字幕类型与语言 + const Type = url.searchParams.get("subtype") ?? Settings.Type, + Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; + Console.info(`Type: ${Type}`, `Languages: ${Languages}`); + // 创建空数据 + let body = { + captions: { + playerCaptionsTracklistRenderer: { + captionTracks: [], + audioTracks: [], + translationLanguages: [], + }, + }, + }; + // 格式判断 + switch (FORMAT) { + case undefined: // 视为无body + break; + case "application/x-www-form-urlencoded": + case "text/plain": default: - // 获取字幕类型与语言 - const Type = url.searchParams.get("subtype") ?? Settings.Type, Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; - log(`⚠ Type: ${Type}, Languages: ${Languages}`, ""); - // 创建空数据 - let body = { "captions": { "playerCaptionsTracklistRenderer": { "captionTracks": [], "audioTracks": [], "translationLanguages": [] } } }; - // 格式判断 - 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)}`, ""); - //$response.body = XML.stringify(body); + break; + case "application/x-mpegURL": + case "application/x-mpegurl": + case "application/vnd.apple.mpegurl": + case "audio/mpegurl": + //body = M3U8.parse($response.body); + //Console.debug(`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); + //Console.debug(`body: ${JSON.stringify(body)}`); + //$response.body = XML.stringify(body); + break; + case "text/vtt": + case "application/vtt": + //body = VTT.parse($response.body); + //Console.debug(`body: ${JSON.stringify(body)}`); + //$response.body = VTT.stringify(body); + break; + case "text/json": + case "application/json": + body = JSON.parse($response.body ?? "{}"); + switch (url.pathname) { + case "/youtubei/v1/player": + if (body?.captions) body.captions = setCaptions(body.captions, Configs); break; - case "text/vtt": - case "application/vtt": - //body = VTT.parse($response.body); - //log(`🚧 body: ${JSON.stringify(body)}`, ""); - //$response.body = VTT.stringify(body); + case "/youtubei/v1/browse": break; - case "text/json": - case "application/json": - body = JSON.parse($response.body ?? "{}"); - switch (PATH) { + } + $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": { + //Console.debug(`$response: ${JSON.stringify($response, null, 2)}`); + let rawBody = $app === "Quantumult X" ? new Uint8Array($response.bodyBytes ?? []) : ($response.body ?? new Uint8Array()); + //Console.debug(`isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`); + switch (FORMAT) { + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + switch (url.pathname) { + case "/youtubei/v1/get_watch": { + /****************** initialization start *******************/ + class get_watch_response$Type extends MessageType { + constructor() { + super("get_watch_response", [ + { + no: 1, + name: "contents", + kind: "message", + repeat: 1 /*RepeatType.PACKED*/, + T: () => GetWatchResponse, + }, + ]); + } + } + const get_watch_response = new get_watch_response$Type(); + /****************** initialization finish *******************/ + body = get_watch_response.fromBinary(rawBody); + Console.debug(`body: ${JSON.stringify(body)}`); + if (body?.contents?.[0]?.playerResponse?.captions) body.contents[0].playerResponse.captions = setCaptions(body.contents[0].playerResponse.captions, Configs); + Console.debug(`body: ${JSON.stringify(body)}`); + rawBody = get_watch_response.toBinary(body); + break; + } case "/youtubei/v1/player": - if (body?.captions) body.captions = captions(body.captions, Configs); + body = PlayerResponse.fromBinary(rawBody); + Console.debug(`body: ${JSON.stringify(body)}`); + /* + let UF = UnknownFieldHandler.list(body?.streamingData?.adaptiveFormats[body?.streamingData?.adaptiveFormats?.length - 2]); + Console.debug(`UF: ${JSON.stringify(UF)}`); + if (UF) { + UF = UF.map(uf => { + //uf.no; // 22 + //uf.wireType; // WireType.Varint + // use the binary reader to decode the raw data: + let reader = new BinaryReader(uf.data); + let addedNumber = reader.int32(); // 7777 + Console.debug(`no: ${uf.no}, wireType: ${uf.wireType}, reader: ${reader}, addedNumber: ${addedNumber}`); + }); + }; + */ + if (body?.captions) body.captions = setCaptions(body.captions, Configs); + Console.debug(`body: ${JSON.stringify(body)}`); + rawBody = PlayerResponse.toBinary(body); break; case "/youtubei/v1/browse": break; - }; - $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": - //log(`🚧 $response: ${JSON.stringify($response, null, 2)}`, ""); - let rawBody = ($platform === "Quantumult X") ? new Uint8Array($response.bodyBytes ?? []) : $response.body ?? new Uint8Array(); - //log(`🚧 isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`, ""); - switch (FORMAT) { - case "application/protobuf": - case "application/x-protobuf": - case "application/vnd.google.protobuf": - switch (PATH) { - case "/youtubei/v1/get_watch": - /****************** initialization start *******************/ - class get_watch_response$Type extends MessageType { - constructor() { - super("get_watch_response", [ - { no: 1, name: "contents", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => GetWatchResponse } - ]); - } - } - const get_watch_response = new get_watch_response$Type(); - /****************** initialization finish *******************/ - body = get_watch_response.fromBinary(rawBody); - log(`🚧 body: ${JSON.stringify(body)}`, ""); - if (body?.contents?.[0]?.playerResponse?.captions) body.contents[0].playerResponse.captions = captions(body.contents[0].playerResponse.captions, Configs); - log(`🚧 body: ${JSON.stringify(body)}`, ""); - rawBody = get_watch_response.toBinary(body); - break; - case "/youtubei/v1/player": - body = PlayerResponse.fromBinary(rawBody); - log(`🚧 body: ${JSON.stringify(body)}`, ""); - /* - let UF = UnknownFieldHandler.list(body?.streamingData?.adaptiveFormats[body?.streamingData?.adaptiveFormats?.length - 2]); - log(`🚧 UF: ${JSON.stringify(UF)}`, ""); - if (UF) { - UF = UF.map(uf => { - //uf.no; // 22 - //uf.wireType; // WireType.Varint - // use the binary reader to decode the raw data: - let reader = new BinaryReader(uf.data); - let addedNumber = reader.int32(); // 7777 - log(`🚧 no: ${uf.no}, wireType: ${uf.wireType}, reader: ${reader}, addedNumber: ${addedNumber}`, ""); - }); - }; - */ - if (body?.captions) body.captions = captions(body.captions, Configs); - log(`🚧 body: ${JSON.stringify(body)}`, ""); - rawBody = PlayerResponse.toBinary(body); - break; - case "/youtubei/v1/browse": - break; - }; - break; - case "application/grpc": - case "application/grpc+proto": - break; - }; - // 写入二进制数据 - $response.body = rawBody; break; - }; + } + // 写入二进制数据 + $response.body = rawBody; break; - case false: - break; - }; -})() - .catch((e) => logError(e)) - .finally(() => done($response)) - -/***************** Function *****************/ -function captions(captions, configs) { - console.log(`☑️ Captions`, ""); - if (captions) { // 有基础字幕 - log(`⚠ Captions`, ""); - if (captions?.playerCaptionsRenderer) { // 有播放器字幕渲染器 - log(`⚠ playerCaptionsRenderer`, ""); - captions.playerCaptionsRenderer.visibility = "ON" // 字幕选项按钮可见 - captions.playerCaptionsRenderer.showAutoCaptions = true; // 包含自动生成的字幕 } - if (captions?.playerCaptionsTracklistRenderer) { // 有播放器字幕列表渲染器 - log(`⚠ playerCaptionsTracklistRenderer`, ""); - if (captions?.playerCaptionsTracklistRenderer?.captionTracks) { - // 改字幕可用性 - captions.playerCaptionsTracklistRenderer.captionTracks = captions?.playerCaptionsTracklistRenderer.captionTracks.map(captionTrack => { - captionTrack.isTranslatable = true; - return captionTrack; - }); - }; - if (captions?.playerCaptionsTracklistRenderer?.audioTracks) { - // 改音轨可用性 - captions.playerCaptionsTracklistRenderer.audioTracks = captions?.playerCaptionsTracklistRenderer.audioTracks.map(audioTrack => { - audioTrack.visibility = 2; //"ON" - audioTrack.hasDefaultTrack = true; - audioTrack.captionsInitialState = 3; //"CAPTIONS_INITIAL_STATE_ON_RECOMMENDED" - return audioTrack; - }); - }; - // 增加自动翻译可用语言 - switch (HOST) { - case "www.youtube.com": - case "tv.youtube.com": - default: - captions.playerCaptionsTracklistRenderer.translationLanguages = configs.translationLanguages.DESKTOP; - break; - case "m.youtube.com": - case "youtubei.googleapis.com": - captions.playerCaptionsTracklistRenderer.translationLanguages = configs.translationLanguages.MOBILE; - break; - }; - // 改默认字幕索引值,来指定“源语言”,从而启用“自动翻译” - if (!captions?.playerCaptionsTracklistRenderer?.defaultCaptionTrackIndex) { - captions.playerCaptionsTracklistRenderer.defaultCaptionTrackIndex = 0; - }; - }; - }; - log(`✅ Captions`, ""); - return captions; -} + } +})() + .catch(e => Console.error(e)) + .finally(() => done($response)); diff --git a/src/response.js b/src/response.js index aa6f2f7..d7cabb7 100644 --- a/src/response.js +++ b/src/response.js @@ -1,168 +1,130 @@ -import { $platform, Lodash as _, Storage, fetch, notification, log, logError, wait, done, getScript, runScript } from "@nsnanocat/util"; +import { $app, Console, done, fetch, Lodash as _, notification, Storage, wait } from "@nsnanocat/util"; +import { URL } from "@nsnanocat/url"; import database from "./function/database.mjs"; import setENV from "./function/setENV.mjs"; import setCache from "./function/setCache.mjs"; +import setCaptions from "./function/setCaptions.mjs"; import { GetWatchResponse } from "./protobuf/google/protos/youtube/api/innertube/GetWatchResponse.js"; import { PlayerResponse } from "./protobuf/google/protos/youtube/api/innertube/PlayerResponse.js"; import { WireType, UnknownFieldHandler, reflectionMergePartial, MESSAGE_TYPE, MessageType, BinaryReader, isJsonObject, typeofJsonValue, jsonWriteOptions } from "@protobuf-ts/runtime"; +Console.debug = () => {}; /***************** Processing *****************/ // 解构URL const url = new URL($request.url); -log(`⚠ url: ${url.toJSON()}`, ""); +Console.info(`url: ${url.toJSON()}`); // 获取连接参数 -const METHOD = $request.method, HOST = url.hostname, PATH = url.pathname; -log(`⚠ METHOD: ${METHOD}, HOST: ${HOST}, PATH: ${PATH}`, ""); +const PATHs = url.pathname.split("/").filter(Boolean); +Console.info(`PATHs: ${PATHs}`); // 解析格式 const FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; -log(`⚠ FORMAT: ${FORMAT}`, ""); +Console.info(`FORMAT: ${FORMAT}`); !(async () => { /** * 设置 * @type {{Settings: import('./types').Settings}} */ const { Settings, Caches, Configs } = setENV("DualSubs", "YouTube", database); - log(`⚠ Settings.Switch: ${Settings?.Switch}`, ""); - switch (Settings.Switch) { - case true: + // 获取字幕类型与语言 + const Type = url.searchParams.get("subtype") ?? Settings.Type, + Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; + Console.info(`Type: ${Type}`, `Languages: ${Languages}`); + // 创建空数据 + let body = { + captions: { + playerCaptionsTracklistRenderer: { + captionTracks: [], + audioTracks: [], + translationLanguages: [], + }, + }, + }; + // 格式判断 + switch (FORMAT) { + case undefined: // 视为无body + break; + case "application/x-www-form-urlencoded": + case "text/plain": default: - // 获取字幕类型与语言 - const Type = url.searchParams.get("subtype") ?? Settings.Type, Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; - log(`⚠ Type: ${Type}, Languages: ${Languages}`, ""); - // 创建空数据 - let body = { "captions": { "playerCaptionsTracklistRenderer": { "captionTracks": [], "audioTracks": [], "translationLanguages": [] } } }; - // 格式判断 - 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": - break; - case "text/xml": - case "text/html": - case "text/plist": - case "application/xml": - case "application/plist": - case "application/x-plist": + break; + case "application/x-mpegURL": + case "application/x-mpegurl": + case "application/vnd.apple.mpegurl": + case "audio/mpegurl": + break; + case "text/xml": + case "text/html": + case "text/plist": + case "application/xml": + case "application/plist": + case "application/x-plist": + break; + case "text/vtt": + case "application/vtt": + break; + case "text/json": + case "application/json": + body = JSON.parse($response.body ?? "{}"); + switch (url.pathname) { + case "/youtubei/v1/player": + if (body?.captions) body.captions = setCaptions(body.captions, Configs); break; - case "text/vtt": - case "application/vtt": + case "/youtubei/v1/browse": break; - case "text/json": - case "application/json": - body = JSON.parse($response.body ?? "{}"); - switch (PATH) { + } + $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": { + let rawBody = $app === "Quantumult X" ? new Uint8Array($response.bodyBytes ?? []) : ($response.body ?? new Uint8Array()); + switch (FORMAT) { + case "application/protobuf": + case "application/x-protobuf": + case "application/vnd.google.protobuf": + switch (url.pathname) { + case "/youtubei/v1/get_watch": { + /****************** initialization start *******************/ + class get_watch_response$Type extends MessageType { + constructor() { + super("get_watch_response", [ + { + no: 1, + name: "contents", + kind: "message", + repeat: 1 /*RepeatType.PACKED*/, + T: () => GetWatchResponse, + }, + ]); + } + } + const get_watch_response = new get_watch_response$Type(); + /****************** initialization finish *******************/ + body = get_watch_response.fromBinary(rawBody); + if (body?.contents?.[0]?.playerResponse?.captions) body.contents[0].playerResponse.captions = setCaptions(body.contents[0].playerResponse.captions, Configs); + rawBody = get_watch_response.toBinary(body); + break; + } case "/youtubei/v1/player": - if (body?.captions) body.captions = captions(body.captions, Configs); + body = PlayerResponse.fromBinary(rawBody); + if (body?.captions) body.captions = setCaptions(body.captions, Configs); + rawBody = PlayerResponse.toBinary(body); break; case "/youtubei/v1/browse": break; - }; - $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": - let rawBody = ($platform === "Quantumult X") ? new Uint8Array($response.bodyBytes ?? []) : $response.body ?? new Uint8Array(); - switch (FORMAT) { - case "application/protobuf": - case "application/x-protobuf": - case "application/vnd.google.protobuf": - switch (PATH) { - case "/youtubei/v1/get_watch": - /****************** initialization start *******************/ - class get_watch_response$Type extends MessageType { - constructor() { - super("get_watch_response", [ - { no: 1, name: "contents", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => GetWatchResponse } - ]); - } - } - const get_watch_response = new get_watch_response$Type(); - /****************** initialization finish *******************/ - body = get_watch_response.fromBinary(rawBody); - if (body?.contents?.[0]?.playerResponse?.captions) body.contents[0].playerResponse.captions = captions(body.contents[0].playerResponse.captions, Configs); - rawBody = get_watch_response.toBinary(body); - break; - case "/youtubei/v1/player": - body = PlayerResponse.fromBinary(rawBody); - if (body?.captions) body.captions = captions(body.captions, Configs); - rawBody = PlayerResponse.toBinary(body); - break; - case "/youtubei/v1/browse": - break; - }; - break; - case "application/grpc": - case "application/grpc+proto": - break; - }; - // 写入二进制数据 - $response.body = rawBody; break; - }; + } + // 写入二进制数据 + $response.body = rawBody; break; - case false: - break; - }; -})() - .catch((e) => logError(e)) - .finally(() => done($response)) - -/***************** Function *****************/ -function captions(captions, configs) { - console.log(`☑️ Captions`, ""); - if (captions) { // 有基础字幕 - log(`⚠ Captions`, ""); - if (captions?.playerCaptionsRenderer) { // 有播放器字幕渲染器 - log(`⚠ playerCaptionsRenderer`, ""); - captions.playerCaptionsRenderer.visibility = "ON" // 字幕选项按钮可见 - captions.playerCaptionsRenderer.showAutoCaptions = true; // 包含自动生成的字幕 } - if (captions?.playerCaptionsTracklistRenderer) { // 有播放器字幕列表渲染器 - log(`⚠ playerCaptionsTracklistRenderer`, ""); - if (captions?.playerCaptionsTracklistRenderer?.captionTracks) { - // 改字幕可用性 - captions.playerCaptionsTracklistRenderer.captionTracks = captions?.playerCaptionsTracklistRenderer.captionTracks.map(captionTrack => { - captionTrack.isTranslatable = true; - return captionTrack; - }); - }; - if (captions?.playerCaptionsTracklistRenderer?.audioTracks) { - // 改音轨可用性 - captions.playerCaptionsTracklistRenderer.audioTracks = captions?.playerCaptionsTracklistRenderer.audioTracks.map(audioTrack => { - audioTrack.visibility = 2; //"ON" - audioTrack.hasDefaultTrack = true; - audioTrack.captionsInitialState = 3; //"CAPTIONS_INITIAL_STATE_ON_RECOMMENDED" - return audioTrack; - }); - }; - // 增加自动翻译可用语言 - switch (HOST) { - case "www.youtube.com": - case "tv.youtube.com": - default: - captions.playerCaptionsTracklistRenderer.translationLanguages = configs.translationLanguages.DESKTOP; - break; - case "m.youtube.com": - case "youtubei.googleapis.com": - captions.playerCaptionsTracklistRenderer.translationLanguages = configs.translationLanguages.MOBILE; - break; - }; - // 改默认字幕索引值,来指定“源语言”,从而启用“自动翻译” - if (!captions?.playerCaptionsTracklistRenderer?.defaultCaptionTrackIndex) { - captions.playerCaptionsTracklistRenderer.defaultCaptionTrackIndex = 0; - }; - }; - }; - log(`✅ Captions`, ""); - return captions; -} + } +})() + .catch(e => Console.error(e)) + .finally(() => done($response)); diff --git a/src/utils b/src/utils deleted file mode 160000 index 40e91e8..0000000 --- a/src/utils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 40e91e80011968370ba7ef400ff768dabf5cf583 diff --git a/template/boxjs.settings.json b/template/boxjs.settings.json index cab36d8..fae7baf 100644 --- a/template/boxjs.settings.json +++ b/template/boxjs.settings.json @@ -1 +1 @@ -[{"id":"@DualSubs.YouTube.Settings.Switch","name":"总功能开关","type":"boolean","val":true,"desc":"是否启用此APP修改"},{"id":"@DualSubs.YouTube.Settings.Type","name":"[字幕] 启用类型","type":"selects","val":"Official","items":[{"key":"Official","label":"官方字幕(合成器)"},{"key":"Translate","label":"翻译字幕(翻译器)"}],"desc":"请选择要使用的字幕,双语字幕将使用您选择类型呈现。"},{"id":"@DualSubs.YouTube.Settings.Types","name":"[歌词] 启用类型","type":"checkboxes","val":["Translate"],"items":[{"key":"Translate","label":"翻译歌词(翻译器)"}],"desc":"请选择要添加的歌词选项,如果为多选,则会自动决定提供的歌词类型。"},{"id":"@DualSubs.YouTube.Settings.AutoCC","name":"[字幕] 自动显示","type":"boolean","val":true,"desc":"是否总是自动开启字幕显示。"},{"id":"@DualSubs.YouTube.Settings.Position","name":"[字幕] 主语言(源语言)字幕位置","type":"selects","val":"Forward","items":[{"key":"Forward","label":"上面(第一行)"},{"key":"Reverse","label":"下面(第二行)"}],"desc":"主语言(源语言)字幕的显示位置。"},{"id":"@DualSubs.YouTube.Settings.Languages[0]","name":"[翻译器] 主语言(源语言)","type":"selects","val":"AUTO","items":[{"key":"AUTO","label":"自动 - Automatic"},{"key":"ZH","label":"中文(自动)"},{"key":"ZH-HANS","label":"中文(简体)"},{"key":"ZH-HK","label":"中文(香港)"},{"key":"ZH-HANT","label":"中文(繁体)"},{"key":"EN","label":"English - 英语(自动)"},{"key":"ES","label":"Español - 西班牙语(自动)"},{"key":"JA","label":"日本語 - 日语"},{"key":"KO","label":"한국어 - 韩语"},{"key":"DE","label":"Deutsch - 德语"},{"key":"FR","label":"Français - 法语"},{"key":"TR","label":"Türkçe - 土耳其语"},{"key":"KM","label":"ភាសាខ្មែរ - 高棉语"}],"desc":"仅当源语言识别不准确时更改此选项。"},{"id":"@DualSubs.YouTube.Settings.Languages[1]","name":"[翻译器] 副语言(目标语言)","type":"selects","val":"ZH","items":[{"key":"ZH","label":"中文(自动)"},{"key":"ZH-HANS","label":"中文(简体)"},{"key":"ZH-HK","label":"中文(香港)"},{"key":"ZH-HANT","label":"中文(繁体)"},{"key":"EN","label":"English - 英语(自动)"},{"key":"EN-US","label":"英语(美国)"},{"key":"ES","label":"Español - 西班牙语(自动)"},{"key":"ES-ES","label":"Español - 西班牙语"},{"key":"ES-419","label":"西班牙语(拉丁美洲)"},{"key":"JA","label":"日本語 - 日语"},{"key":"KO","label":"한국어 - 韩语"},{"key":"DE","label":"Deutsch - 德语"},{"key":"FR","label":"Français - 法语"},{"key":"TR","label":"Türkçe - 土耳其语"},{"key":"KM","label":"ភាសាខ្មែរ - 高棉语"}],"desc":"请指定翻译歌词的目标语言。"},{"id":"@DualSubs.YouTube.Settings.Vendor","name":"[翻译器] 服务商API","type":"selects","val":"Google","items":[{"key":"Google","label":"Google Translate"},{"key":"Microsoft","label":"Microsoft Translator(需填写API)"}],"desc":"请选择翻译器所使用的服务商API,更多翻译选项请使用BoxJs。"},{"id":"@DualSubs.YouTube.Settings.ShowOnly","name":"[翻译器] 只显示“自动翻译”字幕","type":"boolean","val":false,"desc":"是否仅显示“自动翻译”后的字幕,不显示源语言字幕。"}] \ No newline at end of file +[{"id":"@DualSubs.YouTube.Settings.Type","name":"[字幕] 启用类型","type":"selects","val":"Official","items":[{"key":"Official","label":"官方字幕(合成器)"},{"key":"Translate","label":"翻译字幕(翻译器)"}],"desc":"请选择要使用的字幕,双语字幕将使用您选择类型呈现。"},{"id":"@DualSubs.YouTube.Settings.Types","name":"[歌词] 启用类型","type":"checkboxes","val":["Translate"],"items":[{"key":"Translate","label":"翻译歌词(翻译器)"}],"desc":"请选择要添加的歌词选项,如果为多选,则会自动决定提供的歌词类型。"},{"id":"@DualSubs.YouTube.Settings.AutoCC","name":"[字幕] 自动显示","type":"boolean","val":true,"desc":"是否总是自动开启字幕显示。"},{"id":"@DualSubs.YouTube.Settings.Position","name":"[字幕] 主语言(源语言)字幕位置","type":"selects","val":"Forward","items":[{"key":"Forward","label":"上面(第一行)"},{"key":"Reverse","label":"下面(第二行)"}],"desc":"主语言(源语言)字幕的显示位置。"},{"id":"@DualSubs.YouTube.Settings.Languages[0]","name":"[翻译器] 主语言(源语言)","type":"selects","val":"AUTO","items":[{"key":"AUTO","label":"自动 - Automatic"},{"key":"ZH","label":"中文(自动)"},{"key":"ZH-HANS","label":"中文(简体)"},{"key":"ZH-HK","label":"中文(香港)"},{"key":"ZH-HANT","label":"中文(繁体)"},{"key":"EN","label":"English - 英语(自动)"},{"key":"ES","label":"Español - 西班牙语(自动)"},{"key":"JA","label":"日本語 - 日语"},{"key":"KO","label":"한국어 - 韩语"},{"key":"DE","label":"Deutsch - 德语"},{"key":"FR","label":"Français - 法语"},{"key":"TR","label":"Türkçe - 土耳其语"},{"key":"KM","label":"ភាសាខ្មែរ - 高棉语"}],"desc":"仅当源语言识别不准确时更改此选项。"},{"id":"@DualSubs.YouTube.Settings.Languages[1]","name":"[翻译器] 副语言(目标语言)","type":"selects","val":"ZH","items":[{"key":"ZH","label":"中文(自动)"},{"key":"ZH-HANS","label":"中文(简体)"},{"key":"ZH-HK","label":"中文(香港)"},{"key":"ZH-HANT","label":"中文(繁体)"},{"key":"EN","label":"English - 英语(自动)"},{"key":"EN-US","label":"英语(美国)"},{"key":"ES","label":"Español - 西班牙语(自动)"},{"key":"ES-ES","label":"Español - 西班牙语"},{"key":"ES-419","label":"西班牙语(拉丁美洲)"},{"key":"JA","label":"日本語 - 日语"},{"key":"KO","label":"한국어 - 韩语"},{"key":"DE","label":"Deutsch - 德语"},{"key":"FR","label":"Français - 法语"},{"key":"TR","label":"Türkçe - 土耳其语"},{"key":"KM","label":"ភាសាខ្មែរ - 高棉语"}],"desc":"请指定翻译歌词的目标语言。"},{"id":"@DualSubs.YouTube.Settings.Vendor","name":"[翻译器] 服务商API","type":"selects","val":"Google","items":[{"key":"Google","label":"Google Translate"},{"key":"Microsoft","label":"Microsoft Translator(需填写API)"}],"desc":"请选择翻译器所使用的服务商API,更多翻译选项请使用BoxJs。"},{"id":"@DualSubs.YouTube.Settings.ShowOnly","name":"[翻译器] 只显示“自动翻译”字幕","type":"boolean","val":false,"desc":"是否仅显示“自动翻译”后的字幕,不显示源语言字幕。"}] \ No newline at end of file