Skip to content

Commit 8db3d28

Browse files
Merge pull request #7081 from FreeTubeApp/v0.23.3
[Master] Bump package version from 0.23.2 to 0.23.3
2 parents ed14122 + 56f3b73 commit 8db3d28

File tree

10 files changed

+222
-122
lines changed

10 files changed

+222
-122
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "freetube",
33
"productName": "FreeTube",
44
"description": "A private YouTube client",
5-
"version": "0.23.2",
5+
"version": "0.23.3",
66
"license": "AGPL-3.0-or-later",
77
"main": "./dist/main.js",
88
"private": true,
@@ -61,7 +61,7 @@
6161
"@fortawesome/vue-fontawesome": "^2.0.10",
6262
"@seald-io/nedb": "^4.0.4",
6363
"autolinker": "^4.1.0",
64-
"bgutils-js": "^3.1.3",
64+
"bgutils-js": "^3.2.0",
6565
"electron-context-menu": "^4.0.4",
6666
"marked": "^15.0.6",
6767
"path-browserify": "^1.0.1",
@@ -74,7 +74,7 @@
7474
"vue-observe-visibility": "^1.0.0",
7575
"vue-router": "^3.6.5",
7676
"vuex": "^3.6.2",
77-
"youtubei.js": "^13.1.0"
77+
"youtubei.js": "^13.3.0"
7878
},
7979
"devDependencies": {
8080
"@babel/core": "^7.26.7",

src/botGuardScript.js

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,90 @@
1-
import { BG } from 'bgutils-js'
1+
import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js'
22

33
// This script has it's own webpack config, as it gets passed as a string to Electron's evaluateJavaScript function
44
// in src/main/poTokenGenerator.js
5-
export default async function(visitorData) {
5+
6+
/**
7+
* Based on: https://github.com/LuanRT/BgUtils/blob/main/examples/node/innertube-challenge-fetcher-example.ts
8+
* @param {string} videoId
9+
* @param {string} visitorData
10+
* @param {import('youtubei.js').Session['context']} context
11+
*/
12+
export default async function (videoId, visitorData, context) {
613
const requestKey = 'O43z0dpjhgX20SCx4KAo'
714

8-
const bgConfig = {
9-
fetch: (input, init) => fetch(input, init),
10-
requestKey,
11-
globalObj: window,
12-
identifier: visitorData
15+
const challengeResponse = await fetch(
16+
'https://www.youtube.com/youtubei/v1/att/get?prettyPrint=false&alt=json',
17+
{
18+
method: 'POST',
19+
headers: {
20+
Accept: '*/*',
21+
'Content-Type': 'application/json',
22+
'X-Goog-Visitor-Id': visitorData,
23+
'X-Youtube-Client-Version': context.client.clientVersion,
24+
'X-Youtube-Client-Name': '1'
25+
},
26+
body: JSON.stringify({
27+
engagementType: 'ENGAGEMENT_TYPE_UNBOUND',
28+
context
29+
}),
30+
}
31+
)
32+
33+
if (!challengeResponse.ok) {
34+
throw new Error(`Request to ${challengeResponse.url} failed with status ${challengeResponse.status}\n${await challengeResponse.text()}`)
1335
}
1436

15-
const challenge = await BG.Challenge.create(bgConfig)
37+
const challengeData = await challengeResponse.json()
1638

17-
if (!challenge) {
18-
throw new Error('Could not get challenge')
39+
if (!challengeData.bgChallenge) {
40+
throw new Error('Failed to get BotGuard challenge')
1941
}
2042

21-
const interpreterJavascript = challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue
43+
let interpreterUrl = challengeData.bgChallenge.interpreterUrl.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue
44+
45+
if (interpreterUrl.startsWith('//')) {
46+
interpreterUrl = `https:${interpreterUrl}`
47+
}
48+
49+
const bgScriptResponse = await fetch(interpreterUrl)
50+
const interpreterJavascript = await bgScriptResponse.text()
2251

2352
if (interpreterJavascript) {
2453
// eslint-disable-next-line no-new-func
2554
new Function(interpreterJavascript)()
2655
} else {
27-
console.warn('Unable to load VM.')
56+
throw new Error('Could not load VM.')
2857
}
2958

30-
const poTokenResult = await BG.PoToken.generate({
31-
program: challenge.program,
32-
globalName: challenge.globalName,
33-
bgConfig
59+
const botGuard = await BG.BotGuardClient.create({
60+
program: challengeData.bgChallenge.program,
61+
globalName: challengeData.bgChallenge.globalName,
62+
globalObj: window
3463
})
3564

36-
return poTokenResult.poToken
65+
const webPoSignalOutput = []
66+
const botGuardResponse = await botGuard.snapshot({ webPoSignalOutput })
67+
68+
const integrityTokenResponse = await fetch(buildURL('GenerateIT', true), {
69+
method: 'POST',
70+
headers: {
71+
'content-type': 'application/json+protobuf',
72+
'x-goog-api-key': GOOG_API_KEY,
73+
'x-user-agent': 'grpc-web-javascript/0.1',
74+
},
75+
body: JSON.stringify([requestKey, botGuardResponse])
76+
})
77+
78+
const response = await integrityTokenResponse.json()
79+
80+
if (typeof response[0] !== 'string') {
81+
throw new Error('Could not get integrity token')
82+
}
83+
84+
const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken: response[0] }, webPoSignalOutput)
85+
86+
const contentPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
87+
const sessionPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(visitorData)
88+
89+
return { contentPoToken, sessionPoToken }
3790
}

src/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const IpcChannels = {
4747

4848
SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization',
4949

50-
GENERATE_PO_TOKEN: 'generate-po-token',
50+
GENERATE_PO_TOKENS: 'generate-po-tokens',
5151

5252
WRITE_SCREENSHOT: 'write-screenshot',
5353
}

src/main/index.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ function runApp() {
287287
})
288288
}
289289

290+
let proxyUrl
291+
290292
app.on('ready', async (_, __) => {
291293
if (process.env.NODE_ENV === 'production') {
292294
protocol.handle('app', async (request) => {
@@ -400,8 +402,10 @@ function runApp() {
400402
}
401403

402404
if (useProxy) {
405+
proxyUrl = `${proxyProtocol}://${proxyHostname}:${proxyPort}`
406+
403407
session.defaultSession.setProxy({
404-
proxyRules: `${proxyProtocol}://${proxyHostname}:${proxyPort}`
408+
proxyRules: proxyUrl
405409
})
406410
}
407411

@@ -883,19 +887,21 @@ function runApp() {
883887
})
884888
})
885889

886-
ipcMain.handle(IpcChannels.GENERATE_PO_TOKEN, (_, visitorData) => {
887-
return generatePoToken(visitorData)
890+
ipcMain.handle(IpcChannels.GENERATE_PO_TOKENS, (_, videoId, visitorData, context) => {
891+
return generatePoToken(videoId, visitorData, context, proxyUrl)
888892
})
889893

890894
ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => {
891895
session.defaultSession.setProxy({
892896
proxyRules: url
893897
})
898+
proxyUrl = url
894899
session.defaultSession.closeAllConnections()
895900
})
896901

897902
ipcMain.on(IpcChannels.DISABLE_PROXY, () => {
898903
session.defaultSession.setProxy({})
904+
proxyUrl = undefined
899905
session.defaultSession.closeAllConnections()
900906
})
901907

src/main/poTokenGenerator.js

Lines changed: 43 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import { join } from 'path'
99
* This is intentionally split out into it's own thing, with it's own temporary in-memory session,
1010
* as the BotGuard stuff accesses the global `document` and `window` objects and also requires making some requests.
1111
* So we definitely don't want it running in the same places as the rest of the FreeTube code with the user data.
12+
* @param {string} videoId
1213
* @param {string} visitorData
13-
* @returns {Promise<string>}
14+
* @param {string} context
15+
* @param {string|undefined} proxyUrl
16+
* @returns {Promise<{ contentPoToken: string, sessionPoToken: string }>}
1417
*/
15-
export async function generatePoToken(visitorData) {
18+
export async function generatePoToken(videoId, visitorData, context, proxyUrl) {
1619
const sessionUuid = crypto.randomUUID()
1720

1821
const theSession = session.fromPartition(`potoken-${sessionUuid}`, { cache: false })
@@ -21,12 +24,34 @@ export async function generatePoToken(visitorData) {
2124
// eslint-disable-next-line n/no-callback-literal
2225
theSession.setPermissionRequestHandler((webContents, permission, callback) => callback(false))
2326

24-
theSession.setUserAgent(
25-
theSession.getUserAgent()
26-
.split(' ')
27-
.filter(part => !part.includes('Electron'))
28-
.join(' ')
29-
)
27+
theSession.setUserAgent(session.defaultSession.getUserAgent())
28+
29+
if (proxyUrl) {
30+
await theSession.setProxy({
31+
proxyRules: proxyUrl
32+
})
33+
}
34+
35+
theSession.webRequest.onBeforeSendHeaders({
36+
urls: ['https://www.google.com/js/*', 'https://www.youtube.com/youtubei/*']
37+
}, ({ requestHeaders, url }, callback) => {
38+
if (url.startsWith('https://www.youtube.com/youtubei/')) {
39+
// make InnerTube requests work with the fetch function
40+
// InnerTube rejects requests if the referer isn't YouTube or empty
41+
requestHeaders.Referer = 'https://www.youtube.com/'
42+
requestHeaders.Origin = 'https://www.youtube.com'
43+
44+
requestHeaders['Sec-Fetch-Site'] = 'same-origin'
45+
requestHeaders['Sec-Fetch-Mode'] = 'same-origin'
46+
requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false'
47+
} else {
48+
requestHeaders['Sec-Fetch-Dest'] = 'script'
49+
requestHeaders['Sec-Fetch-Site'] = 'cross-site'
50+
requestHeaders['Accept-Language'] = '*'
51+
}
52+
53+
callback({ requestHeaders })
54+
})
3055

3156
const webContentsView = new WebContentsView({
3257
webPreferences: {
@@ -35,7 +60,8 @@ export async function generatePoToken(visitorData) {
3560
sandbox: true,
3661
v8CacheOptions: 'none',
3762
session: theSession,
38-
offscreen: true
63+
offscreen: true,
64+
webSecurity: false
3965
}
4066
})
4167

@@ -51,43 +77,8 @@ export async function generatePoToken(visitorData) {
5177

5278
webContentsView.webContents.debugger.attach()
5379

54-
await webContentsView.webContents.loadURL('data:text/html,', {
55-
baseURLForDataURL: 'https://www.youtube.com'
56-
})
57-
58-
await webContentsView.webContents.debugger.sendCommand('Emulation.setUserAgentOverride', {
59-
userAgent: theSession.getUserAgent(),
60-
acceptLanguage: 'en-US',
61-
platform: 'Win32',
62-
userAgentMetadata: {
63-
brands: [
64-
{
65-
brand: 'Not/A)Brand',
66-
version: '99'
67-
},
68-
{
69-
brand: 'Chromium',
70-
version: process.versions.chrome.split('.')[0]
71-
}
72-
],
73-
fullVersionList: [
74-
{
75-
brand: 'Not/A)Brand',
76-
version: '99.0.0.0'
77-
},
78-
{
79-
brand: 'Chromium',
80-
version: process.versions.chrome
81-
}
82-
],
83-
platform: 'Windows',
84-
platformVersion: '10.0.0',
85-
architecture: 'x86',
86-
model: '',
87-
mobile: false,
88-
bitness: '64',
89-
wow64: false
90-
}
80+
await webContentsView.webContents.loadURL('data:text/html,<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>', {
81+
baseURLForDataURL: 'https://www.youtube.com/'
9182
})
9283

9384
await webContentsView.webContents.debugger.sendCommand('Emulation.setDeviceMetricsOverride', {
@@ -105,7 +96,7 @@ export async function generatePoToken(visitorData) {
10596
}
10697
})
10798

108-
const script = await getScript(visitorData)
99+
const script = await getScript(videoId, visitorData, context)
109100

110101
const response = await webContentsView.webContents.executeJavaScript(script)
111102

@@ -118,9 +109,11 @@ export async function generatePoToken(visitorData) {
118109
let cachedScript
119110

120111
/**
112+
* @param {string} videoId
121113
* @param {string} visitorData
114+
* @param {string} context
122115
*/
123-
async function getScript(visitorData) {
116+
async function getScript(videoId, visitorData, context) {
124117
if (!cachedScript) {
125118
const pathToScript = process.env.NODE_ENV === 'development'
126119
? join(__dirname, '../../dist/botGuardScript.js')
@@ -133,8 +126,8 @@ async function getScript(visitorData) {
133126

134127
const functionName = match[1]
135128

136-
cachedScript = content.replace(match[0], `;${functionName}("FT_VISITOR_DATA")`)
129+
cachedScript = content.replace(match[0], `;${functionName}(FT_PARAMS)`)
137130
}
138131

139-
return cachedScript.replace('FT_VISITOR_DATA', visitorData)
132+
return cachedScript.replace('FT_PARAMS', `"${videoId}","${visitorData}",${context}`)
140133
}

src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default defineComponent({
6565
},
6666
manifestSrc: {
6767
type: String,
68-
required: true
68+
default: null
6969
},
7070
manifestMimeType: {
7171
type: String,

src/renderer/helpers/api/invidious.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,9 @@ export function convertInvidiousToLocalFormat(format) {
859859
const [initStart, initEnd] = format.init.split('-')
860860
const [indexStart, indexEnd] = format.index.split('-')
861861

862-
const duration = parseInt(parseFloat(new URL(format.url).searchParams.get('dur')) * 1000)
862+
const url = new URL(format.url)
863+
864+
const duration = parseInt(parseFloat(url.searchParams.get('dur')) * 1000)
863865

864866
// only converts the properties that are needed to generate a DASH manifest with YouTube.js
865867
// audioQuality and qualityLabel don't go inside the DASH manifest, but are used by YouTube.js
@@ -896,6 +898,27 @@ export function convertInvidiousToLocalFormat(format) {
896898
})
897899
})
898900

901+
if (localFormat.has_audio && url.searchParams.has('xtags')) {
902+
const xtags = url.searchParams.get('xtags').split(':')
903+
904+
localFormat.language = xtags.find((tag) => tag.startsWith('lang='))?.split('=')[1] || null
905+
localFormat.is_drc = xtags.includes('drc=1')
906+
907+
const audioContent = xtags.find((tag) => tag.startsWith('acont='))?.split('=')[1]
908+
localFormat.is_dubbed = audioContent === 'dubbed'
909+
localFormat.is_descriptive = audioContent === 'descriptive'
910+
localFormat.is_secondary = audioContent === 'secondary'
911+
localFormat.is_auto_dubbed = audioContent === 'dubbed-auto'
912+
localFormat.is_original = audioContent === 'original' ||
913+
(
914+
!localFormat.is_dubbed &&
915+
!localFormat.is_descriptive &&
916+
!localFormat.is_secondary &&
917+
!localFormat.is_auto_dubbed &&
918+
!localFormat.is_drc
919+
)
920+
}
921+
899922
return localFormat
900923
}
901924

0 commit comments

Comments
 (0)