Compare commits

..

2 Commits

Author SHA1 Message Date
Émilien (perso)
aa7de1ed4c fix: fallback other yt clients no url found for adaptive formats (#5262) 2025-05-04 12:03:31 +02:00
Emilien
5f1f8ff4b1 Release v2.20250504.0 2025-05-04 12:02:58 +02:00
18 changed files with 250 additions and 657 deletions

View File

@ -23,17 +23,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# - name: Set up QEMU with:
# uses: docker/setup-qemu-action@v3 platforms: arm64
# with:
# platforms: arm64
- name: Install docker from docker webpage
run: |
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -41,18 +34,20 @@ jobs:
- name: Login to registry - name: Login to registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: gitea.ghost.tel registry: quay.io
username: ${{ secrets.USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.PASSWORD }} password: ${{ secrets.QUAY_PASSWORD }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: gitea.ghost.tel/knight/invidious images: quay.io/invidious/invidious
tags: | tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker AMD64 image for Push Event - name: Build and push Docker AMD64 image for Push Event
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@ -60,6 +55,7 @@ jobs:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
@ -69,9 +65,23 @@ jobs:
id: meta-arm64 id: meta-arm64
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: gitea.ghost.tel/knight/invidious images: quay.io/invidious/invidious
flavor: | flavor: |
suffix=-arm64 suffix=-arm64
tags: | tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker ARM64 image for Push Event
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
labels: ${{ steps.meta-arm64.outputs.labels }}
push: true
tags: ${{ steps.meta-arm64.outputs.tags }}
build-args: |
"release=1"

View File

@ -81,9 +81,9 @@
- [Available in many languages](locales/), thanks to [our translators](#contribute) - [Available in many languages](locales/), thanks to [our translators](#contribute)
**Data import/export** **Data import/export**
- Import subscriptions from YouTube, NewPipe and FreeTube - Import subscriptions from YouTube, NewPipe and Freetube
- Import watch history from YouTube and NewPipe - Import watch history from YouTube and NewPipe
- Export subscriptions to NewPipe and FreeTube - Export subscriptions to NewPipe and Freetube
- Import/Export Invidious user data - Import/Export Invidious user data
**Technical features** **Technical features**
@ -95,11 +95,11 @@
## Quick start ## Quick start
**Using Invidious:** **Using invidious:**
- [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now! - [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now!
**Hosting Invidious:** **Hosting invidious:**
- [Follow the installation instructions](https://docs.invidious.io/installation/) - [Follow the installation instructions](https://docs.invidious.io/installation/)
@ -114,8 +114,8 @@ https://github.com/iv-org/documentation
### Extensions ### Extensions
We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get), We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get),
a browser extension that automatically redirects YouTube URLs to any Invidious instance and replaces a browser extension that automatically redirects Youtube URLs to any Invidious instance and replaces
embedded YouTube videos on other websites with Invidious. embedded youtube videos on other websites with invidious.
The documentation contains a list of browser extensions that we recommended to use along with Invidious. The documentation contains a list of browser extensions that we recommended to use along with Invidious.
@ -140,7 +140,7 @@ We use [Weblate](https://weblate.org) to manage Invidious translations.
You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/. You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/.
Creating an account is not required, but recommended, especially if you want to contribute regularly. Creating an account is not required, but recommended, especially if you want to contribute regularly.
Weblate also allows you to log-in with major SSO providers like GitHub, GitLab, BitBucket, Google, ... Weblate also allows you to log-in with major SSO providers like Github, Gitlab, BitBucket, Google, ...
## Projects using Invidious ## Projects using Invidious

View File

@ -1,8 +1,6 @@
'use strict'; 'use strict';
console.log('[Invidious Debug] player.js start');
var player_data = JSON.parse(document.getElementById('player_data').textContent); var player_data = JSON.parse(document.getElementById('player_data').textContent);
var video_data = JSON.parse(document.getElementById('video_data').textContent); var video_data = JSON.parse(document.getElementById('video_data').textContent);
console.log('[Invidious Debug] video_data.params:', JSON.stringify(video_data.params));
var options = { var options = {
liveui: true, liveui: true,
@ -52,27 +50,9 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
return options; return options;
}; };
console.log('[Invidious Debug] Initializing videojs...'); var player = videojs('player', options);
var player;
try {
player = videojs('player', options);
console.log('[Invidious Debug] videojs initialized successfully.');
} catch (e) {
console.error('[Invidious Debug] videojs initialization FAILED:', e);
// Stop further execution if player init fails
throw e;
}
// --- Test listener for loadedmetadata ---
player.on('loadedmetadata', function() {
console.log('[Invidious Debug] Event: loadedmetadata Fired!');
});
player.on('error', function () { player.on('error', function () {
var error = player.error();
console.error('[Invidious Debug] Player Error Event:', error ? error.message : 'Unknown error', 'Code:', error ? error.code : 'N/A', 'Type:', error ? error.type : 'N/A', error);
if (video_data.params.quality === 'dash') return; if (video_data.params.quality === 'dash') return;
var localNotDisabled = ( var localNotDisabled = (
@ -90,7 +70,6 @@ player.on('error', function () {
return source; return source;
})); }));
} else if (reloadMakesSense) { } else if (reloadMakesSense) {
console.log('[Invidious Debug] Player error: Attempting reload in 5 seconds.');
setTimeout(function () { setTimeout(function () {
console.warn('An error occurred in the player, reloading...'); console.warn('An error occurred in the player, reloading...');
@ -107,54 +86,13 @@ player.on('error', function () {
player.playbackRate(playbackRate); player.playbackRate(playbackRate);
if (!paused) player.play(); if (!paused) player.play();
}, 5000); }, 5000);
} else {
console.log('[Invidious Debug] Player error: No specific action taken.');
} }
}); });
try {
player.on('error', function () {
var error = player.error();
console.error('[Invidious Debug] Player Error Event:', error ? error.message : 'Unknown error', 'Code:', error ? error.code : 'N/A', 'Type:', error ? error.type : 'N/A', error);
// Only reload if not dash or an ad is playing.
// Don't reload if the error is Source Not Supported type, as this is used when
});
console.log('[Invidious Debug] Generic player error listener attached.');
// Add listener for tech errors (might give more specific MSE/network errors)
player.ready(function() {
const tech = player.tech_;
if (tech) {
tech.on('error', function(event) {
console.error('[Invidious Debug] Tech Error Event:', event);
if (tech.error) {
console.error('[Invidious Debug] Tech Error Details:', tech.error().message, 'Code:', tech.error().code);
}
});
console.log('[Invidious Debug] Tech error listener attached.');
} else {
console.warn('[Invidious Debug] Could not attach tech error listener.');
}
});
} catch (e) {
console.error('[Invidious Debug] FAILED attaching error listeners:', e);
}
if (player.error) {
console.log('[Invidious Debug] Player error listener attached.');
}
if (video_data.params.quality === 'dash') { if (video_data.params.quality === 'dash') {
console.log('[Invidious Debug] Initializing reloadSourceOnError...'); player.reloadSourceOnError({
try { errorInterval: 10
player.reloadSourceOnError({ });
errorInterval: 10
});
console.log('[Invidious Debug] reloadSourceOnError initialized.');
} catch (e) {
console.error('[Invidious Debug] reloadSourceOnError FAILED:', e);
}
} }
/** /**
@ -224,6 +162,116 @@ player.on('timeupdate', function () {
} }
}); });
var shareOptions = {
socials: ['fbFeed', 'tw', 'reddit', 'email'],
get url() {
return addCurrentTimeToURL(short_url);
},
title: player_data.title,
description: player_data.description,
image: player_data.thumbnail,
get embedCode() {
// Single quotes inside here required. HTML inserted as is into value attribute of input
return "<iframe id='ivplayer' width='640' height='360' src='" +
addCurrentTimeToURL(embed_url) + "' style='border:none;'></iframe>";
}
};
if (location.pathname.startsWith('/embed/')) {
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
player.overlay({
overlays: [
{ start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'},
{ start: 'pause', content: overlay_content, end: 'playing', align: 'top'}
]
});
}
// Detect mobile users and initialize mobileUi for better UX
// Detection code taken from https://stackoverflow.com/a/20293441
function isMobile() {
try{ document.createEvent('TouchEvent'); return true; }
catch(e){ return false; }
}
if (isMobile()) {
player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } });
var buttons = ['playToggle', 'volumePanel', 'captionsButton'];
if (!video_data.params.listen && video_data.params.quality === 'dash') buttons.push('audioTrackButton');
if (video_data.params.listen || video_data.params.quality !== 'dash') buttons.push('qualitySelector');
// Create new control bar object for operation buttons
const ControlBar = videojs.getComponent('controlBar');
let operations_bar = new ControlBar(player, {
children: [],
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
});
buttons.slice(1).forEach(function (child) {operations_bar.addChild(child);});
// Remove operation buttons from primary control bar
var primary_control_bar = player.getChild('controlBar');
buttons.forEach(function (child) {primary_control_bar.removeChild(child);});
var operations_bar_element = operations_bar.el();
operations_bar_element.classList.add('mobile-operations-bar');
player.addChild(operations_bar);
// Playback menu doesn't work when it's initialized outside of the primary control bar
var playback_element = document.getElementsByClassName('vjs-playback-rate')[0];
operations_bar_element.append(playback_element);
// The share and http source selector element can't be fetched till the players ready.
player.one('playing', function () {
var share_element = document.getElementsByClassName('vjs-share-control')[0];
operations_bar_element.append(share_element);
if (!video_data.params.listen && video_data.params.quality === 'dash') {
var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0];
operations_bar_element.append(http_source_selector);
}
});
}
// Enable VR video support
if (!video_data.params.listen && video_data.vr && video_data.params.vr_mode) {
player.crossOrigin('anonymous');
switch (video_data.projection_type) {
case 'EQUIRECTANGULAR':
player.vr({projection: 'equirectangular'});
default: // Should only be 'MESH' but we'll use this as a fallback.
player.vr({projection: 'EAC'});
}
}
// Add markers
if (video_data.params.video_start > 0 || video_data.params.video_end > 0) {
var markers = [{ time: video_data.params.video_start, text: 'Start' }];
if (video_data.params.video_end < 0) {
markers.push({ time: video_data.length_seconds - 0.5, text: 'End' });
} else {
markers.push({ time: video_data.params.video_end, text: 'End' });
}
player.markers({
onMarkerReached: function (marker) {
if (marker.text === 'End')
player.loop() ? player.markers.prev('Start') : player.pause();
},
markers: markers
});
player.currentTime(video_data.params.video_start);
}
player.volume(video_data.params.volume / 100);
player.playbackRate(video_data.params.speed);
/** /**
* Method for getting the contents of a cookie * Method for getting the contents of a cookie
* *
@ -303,12 +351,16 @@ if (video_data.premiere_timestamp && Math.round(new Date() / 1000) < video_data.
} }
if (video_data.params.save_player_pos) { if (video_data.params.save_player_pos) {
console.log('[Invidious Debug] Setting up player position save/restore...'); const url = new URL(location);
var lastUpdated = 0; const hasTimeParam = url.searchParams.has('t');
var save_video_time = function(time) { const rememberedTime = get_video_time();
const all_video_times = get_all_video_times(); let lastUpdated = 0;
all_video_times[video_data.id] = time;
helpers.storage.set(save_player_pos_key, all_video_times); if(!hasTimeParam) {
if (rememberedTime >= video_data.length_seconds - 20)
set_seconds_after_start(0);
else
set_seconds_after_start(rememberedTime);
} }
player.on('timeupdate', function () { player.on('timeupdate', function () {
@ -324,138 +376,64 @@ if (video_data.params.save_player_pos) {
else remove_all_video_times(); else remove_all_video_times();
if (video_data.params.autoplay) { if (video_data.params.autoplay) {
console.log('[Invidious Debug] Handling autoplay setup...');
var bpb = player.getChild('bigPlayButton'); var bpb = player.getChild('bigPlayButton');
bpb.hide(); bpb.hide();
player.ready(function () { player.ready(function () {
console.log('[Invidious Debug] Player ready for autoplay.');
new Promise(function (resolve, reject) { new Promise(function (resolve, reject) {
setTimeout(function () {resolve(1);}, 1); setTimeout(function () {resolve(1);}, 1);
}).then(function (result) { }).then(function (result) {
console.log('[Invidious Debug] Attempting autoplay...');
console.log('[Invidious Debug] Calling player.play() for autoplay...');
var promise = player.play(); var promise = player.play();
if (promise !== undefined) { if (promise !== undefined) {
promise.then(function () { promise.then(function () {
console.log('[Invidious Debug] Autoplay successful.');
}).catch(function (error) { }).catch(function (error) {
console.error('[Invidious Debug] Autoplay FAILED:', error);
bpb.show(); bpb.show();
}); });
} else {
console.log('[Invidious Debug] Autoplay started (no promise returned).');
} }
}); });
}); });
} }
if (!video_data.params.listen && video_data.params.quality === 'dash') { if (!video_data.params.listen && video_data.params.quality === 'dash') {
console.log('[Invidious Debug] Initializing httpSourceSelector...'); player.httpSourceSelector();
console.log('[Invidious Debug] Player sources BEFORE httpSourceSelector init:', JSON.stringify(player.currentSources()));
// --- START Pre-Plugin Environment Check --- if (video_data.params.quality_dash !== 'auto') {
console.log('[Invidious Debug] Checking environment before httpSourceSelector call:');
console.log('[Invidious Debug] - typeof window.fetch:', typeof window.fetch);
console.log('[Invidious Debug] - typeof window.XMLHttpRequest:', typeof window.XMLHttpRequest);
console.log('[Invidious Debug] - typeof window.Promise:', typeof window.Promise);
console.log('[Invidious Debug] - typeof window.URL:', typeof window.URL);
console.log('[Invidious Debug] - typeof window.DOMParser:', typeof window.DOMParser);
console.log('[Invidious Debug] - videojs.options:', JSON.stringify(videojs.options));
// --- END Pre-Plugin Environment Check ---
try {
player.httpSourceSelector(); // This triggers manifest fetch & parsing
console.log('[Invidious Debug] httpSourceSelector initialized (call successful).'); // Log after call
console.log('[Invidious Debug] Player sources AFTER httpSourceSelector init call:', JSON.stringify(player.currentSources())); // Sources might not update yet
// Add listener for source changes after httpSourceSelector
player.on('sourceset', function() {
console.log('[Invidious Debug] Event: sourceset triggered. Current sources:', JSON.stringify(player.currentSources())); // Log when sources actually change
});
console.log('[Invidious Debug] sourceset listener attached.'); // Added
// Listen for potential errors during quality level processing
player.ready(function () { player.ready(function () {
// Add listener for quality level errors player.on('loadedmetadata', function () {
const qualityLevels = player.qualityLevels(); const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;});
if (qualityLevels) { let targetQualityLevel;
qualityLevels.on('error', function(event) { switch (video_data.params.quality_dash) {
console.error('[Invidious Debug] QualityLevels Error Event:', event); case 'best':
}); targetQualityLevel = qualityLevels.length - 1;
console.log('[Invidious Debug] QualityLevels error listener attached.'); // Added break;
case 'worst':
// Log initial quality levels once metadata is loaded targetQualityLevel = 0;
player.on('loadedmetadata', function() { break;
try { default:
console.log('[Invidious Debug] Event: loadedmetadata triggered.'); // Added const targetHeight = parseInt(video_data.params.quality_dash);
const levels = Array.from(player.qualityLevels()); for (let i = 0; i < qualityLevels.length; i++) {
// Log detailed info about each level if (qualityLevels[i].height <= targetHeight)
console.log('[Invidious Debug] Quality levels after loadedmetadata:', JSON.stringify(levels.map(l => ({id: l.id, width: l.width, height: l.height, bitrate: l.bitrate, enabled: l.enabled})))); targetQualityLevel = i;
} catch (e) { else
console.error('[Invidious Debug] Error logging/processing quality levels:', e); // Added
}
});
console.log('[Invidious Debug] loadedmetadata listener attached for quality level logging.'); // Added
} else {
console.warn('[Invidious Debug] Could not attach QualityLevels error listener or get levels.'); // Added
}
});
if (video_data.params.quality_dash !== 'auto') {
console.log('[Invidious Debug] Setting DASH quality:', video_data.params.quality_dash);
player.ready(function () {
player.on('loadedmetadata', function () {
try {
const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;});
let targetQualityLevel;
switch (video_data.params.quality_dash) {
case 'best':
targetQualityLevel = qualityLevels.length - 1;
break; break;
case 'worst':
targetQualityLevel = 0;
break;
default:
const targetHeight = parseInt(video_data.params.quality_dash);
for (let i = 0; i < qualityLevels.length; i++) {
if (qualityLevels[i].height <= targetHeight)
targetQualityLevel = i;
else
break;
}
} }
qualityLevels.forEach(function (level, index) { }
level.enabled = (index === targetQualityLevel); qualityLevels.forEach(function (level, index) {
}); level.enabled = (index === targetQualityLevel);
console.log('[Invidious Debug] DASH quality levels set.');
} catch (e) {
console.error('[Invidious Debug] Error setting DASH quality levels:', e);
}
}); });
}); });
} });
} catch (e) {
console.error('[Invidious Debug] httpSourceSelector FAILED (call threw error):', e); // Log if the call itself fails
} }
} }
console.log('[Invidious Debug] Initializing vttThumbnails...'); player.vttThumbnails({
try { src: '/api/v1/storyboards/' + video_data.id + '?height=90',
player.vttThumbnails({ showTimestamp: true
src: '/api/v1/storyboards/' + video_data.id + '?height=90', });
showTimestamp: true
});
console.log('[Invidious Debug] vttThumbnails initialized.');
} catch (e) {
console.error('[Invidious Debug] vttThumbnails FAILED:', e);
}
// Enable annotations // Enable annotations
if (!video_data.params.listen && video_data.params.annotations) { if (!video_data.params.listen && video_data.params.annotations) {
console.log('[Invidious Debug] Initializing annotations...');
addEventListener('load', function (e) { addEventListener('load', function (e) {
addEventListener('__ar_annotation_click', function (e) { addEventListener('__ar_annotation_click', function (e) {
const url = e.detail.url, const url = e.detail.url,
@ -490,215 +468,65 @@ if (!video_data.params.listen && video_data.params.annotations) {
} else { } else {
player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container });
} }
console.log('[Invidious Debug] Annotations initialized.');
} }
}); });
}); });
} }
player.on('play', function() { function change_volume(delta) {
console.log('[Invidious Debug] Event: play triggered. Current source:', JSON.stringify(player.currentSource())); const curVolume = player.volume();
}); let newVolume = curVolume + delta;
newVolume = helpers.clamp(newVolume, 0, 1);
player.on('playing', function() { player.volume(newVolume);
console.log('[Invidious Debug] Event: playing triggered. Playback has started.');
});
player.on('waiting', function() {
console.log('[Invidious Debug] Event: waiting triggered. Buffering or waiting for data.');
});
var shareOptions = {
socials: ['fbFeed', 'tw', 'reddit', 'email'],
get url() {
return addCurrentTimeToURL(short_url);
},
title: player_data.title,
description: player_data.description,
image: player_data.thumbnail,
get embedCode() {
// Single quotes inside here required. HTML inserted as is into value attribute of input
return "<iframe id='ivplayer' width='640' height='360' src='" +
addCurrentTimeToURL(embed_url) + "' style='border:none;'></iframe>";
}
};
if (location.pathname.startsWith('/embed/')) {
console.log('[Invidious Debug] Initializing overlay...');
try {
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
player.overlay({
overlays: [
{ start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'},
{ start: 'pause', content: overlay_content, end: 'playing', align: 'top'}
]
});
console.log('[Invidious Debug] Overlay initialized.');
} catch (e) {
console.error('[Invidious Debug] Overlay FAILED:', e);
}
} }
// Detect mobile users and initialize mobileUi for better UX function toggle_muted() {
// Detection code taken from https://stackoverflow.com/a/20293441 player.muted(!player.muted());
function isMobile() {
return typeof window.orientation !== 'undefined' || navigator.userAgent.indexOf('IEMobile') !== -1;
} }
if (isMobile()) { function skip_seconds(delta) {
console.log('[Invidious Debug] Mobile detected, initializing mobile UI...'); const duration = player.duration();
try { const curTime = player.currentTime();
player.mobileUi({ touchControls: { seekSeconds: 5, tapTimeout: 200, disableOnEnd: true } }); let newTime = curTime + delta;
console.log('[Invidious Debug] Mobile UI initialized.'); newTime = helpers.clamp(newTime, 0, duration);
} catch (e) { player.currentTime(newTime);
console.error('[Invidious Debug] Mobile UI FAILED:', e);
}
} }
if (player.markers && !video_data.params.listen && video_data.params.chapters) { function set_seconds_after_start(delta) {
console.log('[Invidious Debug] Initializing markers...'); const start = video_data.params.video_start;
var markers = []; player.currentTime(start + delta);
for (var i = 0; i < video_data.params.chapters.length; i++) {
var marker = video_data.params.chapters[i];
markers.push({
time: marker.start,
text: marker.title,
});
}
if (markers.length > 1 && markers[0].time === 0 && markers[0].text === 'Intro') {
markers.shift();
}
markers.sort((a,b) => (a.time > b.time) ? 1 : ((b.time > a.time) ? -1 : 0));
var unique_markers = [];
var last_time = -1;
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
if (marker.time <= last_time) {
continue;
}
unique_markers.push(marker);
last_time = marker.time;
}
markers = unique_markers;
// Add "end" time information to each marker
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
if (i < markers.length - 1) {
marker.end = markers[i+1].time - 0.001;
} else {
marker.end = player.duration();
}
}
player.markers.removeAll();
// Callback function when a marker is reached
function onMarkerReached(marker) {
//console.log("marker reached: " + marker.text + " at " + marker.time);
}
try {
player.markers({
onMarkerReached: onMarkerReached,
markers: markers
});
console.log('[Invidious Debug] Markers initialized.');
} catch (e) {
console.error('[Invidious Debug] Markers FAILED:', e);
}
player.currentTime(video_data.params.video_start);
} }
player.volume(video_data.params.volume / 100);
player.playbackRate(video_data.params.speed);
console.log('[Invidious Debug] Volume and playback rate set.');
if (player.share) {
console.log('[Invidious Debug] Initializing share plugin...');
try {
player.share(shareOptions);
console.log('[Invidious Debug] Share plugin initialized.');
} catch(e) {
console.error('[Invidious Debug] Share plugin FAILED:', e);
}
}
/**
* Method for saving the current video time
*
* @param {number} seconds
*/
function save_video_time(seconds) { function save_video_time(seconds) {
const all_video_times = get_all_video_times(); const all_video_times = get_all_video_times();
all_video_times[video_data.id] = seconds; all_video_times[video_data.id] = seconds;
helpers.storage.set(save_player_pos_key, all_video_times); helpers.storage.set(save_player_pos_key, all_video_times);
} }
/**
* Method for getting the saved video time
*
* @returns {number}
*/
function get_video_time() { function get_video_time() {
return get_all_video_times()[video_data.id] || 0; return get_all_video_times()[video_data.id] || 0;
} }
/**
* Method for getting all saved video times
*
* @returns {Object}
*/
function get_all_video_times() { function get_all_video_times() {
return helpers.storage.get(save_player_pos_key) || {}; return helpers.storage.get(save_player_pos_key) || {};
} }
/**
* Method for removing all saved video times
*/
function remove_all_video_times() { function remove_all_video_times() {
helpers.storage.remove(save_player_pos_key); helpers.storage.remove(save_player_pos_key);
} }
/**
* Method for setting the video time to a certain percentage
*
* @param {number} percent
*/
function set_time_percent(percent) { function set_time_percent(percent) {
const duration = player.duration(); const duration = player.duration();
const newTime = duration * (percent / 100); const newTime = duration * (percent / 100);
player.currentTime(newTime); player.currentTime(newTime);
} }
/**
* Method for playing the video
*/
function play() { player.play(); } function play() { player.play(); }
/**
* Method for pausing the video
*/
function pause() { player.pause(); } function pause() { player.pause(); }
/**
* Method for stopping the video
*/
function stop() { player.pause(); player.currentTime(0); } function stop() { player.pause(); player.currentTime(0); }
/**
* Method for toggling play/pause
*/
function toggle_play() { player.paused() ? play() : pause(); } function toggle_play() { player.paused() ? play() : pause(); }
/**
* Method for toggling captions
*/
const toggle_captions = (function () { const toggle_captions = (function () {
let toggledTrack = null; let toggledTrack = null;
@ -757,18 +585,10 @@ const toggle_captions = (function () {
}; };
})(); })();
/**
* Method for toggling fullscreen
*/
function toggle_fullscreen() { function toggle_fullscreen() {
player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(); player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen();
} }
/**
* Method for increasing playback rate
*
* @param {number} steps
*/
function increase_playback_rate(steps) { function increase_playback_rate(steps) {
const maxIndex = options.playbackRates.length - 1; const maxIndex = options.playbackRates.length - 1;
const curIndex = options.playbackRates.indexOf(player.playbackRate()); const curIndex = options.playbackRates.indexOf(player.playbackRate());
@ -777,7 +597,6 @@ function increase_playback_rate(steps) {
player.playbackRate(options.playbackRates[newIndex]); player.playbackRate(options.playbackRates[newIndex]);
} }
// Add event listener for keydown events
addEventListener('keydown', function (e) { addEventListener('keydown', function (e) {
if (e.target.tagName.toLowerCase() === 'input') { if (e.target.tagName.toLowerCase() === 'input') {
// Ignore input when focus is on certain elements, e.g. form fields. // Ignore input when focus is on certain elements, e.g. form fields.
@ -866,7 +685,7 @@ addEventListener('keydown', function (e) {
break; break;
// TODO: More precise step. Now FPS is taken equal to 29.97 // TODO: More precise step. Now FPS is taken equal to 29.97
// Common FPS: https://forum.videohelp.com/threads/81864#post323588 // Common FPS: https://forum.videohelp.com/threads/81868#post323588
// Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/ // Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/
case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break; case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break;
case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break; case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break;
@ -912,6 +731,9 @@ addEventListener('keydown', function (e) {
player.on('DOMMouseScroll', mouseScroll); player.on('DOMMouseScroll', mouseScroll);
}()); }());
// Since videojs-share can sometimes be blocked, we defer it until last
if (player.share) player.share(shareOptions);
// show the preferred caption by default // show the preferred caption by default
if (player_data.preferred_caption_found) { if (player_data.preferred_caption_found) {
player.ready(function () { player.ready(function () {
@ -926,7 +748,6 @@ if (player_data.preferred_caption_found) {
// Safari audio double duration fix // Safari audio double duration fix
if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
console.log('[Invidious Debug] Applying Safari listen mode duration fix...');
player.on('loadedmetadata', function () { player.on('loadedmetadata', function () {
player.on('timeupdate', function () { player.on('timeupdate', function () {
if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) { if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) {
@ -938,7 +759,6 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
// Safari screen timeout on looped video playback fix // Safari screen timeout on looped video playback fix
if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) { if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) {
console.log('[Invidious Debug] Applying Safari loop mode screen timeout fix...');
player.loop(false); player.loop(false);
player.ready(function () { player.ready(function () {
player.on('ended', function () { player.on('ended', function () {
@ -950,25 +770,19 @@ if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen &&
// Watch on Invidious link // Watch on Invidious link
if (location.pathname.startsWith('/embed/')) { if (location.pathname.startsWith('/embed/')) {
console.log('[Invidious Debug] Adding Watch on Invidious button...'); const Button = videojs.getComponent('Button');
try { let watch_on_invidious_button = new Button(player);
const Button = videojs.getComponent('Button');
let watch_on_invidious_button = new Button(player);
// Create hyperlink for current instance // Create hyperlink for current instance
var redirect_element = document.createElement('a'); var redirect_element = document.createElement('a');
redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v=')); redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v='));
redirect_element.appendChild(document.createTextNode('Invidious')); redirect_element.appendChild(document.createTextNode('Invidious'));
watch_on_invidious_button.el().appendChild(redirect_element); watch_on_invidious_button.el().appendChild(redirect_element);
watch_on_invidious_button.addClass('watch-on-invidious'); watch_on_invidious_button.addClass('watch-on-invidious');
var cb = player.getChild('ControlBar'); var cb = player.getChild('ControlBar');
cb.addChild(watch_on_invidious_button); cb.addChild(watch_on_invidious_button);
console.log('[Invidious Debug] Watch on Invidious button added.');
} catch (e) {
console.error('[Invidious Debug] Watch on Invidious button FAILED:', e);
}
} }
addEventListener('DOMContentLoaded', function () { addEventListener('DOMContentLoaded', function () {
@ -978,5 +792,3 @@ addEventListener('DOMContentLoaded', function () {
changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href);
}); });
}); });
console.log('[Invidious Debug] player.js finished loading.');

View File

@ -54,53 +54,6 @@ db:
## ##
#signature_server: #signature_server:
##
## Invidious companion is an external program
## for loading the video streams from YouTube servers.
##
## When this setting is commented out, Invidious companion is not used.
## Otherwise, Invidious will proxy the requests to Invidious companion.
##
## Note: multiple URL can be configured. In this case, invidious will
## randomly pick one every time video data needs to be retrieved. This
## URL is then kept in the video metadata cache to allow video playback
## to work. Once said cache has expired, requesting that video's data
## again will cause a new companion URL to be picked.
##
## The parameter private_url needs to be configured for the internal
## communication between the companion and Invidious.
## And public_url is the public URL from which companion is listening
## to the requests from the user(s).
##
## If you are using a reverse proxy then you will probably need to
## configure the public_url to be the same as the domain used for Invidious.
## Also apply when used from an external IP address (without a domain).
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282
##
## Both parameter can have identical URL when Invidious is hosted in
## an internal network or at home or locally (localhost).
##
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
## Default: <none>
##
#invidious_companion:
# - private_url: "http://localhost:8282"
# public_url: "http://localhost:8282"
##
## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion.
## The key needs to be exactly 16 characters long.
##
## Note: This parameter is mandatory when Invidious companion is enabled
## and should be a random string.
## Such random string can be generated on linux with the following
## command: `pwgen 16 1`
##
## Accepted values: a string (of length 16)
## Default: <none>
##
#invidious_companion_key: "CHANGE_ME!!"
######################################### #########################################
# #
@ -858,9 +811,9 @@ default_user_preferences:
## Default video quality. ## Default video quality.
## ##
## Accepted values: dash, hd720, medium, small ## Accepted values: dash, hd720, medium, small
## Default: dash ## Default: hd720
## ##
#quality: dash #quality: hd720
## ##
## Default dash video quality. ## Default dash video quality.

View File

@ -1,5 +1,5 @@
name: invidious name: invidious
version: 2.20250314.0-dev version: 2.20250314.0
authors: authors:
- Invidious team <contact@invidious.io> - Invidious team <contact@invidious.io>

View File

@ -97,10 +97,6 @@ YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size) GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
COMPANION_POOL = CompanionConnectionPool.new(
capacity: CONFIG.pool_size
)
# CLI # CLI
Kemal.config.extra_options do |parser| Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]" parser.banner = "Usage: invidious [arguments]"
@ -171,9 +167,16 @@ DECRYPT_FUNCTION =
if sig_helper_address = CONFIG.signature_server.presence if sig_helper_address = CONFIG.signature_server.presence
IV::DecryptFunction.new(sig_helper_address) IV::DecryptFunction.new(sig_helper_address)
else else
LOGGER.warn("WARNING: inv-sig-helper is required for video playback. For more information see https://docs.invidious.io/installation")
nil nil
end end
{% for field in %w(po_token visitor_data) %}
if !CONFIG.{{field.id}}
LOGGER.warn("WARNING: {{field.id}} is required to view and playback videos. For more information see https://docs.invidious.io/installation")
end
{% end %}
# Start jobs # Start jobs
if CONFIG.channel_threads > 0 if CONFIG.channel_threads > 0

View File

@ -35,7 +35,7 @@ struct ConfigPreferences
property max_results : Int32 = 40 property max_results : Int32 = 40
property notifications_only : Bool = false property notifications_only : Bool = false
property player_style : String = "invidious" property player_style : String = "invidious"
property quality : String = "dash" property quality : String = "hd720"
property quality_dash : String = "auto" property quality_dash : String = "auto"
property default_home : String? = "Popular" property default_home : String? = "Popular"
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"] property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
@ -74,16 +74,6 @@ end
class Config class Config
include YAML::Serializable include YAML::Serializable
class CompanionConfig
include YAML::Serializable
@[YAML::Field(converter: Preferences::URIConverter)]
property private_url : URI = URI.parse("")
@[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("")
end
# Number of threads to use for crawling videos from channels (for updating subscriptions) # Number of threads to use for crawling videos from channels (for updating subscriptions)
property channel_threads : Int32 = 1 property channel_threads : Int32 = 1
# Time interval between two executions of the job that crawls channel videos (subscriptions update). # Time interval between two executions of the job that crawls channel videos (subscriptions update).
@ -170,12 +160,6 @@ class Config
# poToken for passing bot attestation # poToken for passing bot attestation
property po_token : String? = nil property po_token : String? = nil
# Invidious companion
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
# Invidious companion API key
property invidious_companion_key : String = ""
# Saved cookies in "name1=value1; name2=value2..." format # Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)] @[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new property cookies : HTTP::Cookies = HTTP::Cookies.new
@ -256,27 +240,6 @@ class Config
end end
{% end %} {% end %}
if config.invidious_companion.present?
# invidious_companion and signature_server can't work together
if config.signature_server
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
exit(1)
elsif config.invidious_companion_key.empty?
puts "Config: Please configure a key if you are using invidious companion."
exit(1)
elsif config.invidious_companion_key == "CHANGE_ME!!"
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
exit(1)
elsif config.invidious_companion_key.size != 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
exit(1)
end
elsif config.signature_server
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
else
puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/companion-installation/")
end
# HMAC_key is mandatory # HMAC_key is mandatory
# See: https://github.com/iv-org/invidious/issues/3854 # See: https://github.com/iv-org/invidious/issues/3854
if config.hmac_key.empty? if config.hmac_key.empty?

View File

@ -23,16 +23,10 @@ module Invidious::Frontend::WatchPage
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>" return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
end end
url = "/download"
if (CONFIG.invidious_companion.present?)
invidious_companion = CONFIG.invidious_companion.sample
url = "#{invidious_companion.public_url}/download?check=#{invidious_companion_encrypt(video.id)}"
end
return String.build(4000) do |str| return String.build(4000) do |str|
str << "<form" str << "<form"
str << " class=\"pure-form pure-form-stacked\"" str << " class=\"pure-form pure-form-stacked\""
str << " action='#{url}'" str << " action='/download'"
str << " method='post'" str << " method='post'"
str << " rel='noopener'" str << " rel='noopener'"
str << " target='_blank'>" str << " target='_blank'>"

View File

@ -383,22 +383,3 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
end end
return text return text
end end
def encrypt_ecb_without_salt(data, key)
cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.encrypt
cipher.key = key
io = IO::Memory.new
io.write(cipher.update(data))
io.write(cipher.final)
io.rewind
return io
end
def invidious_companion_encrypt(data)
timestamp = Time.utc.to_unix
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
return Base64.urlsafe_encode(encrypted_data)
end

View File

@ -8,11 +8,6 @@ module Invidious::Routes::API::Manifest
id = env.params.url["id"] id = env.params.url["id"]
region = env.params.query["region"]? region = env.params.query["region"]?
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
end
# Since some implementations create playlists based on resolution regardless of different codecs, # Since some implementations create playlists based on resolution regardless of different codecs,
# we can opt to only add a source to a representation if it has a unique height within that representation # we can opt to only add a source to a representation if it has a unique height within that representation
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe } unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }

View File

@ -203,14 +203,6 @@ module Invidious::Routes::Embed
return env.redirect url return env.redirect url
end end
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
end
rendered "embed" rendered "embed"
end end
end end

View File

@ -21,7 +21,7 @@ module Invidious::Routes::VideoPlayback
end end
# Sanity check, to avoid being used as an open proxy # Sanity check, to avoid being used as an open proxy
if !host.matches?(/[\w-]+\.(?:googlevideo|c\.youtube)\.com/) if !host.matches?(/[\w-]+.googlevideo.com/)
return error_template(400, "Invalid \"host\" parameter.") return error_template(400, "Invalid \"host\" parameter.")
end end
@ -37,8 +37,7 @@ module Invidious::Routes::VideoPlayback
# See: https://github.com/iv-org/invidious/issues/3302 # See: https://github.com/iv-org/invidious/issues/3302
range_header = env.request.headers["Range"]? range_header = env.request.headers["Range"]?
sq = query_params["sq"]? if range_header.nil?
if range_header.nil? && sq.nil?
range_for_head = query_params["range"]? || "0-640" range_for_head = query_params["range"]? || "0-640"
headers["Range"] = "bytes=#{range_for_head}" headers["Range"] = "bytes=#{range_for_head}"
end end
@ -257,11 +256,6 @@ module Invidious::Routes::VideoPlayback
# YouTube /videoplayback links expire after 6 hours, # YouTube /videoplayback links expire after 6 hours,
# so we have a mechanism here to redirect to the latest version # so we have a mechanism here to redirect to the latest version
def self.latest_version(env) def self.latest_version(env)
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
end
id = env.params.query["id"]? id = env.params.query["id"]?
itag = env.params.query["itag"]?.try &.to_i? itag = env.params.query["itag"]?.try &.to_i?

View File

@ -192,14 +192,6 @@ module Invidious::Routes::Watch
captions: video.captions captions: video.captions
) )
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
end
templated "watch" templated "watch"
end end
@ -293,9 +285,6 @@ module Invidious::Routes::Watch
if CONFIG.disabled?("downloads") if CONFIG.disabled?("downloads")
return error_template(403, "Administrator has disabled this endpoint.") return error_template(403, "Administrator has disabled this endpoint.")
end end
if CONFIG.invidious_companion.present?
return error_template(403, "Downloads should be routed through Companion when present")
end
title = env.params.body["title"]? || "" title = env.params.body["title"]? || ""
video_id = env.params.body["id"]? || "" video_id = env.params.body["id"]? || ""
@ -325,9 +314,10 @@ module Invidious::Routes::Watch
env.params.query["label"] = URI.decode_www_form(label.as_s) env.params.query["label"] = URI.decode_www_form(label.as_s)
return Invidious::Routes::API::V1::Videos.captions(env) return Invidious::Routes::API::V1::Videos.captions(env)
elsif itag = download_widget["itag"]?.try &.as_i.to_s elsif itag = download_widget["itag"]?.try &.as_i
# URL params specific to /latest_version # URL params specific to /latest_version
env.params.query["id"] = video_id env.params.query["id"] = video_id
env.params.query["itag"] = itag.to_s
env.params.query["title"] = filename env.params.query["title"] = filename
env.params.query["local"] = "true" env.params.query["local"] = "true"

View File

@ -15,7 +15,7 @@ struct Video
# NOTE: don't forget to bump this number if any change is made to # NOTE: don't forget to bump this number if any change is made to
# the `params` structure in videos/parser.cr!!! # the `params` structure in videos/parser.cr!!!
# #
SCHEMA_VERSION = 3 SCHEMA_VERSION = 2
property id : String property id : String

View File

@ -108,20 +108,18 @@ def extract_video_info(video_id : String)
params = parse_video_info(video_id, player_response) params = parse_video_info(video_id, player_response)
params["reason"] = JSON::Any.new(reason) if reason params["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present? if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil? LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.") players_fallback = [YoutubeAPI::ClientType::WebMobile, YoutubeAPI::ClientType::TvHtml5]
players_fallback = [YoutubeAPI::ClientType::WebMobile, YoutubeAPI::ClientType::TvHtml5] players_fallback.each do |player_fallback|
players_fallback.each do |player_fallback| client_config.client_type = player_fallback
client_config.client_type = player_fallback player_fallback_response = try_fetch_streaming_data(video_id, client_config)
player_fallback_response = try_fetch_streaming_data(video_id, client_config) if player_fallback_response && player_fallback_response["streamingData"]? &&
if player_fallback_response && player_fallback_response["streamingData"]? && player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url") streaming_data = player_response["streamingData"].as_h
streaming_data = player_response["streamingData"].as_h streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"] player_response["streamingData"] = JSON::Any.new(streaming_data)
player_response["streamingData"] = JSON::Any.new(streaming_data) break
break
end
end end
end end
end end

View File

@ -22,8 +22,6 @@
audio_streams.each_with_index do |fmt, i| audio_streams.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
bitrate = fmt["bitrate"] bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -36,12 +34,8 @@
<% end %> <% end %>
<% end %> <% end %>
<% else %> <% else %>
<% if params.quality == "dash" <% if params.quality == "dash" %>
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1" <source src="/api/manifest/dash/id/<%= video.id %>?local=true&unique_res=1" type='application/dash+xml' label="dash">
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
%>
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% end %> <% end %>
<% <%
@ -50,8 +44,6 @@
fmt_stream.each_with_index do |fmt, i| fmt_stream.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
quality = fmt["quality"] quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)

View File

@ -46,43 +46,6 @@ struct YoutubeConnectionPool
end end
end end
struct CompanionConnectionPool
property pool : DB::Pool(HTTP::Client)
def initialize(capacity = 5, timeout = 5.0)
options = DB::Pool::Options.new(
initial_pool_size: 0,
max_pool_size: capacity,
max_idle_pool_size: capacity,
checkout_timeout: timeout
)
@pool = DB::Pool(HTTP::Client).new(options) do
companion = CONFIG.invidious_companion.sample
next make_client(companion.private_url, use_http_proxy: false)
end
end
def client(&)
conn = pool.checkout
begin
response = yield conn
rescue ex
conn.close
companion = CONFIG.invidious_companion.sample
conn = make_client(companion.private_url, use_http_proxy: false)
response = yield conn
ensure
pool.release(conn)
end
response
end
end
def add_yt_headers(request) def add_yt_headers(request)
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal" request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
@ -98,9 +61,9 @@ def add_yt_headers(request)
end end
end end
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true) def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false)
client = HTTP::Client.new(url) client = HTTP::Client.new(url)
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy && use_http_proxy client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
# Force the usage of a specific configured IP Family # Force the usage of a specific configured IP Family
if force_resolve if force_resolve
@ -115,8 +78,8 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false, force_you
return client return client
end end
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &) def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
client = make_client(url, region, force_resolve: force_resolve, use_http_proxy: use_http_proxy) client = make_client(url, region, force_resolve: force_resolve)
begin begin
yield client yield client
ensure ensure

View File

@ -500,11 +500,7 @@ module YoutubeAPI
data["params"] = params data["params"] = params
end end
if CONFIG.invidious_companion.present? return self._post_json("/youtubei/v1/player", data, client_config)
return self._post_invidious_companion("/youtubei/v1/player", data)
else
return self._post_json("/youtubei/v1/player", data, client_config)
end
end end
#################################################################### ####################################################################
@ -670,49 +666,6 @@ module YoutubeAPI
return initial_data return initial_data
end end
####################################################################
# _post_invidious_companion(endpoint, data)
#
# Internal function that does the actual request to Invidious companion
# and handles errors.
#
# The requested data is an endpoint (URL without the domain part)
# and the data as a Hash object.
#
def _post_invidious_companion(
endpoint : String,
data : Hash,
) : Hash(String, JSON::Any)
headers = HTTP::Headers{
"Content-Type" => "application/json; charset=UTF-8",
"Authorization" => "Bearer #{CONFIG.invidious_companion_key}",
}
# Logging
LOGGER.debug("Invidious companion: Using endpoint: \"#{endpoint}\"")
LOGGER.trace("Invidious companion: POST data: #{data}")
# Send the POST request
begin
response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json)
body = response.body
if (response.status_code != 200)
raise Exception.new(
"Error while communicating with Invidious companion: \
status code: #{response.status_code} and body: #{body.dump}"
)
end
rescue ex
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
end
# Convert result to Hash
initial_data = JSON.parse(body).as_h
return initial_data
end
#################################################################### ####################################################################
# _decompress(body_io, headers) # _decompress(body_io, headers)
# #