diff --git a/src/constants.js b/src/constants.js index 7fc27d8e342ff..cf50e91b83a72 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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 = { diff --git a/src/main/index.js b/src/main/index.js index a2bdaeb8237f8..5defe0157f526 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -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 @@ -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 @@ -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) @@ -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: @@ -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` --- @@ -1011,6 +1040,21 @@ function runApp() { await asyncFs.writeFile(filePath, new Uint8Array(value)) }) + /** @type {Map} */ + 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 // *********** // @@ -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 diff --git a/src/renderer/components/ft-list-channel/ft-list-channel.js b/src/renderer/components/ft-list-channel/ft-list-channel.js index 8e94cb37df68a..709c34cf149c4 100644 --- a/src/renderer/components/ft-list-channel/ft-list-channel.js +++ b/src/renderer/components/ft-list-channel/ft-list-channel.js @@ -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 @@ -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 diff --git a/src/renderer/components/ft-list-playlist/ft-list-playlist.js b/src/renderer/components/ft-list-playlist/ft-list-playlist.js index b88747cd638c9..98747cef34c23 100644 --- a/src/renderer/components/ft-list-playlist/ft-list-playlist.js +++ b/src/renderer/components/ft-list-playlist/ft-list-playlist.js @@ -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() { @@ -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 @@ -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 } diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js index cc91ac027bf55..c90e1fa9c8f75 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -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 () { @@ -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 @@ -192,7 +192,7 @@ export default defineComponent({ }, invidiousChannelUrl: function () { - return `${this.currentInvidiousInstance}/channel/${this.channelId}` + return `${this.currentInvidiousInstanceUrl}/channel/${this.channelId}` }, youtubeUrl: function () { @@ -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' } diff --git a/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js b/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js index 3016a10a4125a..825ddc1119abb 100644 --- a/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js +++ b/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js @@ -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 @@ -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 } diff --git a/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js b/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js index 6166401f85374..c9b8590089209 100644 --- a/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js +++ b/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js @@ -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 @@ -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 }) @@ -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 }) diff --git a/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js b/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js index 9108ff4d1dea8..57beae3b25901 100644 --- a/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js +++ b/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js @@ -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 @@ -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 @@ -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 diff --git a/src/renderer/components/ft-share-button/ft-share-button.js b/src/renderer/components/ft-share-button/ft-share-button.js index 054e3e58ab3cc..6d9eb6f73f149 100644 --- a/src/renderer/components/ft-share-button/ft-share-button.js +++ b/src/renderer/components/ft-share-button/ft-share-button.js @@ -68,8 +68,8 @@ export default defineComponent({ return this.$t('Share.Share Video') }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, selectedUserPlaylist: function () { @@ -86,12 +86,12 @@ export default defineComponent({ invidiousURL() { if (this.isChannel) { - return `${this.currentInvidiousInstance}/channel/${this.id}` + return `${this.currentInvidiousInstanceUrl}/channel/${this.id}` } if (this.isPlaylist) { - return `${this.currentInvidiousInstance}/playlist?list=${this.id}` + return `${this.currentInvidiousInstanceUrl}/playlist?list=${this.id}` } - 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 @@ -102,9 +102,9 @@ export default defineComponent({ invidiousEmbedURL() { if (this.isPlaylist) { - return `${this.currentInvidiousInstance}/embed/videoseries?list=${this.id}` + return `${this.currentInvidiousInstanceUrl}/embed/videoseries?list=${this.id}` } - return `${this.currentInvidiousInstance}/embed/${this.id}` + return `${this.currentInvidiousInstanceUrl}/embed/${this.id}` }, youtubeChannelUrl() { diff --git a/src/renderer/components/ft-video-player/ft-video-player.js b/src/renderer/components/ft-video-player/ft-video-player.js index dc7c463822592..77b919985ef2b 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.js +++ b/src/renderer/components/ft-video-player/ft-video-player.js @@ -40,6 +40,9 @@ videojs.Vhs.xhr.beforeRequest = (options) => { const { uri } = options options.uri = getProxyUrl(uri) } + + const authorization = store.getters.getCurrentInvidiousInstanceAuthorization + // pass in the optional base so it doesn't error for `dashFiles/videoId.xml` (DASH manifest in dev mode) if (new URL(options.uri, window.location.origin).hostname.endsWith('.googlevideo.com')) { // The official clients use POST requests with this body for the DASH requests, so we should do that too @@ -50,6 +53,14 @@ videojs.Vhs.xhr.beforeRequest = (options) => { options.uri += `&range=${options.headers.Range.split('=')[1]}` delete options.headers.Range } + } else if (authorization && options.uri.startsWith(store.getters.getCurrentInvidiousInstanceUrl)) { + if (options.headers) { + options.headers.Authorization = authorization + } else { + options.headers = { + Authorization: authorization + } + } } } // videojs-http-streaming spits out a warning every time you access videojs.Vhs.BANDWIDTH_VARIANCE diff --git a/src/renderer/components/playlist-info/playlist-info.js b/src/renderer/components/playlist-info/playlist-info.js index 5d7dac11a5148..6f9ff99bcfd99 100644 --- a/src/renderer/components/playlist-info/playlist-info.js +++ b/src/renderer/components/playlist-info/playlist-info.js @@ -124,8 +124,8 @@ export default defineComponent({ return this.$store.getters.getHideSharingActions }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, historyCacheById: function () { @@ -204,7 +204,7 @@ export default defineComponent({ let baseUrl = 'https://i.ytimg.com' if (this.backendPreference === 'invidious') { - baseUrl = this.currentInvidiousInstance + baseUrl = this.currentInvidiousInstanceUrl } else if (typeof this.playlistThumbnail === 'string' && this.playlistThumbnail.length > 0) { // Use playlist thumbnail provided by YT when available return this.playlistThumbnail diff --git a/src/renderer/components/side-nav/side-nav.js b/src/renderer/components/side-nav/side-nav.js index a68ac2e686c47..dd050476b5839 100644 --- a/src/renderer/components/side-nav/side-nav.js +++ b/src/renderer/components/side-nav/side-nav.js @@ -20,8 +20,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 @@ -47,7 +47,7 @@ export default defineComponent({ if (this.backendPreference === 'invidious') { subscriptions.forEach((channel) => { - channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance) + channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl) }) } diff --git a/src/renderer/components/subscriptions-community/subscriptions-community.js b/src/renderer/components/subscriptions-community/subscriptions-community.js index e42510c49111f..59df38efcb570 100644 --- a/src/renderer/components/subscriptions-community/subscriptions-community.js +++ b/src/renderer/components/subscriptions-community/subscriptions-community.js @@ -28,8 +28,8 @@ export default defineComponent({ return this.$store.getters.getBackendFallback }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, activeProfile: function () { @@ -167,8 +167,8 @@ export default defineComponent({ if (thumbnailUrl) { if (thumbnailUrl.startsWith('//')) { thumbnailUrl = 'https:' + thumbnailUrl - } else if (thumbnailUrl.startsWith(`${this.currentInvidiousInstance}/ggpht`)) { - thumbnailUrl = thumbnailUrl.replace(`${this.currentInvidiousInstance}/ggpht`, 'https://yt3.googleusercontent.com') + } else if (thumbnailUrl.startsWith(`${this.currentInvidiousInstanceUrl}/ggpht`)) { + thumbnailUrl = thumbnailUrl.replace(`${this.currentInvidiousInstanceUrl}/ggpht`, 'https://yt3.googleusercontent.com') } } diff --git a/src/renderer/components/subscriptions-live/subscriptions-live.js b/src/renderer/components/subscriptions-live/subscriptions-live.js index f556559f55d05..625da4a0096a6 100644 --- a/src/renderer/components/subscriptions-live/subscriptions-live.js +++ b/src/renderer/components/subscriptions-live/subscriptions-live.js @@ -3,7 +3,7 @@ import { mapActions, mapMutations } from 'vuex' import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue' import { setPublishedTimestampsInvidious, copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils' -import { invidiousAPICall } from '../../helpers/api/invidious' +import { invidiousAPICall, invidiousFetch } from '../../helpers/api/invidious' import { getLocalChannelLiveStreams } from '../../helpers/api/local' import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions' @@ -29,8 +29,8 @@ export default defineComponent({ return this.$store.getters.getBackendFallback }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, useRssFeeds: function () { @@ -354,10 +354,10 @@ export default defineComponent({ getChannelLiveInvidiousRSS: async function (channel, failedAttempts = 0) { const playlistId = channel.id.replace('UC', 'UULV') - const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}` + const feedUrl = `${this.currentInvidiousInstanceUrl}/feed/playlist/${playlistId}` try { - const response = await fetch(feedUrl) + const response = await invidiousFetch(feedUrl) if (response.status === 500 || response.status === 404) { return { diff --git a/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js b/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js index f24ae77c49ef3..317e54b2a5416 100644 --- a/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js +++ b/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js @@ -4,6 +4,7 @@ import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions' import { copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils' +import { invidiousFetch } from '../../helpers/api/invidious' export default defineComponent({ name: 'SubscriptionsShorts', @@ -27,8 +28,8 @@ export default defineComponent({ return this.$store.getters.getBackendFallback }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, lastShortRefreshTimestamp: function () { @@ -231,10 +232,10 @@ export default defineComponent({ getChannelShortsInvidious: async function (channel, failedAttempts = 0) { const playlistId = channel.id.replace('UC', 'UUSH') - const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}` + const feedUrl = `${this.currentInvidiousInstanceUrl}/feed/playlist/${playlistId}` try { - const response = await fetch(feedUrl) + const response = await invidiousFetch(feedUrl) if (response.status === 500 || response.status === 404) { return { diff --git a/src/renderer/components/subscriptions-videos/subscriptions-videos.js b/src/renderer/components/subscriptions-videos/subscriptions-videos.js index e728668537665..395c591756565 100644 --- a/src/renderer/components/subscriptions-videos/subscriptions-videos.js +++ b/src/renderer/components/subscriptions-videos/subscriptions-videos.js @@ -3,7 +3,7 @@ import { mapActions, mapMutations } from 'vuex' import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue' import { setPublishedTimestampsInvidious, copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils' -import { invidiousAPICall } from '../../helpers/api/invidious' +import { invidiousAPICall, invidiousFetch } from '../../helpers/api/invidious' import { getLocalChannelVideos } from '../../helpers/api/local' import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions' @@ -29,8 +29,8 @@ export default defineComponent({ return this.$store.getters.getBackendFallback }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, currentLocale: function () { @@ -355,10 +355,10 @@ export default defineComponent({ getChannelVideosInvidiousRSS: async function (channel, failedAttempts = 0) { const playlistId = channel.id.replace('UC', 'UULF') - const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}` + const feedUrl = `${this.currentInvidiousInstanceUrl}/feed/playlist/${playlistId}` try { - const response = await fetch(feedUrl) + const response = await invidiousFetch(feedUrl) if (response.status === 500 || response.status === 404) { this.errorChannels.push(channel) diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index be3d163655817..c0d5400cdb04a 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -71,10 +71,6 @@ export default defineComponent({ return this.$store.getters.getBarColor }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance - }, - backendFallback: function () { return this.$store.getters.getBackendFallback }, diff --git a/src/renderer/helpers/api/invidious.js b/src/renderer/helpers/api/invidious.js index 05898068a0b3f..a842edfb884d7 100644 --- a/src/renderer/helpers/api/invidious.js +++ b/src/renderer/helpers/api/invidious.js @@ -4,12 +4,12 @@ import { isNullOrEmpty } from '../strings' import autolinker from 'autolinker' import { FormatUtils, Misc, Player } from 'youtubei.js' -function getCurrentInstance() { - return store.getters.getCurrentInvidiousInstance +function getCurrentInstanceUrl() { + return store.getters.getCurrentInvidiousInstanceUrl } export function getProxyUrl(uri) { - const currentInstance = getCurrentInstance() + const currentInstance = getCurrentInstanceUrl() const url = new URL(uri) const { origin } = url @@ -20,10 +20,24 @@ export function getProxyUrl(uri) { return url.toString().replace(origin, currentInstance) } +export function invidiousFetch(url) { + const authorization = store.getters.getCurrentInvidiousInstanceAuthorization + + if (authorization) { + return fetch(url, { + headers: { + Authorization: authorization + } + }) + } else { + return fetch(url) + } +} + export function invidiousAPICall({ resource, id = '', params = {}, doLogError = true, subResource = '' }) { return new Promise((resolve, reject) => { - const requestUrl = getCurrentInstance() + '/api/v1/' + resource + '/' + id + (!isNullOrEmpty(subResource) ? `/${subResource}` : '') + '?' + new URLSearchParams(params).toString() - fetch(requestUrl) + const requestUrl = getCurrentInstanceUrl() + '/api/v1/' + resource + '/' + id + (!isNullOrEmpty(subResource) ? `/${subResource}` : '') + '?' + new URLSearchParams(params).toString() + invidiousFetch(requestUrl) .then((response) => response.json()) .then((json) => { if (json.error !== undefined) { @@ -126,7 +140,7 @@ export function youtubeImageUrlToInvidious(url, currentInstance = null) { } if (currentInstance === null) { - currentInstance = getCurrentInstance() + currentInstance = getCurrentInstanceUrl() } // Can be prefixed with `https://` or `//` (protocol relative) if (url.startsWith('//')) { @@ -149,7 +163,7 @@ function parseInvidiousCommentData(response) { comment.authorLink = comment.authorId comment.authorThumb = youtubeImageUrlToInvidious(comment.authorThumbnails.at(-1).url) comment.likes = comment.likeCount - comment.text = autolinker.link(stripHTML(invidiousImageUrlToInvidious(comment.contentHtml, getCurrentInstance()))) + comment.text = autolinker.link(stripHTML(invidiousImageUrlToInvidious(comment.contentHtml, getCurrentInstanceUrl()))) comment.dataType = 'invidious' comment.isOwner = comment.authorIsChannelOwner comment.numReplies = comment.replies?.replyCount ?? 0 diff --git a/src/renderer/helpers/utils.js b/src/renderer/helpers/utils.js index f65161b4a60f4..d924a24a7a302 100644 --- a/src/renderer/helpers/utils.js +++ b/src/renderer/helpers/utils.js @@ -877,3 +877,13 @@ export function ctrlFHandler(event, inputElement) { export function randomArrayItem(array) { return array[Math.floor(Math.random() * array.length)] } + +/** + * @param {string} text + */ +export function base64EncodeUtf8(text) { + const bytes = new TextEncoder().encode(text) + + const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('') + return btoa(binString) +} diff --git a/src/renderer/store/modules/invidious.js b/src/renderer/store/modules/invidious.js index 2116b0085cec9..3ac2d59dec814 100644 --- a/src/renderer/store/modules/invidious.js +++ b/src/renderer/store/modules/invidious.js @@ -1,7 +1,10 @@ -import { createWebURL, fetchWithTimeout, randomArrayItem } from '../../helpers/utils' +import { IpcChannels } from '../../../constants' +import { base64EncodeUtf8, createWebURL, fetchWithTimeout, randomArrayItem } from '../../helpers/utils' const state = { currentInvidiousInstance: '', + currentInvidiousInstanceAuthorization: null, + currentInvidiousInstanceUrl: '', invidiousInstancesList: null } @@ -10,6 +13,14 @@ const getters = { return state.currentInvidiousInstance }, + getCurrentInvidiousInstanceUrl(state) { + return state.currentInvidiousInstanceUrl + }, + + getCurrentInvidiousInstanceAuthorization(state) { + return state.currentInvidiousInstanceAuthorization + }, + getInvidiousInstancesList(state) { return state.invidiousInstancesList } @@ -67,6 +78,42 @@ const actions = { const mutations = { setCurrentInvidiousInstance(state, value) { state.currentInvidiousInstance = value + + let url + try { + url = new URL(value) + } catch { } + + let authorization = null + + if (url && (url.username.length > 0 || url.password.length > 0)) { + authorization = `Basic ${base64EncodeUtf8(`${url.username}:${url.password}`)}` + } + + state.currentInvidiousInstanceAuthorization = authorization + + let instanceUrl + + if (url && authorization) { + url.username = '' + url.password = '' + + instanceUrl = url.toString().replace(/\/$/, '') + } else { + instanceUrl = value + } + + state.currentInvidiousInstanceUrl = instanceUrl + + if (process.env.IS_ELECTRON) { + const { ipcRenderer } = require('electron') + + if (authorization) { + ipcRenderer.send(IpcChannels.SET_INVIDIOUS_AUTHORIZATION, authorization, instanceUrl) + } else { + ipcRenderer.send(IpcChannels.SET_INVIDIOUS_AUTHORIZATION, null) + } + } }, setInvidiousInstancesList(state, value) { diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js index 20ca20794ca4d..8116dbb705fe6 100644 --- a/src/renderer/views/Channel/Channel.js +++ b/src/renderer/views/Channel/Channel.js @@ -153,8 +153,8 @@ export default defineComponent({ return this.$store.getters.getShowFamilyFriendlyOnly }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, activeProfile: function () { @@ -976,7 +976,7 @@ export default defineComponent({ this.isFamilyFriendly = response.isFamilyFriendly this.subCount = response.subCount const thumbnail = response.authorThumbnails[3].url - this.thumbnailUrl = youtubeImageUrlToInvidious(thumbnail, this.currentInvidiousInstance) + this.thumbnailUrl = youtubeImageUrlToInvidious(thumbnail, this.currentInvidiousInstanceUrl) this.updateSubscriptionDetails({ channelThumbnailUrl: thumbnail, channelName: channelName, channelId: channelId }) this.description = autolinker.link(response.description) this.viewCount = response.totalViews @@ -987,12 +987,12 @@ export default defineComponent({ return { name: channel.author, id: channel.authorId, - thumbnailUrl: youtubeImageUrlToInvidious(thumbnailUrl, this.currentInvidiousInstance) + thumbnailUrl: youtubeImageUrlToInvidious(thumbnailUrl, this.currentInvidiousInstanceUrl) } }) if (response.authorBanners instanceof Array && response.authorBanners.length > 0) { - this.bannerUrl = youtubeImageUrlToInvidious(response.authorBanners[0].url, this.currentInvidiousInstance) + this.bannerUrl = youtubeImageUrlToInvidious(response.authorBanners[0].url, this.currentInvidiousInstanceUrl) } else { this.bannerUrl = null } diff --git a/src/renderer/views/Playlist/Playlist.js b/src/renderer/views/Playlist/Playlist.js index 5589067b9cbd6..fc7c8c798b431 100644 --- a/src/renderer/views/Playlist/Playlist.js +++ b/src/renderer/views/Playlist/Playlist.js @@ -87,8 +87,8 @@ export default defineComponent({ backendFallback: function () { return this.$store.getters.getBackendFallback }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, userPlaylistSortOrder: function () { return this.$store.getters.getUserPlaylistSortOrder @@ -356,7 +356,7 @@ export default defineComponent({ this.viewCount = result.viewCount this.videoCount = result.videoCount this.channelName = result.author - this.channelThumbnail = youtubeImageUrlToInvidious(result.authorThumbnails[2].url, this.currentInvidiousInstance) + this.channelThumbnail = youtubeImageUrlToInvidious(result.authorThumbnails[2].url, this.currentInvidiousInstanceUrl) this.channelId = result.authorId this.infoSource = 'invidious' diff --git a/src/renderer/views/SubscribedChannels/SubscribedChannels.js b/src/renderer/views/SubscribedChannels/SubscribedChannels.js index a570375098934..797e379ebe114 100644 --- a/src/renderer/views/SubscribedChannels/SubscribedChannels.js +++ b/src/renderer/views/SubscribedChannels/SubscribedChannels.js @@ -63,8 +63,8 @@ export default defineComponent({ return this.$store.getters.getBackendPreference }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl } }, watch: { @@ -119,13 +119,13 @@ export default defineComponent({ const hostname = new URL(newURL).hostname if (hostname === 'yt3.ggpht.com' || hostname === 'yt3.googleusercontent.com') { if (this.backendPreference === 'invidious') { // YT to IV - newURL = youtubeImageUrlToInvidious(newURL, this.currentInvidiousInstance) + newURL = youtubeImageUrlToInvidious(newURL, this.currentInvidiousInstanceUrl) } } else { if (this.backendPreference === 'local') { // IV to YT newURL = newURL.replace(this.re.ivToYt, `${this.ytBaseURL}/$1`) } else { // IV to IV - newURL = invidiousImageUrlToInvidious(newURL, this.currentInvidiousInstance) + newURL = invidiousImageUrlToInvidious(newURL, this.currentInvidiousInstanceUrl) } } diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index f4a86094b2f40..f676cd3229e2c 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -31,6 +31,7 @@ import { convertInvidiousToLocalFormat, filterInvidiousFormats, generateInvidiousDashManifestLocally, + invidiousFetch, invidiousGetVideoInformation, youtubeImageUrlToInvidious } from '../../helpers/api/invidious' @@ -150,8 +151,8 @@ export default defineComponent({ backendFallback: function () { return this.$store.getters.getBackendFallback }, - currentInvidiousInstance: function () { - return this.$store.getters.getCurrentInvidiousInstance + currentInvidiousInstanceUrl: function () { + return this.$store.getters.getCurrentInvidiousInstanceUrl }, proxyVideos: function () { return this.$store.getters.getProxyVideos @@ -698,7 +699,7 @@ export default defineComponent({ this.isLoading = true } - this.videoStoryboardSrc = `${this.currentInvidiousInstance}/api/v1/storyboards/${this.videoId}?height=90` + this.videoStoryboardSrc = `${this.currentInvidiousInstanceUrl}/api/v1/storyboards/${this.videoId}?height=90` invidiousGetVideoInformation(this.videoId) .then(async result => { @@ -720,7 +721,7 @@ export default defineComponent({ this.channelId = result.authorId this.channelName = result.author const channelThumb = result.authorThumbnails[1] - this.channelThumbnail = channelThumb ? youtubeImageUrlToInvidious(channelThumb.url, this.currentInvidiousInstance) : '' + this.channelThumbnail = channelThumb ? youtubeImageUrlToInvidious(channelThumb.url, this.currentInvidiousInstanceUrl) : '' this.updateSubscriptionDetails({ channelThumbnailUrl: channelThumb?.url, channelName: result.author, @@ -739,7 +740,7 @@ export default defineComponent({ this.isLive = result.liveNow this.isFamilyFriendly = result.isFamilyFriendly this.captionHybridList = result.captions.map(caption => { - caption.url = this.currentInvidiousInstance + caption.url + caption.url = this.currentInvidiousInstanceUrl + caption.url caption.type = '' caption.dataSource = 'invidious' return caption @@ -747,13 +748,13 @@ export default defineComponent({ switch (this.thumbnailPreference) { case 'start': - this.thumbnail = `${this.currentInvidiousInstance}/vi/${this.videoId}/maxres1.jpg` + this.thumbnail = `${this.currentInvidiousInstanceUrl}/vi/${this.videoId}/maxres1.jpg` break case 'middle': - this.thumbnail = `${this.currentInvidiousInstance}/vi/${this.videoId}/maxres2.jpg` + this.thumbnail = `${this.currentInvidiousInstanceUrl}/vi/${this.videoId}/maxres2.jpg` break case 'end': - this.thumbnail = `${this.currentInvidiousInstance}/vi/${this.videoId}/maxres3.jpg` + this.thumbnail = `${this.currentInvidiousInstanceUrl}/vi/${this.videoId}/maxres3.jpg` break default: this.thumbnail = result.videoThumbnails[0].url @@ -1439,7 +1440,7 @@ export default defineComponent({ }, createInvidiousDashManifest: async function () { - let url = `${this.currentInvidiousInstance}/api/manifest/dash/id/${this.videoId}` + let url = `${this.currentInvidiousInstanceUrl}/api/manifest/dash/id/${this.videoId}` // If we are in Electron, // we can use YouTube.js' DASH manifest generator to generate the manifest. @@ -1447,7 +1448,7 @@ export default defineComponent({ if (process.env.SUPPORTS_LOCAL_API) { // Invidious' API response doesn't include the height and width (and fps and qualityLabel for AV1) of video streams // so we need to extract them from Invidious' manifest - const response = await fetch(url) + const response = await invidiousFetch(url) const originalText = await response.text() const parsedManifest = new DOMParser().parseFromString(originalText, 'application/xml')