Skip to content

Commit

Permalink
Support authentication with the Basic scheme for Invidious instances (#…
Browse files Browse the repository at this point in the history
…5569)

* Support authentication with the Basic scheme for Invidious instances

* I forgot to change this line
  • Loading branch information
absidue authored Aug 23, 2024
1 parent fb4a4d1 commit 579d6d3
Show file tree
Hide file tree
Showing 24 changed files with 262 additions and 130 deletions.
4 changes: 3 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ const IpcChannels = {
SHOW_VIDEO_STATISTICS: 'show-video-statistics',

PLAYER_CACHE_GET: 'player-cache-get',
PLAYER_CACHE_SET: 'player-cache-set'
PLAYER_CACHE_SET: 'player-cache-set',

SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization'
}

const DBActions = {
Expand Down
132 changes: 91 additions & 41 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,19 @@ function runApp() {
sameSite: 'no_restriction',
})

// make InnerTube requests work with the fetch function
// InnerTube rejects requests if the referer isn't YouTube or empty
const innertubeAndMediaRequestFilter = { urls: ['https://www.youtube.com/youtubei/*', 'https://*.googlevideo.com/videoplayback?*'] }

session.defaultSession.webRequest.onBeforeSendHeaders(innertubeAndMediaRequestFilter, ({ requestHeaders, url, resourceType }, callback) => {
requestHeaders.Referer = 'https://www.youtube.com/'
requestHeaders.Origin = 'https://www.youtube.com'
const onBeforeSendHeadersRequestFilter = {
urls: ['https://*/*', 'http://*/*'],
types: ['xhr', 'media', 'image']
}
session.defaultSession.webRequest.onBeforeSendHeaders(onBeforeSendHeadersRequestFilter, ({ requestHeaders, url, resourceType, webContents }, callback) => {
const urlObj = new URL(url)

if (url.startsWith('https://www.youtube.com/youtubei/')) {
// make InnerTube requests work with the fetch function
// InnerTube rejects requests if the referer isn't YouTube or empty
requestHeaders.Referer = 'https://www.youtube.com/'
requestHeaders.Origin = 'https://www.youtube.com'

// Make iOS requests work and look more realistic
if (requestHeaders['x-youtube-client-name'] === '5') {
delete requestHeaders.Referer
Expand All @@ -430,41 +434,50 @@ function runApp() {
requestHeaders['Sec-Fetch-Mode'] = 'same-origin'
requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false'
}
} else {
} else if (urlObj.origin.endsWith('.googlevideo.com') && urlObj.pathname === '/videoplayback') {
requestHeaders.Referer = 'https://www.youtube.com/'
requestHeaders.Origin = 'https://www.youtube.com'

// YouTube doesn't send the Content-Type header for the media requests, so we shouldn't either
delete requestHeaders['Content-Type']
}

// YouTube throttles the adaptive formats if you request a chunk larger than 10MiB.
// For the DASH formats we are fine as video.js doesn't seem to ever request chunks that big.
// The legacy formats don't have any chunk size limits.
// For the audio formats we need to handle it ourselves, as the browser requests the entire audio file,
// which means that for most videos that are longer than 10 mins, we get throttled, as the audio track file sizes surpass that 10MiB limit.
// YouTube throttles the adaptive formats if you request a chunk larger than 10MiB.
// For the DASH formats we are fine as video.js doesn't seem to ever request chunks that big.
// The legacy formats don't have any chunk size limits.
// For the audio formats we need to handle it ourselves, as the browser requests the entire audio file,
// which means that for most videos that are longer than 10 mins, we get throttled, as the audio track file sizes surpass that 10MiB limit.

// This code checks if the file is larger than the limit, by checking the `clen` query param,
// which YouTube helpfully populates with the content length for us.
// If it does surpass that limit, it then checks if the requested range is larger than the limit
// (seeking right at the end of the video, would result in a small enough range to be under the chunk limit)
// if that surpasses the limit too, it then limits the requested range to 10MiB, by setting the range to `start-${start + 10MiB}`.
if (resourceType === 'media' && url.includes('&mime=audio') && requestHeaders.Range) {
const TEN_MIB = 10 * 1024 * 1024
// This code checks if the file is larger than the limit, by checking the `clen` query param,
// which YouTube helpfully populates with the content length for us.
// If it does surpass that limit, it then checks if the requested range is larger than the limit
// (seeking right at the end of the video, would result in a small enough range to be under the chunk limit)
// if that surpasses the limit too, it then limits the requested range to 10MiB, by setting the range to `start-${start + 10MiB}`.
if (resourceType === 'media' && urlObj.searchParams.get('mime')?.startsWith('audio/') && requestHeaders.Range) {
const TEN_MIB = 10 * 1024 * 1024

const contentLength = parseInt(new URL(url).searchParams.get('clen'))
const contentLength = parseInt(new URL(url).searchParams.get('clen'))

if (contentLength > TEN_MIB) {
const [startStr, endStr] = requestHeaders.Range.split('=')[1].split('-')
if (contentLength > TEN_MIB) {
const [startStr, endStr] = requestHeaders.Range.split('=')[1].split('-')

const start = parseInt(startStr)
const start = parseInt(startStr)

// handle open ended ranges like `0-` and `1234-`
const end = endStr.length === 0 ? contentLength : parseInt(endStr)
// handle open ended ranges like `0-` and `1234-`
const end = endStr.length === 0 ? contentLength : parseInt(endStr)

if (end - start > TEN_MIB) {
const newEnd = start + TEN_MIB
if (end - start > TEN_MIB) {
const newEnd = start + TEN_MIB

requestHeaders.Range = `bytes=${start}-${newEnd}`
requestHeaders.Range = `bytes=${start}-${newEnd}`
}
}
}
} else if (webContents) {
const invidiousAuthorization = invidiousAuthorizations.get(webContents.id)

if (invidiousAuthorization && url.startsWith(invidiousAuthorization.url)) {
requestHeaders.Authorization = invidiousAuthorization.authorization
}
}

// eslint-disable-next-line n/no-callback-literal
Expand All @@ -488,8 +501,10 @@ function runApp() {
const imageCache = new ImageCache()

protocol.handle('imagecache', (request) => {
const [requestUrl, rawWebContentsId] = request.url.split('#')

return new Promise((resolve, reject) => {
const url = decodeURIComponent(request.url.substring(13))
const url = decodeURIComponent(requestUrl.substring(13))
if (imageCache.has(url)) {
const cached = imageCache.get(url)

Expand All @@ -499,9 +514,22 @@ function runApp() {
return
}

let headers

if (rawWebContentsId) {
const invidiousAuthorization = invidiousAuthorizations.get(parseInt(rawWebContentsId))

if (invidiousAuthorization && url.startsWith(invidiousAuthorization.url)) {
headers = {
Authorization: invidiousAuthorization.authorization
}
}
}

const newRequest = net.request({
method: request.method,
url
url,
headers
})

// Electron doesn't allow certain headers to be set:
Expand Down Expand Up @@ -548,19 +576,20 @@ function runApp() {
})
})

const imageRequestFilter = { urls: ['https://*/*', 'http://*/*'] }
const imageRequestFilter = { urls: ['https://*/*', 'http://*/*'], types: ['image'] }
session.defaultSession.webRequest.onBeforeRequest(imageRequestFilter, (details, callback) => {
// the requests made by the imagecache:// handler to fetch the image,
// are allowed through, as their resourceType is 'other'
if (details.resourceType === 'image') {
// eslint-disable-next-line n/no-callback-literal
callback({
redirectURL: `imagecache://${encodeURIComponent(details.url)}`
})
} else {
// eslint-disable-next-line n/no-callback-literal
callback({})

let redirectURL = `imagecache://${encodeURIComponent(details.url)}`

if (details.webContents) {
redirectURL += `#${details.webContents.id}`
}

callback({
redirectURL
})
})

// --- end of `if experimentsDisableDiskCache` ---
Expand Down Expand Up @@ -1011,6 +1040,21 @@ function runApp() {
await asyncFs.writeFile(filePath, new Uint8Array(value))
})

/** @type {Map<number, { url: string, authorization: string }>} */
const invidiousAuthorizations = new Map()

ipcMain.on(IpcChannels.SET_INVIDIOUS_AUTHORIZATION, (event, authorization, url) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}

if (!authorization) {
invidiousAuthorizations.delete(event.sender.id)
} else if (typeof authorization === 'string' && typeof url === 'string') {
invidiousAuthorizations.set(event.sender.id, { authorization, url })
}
})

// ************************************************* //
// DB related IPC calls
// *********** //
Expand Down Expand Up @@ -1376,6 +1420,12 @@ function runApp() {
}
})

app.on('web-contents-created', (_, webContents) => {
webContents.once('destroyed', () => {
invidiousAuthorizations.delete(webContents.id)
})
})

/*
* Check if an argument was passed and send it over to the GUI (Linux / Windows).
* Remove freetube:// protocol if present
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/ft-list-channel/ft-list-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export default defineComponent({
}
},
computed: {
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
listType: function () {
return this.$store.getters.getListType
Expand Down Expand Up @@ -81,7 +81,7 @@ export default defineComponent({
// Can be prefixed with `https://` or `//` (protocol relative)
const thumbnailUrl = this.data.authorThumbnails[2].url

this.thumbnail = youtubeImageUrlToInvidious(thumbnailUrl, this.currentInvidiousInstance)
this.thumbnail = youtubeImageUrlToInvidious(thumbnailUrl, this.currentInvidiousInstanceUrl)

this.channelName = this.data.author
this.id = this.data.authorId
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/components/ft-list-playlist/ft-list-playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},

quickBookmarkPlaylistId() {
Expand Down Expand Up @@ -131,7 +131,7 @@ export default defineComponent({
parseInvidiousData: function () {
this.title = this.data.title
if (this.thumbnailCanBeShown) {
this.thumbnail = this.data.playlistThumbnail.replace('https://i.ytimg.com', this.currentInvidiousInstance).replace('hqdefault', 'mqdefault')
this.thumbnail = this.data.playlistThumbnail.replace('https://i.ytimg.com', this.currentInvidiousInstanceUrl).replace('hqdefault', 'mqdefault')
}
this.channelName = this.data.author
this.channelId = this.data.authorId
Expand Down Expand Up @@ -159,7 +159,7 @@ export default defineComponent({
if (this.thumbnailCanBeShown && this.data.videos.length > 0) {
const thumbnailURL = `https://i.ytimg.com/vi/${this.data.videos[0].videoId}/mqdefault.jpg`
if (this.backendPreference === 'invidious') {
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstance)
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstanceUrl)
} else {
this.thumbnail = thumbnailURL
}
Expand Down
10 changes: 5 additions & 5 deletions src/renderer/components/ft-list-video/ft-list-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ export default defineComponent({
return this.$store.getters.getBackendPreference
},

currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},

showPlaylists: function () {
Expand Down Expand Up @@ -182,7 +182,7 @@ export default defineComponent({
},

invidiousUrl: function () {
let videoUrl = `${this.currentInvidiousInstance}/watch?v=${this.id}`
let videoUrl = `${this.currentInvidiousInstanceUrl}/watch?v=${this.id}`
// `playlistId` can be undefined
if (this.playlistSharable) {
// `index` seems can be ignored
Expand All @@ -192,7 +192,7 @@ export default defineComponent({
},

invidiousChannelUrl: function () {
return `${this.currentInvidiousInstance}/channel/${this.channelId}`
return `${this.currentInvidiousInstanceUrl}/channel/${this.channelId}`
},

youtubeUrl: function () {
Expand Down Expand Up @@ -338,7 +338,7 @@ export default defineComponent({

let baseUrl
if (this.backendPreference === 'invidious') {
baseUrl = this.currentInvidiousInstance
baseUrl = this.currentInvidiousInstanceUrl
} else {
baseUrl = 'https://i.ytimg.com'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
toBeAddedToPlaylistVideoList: function () {
return this.$store.getters.getToBeAddedToPlaylistVideoList
Expand Down Expand Up @@ -129,7 +129,7 @@ export default defineComponent({
if (this.playlist.videos.length > 0) {
const thumbnailURL = `https://i.ytimg.com/vi/${this.playlist.videos[0].videoId}/mqdefault.jpg`
if (this.backendPreference === 'invidious') {
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstance)
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstanceUrl)
} else {
this.thumbnail = thumbnailURL
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
profileList: function () {
return this.$store.getters.getProfileList
Expand Down Expand Up @@ -76,7 +76,7 @@ export default defineComponent({
})
subscriptions.forEach((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
})
Expand All @@ -92,7 +92,7 @@ export default defineComponent({
})
subscriptions.forEach((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
profileList: function () {
return this.$store.getters.getProfileList
Expand Down Expand Up @@ -71,7 +71,7 @@ export default defineComponent({
return index === -1
}).map((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
return channel
Expand All @@ -92,7 +92,7 @@ export default defineComponent({
return index === -1
}).map((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
return channel
Expand Down
Loading

0 comments on commit 579d6d3

Please sign in to comment.