From 0438fdc68a8f4a529286042de687d910265f84d8 Mon Sep 17 00:00:00 2001 From: Renko Date: Thu, 22 Aug 2024 17:57:09 +0000 Subject: [PATCH 1/7] Translated using Weblate (Romanian) Currently translated at 93.9% (816 of 869 strings) Translation: FreeTube/Translations Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ro/ --- static/locales/ro.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/static/locales/ro.yaml b/static/locales/ro.yaml index 74954dce58984..42a7ed4e1ab01 100644 --- a/static/locales/ro.yaml +++ b/static/locales/ro.yaml @@ -173,6 +173,8 @@ User Playlists: Toast: You haven't selected any playlist yet.: Nu ați selectat încă nicio listă de redare. + "{videoCount} video(s) added to 1 playlist": 1 videoclip adăugat la 1 listă + de redare| {videoCount} videoclupuri adăugate la 1 listă de redare N playlists selected: '{playlistCount} Selectat' "{videoCount}/{totalVideoCount} Videos Already Added": '{videoCount}/{totalVideoCount} Videoclipuri deja adăugate' @@ -238,6 +240,10 @@ User Playlists: acestei liste de redare. Search for Videos: Căutați videoclipuri Remove Duplicate Videos: Șterge videoclipurile duplicate + Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Sunteți + sigur(ă) că doriți să ștergeți 1 videoclip vizualizat din această lista de redare? + Această acțiune este definitivă. | Sunteți sigur(ă) că doriți să ștergeți {playlistItemCount} + videoclip vizualizat din această lista de redare? Această acțiune este definitivă. History: # On History Page History: 'Istoric' @@ -293,6 +299,7 @@ Settings: System Default: Prestabilită de sistem Auto Load Next Page: Label: Încarcă următoarea pagină automat + Tooltip: Încarcă automat pagini și comentarii suplimentare. Theme Settings: Theme Settings: 'Setări temei' Match Top Bar with Main Color: 'Potriviți bara de sus cu culoarea principală' @@ -306,6 +313,8 @@ Settings: Catppuccin Mocha: Catppuccin Mocha Pastel Pink: Pastel Roz Hot Pink: Hot Roz + Solarized Light: Lumină solarizată + Solarized Dark: Întuneric solarizat Main Color Theme: Main Color Theme: 'Culoarea principală a temei' Red: 'Roşu' @@ -345,6 +354,13 @@ Settings: Catppuccin Mocha Maroon: Catppuccin Mocha Maro Catppuccin Mocha Peach: Catppuccin Mocha Piersică Catppuccin Mocha Rosewater: Catppuccin Mocha Apă de Trandafiri + Solarized Violet: Violet solarizat + Solarized Cyan: Turcoaz solarizat + Solarized Red: Roșu solarizat + Solarized Green: Verde solarizat + Solarized Yellow: Galben solarizat + Solarized Blue: Albastru solarizat + Solarized Orange: Portocaliu solarizat Secondary Color Theme: 'Culoarea secundară a temei' #* Main Color Theme UI Scale: Scala UI @@ -426,12 +442,19 @@ Settings: nu poate fi anulat.' Save Watched Videos With Last Viewed Playlist: Salvați videoclipurile vizionate cu ultima listă de redare vizualizată + Are you sure you want to remove all your playlists?: Sunteți sigur(ă) că doriț + să ștergeți toate listele dvs. de redare? + Remove All Playlists: Șterge toate listele de redare + All playlists have been removed: Toate listele de redare au fost șterse Subscription Settings: Subscription Settings: 'Setări de abonament' Hide Videos on Watch: 'Ascunde videoclipurile la vizionare' Fetch Feeds from RSS: 'Preluare de fluxuri din RSS' Manage Subscriptions: 'Gestionați abonamentele' Fetch Automatically: Preluați feedul automat + Confirm Before Unsubscribing: Confirmă înainte de dezabonare + Only Show Latest Video for Each Channel: Arată doar cele mai noi videoclipuri + pentru fiecare canal Data Settings: Data Settings: 'Setări de date' Select Import Type: 'Selectează tipul de import' @@ -596,6 +619,7 @@ Settings: răspundere! Replace HTTP Cache: Înlocuiți cache HTTP Experimental Settings: Setări experimentale + Sort Settings Sections (A-Z): Setări Sortare Secțiuni (A-Z) About: #On About page About: 'Despre' @@ -1131,3 +1155,5 @@ Search Listing: 4K: 4K Subtitles: Subtitrări Closed Captions: Subtitrări Complexe +Feed: + Refresh Feed: Reîmprospătează {subscriptionName} From fb4a4d15bccbf1cc4a79ce3680bef1b87e0398dc Mon Sep 17 00:00:00 2001 From: Renko Date: Thu, 22 Aug 2024 21:22:39 +0000 Subject: [PATCH 2/7] Translated using Weblate (Romanian) Currently translated at 94.0% (817 of 869 strings) Translation: FreeTube/Translations Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ro/ --- static/locales/ro.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/static/locales/ro.yaml b/static/locales/ro.yaml index 42a7ed4e1ab01..ccb291573eb6a 100644 --- a/static/locales/ro.yaml +++ b/static/locales/ro.yaml @@ -98,6 +98,7 @@ Search Filters: Subtitles: Subtitrări Location: Locație HDR: HDR + VR180: VR180 Subscriptions: # On Subscriptions Page Subscriptions: 'Abonamente' From 579d6d397db6bf8884155bf40fd5a4837c295b06 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:18:49 +0200 Subject: [PATCH 3/7] Support authentication with the Basic scheme for Invidious instances (#5569) * Support authentication with the Basic scheme for Invidious instances * I forgot to change this line --- src/constants.js | 4 +- src/main/index.js | 132 ++++++++++++------ .../ft-list-channel/ft-list-channel.js | 6 +- .../ft-list-playlist/ft-list-playlist.js | 8 +- .../components/ft-list-video/ft-list-video.js | 10 +- .../ft-playlist-selector.js | 6 +- .../ft-profile-channel-list.js | 8 +- .../ft-profile-filter-channels-list.js | 8 +- .../ft-share-button/ft-share-button.js | 14 +- .../ft-video-player/ft-video-player.js | 11 ++ .../components/playlist-info/playlist-info.js | 6 +- src/renderer/components/side-nav/side-nav.js | 6 +- .../subscriptions-community.js | 8 +- .../subscriptions-live/subscriptions-live.js | 10 +- .../subscriptions-shorts.js | 9 +- .../subscriptions-videos.js | 10 +- src/renderer/components/top-nav/top-nav.js | 4 - src/renderer/helpers/api/invidious.js | 28 +++- src/renderer/helpers/utils.js | 10 ++ src/renderer/store/modules/invidious.js | 49 ++++++- src/renderer/views/Channel/Channel.js | 10 +- src/renderer/views/Playlist/Playlist.js | 6 +- .../SubscribedChannels/SubscribedChannels.js | 8 +- src/renderer/views/Watch/Watch.js | 21 +-- 24 files changed, 262 insertions(+), 130 deletions(-) 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') From a9ca4a86816530cbc84cd4ab069d6bee116027e4 Mon Sep 17 00:00:00 2001 From: Riki Shinozaki Date: Sun, 25 Aug 2024 09:34:39 +0000 Subject: [PATCH 4/7] Translated using Weblate (Japanese) Currently translated at 100.0% (869 of 869 strings) Translation: FreeTube/Translations Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ja/ --- static/locales/ja.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/static/locales/ja.yaml b/static/locales/ja.yaml index 1290427b9c16d..8c42a5fa5d4e5 100644 --- a/static/locales/ja.yaml +++ b/static/locales/ja.yaml @@ -241,13 +241,13 @@ Settings: Video View Type: Video View Type: '動画一覧の表示方法' Grid: 'グリッド' - List: '一覧' + List: 'リスト' Thumbnail Preference: Thumbnail Preference: 'サムネイルの設定' Default: 'デフォルト' Beginning: '開始' Middle: '中間' - End: '終了' + End: '最後' Hidden: 非表示 Blur: ぼかし 'Invidious Instance (Default is https://invidious.snopyta.org)': '接続先の Invidious @@ -258,7 +258,7 @@ Settings: Check for Updates: 更新の確認 View all Invidious instance information: すべての Invidious インスタンス情報の表示 System Default: システム初期設定 - Clear Default Instance: デフォルトの設定をクリアする + Clear Default Instance: 既定のインスタンスをクリア Set Current Instance as Default: 現在のインスタンスを既定として設定する Current instance will be randomized on startup: 現在のインスタンスは起動時にランダム化されます No default instance has been set: 既定のインスタンスが設定されていません @@ -271,10 +271,10 @@ Settings: External Link Handling: 外部リンク処理 Auto Load Next Page: Label: 次のページを自動で読み込む - Tooltip: 追加のページやコメントを自動的に読み込む。 + Tooltip: 追加のページやコメント欄の続きを自動的に読み込みます。 Theme Settings: Theme Settings: 'テーマの設定' - Match Top Bar with Main Color: 'トップバーをメインカラーに合わせる' + Match Top Bar with Main Color: '上部のバーをメインカラーに合わせる' Base Theme: Base Theme: 'テーマの選択' Black: '黒' @@ -335,7 +335,7 @@ Settings: Solarized Cyan: ソラライズド・シアン Solarized Green: ソラライズド・グリーン Solarized Blue: ソラライズド・ブルー - Secondary Color Theme: 'テーマのセカンダリー カラー' + Secondary Color Theme: 'テーマのアクセント カラー' #* Main Color Theme UI Scale: UI 縮尺率 Expand Side Bar by Default: 幅広のサイド バーで起動 @@ -347,7 +347,7 @@ Settings: Force Local Backend for Legacy Formats: '旧形式であれば内部 API の適用' Remember History: '履歴を記憶' Play Next Video: '次の動画の自動再生' - Turn on Subtitles by Default: '字幕をデフォルトでオンにする' + Turn on Subtitles by Default: 'デフォルトで字幕を有効にする' Autoplay Videos: '動画の自動再生' Proxy Videos Through Invidious: '動画を Invidious プロキシを経由して取得' Autoplay Playlists: '再生リストの自動再生' @@ -375,8 +375,8 @@ Settings: Next Video Interval: 次の動画までの間隔 Fast-Forward / Rewind Interval: 早送り/巻き戻し間隔 Display Play Button In Video Player: 動画プレーヤーに再生ボタンを表示する - Scroll Volume Over Video Player: スクロールで動画の音量調整 - Scroll Playback Rate Over Video Player: スクロールで動画の再生速度調整 + Scroll Volume Over Video Player: 動画プレーヤーをスクロールして音量を変更 + Scroll Playback Rate Over Video Player: 動画プレーヤーをスクロールして再生速度を変更 Max Video Playback Rate: 最大動画再生速度 Video Playback Rate Interval: 動画再生速度間隔 Screenshot: @@ -391,13 +391,13 @@ Settings: Enable: スクリーンショットの有効化 Format Label: スクリーンショット形式 Ask Path: 保存フォルダを尋ねる - Folder Button: フォルダーの選択 + Folder Button: フォルダの選択 Enter Fullscreen on Display Rotate: 画面の回転時に全画面表示にする - Skip by Scrolling Over Video Player: スクロールで動画をスキップ + Skip by Scrolling Over Video Player: 動画プレーヤーをスクロールして動画をスキップ Allow DASH AV1 formats: DASH AV1形式を許可 Subscription Settings: Subscription Settings: '登録チャンネルの設定' - Hide Videos on Watch: '視聴済み動画の非表示' + Hide Videos on Watch: '視聴済みの動画を非表示にする' Subscriptions Export Format: Subscriptions Export Format: '登録のエクスポート形式' #& Freetube @@ -503,8 +503,8 @@ Settings: Hide Channel Subscribers: チャンネル登録者数の非表示 Hide Video Views: 再生数の非表示 Hide Video Likes And Dislikes: 評価の非表示 - Distraction Free Settings: 集中モード - Hide Active Subscriptions: 使用中の登録チャンネルの非表示 + Distraction Free Settings: 集中モード設定 + Hide Active Subscriptions: 登録チャンネルを非表示 Hide Playlists: 再生リストの非表示 Hide Video Description: 動画説明の非表示 Hide Comments: コメントの非表示 @@ -516,11 +516,11 @@ Settings: Hide Channels Placeholder: チャンネル ID Display Titles Without Excessive Capitalisation: 過剰な大文字や句読点のないタイトルの表示 Hide Channel Playlists: チャンネル再生リストの非表示 - Hide Channel Community: チャンネル コミュニティの非表示 + Hide Channel Community: チャンネルコミュニティの非表示 Sections: Side Bar: サイドバー Channel Page: チャンネル ページ - Watch Page: ウォッチページ + Watch Page: 視聴ページ General: 一般 Subscriptions Page: 登録チャンネル ページ Hide Featured Channels: おすすめチャンネルの非表示 @@ -530,7 +530,7 @@ Settings: Hide Subscriptions Live: 登録チャンネルのライブ配信の非表示 Hide Subscriptions Videos: 登録チャンネルの動画の非表示 Hide Channel Releases: チャンネルの新着情報の非表示 - Hide Profile Pictures in Comments: コメントでプロフィール写真を隠す + Hide Profile Pictures in Comments: コメント欄のプロフィール写真を隠す Blur Thumbnails: サムネイルをぼかす Hide Subscriptions Community: 登録チャンネルのコミュニティの非表示 Hide Channels Invalid: 提供されたチャンネル ID が無効です @@ -640,7 +640,7 @@ About: #On Channel Page Donate: 寄付 FreeTube is made possible by: FreeTube が実現できているのは - these people and projects: これらの人々とプロジェクト + these people and projects: これらの人々とプロジェクトのおかげです Credits: 著作権 Translate: 翻訳 room rules: ルームの規則について From 7e14f4cb43338d41c64ad2936dff4e16933f88e5 Mon Sep 17 00:00:00 2001 From: Riki Shinozaki Date: Sun, 25 Aug 2024 15:58:43 +0000 Subject: [PATCH 5/7] Translated using Weblate (Japanese) Currently translated at 100.0% (869 of 869 strings) Translation: FreeTube/Translations Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ja/ --- static/locales/ja.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/locales/ja.yaml b/static/locales/ja.yaml index 8c42a5fa5d4e5..2c312186fbd70 100644 --- a/static/locales/ja.yaml +++ b/static/locales/ja.yaml @@ -499,12 +499,12 @@ Settings: Hide Popular Videos: 人気動画の非表示 Hide Trending Videos: 急上昇動画の非表示 Hide Recommended Videos: おすすめ動画の非表示 - Hide Comment Likes: コメント評価の非表示 + Hide Comment Likes: コメント欄の評価の非表示 Hide Channel Subscribers: チャンネル登録者数の非表示 Hide Video Views: 再生数の非表示 Hide Video Likes And Dislikes: 評価の非表示 Distraction Free Settings: 集中モード設定 - Hide Active Subscriptions: 登録チャンネルを非表示 + Hide Active Subscriptions: 登録チャンネルの非表示 Hide Playlists: 再生リストの非表示 Hide Video Description: 動画説明の非表示 Hide Comments: コメントの非表示 @@ -898,7 +898,7 @@ Share: Share Playlist: '再生リストの共有' Copy Link: 'リンクのコピー' Open Link: 'リンクの表示' - Copy Embed: '埋め込みのコピー' + Copy Embed: '埋め込み コピー' Open Embed: '埋め込みの表示' # On Click Invidious URL copied to clipboard: 'Invidious URL をコピーしました' From 9f8f86802a229761e6b176eb4ddc22b45ae94dc7 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:23:18 +0200 Subject: [PATCH 6/7] Update shaka-player to version 4.10.11 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6932b7ec69fce..a745590dae3d2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "path-browserify": "^1.0.1", "portal-vue": "^2.1.7", "process": "^0.11.10", - "shaka-player": "^4.10.10", + "shaka-player": "^4.10.11", "swiper": "^11.1.9", "vue": "^2.7.16", "vue-i18n": "^8.28.2", diff --git a/yarn.lock b/yarn.lock index aea4031031f54..74aa572eb5ce8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7250,10 +7250,10 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shaka-player@^4.10.10: - version "4.10.10" - resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-4.10.10.tgz#b60b607f833d8d056ea170035f939a449ac268ac" - integrity sha512-r1L7gZIKzbZ5fK6zuuEbemhtPtMvQKvZvUqsUmV1yp0TptwsfqQGSI7v1LYNPO4tYp779OxZgYoL+do1KcVwHA== +shaka-player@^4.10.11: + version "4.10.11" + resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-4.10.11.tgz#480d290db351cfaa248c363b6b235b8d75285879" + integrity sha512-8j2w1B//btLB4EZkslU8SDFI9H7vtOgAeP+oKIvMYqC8wXGMA8kmTir8edSdfaIeLtSsChvO1pXt6dpK02oy7w== dependencies: eme-encryption-scheme-polyfill "^2.1.5" From 2ce07261fdd434b63b61f01de09b894564d201b3 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:51:47 +0200 Subject: [PATCH 7/7] Use the HLS manifests for live streams The live DASH manifests are currently unusable on both API backends as they return 403s after 1 minute of playback. Unfortunately this means we lose the ability to seek and use the audio formats for live streams. --- .../ft-shaka-video-player.js | 29 ++++++-- .../ft-shaka-video-player.vue | 5 +- src/renderer/views/Watch/Watch.js | 67 ++++++++++--------- static/locales/en-US.yaml | 1 + static/locales/en_GB.yaml | 1 + 5 files changed, 65 insertions(+), 38 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 8bad484e887e0..563fb37da207e 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -1434,18 +1434,33 @@ export default defineComponent({ stats.bitrate = (newTrack.bandwidth / 1000).toFixed(2) - // for videos with multiple audio tracks, youtube.js appends the track id to the itag, to make it unique - stats.codecs.audioItag = newTrack.originalAudioId.split('-')[0] - stats.codecs.audioCodec = newTrack.audioCodec + // Combined audio and video HLS streams + if (newTrack.videoCodec.includes(',')) { + stats.codecs.audioItag = '' + stats.codecs.videoItag = '' - if (props.format === 'dash') { - stats.resolution.frameRate = newTrack.frameRate + const [audioCodec, videoCodec] = newTrack.videoCodec.split(',') - stats.codecs.videoItag = newTrack.originalVideoId - stats.codecs.videoCodec = newTrack.videoCodec + stats.codecs.audioCodec = audioCodec + stats.codecs.videoCodec = videoCodec + stats.resolution.frameRate = newTrack.frameRate stats.resolution.width = newTrack.width stats.resolution.height = newTrack.height + } else { + // for videos with multiple audio tracks, youtube.js appends the track id to the itag, to make it unique + stats.codecs.audioItag = newTrack.originalAudioId.split('-')[0] + stats.codecs.audioCodec = newTrack.audioCodec + + if (props.format === 'dash') { + stats.resolution.frameRate = newTrack.frameRate + + stats.codecs.videoItag = newTrack.originalVideoId + stats.codecs.videoCodec = newTrack.videoCodec + + stats.resolution.width = newTrack.width + stats.resolution.height = newTrack.height + } } } diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue index f067e092dc94d..3477b6ad37f4a 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue @@ -52,8 +52,11 @@ v-if="format === 'audio'" >{{ $t('Video.Player.Stats.CodecAudio', stats.codecs) }} {{ $t('Video.Player.Stats.CodecsVideoAudio', stats.codecs) }} + {{ $t('Video.Player.Stats.CodecsVideoAudioNoItags', stats.codecs) }}
{{ $t('Video.Player.Stats.Player Dimensions', stats.playerDimensions) }}
diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 552ce4e334490..a8c6ccb589c82 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -480,21 +480,25 @@ export default defineComponent({ } if (useRemoteManifest) { - if (result.streaming_data.dash_manifest_url) { - let src = result.streaming_data.dash_manifest_url - - if (src.includes('?')) { - src += '&mpd_version=7' - } else { - src += `${src.endsWith('/') ? '' : '/'}mpd_version/7` - } - - this.manifestSrc = src - this.manifestMimeType = MANIFEST_TYPE_DASH - } else { - this.manifestSrc = result.streaming_data.hls_manifest_url - this.manifestMimeType = MANIFEST_TYPE_HLS - } + // The live DASH manifest is currently unusable it is not available on the iOS client + // but the web ones returns 403s after 1 minute of playback so we have to use the HLS one for now. + // Leaving the code here commented out in case we can use it again in the future + + // if (result.streaming_data.dash_manifest_url) { + // let src = result.streaming_data.dash_manifest_url + + // if (src.includes('?')) { + // src += '&mpd_version=7' + // } else { + // src += `${src.endsWith('/') ? '' : '/'}mpd_version/7` + // } + + // this.manifestSrc = src + // this.manifestMimeType = MANIFEST_TYPE_DASH + // } else { + this.manifestSrc = result.streaming_data.hls_manifest_url + this.manifestMimeType = MANIFEST_TYPE_HLS + // } } this.streamingDataExpiryDate = result.streaming_data.expires @@ -799,22 +803,25 @@ export default defineComponent({ this.videoChapters = chapters if (this.isLive || this.isPostLiveDvr) { - const url = `${this.currentInvidiousInstanceUrl}/api/manifest/dash/id/${this.videoId}` - - // Proxying doesn't work for live or post live DVR DASH, so use HLS instead - // https://github.com/iv-org/invidious/pull/4589 - if (this.proxyVideos) { - this.manifestSrc = result.hlsUrl - this.manifestMimeType = MANIFEST_TYPE_HLS - - // The HLS manifests only contain combined audio+video streams, so we can't do audio only - if (this.activeFormat === 'audio') { - this.activeFormat = 'dash' - } - } else { - this.manifestSrc = url - this.manifestMimeType = MANIFEST_TYPE_DASH + // The live DASH manifest is currently unusable as it returns 403s after 1 minute of playback + // so we have to use the HLS one for now. + // Leaving the code here commented out in case we can use it again in the future + // const url = `${this.currentInvidiousInstanceUrl}/api/manifest/dash/id/${this.videoId}` + + // // Proxying doesn't work for live or post live DVR DASH, so use HLS instead + // // https://github.com/iv-org/invidious/pull/4589 + // if (this.proxyVideos) { + this.manifestSrc = result.hlsUrl + this.manifestMimeType = MANIFEST_TYPE_HLS + + // The HLS manifests only contain combined audio+video streams, so we can't do audio only + if (this.activeFormat === 'audio') { + this.activeFormat = 'dash' } + // } else { + // this.manifestSrc = url + // this.manifestMimeType = MANIFEST_TYPE_DASH + // } this.legacyFormats = [] diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index f2580177d2c93..9818747071258 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -900,6 +900,7 @@ Video: Dropped Frames / Total Frames: 'Dropped Frames: {droppedFrames} / Total Frames: {totalFrames}' CodecAudio: 'Codec: {audioCodec} ({audioItag})' CodecsVideoAudio: 'Codecs: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})' + CodecsVideoAudioNoItags: 'Codecs: {videoCodec} / {audioCodec}' You appear to be offline: You appear to be offline. Playback will resume automatically when your connection comes back: Playback will resume automatically when your connection comes back. Skipped segment: 'Skipped {segmentCategory} segment' diff --git a/static/locales/en_GB.yaml b/static/locales/en_GB.yaml index 3a7618fe5b7f4..153e3b48064b6 100644 --- a/static/locales/en_GB.yaml +++ b/static/locales/en_GB.yaml @@ -970,6 +970,7 @@ Video: Dropped Frames / Total Frames: 'Dropped frames: {droppedFrames} / Total frames: {totalFrames}' CodecAudio: 'Codec: {audioCodec} ({audioItag})' CodecsVideoAudio: 'Codecs: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})' + CodecsVideoAudioNoItags: 'Codecs: {videoCodec} / {audioCodec}' You appear to be offline: You appear to be offline. Playback will resume automatically when your connection comes back: Playback will resume automatically when your connection comes back. Skipped segment: 'Skipped {segmentCategory} segment'