Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''
}
Comment on lines +7 to +12
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will not accept a global proxy; there are already separate systems for that.

},
cluster: {
enabled: true, // active cluster (or use env CLUSTER_ENABLED)
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getVersion,
initLogger,
logger,
makeRequest,
parseClient,
verifyDiscordID
} from './utils.ts'
Expand Down Expand Up @@ -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}`)
})
Comment on lines +599 to +618
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad practices

}

/**
Expand Down
40 changes: 39 additions & 1 deletion src/sources/youtube/YouTube.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PassThrough } from 'node:stream'
import { readFileSync } from 'node:fs'
import HLSHandler from '../../playback/hls/HLSHandler.ts'
import type {
PreviousSessionState,
Expand Down Expand Up @@ -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']`). */
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;'
}
})

Expand Down
6 changes: 4 additions & 2 deletions src/sources/youtube/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3111,9 +3111,11 @@ export abstract class BaseClient {
* @internal
*/
_getQualityPriority(): Record<string, number[]> {
// 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],
Comment on lines +3117 to +3118
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A question was raised about ITAgs; it doesn't mean they should be added as a standard feature, it's something that only appears on YouTube Premium accounts to complete the setup, and NodeLink already has a system for selecting the best ITAgs, so this is redundant.

low: [249, 250, 140],
lowest: [249, 139]
}
Expand Down
11 changes: 11 additions & 0 deletions src/typings/config/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +35 to +45
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will not accept a global proxy; there are already separate systems for that.

}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/typings/sources/youtube.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 11 additions & 0 deletions src/typings/utils.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Comment on lines +356 to +366
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will not accept a global proxy; there are already separate systems for that.

}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading