From 336e04a2bcf35a04eb3d8d94dbc6b4a489ba501a Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 00:25:35 -0700 Subject: [PATCH 01/27] feat(YouTube Studio Web): Integrate YouTube Studio Web around as a new Client in Innertube + add support for getting the sessionToken + add a BotGuard interface + add example script for getting YTSW sessionToken with a custom botguard_solver and get_channel_id util --- examples/youtube-studio-web/package-lock.json | 722 ++++++++++++++++++ examples/youtube-studio-web/package.json | 16 + .../youtube-studio-web/src/GetSessionToken.ts | 57 ++ examples/youtube-studio-web/tsconfig.json | 19 + src/Innertube.ts | 19 +- src/core/Actions.ts | 59 +- src/core/Session.ts | 11 + src/core/clients/StudioWeb.ts | 145 ++++ src/core/clients/index.ts | 3 +- src/parser/parser.ts | 36 + src/parser/types/ParsedResponse.ts | 17 +- src/types/BotGuard.ts | 18 + src/types/Misc.ts | 2 +- src/types/index.ts | 1 + src/utils/Constants.ts | 5 +- 15 files changed, 1112 insertions(+), 18 deletions(-) create mode 100644 examples/youtube-studio-web/package-lock.json create mode 100644 examples/youtube-studio-web/package.json create mode 100644 examples/youtube-studio-web/src/GetSessionToken.ts create mode 100644 examples/youtube-studio-web/tsconfig.json create mode 100644 src/core/clients/StudioWeb.ts create mode 100644 src/types/BotGuard.ts diff --git a/examples/youtube-studio-web/package-lock.json b/examples/youtube-studio-web/package-lock.json new file mode 100644 index 0000000000..8a3c1e858b --- /dev/null +++ b/examples/youtube-studio-web/package-lock.json @@ -0,0 +1,722 @@ +{ + "name": "youtube-studio-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "youtube-studio-web", + "version": "0.0.0", + "dependencies": { + "bgutils-js": "^4.0.3", + "jsdom": "^28.1.0", + "youtubei.js": "file:../../" + }, + "devDependencies": { + "@types/jsdom": "^27.0.0", + "typescript": "^5.0.4" + } + }, + "../..": { + "name": "youtubei.js", + "version": "17.2.0", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "fflate": "^0.8.2", + "meriyah": "^6.1.4" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@types/estree": "^1.0.6", + "@types/node": "^25.0.3", + "@typescript-eslint/eslint-plugin": "^8.46.0", + "@typescript-eslint/parser": "^8.46.0", + "cpy-cli": "^6.0.0", + "esbuild": "^0.28.0", + "eslint": "^9.37.0", + "globals": "^17.0.0", + "replace": "^1.2.2", + "rimraf": "^6.0.1", + "ts-patch": "^3.0.2", + "ts-proto": "^2.2.0", + "typedoc": "^0.28.14", + "typedoc-plugin-markdown": "^4.9.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.0", + "vitest": "^3.2.4" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@types/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/bgutils-js": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bgutils-js/-/bgutils-js-4.0.3.tgz", + "integrity": "sha512-nvhrzqRYqFcC3AJbf1fENc/jzyMk95wH9UC9ruQzTZCq+itYWuHi2a9leK/bC1lLfi+R4a28wJZxpD5KiTFf+w==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts/node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/youtubei.js": { + "resolved": "../..", + "link": true + } + } +} diff --git a/examples/youtube-studio-web/package.json b/examples/youtube-studio-web/package.json new file mode 100644 index 0000000000..cfd75789f7 --- /dev/null +++ b/examples/youtube-studio-web/package.json @@ -0,0 +1,16 @@ +{ + "name": "youtube-studio-web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": {}, + "devDependencies": { + "@types/jsdom": "^27.0.0", + "typescript": "^5.0.4" + }, + "dependencies": { + "bgutils-js": "^4.0.3", + "jsdom": "^28.1.0", + "youtubei.js": "file:../../" + } +} diff --git a/examples/youtube-studio-web/src/GetSessionToken.ts b/examples/youtube-studio-web/src/GetSessionToken.ts new file mode 100644 index 0000000000..244bf8a905 --- /dev/null +++ b/examples/youtube-studio-web/src/GetSessionToken.ts @@ -0,0 +1,57 @@ +import { BotGuardClient } from "bgutils-js/botguard"; +import { USER_AGENT } from "bgutils-js/utils"; +import { JSDOM, VirtualConsole } from "jsdom"; +import Innertube, { UniversalCache, type Types } from 'youtubei.js'; + +const botguard_solver: Types.BotGuardSolver = { + solve: async(botguard_challenge, binding) => { + const virtual_console = new VirtualConsole(); + const dom = new JSDOM('', { url: "https://www.youtube.com", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, resources: "usable", runScripts: "dangerously", virtualConsole: virtual_console }); + + Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, origin: dom.window.origin }); + + if (!("navigator" in globalThis)) { + Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator }); + } + + Object.defineProperty(dom.window.HTMLCanvasElement.prototype, "getContext", { value: () => null, writable: true }); + + let interpreter_url = botguard_challenge.interpreter_url ?? ""; + + if (interpreter_url.startsWith("//")) interpreter_url = `https:${interpreter_url}`; + + const bg_script_response = await fetch(interpreter_url); + const interpreter_javascript = await bg_script_response.text(); + + new Function(interpreter_javascript)(); + + const botguard = await BotGuardClient.create({ program: botguard_challenge.program, globalName: botguard_challenge.global_name, globalObject: globalThis }); + + const botguard_response = await botguard.snapshot({ contentBinding: { atr_challenge: binding } }); + return botguard_response; + } +}; + +async function get_channel_id(yt: Innertube, index?: number): Promise { + const account_info = await yt.account.getInfo(true); + const account = account_info[index ?? 0]; + if (account === undefined) throw new Error("No accounts found"); + const resolve_url = `https://www.youtube.com/${account.channel_handle.text}`; + const resolved = await yt.resolveURL(resolve_url); + const channel_id = resolved.payload.browseId as string; + return channel_id; +} + +const COOKIES = ""; // ?? Place your YouTube cookies here + +(async () => { + const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); + + // ?? Place your channel_ID here or use `await get_channel_id(yt)` + const CHANNEL_ID = ""; + + const yt_studio_web = yt.studioWeb; + yt_studio_web.setBotGuardSolver(botguard_solver); + const session_token = await yt_studio_web.getSessionToken(CHANNEL_ID); + console.log(session_token); +})(); \ No newline at end of file diff --git a/examples/youtube-studio-web/tsconfig.json b/examples/youtube-studio-web/tsconfig.json new file mode 100644 index 0000000000..0b71cf69f4 --- /dev/null +++ b/examples/youtube-studio-web/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "strict": true, + "moduleResolution": "bundler", + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + } +} diff --git a/src/Innertube.ts b/src/Innertube.ts index 7e57093d76..c9abd5afa5 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -1,6 +1,6 @@ import Session from './core/Session.js'; -import { Kids, Music, Studio } from './core/clients/index.js'; +import { Kids, Music, Studio, StudioWeb } from './core/clients/index.js'; import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js'; import { Feed, TabbedFeed } from './core/mixins/index.js'; @@ -143,7 +143,7 @@ export default class Innertube { signatureTimestamp: session.player?.signature_timestamp } }, - client: options?.client + client: options?.client }; if (options?.po_token) { @@ -155,7 +155,7 @@ export default class Innertube { poToken: session.po_token }; } - + const watch_response = await watch_endpoint.call(session.actions, extra_payload); const cpn = generateRandomString(16); @@ -296,7 +296,7 @@ export default class Innertube { 'Cookie': session.cookie || '' } }); - + const text = await response.text(); const data = JSON.parse(text.replace('window.google.ac.h(', '').slice(0, -1)); @@ -560,10 +560,10 @@ export default class Innertube { const payload: Record = { engagementType: engagement_type }; - + if (ids) payload.ids = ids; - + return this.actions.execute('/att/get', { parse: true, ...payload }); } @@ -590,6 +590,13 @@ export default class Innertube { return new Studio(this.#session); } + /** + * An interface for interacting with YouTube Studio Web. + */ + get studioWeb() { + return new StudioWeb(this.#session); + } + /** * An interface for interacting with YouTube Kids. */ diff --git a/src/core/Actions.ts b/src/core/Actions.ts index ac557fcc88..1f29cfee5b 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -1,7 +1,10 @@ import type { IBrowseResponse, + IESRChallengeResponse, IGetChallengeResponse, IGetNotificationsMenuResponse, + IGetSessionTokenResponse, + IGetWebReauthURLResponse, INextResponse, IParsedResponse, IPlayerResponse, @@ -34,14 +37,17 @@ export type InnertubeEndpoint = export type ParsedResponse = T extends '/player' ? IPlayerResponse : - T extends '/search' ? ISearchResponse : - T extends '/browse' ? IBrowseResponse : - T extends '/next' ? INextResponse : - T extends '/updated_metadata' ? IUpdatedMetadataResponse : - T extends '/navigation/resolve_url' ? IResolveURLResponse : - T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse : - T extends '/att/get' ? IGetChallengeResponse : - IParsedResponse; + T extends '/search' ? ISearchResponse : + T extends '/browse' ? IBrowseResponse : + T extends '/next' ? INextResponse : + T extends '/updated_metadata' ? IUpdatedMetadataResponse : + T extends '/navigation/resolve_url' ? IResolveURLResponse : + T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse : + T extends '/att/get' ? IGetChallengeResponse : + T extends '/att/esr' ? IESRChallengeResponse : + T extends '/ars/grst' ? IGetSessionTokenResponse : + T extends '/security/get_web_reauth_url' ? IGetWebReauthURLResponse : + IParsedResponse; export default class Actions { public session: Session; @@ -145,6 +151,34 @@ export default class Actions { if (data?.client === 'YTMUSIC') { data.isAudioOnly = true; } + if (data?.client === 'WEB_CREATOR') { + if (this.session.context.request) { // should just be true + this.session.context.request.returnLogEntry = true; + + // TODO maybe I want to manually fetch the initial eats; but it seems that it doesn't matter to much... + if (data?.eats) { + this.session.context.request.eats = data?.eats; + delete data?.eats; + } else { + // TODO put this in CONSTANTS + this.session.context.request.eats = 'AeCS5zA8mwKJA3VzvwD--o2-ZsGbWYFt6LMN2EuOPJLQrg6MIuKxBpbf9WNlMPlARBhbMM-hSWg982LQDZEnOBj-yHFw1TDTzIUdINTExUA6U5lOgLtxyv6guJS9HQ=='; + } + + if (data.reauth_proof_token) { + this.session.context.request.reauthRequestInfo = { encodedReauthProofToken: data.reauth_proof_token }; + delete data.reauth_proof_token; + } + + if (data.session_token) { + this.session.context.request.sessionInfo = { token: data.session_token }; + delete data.session_token; + } + + if (data.attestation_response_data && Reflect.has(data.attestation_response_data, 'challenge') && Reflect.has(data.attestation_response_data, 'webResponse')) { + this.session.context.request.attestationResponseData = data.attestation_response_data; + } + } + } } else if (args) { data = args.serialized_data; } @@ -161,6 +195,15 @@ export default class Actions { } }); + // YouTube Studio Web Context Cleanup + { + delete this.session.context.request?.returnLogEntry; + delete this.session.context.request?.eats; + delete this.session.context.request?.reauthRequestInfo; + delete this.session.context.request?.sessionInfo; + delete this.session.context.request?.attestationResponseData; + } + if (args?.parse) { let parsed_response = Parser.parseResponse>(await response.json()); diff --git a/src/core/Session.ts b/src/core/Session.ts index ed9506452b..2f2f770efe 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -99,6 +99,17 @@ export type Context = { request?: { useSsl: boolean; internalExperimentFlags: any[]; + // Studio Web params + eats?: string; + returnLogEntry?: boolean; + sessionInfo?: { token: string }; + attestationResponseData?: { + challenge: string; + webResponse: string; + }; + reauthRequestInfo?: { + encodedReauthProofToken: string; + }; }; } diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts new file mode 100644 index 0000000000..91deb8829c --- /dev/null +++ b/src/core/clients/StudioWeb.ts @@ -0,0 +1,145 @@ +import type { BotGuardChallenge, BotGuardSolver } from '../../types/BotGuard.js'; +import type { EngagementType } from '../../types/Misc.js'; +import { Constants } from '../../utils/index.js'; +import { InnertubeError } from '../../utils/Utils.js'; +import type { Actions, Session } from '../index.js'; + +interface StudioUnboundChallenge { + bg_challenge: BotGuardChallenge; + challenge: string; + eats: string; + expires_at_ms: number; +}; + +interface StudioBotguardData { + interpreter_url: string; + program: string; +}; + +interface StudioCreatorStudioChallenge { + botguard_data: StudioBotguardData; + challenge: string; + eats: string; +}; + +export default class StudioWeb { + #session: Session; + #actions: Actions; + #botguard_solver: BotGuardSolver|null; + #unbound_challenge_cache: StudioUnboundChallenge|undefined; + + constructor(session: Session) { + this.#session = session; + this.#actions = session.actions; + this.#botguard_solver = null; + if (!session.logged_in) + throw new InnertubeError('You must be signed in to use this client.'); + } + + setBotGuardSolver(botguard_solver: BotGuardSolver) { + this.#botguard_solver = botguard_solver; + } + + async #attGet(engagement_type: EngagementType, ids?: Record[], eats?: string) { + const payload: Record = { + engagementType: engagement_type + }; + + if (ids) payload.ids = ids; + + return this.#actions.execute('/att/get', { client: 'WEB_CREATOR', parse: true, ...payload, ...(eats ? { eats } : {}) }); + } + + #challengeExpiryAtMs(challenge?: string): number { + if (challenge === undefined) return Date.now(); + const params = new URLSearchParams(challenge); + const issued_seconds = Number(params.get('c')); + const ttl_seconds = Number(params.get('t')); + if (isNaN(issued_seconds) || isNaN(ttl_seconds) || issued_seconds === 0) return Date.now(); + return (issued_seconds + ttl_seconds) * 1000; + } + + async #getUnboundChallenge(): Promise { + if (this.#unbound_challenge_cache !== undefined && this.#unbound_challenge_cache.expires_at_ms > Date.now()) return this.#unbound_challenge_cache; + const unbound_challenge = await this.#attGet('ENGAGEMENT_TYPE_UNBOUND'); + + if (!unbound_challenge.eats) throw new InnertubeError('Unbound challenge missing "eats"'); + if (!unbound_challenge.challenge) throw new InnertubeError('Unbound challenge missing "challenge"'); + if (!unbound_challenge.bg_challenge) throw new InnertubeError('Unbound challenge missing "bg_challenge"'); + + this.#unbound_challenge_cache = { + bg_challenge: { + ...unbound_challenge.bg_challenge, + interpreter_url: unbound_challenge.bg_challenge.interpreter_url.private_do_not_access_or_else_safe_script_wrapped_value ?? unbound_challenge.bg_challenge.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value ?? '' + }, + challenge: unbound_challenge.challenge, + eats: unbound_challenge.eats, + expires_at_ms: this.#challengeExpiryAtMs(unbound_challenge.challenge) + }; + if (!this.#unbound_challenge_cache.bg_challenge.interpreter_url) throw new InnertubeError('Unbound challenge bg_challenge missing valid "interpreter_url"'); + return this.#unbound_challenge_cache; + } + + async #getCreatorStudioChallenge(channel_id: string, unbound_challenge_eats: string): Promise { + const creator_studio_challenge = await this.#attGet('ENGAGEMENT_TYPE_CREATOR_STUDIO_ACTION', [ { externalChannelId: channel_id } ], unbound_challenge_eats); + + if (!creator_studio_challenge.eats) throw new InnertubeError('Creator Studio challenge missing "eats"'); + if (!creator_studio_challenge.challenge) throw new InnertubeError('Creator Studio challenge missing "challenge"'); + if (!creator_studio_challenge.botguard_data) throw new InnertubeError('Creator Studio challenge missing "botguard_data"'); + + const interpreter_url = creator_studio_challenge.botguard_data.interpreter_url.private_do_not_access_or_else_safe_script_wrapped_value ?? creator_studio_challenge.botguard_data.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value ?? ''; + if (!interpreter_url) throw new InnertubeError('Creator Studio challenge bg_challenge missing valid "interpreter_url"'); + return { + botguard_data: { + ...creator_studio_challenge.botguard_data, + interpreter_url: interpreter_url + }, + challenge: creator_studio_challenge.challenge, + eats: creator_studio_challenge.eats + }; + } + + // TODO add caching support and whatnot + async getSessionToken(channel_id: string): Promise { + if (!this.#botguard_solver) throw new InnertubeError('BotGuard Solver is not initialized. Please setup with setBotGuardSolver()'); + + const unbound_challenge = await this.#getUnboundChallenge(); + const creator_studio_challenge = await this.#getCreatorStudioChallenge(channel_id, unbound_challenge.eats); + + const botguard_response = await this.#botguard_solver.solve(unbound_challenge.bg_challenge, creator_studio_challenge.challenge); + + const esr_data = await this.#actions.execute('/att/esr', { + client: 'WEB_CREATOR', + parse: true, + challenge: creator_studio_challenge.challenge, + botguardResponse: botguard_response, + xguardClientStatus: 0, + eats: creator_studio_challenge.eats + }); + + if (!esr_data.ctx || esr_data.should_fetch_reauth_session_token === undefined) throw new InnertubeError('/att/esr did not return usable data'); + + let grst_ctx = esr_data.ctx; + let reauth_proof_token: string | undefined; + if (esr_data.should_fetch_reauth_session_token === true) { + const reauth_data = await this.#actions.execute('/security/get_web_reauth_url', { + client: 'WEB_CREATOR', + parse: true, + continueUrl: `${Constants.URLS.YT_STUDIO_WEB_BASE}/reauth`, + flow: 'REAUTH_FLOW_YT_STUDIO_COLD_LOAD', + ivctx: esr_data.ctx, + challenge: creator_studio_challenge.challenge, + botguardResponse: botguard_response, + eats: creator_studio_challenge.eats + }); + if (!reauth_data.encoded_reauth_proof_token || !reauth_data.session_risk_ctx) throw new InnertubeError('/security/get_web_reauth_url did not return a reauth proof'); + grst_ctx = reauth_data.session_risk_ctx; + reauth_proof_token = reauth_data.encoded_reauth_proof_token; + } + + const grst_data = await this.#actions.execute('/ars/grst', { client: 'WEB_CREATOR', parse: true, ctx: grst_ctx, reauth_proof_token, eats: creator_studio_challenge.eats }); + + if (grst_data.session_token === undefined) throw new InnertubeError('/ars/grst did not return a session token'); + return grst_data.session_token; + } +} diff --git a/src/core/clients/index.ts b/src/core/clients/index.ts index 6b7fe70544..b05aed26e7 100644 --- a/src/core/clients/index.ts +++ b/src/core/clients/index.ts @@ -1,3 +1,4 @@ export { default as Kids } from './Kids.js'; export { default as Music } from './Music.js'; -export { default as Studio } from './Studio.js'; \ No newline at end of file +export { default as Studio } from './Studio.js'; +export { default as StudioWeb } from './StudioWeb.js'; \ No newline at end of file diff --git a/src/parser/parser.ts b/src/parser/parser.ts index dec8a1cceb..500605dba1 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -502,6 +502,42 @@ export function parseResponse(data: parsed_data.challenge = data.challenge; } + if (data.botguardData) { + const interpreter_url = { + private_do_not_access_or_else_trusted_resource_url_wrapped_value: data.botguardData.interpreterSafeUrl?.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue, + private_do_not_access_or_else_safe_script_wrapped_value: data.botguardData.interpreterSafeUrl?.privateDoNotAccessOrElseSafeScriptWrappedValue + }; + + parsed_data.botguard_data = { + interpreter_url, + program: data.botguardData.program + }; + } + + if (data.eats) { + parsed_data.eats = data.eats; + } + + if (data.ctx) { + parsed_data.ctx = data.ctx; + } + + if (data.shouldFetchReauthSessionToken) { + parsed_data.should_fetch_reauth_session_token = data.shouldFetchReauthSessionToken; + } + + if (data.encodedReauthProofToken) { + parsed_data.encoded_reauth_proof_token = data.encodedReauthProofToken; + } + + if (data.sessionRiskCtx) { + parsed_data.session_risk_ctx = data.sessionRiskCtx; + } + + if (data.sessionToken) { + parsed_data.session_token = data.sessionToken; + } + if (data.playerResponse) { parsed_data.player_response = parseResponse(data.playerResponse); } diff --git a/src/parser/types/ParsedResponse.ts b/src/parser/types/ParsedResponse.ts index 25f6547e48..c4914f2a8f 100644 --- a/src/parser/types/ParsedResponse.ts +++ b/src/parser/types/ParsedResponse.ts @@ -26,6 +26,13 @@ export interface IParsedResponse { background?: MusicThumbnail; challenge?: string; bg_challenge?: IBotguardChallenge; + botguard_data?: IBotguardData; + ctx?: string; + should_fetch_reauth_session_token?: boolean; + encoded_reauth_proof_token?: string; + session_risk_ctx?: string; + session_token?: string; + eats?: string; actions?: SuperParsedResult; actions_memo?: Memo; contents?: SuperParsedResult; @@ -96,6 +103,11 @@ export interface IBotguardChallenge { client_experiments_state_blob: string; } +export interface IBotguardData { + interpreter_url: ITrustedResource; + program: string; +} + export interface IPlaybackTracking { videostats_watchtime_url: string; videostats_playback_url: string; @@ -147,4 +159,7 @@ export type IGetTranscriptResponse = Pick; export type IUpdatedMetadataResponse = Pick; export type IGuideResponse = Pick; -export type IGetChallengeResponse = Pick; \ No newline at end of file +export type IGetChallengeResponse = Pick; +export type IESRChallengeResponse = Pick; +export type IGetSessionTokenResponse = Pick; +export type IGetWebReauthURLResponse = Pick; diff --git a/src/types/BotGuard.ts b/src/types/BotGuard.ts new file mode 100644 index 0000000000..861962d194 --- /dev/null +++ b/src/types/BotGuard.ts @@ -0,0 +1,18 @@ +export interface BotGuardChallenge { + program: string; + global_name: string; + interpreter_hash?: string; + interpreter_url: string; +} +// TODO, make this more comprehensive +export interface AttestationBinding { + // challenge + c: string; + // engagement_type + e?: string; + externalChannelId?: string; + encryptedVideoId?: string; +} +export interface BotGuardSolver { + solve: (botguard_challenge: BotGuardChallenge, binding: T) => Promise; +} diff --git a/src/types/Misc.ts b/src/types/Misc.ts index af5b55bd64..342d873dd2 100644 --- a/src/types/Misc.ts +++ b/src/types/Misc.ts @@ -2,7 +2,7 @@ import type { SessionOptions } from '../core/index.js'; export type InnerTubeConfig = SessionOptions; export type InnerTubeClient = 'IOS' | 'WEB' | 'MWEB' | 'ANDROID' | 'ANDROID_VR' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'YTSTUDIO_ANDROID' | 'TV' | 'TV_SIMPLY' |'TV_EMBEDDED' | 'YTKIDS' | 'WEB_EMBEDDED' | 'WEB_CREATOR'; -export type EngagementType = 'ENGAGEMENT_TYPE_UNBOUND' | 'ENGAGEMENT_TYPE_VIDEO_LIKE' | 'ENGAGEMENT_TYPE_VIDEO_DISLIKE' | 'ENGAGEMENT_TYPE_SUBSCRIBE' | 'ENGAGEMENT_TYPE_PLAYBACK' | 'ENGAGEMENT_TYPE_YPC_GET_PREMIUM_PAGE' | 'ENGAGEMENT_TYPE_YPC_GET_DOWNLOAD_ACTION'; +export type EngagementType = 'ENGAGEMENT_TYPE_UNBOUND' | 'ENGAGEMENT_TYPE_VIDEO_LIKE' | 'ENGAGEMENT_TYPE_VIDEO_DISLIKE' | 'ENGAGEMENT_TYPE_SUBSCRIBE' | 'ENGAGEMENT_TYPE_PLAYBACK' | 'ENGAGEMENT_TYPE_YPC_GET_PREMIUM_PAGE' | 'ENGAGEMENT_TYPE_YPC_GET_DOWNLOAD_ACTION' | 'ENGAGEMENT_TYPE_CREATOR_STUDIO_ACTION'; export type UploadDate = 'all' | 'today' | 'week' | 'month' | 'year'; export type SearchType = 'all' | 'video' | 'shorts' | 'channel' | 'playlist' | 'movie'; diff --git a/src/types/index.ts b/src/types/index.ts index ec6da7eeb0..085291112d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ export type { default as PlatformShim } from './PlatformShim.js'; +export type * from './BotGuard.js'; export type * from './Cache.js'; export type * from './PlatformShim.js'; export type * from './Misc.js'; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index d82e094d48..c9f63f924a 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -2,8 +2,11 @@ export const URLS = { YT_BASE: 'https://www.youtube.com', YT_MUSIC_BASE: 'https://music.youtube.com', + YT_STUDIO_WEB_BASE: 'https://studio.youtube.com', YT_SUGGESTIONS: 'https://suggestqueries-clients6.youtube.com', YT_UPLOAD: 'https://upload.youtube.com/', + YT_UPLOAD_VIDEO_WEB: 'https://upload.youtube.com/upload/studio', + YT_UPLOAD_THUMBNAIL_WEB: 'https://upload.youtube.com/upload/studiothumbnail', API: { BASE: 'https://youtubei.googleapis.com', PRODUCTION_1: 'https://www.youtube.com/youtubei/', @@ -96,7 +99,7 @@ export const CLIENTS = { }, WEB_CREATOR: { NAME: 'WEB_CREATOR', - VERSION: '1.20241203.01.00', + VERSION: '1.20260728.03.00', API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', API_VERSION: 'v1', STATIC_VISITOR_ID: '6zpwvWUNAco' From 88a56b1068c59fee2ca65d0172f607821cfed4d2 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 01:09:55 -0700 Subject: [PATCH 02/27] feat(YouTube Studio Web): Add support for scotty chunk based file uploading --- src/core/clients/StudioWeb.ts | 92 +++++++++++++++++++++++++++++++++++ src/utils/Constants.ts | 4 +- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index 91deb8829c..e6d186c09c 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -22,6 +22,16 @@ interface StudioCreatorStudioChallenge { eats: string; }; +interface BufferReader { + total_bytes: number; + read_chunk: (position: number, length: number) => Promise; +}; + +interface ScottyStart { upload_url: string, resource_id?: string }; +interface ScottyUploadResult { status?: string, scottyResourceId?: string }; +type ScottyProgress = (written_bytes: number, total_bytes: number) => void; +type ScottyUploadType = 'VIDEO' | 'THUMBNAIL'; + export default class StudioWeb { #session: Session; #actions: Actions; @@ -142,4 +152,86 @@ export default class StudioWeb { if (grst_data.session_token === undefined) throw new InnertubeError('/ars/grst did not return a session token'); return grst_data.session_token; } + + #scottyHeaders(file_name: string): Record { + if (!this.#session.cookie) throw new InnertubeError('Unable to produce scottyHeaders with cookies'); + return { + 'Cookie': this.#session.cookie, + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Origin': Constants.URLS.YT_STUDIO_WEB_BASE, + 'Referer': `${Constants.URLS.YT_STUDIO_WEB_BASE}/`, + 'User-Agent': Constants.CLIENTS['WEB_CREATOR'].USER_AGENT, + 'x-goog-upload-file-name': encodeURIComponent(file_name) + }; + } + + async #scottyStart(start_upload_url: string, file_name: string, source: BufferReader, start_payload: object): Promise { + if (source.total_bytes <= 0) throw new InnertubeError('Can\'t upload an empty file to scotty'); + + const start_response = await fetch(`${start_upload_url}?authuser=0`, { + method: 'POST', + headers: { + ...this.#scottyHeaders(file_name), + 'x-goog-upload-command': 'start', + 'x-goog-upload-header-content-length': String(source.total_bytes), + 'x-goog-upload-protocol': 'resumable' + }, + referrer: Constants.URLS.YT_STUDIO_WEB_BASE, + body: JSON.stringify(start_payload) + }); + const upload_url = start_response.headers.get('x-goog-upload-url'); + if (upload_url === null || upload_url === '') throw new InnertubeError('Scotty did not return an upload url'); + + const resource_id = start_response.headers.get('x-goog-upload-header-scotty-resource-id'); + + return { upload_url, resource_id: !resource_id ? undefined : resource_id }; + } + + async #scottyUploadChunks(upload_url: string, file_name: string, source: BufferReader, on_progress?: ScottyProgress): Promise<{ resource_id?: string }> { + const UPLOAD_CHUNK_SIZE_BYTES = 100 * 1024 * 1024; + + let offset = 0; + while (offset < source.total_bytes) { + const chunk = await source.read_chunk(offset, Math.min(UPLOAD_CHUNK_SIZE_BYTES, source.total_bytes - offset)); + if (chunk.byteLength === 0) throw new InnertubeError('Ran out of bytes before scotty finalized the upload'); + + const is_final = offset + chunk.byteLength >= source.total_bytes; + const chunk_response = await fetch(upload_url, { + method: 'POST', + headers: { + ...this.#scottyHeaders(file_name), + 'x-goog-upload-command': is_final ? 'upload, finalize' : 'upload', + 'x-goog-upload-offset': String(offset) + }, + body: chunk as BodyInit + }); + if (!chunk_response.ok) throw new InnertubeError('Unable to upload a chunk of the buffer to scotty'); + + offset += chunk.byteLength; + on_progress?.(offset, source.total_bytes); + if (!is_final) { + await chunk_response.body?.cancel(); + continue; + } + + const result = await chunk_response.json() as ScottyUploadResult; + if (result.status !== 'STATUS_SUCCESS') throw new InnertubeError('Scotty did not finalize the upload'); + return { resource_id: result.scottyResourceId }; + } + throw new InnertubeError('Scotty upload loop ended without finalizing'); + } + + async #uploadToScotty(upload_type: ScottyUploadType, file_name: string, source: BufferReader, start_payload: object, on_progress?: ScottyProgress): Promise { + const UPLOAD_TYPES_TO_START_URL: Record = { + VIDEO: Constants.URLS.YT_UPLOAD_VIDEO_WEB, + THUMBNAIL: Constants.URLS.YT_UPLOAD_THUMBNAIL_WEB + }; + const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL[upload_type], file_name, source, start_payload); + const uploaded = await this.#scottyUploadChunks(start.upload_url, file_name, source, on_progress); + const resource_id = start.resource_id ?? uploaded.resource_id; + if (!resource_id) throw new InnertubeError('Scotty did not return a resource id'); + return resource_id; + } } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index c9f63f924a..5778427330 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -1,4 +1,3 @@ - export const URLS = { YT_BASE: 'https://www.youtube.com', YT_MUSIC_BASE: 'https://music.youtube.com', @@ -102,7 +101,8 @@ export const CLIENTS = { VERSION: '1.20260728.03.00', API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', API_VERSION: 'v1', - STATIC_VISITOR_ID: '6zpwvWUNAco' + STATIC_VISITOR_ID: '6zpwvWUNAco', + USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36' } } as const; /** From 1a6eddb4e8bff049ccc14a2a84b0d9c03334219e Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 02:46:22 -0700 Subject: [PATCH 03/27] feat(YouTube Studio Web): Add in the `DelegationContext` proto contents + more managing context + added some uploading related types in `StudioWebUploading.ts` + Add sessionToken caching support + refactoring scotty upload system + `uploadThumbnailResource` and `uploadSubtitles` + a managed execute to handle a lot of the complexity of YouTube Studio Web (sorry big commit) --- .../youtube/api/pfiinnertube/user_info.ts | 75 ++++++++- .../youtube/api/pfiinnertube/user_info.proto | 16 +- src/core/Actions.ts | 22 ++- src/core/Session.ts | 6 + src/core/clients/StudioWeb.ts | 152 +++++++++++++++--- src/types/StudioWebUploading.ts | 72 +++++++++ src/types/index.ts | 1 + 7 files changed, 321 insertions(+), 23 deletions(-) create mode 100644 src/types/StudioWebUploading.ts diff --git a/protos/generated/youtube/api/pfiinnertube/user_info.ts b/protos/generated/youtube/api/pfiinnertube/user_info.ts index 85db5ee6d8..1a69d20193 100644 --- a/protos/generated/youtube/api/pfiinnertube/user_info.ts +++ b/protos/generated/youtube/api/pfiinnertube/user_info.ts @@ -28,6 +28,18 @@ export interface UserInfo_DelegatePurchases { } export interface UserInfo_DelegationContext { + externalChannelId?: string | undefined; + roleType?: UserInfo_DelegationContext_RoleType | undefined; +} + +export interface UserInfo_DelegationContext_RoleType { + channelRoleType?: UserInfo_DelegationContext_RoleType_ChannelRoleType | undefined; +} + +export enum UserInfo_DelegationContext_RoleType_ChannelRoleType { + CHANNEL_ROLE_TYPE_UNKNOWN = 0, + CREATOR_CHANNEL_ROLE_TYPE_OWNER = 8, + UNRECOGNIZED = -1, } export interface UserInfo_CredentialTransferToken { @@ -221,11 +233,17 @@ export const UserInfo_DelegatePurchases: MessageFns }; function createBaseUserInfo_DelegationContext(): UserInfo_DelegationContext { - return {}; + return { externalChannelId: undefined, roleType: undefined }; } export const UserInfo_DelegationContext: MessageFns = { - encode(_: UserInfo_DelegationContext, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + encode(message: UserInfo_DelegationContext, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.externalChannelId !== undefined) { + writer.uint32(18).string(message.externalChannelId); + } + if (message.roleType !== undefined) { + UserInfo_DelegationContext_RoleType.encode(message.roleType, writer.uint32(42).fork()).join(); + } return writer; }, @@ -236,6 +254,59 @@ export const UserInfo_DelegationContext: MessageFns while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { + case 2: { + if (tag !== 18) { + break; + } + + message.externalChannelId = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.roleType = UserInfo_DelegationContext_RoleType.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, +}; + +function createBaseUserInfo_DelegationContext_RoleType(): UserInfo_DelegationContext_RoleType { + return { channelRoleType: undefined }; +} + +export const UserInfo_DelegationContext_RoleType: MessageFns = { + encode(message: UserInfo_DelegationContext_RoleType, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.channelRoleType !== undefined) { + writer.uint32(8).int32(message.channelRoleType); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): UserInfo_DelegationContext_RoleType { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUserInfo_DelegationContext_RoleType(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.channelRoleType = reader.int32() as any; + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; diff --git a/protos/youtube/api/pfiinnertube/user_info.proto b/protos/youtube/api/pfiinnertube/user_info.proto index 9ae94541cd..d3a84b71ea 100644 --- a/protos/youtube/api/pfiinnertube/user_info.proto +++ b/protos/youtube/api/pfiinnertube/user_info.proto @@ -14,6 +14,20 @@ message UserInfo { message KidsParent { } message DelegatePurchases { } - message DelegationContext { } + + message DelegationContext { + optional string external_channel_id = 2; + optional RoleType role_type = 5; + + message RoleType { + optional ChannelRoleType channel_role_type = 1; + + enum ChannelRoleType { + CHANNEL_ROLE_TYPE_UNKNOWN = 0; + CREATOR_CHANNEL_ROLE_TYPE_OWNER = 8; + } + } + } + message CredentialTransferToken { } } \ No newline at end of file diff --git a/src/core/Actions.ts b/src/core/Actions.ts index 1f29cfee5b..c6cf00c47b 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -14,7 +14,12 @@ import type { IUpdatedMetadataResponse } from '../parser/index.js'; import { NavigateAction, Parser } from '../parser/index.js'; -import { InnertubeError } from '../utils/Utils.js'; +import { InnertubeError, u8ToBase64 } from '../utils/Utils.js'; + +import { + UserInfo_DelegationContext, + UserInfo_DelegationContext_RoleType_ChannelRoleType +} from '../../protos/generated/youtube/api/pfiinnertube/user_info.js'; import type { Session } from './index.js'; @@ -177,6 +182,19 @@ export default class Actions { if (data.attestation_response_data && Reflect.has(data.attestation_response_data, 'challenge') && Reflect.has(data.attestation_response_data, 'webResponse')) { this.session.context.request.attestationResponseData = data.attestation_response_data; } + + if (data.channel_id && this.session.context.user) { + const delegation_context = { + externalChannelId: data.channel_id, + roleType: { channelRoleType: 'CREATOR_CHANNEL_ROLE_TYPE_OWNER' as const } + }; + + this.session.context.user.delegationContext = delegation_context; + this.session.context.user.serializedDelegationContext = u8ToBase64(UserInfo_DelegationContext.encode({ + externalChannelId: delegation_context.externalChannelId, + roleType: { channelRoleType: UserInfo_DelegationContext_RoleType_ChannelRoleType.CREATOR_CHANNEL_ROLE_TYPE_OWNER } + }).finish()); + } } } } else if (args) { @@ -202,6 +220,8 @@ export default class Actions { delete this.session.context.request?.reauthRequestInfo; delete this.session.context.request?.sessionInfo; delete this.session.context.request?.attestationResponseData; + delete this.session.context.user?.delegationContext; + delete this.session.context.user?.serializedDelegationContext; } if (args?.parse) { diff --git a/src/core/Session.ts b/src/core/Session.ts index 2f2f770efe..76726e5e51 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -92,6 +92,12 @@ export type Context = { enableSafetyMode: boolean; lockedSafetyMode: boolean; onBehalfOfUser?: string; + // Studio Web params + delegationContext?: { + externalChannelId: string, + roleType: { channelRoleType: 'CREATOR_CHANNEL_ROLE_TYPE_OWNER' } + } + serializedDelegationContext?: string; }; thirdParty?: { embedUrl: string; diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index e6d186c09c..851049e19f 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -1,14 +1,23 @@ import type { BotGuardChallenge, BotGuardSolver } from '../../types/BotGuard.js'; import type { EngagementType } from '../../types/Misc.js'; +import type { FileNamedBufferReader, UploadVideoDetails } from '../../types/StudioWebUploading.js'; import { Constants } from '../../utils/index.js'; import { InnertubeError } from '../../utils/Utils.js'; import type { Actions, Session } from '../index.js'; +type AttestationPlacement = 'none' | 'context' | 'top_level'; + +interface BotGuardAttestationResponse { + challenge: string; + webResponse: string; +}; + interface StudioUnboundChallenge { bg_challenge: BotGuardChallenge; challenge: string; eats: string; expires_at_ms: number; + result?: string; }; interface StudioBotguardData { @@ -22,26 +31,40 @@ interface StudioCreatorStudioChallenge { eats: string; }; -interface BufferReader { - total_bytes: number; - read_chunk: (position: number, length: number) => Promise; +interface StudioSessionTokenCache { + session_token: string; + expires_at_ms: number; }; -interface ScottyStart { upload_url: string, resource_id?: string }; -interface ScottyUploadResult { status?: string, scottyResourceId?: string }; +interface ScottyStart { upload_url: string; resource_id?: string }; +interface ScottyUploadResult { status?: string; scottyResourceId?: string }; type ScottyProgress = (written_bytes: number, total_bytes: number) => void; type ScottyUploadType = 'VIDEO' | 'THUMBNAIL'; +interface StudioChallengeResponseContext { + responseContext?: { webResponseContextExtensionData?: { challenge?: { type?: 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE' } } }; +} + +interface CreateCaptionsResponse { translation?: { captionsTranslations?: { contentUpdateTime?: string }[] } }; +interface ParseCaptionsResponse { captionSegments?: object } +interface UpdateCaptionsResponse { framework_updates?: object } + export default class StudioWeb { #session: Session; #actions: Actions; #botguard_solver: BotGuardSolver|null; #unbound_challenge_cache: StudioUnboundChallenge|undefined; + #auto_retry: boolean; + #force_refresh_session_token: boolean; + #channel_id_session_token_cache: Record; constructor(session: Session) { this.#session = session; this.#actions = session.actions; this.#botguard_solver = null; + this.#auto_retry = true; + this.#force_refresh_session_token = false; + this.#channel_id_session_token_cache = {}; if (!session.logged_in) throw new InnertubeError('You must be signed in to use this client.'); } @@ -49,6 +72,9 @@ export default class StudioWeb { setBotGuardSolver(botguard_solver: BotGuardSolver) { this.#botguard_solver = botguard_solver; } + setAutoRetrying(auto_retry: boolean) { + this.#auto_retry = auto_retry; + } async #attGet(engagement_type: EngagementType, ids?: Record[], eats?: string) { const payload: Record = { @@ -109,13 +135,30 @@ export default class StudioWeb { }; } - // TODO add caching support and whatnot + async #getBotGuardAttestation(): Promise { + if (!this.#botguard_solver) throw new InnertubeError('BotGuard Solver is not initialized. Please setup with setBotGuardSolver()'); + const unbound_challenge = this.#unbound_challenge_cache && this.#unbound_challenge_cache.expires_at_ms > Date.now() ? this.#unbound_challenge_cache : await this.#getUnboundChallenge(); + if (unbound_challenge.result) return { challenge: unbound_challenge.challenge, webResponse: unbound_challenge.result }; + const botguard_response = await this.#botguard_solver.solve(unbound_challenge.bg_challenge, unbound_challenge.challenge); + unbound_challenge.result = botguard_response; + this.#unbound_challenge_cache = unbound_challenge; + return { challenge: unbound_challenge.challenge, webResponse: botguard_response }; + } + + #eatForceRefreshSessionToken() { + const force_refresh_session_token = this.#force_refresh_session_token; + this.#force_refresh_session_token = false; + return force_refresh_session_token; + } + async getSessionToken(channel_id: string): Promise { if (!this.#botguard_solver) throw new InnertubeError('BotGuard Solver is not initialized. Please setup with setBotGuardSolver()'); + if (this.#channel_id_session_token_cache[channel_id]?.expires_at_ms > Date.now() && !this.#eatForceRefreshSessionToken()) return this.#channel_id_session_token_cache[channel_id].session_token; const unbound_challenge = await this.#getUnboundChallenge(); const creator_studio_challenge = await this.#getCreatorStudioChallenge(channel_id, unbound_challenge.eats); + // don't cache this result into the unbounded cache since it uses different challenge kinda const botguard_response = await this.#botguard_solver.solve(unbound_challenge.bg_challenge, creator_studio_challenge.challenge); const esr_data = await this.#actions.execute('/att/esr', { @@ -167,15 +210,15 @@ export default class StudioWeb { }; } - async #scottyStart(start_upload_url: string, file_name: string, source: BufferReader, start_payload: object): Promise { - if (source.total_bytes <= 0) throw new InnertubeError('Can\'t upload an empty file to scotty'); + async #scottyStart(start_upload_url: string, file_name_buffer_reader: FileNamedBufferReader, start_payload: object): Promise { + if (file_name_buffer_reader.source.total_bytes <= 0) throw new InnertubeError('Can\'t upload an empty file to scotty'); const start_response = await fetch(`${start_upload_url}?authuser=0`, { method: 'POST', headers: { - ...this.#scottyHeaders(file_name), + ...this.#scottyHeaders(file_name_buffer_reader.file_name), 'x-goog-upload-command': 'start', - 'x-goog-upload-header-content-length': String(source.total_bytes), + 'x-goog-upload-header-content-length': String(file_name_buffer_reader.source.total_bytes), 'x-goog-upload-protocol': 'resumable' }, referrer: Constants.URLS.YT_STUDIO_WEB_BASE, @@ -189,19 +232,19 @@ export default class StudioWeb { return { upload_url, resource_id: !resource_id ? undefined : resource_id }; } - async #scottyUploadChunks(upload_url: string, file_name: string, source: BufferReader, on_progress?: ScottyProgress): Promise<{ resource_id?: string }> { + async #scottyUploadChunks(upload_url: string, file_name_buffer_reader: FileNamedBufferReader, on_progress?: ScottyProgress): Promise<{ resource_id?: string }> { const UPLOAD_CHUNK_SIZE_BYTES = 100 * 1024 * 1024; let offset = 0; - while (offset < source.total_bytes) { - const chunk = await source.read_chunk(offset, Math.min(UPLOAD_CHUNK_SIZE_BYTES, source.total_bytes - offset)); + while (offset < file_name_buffer_reader.source.total_bytes) { + const chunk = await file_name_buffer_reader.source.read_chunk(offset, Math.min(UPLOAD_CHUNK_SIZE_BYTES, file_name_buffer_reader.source.total_bytes - offset)); if (chunk.byteLength === 0) throw new InnertubeError('Ran out of bytes before scotty finalized the upload'); - const is_final = offset + chunk.byteLength >= source.total_bytes; + const is_final = offset + chunk.byteLength >= file_name_buffer_reader.source.total_bytes; const chunk_response = await fetch(upload_url, { method: 'POST', headers: { - ...this.#scottyHeaders(file_name), + ...this.#scottyHeaders(file_name_buffer_reader.file_name), 'x-goog-upload-command': is_final ? 'upload, finalize' : 'upload', 'x-goog-upload-offset': String(offset) }, @@ -210,7 +253,7 @@ export default class StudioWeb { if (!chunk_response.ok) throw new InnertubeError('Unable to upload a chunk of the buffer to scotty'); offset += chunk.byteLength; - on_progress?.(offset, source.total_bytes); + on_progress?.(offset, file_name_buffer_reader.source.total_bytes); if (!is_final) { await chunk_response.body?.cancel(); continue; @@ -223,15 +266,86 @@ export default class StudioWeb { throw new InnertubeError('Scotty upload loop ended without finalizing'); } - async #uploadToScotty(upload_type: ScottyUploadType, file_name: string, source: BufferReader, start_payload: object, on_progress?: ScottyProgress): Promise { + async #uploadToScotty(upload_type: ScottyUploadType, file_name_buffer_reader: FileNamedBufferReader, start_payload: object, on_progress?: ScottyProgress): Promise { const UPLOAD_TYPES_TO_START_URL: Record = { VIDEO: Constants.URLS.YT_UPLOAD_VIDEO_WEB, THUMBNAIL: Constants.URLS.YT_UPLOAD_THUMBNAIL_WEB }; - const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL[upload_type], file_name, source, start_payload); - const uploaded = await this.#scottyUploadChunks(start.upload_url, file_name, source, on_progress); + const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL[upload_type], file_name_buffer_reader, start_payload); + const uploaded = await this.#scottyUploadChunks(start.upload_url, file_name_buffer_reader, on_progress); const resource_id = start.resource_id ?? uploaded.resource_id; if (!resource_id) throw new InnertubeError('Scotty did not return a resource id'); return resource_id; } + + async #uploadThumbnailResource(file_name_buffer_reader: FileNamedBufferReader): Promise { + return await this.#uploadToScotty('THUMBNAIL', file_name_buffer_reader, {}); + } + + async managedExecute(endpoint: string, payload: object, channel_id: string, attestation_placement: AttestationPlacement = 'none', eats?: string, is_retry = false): Promise { + const attestation_response_data = attestation_placement === 'none' ? {} : { attestationResponseData: await this.#getBotGuardAttestation() }; + + const response = await this.#actions.execute(endpoint, { + client: 'WEB_CREATOR', + session_token: await this.getSessionToken(channel_id), + ...payload, + ...(attestation_placement === 'context' ? {} : { attestation_response_data }), + ...(attestation_placement === 'top_level' ? {} : attestation_response_data), + ...(!eats ? {} : { eats }) + }); + if (!response.success) throw new InnertubeError(`YouTube Studio call to ${endpoint} failed`); + const data = response.data as T & StudioChallengeResponseContext; + + if (data.responseContext?.webResponseContextExtensionData?.challenge?.type === 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE') { + if (!is_retry && this.#auto_retry && channel_id !== undefined) { + this.#force_refresh_session_token = true; + return await this.managedExecute(endpoint, payload, channel_id, attestation_placement, eats, true); + } + throw new InnertubeError('YouTube Studio is requesting an authentication challenge, likely a stale session token'); + } + return data; + } + + async #uploadSubtitles(channel_id: string, video_id: string, subtitles: NonNullable, language = 'en-US') { + const data_base64 = await subtitles.data.source.base64; + const data_uri = `data:application/octet-stream;base64,${data_base64}`; + const tts_track_id = { lang: language, kind: '', name: '' }; + + const created = await this.managedExecute('/globalization/create_captions', { + parse: true, + videoId: video_id, + channelId: channel_id, + newTrack: tts_track_id, + overwrite: subtitles.overwrite ?? true, + autoTranslate: subtitles.auto_translate ?? false, + channel_id + }, channel_id); + + const content_update_time = created.translation?.captionsTranslations?.[0]?.contentUpdateTime; + if (content_update_time === undefined) throw new InnertubeError('create_captions did not return a contentUpdateTime'); + + const parsed = await this.managedExecute('/globalization/parse_captions', { + fileType: subtitles.synced ? 'CAPTIONS_FILE_TYPE_TIMED_TEXT' : 'CAPTIONS_FILE_TYPE_TRANSCRIPT', + fileName: subtitles.data.file_name, + dataUri: data_uri, + channel_id + }, channel_id); + + const updated = await this.managedExecute('/globalization/update_captions', { + videoId: video_id, + channelId: channel_id, + operations: [ + { + ttsTrackId: tts_track_id, + userIntent: 'USER_INTENT_EDIT_LATEST_DRAFT', + vote: 'VOTE_PUBLISH', + isContentEdited: false, + contentUpdateTime: content_update_time, + captionsFile: { dataUri: data_uri, fileName: subtitles.data.file_name } + } + ], + channel_id + }, channel_id); + return { created, parsed, updated }; + } } diff --git a/src/types/StudioWebUploading.ts b/src/types/StudioWebUploading.ts new file mode 100644 index 0000000000..e9e5486882 --- /dev/null +++ b/src/types/StudioWebUploading.ts @@ -0,0 +1,72 @@ +export interface BufferReader { + total_bytes: number; + read_chunk: (position: number, length: number) => Promise; +}; +export interface BufferBase64 { + base64: string; +}; + +export interface FileNamedBufferReader { + file_name: string; + source: BufferReader; +}; + +export interface FileNamedBufferReader { + file_name: string; + source: BufferReader; +}; + +export interface FileNamedBufferBase64 { + file_name: string; + source: BufferBase64; +}; + +export type StudioVisibility = 'PUBLIC' | 'UNLISTED' | 'PRIVATE'; + +export interface UploadVideoDetails { + title?: string; + description?: string; + thumbnail?: FileNamedBufferReader; + playlists?: string[]; + audience?: 'MADE_FOR_KIDS'|'NOT_MADE_FOR_KIDS'; + + paid_promotion?: boolean; + ai_use?: boolean; + + collaboration_channels?: { + id: string; + analytics_setting: 'VIDEO_COLLABORATOR_ANALYTICS_SETTING_NONE'|'VIDEO_COLLABORATOR_ANALYTICS_SETTING_BASIC' + }[]; + + automatic_chapters?: boolean; + featured_places?: boolean; + automatic_concepts?: boolean; + tags?: string[]; + + video_language?: string; + caption_certification?: string; + title_and_description_language?: string; + + recording_date?: Date; + video_location?: string; + + license?: string; + allow_embedding?: boolean; + publish_to_subscriptions_feed_and_notify_subscribers?: boolean; + shorts_remixing?: 'ALLOW_VIDEO_AND_AUDIO'|'ALLOW_ONLY_AUDIO'|'DONT_ALLOW'; + category?: 'FILM'|'AUTOS'|'MUSIC'|'PETS'|'SPORTS'|'TRAVEL'|'GADGETS'|'PEOPLE'|'COMEDY'|'ENTERTAINMENT'|'NEWS'|'HOWTO'|'EDUCATION'|'SCIENCE'|'GOVERNMENT'; + + allow_comments?: 'ON'|'OFF'|'PAUSE'; + comment_moderation?: 'NONE'|'BASIC'|'STRICT'|'HOLD_ALL'; + who_can_comment?: 'ANYONE'|'SUBSCRIBERS_AND_MEMBERS'; + sort_comments_by?: 'TOP'|'NEWEST'; + show_how_many_viewers_like_this_video?: boolean; + + visibility?: StudioVisibility; + subtitles?: { + data: FileNamedBufferBase64; + synced: boolean; + auto_translate?: boolean; + overwrite?: boolean; + }; +}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 085291112d..0829b4bfb7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ export type { default as PlatformShim } from './PlatformShim.js'; +export type * from './StudioWebUploading.js'; export type * from './BotGuard.js'; export type * from './Cache.js'; export type * from './PlatformShim.js'; From 7793af53a4c9846d4a0b464fa08d7c2b5d1066cb Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 02:47:31 -0700 Subject: [PATCH 04/27] chore(YouTube Studio Web Examples): remove unecessary package-lock.json (whoops) --- examples/youtube-studio-web/package-lock.json | 722 ------------------ 1 file changed, 722 deletions(-) delete mode 100644 examples/youtube-studio-web/package-lock.json diff --git a/examples/youtube-studio-web/package-lock.json b/examples/youtube-studio-web/package-lock.json deleted file mode 100644 index 8a3c1e858b..0000000000 --- a/examples/youtube-studio-web/package-lock.json +++ /dev/null @@ -1,722 +0,0 @@ -{ - "name": "youtube-studio-web", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "youtube-studio-web", - "version": "0.0.0", - "dependencies": { - "bgutils-js": "^4.0.3", - "jsdom": "^28.1.0", - "youtubei.js": "file:../../" - }, - "devDependencies": { - "@types/jsdom": "^27.0.0", - "typescript": "^5.0.4" - } - }, - "../..": { - "name": "youtubei.js", - "version": "17.2.0", - "funding": [ - "https://github.com/sponsors/LuanRT" - ], - "license": "MIT", - "dependencies": { - "@bufbuild/protobuf": "^2.0.0", - "fflate": "^0.8.2", - "meriyah": "^6.1.4" - }, - "devDependencies": { - "@eslint/js": "^9.37.0", - "@types/estree": "^1.0.6", - "@types/node": "^25.0.3", - "@typescript-eslint/eslint-plugin": "^8.46.0", - "@typescript-eslint/parser": "^8.46.0", - "cpy-cli": "^6.0.0", - "esbuild": "^0.28.0", - "eslint": "^9.37.0", - "globals": "^17.0.0", - "replace": "^1.2.2", - "rimraf": "^6.0.1", - "ts-patch": "^3.0.2", - "ts-proto": "^2.2.0", - "typedoc": "^0.28.14", - "typedoc-plugin-markdown": "^4.9.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.46.0", - "vitest": "^3.2.4" - } - }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "license": "MIT" - }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "license": "MIT" - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", - "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.3.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", - "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@types/jsdom": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", - "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@types/jsdom/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/@types/jsdom/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/@types/node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", - "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/node/node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/bgutils-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bgutils-js/-/bgutils-js-4.0.3.tgz", - "integrity": "sha512-nvhrzqRYqFcC3AJbf1fENc/jzyMk95wH9UC9ruQzTZCq+itYWuHi2a9leK/bC1lLfi+R4a28wJZxpD5KiTFf+w==", - "funding": [ - "https://github.com/sponsors/LuanRT" - ], - "license": "MIT" - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/cssstyle": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", - "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.0.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.28", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.6" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", - "license": "MIT", - "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", - "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "license": "CC0-1.0" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "license": "MIT" - }, - "node_modules/tldts": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", - "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.10" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts/node_modules/tldts-core": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", - "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "license": "MIT" - }, - "node_modules/youtubei.js": { - "resolved": "../..", - "link": true - } - } -} From a438f6413652b4a36904b4c2aee20a9ae42ebaa0 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 03:51:26 -0700 Subject: [PATCH 05/27] feat(YouTube Studio Web): new access interface that requires channel id upfront + `updateVideo()`, `publishVideo()`, `uploadFeedbackCycle()`, `uploadFeedback()`, and `uploadVideo()` + update the example to use the new interface + add a wait util to `Utils.ts` Also last commit I forgot to _actually_ cache the sessionToken --- .../youtube-studio-web/src/GetSessionToken.ts | 4 +- .../youtube-studio-web/src/UploadVideo.ts | 57 +++ src/Innertube.ts | 5 +- src/core/clients/StudioWeb.ts | 398 ++++++++++++++++-- src/utils/Utils.ts | 6 + 5 files changed, 436 insertions(+), 34 deletions(-) create mode 100644 examples/youtube-studio-web/src/UploadVideo.ts diff --git a/examples/youtube-studio-web/src/GetSessionToken.ts b/examples/youtube-studio-web/src/GetSessionToken.ts index 244bf8a905..eecb9e6ba8 100644 --- a/examples/youtube-studio-web/src/GetSessionToken.ts +++ b/examples/youtube-studio-web/src/GetSessionToken.ts @@ -50,8 +50,8 @@ const COOKIES = ""; // ?? Place your YouTube cookies here // ?? Place your channel_ID here or use `await get_channel_id(yt)` const CHANNEL_ID = ""; - const yt_studio_web = yt.studioWeb; + const yt_studio_web = yt.studioWeb(CHANNEL_ID); yt_studio_web.setBotGuardSolver(botguard_solver); - const session_token = await yt_studio_web.getSessionToken(CHANNEL_ID); + const session_token = await yt_studio_web.getSessionToken(); console.log(session_token); })(); \ No newline at end of file diff --git a/examples/youtube-studio-web/src/UploadVideo.ts b/examples/youtube-studio-web/src/UploadVideo.ts new file mode 100644 index 0000000000..eecb9e6ba8 --- /dev/null +++ b/examples/youtube-studio-web/src/UploadVideo.ts @@ -0,0 +1,57 @@ +import { BotGuardClient } from "bgutils-js/botguard"; +import { USER_AGENT } from "bgutils-js/utils"; +import { JSDOM, VirtualConsole } from "jsdom"; +import Innertube, { UniversalCache, type Types } from 'youtubei.js'; + +const botguard_solver: Types.BotGuardSolver = { + solve: async(botguard_challenge, binding) => { + const virtual_console = new VirtualConsole(); + const dom = new JSDOM('', { url: "https://www.youtube.com", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, resources: "usable", runScripts: "dangerously", virtualConsole: virtual_console }); + + Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, origin: dom.window.origin }); + + if (!("navigator" in globalThis)) { + Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator }); + } + + Object.defineProperty(dom.window.HTMLCanvasElement.prototype, "getContext", { value: () => null, writable: true }); + + let interpreter_url = botguard_challenge.interpreter_url ?? ""; + + if (interpreter_url.startsWith("//")) interpreter_url = `https:${interpreter_url}`; + + const bg_script_response = await fetch(interpreter_url); + const interpreter_javascript = await bg_script_response.text(); + + new Function(interpreter_javascript)(); + + const botguard = await BotGuardClient.create({ program: botguard_challenge.program, globalName: botguard_challenge.global_name, globalObject: globalThis }); + + const botguard_response = await botguard.snapshot({ contentBinding: { atr_challenge: binding } }); + return botguard_response; + } +}; + +async function get_channel_id(yt: Innertube, index?: number): Promise { + const account_info = await yt.account.getInfo(true); + const account = account_info[index ?? 0]; + if (account === undefined) throw new Error("No accounts found"); + const resolve_url = `https://www.youtube.com/${account.channel_handle.text}`; + const resolved = await yt.resolveURL(resolve_url); + const channel_id = resolved.payload.browseId as string; + return channel_id; +} + +const COOKIES = ""; // ?? Place your YouTube cookies here + +(async () => { + const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); + + // ?? Place your channel_ID here or use `await get_channel_id(yt)` + const CHANNEL_ID = ""; + + const yt_studio_web = yt.studioWeb(CHANNEL_ID); + yt_studio_web.setBotGuardSolver(botguard_solver); + const session_token = await yt_studio_web.getSessionToken(); + console.log(session_token); +})(); \ No newline at end of file diff --git a/src/Innertube.ts b/src/Innertube.ts index c9abd5afa5..57eba13f27 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -592,9 +592,10 @@ export default class Innertube { /** * An interface for interacting with YouTube Studio Web. + * @param channel_id - the channel id to interface with */ - get studioWeb() { - return new StudioWeb(this.#session); + studioWeb(channel_id: string) { + return new StudioWeb(this.#session, channel_id); } /** diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index 851049e19f..f7c143dcdc 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -1,8 +1,8 @@ import type { BotGuardChallenge, BotGuardSolver } from '../../types/BotGuard.js'; import type { EngagementType } from '../../types/Misc.js'; -import type { FileNamedBufferReader, UploadVideoDetails } from '../../types/StudioWebUploading.js'; -import { Constants } from '../../utils/index.js'; -import { InnertubeError } from '../../utils/Utils.js'; +import type { ContinuationsUploadFeedback, CreateVideoResponse, CreatorVideo, FileNamedBufferReader, StudioVisibility, UploadVideoDetails } from '../../types/StudioWebUploading.js'; +import { Constants, Log } from '../../utils/index.js'; +import { InnertubeError, Platform, wait } from '../../utils/Utils.js'; import type { Actions, Session } from '../index.js'; type AttestationPlacement = 'none' | 'context' | 'top_level'; @@ -49,22 +49,88 @@ interface CreateCaptionsResponse { translation?: { captionsTranslations?: { cont interface ParseCaptionsResponse { captionSegments?: object } interface UpdateCaptionsResponse { framework_updates?: object } +interface MetadataUpdateResponse { videos?: CreatorVideo[] }; +type UpdateMetadataPayload = Record; + +interface UploadSubtitlesResponse { + created: CreateCaptionsResponse; + parsed: ParseCaptionsResponse; + updated: UpdateCaptionsResponse; +} + +interface UploadFeedbackResult { contents: ContinuationsUploadFeedback, next: () => Promise }; + +const VIDEO_READ_MASK = { + videoId: true, + channelId: true, + title: true, + description: true, + privacy: true, + status: true, + draftStatus: true, + shareUrl: true, + lengthSeconds: true, + videoDurationMs: true, + timeCreatedSeconds: true, + timePublishedSeconds: true, + thumbnailDetails: { all: true }, + responseStatus: { all: true }, + statusDetails: { all: true } +} as const; + +const CREATOR_VIDEO_CATEGORY_IDS = { + FILM: 1, AUTOS: 2, MUSIC: 10, PETS: 15, SPORTS: 17, TRAVEL: 19, GADGETS: 20, + PEOPLE: 22, COMEDY: 23, ENTERTAINMENT: 24, NEWS: 25, HOWTO: 26, EDUCATION: 27, + SCIENCE: 28, GOVERNMENT: 29 +} as const; +const ALLOW_COMMENT_MODES = { + NONE: 'ALL_COMMENTS', + BASIC: 'AUTOMATED_COMMENTS', + STRICT: 'AUTO_MODERATED_COMMENTS_HOLD_MORE', + HOLD_ALL: 'APPROVED_COMMENTS' +} as const; +const COMMENT_ENABLED_STATES = { + ON: 'MDE_COMMENT_ENABLED_STATE_ON', + OFF: 'MDE_COMMENT_ENABLED_STATE_OFF', + PAUSE: 'MDE_COMMENT_ENABLED_STATE_PAUSED' +} as const; +const ALLOWED_COMMENTER_MODES = { + ANYONE: 'MDE_ALLOWED_COMMENTER_MODE_ANYONE', + SUBSCRIBERS_AND_MEMBERS: 'MDE_ALLOWED_COMMENTER_MODE_SUBSCRIBERS_MEMBERS_APPROVED_USERS' +} as const; +const COMMENT_SORT_ORDERS = { + TOP: 'MDE_COMMENT_SORT_ORDER_TOP', + NEWEST: 'MDE_COMMENT_SORT_ORDER_LATEST' +} as const; +const REMIX_SOURCE_OPTIONS = { + ALLOW_VIDEO_AND_AUDIO: 'MDE_REMIX_SOURCE_OPTION_OPT_IN', + ALLOW_ONLY_AUDIO: 'MDE_REMIX_SOURCE_OPTION_VISUAL_OPT_OUT_AND_PERFORM_ACTIONS', + DONT_ALLOW: 'MDE_REMIX_SOURCE_OPTION_OPT_OUT_AND_MUTE_DERIVATIVES' +} as const; + +const UPLOAD_TYPES_TO_START_URL: Record = { + VIDEO: Constants.URLS.YT_UPLOAD_VIDEO_WEB, + THUMBNAIL: Constants.URLS.YT_UPLOAD_THUMBNAIL_WEB +} as const; + export default class StudioWeb { #session: Session; #actions: Actions; + #channel_id: string; #botguard_solver: BotGuardSolver|null; #unbound_challenge_cache: StudioUnboundChallenge|undefined; #auto_retry: boolean; #force_refresh_session_token: boolean; - #channel_id_session_token_cache: Record; + #channel_id_session_token_cache: StudioSessionTokenCache | null; - constructor(session: Session) { + constructor(session: Session, channel_id: string) { this.#session = session; this.#actions = session.actions; + this.#channel_id = channel_id; this.#botguard_solver = null; this.#auto_retry = true; this.#force_refresh_session_token = false; - this.#channel_id_session_token_cache = {}; + this.#channel_id_session_token_cache = null; if (!session.logged_in) throw new InnertubeError('You must be signed in to use this client.'); } @@ -116,8 +182,10 @@ export default class StudioWeb { return this.#unbound_challenge_cache; } - async #getCreatorStudioChallenge(channel_id: string, unbound_challenge_eats: string): Promise { - const creator_studio_challenge = await this.#attGet('ENGAGEMENT_TYPE_CREATOR_STUDIO_ACTION', [ { externalChannelId: channel_id } ], unbound_challenge_eats); + async #getCreatorStudioChallenge(unbound_challenge_eats: string): Promise { + const creator_studio_challenge = await this.#attGet('ENGAGEMENT_TYPE_CREATOR_STUDIO_ACTION', [ + { externalChannelId: this.#channel_id } + ], unbound_challenge_eats); if (!creator_studio_challenge.eats) throw new InnertubeError('Creator Studio challenge missing "eats"'); if (!creator_studio_challenge.challenge) throw new InnertubeError('Creator Studio challenge missing "challenge"'); @@ -151,12 +219,13 @@ export default class StudioWeb { return force_refresh_session_token; } - async getSessionToken(channel_id: string): Promise { + async getSessionToken(): Promise { if (!this.#botguard_solver) throw new InnertubeError('BotGuard Solver is not initialized. Please setup with setBotGuardSolver()'); - if (this.#channel_id_session_token_cache[channel_id]?.expires_at_ms > Date.now() && !this.#eatForceRefreshSessionToken()) return this.#channel_id_session_token_cache[channel_id].session_token; + if (this.#channel_id_session_token_cache && this.#channel_id_session_token_cache.expires_at_ms > Date.now() && !this.#eatForceRefreshSessionToken()) + return this.#channel_id_session_token_cache.session_token; const unbound_challenge = await this.#getUnboundChallenge(); - const creator_studio_challenge = await this.#getCreatorStudioChallenge(channel_id, unbound_challenge.eats); + const creator_studio_challenge = await this.#getCreatorStudioChallenge(unbound_challenge.eats); // don't cache this result into the unbounded cache since it uses different challenge kinda const botguard_response = await this.#botguard_solver.solve(unbound_challenge.bg_challenge, creator_studio_challenge.challenge); @@ -193,6 +262,10 @@ export default class StudioWeb { const grst_data = await this.#actions.execute('/ars/grst', { client: 'WEB_CREATOR', parse: true, ctx: grst_ctx, reauth_proof_token, eats: creator_studio_challenge.eats }); if (grst_data.session_token === undefined) throw new InnertubeError('/ars/grst did not return a session token'); + this.#channel_id_session_token_cache = { + session_token: grst_data.session_token, + expires_at_ms: unbound_challenge.expires_at_ms + }; return grst_data.session_token; } @@ -267,10 +340,6 @@ export default class StudioWeb { } async #uploadToScotty(upload_type: ScottyUploadType, file_name_buffer_reader: FileNamedBufferReader, start_payload: object, on_progress?: ScottyProgress): Promise { - const UPLOAD_TYPES_TO_START_URL: Record = { - VIDEO: Constants.URLS.YT_UPLOAD_VIDEO_WEB, - THUMBNAIL: Constants.URLS.YT_UPLOAD_THUMBNAIL_WEB - }; const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL[upload_type], file_name_buffer_reader, start_payload); const uploaded = await this.#scottyUploadChunks(start.upload_url, file_name_buffer_reader, on_progress); const resource_id = start.resource_id ?? uploaded.resource_id; @@ -282,16 +351,17 @@ export default class StudioWeb { return await this.#uploadToScotty('THUMBNAIL', file_name_buffer_reader, {}); } - async managedExecute(endpoint: string, payload: object, channel_id: string, attestation_placement: AttestationPlacement = 'none', eats?: string, is_retry = false): Promise { + async managedExecute(endpoint: string, payload: object, channel_id?: string, attestation_placement: AttestationPlacement = 'none', eats?: string, is_retry = false): Promise { const attestation_response_data = attestation_placement === 'none' ? {} : { attestationResponseData: await this.#getBotGuardAttestation() }; const response = await this.#actions.execute(endpoint, { client: 'WEB_CREATOR', - session_token: await this.getSessionToken(channel_id), + session_token: await this.getSessionToken(), ...payload, ...(attestation_placement === 'context' ? {} : { attestation_response_data }), ...(attestation_placement === 'top_level' ? {} : attestation_response_data), - ...(!eats ? {} : { eats }) + ...(!eats ? {} : { eats }), + ...(!channel_id ? {} : { channel_id }) }); if (!response.success) throw new InnertubeError(`YouTube Studio call to ${endpoint} failed`); const data = response.data as T & StudioChallengeResponseContext; @@ -306,7 +376,7 @@ export default class StudioWeb { return data; } - async #uploadSubtitles(channel_id: string, video_id: string, subtitles: NonNullable, language = 'en-US') { + async #uploadSubtitles(video_id: string, subtitles: NonNullable, language = 'en-US'): Promise { const data_base64 = await subtitles.data.source.base64; const data_uri = `data:application/octet-stream;base64,${data_base64}`; const tts_track_id = { lang: language, kind: '', name: '' }; @@ -314,12 +384,11 @@ export default class StudioWeb { const created = await this.managedExecute('/globalization/create_captions', { parse: true, videoId: video_id, - channelId: channel_id, + channelId: this.#channel_id, newTrack: tts_track_id, overwrite: subtitles.overwrite ?? true, - autoTranslate: subtitles.auto_translate ?? false, - channel_id - }, channel_id); + autoTranslate: subtitles.auto_translate ?? false + }, this.#channel_id); const content_update_time = created.translation?.captionsTranslations?.[0]?.contentUpdateTime; if (content_update_time === undefined) throw new InnertubeError('create_captions did not return a contentUpdateTime'); @@ -327,13 +396,12 @@ export default class StudioWeb { const parsed = await this.managedExecute('/globalization/parse_captions', { fileType: subtitles.synced ? 'CAPTIONS_FILE_TYPE_TIMED_TEXT' : 'CAPTIONS_FILE_TYPE_TRANSCRIPT', fileName: subtitles.data.file_name, - dataUri: data_uri, - channel_id - }, channel_id); + dataUri: data_uri + }, this.#channel_id); const updated = await this.managedExecute('/globalization/update_captions', { videoId: video_id, - channelId: channel_id, + channelId: this.#channel_id, operations: [ { ttsTrackId: tts_track_id, @@ -343,9 +411,279 @@ export default class StudioWeb { contentUpdateTime: content_update_time, captionsFile: { dataUri: data_uri, fileName: subtitles.data.file_name } } - ], - channel_id - }, channel_id); + ] + }, this.#channel_id); return { created, parsed, updated }; } + #isNumberString(value: string): boolean { + return !!value && !isNaN(Number(value)); + } + + #buildCommentOptions(details: Partial): UpdateMetadataPayload | undefined { + const enabled_state = details.allow_comments === undefined ? undefined : COMMENT_ENABLED_STATES[details.allow_comments]; + const has_options = enabled_state !== undefined + || details.sort_comments_by !== undefined + || details.show_how_many_viewers_like_this_video !== undefined + || details.comment_moderation !== undefined + || details.who_can_comment !== undefined; + if (!has_options) return undefined; + + const comment_options: UpdateMetadataPayload = {}; + if (enabled_state !== undefined) comment_options.newCommentEnabledState = enabled_state; + if (details.sort_comments_by !== undefined) comment_options.newDefaultSortOrder = COMMENT_SORT_ORDERS[details.sort_comments_by]; + if (details.show_how_many_viewers_like_this_video !== undefined) comment_options.newCanViewRatings = details.show_how_many_viewers_like_this_video; + if (enabled_state === undefined || enabled_state === COMMENT_ENABLED_STATES.ON) { + if (details.comment_moderation !== undefined) comment_options.newAllowCommentsMode = ALLOW_COMMENT_MODES[details.comment_moderation]; + if (details.who_can_comment !== undefined) comment_options.newAllowedCommenterMode = ALLOWED_COMMENTER_MODES[details.who_can_comment]; + } + return comment_options; + } + + #buildMetadataUpdate(details: UploadVideoDetails, thumbnail_resource_id?: string): UpdateMetadataPayload { + const payload: UpdateMetadataPayload = {}; + + if (details.title !== undefined) payload.title = { newTitle: details.title, titleOperation: 'MDE_TEXT_UPDATE_OPERATION_SET' }; + if (details.description !== undefined) payload.description = { newDescription: details.description, descriptionOperation: 'MDE_TEXT_UPDATE_OPERATION_SET' }; + if (details.tags !== undefined) payload.tags = { newTags: details.tags }; + if (details.playlists !== undefined) payload.addToPlaylist = { addToPlaylistIds: details.playlists, deleteFromPlaylistIds: [] }; + if (details.audience !== undefined) { + payload.madeForKids = { + operation: 'MDE_MADE_FOR_KIDS_UPDATE_OPERATION_SET', + newMfk: details.audience === 'MADE_FOR_KIDS' ? 'MDE_MADE_FOR_KIDS_TYPE_MFK' : 'MDE_MADE_FOR_KIDS_TYPE_NOT_MFK' + }; + } + if (details.paid_promotion !== undefined) payload.productPlacement = { newHasPaidProductPlacement: details.paid_promotion }; + if (details.ai_use !== undefined) { + payload.alteredContent = { + operation: 'MDE_ALTERED_CONTENT_UPDATE_OPERATION_SET', + newCreatorDisclosedHasAlteredContent: details.ai_use ? 'MDE_HAS_ALTERED_CONTENT_YES' : 'MDE_HAS_ALTERED_CONTENT_NO' + }; + } + + if (details.collaboration_channels) { + payload.collaboration = { + inviteCollaborators: details.collaboration_channels.map((channel) => ({ externalChannelId: channel.id, analyticsSetting: channel.analytics_setting })) + }; + } + + if (details.automatic_chapters !== undefined) payload.autoChapter = { creatorOptOut: !details.automatic_chapters }; + if (details.featured_places !== undefined) payload.autoPlaces = { creatorOptOut: !details.featured_places }; + if (details.automatic_concepts !== undefined) payload.learningConcepts = { autoConceptsCreatorOptOut: !details.automatic_concepts }; + + if (details.video_language !== undefined) payload.audioLanguage = { newAudioLanguage: details.video_language }; + if (details.title_and_description_language !== undefined) payload.metadataLanguage = { newMetadataLanguage: details.title_and_description_language }; + if (details.caption_certification !== undefined) payload.captionsCertificate = { newUncaptionedReason: details.caption_certification }; + + if (details.recording_date !== undefined) { + payload.recordedDate = { + operation: 'MDE_RECORDED_DATE_UPDATE_OPERATION_SET', + newRecordedDate: { + year: details.recording_date.getFullYear(), + month: details.recording_date.getMonth() + 1, + day: details.recording_date.getDate() + } + }; + } + if (details.video_location !== undefined) { + payload.location = { operation: 'MDE_LOCATION_UPDATE_OPERATION_SET_LOCATION', description: details.video_location }; + } + + if (details.license !== undefined) payload.license = { newLicenseId: details.license }; + if (details.allow_embedding !== undefined) payload.distributionOptions = { newAllowEmbedding: details.allow_embedding }; + if (details.publish_to_subscriptions_feed_and_notify_subscribers !== undefined) { + payload.publishingOptions = { newPostToFeed: details.publish_to_subscriptions_feed_and_notify_subscribers }; + } + if (details.shorts_remixing !== undefined) { + payload.remix = { operation: 'MDE_REMIX_UPDATE_OPERATION_SET', newRemixSourceOption: REMIX_SOURCE_OPTIONS[details.shorts_remixing] }; + } + if (details.category !== undefined) { + const category_id = this.#isNumberString(details.category) ? Number(details.category) : CREATOR_VIDEO_CATEGORY_IDS[details.category.toUpperCase() as keyof typeof CREATOR_VIDEO_CATEGORY_IDS]; + if (category_id !== undefined) payload.category = { newCategoryId: category_id }; + } + + if (details.visibility) { + payload.privacyState = { newPrivacy: details.visibility }; + } + + const comment_options = this.#buildCommentOptions(details); + if (comment_options !== undefined) payload.commentOptions = comment_options; + + if (thumbnail_resource_id !== undefined) { + payload.videoStill = { + operation: 'UPLOAD_CUSTOM_THUMBNAIL', + image: { + encryptedScottyResourceId: thumbnail_resource_id, + name: 'CUSTOM_THUMBNAIL_IMAGE_NAME_DEFAULT', + format: 'CUSTOM_THUMBNAIL_IMAGE_FORMAT_JPEG' + } + }; + } + + return payload; + } + + async #updateMetadata(video_id: string, payload: UpdateMetadataPayload): Promise { + return await this.managedExecute('/video_manager/metadata_update', { + encryptedVideoId: video_id, + videoReadMask: VIDEO_READ_MASK, + flowType: 'MDE_FLOW_TYPE_UPLOAD', + ...payload + }, this.#channel_id, 'top_level'); + } + + async updateVideo(video_id: string, details: Partial) { + let thumbnail_resource_id: string | undefined; + if (details.thumbnail !== undefined) { + const resource_id = await this.#uploadThumbnailResource(details.thumbnail); + thumbnail_resource_id = resource_id; + } + + const payload = this.#buildMetadataUpdate(details, thumbnail_resource_id); + + let update_metadata_response: MetadataUpdateResponse | object = {}; + let update_subtitles_response: UploadSubtitlesResponse | object = {}; + + if (Object.keys(payload).length > 0) { + update_metadata_response = await this.#updateMetadata(video_id, payload); + } + if (details.subtitles !== undefined) { + update_subtitles_response = await this.#uploadSubtitles(video_id, details.subtitles, details.video_language); + } + return { update_metadata_response, update_subtitles_response }; + } + + async publishVideo(video_id: string, visibility: StudioVisibility = 'PRIVATE') { + const update_metadata_response = await this.#updateMetadata(video_id, { + privacyState: { newPrivacy: visibility }, + draftState: { operation: 'MDE_DRAFT_STATE_UPDATE_OPERATION_REMOVE_DRAFT_STATE' } + }); + return update_metadata_response; + } + + #tryGetFeedbackToken(feedback: ContinuationsUploadFeedback): string | null { + try { + const feedback_token = feedback[0].uploadFeedbackItemContinuation.continuations[0].uploadFeedbackRefreshContinuation?.continuation; + if (feedback_token) return feedback_token; + const timed_token = feedback[0].uploadFeedbackItemContinuation.continuations[0].timedContinuationData?.continuation; + if (timed_token) return timed_token; + return null; + } catch (_) { + return null; + } + } + #tryGetFeedbackDelay(feedback: ContinuationsUploadFeedback): number | null { + try { + const feedback_timeout = feedback[0].uploadFeedbackItemContinuation.continuations[0].uploadFeedbackRefreshContinuation?.continueInMs; + if (feedback_timeout) return feedback_timeout; + const timed_timeout = feedback[0].uploadFeedbackItemContinuation.continuations[0].timedContinuationData?.timeoutMs; + if (timed_timeout) return timed_timeout; + return null; + } catch (_) { + return null; + } + } + + // from https://studio.youtube.com/youtubei/v1/creator/get_channel_dashboard?alt=json under interactionRecordingParams; likely useless + async uploadFeedback(tokens: string[], type: 'FEEDBACK_TOKENS'): Promise<{ isProcessed: boolean }> + // initial from createvideo under uploadFeedbackRefreshContinuation + async uploadFeedback(tokens: string[], type: 'CONTINUATION_TOKENS'): Promise + async uploadFeedback(tokens: string[], type: 'FEEDBACK_TOKENS' | 'CONTINUATION_TOKENS'): Promise<{ isProcessed: boolean } | UploadFeedbackResult | null> { + if (tokens.filter((token) => token).length === 0) return null; + const feedback_data = await this.managedExecute<{ feedbackResponse?: { isProcessed: boolean }, continuationContents?: ContinuationsUploadFeedback }>('upload/feedback', + type === 'CONTINUATION_TOKENS' ? { continuations: tokens } : { feedbackTokens: tokens }); + try { + if (type === 'FEEDBACK_TOKENS') return feedback_data.feedbackResponse as { isProcessed: boolean }; + const contents = feedback_data.continuationContents as ContinuationsUploadFeedback; + return { + contents, next: async () => await this.uploadFeedback([ + this.#tryGetFeedbackToken(contents) ?? '' + ], 'CONTINUATION_TOKENS') + }; + } catch (_) { + return null; + } + } + + async uploadFeedbackCycle(initial_tokens: (string | null)[], callback_continue: (content: ContinuationsUploadFeedback) => boolean) { + if (initial_tokens.some((token) => token === null)) return; + let feedback: null | UploadFeedbackResult = await this.uploadFeedback(initial_tokens as string[], 'CONTINUATION_TOKENS'); + if (feedback === null) return; + do { + try { + if (!callback_continue(feedback.contents)) break; + const delay = this.#tryGetFeedbackDelay(feedback.contents); + if (delay === null) { + Log.warn('upload feedback delay is null; exiting for safety'); + break; + } + if (delay < 1000) { + Log.warn('upload feedback delay is unusually low; exiting for safety'); + break; + } + await wait(delay); + } catch (e) { + const error = e as Error; + Log.warn(error.message); + } + } + while ((feedback = await feedback.next()) !== null); + } + + #tryGetUploadVideoFeedbackToken(created: CreateVideoResponse): string | null { + try { + const feedback = created.contents.uploadFeedbackItemRenderer.continuations[0].uploadFeedbackRefreshContinuation?.continuation; + if (feedback) return feedback; + const timed = created.contents.uploadFeedbackItemRenderer.continuations[0].timedContinuationData?.continuation; + if (timed) return timed; + return null; + } catch (_) { + return null; + } + } + + async uploadVideo(channel_id: string, file: FileNamedBufferReader, details: Partial = {}, on_scotty_progress?: (written_bytes: number, total_bytes: number) => void, + on_initial_create_video?: (full_created: { created: CreateVideoResponse, feedback_token: string | null }) => any) { + const frontend_upload_id = `innertube_studio:${Platform.shim.uuidv4().toUpperCase()}:0`; + const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL['VIDEO'], file, { frontendUploadId: frontend_upload_id }); + + const chunks_uploaded = this.#scottyUploadChunks(start.upload_url, file, on_scotty_progress); + + const created = await this.managedExecute('/upload/createvideo', { + channelId: channel_id, + resourceId: { scottyResourceId: { id: start.resource_id } }, + frontendUploadId: frontend_upload_id, + initialMetadata: { + title: { newTitle: details.title ?? file.file_name }, + privacy: { newPrivacy: 'PRIVATE' }, + draftState: { isDraft: true }, + ...(details.tags === undefined ? {} : { tags: { newTags: details.tags } }), + // audience here is age restriction, not made for kids; details.audience goes up as madeForKids below + targetedAudience: { + operation: 'MDE_TARGETED_AUDIENCE_UPDATE_OPERATION_SET', + newTargetedAudience: 'MDE_TARGETED_AUDIENCE_TYPE_ALL' + } + }, + contentLevelProtection: { enableRequiresContentLevelProtection: false }, + presumedShort: false, + channel_id + }, channel_id, 'context'); // createvideo reads its snapshot out of context.request + + on_initial_create_video?.({ created, feedback_token: this.#tryGetUploadVideoFeedbackToken(created) }); + + const video_id = created.videoId; + if (video_id === undefined || video_id === '') { + await chunks_uploaded; + throw new InnertubeError('createvideo did not return a videoId'); + } + + const uploaded = await chunks_uploaded; + + // title and tags already went up with createvideo + const { title: _title, tags: _tags, visibility, ...remaining_details } = details; + const updated = await this.updateVideo(video_id, remaining_details); + + const published = await this.publishVideo(video_id, visibility); + + return { created, updated, published }; + } } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index d484057acc..bb93ec475f 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -291,4 +291,10 @@ export function getNsigProcessorFn(n?: string | null, sp?: string | null, s?: st } return process("${n || ''}", "${sp || ''}", "${s || ''}");`; +} + +export async function wait(milliseconds: number) { + return new Promise(function (resolve) { + setTimeout(resolve, milliseconds, 'HASH_TIMED_OUT'); + }); } \ No newline at end of file From 745db99f5ec621368cc5150d0c69aaecc7d211a6 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 03:52:11 -0700 Subject: [PATCH 06/27] chore(YouTube Studio Web): I forgot to commit the updated types from last commit --- src/types/StudioWebUploading.ts | 213 +++++++++++++++++++++++++++++++- 1 file changed, 212 insertions(+), 1 deletion(-) diff --git a/src/types/StudioWebUploading.ts b/src/types/StudioWebUploading.ts index e9e5486882..b9ba346581 100644 --- a/src/types/StudioWebUploading.ts +++ b/src/types/StudioWebUploading.ts @@ -69,4 +69,215 @@ export interface UploadVideoDetails { auto_translate?: boolean; overwrite?: boolean; }; -}; \ No newline at end of file +}; + +export interface CreatorVideo { + videoId: string; + channelId?: string; + title?: string; + description?: string; + privacy?: 'VIDEO_PRIVACY_PUBLIC' | 'VIDEO_PRIVACY_UNLISTED' | 'VIDEO_PRIVACY_PRIVATE'; + status?: 'VIDEO_STATUS_PROCESSED'; + draftStatus?: 'DRAFT_STATUS_NONE'; + shareUrl?: string; + watchUrl?: string; + lengthSeconds?: string; + videoDurationMs?: number; + timeCreatedSeconds?: string; + timePublishedSeconds?: string; + thumbnailDetails?: { thumbnails: { url: string, width: number, height: number }[] }; +}; + +// [Raw Types] + +export interface CreateVideoResponse { + responseContext: ResponseContext; + videoId: string; + contents: Contents; +} + +export interface ResponseContext { + serviceTrackingParams: ServiceTrackingParam[]; + consistencyTokenJar: ConsistencyTokenJar; + stateTags: StateTags; + responseId: string; + webResponseContextExtensionData: WebResponseContextExtensionData; +} + +export interface ServiceTrackingParam { + service: string; + params: Param[]; +} + +export interface Param { + key: string; + value: string; +} + +export interface ConsistencyTokenJar { + encryptedTokenJarContents: string; + expirationSeconds: string; +} + +export interface StateTags { + stateTagsModified: number[]; +} + +export interface WebResponseContextExtensionData { + hasDecorated: boolean; + challenge?: { type?: 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE' } +} + +export interface Contents { + uploadFeedbackItemRenderer: UploadFeedbackItemRenderer; +} + +export interface UploadFeedbackItemRenderer { + id: Id; + continuations: Continuation[]; + dataFreshnessEntity: DataFreshnessEntity; +} + +export interface Id { + frontendUploadId: string; + videoId: string; +} + +export interface Continuation { + timedContinuationData?: TimedContinuationData; + uploadFeedbackRefreshContinuation?: UploadFeedbackRefreshContinuation; +} + +export interface TimedContinuationData { + timeoutMs: number; + continuation: string; + clickTrackingParams: string; +} + +export interface UploadFeedbackRefreshContinuation { + continuation: string; + continueInMs: number; + clickTrackingParams: string; +} + +export interface DataFreshnessEntity { + key: string; + lastUpdated: LastUpdated; +} + +export interface LastUpdated { + seconds: string; + nanos: number; +} + +// [FeedbackContinuation] ========================================================== + +export type ContinuationsUploadFeedback = ContinuationUploadFeedback[]; + +export interface ContinuationUploadFeedback { + uploadFeedbackItemContinuation: UploadFeedbackItemContinuation; +} + +export interface UploadFeedbackItemContinuation { + id: Id; + contents: Content[]; + continuations: Continuation[]; + dataFreshnessEntity: DataFreshnessEntity; +} + +export interface Id { + frontendUploadId: string; + videoId: string; +} + +export interface Content { + transferProgressBar?: TransferProgressBar; + uploadChecksRenderer?: UploadChecksRenderer; +} + +export interface TransferProgressBar { + fractionCompleted: number; + progressMessage: ProgressMessage; +} + +export interface ProgressMessage { + simpleText: string; +} + +export interface UploadChecksRenderer { + checksDataVideoMonetized: ChecksDataVideoMonetized; + checksDataVideoNotMonetized: ChecksDataVideoNotMonetized; +} + +export interface ChecksDataVideoMonetized { + checksSummary: ChecksSummary; + copyrightCheck: CopyrightCheck; + adSuitabilityCheck: AdSuitabilityCheck; + communityGuidelinesCheck: CommunityGuidelinesCheck; +} + +export interface ChecksSummary { + status: string; +} + +export interface CopyrightCheck { + checkStatus: string; +} + +export interface AdSuitabilityCheck { + checkStatus: string; +} + +export interface CommunityGuidelinesCheck { + checkStatus: string; +} + +export interface ChecksDataVideoNotMonetized { + checksSummary: ChecksSummary2; + copyrightCheck: CopyrightCheck2; + adSuitabilityCheck: AdSuitabilityCheck2; + communityGuidelinesCheck: CommunityGuidelinesCheck2; +} + +export interface ChecksSummary2 { + status: string; +} + +export interface CopyrightCheck2 { + checkStatus: string; +} + +export interface AdSuitabilityCheck2 { + checkStatus: string; +} + +export interface CommunityGuidelinesCheck2 { + checkStatus: string; +} + +export interface Continuation { + timedContinuationData?: TimedContinuationData; + uploadFeedbackRefreshContinuation?: UploadFeedbackRefreshContinuation; +} + +export interface TimedContinuationData { + timeoutMs: number; + continuation: string; + clickTrackingParams: string; +} + +export interface UploadFeedbackRefreshContinuation { + continuation: string; + continueInMs: number; + clickTrackingParams: string; +} + +export interface DataFreshnessEntity { + key: string; + lastUpdated: LastUpdated; +} + +export interface LastUpdated { + seconds: string; + nanos: number; +} From 8da2107e433d9cb8212c566f1bac3563edb2f1b9 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 03:54:51 -0700 Subject: [PATCH 07/27] fix(YouTube Studio Web): make `uploadVideo` use the new single channel_id system --- src/core/clients/StudioWeb.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index f7c143dcdc..078832cbfa 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -641,7 +641,7 @@ export default class StudioWeb { } } - async uploadVideo(channel_id: string, file: FileNamedBufferReader, details: Partial = {}, on_scotty_progress?: (written_bytes: number, total_bytes: number) => void, + async uploadVideo(file: FileNamedBufferReader, details: Partial = {}, on_scotty_progress?: (written_bytes: number, total_bytes: number) => void, on_initial_create_video?: (full_created: { created: CreateVideoResponse, feedback_token: string | null }) => any) { const frontend_upload_id = `innertube_studio:${Platform.shim.uuidv4().toUpperCase()}:0`; const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL['VIDEO'], file, { frontendUploadId: frontend_upload_id }); @@ -649,7 +649,7 @@ export default class StudioWeb { const chunks_uploaded = this.#scottyUploadChunks(start.upload_url, file, on_scotty_progress); const created = await this.managedExecute('/upload/createvideo', { - channelId: channel_id, + channelId: this.#channel_id, resourceId: { scottyResourceId: { id: start.resource_id } }, frontendUploadId: frontend_upload_id, initialMetadata: { @@ -664,9 +664,8 @@ export default class StudioWeb { } }, contentLevelProtection: { enableRequiresContentLevelProtection: false }, - presumedShort: false, - channel_id - }, channel_id, 'context'); // createvideo reads its snapshot out of context.request + presumedShort: false + }, this.#channel_id, 'context'); // createvideo reads its snapshot out of context.request on_initial_create_video?.({ created, feedback_token: this.#tryGetUploadVideoFeedbackToken(created) }); From d348e96c0ef0d124038c7736613b127cfa237fcb Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 04:41:03 -0700 Subject: [PATCH 08/27] fix(YouTube Studio Web): `uploadSubtitles` was broken because of an accidental `parse: true` and make `uploadSubtitles` public --- src/core/clients/StudioWeb.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index 078832cbfa..f052353846 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -376,13 +376,12 @@ export default class StudioWeb { return data; } - async #uploadSubtitles(video_id: string, subtitles: NonNullable, language = 'en-US'): Promise { + async uploadSubtitles(video_id: string, subtitles: NonNullable, language = 'en-US'): Promise { const data_base64 = await subtitles.data.source.base64; const data_uri = `data:application/octet-stream;base64,${data_base64}`; const tts_track_id = { lang: language, kind: '', name: '' }; const created = await this.managedExecute('/globalization/create_captions', { - parse: true, videoId: video_id, channelId: this.#channel_id, newTrack: tts_track_id, @@ -547,7 +546,7 @@ export default class StudioWeb { update_metadata_response = await this.#updateMetadata(video_id, payload); } if (details.subtitles !== undefined) { - update_subtitles_response = await this.#uploadSubtitles(video_id, details.subtitles, details.video_language); + update_subtitles_response = await this.uploadSubtitles(video_id, details.subtitles, details.video_language); } return { update_metadata_response, update_subtitles_response }; } From 0e4d89f5927d88e9f19e3a265dd19c91702cec15 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 04:43:14 -0700 Subject: [PATCH 09/27] feat(YouTube Studio Web Examples): two working good examples of usage :3 --- .../youtube-studio-web/src/GetSessionToken.ts | 47 +---------- .../youtube-studio-web/src/UploadVideo.ts | 79 ++++++++----------- examples/youtube-studio-web/src/utils.ts | 75 ++++++++++++++++++ examples/youtube-studio-web/tsconfig.json | 3 +- 4 files changed, 113 insertions(+), 91 deletions(-) create mode 100644 examples/youtube-studio-web/src/utils.ts diff --git a/examples/youtube-studio-web/src/GetSessionToken.ts b/examples/youtube-studio-web/src/GetSessionToken.ts index eecb9e6ba8..b5d74c1d7b 100644 --- a/examples/youtube-studio-web/src/GetSessionToken.ts +++ b/examples/youtube-studio-web/src/GetSessionToken.ts @@ -1,46 +1,5 @@ -import { BotGuardClient } from "bgutils-js/botguard"; -import { USER_AGENT } from "bgutils-js/utils"; -import { JSDOM, VirtualConsole } from "jsdom"; -import Innertube, { UniversalCache, type Types } from 'youtubei.js'; - -const botguard_solver: Types.BotGuardSolver = { - solve: async(botguard_challenge, binding) => { - const virtual_console = new VirtualConsole(); - const dom = new JSDOM('', { url: "https://www.youtube.com", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, resources: "usable", runScripts: "dangerously", virtualConsole: virtual_console }); - - Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, origin: dom.window.origin }); - - if (!("navigator" in globalThis)) { - Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator }); - } - - Object.defineProperty(dom.window.HTMLCanvasElement.prototype, "getContext", { value: () => null, writable: true }); - - let interpreter_url = botguard_challenge.interpreter_url ?? ""; - - if (interpreter_url.startsWith("//")) interpreter_url = `https:${interpreter_url}`; - - const bg_script_response = await fetch(interpreter_url); - const interpreter_javascript = await bg_script_response.text(); - - new Function(interpreter_javascript)(); - - const botguard = await BotGuardClient.create({ program: botguard_challenge.program, globalName: botguard_challenge.global_name, globalObject: globalThis }); - - const botguard_response = await botguard.snapshot({ contentBinding: { atr_challenge: binding } }); - return botguard_response; - } -}; - -async function get_channel_id(yt: Innertube, index?: number): Promise { - const account_info = await yt.account.getInfo(true); - const account = account_info[index ?? 0]; - if (account === undefined) throw new Error("No accounts found"); - const resolve_url = `https://www.youtube.com/${account.channel_handle.text}`; - const resolved = await yt.resolveURL(resolve_url); - const channel_id = resolved.payload.browseId as string; - return channel_id; -} +import Innertube, { UniversalCache } from 'youtubei.js'; +import { botguard_solver, get_channel_id } from './utils.ts'; const COOKIES = ""; // ?? Place your YouTube cookies here @@ -48,7 +7,7 @@ const COOKIES = ""; // ?? Place your YouTube cookies here const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); // ?? Place your channel_ID here or use `await get_channel_id(yt)` - const CHANNEL_ID = ""; + const CHANNEL_ID = ''; const yt_studio_web = yt.studioWeb(CHANNEL_ID); yt_studio_web.setBotGuardSolver(botguard_solver); diff --git a/examples/youtube-studio-web/src/UploadVideo.ts b/examples/youtube-studio-web/src/UploadVideo.ts index eecb9e6ba8..60f4babcd3 100644 --- a/examples/youtube-studio-web/src/UploadVideo.ts +++ b/examples/youtube-studio-web/src/UploadVideo.ts @@ -1,57 +1,44 @@ -import { BotGuardClient } from "bgutils-js/botguard"; -import { USER_AGENT } from "bgutils-js/utils"; -import { JSDOM, VirtualConsole } from "jsdom"; -import Innertube, { UniversalCache, type Types } from 'youtubei.js'; - -const botguard_solver: Types.BotGuardSolver = { - solve: async(botguard_challenge, binding) => { - const virtual_console = new VirtualConsole(); - const dom = new JSDOM('', { url: "https://www.youtube.com", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, resources: "usable", runScripts: "dangerously", virtualConsole: virtual_console }); - - Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, origin: dom.window.origin }); - - if (!("navigator" in globalThis)) { - Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator }); - } - - Object.defineProperty(dom.window.HTMLCanvasElement.prototype, "getContext", { value: () => null, writable: true }); - - let interpreter_url = botguard_challenge.interpreter_url ?? ""; - - if (interpreter_url.startsWith("//")) interpreter_url = `https:${interpreter_url}`; - - const bg_script_response = await fetch(interpreter_url); - const interpreter_javascript = await bg_script_response.text(); - - new Function(interpreter_javascript)(); - - const botguard = await BotGuardClient.create({ program: botguard_challenge.program, globalName: botguard_challenge.global_name, globalObject: globalThis }); - - const botguard_response = await botguard.snapshot({ contentBinding: { atr_challenge: binding } }); - return botguard_response; - } -}; - -async function get_channel_id(yt: Innertube, index?: number): Promise { - const account_info = await yt.account.getInfo(true); - const account = account_info[index ?? 0]; - if (account === undefined) throw new Error("No accounts found"); - const resolve_url = `https://www.youtube.com/${account.channel_handle.text}`; - const resolved = await yt.resolveURL(resolve_url); - const channel_id = resolved.payload.browseId as string; - return channel_id; -} +import Innertube, { UniversalCache } from 'youtubei.js'; +import { botguard_solver, get_channel_id, get_file_named_buffer_base64, get_file_named_buffer_reader } from "./utils.ts"; const COOKIES = ""; // ?? Place your YouTube cookies here +const VIDEO_FILE_PATH = ""; // ?? Place your video file path here +const THUMBNAIL_FILE_PATH = ""; // ?? Place your thumbnail file path here +const SRT_FILE_PATH = ""; // ?? Place your srt file path here (async () => { const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); // ?? Place your channel_ID here or use `await get_channel_id(yt)` - const CHANNEL_ID = ""; + const CHANNEL_ID = ''; const yt_studio_web = yt.studioWeb(CHANNEL_ID); yt_studio_web.setBotGuardSolver(botguard_solver); - const session_token = await yt_studio_web.getSessionToken(); - console.log(session_token); + + console.log('creating...'); + + const created_response = await yt_studio_web.uploadVideo(await get_file_named_buffer_reader(VIDEO_FILE_PATH), { + title: "This is a test upload", + thumbnail: await get_file_named_buffer_reader(THUMBNAIL_FILE_PATH), + subtitles: { + synced: true, + data: await get_file_named_buffer_base64(SRT_FILE_PATH) + }, + description: "This is a test description", + visibility: "PRIVATE" + }, (written_bytes, total_bytes) => { + // NOTE this callback only fires around every 100mb + console.log(`${written_bytes / total_bytes}%`); + }, async (full_created) => { + // NOTE this cycle fires every 2s-ish + await yt_studio_web.uploadFeedbackCycle([full_created.feedback_token], (content) => { + if (content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.transferProgressBar) + console.log(content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.transferProgressBar); + else if (content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.uploadChecksRenderer) + console.log(content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.uploadChecksRenderer); + else console.log(content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]); + return true; + }); + }); + console.log(created_response); })(); \ No newline at end of file diff --git a/examples/youtube-studio-web/src/utils.ts b/examples/youtube-studio-web/src/utils.ts new file mode 100644 index 0000000000..c2163a0ff5 --- /dev/null +++ b/examples/youtube-studio-web/src/utils.ts @@ -0,0 +1,75 @@ +import { BotGuardClient } from "bgutils-js/botguard"; +import { USER_AGENT } from "bgutils-js/utils"; +import { JSDOM, VirtualConsole } from "jsdom"; +import Innertube, { type Types } from 'youtubei.js'; +import path from "path"; +import fs from 'fs/promises'; + +export const botguard_solver: Types.BotGuardSolver = { + solve: async(botguard_challenge, binding) => { + const virtual_console = new VirtualConsole(); + const dom = new JSDOM('', { url: "https://www.youtube.com", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, resources: "usable", runScripts: "dangerously", virtualConsole: virtual_console }); + + Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, origin: dom.window.origin }); + + if (!("navigator" in globalThis)) { + Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator }); + } + + Object.defineProperty(dom.window.HTMLCanvasElement.prototype, "getContext", { value: () => null, writable: true }); + + let interpreter_url = botguard_challenge.interpreter_url ?? ""; + + if (interpreter_url.startsWith("//")) interpreter_url = `https:${interpreter_url}`; + + const bg_script_response = await fetch(interpreter_url); + const interpreter_javascript = await bg_script_response.text(); + + new Function(interpreter_javascript)(); + + const botguard = await BotGuardClient.create({ program: botguard_challenge.program, globalName: botguard_challenge.global_name, globalObject: globalThis }); + + const botguard_response = await botguard.snapshot({ contentBinding: { atr_challenge: binding } }); + return botguard_response; + } +}; + +export async function get_channel_id(yt: Innertube, index?: number): Promise { + const account_info = await yt.account.getInfo(true); + const account = account_info[index ?? 0]; + if (account === undefined) throw new Error("No accounts found"); + const resolve_url = `https://www.youtube.com/${account.channel_handle.text}`; + const resolved = await yt.resolveURL(resolve_url); + const channel_id = resolved.payload.browseId as string; + return channel_id; +} + +export async function get_file_named_buffer_reader(file_path: string): Promise { + const stats = await fs.stat(file_path); + return { + file_name: path.basename(file_path), + source: { + total_bytes: stats.size, + read_chunk: async (position: number, length: number) => { + let handle: fs.FileHandle | undefined = undefined; + try { + handle = await fs.open(file_path, "r"); + const buffer = Buffer.allocUnsafe(length); + const { bytesRead } = await handle.read(buffer, 0, length, position); + return new Uint8Array(buffer.buffer, buffer.byteOffset, bytesRead); + } finally { + await handle?.close(); + } + } + } + }; +} + +export async function get_file_named_buffer_base64(file_path: string): Promise { + return { + file_name: path.basename(file_path), + source: { + base64: (await fs.readFile(file_path, { encoding: "base64" })).toString() + } + }; +} \ No newline at end of file diff --git a/examples/youtube-studio-web/tsconfig.json b/examples/youtube-studio-web/tsconfig.json index 0b71cf69f4..f3edf9b9a4 100644 --- a/examples/youtube-studio-web/tsconfig.json +++ b/examples/youtube-studio-web/tsconfig.json @@ -14,6 +14,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, - "skipLibCheck": true + "skipLibCheck": true, + "allowImportingTsExtensions": true } } From 81006dbf8c92aafd4f9146d8bce33a19449ca98c Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 05:20:16 -0700 Subject: [PATCH 10/27] refactor(YouTube Studio Web): put initial EATS in constants --- src/core/Actions.ts | 4 ++-- src/utils/Constants.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/Actions.ts b/src/core/Actions.ts index c6cf00c47b..acd2d044b4 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -22,6 +22,7 @@ import { } from '../../protos/generated/youtube/api/pfiinnertube/user_info.js'; import type { Session } from './index.js'; +import { Constants } from '../utils/index.js'; export interface ApiResponse { success: boolean; @@ -165,8 +166,7 @@ export default class Actions { this.session.context.request.eats = data?.eats; delete data?.eats; } else { - // TODO put this in CONSTANTS - this.session.context.request.eats = 'AeCS5zA8mwKJA3VzvwD--o2-ZsGbWYFt6LMN2EuOPJLQrg6MIuKxBpbf9WNlMPlARBhbMM-hSWg982LQDZEnOBj-yHFw1TDTzIUdINTExUA6U5lOgLtxyv6guJS9HQ=='; + this.session.context.request.eats = Constants.CLIENTS.WEB_CREATOR.EATS; } if (data.reauth_proof_token) { diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 5778427330..ceba20df53 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -102,7 +102,8 @@ export const CLIENTS = { API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', API_VERSION: 'v1', STATIC_VISITOR_ID: '6zpwvWUNAco', - USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36' + USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36', + EATS: 'AeCS5zA8mwKJA3VzvwD--o2-ZsGbWYFt6LMN2EuOPJLQrg6MIuKxBpbf9WNlMPlARBhbMM-hSWg982LQDZEnOBj-yHFw1TDTzIUdINTExUA6U5lOgLtxyv6guJS9HQ==' } } as const; /** From c054521c834c7f1c1cc0aabd2602df447ce1774e Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 05:22:42 -0700 Subject: [PATCH 11/27] chore(YouTube Studio Web): clean unecessary comments from porting --- src/core/clients/StudioWeb.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index f052353846..e48647fda5 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -656,7 +656,6 @@ export default class StudioWeb { privacy: { newPrivacy: 'PRIVATE' }, draftState: { isDraft: true }, ...(details.tags === undefined ? {} : { tags: { newTags: details.tags } }), - // audience here is age restriction, not made for kids; details.audience goes up as madeForKids below targetedAudience: { operation: 'MDE_TARGETED_AUDIENCE_UPDATE_OPERATION_SET', newTargetedAudience: 'MDE_TARGETED_AUDIENCE_TYPE_ALL' @@ -664,7 +663,7 @@ export default class StudioWeb { }, contentLevelProtection: { enableRequiresContentLevelProtection: false }, presumedShort: false - }, this.#channel_id, 'context'); // createvideo reads its snapshot out of context.request + }, this.#channel_id, 'context'); on_initial_create_video?.({ created, feedback_token: this.#tryGetUploadVideoFeedbackToken(created) }); From 15c3964bebd4d6f491bc2cce1db6a44071ec7cc1 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 05:43:48 -0700 Subject: [PATCH 12/27] chore(Utils): remove unecessary arg in `wait` `setTimeout` --- src/utils/Utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index bb93ec475f..0f092f43b6 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -295,6 +295,6 @@ return process("${n || ''}", "${sp || ''}", "${s || ''}");`; export async function wait(milliseconds: number) { return new Promise(function (resolve) { - setTimeout(resolve, milliseconds, 'HASH_TIMED_OUT'); + setTimeout(resolve, milliseconds); }); } \ No newline at end of file From 66b2fd9697ca0ebb83273002588c1f5da0814d86 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 05:45:36 -0700 Subject: [PATCH 13/27] fix(Parser): fix potentaily bug from checking wheather a boolean is actually in the data --- src/parser/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 500605dba1..e5d5fd51e4 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -522,7 +522,7 @@ export function parseResponse(data: parsed_data.ctx = data.ctx; } - if (data.shouldFetchReauthSessionToken) { + if (data.shouldFetchReauthSessionToken !== undefined) { parsed_data.should_fetch_reauth_session_token = data.shouldFetchReauthSessionToken; } From 81e91f44d4d9f211980e08fa74ac6a010dba05e8 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 05:46:39 -0700 Subject: [PATCH 14/27] chore(Types): remove duplicate `FileNamedBufferReader ` --- src/types/StudioWebUploading.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/types/StudioWebUploading.ts b/src/types/StudioWebUploading.ts index b9ba346581..23cb3953cd 100644 --- a/src/types/StudioWebUploading.ts +++ b/src/types/StudioWebUploading.ts @@ -11,11 +11,6 @@ export interface FileNamedBufferReader { source: BufferReader; }; -export interface FileNamedBufferReader { - file_name: string; - source: BufferReader; -}; - export interface FileNamedBufferBase64 { file_name: string; source: BufferBase64; From 45db0bb0f570ddda5c45043c42daab5ddbb14b0d Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 05:48:24 -0700 Subject: [PATCH 15/27] fix(Actions): fix edge case regarding not cleaning up Studio Web Context if exectute throws --- src/core/Actions.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/Actions.ts b/src/core/Actions.ts index acd2d044b4..3681af2e73 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -157,6 +157,14 @@ export default class Actions { if (data?.client === 'YTMUSIC') { data.isAudioOnly = true; } + + delete this.session.context.request?.returnLogEntry; + delete this.session.context.request?.eats; + delete this.session.context.request?.reauthRequestInfo; + delete this.session.context.request?.sessionInfo; + delete this.session.context.request?.attestationResponseData; + delete this.session.context.user?.delegationContext; + delete this.session.context.user?.serializedDelegationContext; if (data?.client === 'WEB_CREATOR') { if (this.session.context.request) { // should just be true this.session.context.request.returnLogEntry = true; From 7a533348754e8e3ef85ff2ae30aff275f92dd22c Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Wed, 12 Aug 2026 05:53:52 -0700 Subject: [PATCH 16/27] fix(YouTube Studio Web): `managedExecute` fix the attestation_placement --- src/core/clients/StudioWeb.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index e48647fda5..460921761e 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -352,14 +352,14 @@ export default class StudioWeb { } async managedExecute(endpoint: string, payload: object, channel_id?: string, attestation_placement: AttestationPlacement = 'none', eats?: string, is_retry = false): Promise { - const attestation_response_data = attestation_placement === 'none' ? {} : { attestationResponseData: await this.#getBotGuardAttestation() }; + const attestation_response_data = attestation_placement === 'none' ? undefined : await this.#getBotGuardAttestation(); const response = await this.#actions.execute(endpoint, { client: 'WEB_CREATOR', session_token: await this.getSessionToken(), ...payload, - ...(attestation_placement === 'context' ? {} : { attestation_response_data }), - ...(attestation_placement === 'top_level' ? {} : attestation_response_data), + ...(attestation_placement === 'context' ? { attestation_response_data } : {}), + ...(attestation_placement === 'top_level' ? { attestationResponseData: attestation_response_data } : {}), ...(!eats ? {} : { eats }), ...(!channel_id ? {} : { channel_id }) }); @@ -647,6 +647,8 @@ export default class StudioWeb { const chunks_uploaded = this.#scottyUploadChunks(start.upload_url, file, on_scotty_progress); + if (!start.resource_id) throw new InnertubeError('Scotty didn\'t resolve a resource ID'); + const created = await this.managedExecute('/upload/createvideo', { channelId: this.#channel_id, resourceId: { scottyResourceId: { id: start.resource_id } }, From 7e4ca0f77ac9cdd153d4c1154edc65747d184f98 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 16:39:13 -0700 Subject: [PATCH 17/27] feat(YouTube Studio Web/Parser): updating the parser to have many of the types instead of just assuming types and actually including all the data for all the types that were skimped out on previously --- .../youtube/api/pfiinnertube/user_info.ts | 1 + src/core/Actions.ts | 15 +- src/core/Session.ts | 7 +- src/core/clients/StudioWeb.ts | 115 +-- src/parser/classes/ytstudio/CreatorVideo.ts | 792 ++++++++++++++++++ .../classes/ytstudio/DataFreshnessEntity.ts | 15 + src/parser/classes/ytstudio/Translation.ts | 62 ++ .../classes/ytstudio/UploadFeedbackItem.ts | 99 +++ .../classes/ytstudio/VideoUploadChecks.ts | 59 ++ src/parser/nodes.ts | 5 + src/parser/parser.ts | 59 +- src/parser/types/ParsedResponse.ts | 18 +- src/parser/types/RawResponse.ts | 6 +- src/parser/youtube/History.ts | 2 +- src/types/StudioWebUploading.ts | 297 +------ 15 files changed, 1216 insertions(+), 336 deletions(-) create mode 100644 src/parser/classes/ytstudio/CreatorVideo.ts create mode 100644 src/parser/classes/ytstudio/DataFreshnessEntity.ts create mode 100644 src/parser/classes/ytstudio/Translation.ts create mode 100644 src/parser/classes/ytstudio/UploadFeedbackItem.ts create mode 100644 src/parser/classes/ytstudio/VideoUploadChecks.ts diff --git a/protos/generated/youtube/api/pfiinnertube/user_info.ts b/protos/generated/youtube/api/pfiinnertube/user_info.ts index 1a69d20193..a1583cfe39 100644 --- a/protos/generated/youtube/api/pfiinnertube/user_info.ts +++ b/protos/generated/youtube/api/pfiinnertube/user_info.ts @@ -39,6 +39,7 @@ export interface UserInfo_DelegationContext_RoleType { export enum UserInfo_DelegationContext_RoleType_ChannelRoleType { CHANNEL_ROLE_TYPE_UNKNOWN = 0, CREATOR_CHANNEL_ROLE_TYPE_OWNER = 8, + // TODO fill in the rest off the role types UNRECOGNIZED = -1, } diff --git a/src/core/Actions.ts b/src/core/Actions.ts index 3681af2e73..b6c231fa32 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -1,17 +1,23 @@ import type { IBrowseResponse, + ICreateCaptionsResponse, + ICreateVideoResponse, IESRChallengeResponse, IGetChallengeResponse, IGetNotificationsMenuResponse, IGetSessionTokenResponse, IGetWebReauthURLResponse, + IMetadataUpdateResponse, INextResponse, + IParseCaptionsResponse, IParsedResponse, IPlayerResponse, IRawResponse, IResolveURLResponse, ISearchResponse, - IUpdatedMetadataResponse + IUpdateCaptionsResponse, + IUpdatedMetadataResponse, + IUploadFeedbackResponse } from '../parser/index.js'; import { NavigateAction, Parser } from '../parser/index.js'; import { InnertubeError, u8ToBase64 } from '../utils/Utils.js'; @@ -53,6 +59,12 @@ export type ParsedResponse = T extends '/att/esr' ? IESRChallengeResponse : T extends '/ars/grst' ? IGetSessionTokenResponse : T extends '/security/get_web_reauth_url' ? IGetWebReauthURLResponse : + T extends '/globalization/create_captions' ? ICreateCaptionsResponse : + T extends '/globalization/parse_captions' ? IParseCaptionsResponse : + T extends '/globalization/update_captions' ? IUpdateCaptionsResponse : + T extends '/video_manager/metadata_update' ? IMetadataUpdateResponse : + T extends '/upload/createvideo' ? ICreateVideoResponse : + T extends '/upload/feedback' ? IUploadFeedbackResponse : IParsedResponse; export default class Actions { @@ -194,6 +206,7 @@ export default class Actions { if (data.channel_id && this.session.context.user) { const delegation_context = { externalChannelId: data.channel_id, + // ?? Not to sure when this is ever not 'CREATOR_CHANNEL_ROLE_TYPE_OWNER', but if it can be else-things then gotta fetch it... roleType: { channelRoleType: 'CREATOR_CHANNEL_ROLE_TYPE_OWNER' as const } }; diff --git a/src/core/Session.ts b/src/core/Session.ts index 9d18a86d42..b171741cc4 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -96,7 +96,12 @@ export type Context = { // Studio Web params delegationContext?: { externalChannelId: string, - roleType: { channelRoleType: 'CREATOR_CHANNEL_ROLE_TYPE_OWNER' } + roleType: { + channelRoleType: + 'CREATOR_CHANNEL_ROLE_TYPE_OWNER' | 'CREATOR_CHANNEL_ROLE_TYPE_MANAGER' | 'CREATOR_CHANNEL_ROLE_TYPE_EDITOR' | 'CREATOR_CHANNEL_ROLE_TYPE_EDITOR_LIMITED' | + 'CREATOR_CHANNEL_ROLE_TYPE_SUBTITLE_EDITOR' | 'CREATOR_CHANNEL_ROLE_TYPE_VIEWER' | 'CREATOR_CHANNEL_ROLE_TYPE_VIEWER_LIMITED' | 'CREATOR_CHANNEL_ROLE_TYPE_UNSPECIFIED' | + 'CREATOR_CHANNEL_ROLE_TYPE_MODERATOR' | 'CREATOR_CHANNEL_ROLE_TYPE_CUSTOM' + } } serializedDelegationContext?: string; }; diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index 460921761e..06d688f072 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -1,12 +1,22 @@ import type { BotGuardChallenge, BotGuardSolver } from '../../types/BotGuard.js'; import type { EngagementType } from '../../types/Misc.js'; -import type { ContinuationsUploadFeedback, CreateVideoResponse, CreatorVideo, FileNamedBufferReader, StudioVisibility, UploadVideoDetails } from '../../types/StudioWebUploading.js'; +import type { FileNamedBufferReader, StudioVisibility, UploadVideoDetails } from '../../types/StudioWebUploading.js'; +import type { ICreateCaptionsResponse, ICreateVideoResponse, IMetadataUpdateResponse, IParseCaptionsResponse, IParsedResponse, IUpdateCaptionsResponse } from '../../parser/index.js'; +import type UploadFeedbackItem from '../../parser/classes/ytstudio/UploadFeedbackItem.js'; import { Constants, Log } from '../../utils/index.js'; import { InnertubeError, Platform, wait } from '../../utils/Utils.js'; -import type { Actions, Session } from '../index.js'; +import type { Actions, ParsedResponse, Session } from '../index.js'; type AttestationPlacement = 'none' | 'context' | 'top_level'; +type StudioManagedEndpoint = + | '/globalization/create_captions' + | '/globalization/parse_captions' + | '/globalization/update_captions' + | '/video_manager/metadata_update' + | '/upload/createvideo' + | '/upload/feedback'; + interface BotGuardAttestationResponse { challenge: string; webResponse: string; @@ -41,24 +51,15 @@ interface ScottyUploadResult { status?: string; scottyResourceId?: string }; type ScottyProgress = (written_bytes: number, total_bytes: number) => void; type ScottyUploadType = 'VIDEO' | 'THUMBNAIL'; -interface StudioChallengeResponseContext { - responseContext?: { webResponseContextExtensionData?: { challenge?: { type?: 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE' } } }; -} - -interface CreateCaptionsResponse { translation?: { captionsTranslations?: { contentUpdateTime?: string }[] } }; -interface ParseCaptionsResponse { captionSegments?: object } -interface UpdateCaptionsResponse { framework_updates?: object } - -interface MetadataUpdateResponse { videos?: CreatorVideo[] }; type UpdateMetadataPayload = Record; interface UploadSubtitlesResponse { - created: CreateCaptionsResponse; - parsed: ParseCaptionsResponse; - updated: UpdateCaptionsResponse; + created: ICreateCaptionsResponse; + parsed: IParseCaptionsResponse; + updated: IUpdateCaptionsResponse; } -interface UploadFeedbackResult { contents: ContinuationsUploadFeedback, next: () => Promise }; +interface UploadFeedbackResult { contents: UploadFeedbackItem[], next: () => Promise }; const VIDEO_READ_MASK = { videoId: true, @@ -351,29 +352,28 @@ export default class StudioWeb { return await this.#uploadToScotty('THUMBNAIL', file_name_buffer_reader, {}); } - async managedExecute(endpoint: string, payload: object, channel_id?: string, attestation_placement: AttestationPlacement = 'none', eats?: string, is_retry = false): Promise { + async managedExecute(endpoint: T, payload: object, channel_id?: string, attestation_placement: AttestationPlacement = 'none', eats?: string, is_retry = false): Promise> { const attestation_response_data = attestation_placement === 'none' ? undefined : await this.#getBotGuardAttestation(); - const response = await this.#actions.execute(endpoint, { + const data = await this.#actions.execute(endpoint, { client: 'WEB_CREATOR', + parse: true, session_token: await this.getSessionToken(), ...payload, ...(attestation_placement === 'context' ? { attestation_response_data } : {}), ...(attestation_placement === 'top_level' ? { attestationResponseData: attestation_response_data } : {}), ...(!eats ? {} : { eats }), ...(!channel_id ? {} : { channel_id }) - }); - if (!response.success) throw new InnertubeError(`YouTube Studio call to ${endpoint} failed`); - const data = response.data as T & StudioChallengeResponseContext; + }) as IParsedResponse; - if (data.responseContext?.webResponseContextExtensionData?.challenge?.type === 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE') { + if (data.challenge_prompt_type === 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE') { if (!is_retry && this.#auto_retry && channel_id !== undefined) { this.#force_refresh_session_token = true; return await this.managedExecute(endpoint, payload, channel_id, attestation_placement, eats, true); } throw new InnertubeError('YouTube Studio is requesting an authentication challenge, likely a stale session token'); } - return data; + return data as ParsedResponse; } async uploadSubtitles(video_id: string, subtitles: NonNullable, language = 'en-US'): Promise { @@ -381,7 +381,7 @@ export default class StudioWeb { const data_uri = `data:application/octet-stream;base64,${data_base64}`; const tts_track_id = { lang: language, kind: '', name: '' }; - const created = await this.managedExecute('/globalization/create_captions', { + const created = await this.managedExecute('/globalization/create_captions', { videoId: video_id, channelId: this.#channel_id, newTrack: tts_track_id, @@ -389,16 +389,16 @@ export default class StudioWeb { autoTranslate: subtitles.auto_translate ?? false }, this.#channel_id); - const content_update_time = created.translation?.captionsTranslations?.[0]?.contentUpdateTime; + const content_update_time = created.translation?.captions_translations?.[0]?.content_update_time; if (content_update_time === undefined) throw new InnertubeError('create_captions did not return a contentUpdateTime'); - const parsed = await this.managedExecute('/globalization/parse_captions', { + const parsed = await this.managedExecute('/globalization/parse_captions', { fileType: subtitles.synced ? 'CAPTIONS_FILE_TYPE_TIMED_TEXT' : 'CAPTIONS_FILE_TYPE_TRANSCRIPT', fileName: subtitles.data.file_name, dataUri: data_uri }, this.#channel_id); - const updated = await this.managedExecute('/globalization/update_captions', { + const updated = await this.managedExecute('/globalization/update_captions', { videoId: video_id, channelId: this.#channel_id, operations: [ @@ -521,8 +521,8 @@ export default class StudioWeb { return payload; } - async #updateMetadata(video_id: string, payload: UpdateMetadataPayload): Promise { - return await this.managedExecute('/video_manager/metadata_update', { + async #updateMetadata(video_id: string, payload: UpdateMetadataPayload): Promise { + return await this.managedExecute('/video_manager/metadata_update', { encryptedVideoId: video_id, videoReadMask: VIDEO_READ_MASK, flowType: 'MDE_FLOW_TYPE_UPLOAD', @@ -539,7 +539,7 @@ export default class StudioWeb { const payload = this.#buildMetadataUpdate(details, thumbnail_resource_id); - let update_metadata_response: MetadataUpdateResponse | object = {}; + let update_metadata_response: IMetadataUpdateResponse | object = {}; let update_subtitles_response: UploadSubtitlesResponse | object = {}; if (Object.keys(payload).length > 0) { @@ -559,43 +559,20 @@ export default class StudioWeb { return update_metadata_response; } - #tryGetFeedbackToken(feedback: ContinuationsUploadFeedback): string | null { - try { - const feedback_token = feedback[0].uploadFeedbackItemContinuation.continuations[0].uploadFeedbackRefreshContinuation?.continuation; - if (feedback_token) return feedback_token; - const timed_token = feedback[0].uploadFeedbackItemContinuation.continuations[0].timedContinuationData?.continuation; - if (timed_token) return timed_token; - return null; - } catch (_) { - return null; - } - } - #tryGetFeedbackDelay(feedback: ContinuationsUploadFeedback): number | null { - try { - const feedback_timeout = feedback[0].uploadFeedbackItemContinuation.continuations[0].uploadFeedbackRefreshContinuation?.continueInMs; - if (feedback_timeout) return feedback_timeout; - const timed_timeout = feedback[0].uploadFeedbackItemContinuation.continuations[0].timedContinuationData?.timeoutMs; - if (timed_timeout) return timed_timeout; - return null; - } catch (_) { - return null; - } - } - // from https://studio.youtube.com/youtubei/v1/creator/get_channel_dashboard?alt=json under interactionRecordingParams; likely useless async uploadFeedback(tokens: string[], type: 'FEEDBACK_TOKENS'): Promise<{ isProcessed: boolean }> // initial from createvideo under uploadFeedbackRefreshContinuation async uploadFeedback(tokens: string[], type: 'CONTINUATION_TOKENS'): Promise async uploadFeedback(tokens: string[], type: 'FEEDBACK_TOKENS' | 'CONTINUATION_TOKENS'): Promise<{ isProcessed: boolean } | UploadFeedbackResult | null> { if (tokens.filter((token) => token).length === 0) return null; - const feedback_data = await this.managedExecute<{ feedbackResponse?: { isProcessed: boolean }, continuationContents?: ContinuationsUploadFeedback }>('upload/feedback', + const feedback_data = await this.managedExecute('/upload/feedback', type === 'CONTINUATION_TOKENS' ? { continuations: tokens } : { feedbackTokens: tokens }); try { - if (type === 'FEEDBACK_TOKENS') return feedback_data.feedbackResponse as { isProcessed: boolean }; - const contents = feedback_data.continuationContents as ContinuationsUploadFeedback; + if (type === 'FEEDBACK_TOKENS') return feedback_data.feedback_responses?.[0] as { isProcessed: boolean }; + const contents = feedback_data.upload_feedback_items ?? []; return { contents, next: async () => await this.uploadFeedback([ - this.#tryGetFeedbackToken(contents) ?? '' + contents[0]?.continuation_token ?? '' ], 'CONTINUATION_TOKENS') }; } catch (_) { @@ -603,14 +580,14 @@ export default class StudioWeb { } } - async uploadFeedbackCycle(initial_tokens: (string | null)[], callback_continue: (content: ContinuationsUploadFeedback) => boolean) { + async uploadFeedbackCycle(initial_tokens: (string | null)[], callback_continue: (content: UploadFeedbackItem[]) => boolean) { if (initial_tokens.some((token) => token === null)) return; let feedback: null | UploadFeedbackResult = await this.uploadFeedback(initial_tokens as string[], 'CONTINUATION_TOKENS'); if (feedback === null) return; do { try { if (!callback_continue(feedback.contents)) break; - const delay = this.#tryGetFeedbackDelay(feedback.contents); + const delay = feedback.contents[0]?.continuation_delay_ms ?? null; if (delay === null) { Log.warn('upload feedback delay is null; exiting for safety'); break; @@ -628,20 +605,8 @@ export default class StudioWeb { while ((feedback = await feedback.next()) !== null); } - #tryGetUploadVideoFeedbackToken(created: CreateVideoResponse): string | null { - try { - const feedback = created.contents.uploadFeedbackItemRenderer.continuations[0].uploadFeedbackRefreshContinuation?.continuation; - if (feedback) return feedback; - const timed = created.contents.uploadFeedbackItemRenderer.continuations[0].timedContinuationData?.continuation; - if (timed) return timed; - return null; - } catch (_) { - return null; - } - } - async uploadVideo(file: FileNamedBufferReader, details: Partial = {}, on_scotty_progress?: (written_bytes: number, total_bytes: number) => void, - on_initial_create_video?: (full_created: { created: CreateVideoResponse, feedback_token: string | null }) => any) { + on_initial_create_video?: (full_created: { created: ICreateVideoResponse, feedback_token: string | null }) => any) { const frontend_upload_id = `innertube_studio:${Platform.shim.uuidv4().toUpperCase()}:0`; const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL['VIDEO'], file, { frontendUploadId: frontend_upload_id }); @@ -649,7 +614,7 @@ export default class StudioWeb { if (!start.resource_id) throw new InnertubeError('Scotty didn\'t resolve a resource ID'); - const created = await this.managedExecute('/upload/createvideo', { + const created = await this.managedExecute('/upload/createvideo', { channelId: this.#channel_id, resourceId: { scottyResourceId: { id: start.resource_id } }, frontendUploadId: frontend_upload_id, @@ -667,15 +632,15 @@ export default class StudioWeb { presumedShort: false }, this.#channel_id, 'context'); - on_initial_create_video?.({ created, feedback_token: this.#tryGetUploadVideoFeedbackToken(created) }); + on_initial_create_video?.({ created, feedback_token: created.upload_feedback_item?.continuation_token ?? null }); - const video_id = created.videoId; + const video_id = created.video_id; if (video_id === undefined || video_id === '') { await chunks_uploaded; throw new InnertubeError('createvideo did not return a videoId'); } - const uploaded = await chunks_uploaded; + await chunks_uploaded; // title and tags already went up with createvideo const { title: _title, tags: _tags, visibility, ...remaining_details } = details; diff --git a/src/parser/classes/ytstudio/CreatorVideo.ts b/src/parser/classes/ytstudio/CreatorVideo.ts new file mode 100644 index 0000000000..b32f64c0ab --- /dev/null +++ b/src/parser/classes/ytstudio/CreatorVideo.ts @@ -0,0 +1,792 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; +import { parseAll, parseObject } from '../../parser.js'; +import Thumbnail from '../misc/Thumbnail.js'; +import VideoUploadChecks from './VideoUploadChecks.js'; + +export type CreatorVideoStatus = + 'VIDEO_STATUS_DELETED' | 'VIDEO_STATUS_FAILED' | 'VIDEO_STATUS_PROCESSED' | + 'VIDEO_STATUS_REJECTED' | 'VIDEO_STATUS_UNKNOWN' | 'VIDEO_STATUS_UPLOADED' | 'VIDEO_STATUS_WRONG'; + +export type DraftStatus = 'DRAFT_STATUS_NONE' | 'DRAFT_STATUS_PUBLIC' | 'DRAFT_STATUS_SCHEDULED'; + +export type CreatorVideoPrivacy = + 'VIDEO_PRIVACY_PRIVATE' | 'VIDEO_PRIVACY_PUBLIC' | 'VIDEO_PRIVACY_SCHEDULED' | + 'VIDEO_PRIVACY_UNKNOWN' | 'VIDEO_PRIVACY_UNLISTED'; + +export type CreatorVideoCategory = + 'CREATOR_VIDEO_CATEGORY_AUTOS' | 'CREATOR_VIDEO_CATEGORY_COMEDY' | 'CREATOR_VIDEO_CATEGORY_EDUCATION' | + 'CREATOR_VIDEO_CATEGORY_ENTERTAINMENT' | 'CREATOR_VIDEO_CATEGORY_FILM' | 'CREATOR_VIDEO_CATEGORY_GADGETS' | + 'CREATOR_VIDEO_CATEGORY_GOVERNMENT' | 'CREATOR_VIDEO_CATEGORY_HOWTO' | 'CREATOR_VIDEO_CATEGORY_MUSIC' | + 'CREATOR_VIDEO_CATEGORY_NEWS' | 'CREATOR_VIDEO_CATEGORY_PEOPLE' | 'CREATOR_VIDEO_CATEGORY_PETS' | + 'CREATOR_VIDEO_CATEGORY_SCIENCE' | 'CREATOR_VIDEO_CATEGORY_SPORTS' | 'CREATOR_VIDEO_CATEGORY_TRAVEL' | + 'CREATOR_VIDEO_CATEGORY_UNKNOWN'; + +export type VideoCommentFilter = + 'VIDEO_COMMENT_FILTER_APPROVE' | 'VIDEO_COMMENT_FILTER_AUTO_MODERATE' | + 'VIDEO_COMMENT_FILTER_AUTO_MODERATE_HOLD_MORE' | 'VIDEO_COMMENT_FILTER_NONE' | 'VIDEO_COMMENT_FILTER_UNKNOWN'; + +export type VideoCommentSortOrder = 'VIDEO_COMMENT_SORT_ORDER_LATEST' | 'VIDEO_COMMENT_SORT_ORDER_TOP'; + +export type VideoAgeRestriction = + 'VIDEO_AGE_RESTRICTION_APPEAL_BUTTON' | 'VIDEO_AGE_RESTRICTION_NONE' | + 'VIDEO_AGE_RESTRICTION_SELF' | 'VIDEO_AGE_RESTRICTION_SYSTEM'; + +export type VideoLicense = 'VIDEO_LICENSE_CREATIVE_COMMONS' | 'VIDEO_LICENSE_STANDARD' | 'VIDEO_LICENSE_UNKNOWN'; + +export type VideoUncaptionedReason = + 'VIDEO_UNCAPTIONED_REASON_CAPTIONLESS_TV_CONTENT' | 'VIDEO_UNCAPTIONED_REASON_EXCEPTION_GRANTED' | + 'VIDEO_UNCAPTIONED_REASON_LEGACY' | 'VIDEO_UNCAPTIONED_REASON_NO_FULL_LENGTH_VIDEO' | + 'VIDEO_UNCAPTIONED_REASON_NO_US_TV_CONTENT' | 'VIDEO_UNCAPTIONED_REASON_NOT_REQUIRED' | + 'VIDEO_UNCAPTIONED_REASON_UNNECESSARY_OR_NOT_SET'; + +export type VideoSubscriberNotification = + 'VIDEO_SUBSCRIBER_NOTIFICATION_DISABLED' | 'VIDEO_SUBSCRIBER_NOTIFICATION_ENABLED' | + 'VIDEO_SUBSCRIBER_NOTIFICATION_NONE' | 'VIDEO_SUBSCRIBER_NOTIFICATION_UNKNOWN'; + +export type VideoPaidProductPlacement = + 'VIDEO_PAID_PRODUCT_PLACEMENT_NO' | 'VIDEO_PAID_PRODUCT_PLACEMENT_NOTIFY' | + 'VIDEO_PAID_PRODUCT_PLACEMENT_UNKNOWN' | 'VIDEO_PAID_PRODUCT_PLACEMENT_UNSET' | 'VIDEO_PAID_PRODUCT_PLACEMENT_YES'; + +export type VideoMusicLicensedStatus = + 'VIDEO_MUSIC_LICENSED_STATUS_CLAIMED_BY_CHANNEL_OWNER' | 'VIDEO_MUSIC_LICENSED_STATUS_DISABLED_BY_CLAIM_PREFERENCE' | + 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_CHANNEL_OWNER' | 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_CHANNEL_OWNER_UNCLAIMED' | + 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_PARTNER_UPLOADED_CLAIM' | 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_THIRD_PARTY_CLAIMS' | + 'VIDEO_MUSIC_LICENSED_STATUS_UNCLAIMED'; + +export type VideoUserSetMonetization = 'VIDEO_USER_SET_MONETIZATION_OFF' | 'VIDEO_USER_SET_MONETIZATION_ON'; + +export type VideoMonetizingStatus = + 'VIDEO_MONETIZING_STATUS_INDETERMINATE' | 'VIDEO_MONETIZING_STATUS_MONETIZING' | + 'VIDEO_MONETIZING_STATUS_MONETIZING_CREATOR_REVSHARE' | 'VIDEO_MONETIZING_STATUS_MONETIZING_IN_ESCROW' | + 'VIDEO_MONETIZING_STATUS_MONETIZING_WITH_EXCEPTIONS' | 'VIDEO_MONETIZING_STATUS_MONETIZING_WITH_LIMITED_ADS' | + 'VIDEO_MONETIZING_STATUS_MONETIZING_WITH_REVSHARE' | 'VIDEO_MONETIZING_STATUS_NOT_FOR_DISPLAY' | + 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_CHANNEL_NOT_MONETIZING' | 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_INELIGIBLE' | + 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_INELIGIBLE_INNOCUOUS' | 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF' | + 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF_CREATOR_REVSHARE' | 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF_SHORTS_REVSHARE' | + 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF_WITH_REVSHARE' | 'VIDEO_MONETIZING_STATUS_PENDING_CHECKS' | + 'VIDEO_MONETIZING_STATUS_UNSPECIFIED' | 'VIDEO_MONETIZING_STATUS_VIDEO_NOT_FINAL'; + +export type VideoVisibilityStatus = + 'VIDEO_VISIBILITY_STATUS_AGE_RESTRICTED' | 'VIDEO_VISIBILITY_STATUS_BLOCKED_FOR_COPYRIGHT_GLOBALLY' | + 'VIDEO_VISIBILITY_STATUS_BLOCKED_FOR_COPYRIGHT_PARTIALLY' | 'VIDEO_VISIBILITY_STATUS_DRAFT' | + 'VIDEO_VISIBILITY_STATUS_FORCED_PRIVATE' | 'VIDEO_VISIBILITY_STATUS_INDETERMINATE' | + 'VIDEO_VISIBILITY_STATUS_LIMITED_FEATURES' | 'VIDEO_VISIBILITY_STATUS_PENDING_REMOVAL_FOR_COPYRIGHT' | + 'VIDEO_VISIBILITY_STATUS_REMOVED_FOR_COMMUNITY_GUIDELINES' | 'VIDEO_VISIBILITY_STATUS_REMOVED_FOR_COPYRIGHT' | + 'VIDEO_VISIBILITY_STATUS_TARGETED_FOR_KIDS' | 'VIDEO_VISIBILITY_STATUS_UNKNOWN' | + 'VIDEO_VISIBILITY_STATUS_UPLOAD_FAILED' | 'VIDEO_VISIBILITY_STATUS_UPLOADING_OR_PROCESSING' | + 'VIDEO_VISIBILITY_STATUS_USER_CONFIG'; + +export type VideoUserSetVisibility = + 'VIDEO_USER_SET_VISIBILITY_DRAFT' | 'VIDEO_USER_SET_VISIBILITY_FUTURE_PREMIERE' | 'VIDEO_USER_SET_VISIBILITY_MEMBERS' | + 'VIDEO_USER_SET_VISIBILITY_PREMIERING' | 'VIDEO_USER_SET_VISIBILITY_PRIMETIME_SUBSCRIBERS' | 'VIDEO_USER_SET_VISIBILITY_PRIVATE' | + 'VIDEO_USER_SET_VISIBILITY_PUBLIC' | 'VIDEO_USER_SET_VISIBILITY_SCHEDULED' | + 'VIDEO_USER_SET_VISIBILITY_SCHEDULED_TO_PRIMETIME_SUBSCRIBERS' | 'VIDEO_USER_SET_VISIBILITY_SPECIFIC_PEOPLE' | + 'VIDEO_USER_SET_VISIBILITY_SUPERFANS' | 'VIDEO_USER_SET_VISIBILITY_UNLISTED' | 'VIDEO_USER_SET_VISIBILITY_UNSPECIFIED'; + +export type VideoUserInflictedVisibility = 'VIDEO_USER_INFLICTED_VISIBILITY_AGE_RESTRICTED' | 'VIDEO_USER_INFLICTED_VISIBILITY_UNSPECIFIED'; + +export type VideoEffectiveVisibility = + 'VIDEO_EFFECTIVE_VISIBILITY_BLOCKED' | 'VIDEO_EFFECTIVE_VISIBILITY_FORCED_PRIVATE' | + 'VIDEO_EFFECTIVE_VISIBILITY_REMOVED' | 'VIDEO_EFFECTIVE_VISIBILITY_UNSPECIFIED' | + 'VIDEO_EFFECTIVE_VISIBILITY_UPLOAD_FAILED' | 'VIDEO_EFFECTIVE_VISIBILITY_UPLOADING_OR_PROCESSING' | + 'VIDEO_EFFECTIVE_VISIBILITY_USER_CONFIG'; + +export type VideoOrigin = 'VIDEO_ORIGIN_LIVESTREAM' | 'VIDEO_ORIGIN_STORY' | 'VIDEO_ORIGIN_UNKNOWN' | 'VIDEO_ORIGIN_UPLOAD'; + +export type VideoProcessingStatus = + 'VIDEO_PROCESSING_STATUS_EDITED' | 'VIDEO_PROCESSING_STATUS_FAILED' | 'VIDEO_PROCESSING_STATUS_PROCESSING' | + 'VIDEO_PROCESSING_STATUS_PROCESSING_NON_PRIMARY_ASSETS' | 'VIDEO_PROCESSING_STATUS_READY' | + 'VIDEO_PROCESSING_STATUS_REVERTED' | 'VIDEO_PROCESSING_STATUS_UNEDITED' | 'VIDEO_PROCESSING_STATUS_UNKNOWN'; + +export type VideoTargetedAudience = + 'VIDEO_TARGETED_AUDIENCE_ALL' | 'VIDEO_TARGETED_AUDIENCE_AGE_RESTRICTED' | + 'VIDEO_TARGETED_AUDIENCE_CROSSWALK' | 'VIDEO_TARGETED_AUDIENCE_UNKNOWN'; + +export type VideoTargetedAudienceImposer = + 'VIDEO_TARGETED_AUDIENCE_IMPOSER_SELF' | 'VIDEO_TARGETED_AUDIENCE_IMPOSER_SYSTEM' | 'VIDEO_TARGETED_AUDIENCE_IMPOSER_UNSPECIFIED'; + +export type ResolutionStatus = + 'RESOLUTION_STATUS_APPEAL_IN_PROGRESS' | 'RESOLUTION_STATUS_APPEAL_REJECTED' | 'RESOLUTION_STATUS_AVAILABLE' | + 'RESOLUTION_STATUS_DEFERRED' | 'RESOLUTION_STATUS_DONE' | 'RESOLUTION_STATUS_PROCESSING' | + 'RESOLUTION_STATUS_STARTING_SOON' | 'RESOLUTION_STATUS_UNAVAILABLE' | 'RESOLUTION_STATUS_UNKNOWN' | + 'RESOLUTION_STATUS_UNSPECIFIED'; + +export type MonetizedStatus = 'MONETIZED_STATUS_ACTIVE' | 'MONETIZED_STATUS_INACTIVE' | 'MONETIZED_STATUS_UNSPECIFIED'; + +export type RestrictionsSeverity = + 'VIDEO_RESTRICTIONS_SEVERITY_HIGH' | 'VIDEO_RESTRICTIONS_SEVERITY_LOW' | + 'VIDEO_RESTRICTIONS_SEVERITY_MEDIUM' | 'VIDEO_RESTRICTIONS_SEVERITY_UNSPECIFIED'; + +export type AutoGenMidrollsStatus = 'AUTO_GEN_MIDROLLS_STATUS_AVAILABLE' | 'AUTO_GEN_MIDROLLS_STATUS_FAILED' | 'AUTO_GEN_MIDROLLS_STATUS_PROCESSING'; + +export type VideoCopyrightSummaryStatus = + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_BLOCKED' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_COPYRIGHT_CONTENT_FOUND' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_COUNTER_REJECTED' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_DELAYED_TAKEDOWN' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_EXPIRED_STRIKE_TAKEDOWN' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LICENSED' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LIKENESS_CLAIM' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LIKENESS_PENDING_REMOVAL' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LIKENESS_REMOVAL' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZABLE_WITH_LICENSES' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_CREATOR_REVSHARE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_CREATOR_REVSHARE_ELIGIBLE' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_ENABLED_WITH_LICENSES' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_RESTRICTED' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_REVSHARE_ELIGIBLE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_REVSHARE_ENABLED' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_SHORTS_REVSHARE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_SHORTS_REVSHARE_ELIGIBLE' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_UNAVAILABLE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MULTIPLE_CLAIMS' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MULTIPLE_REMOVALS' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_NO_CLAIMS_FOUND' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_PARTIALLY_BLOCKED_REVSHARE_ENABLED' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_SHORTS_NO_UPLOADER_CLAIM' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_STRIKE_TAKEDOWN' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN_COUNTER' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN_NO_STRIKE' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN_UNDER_REVIEW' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_VIDEO_APPEAL' | + 'VIDEO_COPYRIGHT_SUMMARY_STATUS_VIDEO_DISPUTE'; + +export type VideoCopyrightChannelImpact = + 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_NO_CHANNEL_IMPACT' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_NO_CHANNEL_IMPACT_WITH_LICENSES' | + 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_NO_CHANNEL_IMPACT_WITHOUT_CLAIMS' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_PENDING_STRIKE_REVIEW' | + 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_COUNTER' | + 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_EXPIRED' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_PENDING' | + 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_RELEASED_DURING_COUNTER'; + +export type VideoCopyrightVisibilityImpact = + 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_APPEAL' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_CLAIM_BLOCK' | + 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_CLAIM_PARTIAL_BLOCK' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_COMMERCIAL_SHORTS_BLOCK' | + 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_DELAYED_TAKEDOWN' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_DISPUTE' | + 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_LICENSE_RESTRICTED_SHORTS_BLOCK' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_LIKENESS_BLOCK' | + 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_MULTIPLE_CLAIMS_BLOCK' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_NOT_AFFECTED' | + 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_PENDING_LIKENESS_REMOVAL' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_TAKEDOWN' | + 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_TAKEDOWN_COUNTER' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_UNKNOWN'; + +export type VideoCopyrightMonetizationImpact = + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CLAIM_BLOCK' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CLAIM_PARTIAL_BLOCK' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CLAIM_PARTIAL_BLOCK_MONETIZED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CREATOR_REVSHARE' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CREATOR_REVSHARE_ELIGIBLE' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_DO_NOT_DISPLAY' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_LIKENESS_REVSHARE_ELIGIBLE' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_LIKENESS_REVSHARE_ENABLED' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_MONETIZABLE_WITH_LICENSES' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_MONETIZED_DURING_DISPUTE' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_NOT_AFFECTED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_NOT_AFFECTED_LICENSED' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_RESTRICTED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_REVSHARE_ELIGIBLE' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_REVSHARE_ENABLED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_SHORTS_NO_UPLOADER_CLAIM' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_SHUNA_CLAIM_DEMONETIZATION' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_TAKEDOWN' | + 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_UNKNOWN'; + +export type TouPolicyVertical = + 'TOU_POLICY_VERTICAL_CHILD_SAFETY' | 'TOU_POLICY_VERTICAL_HARMFUL_DANGEROUS' | 'TOU_POLICY_VERTICAL_SUICIDE_SELF_HARM' | + 'TOU_POLICY_VERTICAL_UNKNOWN' | 'TOU_POLICY_VERTICAL_VIOLENT_GRAPHIC_SHOCKING'; + +export type HumanReviewState = 'HUMAN_REVIEW_STATE_DONE' | 'HUMAN_REVIEW_STATE_INELIGIBLE' | 'HUMAN_REVIEW_STATE_NOT_REQUESTED'; + +export type VideoMadeForKids = 'VIDEO_MADE_FOR_KIDS_MFK' | 'VIDEO_MADE_FOR_KIDS_NOT_MFK' | 'VIDEO_MADE_FOR_KIDS_UNKNOWN'; + +export type VideoMadeForKidsImposer = + 'VIDEO_MADE_FOR_KIDS_IMPOSER_SELF' | 'VIDEO_MADE_FOR_KIDS_IMPOSER_UNSPECIFIED' | 'VIDEO_MADE_FOR_KIDS_IMPOSER_YOUTUBE'; + +export type RemixSourceOptionEligibility = + 'REMIX_SOURCE_OPTION_ELIGIBILITY_BY_CLIENT' | 'REMIX_SOURCE_OPTION_ELIGIBILITY_ELIGIBLE' | 'REMIX_SOURCE_OPTION_ELIGIBILITY_INELIGIBLE'; + +export type RemixSourceShorts = 'REMIX_SOURCE_SHORTS_IS_SHORT' | 'REMIX_SOURCE_SHORTS_NOT_SHORT' | 'REMIX_SOURCE_SHORTS_PROCESSING'; + +export type VideoCommentsEnabledState = + 'VIDEO_COMMENTS_ENABLED_STATE_OFF' | 'VIDEO_COMMENTS_ENABLED_STATE_ON' | + 'VIDEO_COMMENTS_ENABLED_STATE_PAUSED' | 'VIDEO_COMMENTS_ENABLED_STATE_UNKNOWN'; + +export type AllowedCommenterMode = + 'ALLOWED_COMMENTER_MODE_ANYONE' | 'ALLOWED_COMMENTER_MODE_SUBSCRIBERS_MEMBERS_APPROVED_USERS' | 'ALLOWED_COMMENTER_MODE_UNKNOWN'; + +export type CommenterMinimumSubscriptionTime = + 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ANY' | 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ONE_DAY' | + 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ONE_HOUR' | 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ONE_WEEK' | + 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_UNKNOWN'; + +export type CreatorContentType = + 'CREATOR_CONTENT_TYPE_LIVE_STREAM' | 'CREATOR_CONTENT_TYPE_SHORTS' | + 'CREATOR_CONTENT_TYPE_UNSPECIFIED' | 'CREATOR_CONTENT_TYPE_VIDEO_ON_DEMAND'; + +export type CreatorVideoPermission = + 'CREATOR_VIDEO_PERMISSION_ANALYTICS_READ' | 'CREATOR_VIDEO_PERMISSION_BASIC_METADATA_READ' | + 'CREATOR_VIDEO_PERMISSION_CAPTIONS_READ' | 'CREATOR_VIDEO_PERMISSION_CAPTIONS_WRITE' | + 'CREATOR_VIDEO_PERMISSION_COLLABORATOR' | 'CREATOR_VIDEO_PERMISSION_COLLABORATOR_INVITEE' | + 'CREATOR_VIDEO_PERMISSION_COLLABORATOR_LIMITED' | 'CREATOR_VIDEO_PERMISSION_COLLABORATOR_LIMITED_INVITEE' | + 'CREATOR_VIDEO_PERMISSION_COMMENTS_MANAGER' | 'CREATOR_VIDEO_PERMISSION_COMMENTS_READ' | + 'CREATOR_VIDEO_PERMISSION_COMMENT_SETTINGS_READ' | 'CREATOR_VIDEO_PERMISSION_COMMENT_SETTINGS_WRITE' | + 'CREATOR_VIDEO_PERMISSION_DELETE' | 'CREATOR_VIDEO_PERMISSION_DOWNLOAD' | + 'CREATOR_VIDEO_PERMISSION_ENFORCEMENT_APPELLANT' | 'CREATOR_VIDEO_PERMISSION_ENFORCEMENT_READER' | + 'CREATOR_VIDEO_PERMISSION_MONETIZATION_SETTINGS_READ' | 'CREATOR_VIDEO_PERMISSION_MONETIZATION_WRITE' | + 'CREATOR_VIDEO_PERMISSION_PRIVACY_STATUS_PRIVATE_WRITE' | 'CREATOR_VIDEO_PERMISSION_PRIVACY_STATUS_PUBLIC_WRITE' | + 'CREATOR_VIDEO_PERMISSION_RATING_SETTINGS_WRITE' | 'CREATOR_VIDEO_PERMISSION_READ' | + 'CREATOR_VIDEO_PERMISSION_WATCH' | 'CREATOR_VIDEO_PERMISSION_WRITE'; + +export type CreatorEntityStatus = 'CREATOR_ENTITY_STATUS_FAILURE' | 'CREATOR_ENTITY_STATUS_OK' | 'CREATOR_ENTITY_STATUS_PARTIAL_FAILURE'; + +export type CreatorFeatureStatus = + 'CREATOR_FEATURE_STATUS_DISABLED' | 'CREATOR_FEATURE_STATUS_ELIGIBLE' | + 'CREATOR_FEATURE_STATUS_ENABLED' | 'CREATOR_FEATURE_STATUS_UNKNOWN'; + +export interface ClaimDetails { + is_embed_disabled?: boolean; + video_has_commercial_block?: boolean; + video_has_third_party_claim?: boolean; +} + +export interface Permissions { + overall_permissions?: CreatorVideoPermission[]; +} + +export interface ResponseStatus { + status_code?: CreatorEntityStatus; +} + +export interface ThumbnailEditorState { + stills?: Thumbnail[][]; + still_id?: number; + default_still?: boolean; +} + +export interface VideoEditorProject { + video_dimensions?: { width?: number, height?: number }; +} + +export interface StatusDetails { + feedback_service_continuation_token?: string; +} + +export interface VideoTag { + value: string; +} + +export interface AudioLanguage { + language_code?: string; +} + +export interface FeatureState { + status?: CreatorFeatureStatus; + status_details?: 'CREATOR_FEATURE_STATUS_DETAILS_NOT_APPLICABLE'; +} + +export interface Publishing { + notify_subscribers?: VideoSubscriberNotification; +} + +export interface Music { + licensed_status?: VideoMusicLicensedStatus; +} + +export interface AdMonetization { + user_set_monetization?: VideoUserSetMonetization; + effective_status?: VideoMonetizingStatus; +} + +export interface Monetization { + ad_monetization?: AdMonetization; +} + +export interface VideoVisibility { + effective_status?: VideoVisibilityStatus; + user_set_visibility?: VideoUserSetVisibility; + user_inflicted_visibility?: VideoUserInflictedVisibility; + effective_visibility?: VideoEffectiveVisibility; +} + +export interface CopyrightSummary { + video_copyright_summary_status?: VideoCopyrightSummaryStatus; + channel_impacts?: VideoCopyrightChannelImpact[]; + video_visibility_impacts?: VideoCopyrightVisibilityImpact[]; + video_monetization_impact?: VideoCopyrightMonetizationImpact; + active_third_party_claims_count?: number; +} + +export interface SponsorsOnly { + is_sponsors_only?: boolean; +} + +export interface AudienceRestriction { + self_rating?: VideoTargetedAudience; + system_rating?: VideoTargetedAudience; + override_enabled?: boolean; + effective_rating?: VideoTargetedAudience; + imposer?: VideoTargetedAudienceImposer; +} + +export interface OwnedClaimDetails { + can_edit_owned_claim?: boolean; + can_enable_matching?: boolean; +} + +export interface RestrictionsSummary { + severity?: RestrictionsSeverity; +} + +export interface AllRestrictions { + summary?: RestrictionsSummary; +} + +export interface VideoResolutions { + status_sd?: ResolutionStatus; + status_hd?: ResolutionStatus; + status4k?: ResolutionStatus; + status2k?: ResolutionStatus; +} + +export interface AdFormats { + has_skippable_video_ads?: boolean; + has_non_skippable_video_ads?: boolean; + has_display_ads?: boolean; + has_live_display_ads?: boolean; +} + +export interface AdBreaks { + has_preroll_ads?: boolean; + has_midroll_ads?: boolean; + has_postroll_ads?: boolean; + auto_gen_midrolls_status?: AutoGenMidrollsStatus; +} + +export interface AdSettings { + ad_formats?: AdFormats; + ad_breaks?: AdBreaks; +} + +export interface PolicyDetail { + vertical?: TouPolicyVertical; +} + +export interface CommunityGuidelinesDetails { + all_policy_details?: PolicyDetail[]; + human_review_state?: HumanReviewState; +} + +export interface AdditionalDetails { + community_guidelines_details?: CommunityGuidelinesDetails; +} + +export interface VideoPrechecks { + copyright_prechecks_done?: boolean; + brand_safety_prechecks_done?: boolean; + video_upload_checks_monetized?: VideoUploadChecks; + video_upload_checks_not_monetized?: VideoUploadChecks; + additional_details?: AdditionalDetails; +} + +export interface Notification { + precheck_notifications_enabled?: boolean; +} + +export interface MfkSettings { + mfk_by_creator?: VideoMadeForKids; + mfk_without_creator_input?: VideoMadeForKids; + override_enabled?: boolean; + effective_mfk?: VideoMadeForKids; + imposer?: VideoMadeForKidsImposer; +} + +export interface CreatorOptOutSetting { + creator_opt_out?: boolean; +} + +export interface Remix { + remix_source_option_eligibility?: RemixSourceOptionEligibility; + is_source?: boolean; + remix_source_shorts?: RemixSourceShorts; +} + +export interface ContentOwnershipModelSettings { + is_off_network_upload?: boolean; +} + +export interface PublicMetrics { + view_count?: number; + comment_count?: number; + like_count?: number; + external_view_count?: number; +} + +export interface Shorts { + is_shorts_renderable?: boolean; +} + +export interface CommentSettings { + comments_enabled_state?: VideoCommentsEnabledState; + allowed_commenter_mode?: AllowedCommenterMode; + commenter_minimum_subscription_time?: CommenterMinimumSubscriptionTime; +} + +export interface Collaboration { + serialized_share_entity?: string; +} + +export interface PaidPoliticalContent { + paid_product_placement_political_content_from_eu_creator?: boolean; +} + +export interface SuperfansOnly { + is_superfans_only?: boolean; +} + +function toInt(value: unknown): number | undefined { + if (typeof value === 'number') return value; + if (typeof value !== 'string' || value === '' || isNaN(Number(value))) return undefined; + return parseInt(value, 10); +} + +export default class CreatorVideo extends YTNode { + static type = 'CreatorVideo'; + + video_id?: string; + channel_id?: string; + title?: string; + description?: string; + privacy?: CreatorVideoPrivacy; + status?: CreatorVideoStatus; + draft_status?: DraftStatus; + share_url?: string; + watch_url?: string; + length_seconds?: number; + video_duration_ms?: number; + time_created_seconds?: number; + time_published_seconds?: number; + thumbnail_details?: Thumbnail[]; + claim_details?: ClaimDetails; + permissions?: Permissions; + response_status?: ResponseStatus; + thumbnail_editor_state?: ThumbnailEditorState; + video_editor_project?: VideoEditorProject; + status_details?: StatusDetails; + tags?: VideoTag[]; + category?: CreatorVideoCategory; + comment_filter?: VideoCommentFilter; + default_comment_sort_order?: VideoCommentSortOrder; + audio_language?: AudioLanguage; + allow_ratings?: boolean; + age_restriction?: VideoAgeRestriction; + license?: VideoLicense; + features?: Record; + uncaptioned_reason?: VideoUncaptionedReason; + publishing?: Publishing; + paid_product_placement?: VideoPaidProductPlacement; + allow_embed?: boolean; + music?: Music; + monetization?: Monetization; + visibility?: VideoVisibility; + origin?: VideoOrigin; + inline_edit_processing_status?: VideoProcessingStatus; + copyright_summary?: CopyrightSummary; + sponsors_only?: SponsorsOnly; + serialized_share_entity?: string; + unlisted_expired?: boolean; + original_filename?: string; + audience_restriction?: AudienceRestriction; + owned_claim_details?: OwnedClaimDetails; + monetized_status?: MonetizedStatus; + comments_disabled_internally?: boolean; + all_restrictions?: AllRestrictions; + video_resolutions?: VideoResolutions; + ad_settings?: AdSettings; + video_prechecks?: VideoPrechecks; + view_count_is_hidden?: boolean; + notification?: Notification; + mfk_settings?: MfkSettings; + auto_chapter_settings?: CreatorOptOutSetting; + remix?: Remix; + content_ownership_model_settings?: ContentOwnershipModelSettings; + public_metrics?: PublicMetrics; + auto_places_mentioned_settings?: CreatorOptOutSetting; + shorts?: Shorts; + content_type?: CreatorContentType; + is_paygated?: boolean; + learning_concept_settings?: CreatorOptOutSetting; + comment_settings?: CommentSettings; + product_autotagging_settings?: CreatorOptOutSetting; + collaboration?: Collaboration; + paid_political_content?: PaidPoliticalContent; + superfans_only?: SuperfansOnly; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'videoId')) { + this.video_id = data.videoId; + } + + if (Reflect.has(data, 'channelId')) { + this.channel_id = data.channelId; + } + + if (Reflect.has(data, 'title')) { + this.title = data.title; + } + + if (Reflect.has(data, 'description')) { + this.description = data.description; + } + + if (Reflect.has(data, 'privacy')) { + this.privacy = data.privacy; + } + + if (Reflect.has(data, 'status')) { + this.status = data.status; + } + + if (Reflect.has(data, 'draftStatus')) { + this.draft_status = data.draftStatus; + } + + if (Reflect.has(data, 'shareUrl')) { + this.share_url = data.shareUrl; + } + + if (Reflect.has(data, 'watchUrl')) { + this.watch_url = data.watchUrl; + } + + if (Reflect.has(data, 'lengthSeconds')) { + this.length_seconds = toInt(data.lengthSeconds); + } + + if (Reflect.has(data, 'videoDurationMs')) { + this.video_duration_ms = toInt(data.videoDurationMs); + } + + if (Reflect.has(data, 'timeCreatedSeconds')) { + this.time_created_seconds = toInt(data.timeCreatedSeconds); + } + + if (Reflect.has(data, 'timePublishedSeconds')) { + this.time_published_seconds = toInt(data.timePublishedSeconds); + } + + if (Reflect.has(data, 'thumbnailDetails')) { + this.thumbnail_details = Thumbnail.fromResponse(data.thumbnailDetails); + } + + if (Reflect.has(data, 'claimDetails')) { + this.claim_details = parseObject(data.claimDetails); + } + + if (Reflect.has(data, 'permissions')) { + this.permissions = parseObject(data.permissions); + } + + if (Reflect.has(data, 'responseStatus')) { + this.response_status = parseObject(data.responseStatus); + } + + if (Reflect.has(data, 'thumbnailEditorState')) { + const thumbnail_editor_state = data.thumbnailEditorState; + this.thumbnail_editor_state = { + stills: Array.isArray(thumbnail_editor_state.stills) ? thumbnail_editor_state.stills.map((still: RawNode) => Thumbnail.fromResponse(still)) : undefined, + still_id: thumbnail_editor_state.stillId, + default_still: thumbnail_editor_state.defaultStill + }; + } + + if (Reflect.has(data, 'videoEditorProject')) { + this.video_editor_project = parseObject(data.videoEditorProject); + } + + if (Reflect.has(data, 'statusDetails')) { + this.status_details = parseObject(data.statusDetails); + } + + if (Reflect.has(data, 'tags')) { + this.tags = parseAll(data.tags); + } + + if (Reflect.has(data, 'category')) { + this.category = data.category; + } + + if (Reflect.has(data, 'commentFilter')) { + this.comment_filter = data.commentFilter; + } + + if (Reflect.has(data, 'defaultCommentSortOrder')) { + this.default_comment_sort_order = data.defaultCommentSortOrder; + } + + if (Reflect.has(data, 'audioLanguage')) { + this.audio_language = parseObject(data.audioLanguage); + } + + if (Reflect.has(data, 'allowRatings')) { + this.allow_ratings = data.allowRatings; + } + + if (Reflect.has(data, 'ageRestriction')) { + this.age_restriction = data.ageRestriction; + } + + if (Reflect.has(data, 'license')) { + this.license = data.license; + } + + if (Reflect.has(data, 'features')) { + this.features = parseObject>(data.features); + } + + if (Reflect.has(data, 'uncaptionedReason')) { + this.uncaptioned_reason = data.uncaptionedReason; + } + + if (Reflect.has(data, 'publishing')) { + this.publishing = parseObject(data.publishing); + } + + if (Reflect.has(data, 'paidProductPlacement')) { + this.paid_product_placement = data.paidProductPlacement; + } + + if (Reflect.has(data, 'allowEmbed')) { + this.allow_embed = data.allowEmbed; + } + + if (Reflect.has(data, 'music')) { + this.music = parseObject(data.music); + } + + if (Reflect.has(data, 'monetization')) { + this.monetization = parseObject(data.monetization); + } + + if (Reflect.has(data, 'visibility')) { + this.visibility = parseObject(data.visibility); + } + + if (Reflect.has(data, 'origin')) { + this.origin = data.origin; + } + + if (Reflect.has(data, 'inlineEditProcessingStatus')) { + this.inline_edit_processing_status = data.inlineEditProcessingStatus; + } + + if (Reflect.has(data, 'copyrightSummary')) { + this.copyright_summary = parseObject(data.copyrightSummary); + } + + if (Reflect.has(data, 'sponsorsOnly')) { + this.sponsors_only = parseObject(data.sponsorsOnly); + } + + if (Reflect.has(data, 'serializedShareEntity')) { + this.serialized_share_entity = data.serializedShareEntity; + } + + if (Reflect.has(data, 'unlistedExpired')) { + this.unlisted_expired = data.unlistedExpired; + } + + if (Reflect.has(data, 'originalFilename')) { + this.original_filename = data.originalFilename; + } + + if (Reflect.has(data, 'audienceRestriction')) { + this.audience_restriction = parseObject(data.audienceRestriction); + } + + if (Reflect.has(data, 'ownedClaimDetails')) { + this.owned_claim_details = parseObject(data.ownedClaimDetails); + } + + if (Reflect.has(data, 'monetizedStatus')) { + this.monetized_status = data.monetizedStatus; + } + + if (Reflect.has(data, 'commentsDisabledInternally')) { + this.comments_disabled_internally = data.commentsDisabledInternally; + } + + if (Reflect.has(data, 'allRestrictions')) { + this.all_restrictions = parseObject(data.allRestrictions); + } + + if (Reflect.has(data, 'videoResolutions')) { + this.video_resolutions = parseObject(data.videoResolutions); + } + + if (Reflect.has(data, 'adSettings')) { + this.ad_settings = parseObject(data.adSettings); + } + + if (Reflect.has(data, 'videoPrechecks')) { + const raw_prechecks = data.videoPrechecks; + this.video_prechecks = { + copyright_prechecks_done: raw_prechecks.copyrightPrechecksDone, + brand_safety_prechecks_done: raw_prechecks.brandSafetyPrechecksDone, + video_upload_checks_monetized: Reflect.has(raw_prechecks, 'videoUploadChecksMonetized') ? new VideoUploadChecks(raw_prechecks.videoUploadChecksMonetized) : undefined, + video_upload_checks_not_monetized: Reflect.has(raw_prechecks, 'videoUploadChecksNotMonetized') ? new VideoUploadChecks(raw_prechecks.videoUploadChecksNotMonetized) : undefined, + additional_details: Reflect.has(raw_prechecks, 'additionalDetails') ? parseObject(raw_prechecks.additionalDetails) : undefined + }; + } + + if (Reflect.has(data, 'viewCountIsHidden')) { + this.view_count_is_hidden = data.viewCountIsHidden; + } + + if (Reflect.has(data, 'notification')) { + this.notification = parseObject(data.notification); + } + + if (Reflect.has(data, 'mfkSettings')) { + this.mfk_settings = parseObject(data.mfkSettings); + } + + if (Reflect.has(data, 'autoChapterSettings')) { + this.auto_chapter_settings = parseObject(data.autoChapterSettings); + } + + if (Reflect.has(data, 'remix')) { + this.remix = parseObject(data.remix); + } + + if (Reflect.has(data, 'contentOwnershipModelSettings')) { + this.content_ownership_model_settings = parseObject(data.contentOwnershipModelSettings); + } + + if (Reflect.has(data, 'publicMetrics')) { + const public_metrics = parseObject(data.publicMetrics); + for (const key of [ 'view_count', 'comment_count', 'like_count', 'external_view_count' ] as const) { + if (Reflect.has(public_metrics, key)) { + public_metrics[key] = toInt(public_metrics[key]); + } + } + this.public_metrics = public_metrics; + } + + if (Reflect.has(data, 'autoPlacesMentionedSettings')) { + this.auto_places_mentioned_settings = parseObject(data.autoPlacesMentionedSettings); + } + + if (Reflect.has(data, 'shorts')) { + this.shorts = parseObject(data.shorts); + } + + if (Reflect.has(data, 'contentType')) { + this.content_type = data.contentType; + } + + if (Reflect.has(data, 'isPaygated')) { + this.is_paygated = data.isPaygated; + } + + if (Reflect.has(data, 'learningConceptSettings')) { + this.learning_concept_settings = parseObject(data.learningConceptSettings); + } + + if (Reflect.has(data, 'commentSettings')) { + this.comment_settings = parseObject(data.commentSettings); + } + + if (Reflect.has(data, 'productAutotaggingSettings')) { + this.product_autotagging_settings = parseObject(data.productAutotaggingSettings); + } + + if (Reflect.has(data, 'collaboration')) { + this.collaboration = parseObject(data.collaboration); + } + + if (Reflect.has(data, 'paidPoliticalContent')) { + this.paid_political_content = parseObject(data.paidPoliticalContent); + } + + if (Reflect.has(data, 'superfansOnly')) { + this.superfans_only = parseObject(data.superfansOnly); + } + } +} diff --git a/src/parser/classes/ytstudio/DataFreshnessEntity.ts b/src/parser/classes/ytstudio/DataFreshnessEntity.ts new file mode 100644 index 0000000000..e9e2fd3b08 --- /dev/null +++ b/src/parser/classes/ytstudio/DataFreshnessEntity.ts @@ -0,0 +1,15 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; + +export default class DataFreshnessEntity extends YTNode { + static type = 'DataFreshnessEntity'; + + entity_key: string; + last_updated: { seconds: string, nanos: number }; + + constructor(data: RawNode) { + super(); + this.entity_key = data.key; + this.last_updated = data.lastUpdated; + } +} diff --git a/src/parser/classes/ytstudio/Translation.ts b/src/parser/classes/ytstudio/Translation.ts new file mode 100644 index 0000000000..3824820257 --- /dev/null +++ b/src/parser/classes/ytstudio/Translation.ts @@ -0,0 +1,62 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; +import { parseAll } from '../../parser.js'; + +export type TranslationStatus = + 'TRANSLATION_STATUS_DELETING' | 'TRANSLATION_STATUS_DRAFT' | 'TRANSLATION_STATUS_FAILED' | + 'TRANSLATION_STATUS_PROCESSING' | 'TRANSLATION_STATUS_PUBLISHED' | 'TRANSLATION_STATUS_PUBLISHING' | + 'TRANSLATION_STATUS_QC_FAILED' | 'TRANSLATION_STATUS_REVIEW' | 'TRANSLATION_STATUS_SUBMITTED' | + 'TRANSLATION_STATUS_SUCCESS' | 'TRANSLATION_STATUS_SYNCING_DRAFT' | 'TRANSLATION_STATUS_UNKNOWN'; + +export type TranslationSource = + 'TRANSLATION_SOURCE_CREATOR' | 'TRANSLATION_SOURCE_COMMUNITY' | 'TRANSLATION_SOURCE_AUTOMATIC'; + +export interface TTSTrackId { + kind: string; + lang: string; + name: string; +} + +export interface Segment { + start_time_ms: string; + duration_ms: string; + text: string; +} + +export interface CaptionSegments { + segments: Segment[]; +} + +export interface CaptionsTranslation { + status?: TranslationStatus; + source?: TranslationSource; + time_updated_seconds?: string; + tts_track_id?: TTSTrackId; + caption_segments?: CaptionSegments; + content_update_time?: string; + is_complex_track?: boolean; +} + +export default class Translation extends YTNode { + static type = 'Translation'; + + display_name?: string; + language_code?: string; + captions_translations?: CaptionsTranslation[]; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'displayName')) { + this.display_name = data.displayName; + } + + if (Reflect.has(data, 'languageCode')) { + this.language_code = data.languageCode; + } + + if (Reflect.has(data, 'captionsTranslations')) { + this.captions_translations = parseAll(data.captionsTranslations); + } + } +} diff --git a/src/parser/classes/ytstudio/UploadFeedbackItem.ts b/src/parser/classes/ytstudio/UploadFeedbackItem.ts new file mode 100644 index 0000000000..f20d9848ca --- /dev/null +++ b/src/parser/classes/ytstudio/UploadFeedbackItem.ts @@ -0,0 +1,99 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; +import { parseObject } from '../../parser.js'; +import DataFreshnessEntity from './DataFreshnessEntity.js'; +import VideoUploadChecks from './VideoUploadChecks.js'; + +export interface TimedContinuationData { + timeout_ms: number; + continuation: string; + click_tracking_params: string; +} + +export interface UploadFeedbackRefreshContinuation { + continuation: string; + continue_in_ms: number; + click_tracking_params: string; +} + +export interface UploadFeedbackContinuation { + timed_continuation_data?: TimedContinuationData; + upload_feedback_refresh_continuation?: UploadFeedbackRefreshContinuation; +} + +export interface TransferProgressBar { + fraction_completed?: number; + progress_message?: string; +} + +export interface UploadChecksRenderer { + checks_data_video_monetized?: VideoUploadChecks; + checks_data_video_not_monetized?: VideoUploadChecks; +} + +export default class UploadFeedbackItem extends YTNode { + static type = 'UploadFeedbackItem'; + + frontend_upload_id?: string; + video_id?: string; + continuations?: UploadFeedbackContinuation; + data_freshness_entity?: DataFreshnessEntity; + transfer_progress_bar?: TransferProgressBar; + is_processing?: boolean; + upload_checks?: UploadChecksRenderer; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'id')) { + this.frontend_upload_id = data.id.frontendUploadId; + this.video_id = data.id.videoId; + } + + this.is_processing = false; + if (Reflect.has(data, 'contents') && Array.isArray(data.contents)) { + data.contents.forEach((item: RawNode) => { + if (Reflect.has(item, 'transferProgressBar')) { + this.transfer_progress_bar = { + fraction_completed: item.transferProgressBar.fractionCompleted, + progress_message: item.transferProgressBar.progressMessage?.simpleText + }; + } + + if (Reflect.has(item, 'uploadChecksRenderer')) { + const renderer = item.uploadChecksRenderer; + this.upload_checks = { + checks_data_video_monetized: Reflect.has(renderer, 'checksDataVideoMonetized') ? new VideoUploadChecks(renderer.checksDataVideoMonetized) : undefined, + checks_data_video_not_monetized: Reflect.has(renderer, 'checksDataVideoNotMonetized') ? new VideoUploadChecks(renderer.checksDataVideoNotMonetized) : undefined + }; + } + + if (Reflect.has(item, 'processingResolutionsStatusRenderer')) { + this.is_processing = true; + } + }); + } + if (Reflect.has(data, 'continuations')) { + this.continuations = { + timed_continuation_data: parseObject(data.continuations[0]?.timedContinuationData), + upload_feedback_refresh_continuation: parseObject(data.continuations[1]?.uploadFeedbackRefreshContinuation) + }; + } + if (Reflect.has(data, 'dataFreshnessEntity')) { + this.data_freshness_entity = new DataFreshnessEntity(data.dataFreshnessEntity); + } + } + + get continuation_token(): string | null { + const continuation = this.continuations; + return this.continuations?.upload_feedback_refresh_continuation?.continuation ?? + this.continuations?.timed_continuation_data?.continuation ?? + null; + } + + get continuation_delay_ms(): number | null { + return this.continuations?.upload_feedback_refresh_continuation?.continue_in_ms ?? + this.continuations?.timed_continuation_data?.timeout_ms ?? + null; + } +} diff --git a/src/parser/classes/ytstudio/VideoUploadChecks.ts b/src/parser/classes/ytstudio/VideoUploadChecks.ts new file mode 100644 index 0000000000..0f864a4d88 --- /dev/null +++ b/src/parser/classes/ytstudio/VideoUploadChecks.ts @@ -0,0 +1,59 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; + +export type UploadChecksSummaryStatus = + 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_COMPLETED' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_EXTENDED_CHECK_STARTED' | + 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_INLINE_EDIT_IN_PROGRESS' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_NOT_STARTED' | + 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_OVERDUE' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_SELF_CERTIFICATION_MISSING' | + 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_SHORTS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_STARTED' | + 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_UNABLE_TO_RUN' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_UNKNOWN'; + +export type UploadChecksCopyrightStatus = + 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_COMPLETED' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_INLINE_EDIT_IN_PROGRESS' | + 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_NOT_STARTED' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_OVERDUE' | + 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_SHORTS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_STARTED' | + 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_UNABLE_TO_RUN' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_UNKNOWN'; + +export type UploadChecksAdSuitabilityStatus = + 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_CHANNEL_NOT_MONETIZED' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_COMPLETED' | + 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_EXTENDED_CHECK_STARTED' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_NOT_STARTED' | + 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_OVERDUE' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_SELF_CERTIFICATION_MISSING' | + 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_SHORTS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_STARTED' | + 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_UNABLE_TO_RUN' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_UNKNOWN' | + 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_VIDEO_NOT_MONETIZED'; + +export type UploadChecksCommunityGuidelinesStatus = + 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_COMPLETED' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_NOT_AVAILABLE_ON_PUBLISHED_VIDEO' | + 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_NOT_STARTED' | + 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_OVERDUE' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_SHORTS_NOT_ELIGIBLE' | + 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_STARTED' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_UNABLE_TO_RUN' | + 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_UNKNOWN'; + +export default class VideoUploadChecks extends YTNode { + static type = 'VideoUploadChecks'; + + checks_summary?: UploadChecksSummaryStatus; + copyright_check?: UploadChecksCopyrightStatus; + ad_suitability_check?: UploadChecksAdSuitabilityStatus; + community_guidelines_check?: UploadChecksCommunityGuidelinesStatus; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'checksSummary') && Reflect.has(data.checksSummary, 'status')) { + this.checks_summary = data.checksSummary.status; + } + + if (Reflect.has(data, 'copyrightCheck') && Reflect.has(data.copyrightCheck, 'checkStatus')) { + this.copyright_check = data.copyrightCheck.checkStatus; + } + + if (Reflect.has(data, 'adSuitabilityCheck') && Reflect.has(data.adSuitabilityCheck, 'checkStatus')) { + this.ad_suitability_check = data.adSuitabilityCheck.checkStatus; + } + + if (Reflect.has(data, 'communityGuidelinesCheck') && Reflect.has(data.communityGuidelinesCheck, 'checkStatus')) { + this.community_guidelines_check = data.communityGuidelinesCheck.checkStatus; + } + } +} diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index 21bd60f064..4d95683ff3 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -559,3 +559,8 @@ export { default as KidsBlocklistPickerItem } from './classes/ytkids/KidsBlockli export { default as KidsCategoriesHeader } from './classes/ytkids/KidsCategoriesHeader.js'; export { default as KidsCategoryTab } from './classes/ytkids/KidsCategoryTab.js'; export { default as KidsHomeScreen } from './classes/ytkids/KidsHomeScreen.js'; +export { default as CreatorVideo } from './classes/ytstudio/CreatorVideo.js'; +export { default as DataFreshnessEntity } from './classes/ytstudio/DataFreshnessEntity.js'; +export { default as Translation } from './classes/ytstudio/Translation.js'; +export { default as UploadFeedbackItem } from './classes/ytstudio/UploadFeedbackItem.js'; +export { default as VideoUploadChecks } from './classes/ytstudio/VideoUploadChecks.js'; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 9e980645ad..c0d2e4cd74 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -232,8 +232,15 @@ export function getDynamicParsers() { export function parseResponse(data: IRawResponse): T { const parsed_data = {} as T; + const upload_feedback_item = (data.contents && !Array.isArray(data.contents) && data.contents.uploadFeedbackItemRenderer) ? + new YTNodes.UploadFeedbackItem(data.contents.uploadFeedbackItemRenderer) : + null; + if (upload_feedback_item) { + parsed_data.upload_feedback_item = upload_feedback_item; + } + _createMemo(); - const contents = parse(data.contents); + const contents = upload_feedback_item ? null : parse(data.contents); const contents_memo = _getMemo(); if (contents) { parsed_data.contents = contents; @@ -267,9 +274,15 @@ export function parseResponse(data: parsed_data.on_response_received_commands_memo = on_response_received_commands_memo; } _clearMemo(); + if (Array.isArray(data.continuationContents)) { + parsed_data.upload_feedback_items = data.continuationContents + .map((entry) => entry.uploadFeedbackItemContinuation) + .filter((entry) => entry) + .map((entry) => new YTNodes.UploadFeedbackItem(entry)); + } _createMemo(); - const continuation_contents = data.continuationContents ? parseLC(data.continuationContents) : null; + const continuation_contents = (data.continuationContents && !Array.isArray(data.continuationContents)) ? parseLC(data.continuationContents) : null; const continuation_contents_memo = _getMemo(); if (continuation_contents) { parsed_data.continuation_contents = continuation_contents; @@ -566,6 +579,27 @@ export function parseResponse(data: parsed_data.target_id = data.targetId; } + if (data.videoId) { + parsed_data.video_id = data.videoId; + } + + if (data.translation) { + parsed_data.translation = new YTNodes.Translation(data.translation); + } + + if (data.videos) { + parsed_data.creator_videos = data.videos.map((video) => new YTNodes.CreatorVideo(video)); + } + + if (data.feedbackResponses) { + parsed_data.feedback_responses = data.feedbackResponses; + } + + const challenge_prompt_type = data.responseContext?.webResponseContextExtensionData?.challenge?.type; + if (challenge_prompt_type) { + parsed_data.challenge_prompt_type = challenge_prompt_type; + } + return parsed_data; } @@ -810,6 +844,27 @@ export function parseFormats(formats: RawNode[], this_response_nsig_cache: Map new Format(format, this_response_nsig_cache)) || []; } +// useful for YTStudio since it outputs more in a nested data format rather than a 'rendering' format +export function parseObject(data: unknown): T { + if (Array.isArray(data)) { + return data.map((item) => parseObject(item)) as T; + } + + if (data !== null && typeof data === 'object') { + const result: Record = {}; + for (const [ key, value ] of Object.entries(data)) { + result[camelToSnake(key)] = parseObject(value); + } + return result as T; + } + + return data as T; +} +export function parseAll(data: unknown): T[] { + if (!Array.isArray(data)) return []; + return data.map((item) => parseObject(item)); +} + export function applyMutations(memo: Memo, mutations: RawNode[]) { // Apply mutations to MusicMultiSelectMenuItems const music_multi_select_menu_items = memo.getType(MusicMultiSelectMenuItem); diff --git a/src/parser/types/ParsedResponse.ts b/src/parser/types/ParsedResponse.ts index 56541a94cb..50a2b23866 100644 --- a/src/parser/types/ParsedResponse.ts +++ b/src/parser/types/ParsedResponse.ts @@ -4,6 +4,9 @@ import type { ItemSectionContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, PlaylistPanelContinuation, SectionListContinuation, ContinuationCommand, ShowMiniplayerCommand, NavigateAction } from '../index.js'; +import type Translation from '../classes/ytstudio/Translation.js'; +import type CreatorVideo from '../classes/ytstudio/CreatorVideo.js'; +import type UploadFeedbackItem from '../classes/ytstudio/UploadFeedbackItem.js'; import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js'; import type CardCollection from '../classes/CardCollection.js'; @@ -89,6 +92,13 @@ export interface IParsedResponse { continuation_endpoint?: YTNode; player_response?: IPlayerResponse; watch_next_response?: INextResponse; + video_id?: string; + translation?: Translation; + creator_videos?: CreatorVideo[]; + feedback_responses?: { isProcessed: boolean }[]; + upload_feedback_item?: UploadFeedbackItem; + upload_feedback_items?: UploadFeedbackItem[]; + challenge_prompt_type?: 'CHALLENGE_PROMPT_TYPE_UNSPECIFIED' | 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE'; } export interface ITrustedResource { @@ -164,4 +174,10 @@ export type IGetChallengeResponse = Pick; export type IGetSessionTokenResponse = Pick; export type IGetWebReauthURLResponse = Pick; -export type IShowEngagementPanelResponse = Pick; \ No newline at end of file +export type IShowEngagementPanelResponse = Pick; +export type ICreateCaptionsResponse = Pick; +export type IParseCaptionsResponse = Pick; +export type IUpdateCaptionsResponse = Pick; +export type IMetadataUpdateResponse = Pick; +export type ICreateVideoResponse = Pick; +export type IUploadFeedbackResponse = Pick; diff --git a/src/parser/types/RawResponse.ts b/src/parser/types/RawResponse.ts index 25d4c7dc6f..861c7ae2a7 100644 --- a/src/parser/types/RawResponse.ts +++ b/src/parser/types/RawResponse.ts @@ -1586,7 +1586,7 @@ export interface IRawResponse { onResponseReceivedActions?: RawNode[]; onResponseReceivedEndpoints?: RawNode[]; onResponseReceivedCommands?: RawNode[]; - continuationContents?: RawNode; + continuationContents?: RawData; actions?: RawNode[]; liveChatItemContextMenuSupportedRenderers?: RawNode; header?: RawNode; @@ -1641,5 +1641,9 @@ export interface IRawResponse { frameworkUpdates?: any; engagementPanels?: RawNode[]; entries?: RawNode[]; + videoId?: string; + translation?: RawNode; + videos?: RawNode[]; + feedbackResponses?: { isProcessed: boolean }[]; [key: string]: any; } diff --git a/src/parser/youtube/History.ts b/src/parser/youtube/History.ts index 5467b72a4b..fb61e18add 100644 --- a/src/parser/youtube/History.ts +++ b/src/parser/youtube/History.ts @@ -65,7 +65,7 @@ export default class History extends Feed { const response = await this.actions.execute('/feedback', body); const data = response.data; - if (!data.feedbackResponses[0].isProcessed) { + if (!data.feedbackResponses?.[0].isProcessed) { throw new Error('Failed to remove video from watch history'); } diff --git a/src/types/StudioWebUploading.ts b/src/types/StudioWebUploading.ts index 23cb3953cd..ba8b9489b0 100644 --- a/src/types/StudioWebUploading.ts +++ b/src/types/StudioWebUploading.ts @@ -19,260 +19,49 @@ export interface FileNamedBufferBase64 { export type StudioVisibility = 'PUBLIC' | 'UNLISTED' | 'PRIVATE'; export interface UploadVideoDetails { - title?: string; - description?: string; - thumbnail?: FileNamedBufferReader; - playlists?: string[]; - audience?: 'MADE_FOR_KIDS'|'NOT_MADE_FOR_KIDS'; - - paid_promotion?: boolean; - ai_use?: boolean; - - collaboration_channels?: { - id: string; - analytics_setting: 'VIDEO_COLLABORATOR_ANALYTICS_SETTING_NONE'|'VIDEO_COLLABORATOR_ANALYTICS_SETTING_BASIC' - }[]; - - automatic_chapters?: boolean; - featured_places?: boolean; - automatic_concepts?: boolean; - tags?: string[]; - - video_language?: string; - caption_certification?: string; - title_and_description_language?: string; - - recording_date?: Date; - video_location?: string; - - license?: string; - allow_embedding?: boolean; - publish_to_subscriptions_feed_and_notify_subscribers?: boolean; - shorts_remixing?: 'ALLOW_VIDEO_AND_AUDIO'|'ALLOW_ONLY_AUDIO'|'DONT_ALLOW'; - category?: 'FILM'|'AUTOS'|'MUSIC'|'PETS'|'SPORTS'|'TRAVEL'|'GADGETS'|'PEOPLE'|'COMEDY'|'ENTERTAINMENT'|'NEWS'|'HOWTO'|'EDUCATION'|'SCIENCE'|'GOVERNMENT'; - - allow_comments?: 'ON'|'OFF'|'PAUSE'; - comment_moderation?: 'NONE'|'BASIC'|'STRICT'|'HOLD_ALL'; - who_can_comment?: 'ANYONE'|'SUBSCRIBERS_AND_MEMBERS'; - sort_comments_by?: 'TOP'|'NEWEST'; - show_how_many_viewers_like_this_video?: boolean; - - visibility?: StudioVisibility; - subtitles?: { - data: FileNamedBufferBase64; - synced: boolean; - auto_translate?: boolean; - overwrite?: boolean; - }; -}; - -export interface CreatorVideo { - videoId: string; - channelId?: string; title?: string; description?: string; - privacy?: 'VIDEO_PRIVACY_PUBLIC' | 'VIDEO_PRIVACY_UNLISTED' | 'VIDEO_PRIVACY_PRIVATE'; - status?: 'VIDEO_STATUS_PROCESSED'; - draftStatus?: 'DRAFT_STATUS_NONE'; - shareUrl?: string; - watchUrl?: string; - lengthSeconds?: string; - videoDurationMs?: number; - timeCreatedSeconds?: string; - timePublishedSeconds?: string; - thumbnailDetails?: { thumbnails: { url: string, width: number, height: number }[] }; + thumbnail?: FileNamedBufferReader; + playlists?: string[]; + audience?: 'MADE_FOR_KIDS'|'NOT_MADE_FOR_KIDS'; + + paid_promotion?: boolean; + ai_use?: boolean; + + collaboration_channels?: { + id: string; + analytics_setting: 'VIDEO_COLLABORATOR_ANALYTICS_SETTING_NONE'|'VIDEO_COLLABORATOR_ANALYTICS_SETTING_BASIC' + }[]; + + automatic_chapters?: boolean; + featured_places?: boolean; + automatic_concepts?: boolean; + tags?: string[]; + + video_language?: string; + caption_certification?: string; + title_and_description_language?: string; + + recording_date?: Date; + video_location?: string; + + license?: string; + allow_embedding?: boolean; + publish_to_subscriptions_feed_and_notify_subscribers?: boolean; + shorts_remixing?: 'ALLOW_VIDEO_AND_AUDIO'|'ALLOW_ONLY_AUDIO'|'DONT_ALLOW'; + category?: 'FILM'|'AUTOS'|'MUSIC'|'PETS'|'SPORTS'|'TRAVEL'|'GADGETS'|'PEOPLE'|'COMEDY'|'ENTERTAINMENT'|'NEWS'|'HOWTO'|'EDUCATION'|'SCIENCE'|'GOVERNMENT'; + + allow_comments?: 'ON'|'OFF'|'PAUSE'; + comment_moderation?: 'NONE'|'BASIC'|'STRICT'|'HOLD_ALL'; + who_can_comment?: 'ANYONE'|'SUBSCRIBERS_AND_MEMBERS'; + sort_comments_by?: 'TOP'|'NEWEST'; + show_how_many_viewers_like_this_video?: boolean; + + visibility?: StudioVisibility; + subtitles?: { + data: FileNamedBufferBase64; + synced: boolean; + auto_translate?: boolean; + overwrite?: boolean; + }; }; - -// [Raw Types] - -export interface CreateVideoResponse { - responseContext: ResponseContext; - videoId: string; - contents: Contents; -} - -export interface ResponseContext { - serviceTrackingParams: ServiceTrackingParam[]; - consistencyTokenJar: ConsistencyTokenJar; - stateTags: StateTags; - responseId: string; - webResponseContextExtensionData: WebResponseContextExtensionData; -} - -export interface ServiceTrackingParam { - service: string; - params: Param[]; -} - -export interface Param { - key: string; - value: string; -} - -export interface ConsistencyTokenJar { - encryptedTokenJarContents: string; - expirationSeconds: string; -} - -export interface StateTags { - stateTagsModified: number[]; -} - -export interface WebResponseContextExtensionData { - hasDecorated: boolean; - challenge?: { type?: 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE' } -} - -export interface Contents { - uploadFeedbackItemRenderer: UploadFeedbackItemRenderer; -} - -export interface UploadFeedbackItemRenderer { - id: Id; - continuations: Continuation[]; - dataFreshnessEntity: DataFreshnessEntity; -} - -export interface Id { - frontendUploadId: string; - videoId: string; -} - -export interface Continuation { - timedContinuationData?: TimedContinuationData; - uploadFeedbackRefreshContinuation?: UploadFeedbackRefreshContinuation; -} - -export interface TimedContinuationData { - timeoutMs: number; - continuation: string; - clickTrackingParams: string; -} - -export interface UploadFeedbackRefreshContinuation { - continuation: string; - continueInMs: number; - clickTrackingParams: string; -} - -export interface DataFreshnessEntity { - key: string; - lastUpdated: LastUpdated; -} - -export interface LastUpdated { - seconds: string; - nanos: number; -} - -// [FeedbackContinuation] ========================================================== - -export type ContinuationsUploadFeedback = ContinuationUploadFeedback[]; - -export interface ContinuationUploadFeedback { - uploadFeedbackItemContinuation: UploadFeedbackItemContinuation; -} - -export interface UploadFeedbackItemContinuation { - id: Id; - contents: Content[]; - continuations: Continuation[]; - dataFreshnessEntity: DataFreshnessEntity; -} - -export interface Id { - frontendUploadId: string; - videoId: string; -} - -export interface Content { - transferProgressBar?: TransferProgressBar; - uploadChecksRenderer?: UploadChecksRenderer; -} - -export interface TransferProgressBar { - fractionCompleted: number; - progressMessage: ProgressMessage; -} - -export interface ProgressMessage { - simpleText: string; -} - -export interface UploadChecksRenderer { - checksDataVideoMonetized: ChecksDataVideoMonetized; - checksDataVideoNotMonetized: ChecksDataVideoNotMonetized; -} - -export interface ChecksDataVideoMonetized { - checksSummary: ChecksSummary; - copyrightCheck: CopyrightCheck; - adSuitabilityCheck: AdSuitabilityCheck; - communityGuidelinesCheck: CommunityGuidelinesCheck; -} - -export interface ChecksSummary { - status: string; -} - -export interface CopyrightCheck { - checkStatus: string; -} - -export interface AdSuitabilityCheck { - checkStatus: string; -} - -export interface CommunityGuidelinesCheck { - checkStatus: string; -} - -export interface ChecksDataVideoNotMonetized { - checksSummary: ChecksSummary2; - copyrightCheck: CopyrightCheck2; - adSuitabilityCheck: AdSuitabilityCheck2; - communityGuidelinesCheck: CommunityGuidelinesCheck2; -} - -export interface ChecksSummary2 { - status: string; -} - -export interface CopyrightCheck2 { - checkStatus: string; -} - -export interface AdSuitabilityCheck2 { - checkStatus: string; -} - -export interface CommunityGuidelinesCheck2 { - checkStatus: string; -} - -export interface Continuation { - timedContinuationData?: TimedContinuationData; - uploadFeedbackRefreshContinuation?: UploadFeedbackRefreshContinuation; -} - -export interface TimedContinuationData { - timeoutMs: number; - continuation: string; - clickTrackingParams: string; -} - -export interface UploadFeedbackRefreshContinuation { - continuation: string; - continueInMs: number; - clickTrackingParams: string; -} - -export interface DataFreshnessEntity { - key: string; - lastUpdated: LastUpdated; -} - -export interface LastUpdated { - seconds: string; - nanos: number; -} From 5954f2540f42ef27d47c0da2a2968cb006741156 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 16:44:52 -0700 Subject: [PATCH 18/27] refactor(YouTube Studio Web Examples): just simply fetch `CHANNEL_ID` instead --- examples/youtube-studio-web/src/GetSessionToken.ts | 3 +-- examples/youtube-studio-web/src/UploadVideo.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/youtube-studio-web/src/GetSessionToken.ts b/examples/youtube-studio-web/src/GetSessionToken.ts index b5d74c1d7b..fa44f29dac 100644 --- a/examples/youtube-studio-web/src/GetSessionToken.ts +++ b/examples/youtube-studio-web/src/GetSessionToken.ts @@ -6,8 +6,7 @@ const COOKIES = ""; // ?? Place your YouTube cookies here (async () => { const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); - // ?? Place your channel_ID here or use `await get_channel_id(yt)` - const CHANNEL_ID = ''; + const CHANNEL_ID = await get_channel_id(yt); const yt_studio_web = yt.studioWeb(CHANNEL_ID); yt_studio_web.setBotGuardSolver(botguard_solver); diff --git a/examples/youtube-studio-web/src/UploadVideo.ts b/examples/youtube-studio-web/src/UploadVideo.ts index 60f4babcd3..a2aff08d89 100644 --- a/examples/youtube-studio-web/src/UploadVideo.ts +++ b/examples/youtube-studio-web/src/UploadVideo.ts @@ -9,8 +9,7 @@ const SRT_FILE_PATH = ""; // ?? Place your srt file path here (async () => { const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); - // ?? Place your channel_ID here or use `await get_channel_id(yt)` - const CHANNEL_ID = ''; + const CHANNEL_ID = await get_channel_id(yt); const yt_studio_web = yt.studioWeb(CHANNEL_ID); yt_studio_web.setBotGuardSolver(botguard_solver); From 99a93311e33393e946847d75135947a0206dc3f6 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 18:51:50 -0700 Subject: [PATCH 19/27] refactor(YouTube Studio Web): move the constant context into HTTPClient just like the rest of them --- src/core/Actions.ts | 2 -- src/utils/HTTPClient.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/Actions.ts b/src/core/Actions.ts index b6c231fa32..13b135ec90 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -179,8 +179,6 @@ export default class Actions { delete this.session.context.user?.serializedDelegationContext; if (data?.client === 'WEB_CREATOR') { if (this.session.context.request) { // should just be true - this.session.context.request.returnLogEntry = true; - // TODO maybe I want to manually fetch the initial eats; but it seems that it doesn't matter to much... if (data?.eats) { this.session.context.request.eats = data?.eats; diff --git a/src/utils/HTTPClient.ts b/src/utils/HTTPClient.ts index 18aff55120..f81dcecec5 100644 --- a/src/utils/HTTPClient.ts +++ b/src/utils/HTTPClient.ts @@ -333,6 +333,7 @@ export default class HTTPClient { case 'WEB_CREATOR': ctx.client.clientName = Constants.CLIENTS.WEB_CREATOR.NAME; ctx.client.clientVersion = Constants.CLIENTS.WEB_CREATOR.VERSION; + if (ctx.request) ctx.request.returnLogEntry = true; break; default: break; From f6df8f758086becf1cdffa09c5fe66f262d0d054 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 19:01:15 -0700 Subject: [PATCH 20/27] fix(YouTube Studio Web Examples): use new system that uses parser classes instead of raw types --- examples/youtube-studio-web/src/UploadVideo.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/youtube-studio-web/src/UploadVideo.ts b/examples/youtube-studio-web/src/UploadVideo.ts index a2aff08d89..a3a3dc5dd5 100644 --- a/examples/youtube-studio-web/src/UploadVideo.ts +++ b/examples/youtube-studio-web/src/UploadVideo.ts @@ -31,11 +31,8 @@ const SRT_FILE_PATH = ""; // ?? Place your srt file path here }, async (full_created) => { // NOTE this cycle fires every 2s-ish await yt_studio_web.uploadFeedbackCycle([full_created.feedback_token], (content) => { - if (content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.transferProgressBar) - console.log(content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.transferProgressBar); - else if (content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.uploadChecksRenderer) - console.log(content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]?.uploadChecksRenderer); - else console.log(content?.[0]?.uploadFeedbackItemContinuation?.contents?.[0]); + if(content[0].transfer_progress_bar?.progress_message) console.log(content[0].transfer_progress_bar?.progress_message); + if(content[0].is_processing) console.log("processing..."); return true; }); }); From 3f1cd882ba806a0ea2266bdd6d3094d77a023ee9 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 19:22:02 -0700 Subject: [PATCH 21/27] fix(YouTube Studio Web): metadata endpoint doesn't return videos, but just a single double wrapped creator video --- src/parser/parser.ts | 4 ++-- src/parser/types/ParsedResponse.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index c0d2e4cd74..3f94ca007c 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -587,8 +587,8 @@ export function parseResponse(data: parsed_data.translation = new YTNodes.Translation(data.translation); } - if (data.videos) { - parsed_data.creator_videos = data.videos.map((video) => new YTNodes.CreatorVideo(video)); + if (data?.creatorEntities?.wrappedVideoData?.video) { + parsed_data.creator_video = new YTNodes.CreatorVideo(data.creatorEntities.wrappedVideoData.video); } if (data.feedbackResponses) { diff --git a/src/parser/types/ParsedResponse.ts b/src/parser/types/ParsedResponse.ts index 50a2b23866..b843e85135 100644 --- a/src/parser/types/ParsedResponse.ts +++ b/src/parser/types/ParsedResponse.ts @@ -94,7 +94,7 @@ export interface IParsedResponse { watch_next_response?: INextResponse; video_id?: string; translation?: Translation; - creator_videos?: CreatorVideo[]; + creator_video?: CreatorVideo; feedback_responses?: { isProcessed: boolean }[]; upload_feedback_item?: UploadFeedbackItem; upload_feedback_items?: UploadFeedbackItem[]; @@ -178,6 +178,6 @@ export type IShowEngagementPanelResponse = Pick; export type ICreateCaptionsResponse = Pick; export type IParseCaptionsResponse = Pick; export type IUpdateCaptionsResponse = Pick; -export type IMetadataUpdateResponse = Pick; +export type IMetadataUpdateResponse = Pick; export type ICreateVideoResponse = Pick; export type IUploadFeedbackResponse = Pick; From 99b2e0ab63fbe58cb19755a8b73d7a2f9157122a Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 19:28:48 -0700 Subject: [PATCH 22/27] refactor(YouTube Studio Web): make `update_metadata_response` and `update_subtitles_response` initially null instead of object to allow to type autocomplete --- src/core/clients/StudioWeb.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index 06d688f072..3ade7c8a8f 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -539,8 +539,8 @@ export default class StudioWeb { const payload = this.#buildMetadataUpdate(details, thumbnail_resource_id); - let update_metadata_response: IMetadataUpdateResponse | object = {}; - let update_subtitles_response: UploadSubtitlesResponse | object = {}; + let update_metadata_response: IMetadataUpdateResponse | null = null; + let update_subtitles_response: UploadSubtitlesResponse | null = null; if (Object.keys(payload).length > 0) { update_metadata_response = await this.#updateMetadata(video_id, payload); From 449d9681a63848030c4cec7f71ee1eb9b8410a53 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 19:30:49 -0700 Subject: [PATCH 23/27] fix(YouTube Studio Web Example): should multiply by 100 to show actual percent --- examples/youtube-studio-web/src/UploadVideo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/youtube-studio-web/src/UploadVideo.ts b/examples/youtube-studio-web/src/UploadVideo.ts index a3a3dc5dd5..00878d1d04 100644 --- a/examples/youtube-studio-web/src/UploadVideo.ts +++ b/examples/youtube-studio-web/src/UploadVideo.ts @@ -27,7 +27,7 @@ const SRT_FILE_PATH = ""; // ?? Place your srt file path here visibility: "PRIVATE" }, (written_bytes, total_bytes) => { // NOTE this callback only fires around every 100mb - console.log(`${written_bytes / total_bytes}%`); + console.log(`${(written_bytes / total_bytes) * 100}%`); }, async (full_created) => { // NOTE this cycle fires every 2s-ish await yt_studio_web.uploadFeedbackCycle([full_created.feedback_token], (content) => { From bfc6b12234c176b818a313fa8f22ee250ee7e865 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 21:44:50 -0700 Subject: [PATCH 24/27] refactor(YouTube Studio Web / Parser): rework the new Studio part of the parser to use what already exists instead --- src/core/clients/StudioWeb.ts | 8 +++--- src/parser/parser.ts | 42 ++++++++++++++++-------------- src/parser/types/ParsedResponse.ts | 13 +++++---- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index 3ade7c8a8f..40753b0a2e 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -2,10 +2,10 @@ import type { BotGuardChallenge, BotGuardSolver } from '../../types/BotGuard.js' import type { EngagementType } from '../../types/Misc.js'; import type { FileNamedBufferReader, StudioVisibility, UploadVideoDetails } from '../../types/StudioWebUploading.js'; import type { ICreateCaptionsResponse, ICreateVideoResponse, IMetadataUpdateResponse, IParseCaptionsResponse, IParsedResponse, IUpdateCaptionsResponse } from '../../parser/index.js'; -import type UploadFeedbackItem from '../../parser/classes/ytstudio/UploadFeedbackItem.js'; import { Constants, Log } from '../../utils/index.js'; import { InnertubeError, Platform, wait } from '../../utils/Utils.js'; import type { Actions, ParsedResponse, Session } from '../index.js'; +import { UploadFeedbackItem } from '../../parser/nodes.js'; type AttestationPlacement = 'none' | 'context' | 'top_level'; @@ -569,10 +569,10 @@ export default class StudioWeb { type === 'CONTINUATION_TOKENS' ? { continuations: tokens } : { feedbackTokens: tokens }); try { if (type === 'FEEDBACK_TOKENS') return feedback_data.feedback_responses?.[0] as { isProcessed: boolean }; - const contents = feedback_data.upload_feedback_items ?? []; + const contents = (feedback_data.continuation_contents_array ?? []) as UploadFeedbackItem[]; return { contents, next: async () => await this.uploadFeedback([ - contents[0]?.continuation_token ?? '' + contents[0]?.as(UploadFeedbackItem)?.continuation_token ?? '' ], 'CONTINUATION_TOKENS') }; } catch (_) { @@ -632,7 +632,7 @@ export default class StudioWeb { presumedShort: false }, this.#channel_id, 'context'); - on_initial_create_video?.({ created, feedback_token: created.upload_feedback_item?.continuation_token ?? null }); + on_initial_create_video?.({ created, feedback_token: created.contents?.item()?.as(UploadFeedbackItem).continuation_token ?? null }); const video_id = created.video_id; if (video_id === undefined || video_id === '') { diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 3f94ca007c..92097eaaaa 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -41,6 +41,9 @@ import CommentView from './classes/comments/CommentView.js'; import MusicThumbnail from './classes/MusicThumbnail.js'; import OpenPopupAction from './classes/actions/OpenPopupAction.js'; import AppendContinuationItemsAction from './classes/actions/AppendContinuationItemsAction.js'; +import UploadFeedbackItem from './classes/ytstudio/UploadFeedbackItem.js'; +import CreatorVideo from './classes/ytstudio/CreatorVideo.js'; +import Translation from './classes/ytstudio/Translation.js'; import type { IParsedResponse, IRawResponse, RawData, RawNode } from './types/index.js'; const TAG = 'Parser'; @@ -232,15 +235,8 @@ export function getDynamicParsers() { export function parseResponse(data: IRawResponse): T { const parsed_data = {} as T; - const upload_feedback_item = (data.contents && !Array.isArray(data.contents) && data.contents.uploadFeedbackItemRenderer) ? - new YTNodes.UploadFeedbackItem(data.contents.uploadFeedbackItemRenderer) : - null; - if (upload_feedback_item) { - parsed_data.upload_feedback_item = upload_feedback_item; - } - _createMemo(); - const contents = upload_feedback_item ? null : parse(data.contents); + const contents = parse(data.contents); const contents_memo = _getMemo(); if (contents) { parsed_data.contents = contents; @@ -274,15 +270,9 @@ export function parseResponse(data: parsed_data.on_response_received_commands_memo = on_response_received_commands_memo; } _clearMemo(); - if (Array.isArray(data.continuationContents)) { - parsed_data.upload_feedback_items = data.continuationContents - .map((entry) => entry.uploadFeedbackItemContinuation) - .filter((entry) => entry) - .map((entry) => new YTNodes.UploadFeedbackItem(entry)); - } _createMemo(); - const continuation_contents = (data.continuationContents && !Array.isArray(data.continuationContents)) ? parseLC(data.continuationContents) : null; + const continuation_contents = data.continuationContents ? parseLC(data.continuationContents) : null; const continuation_contents_memo = _getMemo(); if (continuation_contents) { parsed_data.continuation_contents = continuation_contents; @@ -290,6 +280,15 @@ export function parseResponse(data: } _clearMemo(); + _createMemo(); + const continuation_contents_array = data.continuationContents && Array.isArray(data.continuationContents) ? data.continuationContents.map((item) => parseLC(item)) : null; + const continuation_contents_array_memo = _getMemo(); + if (continuation_contents_array) { + parsed_data.continuation_contents_array = continuation_contents_array; + parsed_data.continuation_contents_array_memo = continuation_contents_array_memo; + } + _clearMemo(); + _createMemo(); const actions = data.actions ? parseActions(data.actions) : null; const actions_memo = _getMemo(); @@ -584,11 +583,11 @@ export function parseResponse(data: } if (data.translation) { - parsed_data.translation = new YTNodes.Translation(data.translation); + parsed_data.translation = new Translation(data.translation); } if (data?.creatorEntities?.wrappedVideoData?.video) { - parsed_data.creator_video = new YTNodes.CreatorVideo(data.creatorEntities.wrappedVideoData.video); + parsed_data.creator_video = new CreatorVideo(data.creatorEntities.wrappedVideoData.video); } if (data.feedbackResponses) { @@ -794,7 +793,11 @@ export function parseC(data: RawNode) { return null; } -export function parseLC(data: RawNode) { +export type ContinuationContents = null | ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | + MusicPlaylistShelfContinuation | MusicShelfContinuation | GridContinuation | + PlaylistPanelContinuation | ContinuationCommand | UploadFeedbackItem; +export function parseLC(data: RawNode): ContinuationContents +export function parseLC(data: RawNode): ContinuationContents { if (data.itemSectionContinuation) return new ItemSectionContinuation(data.itemSectionContinuation); if (data.sectionListContinuation) @@ -811,7 +814,8 @@ export function parseLC(data: RawNode) { return new PlaylistPanelContinuation(data.playlistPanelContinuation); if (data.continuationCommand) return new ContinuationCommand(data.continuationCommand); - + if (data.uploadFeedbackItemContinuation) + return new UploadFeedbackItem(data.uploadFeedbackItemContinuation); return null; } diff --git a/src/parser/types/ParsedResponse.ts b/src/parser/types/ParsedResponse.ts index b843e85135..48e46ae57b 100644 --- a/src/parser/types/ParsedResponse.ts +++ b/src/parser/types/ParsedResponse.ts @@ -6,7 +6,6 @@ import type { } from '../index.js'; import type Translation from '../classes/ytstudio/Translation.js'; import type CreatorVideo from '../classes/ytstudio/CreatorVideo.js'; -import type UploadFeedbackItem from '../classes/ytstudio/UploadFeedbackItem.js'; import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js'; import type CardCollection from '../classes/CardCollection.js'; @@ -24,6 +23,7 @@ import type EngagementPanelSectionList from '../classes/EngagementPanelSectionLi import type AppendContinuationItemsAction from '../classes/actions/AppendContinuationItemsAction.js'; import type MusicThumbnail from '../classes/MusicThumbnail.js'; import type OpenPopupAction from '../classes/actions/OpenPopupAction.js'; +import type { ContinuationContents } from '../parser.js'; export interface IParsedResponse { background?: MusicThumbnail; @@ -55,9 +55,10 @@ export interface IParsedResponse { on_response_received_commands?: ObservedArray; on_response_received_commands_memo?: Memo; continuation?: Continuation; - continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation | - MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation | ContinuationCommand; + continuation_contents?: ContinuationContents; continuation_contents_memo?: Memo; + continuation_contents_array?: ContinuationContents[]; + continuation_contents_array_memo?: Memo; metadata?: SuperParsedResult; microformat?: YTNode; overlay?: YTNode; @@ -96,8 +97,6 @@ export interface IParsedResponse { translation?: Translation; creator_video?: CreatorVideo; feedback_responses?: { isProcessed: boolean }[]; - upload_feedback_item?: UploadFeedbackItem; - upload_feedback_items?: UploadFeedbackItem[]; challenge_prompt_type?: 'CHALLENGE_PROMPT_TYPE_UNSPECIFIED' | 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE'; } @@ -179,5 +178,5 @@ export type ICreateCaptionsResponse = Pick; export type IUpdateCaptionsResponse = Pick; export type IMetadataUpdateResponse = Pick; -export type ICreateVideoResponse = Pick; -export type IUploadFeedbackResponse = Pick; +export type ICreateVideoResponse = Pick; +export type IUploadFeedbackResponse = Pick; From f7f41ffdf3efae318d2af5ac82934c9302ab2d31 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 22:26:30 -0700 Subject: [PATCH 25/27] refactor(YouTube Studio Web): make parser classes self-parsing and strip useless long info off enums --- src/parser/classes/ytstudio/CreatorVideo.ts | 546 +++++++++++------- src/parser/classes/ytstudio/Translation.ts | 61 +- .../classes/ytstudio/UploadFeedbackItem.ts | 15 +- .../classes/ytstudio/VideoUploadChecks.ts | 48 +- src/parser/parser.ts | 21 - 5 files changed, 421 insertions(+), 270 deletions(-) diff --git a/src/parser/classes/ytstudio/CreatorVideo.ts b/src/parser/classes/ytstudio/CreatorVideo.ts index b32f64c0ab..c5ef7c835e 100644 --- a/src/parser/classes/ytstudio/CreatorVideo.ts +++ b/src/parser/classes/ytstudio/CreatorVideo.ts @@ -1,222 +1,215 @@ import { YTNode } from '../../helpers.js'; import { type RawNode } from '../../index.js'; -import { parseAll, parseObject } from '../../parser.js'; import Thumbnail from '../misc/Thumbnail.js'; import VideoUploadChecks from './VideoUploadChecks.js'; -export type CreatorVideoStatus = - 'VIDEO_STATUS_DELETED' | 'VIDEO_STATUS_FAILED' | 'VIDEO_STATUS_PROCESSED' | - 'VIDEO_STATUS_REJECTED' | 'VIDEO_STATUS_UNKNOWN' | 'VIDEO_STATUS_UPLOADED' | 'VIDEO_STATUS_WRONG'; +export type CreatorVideoStatus = 'DELETED' | 'FAILED' | 'PROCESSED' | 'REJECTED' | 'UNKNOWN' | 'UPLOADED' | 'WRONG'; -export type DraftStatus = 'DRAFT_STATUS_NONE' | 'DRAFT_STATUS_PUBLIC' | 'DRAFT_STATUS_SCHEDULED'; +export type DraftStatus = 'NONE' | 'PUBLIC' | 'SCHEDULED'; -export type CreatorVideoPrivacy = - 'VIDEO_PRIVACY_PRIVATE' | 'VIDEO_PRIVACY_PUBLIC' | 'VIDEO_PRIVACY_SCHEDULED' | - 'VIDEO_PRIVACY_UNKNOWN' | 'VIDEO_PRIVACY_UNLISTED'; +export type CreatorVideoPrivacy = 'PRIVATE' | 'PUBLIC' | 'SCHEDULED' | 'UNKNOWN' | 'UNLISTED'; export type CreatorVideoCategory = - 'CREATOR_VIDEO_CATEGORY_AUTOS' | 'CREATOR_VIDEO_CATEGORY_COMEDY' | 'CREATOR_VIDEO_CATEGORY_EDUCATION' | - 'CREATOR_VIDEO_CATEGORY_ENTERTAINMENT' | 'CREATOR_VIDEO_CATEGORY_FILM' | 'CREATOR_VIDEO_CATEGORY_GADGETS' | - 'CREATOR_VIDEO_CATEGORY_GOVERNMENT' | 'CREATOR_VIDEO_CATEGORY_HOWTO' | 'CREATOR_VIDEO_CATEGORY_MUSIC' | - 'CREATOR_VIDEO_CATEGORY_NEWS' | 'CREATOR_VIDEO_CATEGORY_PEOPLE' | 'CREATOR_VIDEO_CATEGORY_PETS' | - 'CREATOR_VIDEO_CATEGORY_SCIENCE' | 'CREATOR_VIDEO_CATEGORY_SPORTS' | 'CREATOR_VIDEO_CATEGORY_TRAVEL' | - 'CREATOR_VIDEO_CATEGORY_UNKNOWN'; + 'AUTOS' | 'COMEDY' | 'EDUCATION' | + 'ENTERTAINMENT' | 'FILM' | 'GADGETS' | + 'GOVERNMENT' | 'HOWTO' | 'MUSIC' | + 'NEWS' | 'PEOPLE' | 'PETS' | + 'SCIENCE' | 'SPORTS' | 'TRAVEL' | + 'UNKNOWN'; export type VideoCommentFilter = - 'VIDEO_COMMENT_FILTER_APPROVE' | 'VIDEO_COMMENT_FILTER_AUTO_MODERATE' | - 'VIDEO_COMMENT_FILTER_AUTO_MODERATE_HOLD_MORE' | 'VIDEO_COMMENT_FILTER_NONE' | 'VIDEO_COMMENT_FILTER_UNKNOWN'; + 'APPROVE' | 'AUTO_MODERATE' | + 'AUTO_MODERATE_HOLD_MORE' | 'NONE' | 'UNKNOWN'; -export type VideoCommentSortOrder = 'VIDEO_COMMENT_SORT_ORDER_LATEST' | 'VIDEO_COMMENT_SORT_ORDER_TOP'; +export type VideoCommentSortOrder = 'LATEST' | 'TOP'; -export type VideoAgeRestriction = - 'VIDEO_AGE_RESTRICTION_APPEAL_BUTTON' | 'VIDEO_AGE_RESTRICTION_NONE' | - 'VIDEO_AGE_RESTRICTION_SELF' | 'VIDEO_AGE_RESTRICTION_SYSTEM'; +export type VideoAgeRestriction = 'APPEAL_BUTTON' | 'NONE' | 'SELF' | 'SYSTEM'; -export type VideoLicense = 'VIDEO_LICENSE_CREATIVE_COMMONS' | 'VIDEO_LICENSE_STANDARD' | 'VIDEO_LICENSE_UNKNOWN'; +export type VideoLicense = 'CREATIVE_COMMONS' | 'STANDARD' | 'UNKNOWN'; export type VideoUncaptionedReason = - 'VIDEO_UNCAPTIONED_REASON_CAPTIONLESS_TV_CONTENT' | 'VIDEO_UNCAPTIONED_REASON_EXCEPTION_GRANTED' | - 'VIDEO_UNCAPTIONED_REASON_LEGACY' | 'VIDEO_UNCAPTIONED_REASON_NO_FULL_LENGTH_VIDEO' | - 'VIDEO_UNCAPTIONED_REASON_NO_US_TV_CONTENT' | 'VIDEO_UNCAPTIONED_REASON_NOT_REQUIRED' | - 'VIDEO_UNCAPTIONED_REASON_UNNECESSARY_OR_NOT_SET'; + 'CAPTIONLESS_TV_CONTENT' | 'EXCEPTION_GRANTED' | + 'LEGACY' | 'NO_FULL_LENGTH_VIDEO' | + 'NO_US_TV_CONTENT' | 'NOT_REQUIRED' | + 'UNNECESSARY_OR_NOT_SET'; export type VideoSubscriberNotification = - 'VIDEO_SUBSCRIBER_NOTIFICATION_DISABLED' | 'VIDEO_SUBSCRIBER_NOTIFICATION_ENABLED' | - 'VIDEO_SUBSCRIBER_NOTIFICATION_NONE' | 'VIDEO_SUBSCRIBER_NOTIFICATION_UNKNOWN'; + 'DISABLED' | 'ENABLED' | 'NONE' | 'UNKNOWN'; export type VideoPaidProductPlacement = - 'VIDEO_PAID_PRODUCT_PLACEMENT_NO' | 'VIDEO_PAID_PRODUCT_PLACEMENT_NOTIFY' | - 'VIDEO_PAID_PRODUCT_PLACEMENT_UNKNOWN' | 'VIDEO_PAID_PRODUCT_PLACEMENT_UNSET' | 'VIDEO_PAID_PRODUCT_PLACEMENT_YES'; + 'NO' | 'NOTIFY' | 'UNKNOWN' | 'UNSET' | 'YES'; export type VideoMusicLicensedStatus = - 'VIDEO_MUSIC_LICENSED_STATUS_CLAIMED_BY_CHANNEL_OWNER' | 'VIDEO_MUSIC_LICENSED_STATUS_DISABLED_BY_CLAIM_PREFERENCE' | - 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_CHANNEL_OWNER' | 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_CHANNEL_OWNER_UNCLAIMED' | - 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_PARTNER_UPLOADED_CLAIM' | 'VIDEO_MUSIC_LICENSED_STATUS_ENABLED_BY_THIRD_PARTY_CLAIMS' | - 'VIDEO_MUSIC_LICENSED_STATUS_UNCLAIMED'; + 'CLAIMED_BY_CHANNEL_OWNER' | 'DISABLED_BY_CLAIM_PREFERENCE' | + 'ENABLED_BY_CHANNEL_OWNER' | 'ENABLED_BY_CHANNEL_OWNER_UNCLAIMED' | + 'ENABLED_BY_PARTNER_UPLOADED_CLAIM' | 'ENABLED_BY_THIRD_PARTY_CLAIMS' | + 'UNCLAIMED'; -export type VideoUserSetMonetization = 'VIDEO_USER_SET_MONETIZATION_OFF' | 'VIDEO_USER_SET_MONETIZATION_ON'; +export type VideoUserSetMonetization = 'OFF' | 'ON'; export type VideoMonetizingStatus = - 'VIDEO_MONETIZING_STATUS_INDETERMINATE' | 'VIDEO_MONETIZING_STATUS_MONETIZING' | - 'VIDEO_MONETIZING_STATUS_MONETIZING_CREATOR_REVSHARE' | 'VIDEO_MONETIZING_STATUS_MONETIZING_IN_ESCROW' | - 'VIDEO_MONETIZING_STATUS_MONETIZING_WITH_EXCEPTIONS' | 'VIDEO_MONETIZING_STATUS_MONETIZING_WITH_LIMITED_ADS' | - 'VIDEO_MONETIZING_STATUS_MONETIZING_WITH_REVSHARE' | 'VIDEO_MONETIZING_STATUS_NOT_FOR_DISPLAY' | - 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_CHANNEL_NOT_MONETIZING' | 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_INELIGIBLE' | - 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_INELIGIBLE_INNOCUOUS' | 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF' | - 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF_CREATOR_REVSHARE' | 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF_SHORTS_REVSHARE' | - 'VIDEO_MONETIZING_STATUS_NOT_MONETIZING_OFF_WITH_REVSHARE' | 'VIDEO_MONETIZING_STATUS_PENDING_CHECKS' | - 'VIDEO_MONETIZING_STATUS_UNSPECIFIED' | 'VIDEO_MONETIZING_STATUS_VIDEO_NOT_FINAL'; + 'INDETERMINATE' | 'MONETIZING' | + 'MONETIZING_CREATOR_REVSHARE' | 'MONETIZING_IN_ESCROW' | + 'MONETIZING_WITH_EXCEPTIONS' | 'MONETIZING_WITH_LIMITED_ADS' | + 'MONETIZING_WITH_REVSHARE' | 'NOT_FOR_DISPLAY' | + 'NOT_MONETIZING_CHANNEL_NOT_MONETIZING' | 'NOT_MONETIZING_INELIGIBLE' | + 'NOT_MONETIZING_INELIGIBLE_INNOCUOUS' | 'NOT_MONETIZING_OFF' | + 'NOT_MONETIZING_OFF_CREATOR_REVSHARE' | 'NOT_MONETIZING_OFF_SHORTS_REVSHARE' | + 'NOT_MONETIZING_OFF_WITH_REVSHARE' | 'PENDING_CHECKS' | + 'UNSPECIFIED' | 'VIDEO_NOT_FINAL'; export type VideoVisibilityStatus = - 'VIDEO_VISIBILITY_STATUS_AGE_RESTRICTED' | 'VIDEO_VISIBILITY_STATUS_BLOCKED_FOR_COPYRIGHT_GLOBALLY' | - 'VIDEO_VISIBILITY_STATUS_BLOCKED_FOR_COPYRIGHT_PARTIALLY' | 'VIDEO_VISIBILITY_STATUS_DRAFT' | - 'VIDEO_VISIBILITY_STATUS_FORCED_PRIVATE' | 'VIDEO_VISIBILITY_STATUS_INDETERMINATE' | - 'VIDEO_VISIBILITY_STATUS_LIMITED_FEATURES' | 'VIDEO_VISIBILITY_STATUS_PENDING_REMOVAL_FOR_COPYRIGHT' | - 'VIDEO_VISIBILITY_STATUS_REMOVED_FOR_COMMUNITY_GUIDELINES' | 'VIDEO_VISIBILITY_STATUS_REMOVED_FOR_COPYRIGHT' | - 'VIDEO_VISIBILITY_STATUS_TARGETED_FOR_KIDS' | 'VIDEO_VISIBILITY_STATUS_UNKNOWN' | - 'VIDEO_VISIBILITY_STATUS_UPLOAD_FAILED' | 'VIDEO_VISIBILITY_STATUS_UPLOADING_OR_PROCESSING' | - 'VIDEO_VISIBILITY_STATUS_USER_CONFIG'; + 'AGE_RESTRICTED' | 'BLOCKED_FOR_COPYRIGHT_GLOBALLY' | + 'BLOCKED_FOR_COPYRIGHT_PARTIALLY' | 'DRAFT' | + 'FORCED_PRIVATE' | 'INDETERMINATE' | + 'LIMITED_FEATURES' | 'PENDING_REMOVAL_FOR_COPYRIGHT' | + 'REMOVED_FOR_COMMUNITY_GUIDELINES' | 'REMOVED_FOR_COPYRIGHT' | + 'TARGETED_FOR_KIDS' | 'UNKNOWN' | + 'UPLOAD_FAILED' | 'UPLOADING_OR_PROCESSING' | + 'USER_CONFIG'; export type VideoUserSetVisibility = - 'VIDEO_USER_SET_VISIBILITY_DRAFT' | 'VIDEO_USER_SET_VISIBILITY_FUTURE_PREMIERE' | 'VIDEO_USER_SET_VISIBILITY_MEMBERS' | - 'VIDEO_USER_SET_VISIBILITY_PREMIERING' | 'VIDEO_USER_SET_VISIBILITY_PRIMETIME_SUBSCRIBERS' | 'VIDEO_USER_SET_VISIBILITY_PRIVATE' | - 'VIDEO_USER_SET_VISIBILITY_PUBLIC' | 'VIDEO_USER_SET_VISIBILITY_SCHEDULED' | - 'VIDEO_USER_SET_VISIBILITY_SCHEDULED_TO_PRIMETIME_SUBSCRIBERS' | 'VIDEO_USER_SET_VISIBILITY_SPECIFIC_PEOPLE' | - 'VIDEO_USER_SET_VISIBILITY_SUPERFANS' | 'VIDEO_USER_SET_VISIBILITY_UNLISTED' | 'VIDEO_USER_SET_VISIBILITY_UNSPECIFIED'; + 'DRAFT' | 'FUTURE_PREMIERE' | 'MEMBERS' | + 'PREMIERING' | 'PRIMETIME_SUBSCRIBERS' | 'PRIVATE' | + 'PUBLIC' | 'SCHEDULED' | + 'SCHEDULED_TO_PRIMETIME_SUBSCRIBERS' | 'SPECIFIC_PEOPLE' | + 'SUPERFANS' | 'UNLISTED' | 'UNSPECIFIED'; -export type VideoUserInflictedVisibility = 'VIDEO_USER_INFLICTED_VISIBILITY_AGE_RESTRICTED' | 'VIDEO_USER_INFLICTED_VISIBILITY_UNSPECIFIED'; +export type VideoUserInflictedVisibility = 'AGE_RESTRICTED' | 'UNSPECIFIED'; export type VideoEffectiveVisibility = - 'VIDEO_EFFECTIVE_VISIBILITY_BLOCKED' | 'VIDEO_EFFECTIVE_VISIBILITY_FORCED_PRIVATE' | - 'VIDEO_EFFECTIVE_VISIBILITY_REMOVED' | 'VIDEO_EFFECTIVE_VISIBILITY_UNSPECIFIED' | - 'VIDEO_EFFECTIVE_VISIBILITY_UPLOAD_FAILED' | 'VIDEO_EFFECTIVE_VISIBILITY_UPLOADING_OR_PROCESSING' | - 'VIDEO_EFFECTIVE_VISIBILITY_USER_CONFIG'; + 'BLOCKED' | 'FORCED_PRIVATE' | + 'REMOVED' | 'UNSPECIFIED' | + 'UPLOAD_FAILED' | 'UPLOADING_OR_PROCESSING' | + 'USER_CONFIG'; -export type VideoOrigin = 'VIDEO_ORIGIN_LIVESTREAM' | 'VIDEO_ORIGIN_STORY' | 'VIDEO_ORIGIN_UNKNOWN' | 'VIDEO_ORIGIN_UPLOAD'; +export type VideoOrigin = 'LIVESTREAM' | 'STORY' | 'UNKNOWN' | 'UPLOAD'; export type VideoProcessingStatus = - 'VIDEO_PROCESSING_STATUS_EDITED' | 'VIDEO_PROCESSING_STATUS_FAILED' | 'VIDEO_PROCESSING_STATUS_PROCESSING' | - 'VIDEO_PROCESSING_STATUS_PROCESSING_NON_PRIMARY_ASSETS' | 'VIDEO_PROCESSING_STATUS_READY' | - 'VIDEO_PROCESSING_STATUS_REVERTED' | 'VIDEO_PROCESSING_STATUS_UNEDITED' | 'VIDEO_PROCESSING_STATUS_UNKNOWN'; + 'EDITED' | 'FAILED' | 'PROCESSING' | + 'PROCESSING_NON_PRIMARY_ASSETS' | 'READY' | + 'REVERTED' | 'UNEDITED' | 'UNKNOWN'; export type VideoTargetedAudience = - 'VIDEO_TARGETED_AUDIENCE_ALL' | 'VIDEO_TARGETED_AUDIENCE_AGE_RESTRICTED' | - 'VIDEO_TARGETED_AUDIENCE_CROSSWALK' | 'VIDEO_TARGETED_AUDIENCE_UNKNOWN'; + 'ALL' | 'AGE_RESTRICTED' | + 'CROSSWALK' | 'UNKNOWN'; export type VideoTargetedAudienceImposer = - 'VIDEO_TARGETED_AUDIENCE_IMPOSER_SELF' | 'VIDEO_TARGETED_AUDIENCE_IMPOSER_SYSTEM' | 'VIDEO_TARGETED_AUDIENCE_IMPOSER_UNSPECIFIED'; + 'SELF' | 'SYSTEM' | 'UNSPECIFIED'; export type ResolutionStatus = - 'RESOLUTION_STATUS_APPEAL_IN_PROGRESS' | 'RESOLUTION_STATUS_APPEAL_REJECTED' | 'RESOLUTION_STATUS_AVAILABLE' | - 'RESOLUTION_STATUS_DEFERRED' | 'RESOLUTION_STATUS_DONE' | 'RESOLUTION_STATUS_PROCESSING' | - 'RESOLUTION_STATUS_STARTING_SOON' | 'RESOLUTION_STATUS_UNAVAILABLE' | 'RESOLUTION_STATUS_UNKNOWN' | - 'RESOLUTION_STATUS_UNSPECIFIED'; + 'APPEAL_IN_PROGRESS' | 'APPEAL_REJECTED' | 'AVAILABLE' | + 'DEFERRED' | 'DONE' | 'PROCESSING' | + 'STARTING_SOON' | 'UNAVAILABLE' | 'UNKNOWN' | + 'UNSPECIFIED'; -export type MonetizedStatus = 'MONETIZED_STATUS_ACTIVE' | 'MONETIZED_STATUS_INACTIVE' | 'MONETIZED_STATUS_UNSPECIFIED'; +export type MonetizedStatus = 'ACTIVE' | 'INACTIVE' | 'UNSPECIFIED'; export type RestrictionsSeverity = - 'VIDEO_RESTRICTIONS_SEVERITY_HIGH' | 'VIDEO_RESTRICTIONS_SEVERITY_LOW' | - 'VIDEO_RESTRICTIONS_SEVERITY_MEDIUM' | 'VIDEO_RESTRICTIONS_SEVERITY_UNSPECIFIED'; + 'HIGH' | 'LOW' | + 'MEDIUM' | 'UNSPECIFIED'; -export type AutoGenMidrollsStatus = 'AUTO_GEN_MIDROLLS_STATUS_AVAILABLE' | 'AUTO_GEN_MIDROLLS_STATUS_FAILED' | 'AUTO_GEN_MIDROLLS_STATUS_PROCESSING'; +export type AutoGenMidrollsStatus = 'AVAILABLE' | 'FAILED' | 'PROCESSING'; export type VideoCopyrightSummaryStatus = - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_BLOCKED' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_COPYRIGHT_CONTENT_FOUND' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_COUNTER_REJECTED' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_DELAYED_TAKEDOWN' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_EXPIRED_STRIKE_TAKEDOWN' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LICENSED' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LIKENESS_CLAIM' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LIKENESS_PENDING_REMOVAL' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_LIKENESS_REMOVAL' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZABLE_WITH_LICENSES' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_CREATOR_REVSHARE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_CREATOR_REVSHARE_ELIGIBLE' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_ENABLED_WITH_LICENSES' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_RESTRICTED' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_REVSHARE_ELIGIBLE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_REVSHARE_ENABLED' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_SHORTS_REVSHARE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_SHORTS_REVSHARE_ELIGIBLE' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MONETIZATION_UNAVAILABLE' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MULTIPLE_CLAIMS' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_MULTIPLE_REMOVALS' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_NO_CLAIMS_FOUND' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_PARTIALLY_BLOCKED_REVSHARE_ENABLED' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_SHORTS_NO_UPLOADER_CLAIM' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_STRIKE_TAKEDOWN' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN_COUNTER' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN_NO_STRIKE' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_TAKEDOWN_UNDER_REVIEW' | 'VIDEO_COPYRIGHT_SUMMARY_STATUS_VIDEO_APPEAL' | - 'VIDEO_COPYRIGHT_SUMMARY_STATUS_VIDEO_DISPUTE'; + 'BLOCKED' | 'COPYRIGHT_CONTENT_FOUND' | + 'COUNTER_REJECTED' | 'DELAYED_TAKEDOWN' | + 'EXPIRED_STRIKE_TAKEDOWN' | 'LICENSED' | + 'LIKENESS_CLAIM' | 'LIKENESS_PENDING_REMOVAL' | + 'LIKENESS_REMOVAL' | 'MONETIZABLE_WITH_LICENSES' | + 'MONETIZATION_CREATOR_REVSHARE' | 'MONETIZATION_CREATOR_REVSHARE_ELIGIBLE' | + 'MONETIZATION_ENABLED_WITH_LICENSES' | 'MONETIZATION_RESTRICTED' | + 'MONETIZATION_REVSHARE_ELIGIBLE' | 'MONETIZATION_REVSHARE_ENABLED' | + 'MONETIZATION_SHORTS_REVSHARE' | 'MONETIZATION_SHORTS_REVSHARE_ELIGIBLE' | + 'MONETIZATION_UNAVAILABLE' | 'MULTIPLE_CLAIMS' | + 'MULTIPLE_REMOVALS' | 'NO_CLAIMS_FOUND' | + 'PARTIALLY_BLOCKED_REVSHARE_ENABLED' | 'SHORTS_NO_UPLOADER_CLAIM' | + 'STRIKE_TAKEDOWN' | 'TAKEDOWN' | + 'TAKEDOWN_COUNTER' | 'TAKEDOWN_NO_STRIKE' | + 'TAKEDOWN_UNDER_REVIEW' | 'VIDEO_APPEAL' | + 'VIDEO_DISPUTE'; export type VideoCopyrightChannelImpact = - 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_NO_CHANNEL_IMPACT' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_NO_CHANNEL_IMPACT_WITH_LICENSES' | - 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_NO_CHANNEL_IMPACT_WITHOUT_CLAIMS' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_PENDING_STRIKE_REVIEW' | - 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_COUNTER' | - 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_EXPIRED' | 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_PENDING' | - 'VIDEO_COPYRIGHT_CHANNEL_IMPACT_STRIKE_RELEASED_DURING_COUNTER'; + 'NO_CHANNEL_IMPACT' | 'NO_CHANNEL_IMPACT_WITH_LICENSES' | + 'NO_CHANNEL_IMPACT_WITHOUT_CLAIMS' | 'PENDING_STRIKE_REVIEW' | + 'STRIKE' | 'STRIKE_COUNTER' | + 'STRIKE_EXPIRED' | 'STRIKE_PENDING' | + 'STRIKE_RELEASED_DURING_COUNTER'; export type VideoCopyrightVisibilityImpact = - 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_APPEAL' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_CLAIM_BLOCK' | - 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_CLAIM_PARTIAL_BLOCK' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_COMMERCIAL_SHORTS_BLOCK' | - 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_DELAYED_TAKEDOWN' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_DISPUTE' | - 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_LICENSE_RESTRICTED_SHORTS_BLOCK' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_LIKENESS_BLOCK' | - 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_MULTIPLE_CLAIMS_BLOCK' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_NOT_AFFECTED' | - 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_PENDING_LIKENESS_REMOVAL' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_TAKEDOWN' | - 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_TAKEDOWN_COUNTER' | 'VIDEO_COPYRIGHT_VISIBILITY_IMPACT_UNKNOWN'; + 'APPEAL' | 'CLAIM_BLOCK' | + 'CLAIM_PARTIAL_BLOCK' | 'COMMERCIAL_SHORTS_BLOCK' | + 'DELAYED_TAKEDOWN' | 'DISPUTE' | + 'LICENSE_RESTRICTED_SHORTS_BLOCK' | 'LIKENESS_BLOCK' | + 'MULTIPLE_CLAIMS_BLOCK' | 'NOT_AFFECTED' | + 'PENDING_LIKENESS_REMOVAL' | 'TAKEDOWN' | + 'TAKEDOWN_COUNTER' | 'UNKNOWN'; export type VideoCopyrightMonetizationImpact = - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CLAIM_BLOCK' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CLAIM_PARTIAL_BLOCK' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CLAIM_PARTIAL_BLOCK_MONETIZED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CREATOR_REVSHARE' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_CREATOR_REVSHARE_ELIGIBLE' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_DO_NOT_DISPLAY' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_LIKENESS_REVSHARE_ELIGIBLE' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_LIKENESS_REVSHARE_ENABLED' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_MONETIZABLE_WITH_LICENSES' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_MONETIZED_DURING_DISPUTE' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_NOT_AFFECTED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_NOT_AFFECTED_LICENSED' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_RESTRICTED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_REVSHARE_ELIGIBLE' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_REVSHARE_ENABLED' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_SHORTS_NO_UPLOADER_CLAIM' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_SHUNA_CLAIM_DEMONETIZATION' | 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_TAKEDOWN' | - 'VIDEO_COPYRIGHT_MONETIZATION_IMPACT_UNKNOWN'; + 'CLAIM_BLOCK' | 'CLAIM_PARTIAL_BLOCK' | + 'CLAIM_PARTIAL_BLOCK_MONETIZED' | 'CREATOR_REVSHARE' | + 'CREATOR_REVSHARE_ELIGIBLE' | 'DO_NOT_DISPLAY' | + 'LIKENESS_REVSHARE_ELIGIBLE' | 'LIKENESS_REVSHARE_ENABLED' | + 'MONETIZABLE_WITH_LICENSES' | 'MONETIZED_DURING_DISPUTE' | + 'NOT_AFFECTED' | 'NOT_AFFECTED_LICENSED' | + 'RESTRICTED' | 'REVSHARE_ELIGIBLE' | + 'REVSHARE_ENABLED' | 'SHORTS_NO_UPLOADER_CLAIM' | + 'SHUNA_CLAIM_DEMONETIZATION' | 'TAKEDOWN' | + 'UNKNOWN'; export type TouPolicyVertical = - 'TOU_POLICY_VERTICAL_CHILD_SAFETY' | 'TOU_POLICY_VERTICAL_HARMFUL_DANGEROUS' | 'TOU_POLICY_VERTICAL_SUICIDE_SELF_HARM' | - 'TOU_POLICY_VERTICAL_UNKNOWN' | 'TOU_POLICY_VERTICAL_VIOLENT_GRAPHIC_SHOCKING'; + 'CHILD_SAFETY' | 'HARMFUL_DANGEROUS' | 'SUICIDE_SELF_HARM' | + 'UNKNOWN' | 'VIOLENT_GRAPHIC_SHOCKING'; -export type HumanReviewState = 'HUMAN_REVIEW_STATE_DONE' | 'HUMAN_REVIEW_STATE_INELIGIBLE' | 'HUMAN_REVIEW_STATE_NOT_REQUESTED'; +export type HumanReviewState = 'DONE' | 'INELIGIBLE' | 'NOT_REQUESTED'; -export type VideoMadeForKids = 'VIDEO_MADE_FOR_KIDS_MFK' | 'VIDEO_MADE_FOR_KIDS_NOT_MFK' | 'VIDEO_MADE_FOR_KIDS_UNKNOWN'; +export type VideoMadeForKids = 'MFK' | 'NOT_MFK' | 'UNKNOWN'; export type VideoMadeForKidsImposer = - 'VIDEO_MADE_FOR_KIDS_IMPOSER_SELF' | 'VIDEO_MADE_FOR_KIDS_IMPOSER_UNSPECIFIED' | 'VIDEO_MADE_FOR_KIDS_IMPOSER_YOUTUBE'; + 'SELF' | 'UNSPECIFIED' | 'YOUTUBE'; export type RemixSourceOptionEligibility = - 'REMIX_SOURCE_OPTION_ELIGIBILITY_BY_CLIENT' | 'REMIX_SOURCE_OPTION_ELIGIBILITY_ELIGIBLE' | 'REMIX_SOURCE_OPTION_ELIGIBILITY_INELIGIBLE'; + 'BY_CLIENT' | 'ELIGIBLE' | 'INELIGIBLE'; -export type RemixSourceShorts = 'REMIX_SOURCE_SHORTS_IS_SHORT' | 'REMIX_SOURCE_SHORTS_NOT_SHORT' | 'REMIX_SOURCE_SHORTS_PROCESSING'; +export type RemixSourceShorts = 'IS_SHORT' | 'NOT_SHORT' | 'PROCESSING'; export type VideoCommentsEnabledState = - 'VIDEO_COMMENTS_ENABLED_STATE_OFF' | 'VIDEO_COMMENTS_ENABLED_STATE_ON' | - 'VIDEO_COMMENTS_ENABLED_STATE_PAUSED' | 'VIDEO_COMMENTS_ENABLED_STATE_UNKNOWN'; + 'OFF' | 'ON' | + 'PAUSED' | 'UNKNOWN'; export type AllowedCommenterMode = - 'ALLOWED_COMMENTER_MODE_ANYONE' | 'ALLOWED_COMMENTER_MODE_SUBSCRIBERS_MEMBERS_APPROVED_USERS' | 'ALLOWED_COMMENTER_MODE_UNKNOWN'; + 'ANYONE' | 'SUBSCRIBERS_MEMBERS_APPROVED_USERS' | 'UNKNOWN'; export type CommenterMinimumSubscriptionTime = - 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ANY' | 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ONE_DAY' | - 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ONE_HOUR' | 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_ONE_WEEK' | - 'COMMENTER_MINIMUM_SUBSCRIPTION_TIME_UNKNOWN'; + 'ANY' | 'ONE_DAY' | + 'ONE_HOUR' | 'ONE_WEEK' | + 'UNKNOWN'; export type CreatorContentType = - 'CREATOR_CONTENT_TYPE_LIVE_STREAM' | 'CREATOR_CONTENT_TYPE_SHORTS' | - 'CREATOR_CONTENT_TYPE_UNSPECIFIED' | 'CREATOR_CONTENT_TYPE_VIDEO_ON_DEMAND'; + 'LIVE_STREAM' | 'SHORTS' | + 'UNSPECIFIED' | 'VIDEO_ON_DEMAND'; export type CreatorVideoPermission = - 'CREATOR_VIDEO_PERMISSION_ANALYTICS_READ' | 'CREATOR_VIDEO_PERMISSION_BASIC_METADATA_READ' | - 'CREATOR_VIDEO_PERMISSION_CAPTIONS_READ' | 'CREATOR_VIDEO_PERMISSION_CAPTIONS_WRITE' | - 'CREATOR_VIDEO_PERMISSION_COLLABORATOR' | 'CREATOR_VIDEO_PERMISSION_COLLABORATOR_INVITEE' | - 'CREATOR_VIDEO_PERMISSION_COLLABORATOR_LIMITED' | 'CREATOR_VIDEO_PERMISSION_COLLABORATOR_LIMITED_INVITEE' | - 'CREATOR_VIDEO_PERMISSION_COMMENTS_MANAGER' | 'CREATOR_VIDEO_PERMISSION_COMMENTS_READ' | - 'CREATOR_VIDEO_PERMISSION_COMMENT_SETTINGS_READ' | 'CREATOR_VIDEO_PERMISSION_COMMENT_SETTINGS_WRITE' | - 'CREATOR_VIDEO_PERMISSION_DELETE' | 'CREATOR_VIDEO_PERMISSION_DOWNLOAD' | - 'CREATOR_VIDEO_PERMISSION_ENFORCEMENT_APPELLANT' | 'CREATOR_VIDEO_PERMISSION_ENFORCEMENT_READER' | - 'CREATOR_VIDEO_PERMISSION_MONETIZATION_SETTINGS_READ' | 'CREATOR_VIDEO_PERMISSION_MONETIZATION_WRITE' | - 'CREATOR_VIDEO_PERMISSION_PRIVACY_STATUS_PRIVATE_WRITE' | 'CREATOR_VIDEO_PERMISSION_PRIVACY_STATUS_PUBLIC_WRITE' | - 'CREATOR_VIDEO_PERMISSION_RATING_SETTINGS_WRITE' | 'CREATOR_VIDEO_PERMISSION_READ' | - 'CREATOR_VIDEO_PERMISSION_WATCH' | 'CREATOR_VIDEO_PERMISSION_WRITE'; - -export type CreatorEntityStatus = 'CREATOR_ENTITY_STATUS_FAILURE' | 'CREATOR_ENTITY_STATUS_OK' | 'CREATOR_ENTITY_STATUS_PARTIAL_FAILURE'; + 'ANALYTICS_READ' | 'BASIC_METADATA_READ' | + 'CAPTIONS_READ' | 'CAPTIONS_WRITE' | + 'COLLABORATOR' | 'COLLABORATOR_INVITEE' | + 'COLLABORATOR_LIMITED' | 'COLLABORATOR_LIMITED_INVITEE' | + 'COMMENTS_MANAGER' | 'COMMENTS_READ' | + 'COMMENT_SETTINGS_READ' | 'COMMENT_SETTINGS_WRITE' | + 'DELETE' | 'DOWNLOAD' | + 'ENFORCEMENT_APPELLANT' | 'ENFORCEMENT_READER' | + 'MONETIZATION_SETTINGS_READ' | 'MONETIZATION_WRITE' | + 'PRIVACY_STATUS_PRIVATE_WRITE' | 'PRIVACY_STATUS_PUBLIC_WRITE' | + 'RATING_SETTINGS_WRITE' | 'READ' | + 'WATCH' | 'WRITE'; + +export type CreatorEntityStatus = 'FAILURE' | 'OK' | 'PARTIAL_FAILURE'; export type CreatorFeatureStatus = - 'CREATOR_FEATURE_STATUS_DISABLED' | 'CREATOR_FEATURE_STATUS_ELIGIBLE' | - 'CREATOR_FEATURE_STATUS_ENABLED' | 'CREATOR_FEATURE_STATUS_UNKNOWN'; + 'DISABLED' | 'ELIGIBLE' | + 'ENABLED' | 'UNKNOWN'; + +export type CreatorFeatureStatusDetails = 'NOT_APPLICABLE'; export interface ClaimDetails { is_embed_disabled?: boolean; @@ -256,7 +249,7 @@ export interface AudioLanguage { export interface FeatureState { status?: CreatorFeatureStatus; - status_details?: 'CREATOR_FEATURE_STATUS_DETAILS_NOT_APPLICABLE'; + status_details?: CreatorFeatureStatusDetails; } export interface Publishing { @@ -418,12 +411,6 @@ export interface SuperfansOnly { is_superfans_only?: boolean; } -function toInt(value: unknown): number | undefined { - if (typeof value === 'number') return value; - if (typeof value !== 'string' || value === '' || isNaN(Number(value))) return undefined; - return parseInt(value, 10); -} - export default class CreatorVideo extends YTNode { static type = 'CreatorVideo'; @@ -516,15 +503,15 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'privacy')) { - this.privacy = data.privacy; + this.privacy = data.privacy.replace('VIDEO_PRIVACY_', ''); } if (Reflect.has(data, 'status')) { - this.status = data.status; + this.status = data.status.replace('VIDEO_STATUS_', ''); } if (Reflect.has(data, 'draftStatus')) { - this.draft_status = data.draftStatus; + this.draft_status = data.draftStatus.replace('DRAFT_STATUS_', ''); } if (Reflect.has(data, 'shareUrl')) { @@ -536,19 +523,19 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'lengthSeconds')) { - this.length_seconds = toInt(data.lengthSeconds); + this.length_seconds = Number(data.lengthSeconds); } if (Reflect.has(data, 'videoDurationMs')) { - this.video_duration_ms = toInt(data.videoDurationMs); + this.video_duration_ms = Number(data.videoDurationMs); } if (Reflect.has(data, 'timeCreatedSeconds')) { - this.time_created_seconds = toInt(data.timeCreatedSeconds); + this.time_created_seconds = Number(data.timeCreatedSeconds); } if (Reflect.has(data, 'timePublishedSeconds')) { - this.time_published_seconds = toInt(data.timePublishedSeconds); + this.time_published_seconds = Number(data.timePublishedSeconds); } if (Reflect.has(data, 'thumbnailDetails')) { @@ -556,15 +543,27 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'claimDetails')) { - this.claim_details = parseObject(data.claimDetails); + const claim_details = data.claimDetails; + this.claim_details = { + is_embed_disabled: claim_details.isEmbedDisabled, + video_has_commercial_block: claim_details.videoHasCommercialBlock, + video_has_third_party_claim: claim_details.videoHasThirdPartyClaim + }; } if (Reflect.has(data, 'permissions')) { - this.permissions = parseObject(data.permissions); + const permissions = data.permissions; + this.permissions = { + overall_permissions: Array.isArray(permissions.overallPermissions) ? + permissions.overallPermissions.map((permission: string) => permission.replace('CREATOR_VIDEO_PERMISSION_', '')) : undefined + }; } if (Reflect.has(data, 'responseStatus')) { - this.response_status = parseObject(data.responseStatus); + const response_status = data.responseStatus; + this.response_status = { + status_code: Reflect.has(response_status, 'statusCode') ? response_status.statusCode.replace('CREATOR_ENTITY_STATUS_', '') : undefined + }; } if (Reflect.has(data, 'thumbnailEditorState')) { @@ -577,31 +576,41 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'videoEditorProject')) { - this.video_editor_project = parseObject(data.videoEditorProject); + const video_dimensions = data.videoEditorProject.videoDimensions; + this.video_editor_project = { + video_dimensions: video_dimensions ? { + width: video_dimensions.width, + height: video_dimensions.height + } : undefined + }; } if (Reflect.has(data, 'statusDetails')) { - this.status_details = parseObject(data.statusDetails); + this.status_details = { + feedback_service_continuation_token: data.statusDetails.feedbackServiceContinuationToken + }; } if (Reflect.has(data, 'tags')) { - this.tags = parseAll(data.tags); + this.tags = Array.isArray(data.tags) ? data.tags.map((tag: RawNode) => ({ value: tag.value })) : undefined; } if (Reflect.has(data, 'category')) { - this.category = data.category; + this.category = data.category.replace('CREATOR_VIDEO_CATEGORY_', ''); } if (Reflect.has(data, 'commentFilter')) { - this.comment_filter = data.commentFilter; + this.comment_filter = data.commentFilter.replace('VIDEO_COMMENT_FILTER_', ''); } if (Reflect.has(data, 'defaultCommentSortOrder')) { - this.default_comment_sort_order = data.defaultCommentSortOrder; + this.default_comment_sort_order = data.defaultCommentSortOrder.replace('VIDEO_COMMENT_SORT_ORDER_', ''); } if (Reflect.has(data, 'audioLanguage')) { - this.audio_language = parseObject(data.audioLanguage); + this.audio_language = { + language_code: data.audioLanguage.languageCode + }; } if (Reflect.has(data, 'allowRatings')) { @@ -609,27 +618,36 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'ageRestriction')) { - this.age_restriction = data.ageRestriction; + this.age_restriction = data.ageRestriction.replace('VIDEO_AGE_RESTRICTION_', ''); } if (Reflect.has(data, 'license')) { - this.license = data.license; + this.license = data.license.replace('VIDEO_LICENSE_', ''); } if (Reflect.has(data, 'features')) { - this.features = parseObject>(data.features); + const features: Record = {}; + for (const [ key, value ] of Object.entries(data.features)) { + features[key] = { + status: Reflect.has(value, 'status') ? value.status.replace('CREATOR_FEATURE_STATUS_', '') : undefined, + status_details: Reflect.has(value, 'statusDetails') ? value.statusDetails.replace('CREATOR_FEATURE_STATUS_DETAILS_', '') : undefined + }; + } + this.features = features; } if (Reflect.has(data, 'uncaptionedReason')) { - this.uncaptioned_reason = data.uncaptionedReason; + this.uncaptioned_reason = data.uncaptionedReason.replace('VIDEO_UNCAPTIONED_REASON_', ''); } if (Reflect.has(data, 'publishing')) { - this.publishing = parseObject(data.publishing); + this.publishing = { + notify_subscribers: Reflect.has(data.publishing, 'notifySubscribers') ? data.publishing.notifySubscribers.replace('VIDEO_SUBSCRIBER_NOTIFICATION_', '') : undefined + }; } if (Reflect.has(data, 'paidProductPlacement')) { - this.paid_product_placement = data.paidProductPlacement; + this.paid_product_placement = data.paidProductPlacement.replace('VIDEO_PAID_PRODUCT_PLACEMENT_', ''); } if (Reflect.has(data, 'allowEmbed')) { @@ -637,31 +655,56 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'music')) { - this.music = parseObject(data.music); + this.music = { + licensed_status: Reflect.has(data.music, 'licensedStatus') ? data.music.licensedStatus.replace('VIDEO_MUSIC_LICENSED_STATUS_', '') : undefined + }; } if (Reflect.has(data, 'monetization')) { - this.monetization = parseObject(data.monetization); + const ad_monetization = data.monetization.adMonetization; + this.monetization = { + ad_monetization: ad_monetization ? { + user_set_monetization: Reflect.has(ad_monetization, 'userSetMonetization') ? ad_monetization.userSetMonetization.replace('VIDEO_USER_SET_MONETIZATION_', '') : undefined, + effective_status: Reflect.has(ad_monetization, 'effectiveStatus') ? ad_monetization.effectiveStatus.replace('VIDEO_MONETIZING_STATUS_', '') : undefined + } : undefined + }; } if (Reflect.has(data, 'visibility')) { - this.visibility = parseObject(data.visibility); + const visibility = data.visibility; + this.visibility = { + effective_status: Reflect.has(visibility, 'effectiveStatus') ? visibility.effectiveStatus.replace('VIDEO_VISIBILITY_STATUS_', '') : undefined, + user_set_visibility: Reflect.has(visibility, 'userSetVisibility') ? visibility.userSetVisibility.replace('VIDEO_USER_SET_VISIBILITY_', '') : undefined, + user_inflicted_visibility: Reflect.has(visibility, 'userInflictedVisibility') ? visibility.userInflictedVisibility.replace('VIDEO_USER_INFLICTED_VISIBILITY_', '') : undefined, + effective_visibility: Reflect.has(visibility, 'effectiveVisibility') ? visibility.effectiveVisibility.replace('VIDEO_EFFECTIVE_VISIBILITY_', '') : undefined + }; } if (Reflect.has(data, 'origin')) { - this.origin = data.origin; + this.origin = data.origin.replace('VIDEO_ORIGIN_', ''); } if (Reflect.has(data, 'inlineEditProcessingStatus')) { - this.inline_edit_processing_status = data.inlineEditProcessingStatus; + this.inline_edit_processing_status = data.inlineEditProcessingStatus.replace('VIDEO_PROCESSING_STATUS_', ''); } if (Reflect.has(data, 'copyrightSummary')) { - this.copyright_summary = parseObject(data.copyrightSummary); + const copyright_summary = data.copyrightSummary; + this.copyright_summary = { + video_copyright_summary_status: Reflect.has(copyright_summary, 'videoCopyrightSummaryStatus') ? copyright_summary.videoCopyrightSummaryStatus.replace('VIDEO_COPYRIGHT_SUMMARY_STATUS_', '') : undefined, + channel_impacts: Array.isArray(copyright_summary.channelImpacts) ? + copyright_summary.channelImpacts.map((impact: string) => impact.replace('VIDEO_COPYRIGHT_CHANNEL_IMPACT_', '')) : undefined, + video_visibility_impacts: Array.isArray(copyright_summary.videoVisibilityImpacts) ? + copyright_summary.videoVisibilityImpacts.map((impact: string) => impact.replace('VIDEO_COPYRIGHT_VISIBILITY_IMPACT_', '')) : undefined, + video_monetization_impact: Reflect.has(copyright_summary, 'videoMonetizationImpact') ? copyright_summary.videoMonetizationImpact.replace('VIDEO_COPYRIGHT_MONETIZATION_IMPACT_', '') : undefined, + active_third_party_claims_count: copyright_summary.activeThirdPartyClaimsCount + }; } if (Reflect.has(data, 'sponsorsOnly')) { - this.sponsors_only = parseObject(data.sponsorsOnly); + this.sponsors_only = { + is_sponsors_only: data.sponsorsOnly.isSponsorsOnly + }; } if (Reflect.has(data, 'serializedShareEntity')) { @@ -677,15 +720,26 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'audienceRestriction')) { - this.audience_restriction = parseObject(data.audienceRestriction); + const audience_restriction = data.audienceRestriction; + this.audience_restriction = { + self_rating: Reflect.has(audience_restriction, 'selfRating') ? audience_restriction.selfRating.replace('VIDEO_TARGETED_AUDIENCE_', '') : undefined, + system_rating: Reflect.has(audience_restriction, 'systemRating') ? audience_restriction.systemRating.replace('VIDEO_TARGETED_AUDIENCE_', '') : undefined, + override_enabled: audience_restriction.overrideEnabled, + effective_rating: Reflect.has(audience_restriction, 'effectiveRating') ? audience_restriction.effectiveRating.replace('VIDEO_TARGETED_AUDIENCE_', '') : undefined, + imposer: Reflect.has(audience_restriction, 'imposer') ? audience_restriction.imposer.replace('VIDEO_TARGETED_AUDIENCE_IMPOSER_', '') : undefined + }; } if (Reflect.has(data, 'ownedClaimDetails')) { - this.owned_claim_details = parseObject(data.ownedClaimDetails); + const owned_claim_details = data.ownedClaimDetails; + this.owned_claim_details = { + can_edit_owned_claim: owned_claim_details.canEditOwnedClaim, + can_enable_matching: owned_claim_details.canEnableMatching + }; } if (Reflect.has(data, 'monetizedStatus')) { - this.monetized_status = data.monetizedStatus; + this.monetized_status = data.monetizedStatus.replace('MONETIZED_STATUS_', ''); } if (Reflect.has(data, 'commentsDisabledInternally')) { @@ -693,25 +747,60 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'allRestrictions')) { - this.all_restrictions = parseObject(data.allRestrictions); + const summary = data.allRestrictions.summary; + this.all_restrictions = { + summary: summary ? { + severity: Reflect.has(summary, 'severity') ? summary.severity.replace('VIDEO_RESTRICTIONS_SEVERITY_', '') : undefined + } : undefined + }; } if (Reflect.has(data, 'videoResolutions')) { - this.video_resolutions = parseObject(data.videoResolutions); + const video_resolutions = data.videoResolutions; + this.video_resolutions = { + status_sd: Reflect.has(video_resolutions, 'statusSd') ? video_resolutions.statusSd.replace('RESOLUTION_STATUS_', '') : undefined, + status_hd: Reflect.has(video_resolutions, 'statusHd') ? video_resolutions.statusHd.replace('RESOLUTION_STATUS_', '') : undefined, + status4k: Reflect.has(video_resolutions, 'status4k') ? video_resolutions.status4k.replace('RESOLUTION_STATUS_', '') : undefined, + status2k: Reflect.has(video_resolutions, 'status2k') ? video_resolutions.status2k.replace('RESOLUTION_STATUS_', '') : undefined + }; } if (Reflect.has(data, 'adSettings')) { - this.ad_settings = parseObject(data.adSettings); + const ad_formats = data.adSettings.adFormats; + const ad_breaks = data.adSettings.adBreaks; + this.ad_settings = { + ad_formats: ad_formats ? { + has_skippable_video_ads: ad_formats.hasSkippableVideoAds, + has_non_skippable_video_ads: ad_formats.hasNonSkippableVideoAds, + has_display_ads: ad_formats.hasDisplayAds, + has_live_display_ads: ad_formats.hasLiveDisplayAds + } : undefined, + ad_breaks: ad_breaks ? { + has_preroll_ads: ad_breaks.hasPrerollAds, + has_midroll_ads: ad_breaks.hasMidrollAds, + has_postroll_ads: ad_breaks.hasPostrollAds, + auto_gen_midrolls_status: Reflect.has(ad_breaks, 'autoGenMidrollsStatus') ? ad_breaks.autoGenMidrollsStatus.replace('AUTO_GEN_MIDROLLS_STATUS_', '') : undefined + } : undefined + }; } if (Reflect.has(data, 'videoPrechecks')) { const raw_prechecks = data.videoPrechecks; + const community_guidelines_details = raw_prechecks.additionalDetails?.communityGuidelinesDetails; this.video_prechecks = { copyright_prechecks_done: raw_prechecks.copyrightPrechecksDone, brand_safety_prechecks_done: raw_prechecks.brandSafetyPrechecksDone, video_upload_checks_monetized: Reflect.has(raw_prechecks, 'videoUploadChecksMonetized') ? new VideoUploadChecks(raw_prechecks.videoUploadChecksMonetized) : undefined, video_upload_checks_not_monetized: Reflect.has(raw_prechecks, 'videoUploadChecksNotMonetized') ? new VideoUploadChecks(raw_prechecks.videoUploadChecksNotMonetized) : undefined, - additional_details: Reflect.has(raw_prechecks, 'additionalDetails') ? parseObject(raw_prechecks.additionalDetails) : undefined + additional_details: Reflect.has(raw_prechecks, 'additionalDetails') ? { + community_guidelines_details: community_guidelines_details ? { + all_policy_details: Array.isArray(community_guidelines_details.allPolicyDetails) ? + community_guidelines_details.allPolicyDetails.map((policy_detail: RawNode) => ({ + vertical: Reflect.has(policy_detail, 'vertical') ? policy_detail.vertical.replace('TOU_POLICY_VERTICAL_', '') : undefined + })) : undefined, + human_review_state: Reflect.has(community_guidelines_details, 'humanReviewState') ? community_guidelines_details.humanReviewState.replace('HUMAN_REVIEW_STATE_', '') : undefined + } : undefined + } : undefined }; } @@ -720,45 +809,67 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'notification')) { - this.notification = parseObject(data.notification); + this.notification = { + precheck_notifications_enabled: data.notification.precheckNotificationsEnabled + }; } if (Reflect.has(data, 'mfkSettings')) { - this.mfk_settings = parseObject(data.mfkSettings); + const mfk_settings = data.mfkSettings; + this.mfk_settings = { + mfk_by_creator: Reflect.has(mfk_settings, 'mfkByCreator') ? mfk_settings.mfkByCreator.replace('VIDEO_MADE_FOR_KIDS_', '') : undefined, + mfk_without_creator_input: Reflect.has(mfk_settings, 'mfkWithoutCreatorInput') ? mfk_settings.mfkWithoutCreatorInput.replace('VIDEO_MADE_FOR_KIDS_', '') : undefined, + override_enabled: mfk_settings.overrideEnabled, + effective_mfk: Reflect.has(mfk_settings, 'effectiveMfk') ? mfk_settings.effectiveMfk.replace('VIDEO_MADE_FOR_KIDS_', '') : undefined, + imposer: Reflect.has(mfk_settings, 'imposer') ? mfk_settings.imposer.replace('VIDEO_MADE_FOR_KIDS_IMPOSER_', '') : undefined + }; } if (Reflect.has(data, 'autoChapterSettings')) { - this.auto_chapter_settings = parseObject(data.autoChapterSettings); + this.auto_chapter_settings = { + creator_opt_out: data.autoChapterSettings.creatorOptOut + }; } if (Reflect.has(data, 'remix')) { - this.remix = parseObject(data.remix); + const remix = data.remix; + this.remix = { + remix_source_option_eligibility: Reflect.has(remix, 'remixSourceOptionEligibility') ? remix.remixSourceOptionEligibility.replace('REMIX_SOURCE_OPTION_ELIGIBILITY_', '') : undefined, + is_source: remix.isSource, + remix_source_shorts: Reflect.has(remix, 'remixSourceShorts') ? remix.remixSourceShorts.replace('REMIX_SOURCE_SHORTS_', '') : undefined + }; } if (Reflect.has(data, 'contentOwnershipModelSettings')) { - this.content_ownership_model_settings = parseObject(data.contentOwnershipModelSettings); + this.content_ownership_model_settings = { + is_off_network_upload: data.contentOwnershipModelSettings.isOffNetworkUpload + }; } if (Reflect.has(data, 'publicMetrics')) { - const public_metrics = parseObject(data.publicMetrics); - for (const key of [ 'view_count', 'comment_count', 'like_count', 'external_view_count' ] as const) { - if (Reflect.has(public_metrics, key)) { - public_metrics[key] = toInt(public_metrics[key]); - } - } - this.public_metrics = public_metrics; + const public_metrics = data.publicMetrics; + this.public_metrics = { + view_count: Number(public_metrics.viewCount), + comment_count: Number(public_metrics.commentCount), + like_count: Number(public_metrics.likeCount), + external_view_count: Number(public_metrics.externalViewCount) + }; } if (Reflect.has(data, 'autoPlacesMentionedSettings')) { - this.auto_places_mentioned_settings = parseObject(data.autoPlacesMentionedSettings); + this.auto_places_mentioned_settings = { + creator_opt_out: data.autoPlacesMentionedSettings.creatorOptOut + }; } if (Reflect.has(data, 'shorts')) { - this.shorts = parseObject(data.shorts); + this.shorts = { + is_shorts_renderable: data.shorts.isShortsRenderable + }; } if (Reflect.has(data, 'contentType')) { - this.content_type = data.contentType; + this.content_type = data.contentType.replace('CREATOR_CONTENT_TYPE_', ''); } if (Reflect.has(data, 'isPaygated')) { @@ -766,27 +877,42 @@ export default class CreatorVideo extends YTNode { } if (Reflect.has(data, 'learningConceptSettings')) { - this.learning_concept_settings = parseObject(data.learningConceptSettings); + this.learning_concept_settings = { + creator_opt_out: data.learningConceptSettings.creatorOptOut + }; } if (Reflect.has(data, 'commentSettings')) { - this.comment_settings = parseObject(data.commentSettings); + const comment_settings = data.commentSettings; + this.comment_settings = { + comments_enabled_state: Reflect.has(comment_settings, 'commentsEnabledState') ? comment_settings.commentsEnabledState.replace('VIDEO_COMMENTS_ENABLED_STATE_', '') : undefined, + allowed_commenter_mode: Reflect.has(comment_settings, 'allowedCommenterMode') ? comment_settings.allowedCommenterMode.replace('ALLOWED_COMMENTER_MODE_', '') : undefined, + commenter_minimum_subscription_time: Reflect.has(comment_settings, 'commenterMinimumSubscriptionTime') ? comment_settings.commenterMinimumSubscriptionTime.replace('COMMENTER_MINIMUM_SUBSCRIPTION_TIME_', '') : undefined + }; } if (Reflect.has(data, 'productAutotaggingSettings')) { - this.product_autotagging_settings = parseObject(data.productAutotaggingSettings); + this.product_autotagging_settings = { + creator_opt_out: data.productAutotaggingSettings.creatorOptOut + }; } if (Reflect.has(data, 'collaboration')) { - this.collaboration = parseObject(data.collaboration); + this.collaboration = { + serialized_share_entity: data.collaboration.serializedShareEntity + }; } if (Reflect.has(data, 'paidPoliticalContent')) { - this.paid_political_content = parseObject(data.paidPoliticalContent); + this.paid_political_content = { + paid_product_placement_political_content_from_eu_creator: data.paidPoliticalContent.paidProductPlacementPoliticalContentFromEuCreator + }; } if (Reflect.has(data, 'superfansOnly')) { - this.superfans_only = parseObject(data.superfansOnly); + this.superfans_only = { + is_superfans_only: data.superfansOnly.isSuperfansOnly + }; } } } diff --git a/src/parser/classes/ytstudio/Translation.ts b/src/parser/classes/ytstudio/Translation.ts index 3824820257..a5a4cc25cd 100644 --- a/src/parser/classes/ytstudio/Translation.ts +++ b/src/parser/classes/ytstudio/Translation.ts @@ -1,15 +1,13 @@ import { YTNode } from '../../helpers.js'; import { type RawNode } from '../../index.js'; -import { parseAll } from '../../parser.js'; export type TranslationStatus = - 'TRANSLATION_STATUS_DELETING' | 'TRANSLATION_STATUS_DRAFT' | 'TRANSLATION_STATUS_FAILED' | - 'TRANSLATION_STATUS_PROCESSING' | 'TRANSLATION_STATUS_PUBLISHED' | 'TRANSLATION_STATUS_PUBLISHING' | - 'TRANSLATION_STATUS_QC_FAILED' | 'TRANSLATION_STATUS_REVIEW' | 'TRANSLATION_STATUS_SUBMITTED' | - 'TRANSLATION_STATUS_SUCCESS' | 'TRANSLATION_STATUS_SYNCING_DRAFT' | 'TRANSLATION_STATUS_UNKNOWN'; + 'DELETING' | 'DRAFT' | 'FAILED' | + 'PROCESSING' | 'PUBLISHED' | 'PUBLISHING' | + 'QC_FAILED' | 'REVIEW' | 'SUBMITTED' | + 'SUCCESS' | 'SYNCING_DRAFT' | 'UNKNOWN'; -export type TranslationSource = - 'TRANSLATION_SOURCE_CREATOR' | 'TRANSLATION_SOURCE_COMMUNITY' | 'TRANSLATION_SOURCE_AUTOMATIC'; +export type TranslationSource = 'CREATOR' | 'COMMUNITY' | 'AUTOMATIC'; export interface TTSTrackId { kind: string; @@ -30,10 +28,10 @@ export interface CaptionSegments { export interface CaptionsTranslation { status?: TranslationStatus; source?: TranslationSource; - time_updated_seconds?: string; + time_updated_seconds?: number; tts_track_id?: TTSTrackId; caption_segments?: CaptionSegments; - content_update_time?: string; + content_update_time?: number; is_complex_track?: boolean; } @@ -56,7 +54,50 @@ export default class Translation extends YTNode { } if (Reflect.has(data, 'captionsTranslations')) { - this.captions_translations = parseAll(data.captionsTranslations); + this.captions_translations = data.captionsTranslations.map((captions_translation: RawNode) => { + const parsed_captions_translation: CaptionsTranslation = {}; + + if (Reflect.has(captions_translation, 'status')) { + parsed_captions_translation.status = captions_translation.status.replace('TRANSLATION_STATUS_', ''); + } + + if (Reflect.has(captions_translation, 'source')) { + parsed_captions_translation.source = captions_translation.source.replace('TRANSLATION_SOURCE_', ''); + } + + if (Reflect.has(captions_translation, 'contentUpdateTime')) { + parsed_captions_translation.content_update_time = Number(captions_translation.contentUpdateTime); + } + + if (Reflect.has(captions_translation, 'timeUpdatedSeconds')) { + parsed_captions_translation.time_updated_seconds = Number(captions_translation.timeUpdatedSeconds); + } + + if (Reflect.has(captions_translation, 'ttsTrackId')) { + parsed_captions_translation.tts_track_id = { + kind: captions_translation.ttsTrackId.kind, + lang: captions_translation.ttsTrackId.lang, + name: captions_translation.ttsTrackId.name + }; + } + + if (Reflect.has(captions_translation, 'captionSegments') && Array.isArray(captions_translation.captionSegments?.segments)) { + parsed_captions_translation.caption_segments = { + segments: + captions_translation.captionSegments.segments.map((segment: RawNode) => ({ + start_time_ms: segment.startTimeMs, + duration_ms: segment.durationMs, + text: segment.text + })) + }; + } + + if (Reflect.has(captions_translation, 'isComplexTrack')) { + parsed_captions_translation.is_complex_track = captions_translation.isComplexTrack; + } + + return parsed_captions_translation; + }); } } } diff --git a/src/parser/classes/ytstudio/UploadFeedbackItem.ts b/src/parser/classes/ytstudio/UploadFeedbackItem.ts index f20d9848ca..2a9ad1287f 100644 --- a/src/parser/classes/ytstudio/UploadFeedbackItem.ts +++ b/src/parser/classes/ytstudio/UploadFeedbackItem.ts @@ -1,19 +1,16 @@ import { YTNode } from '../../helpers.js'; import { type RawNode } from '../../index.js'; -import { parseObject } from '../../parser.js'; import DataFreshnessEntity from './DataFreshnessEntity.js'; import VideoUploadChecks from './VideoUploadChecks.js'; export interface TimedContinuationData { timeout_ms: number; continuation: string; - click_tracking_params: string; } export interface UploadFeedbackRefreshContinuation { continuation: string; continue_in_ms: number; - click_tracking_params: string; } export interface UploadFeedbackContinuation { @@ -74,9 +71,17 @@ export default class UploadFeedbackItem extends YTNode { }); } if (Reflect.has(data, 'continuations')) { + const timed_continuation_data = data.continuations[0]?.timedContinuationData; + const upload_feedback_refresh_continuation_data = data.continuations[1]?.uploadFeedbackRefreshContinuation; this.continuations = { - timed_continuation_data: parseObject(data.continuations[0]?.timedContinuationData), - upload_feedback_refresh_continuation: parseObject(data.continuations[1]?.uploadFeedbackRefreshContinuation) + timed_continuation_data: { + continuation: timed_continuation_data.continuation, + timeout_ms: timed_continuation_data.timeoutMs + }, + upload_feedback_refresh_continuation: { + continuation: upload_feedback_refresh_continuation_data.continuation, + continue_in_ms: upload_feedback_refresh_continuation_data.continueInMs + } }; } if (Reflect.has(data, 'dataFreshnessEntity')) { diff --git a/src/parser/classes/ytstudio/VideoUploadChecks.ts b/src/parser/classes/ytstudio/VideoUploadChecks.ts index 0f864a4d88..610a440843 100644 --- a/src/parser/classes/ytstudio/VideoUploadChecks.ts +++ b/src/parser/classes/ytstudio/VideoUploadChecks.ts @@ -2,32 +2,32 @@ import { YTNode } from '../../helpers.js'; import { type RawNode } from '../../index.js'; export type UploadChecksSummaryStatus = - 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_COMPLETED' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_EXTENDED_CHECK_STARTED' | - 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_INLINE_EDIT_IN_PROGRESS' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_NOT_STARTED' | - 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_OVERDUE' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_SELF_CERTIFICATION_MISSING' | - 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_SHORTS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_STARTED' | - 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_UNABLE_TO_RUN' | 'UPLOAD_CHECKS_DATA_SUMMARY_STATUS_UNKNOWN'; + 'COMPLETED' | 'EXTENDED_CHECK_STARTED' | + 'INLINE_EDIT_IN_PROGRESS' | 'NOT_STARTED' | + 'OVERDUE' | 'SELF_CERTIFICATION_MISSING' | + 'SHORTS_NOT_ELIGIBLE' | 'STARTED' | + 'UNABLE_TO_RUN' | 'UNKNOWN'; export type UploadChecksCopyrightStatus = - 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_COMPLETED' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_INLINE_EDIT_IN_PROGRESS' | - 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_NOT_STARTED' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_OVERDUE' | - 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_SHORTS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_STARTED' | - 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_UNABLE_TO_RUN' | 'UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_UNKNOWN'; + 'COMPLETED' | 'INLINE_EDIT_IN_PROGRESS' | + 'NOT_STARTED' | 'OVERDUE' | + 'SHORTS_NOT_ELIGIBLE' | 'STARTED' | + 'UNABLE_TO_RUN' | 'UNKNOWN'; export type UploadChecksAdSuitabilityStatus = - 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_CHANNEL_NOT_MONETIZED' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_COMPLETED' | - 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_EXTENDED_CHECK_STARTED' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_NOT_STARTED' | - 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_OVERDUE' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_SELF_CERTIFICATION_MISSING' | - 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_SHORTS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_STARTED' | - 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_UNABLE_TO_RUN' | 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_UNKNOWN' | - 'UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_VIDEO_NOT_MONETIZED'; + 'CHANNEL_NOT_MONETIZED' | 'COMPLETED' | + 'EXTENDED_CHECK_STARTED' | 'NOT_STARTED' | + 'OVERDUE' | 'SELF_CERTIFICATION_MISSING' | + 'SHORTS_NOT_ELIGIBLE' | 'STARTED' | + 'UNABLE_TO_RUN' | 'UNKNOWN' | + 'VIDEO_NOT_MONETIZED'; export type UploadChecksCommunityGuidelinesStatus = - 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_COMPLETED' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_NOT_AVAILABLE_ON_PUBLISHED_VIDEO' | - 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_NOT_ELIGIBLE' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_NOT_STARTED' | - 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_OVERDUE' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_SHORTS_NOT_ELIGIBLE' | - 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_STARTED' | 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_UNABLE_TO_RUN' | - 'UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_UNKNOWN'; + 'COMPLETED' | 'NOT_AVAILABLE_ON_PUBLISHED_VIDEO' | + 'NOT_ELIGIBLE' | 'NOT_STARTED' | + 'OVERDUE' | 'SHORTS_NOT_ELIGIBLE' | + 'STARTED' | 'UNABLE_TO_RUN' | + 'UNKNOWN'; export default class VideoUploadChecks extends YTNode { static type = 'VideoUploadChecks'; @@ -41,19 +41,19 @@ export default class VideoUploadChecks extends YTNode { super(); if (Reflect.has(data, 'checksSummary') && Reflect.has(data.checksSummary, 'status')) { - this.checks_summary = data.checksSummary.status; + this.checks_summary = data.checksSummary.status.replace('UPLOAD_CHECKS_DATA_SUMMARY_STATUS_', ''); } if (Reflect.has(data, 'copyrightCheck') && Reflect.has(data.copyrightCheck, 'checkStatus')) { - this.copyright_check = data.copyrightCheck.checkStatus; + this.copyright_check = data.copyrightCheck.checkStatus.replace('UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_', ''); } if (Reflect.has(data, 'adSuitabilityCheck') && Reflect.has(data.adSuitabilityCheck, 'checkStatus')) { - this.ad_suitability_check = data.adSuitabilityCheck.checkStatus; + this.ad_suitability_check = data.adSuitabilityCheck.checkStatus.replace('UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_', ''); } if (Reflect.has(data, 'communityGuidelinesCheck') && Reflect.has(data.communityGuidelinesCheck, 'checkStatus')) { - this.community_guidelines_check = data.communityGuidelinesCheck.checkStatus; + this.community_guidelines_check = data.communityGuidelinesCheck.checkStatus.replace('UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_', ''); } } } diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 92097eaaaa..7ed052b04e 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -848,27 +848,6 @@ export function parseFormats(formats: RawNode[], this_response_nsig_cache: Map new Format(format, this_response_nsig_cache)) || []; } -// useful for YTStudio since it outputs more in a nested data format rather than a 'rendering' format -export function parseObject(data: unknown): T { - if (Array.isArray(data)) { - return data.map((item) => parseObject(item)) as T; - } - - if (data !== null && typeof data === 'object') { - const result: Record = {}; - for (const [ key, value ] of Object.entries(data)) { - result[camelToSnake(key)] = parseObject(value); - } - return result as T; - } - - return data as T; -} -export function parseAll(data: unknown): T[] { - if (!Array.isArray(data)) return []; - return data.map((item) => parseObject(item)); -} - export function applyMutations(memo: Memo, mutations: RawNode[]) { // Apply mutations to MusicMultiSelectMenuItems const music_multi_select_menu_items = memo.getType(MusicMultiSelectMenuItem); From 4ff7b6c28c77ba37545881d6e4a8a38d79a45f34 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Fri, 14 Aug 2026 23:41:23 -0700 Subject: [PATCH 26/27] feat(YouTube Studio Web): update VIDEO_READ_MASK to include everything just like the actual requests --- src/core/clients/StudioWeb.ts | 249 ++++++++++++++++++++++++++++++++-- 1 file changed, 241 insertions(+), 8 deletions(-) diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts index 40753b0a2e..12f3b188e7 100644 --- a/src/core/clients/StudioWeb.ts +++ b/src/core/clients/StudioWeb.ts @@ -62,21 +62,254 @@ interface UploadSubtitlesResponse { interface UploadFeedbackResult { contents: UploadFeedbackItem[], next: () => Promise }; const VIDEO_READ_MASK = { + metadataLanguage: { + all: true + }, + notification: { + all: true + }, + status: true, + statusDetails: { + all: true + }, + ownedClaimDetails: { + all: true + }, + thumbnailDetails: { + all: true + }, videoId: true, + permissions: { + all: true + }, + origin: true, + inlineEditProcessingStatus: true, + monetization: { + all: true + }, + allRestrictions: { + all: true + }, + videoPrechecks: { + all: true + }, + audienceRestriction: { + all: true + }, + mfkSettings: { + all: true + }, + selfCertification: { + all: true + }, + videoStreamUrl: true, + visibility: { + all: true + }, + shorts: { + all: true + }, + responseStatus: { + all: true + }, + contentType: true, channelId: true, + features: { + all: true + }, + draftStatus: true, + brandDealVideoLink: { + all: true + }, + activeBrandDealVideoLinks: { + all: true + }, + audioLanguage: { + all: true + }, + videoAdvertiserSpecificAgeGates: { + all: true + }, + claimDetails: { + all: true + }, + commentsDisabledInternally: true, + livestream: { + all: true + }, + music: { + all: true + }, + premiere: { + all: true + }, + timePublishedSeconds: true, + uncaptionedReason: true, + remix: { + all: true + }, + contentOwnershipModelSettings: { + all: true + }, + creatorInitiatedVideoChannelLinks: { + all: true + }, + releaseInfo: { + all: true + }, + podcastRssMetadata: { + all: true + }, + googleAdsVideoLinks: { + all: true + }, + alteredContentSettings: { + all: true + }, + collaboration: { + all: true + }, + videoResolutions: { + all: true + }, + publicLivestream: { + all: true + }, + publicPremiere: { + all: true + }, + thumbnailEditorState: { + all: true + }, title: true, - description: true, + videoCreatorExperiment: { + all: true + }, + lengthSeconds: true, + tvfilmMetadata: { + all: true + }, privacy: true, - status: true, - draftStatus: true, shareUrl: true, - lengthSeconds: true, + scheduledPublishingDetails: { + all: true + }, + privateShare: { + all: true + }, + sponsorsOnly: { + all: true + }, + superfansOnly: { + all: true + }, + unlistedExpired: true, + videoTrailers: { + all: true + }, + isPaygated: true, + suggestions: { + all: true + }, + tvType: { + all: true + }, + genres: { + all: true + }, + episode: { + all: true + }, + titleDetails: { + all: true + }, + copyrightSummary: { + all: true + }, + productSelection: { + all: true + }, + productAutotaggingSettings: { + all: true + }, + videoLinkageShortsAttribution: { + all: true + }, + academicLearning: { + all: true + }, + allowEmbed: true, + allowRatings: true, + ageRestriction: true, + category: true, + commentFilter: true, + commentSettings: { + all: true + }, + crowdsourcingEnabled: true, + dateRecorded: { + all: true + }, + defaultCommentSortOrder: true, + description: true, + descriptionFormattedString: { + all: true + }, + gameTitle: { + all: true + }, + license: true, + liveChat: { + all: true + }, + location: { + all: true + }, + paidProductPlacement: true, + paidPoliticalContent: { + all: true + }, + publishing: { + all: true + }, + tags: { + all: true + }, + titleFormattedString: { + all: true + }, videoDurationMs: true, + viewCountIsHidden: true, + autoChapterSettings: { + all: true + }, + autoPlacesMentionedSettings: { + all: true + }, + videoArtworkEditorState: { + all: true + }, + learningConceptSettings: { + all: true + }, + videoEditorProject: { + videoDimensions: { + all: true + } + }, + originalFilename: true, timeCreatedSeconds: true, - timePublishedSeconds: true, - thumbnailDetails: { all: true }, - responseStatus: { all: true }, - statusDetails: { all: true } + files: { + all: true + }, + adSettings: { + all: true + }, + monetizedStatus: true, + serializedShareEntity: true, + publicMetrics: { + all: true + } } as const; const CREATOR_VIDEO_CATEGORY_IDS = { From ef25cc01e9f735c6bc9f10af34321a4cf7a43d95 Mon Sep 17 00:00:00 2001 From: Illusion137 Date: Sun, 30 Aug 2026 16:00:38 -0700 Subject: [PATCH 27/27] feat(YouTube Studio Web Example): use `cookie-eater` for easier testing --- examples/youtube-studio-web/package.json | 1 + examples/youtube-studio-web/src/GetSessionToken.ts | 5 +++-- examples/youtube-studio-web/src/utils.ts | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/youtube-studio-web/package.json b/examples/youtube-studio-web/package.json index cfd75789f7..f20be0025c 100644 --- a/examples/youtube-studio-web/package.json +++ b/examples/youtube-studio-web/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "bgutils-js": "^4.0.3", + "cookie-eater": "^1.0.0", "jsdom": "^28.1.0", "youtubei.js": "file:../../" } diff --git a/examples/youtube-studio-web/src/GetSessionToken.ts b/examples/youtube-studio-web/src/GetSessionToken.ts index fa44f29dac..d537e6d1ba 100644 --- a/examples/youtube-studio-web/src/GetSessionToken.ts +++ b/examples/youtube-studio-web/src/GetSessionToken.ts @@ -1,9 +1,10 @@ import Innertube, { UniversalCache } from 'youtubei.js'; -import { botguard_solver, get_channel_id } from './utils.ts'; +import { botguard_solver, get_channel_id, youtube_cookies } from './utils.ts'; -const COOKIES = ""; // ?? Place your YouTube cookies here (async () => { + const COOKIES = await youtube_cookies(); + const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); const CHANNEL_ID = await get_channel_id(yt); diff --git a/examples/youtube-studio-web/src/utils.ts b/examples/youtube-studio-web/src/utils.ts index c2163a0ff5..ff0c9b14d8 100644 --- a/examples/youtube-studio-web/src/utils.ts +++ b/examples/youtube-studio-web/src/utils.ts @@ -4,6 +4,7 @@ import { JSDOM, VirtualConsole } from "jsdom"; import Innertube, { type Types } from 'youtubei.js'; import path from "path"; import fs from 'fs/promises'; +import { get_cookies } from 'cookie-eater'; export const botguard_solver: Types.BotGuardSolver = { solve: async(botguard_challenge, binding) => { @@ -72,4 +73,13 @@ export async function get_file_named_buffer_base64(file_path: string): Promise { + const HOST_NAME = '.youtube.com'; + const cookies_map = await get_cookies([HOST_NAME]); + const cookies = cookies_map?.[HOST_NAME].toString() ?? undefined; + + if(cookies) console.log(`Successfully found ${cookies_map?.[HOST_NAME].getCookies().length} cookies`); + return cookies; } \ No newline at end of file