From 7b83ef0e60abc2c8e33f607b2fd03812e56a11f5 Mon Sep 17 00:00:00 2001 From: doyimmiuink Date: Fri, 24 Apr 2026 02:18:20 +0000 Subject: [PATCH] feat(youtube,server): add cookiePath, httpProxy and HQ audio tags 774/141 feat(youtube): cookiePath config support src/typings/sources/youtube.types.ts: add cookiePath? with JSDoc config.default.js: add cookiePath to youtube block src/sources/youtube/YouTube.ts: - import readFileSync from node:fs - add private cookieHeader field - constructor: init cookieHeader = null - initialize: parse Netscape or inline cookie format - _fetchVisitorData: use this.cookieHeader ?? hardcoded fallback feat(server): global httpProxy config config.default.js: add httpProxy to server block src/typings/config/config.types.ts: add httpProxy? to ServerConfig src/typings/utils.types.ts: add options?.server?.httpProxy src/utils.ts: makeRequest falls back to server.httpProxy src/index.ts: import makeRequest + _testProxyConnectivity feat(youtube): HQ audio format tags 774 and 141 src/sources/youtube/common.ts: add 774/141 to high/medium priority --- config.default.js | 9 ++++++- src/index.ts | 21 +++++++++++++++ src/sources/youtube/YouTube.ts | 40 +++++++++++++++++++++++++++- src/sources/youtube/common.ts | 6 +++-- src/typings/config/config.types.ts | 11 ++++++++ src/typings/sources/youtube.types.ts | 7 +++++ src/typings/utils.types.ts | 11 ++++++++ src/utils.ts | 11 ++++++++ 8 files changed, 112 insertions(+), 4 deletions(-) diff --git a/config.default.js b/config.default.js index 01e3f7c0..92fb4e0b 100644 --- a/config.default.js +++ b/config.default.js @@ -3,7 +3,13 @@ export default { host: '0.0.0.0', port: 3000, password: 'youshallnotpass', - useBunServer: false // set to true to use Bun.serve websocket (experimental) + useBunServer: false, // set to true to use Bun.serve websocket (experimental) + httpProxy: { + enabled: false, // route all outbound HTTP requests through a proxy + url: '', // e.g. 'http://user:pass@host:3128' or 'socks5://host:1080' + username: '', + password: '' + } }, cluster: { enabled: true, // active cluster (or use env CLUSTER_ENABLED) @@ -374,6 +380,7 @@ export default { } } }, + cookiePath: '', // (optional) path to a Netscape cookies.txt file for authenticated YouTube requests cipher: { url: 'https://cipher.kikkia.dev/api', token: null diff --git a/src/index.ts b/src/index.ts index b39d32ca..d296a9e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import { getVersion, initLogger, logger, + makeRequest, parseClient, verifyDiscordID } from './utils.ts' @@ -595,6 +596,26 @@ class NodelinkServer extends EventEmitter { 'Server', `git branch: ${this.gitInfo.branch}, commit: ${this.gitInfo.commit}, committed on: ${new Date(this.gitInfo.commitTime).toISOString()}` ) + + // global-http-proxy: log proxy and test connectivity at startup + const proxyCfg = options?.server?.httpProxy + if (proxyCfg?.enabled && proxyCfg.url) { + logger('info', 'Proxy', `HTTP proxy enabled: ${proxyCfg.url}`) + this._testProxyConnectivity(proxyCfg.url) + } + } + + /** @internal */ + private _testProxyConnectivity(proxyUrl: string): void { + makeRequest('http://connectivitycheck.gstatic.com/generate_204', { + proxy: { url: proxyUrl } + }) + .then(() => { + logger('info', 'Proxy', `Connected successfully via ${proxyUrl}`) + }) + .catch((err: Error) => { + logger('warn', 'Proxy', `Proxy connectivity check failed (${proxyUrl}): ${err.message}`) + }) } /** diff --git a/src/sources/youtube/YouTube.ts b/src/sources/youtube/YouTube.ts index 6ff0ccff..0fa84332 100644 --- a/src/sources/youtube/YouTube.ts +++ b/src/sources/youtube/YouTube.ts @@ -1,4 +1,5 @@ import { PassThrough } from 'node:stream' +import { readFileSync } from 'node:fs' import HLSHandler from '../../playback/hls/HLSHandler.ts' import type { PreviousSessionState, @@ -216,6 +217,9 @@ export default class YouTubeSource { /** YouTube innertube request context sent with every API call (device info, locale, visitor data). */ private ytContext: YouTubeContext + /** Cookie string parsed from cookiePath at startup, sent with every YouTube request. */ + private cookieHeader: string | null + // -- Public fields consumed by the framework -- /** Additional source names this source can proxy through (e.g. `['ytmusic']`). */ @@ -252,6 +256,7 @@ export default class YouTubeSource { this.clients = {} this.oauth = null this.visitorDataInterval = null + this.cookieHeader = null this.cipherManager = new CipherManager(nodelink) this.liveChat = new YouTubeLiveChat(nodelink, { getProxy: this.getProxy.bind(this), @@ -334,6 +339,39 @@ export default class YouTubeSource { `Initialized clients: ${Object.keys(this.clients).join(', ')}` ) + // cookiePath: parse cookies.txt and attach to every request + const cookiePath = this.config.cookiePath + if (cookiePath) { + try { + const raw = readFileSync(cookiePath, 'utf8').trim() + let cookieHeader: string + + if (raw.includes('\t')) { + cookieHeader = raw + .split('\n') + .filter((line) => line && !line.startsWith('#')) + .map((line) => { + const parts = line.split('\t') + if (parts.length >= 7) return `${parts[5]}=${parts[6]}` + return null + }) + .filter(Boolean) + .join('; ') + } else { + cookieHeader = raw.replace(/;\s*$/, '') + } + + if (cookieHeader) { + this.cookieHeader = cookieHeader + logger('info', 'YouTube', `Loaded cookies from ${cookiePath}`) + } else { + logger('warn', 'YouTube', `cookiePath set but no cookies parsed from ${cookiePath}`) + } + } catch (e) { + logger('error', 'YouTube', `Failed to read cookiePath "${cookiePath}": ${(e as Error).message}`) + } + } + await this._fetchVisitorData() await this.cipherManager.getCachedPlayerScript() await this.cipherManager.checkCipherServerStatus() @@ -401,7 +439,7 @@ export default class YouTubeSource { } = await makeRequest('https://www.youtube.com/embed', { method: 'GET', headers: { - Cookie: 'YSC=cz5kYp3ZuIE; VISITOR_INFO1_LIVE=U-0T5oUyzf8;' + Cookie: this.cookieHeader ?? 'YSC=cz5kYp3ZuIE; VISITOR_INFO1_LIVE=U-0T5oUyzf8;' } }) diff --git a/src/sources/youtube/common.ts b/src/sources/youtube/common.ts index 5341c677..851976c6 100644 --- a/src/sources/youtube/common.ts +++ b/src/sources/youtube/common.ts @@ -3111,9 +3111,11 @@ export abstract class BaseClient { * @internal */ _getQualityPriority(): Record { + // 774 = Opus ~260-360 kbps (premium HQ, requires cookiePath) + // 141 = AAC 256 kbps (premium HQ fallback, requires cookiePath) return { - high: [251, 250, 140], - medium: [250, 140], + high: [774, 141, 251, 250, 140], + medium: [141, 251, 250, 140], low: [249, 250, 140], lowest: [249, 139] } diff --git a/src/typings/config/config.types.ts b/src/typings/config/config.types.ts index 4ff12a0b..4d5adc78 100644 --- a/src/typings/config/config.types.ts +++ b/src/typings/config/config.types.ts @@ -32,6 +32,17 @@ export interface ServerConfig { * @experimental */ useBunServer?: boolean + + /** + * Global HTTP proxy for all outbound NodeLink requests. + * Supports http, https, and socks5 URLs. + */ + httpProxy?: { + enabled: boolean + url: string + username?: string + password?: string + } } /** diff --git a/src/typings/sources/youtube.types.ts b/src/typings/sources/youtube.types.ts index 728c130b..68961164 100644 --- a/src/typings/sources/youtube.types.ts +++ b/src/typings/sources/youtube.types.ts @@ -866,6 +866,13 @@ export interface YouTubeSourceConfig { resolveExternalLinks?: boolean /** When `true`, fetches channel information (avatar, subscriber count) during Holo resolution. */ fetchChannelInfo?: boolean + /** + * Path to a Netscape-format cookies.txt file for YouTube. + * When provided, the cookies are parsed at startup and attached to every + * YouTube request, allowing access to age-restricted videos and reducing + * bot-detection friction when using a real account's cookies. + */ + cookiePath?: string /** Allow additional unknown configuration keys for forward compatibility. */ [key: string]: unknown } diff --git a/src/typings/utils.types.ts b/src/typings/utils.types.ts index c73c0f9e..d3ff3809 100644 --- a/src/typings/utils.types.ts +++ b/src/typings/utils.types.ts @@ -353,6 +353,17 @@ export interface NodelinkRuntime { routePlanner?: RoutePlannerRuntime /** Optional stats manager instance. */ statsManager?: StatsManager + /** Global HTTP proxy server options. */ + options?: { + server?: { + httpProxy?: { + enabled: boolean + url: string + username?: string + password?: string + } + } + } } /** diff --git a/src/utils.ts b/src/utils.ts index 4b8466ba..0ade3b68 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1658,6 +1658,17 @@ async function makeRequest( return http1makeRequest(urlString, options) } + // global-http-proxy: fall back to server-wide proxy when no per-request proxy is set + const globalProxyCfg = finalNodeLink?.options?.server?.httpProxy + if (globalProxyCfg?.enabled && globalProxyCfg.url) { + const globalProxy: HttpProxyConfig = { + url: globalProxyCfg.url, + username: globalProxyCfg.username, + password: globalProxyCfg.password + } + return http1makeRequest(urlString, { ...options, proxy: globalProxy }) + } + const localAddress = finalNodeLink?.routePlanner?.getIP?.() ?? undefined try {