Skip to content
Draft
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
5 changes: 4 additions & 1 deletion src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,10 @@ function runApp() {
requestHeaders['Sec-Fetch-Site'] = 'same-origin'
requestHeaders['Sec-Fetch-Mode'] = 'same-origin'
requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false'
} else if (url.startsWith('https://www.youtube.com/watch')) {
} else if (
url.startsWith('https://www.youtube.com/watch') ||
(urlObj.origin === 'www.youtube.com' && urlObj.pathname === '/')
) {
delete requestHeaders.Referer
delete requestHeaders.Origin
requestHeaders['Sec-Fetch-Dest'] = 'document'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,7 @@ export default defineComponent({

/** @type {object[]} */
const legacyFormats = props.legacyFormats
if (legacyFormats.length === 0) { return }

const isPortrait = legacyFormats[0].height > legacyFormats[0].width

Expand Down Expand Up @@ -2830,7 +2831,7 @@ export default defineComponent({
forceAspectRatio.value = firstVariant.width / firstVariant.height < 1.5
}
})
} else {
} else if (props.legacyFormats.length > 0) {
// force the player aspect ratio to 16:9 to avoid overflowing the layout, when the video is too tall

const firstFormat = props.legacyFormats[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ export class LegacyQualitySelection extends shaka.ui.SettingsMenu {

const sortedLegacyFormats = [...legacyFormats]

const isPortrait = legacyFormats[0].height > legacyFormats[0].width
sortedLegacyFormats.sort((a, b) => isPortrait ? b.width - a.width : b.height - a.height)
if (legacyFormats.length > 0) {
const isPortrait = legacyFormats[0].height > legacyFormats[0].width
sortedLegacyFormats.sort((a, b) => isPortrait ? b.width - a.width : b.height - a.height)
}

/** @private */
this.legacyFormats_ = sortedLegacyFormats
Expand Down
90 changes: 61 additions & 29 deletions src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,12 @@ export async function getLocalSearchContinuation(continuationData) {
}

/**
* @param {string} videoId
* @param {(input, init) => Promise<Response>} fetchFunc
* @param {string} url
* @param {string} kind
*/
async function getWatchHTMLWatchPage(videoId, fetchFunc) {
async function getHTMLPage(url, kind) {
// This returns session/tracking cookies but they get removed in onHeadersReceived in the main process before they are saved by Electron
const htmlResponse = await fetch(`https://www.youtube.com/watch?v=${videoId}&bpctr=9999999999&has_verified=1`,
const htmlResponse = await fetch(url,
{
headers: {
// We need to be able to parse the localised strings in the /next response data (e.g. view counts and published dates)
Expand All @@ -398,7 +398,7 @@ async function getWatchHTMLWatchPage(videoId, fetchFunc) {
const ytConfigStr = htmlPage.match(/ytcfg\.set\(({.+?})\);/s)?.[1]
if (!ytConfigStr) {
// required for botguard
throw new Error('Could not find ytcfg in the HTML page')
throw new Error(`Could not find ytcfg in the ${kind} HTML page`)
}

const ytConfig = JSON.parse(ytConfigStr)
Expand All @@ -407,50 +407,82 @@ async function getWatchHTMLWatchPage(videoId, fetchFunc) {

if (!initialAttestationDataMatch) {
// required for botguard
throw new Error('Could not find challenge in the HTML page')
throw new Error(`Could not find challenge in the ${kind} HTML page`)
}

let initialAttestationData

try {
initialAttestationData = parseLooseJSON(initialAttestationDataMatch[1])
} catch (e) {
const error = new Error('Failed to parse the initial attestation data', { cause: e })
const error = new Error(`Failed to parse the ${kind} home page initial attestation data`, { cause: e })
console.error(error, initialAttestationDataMatch[1])
throw error
}

/** @type {string | undefined} */
let playerId = ytConfig.PLAYER_JS_URL?.match(/player\/([^/]+)\//)?.[1]

playerId ??= htmlPage.match(/<script[^>]+src="[^">]+player\/([^/]+)\/[^"]+\/base.js"/)?.[1]

const playerResponseStr = htmlPage.match(/ytInitialPlayerResponse\s*=\s*(\{.+?\});/)?.[1]
let playerResponse

// not fatal if it is missing as we can retrieve it from Innertube ourselves
if (playerResponseStr) {
try {
playerResponse = JSON.parse(playerResponseStr)
} catch (e) {
console.warn('/player response extracted from the HTML page is invalid JSON', e)
}
} else {
console.warn('Could not find /player response in the HTML page')
return {
htmlPage,
ytConfig,
initialAttestationData,
playerId
}
}

const nextResponseStr = htmlPage.match(/(?:window\s*\[\s*["']ytInitialData["']\s*\]|ytInitialData)\s*=\s*(\{.+?\});/)?.[1]
let nextResponse
/**
* @param {string} videoId
* @param {(input, init) => Promise<Response>} fetchFunc
*/
async function getWatchHTMLWatchPage(videoId, fetchFunc) {
let htmlPage, ytConfig, initialAttestationData

// not fatal if it is missing as we can retrieve it from Innertube ourselves
if (nextResponseStr) {
let playerResponse, nextResponse
/** @type {string | undefined} */
let playerId

if (sessionStorage.getItem('playerHtmlHomepageFallback') === '1') {
({ ytConfig, initialAttestationData, playerId } = await getHTMLPage('https://www.youtube.com', 'home'))
} else {
try {
nextResponse = JSON.parse(nextResponseStr)
} catch (e) {
console.warn('/next response extracted from the HTML page is invalid JSON', e)
({ htmlPage, ytConfig, initialAttestationData, playerId } = await getHTMLPage(`https://www.youtube.com/watch?v=${videoId}&bpctr=9999999999&has_verified=1`, 'watch'))

const playerResponseStr = htmlPage.match(/ytInitialPlayerResponse\s*=\s*(\{.+?\});/)?.[1]

// not fatal if it is missing as we can retrieve it from Innertube ourselves
if (playerResponseStr) {
try {
playerResponse = JSON.parse(playerResponseStr)
} catch (e) {
console.warn('/player response extracted from the HTML page is invalid JSON', e)
}
} else {
console.warn('Could not find /player response in the HTML page')
}

const nextResponseStr = htmlPage.match(/(?:window\s*\[\s*["']ytInitialData["']\s*\]|ytInitialData)\s*=\s*(\{.+?\});/)?.[1]

// not fatal if it is missing as we can retrieve it from Innertube ourselves
if (nextResponseStr) {
try {
nextResponse = JSON.parse(nextResponseStr)
} catch (e) {
console.warn('/next response extracted from the HTML page is invalid JSON', e)
}
} else {
console.warn('Could not find /next response in the HTML page')
}
} catch (watchError) {
console.warn('Falling back to the YT home page for the rest of the session because of:', watchError)

// Fall back to the home page for the rest of the session/until FreeTube is restarted
// assuming that getting the watch page captcha once is a sign that it will continue happening
sessionStorage.setItem('playerHtmlHomepageFallback', '1');

({ ytConfig, initialAttestationData, playerId } = await getHTMLPage('https://www.youtube.com', 'home'))
}
} else {
console.warn('Could not find /next response in the HTML page')
}

const session = buildSessionFromYtConfig(ytConfig, fetchFunc)
Expand Down
10 changes: 10 additions & 0 deletions src/renderer/views/Watch/Watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,12 @@ export default defineComponent({
}
}

if (this.activeFormat === 'legacy' && (this.isLive || this.isPostLiveDvr || this.legacyFormats.length === 0)) {
// Legacy wanted as default but unavailable
showToast(this.t('Change Format.Legacy formats are not available for this video'))
this.handleActiveFormatUnavailable()
}

this.isLoading = false
this.updateTitle()
} catch (err) {
Expand Down Expand Up @@ -1602,6 +1608,10 @@ export default defineComponent({
}
}

this.handleActiveFormatUnavailable()
},

handleActiveFormatUnavailable: function() {
if (this.isLive || this.isPostLiveDvr) {
// live streams don't have legacy formats, so only switch between dash and audio

Expand Down