Compare commits

..

23 Commits

Author SHA1 Message Date
3bd6603396 Add initial video player implementation with controls and error handling
All checks were successful
Build and release container directly from master / release (push) Successful in 5m42s
Stale issue handler / stale (push) Successful in 13s
2025-05-05 20:31:12 -04:00
5c2700f9fd Add video player with controls, hotkeys, and quality selection for DASH streams
All checks were successful
Build and release container directly from master / release (push) Successful in 5m33s
Stale issue handler / stale (push) Successful in 24s
2025-05-05 16:34:33 -04:00
16333615fa Add initial player.js with video playback and controls implementation
All checks were successful
Build and release container directly from master / release (push) Successful in 5m40s
2025-05-05 16:03:10 -04:00
3b56443825 Add initial player.js with Video.js setup and event handling
All checks were successful
Build and release container directly from master / release (push) Successful in 5m34s
2025-05-05 15:01:51 -04:00
8d3ff993dd Loggn
All checks were successful
Build and release container directly from master / release (push) Successful in 5m24s
2025-05-05 14:31:34 -04:00
7a750167eb Add video player with autoplay, DASH streaming and mobile UI support
All checks were successful
Build and release container directly from master / release (push) Successful in 5m26s
2025-05-05 14:20:21 -04:00
9a83674a42 Add video player implementation with controls, autoplay and quality settings
All checks were successful
Build and release container directly from master / release (push) Successful in 5m32s
2025-05-05 14:06:13 -04:00
91a4ac3744 Add video player functionality with controls, autoplay, and time tracking
All checks were successful
Build and release container directly from master / release (push) Successful in 11m48s
2025-05-05 13:26:42 -04:00
f22891d7b6 Add GitHub workflow for building nightly container images and player.js
All checks were successful
Build and release container directly from master / release (push) Successful in 6m2s
2025-05-05 13:14:29 -04:00
1fca61734b Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Has been cancelled
2025-05-05 11:22:18 -04:00
e447d4275e Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Failing after 48s
2025-05-05 11:21:11 -04:00
43cf4fabaa Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Has been cancelled
2025-05-05 11:12:45 -04:00
668aff0cc2 Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Failing after 29s
2025-05-05 11:11:41 -04:00
5c282637a6 ehh
Some checks failed
Build and release container directly from master / release (push) Failing after 28s
2025-05-05 11:10:56 -04:00
2ba1fe4db9 cant' start with gitea
Some checks failed
Build and release container directly from master / release (push) Failing after 34s
2025-05-05 11:09:20 -04:00
d891212545 Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64 2025-05-05 11:08:10 -04:00
f50559ac18 Add initial video player implementation with controls and features
Some checks failed
Build and release container directly from master / release (push) Failing after 34s
2025-05-05 10:56:06 -04:00
Emilien
d1bc15b8bf Release v2.20250504.0 2025-05-04 11:59:42 +02:00
Vyquos
1f028fee0f
Reflect companion secret character limit in example config comment (#5269)
Update the comments in the example config to show that the companion secret key must be exactly 16 characters long as per https://github.com/iv-org/invidious-companion/pull/81#issuecomment-2750675405.
2025-05-04 07:47:42 +00:00
absidue
2c1400c41e
Fix proxying live DASH streams (#4589) 2025-05-03 20:28:19 +00:00
Alex Maras
8fd0b82c38
feat: route to invidious companion on downloads (#5224) 2025-05-03 01:28:18 +02:00
Émilien (perso)
7579adc3a3
fix: fallback other yt clients no url found for adaptive formats (#5262) 2025-05-02 16:57:02 +02:00
efb4f5ff-1298-471a-8973-3d47447115dc
d567c6be6e
Fix minor casing issues in brand names (#5258) 2025-05-02 15:36:31 +02:00
29 changed files with 623 additions and 616 deletions

View File

@ -25,7 +25,7 @@ Lint/NotNil:
Lint/SpecFilename: Lint/SpecFilename:
Excluded: Excluded:
- spec/*_helper.cr - spec/parsers_helper.cr
# #

View File

@ -23,10 +23,17 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with: # - name: Set up QEMU
platforms: arm64 # uses: docker/setup-qemu-action@v3
# 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
@ -34,20 +41,18 @@ jobs:
- name: Login to registry - name: Login to registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: quay.io registry: gitea.ghost.tel
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }} password: ${{ secrets.PASSWORD }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: quay.io/invidious/invidious images: gitea.ghost.tel/knight/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
@ -55,7 +60,6 @@ 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: |
@ -65,23 +69,9 @@ jobs:
id: meta-arm64 id: meta-arm64
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: quay.io/invidious/invidious images: gitea.ghost.tel/knight/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

@ -2,6 +2,12 @@
## vX.Y.0 (future) ## vX.Y.0 (future)
## v2.20250504.0
Small release with quick workaround fix for issue #4251 (Nil assertion failed).
PR: https://github.com/iv-org/invidious/issues/5263
## v2.20250314.0 ## v2.20250314.0
### Wrap-up ### Wrap-up

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,6 +1,8 @@
'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,
@ -50,9 +52,27 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
return options; return options;
}; };
var player = videojs('player', options); console.log('[Invidious Debug] Initializing videojs...');
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 = (
@ -70,6 +90,7 @@ 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...');
@ -86,13 +107,54 @@ 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.');
} }
}); });
if (video_data.params.quality === 'dash') { try {
player.reloadSourceOnError({ player.on('error', function () {
errorInterval: 10 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') {
console.log('[Invidious Debug] Initializing reloadSourceOnError...');
try {
player.reloadSourceOnError({
errorInterval: 10
});
console.log('[Invidious Debug] reloadSourceOnError initialized.');
} catch (e) {
console.error('[Invidious Debug] reloadSourceOnError FAILED:', e);
}
} }
/** /**
@ -162,116 +224,6 @@ 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
* *
@ -351,16 +303,12 @@ 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) {
const url = new URL(location); console.log('[Invidious Debug] Setting up player position save/restore...');
const hasTimeParam = url.searchParams.has('t'); var lastUpdated = 0;
const rememberedTime = get_video_time(); var save_video_time = function(time) {
let lastUpdated = 0; const all_video_times = get_all_video_times();
all_video_times[video_data.id] = time;
if(!hasTimeParam) { helpers.storage.set(save_player_pos_key, all_video_times);
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 () {
@ -376,64 +324,138 @@ 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') {
player.httpSourceSelector(); console.log('[Invidious Debug] Initializing httpSourceSelector...');
console.log('[Invidious Debug] Player sources BEFORE httpSourceSelector init:', JSON.stringify(player.currentSources()));
if (video_data.params.quality_dash !== 'auto') { // --- START Pre-Plugin Environment Check ---
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 () {
player.on('loadedmetadata', function () { // Add listener for quality level errors
const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;}); const qualityLevels = player.qualityLevels();
let targetQualityLevel; if (qualityLevels) {
switch (video_data.params.quality_dash) { qualityLevels.on('error', function(event) {
case 'best': console.error('[Invidious Debug] QualityLevels Error Event:', event);
targetQualityLevel = qualityLevels.length - 1; });
break; console.log('[Invidious Debug] QualityLevels error listener attached.'); // Added
case 'worst':
targetQualityLevel = 0; // Log initial quality levels once metadata is loaded
break; player.on('loadedmetadata', function() {
default: try {
const targetHeight = parseInt(video_data.params.quality_dash); console.log('[Invidious Debug] Event: loadedmetadata triggered.'); // Added
for (let i = 0; i < qualityLevels.length; i++) { const levels = Array.from(player.qualityLevels());
if (qualityLevels[i].height <= targetHeight) // Log detailed info about each level
targetQualityLevel = i; 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}))));
else } catch (e) {
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) {
qualityLevels.forEach(function (level, index) { level.enabled = (index === targetQualityLevel);
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
} }
} }
player.vttThumbnails({ console.log('[Invidious Debug] Initializing vttThumbnails...');
src: '/api/v1/storyboards/' + video_data.id + '?height=90', try {
showTimestamp: true player.vttThumbnails({
}); 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,
@ -468,65 +490,215 @@ 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.');
} }
}); });
}); });
} }
function change_volume(delta) { player.on('play', function() {
const curVolume = player.volume(); console.log('[Invidious Debug] Event: play triggered. Current source:', JSON.stringify(player.currentSource()));
let newVolume = curVolume + delta; });
newVolume = helpers.clamp(newVolume, 0, 1);
player.volume(newVolume); player.on('playing', function() {
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);
}
} }
function toggle_muted() { // Detect mobile users and initialize mobileUi for better UX
player.muted(!player.muted()); // Detection code taken from https://stackoverflow.com/a/20293441
function isMobile() {
return typeof window.orientation !== 'undefined' || navigator.userAgent.indexOf('IEMobile') !== -1;
} }
function skip_seconds(delta) { if (isMobile()) {
const duration = player.duration(); console.log('[Invidious Debug] Mobile detected, initializing mobile UI...');
const curTime = player.currentTime(); try {
let newTime = curTime + delta; player.mobileUi({ touchControls: { seekSeconds: 5, tapTimeout: 200, disableOnEnd: true } });
newTime = helpers.clamp(newTime, 0, duration); console.log('[Invidious Debug] Mobile UI initialized.');
player.currentTime(newTime); } catch (e) {
console.error('[Invidious Debug] Mobile UI FAILED:', e);
}
} }
function set_seconds_after_start(delta) { if (player.markers && !video_data.params.listen && video_data.params.chapters) {
const start = video_data.params.video_start; console.log('[Invidious Debug] Initializing markers...');
player.currentTime(start + delta); var markers = [];
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;
@ -585,10 +757,18 @@ 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());
@ -597,6 +777,7 @@ 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.
@ -685,7 +866,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/81868#post323588 // Common FPS: https://forum.videohelp.com/threads/81864#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;
@ -731,9 +912,6 @@ 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 () {
@ -748,6 +926,7 @@ 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) {
@ -759,6 +938,7 @@ 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 () {
@ -770,19 +950,25 @@ 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/')) {
const Button = videojs.getComponent('Button'); console.log('[Invidious Debug] Adding Watch on Invidious button...');
let watch_on_invidious_button = new Button(player); try {
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 () {
@ -792,3 +978,5 @@ addEventListener('DOMContentLoaded', function () {
changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href);
}); });
}); });
console.log('[Invidious Debug] player.js finished loading.');

View File

@ -90,14 +90,14 @@ db:
## ##
## API key for Invidious companion, used for securing the communication ## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion. ## between Invidious and Invidious companion.
## The size of the key needs to be more or equal to 16. ## The key needs to be exactly 16 characters long.
## ##
## Note: This parameter is mandatory when Invidious companion is enabled ## Note: This parameter is mandatory when Invidious companion is enabled
## and should be a random string. ## and should be a random string.
## Such random string can be generated on linux with the following ## Such random string can be generated on linux with the following
## command: `pwgen 16 1` ## command: `pwgen 16 1`
## ##
## Accepted values: a string ## Accepted values: a string (of length 16)
## Default: <none> ## Default: <none>
## ##
#invidious_companion_key: "CHANGE_ME!!" #invidious_companion_key: "CHANGE_ME!!"
@ -206,7 +206,7 @@ https_only: false
#disable_proxy: false #disable_proxy: false
## ##
## Max size of the HTTP pool used to connect to youtube. Each ## Size of the HTTP pool used to connect to youtube. Each
## domain ('youtube.com', 'ytimg.com', ...) has its own pool. ## domain ('youtube.com', 'ytimg.com', ...) has its own pool.
## ##
## Accepted values: a positive integer ## Accepted values: a positive integer
@ -214,16 +214,6 @@ https_only: false
## ##
#pool_size: 100 #pool_size: 100
##
## Amount of seconds to wait for a client to be free from the pool
## before raising an error
##
##
## Accepted values: a positive integer
## Default: 5
##
#pool_checkout_timeout: 5
## ##
## Additional cookies to be sent when requesting the youtube API. ## Additional cookies to be sent when requesting the youtube API.

View File

@ -1,112 +0,0 @@
# Due to the way that specs are handled this file cannot be run
# together with everything else without causing a compile time error
#
# TODO: Allow running different isolated spec through make
#
# For now run this with `crystal spec -p spec/helpers/networking/connection_pool_spec.cr -Drunning_by_self`
{% skip_file unless flag?(:running_by_self) %}
# Based on https://github.com/jgaskins/http_client/blob/958cf56064c0d31264a117467022b90397eb65d7/spec/http_client_spec.cr
require "wait_group"
require "uri"
require "http"
require "http/server"
require "http_proxy"
require "db"
require "pg"
require "spectator"
require "../../load_config_helper"
require "../../../src/invidious/helpers/crystal_class_overrides"
require "../../../src/invidious/connection/*"
TEST_SERVER_URL = URI.parse("http://localhost:12345")
server = HTTP::Server.new do |context|
request = context.request
response = context.response
case {request.method, request.path}
when {"GET", "/get"}
response << "get"
when {"POST", "/post"}
response.status = :created
response << "post"
when {"GET", "/sleep"}
duration = request.query_params["duration_sec"].to_i.seconds
sleep duration
end
end
spawn server.listen 12345
Fiber.yield
Spectator.describe Invidious::ConnectionPool do
describe "Pool" do
it "Can make a requests through standard HTTP methods" do
pool = Invidious::ConnectionPool::Pool.new(max_capacity: 100) { next make_client(TEST_SERVER_URL) }
expect(pool.get("/get").body).to eq("get")
expect(pool.post("/post").body).to eq("post")
end
it "Can make streaming requests" do
pool = Invidious::ConnectionPool::Pool.new(max_capacity: 100) { next make_client(TEST_SERVER_URL) }
expect(pool.get("/get", &.body_io.gets_to_end)).to eq("get")
expect(pool.get("/post", &.body)).to eq("")
expect(pool.post("/post", &.body_io.gets_to_end)).to eq("post")
end
it "Allows more than one clients to be checked out (if applicable)" do
pool = Invidious::ConnectionPool::Pool.new(max_capacity: 100) { next make_client(TEST_SERVER_URL) }
pool.checkout do |_|
expect(pool.post("/post").body).to eq("post")
end
end
it "Can make multiple requests with the same client" do
pool = Invidious::ConnectionPool::Pool.new(max_capacity: 100) { next make_client(TEST_SERVER_URL) }
pool.checkout do |client|
expect(client.get("/get").body).to eq("get")
expect(client.post("/post").body).to eq("post")
expect(client.get("/get").body).to eq("get")
end
end
it "Allows concurrent requests" do
pool = Invidious::ConnectionPool::Pool.new(max_capacity: 100) { next make_client(TEST_SERVER_URL) }
responses = [] of HTTP::Client::Response
WaitGroup.wait do |wg|
100.times do
wg.spawn { responses << pool.get("/get") }
end
end
expect(responses.map(&.body)).to eq(["get"] * 100)
end
it "Raises on checkout timeout" do
pool = Invidious::ConnectionPool::Pool.new(max_capacity: 2, timeout: 0.01) { next make_client(TEST_SERVER_URL) }
# Long running requests
2.times do
spawn { pool.get("/sleep?duration_sec=2") }
end
Fiber.yield
expect { pool.get("/get") }.to raise_error(Invidious::ConnectionPool::Error)
end
it "Raises when an error is encountered" do
pool = Invidious::ConnectionPool::Pool.new(max_capacity: 100) { next make_client(TEST_SERVER_URL) }
expect { pool.get("/get") { raise IO::Error.new } }.to raise_error(Invidious::ConnectionPool::Error)
end
end
end

View File

@ -1,15 +0,0 @@
require "yaml"
require "log"
abstract class Kemal::BaseLogHandler
end
require "../src/invidious/config"
require "../src/invidious/jobs/base_job"
require "../src/invidious/jobs.cr"
require "../src/invidious/user/preferences.cr"
require "../src/invidious/helpers/logger"
require "../src/invidious/helpers/utils"
CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
HMAC_KEY = CONFIG.hmac_key

View File

@ -35,7 +35,6 @@ require "protodec/utils"
require "./invidious/database/*" require "./invidious/database/*"
require "./invidious/database/migrations/*" require "./invidious/database/migrations/*"
require "./invidious/connection/*"
require "./invidious/http_server/*" require "./invidious/http_server/*"
require "./invidious/helpers/*" require "./invidious/helpers/*"
require "./invidious/yt_backend/*" require "./invidious/yt_backend/*"
@ -92,31 +91,15 @@ SOFTWARE = {
"branch" => "#{CURRENT_BRANCH}", "branch" => "#{CURRENT_BRANCH}",
} }
YT_POOL = Invidious::ConnectionPool::Pool.new( YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
max_capacity: CONFIG.pool_size,
timeout: CONFIG.pool_checkout_timeout
) do
next make_client(YT_URL, force_resolve: true)
end
# Image request pool # Image request pool
GGPHT_URL = URI.parse("https://yt3.ggpht.com") GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
GGPHT_POOL = Invidious::ConnectionPool::Pool.new( COMPANION_POOL = CompanionConnectionPool.new(
max_capacity: CONFIG.pool_size, capacity: CONFIG.pool_size
timeout: CONFIG.pool_checkout_timeout )
) do
next make_client(GGPHT_URL, force_resolve: true)
end
COMPANION_POOL = Invidious::ConnectionPool::Pool.new(
max_capacity: CONFIG.pool_size,
reinitialize_proxy: false
) do
companion = CONFIG.invidious_companion.sample
next make_client(companion.private_url, use_http_proxy: false)
end
# CLI # CLI
Kemal.config.extra_options do |parser| Kemal.config.extra_options do |parser|

View File

@ -166,7 +166,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
} }
LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed") LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed")
rss = YT_POOL.get("/feeds/videos.xml?channel_id=#{ucid}").body rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body
LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed") LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed")
rss = XML.parse(rss) rss = XML.parse(rss)

View File

@ -157,13 +157,8 @@ class Config
property host_binding : String = "0.0.0.0" property host_binding : String = "0.0.0.0"
# Path and permissions to make Invidious listen on a UNIX socket instead of a TCP port # Path and permissions to make Invidious listen on a UNIX socket instead of a TCP port
property socket_binding : SocketBindingConfig? = nil property socket_binding : SocketBindingConfig? = nil
# Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
# Max pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool)
property pool_size : Int32 = 100 property pool_size : Int32 = 100
# Amount of seconds to wait for a client to be free from the pool before rasing an error
property pool_checkout_timeout : Float64 = 5
# HTTP Proxy configuration # HTTP Proxy configuration
property http_proxy : HTTPProxyConfig? = nil property http_proxy : HTTPProxyConfig? = nil

View File

@ -1,53 +0,0 @@
def add_yt_headers(request)
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["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
request.headers["Accept-Language"] ||= "en-us,en;q=0.5"
# Preserve original cookies and add new YT consent cookie for EU servers
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"
if !CONFIG.cookies.empty?
request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
end
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true)
client = HTTP::Client.new(url)
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy && use_http_proxy
# Force the usage of a specific configured IP Family
if force_resolve
client.family = CONFIG.force_resolve
client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
end
client.before_request { |r| add_yt_headers(r) } if url.host.try &.ends_with?("youtube.com") || force_youtube_headers
client.read_timeout = 10.seconds
client.connect_timeout = 10.seconds
return client
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &)
client = make_client(url, region, force_resolve: force_resolve, use_http_proxy: use_http_proxy)
begin
yield client
ensure
client.close
end
end
def make_configured_http_proxy_client
# This method is only called when configuration for an HTTP proxy are set
config_proxy = CONFIG.http_proxy.not_nil!
return HTTP::Proxy::Client.new(
config_proxy.host,
config_proxy.port,
username: config_proxy.user,
password: config_proxy.password,
)
end

View File

@ -1,116 +0,0 @@
module Invidious::ConnectionPool
# A connection pool to reuse `HTTP::Client` connections
struct Pool
getter pool : DB::Pool(HTTP::Client)
# Creates a connection pool with the provided options, and client factory block.
def initialize(
*,
max_capacity : Int32 = 5,
timeout : Float64 = 5.0,
@reinitialize_proxy : Bool = true, # Whether or not http-proxy should be reinitialized on checkout
&client_factory : -> HTTP::Client
)
pool_options = DB::Pool::Options.new(
initial_pool_size: 0,
max_pool_size: max_capacity,
max_idle_pool_size: max_capacity,
checkout_timeout: timeout
)
@pool = DB::Pool(HTTP::Client).new(pool_options, &client_factory)
end
{% for method in %w[get post put patch delete head options] %}
# Streaming API for {{method.id.upcase}} request.
# The response will have its body as an `IO` accessed via `HTTP::Client::Response#body_io`.
def {{method.id}}(*args, **kwargs, &)
self.checkout do | client |
client.{{method.id}}(*args, **kwargs) do | response |
result = yield response
return result
ensure
response.body_io?.try &.skip_to_end
end
end
end
# Executes a {{method.id.upcase}} request.
# The response will have its body as a `String`, accessed via `HTTP::Client::Response#body`.
def {{method.id}}(*args, **kwargs)
self.checkout do | client |
return client.{{method.id}}(*args, **kwargs)
end
end
{% end %}
# Checks out a client in the pool
def checkout(&)
# If a client has been deleted from the pool
# we won't try to release it
client_exists_in_pool = true
http_client = pool.checkout
# When the HTTP::Client connection is closed, the automatic reconnection
# feature will create a new IO to connect to the server with
#
# This new TCP IO will be a direct connection to the server and will not go
# through the proxy. As such we'll need to reinitialize the proxy connection
http_client.proxy = make_configured_http_proxy_client() if @reinitialize_proxy && CONFIG.http_proxy
response = yield http_client
rescue ex : DB::PoolTimeout
# Failed to checkout a client
raise ConnectionPool::PoolCheckoutError.new(ex.message)
rescue ex
# An error occurred with the client itself.
# Delete the client from the pool and close the connection
if http_client
client_exists_in_pool = false
@pool.delete(http_client)
http_client.close
end
# Raise exception for outer methods to handle
raise ConnectionPool::Error.new(ex.message, cause: ex)
ensure
pool.release(http_client) if http_client && client_exists_in_pool
end
end
class Error < Exception
end
# Raised when the pool failed to get a client in time
class PoolCheckoutError < Error
end
# Mapping of subdomain => Invidious::ConnectionPool::Pool
# This is needed as we may need to access arbitrary subdomains of ytimg
private YTIMG_POOLS = {} of String => ConnectionPool::Pool
# Fetches a HTTP pool for the specified subdomain of ytimg.com
#
# Creates a new one when the specified pool for the subdomain does not exist
def self.get_ytimg_pool(subdomain)
if pool = YTIMG_POOLS[subdomain]?
return pool
else
LOGGER.info("ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"")
url = URI.parse("https://#{subdomain}.ytimg.com")
pool = ConnectionPool::Pool.new(
max_capacity: CONFIG.pool_size,
timeout: CONFIG.pool_checkout_timeout
) do
next make_client(url, force_resolve: true)
end
YTIMG_POOLS[subdomain] = pool
return pool
end
end
end

View File

@ -23,10 +23,16 @@ 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='/download'" str << " action='#{url}'"
str << " method='post'" str << " method='post'"
str << " rel='noopener'" str << " rel='noopener'"
str << " target='_blank'>" str << " target='_blank'>"

View File

@ -26,7 +26,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
end end
video_id = "CvFH_6DNRCY" if rdid.starts_with? "OLAK5uy_" video_id = "CvFH_6DNRCY" if rdid.starts_with? "OLAK5uy_"
response = YT_POOL.get("/watch?v=#{video_id}&list=#{rdid}&gl=US&hl=en", headers) response = YT_POOL.client &.get("/watch?v=#{video_id}&list=#{rdid}&gl=US&hl=en", headers)
initial_data = extract_initial_data(response.body) initial_data = extract_initial_data(response.body)
if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]? if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]?

View File

@ -26,7 +26,7 @@ module Invidious::Routes::API::Manifest
end end
if dashmpd = video.dash_manifest_url if dashmpd = video.dash_manifest_url
response = YT_POOL.get(URI.parse(dashmpd).request_target) response = YT_POOL.client &.get(URI.parse(dashmpd).request_target)
if response.status_code != 200 if response.status_code != 200
haltf env, status_code: response.status_code haltf env, status_code: response.status_code
@ -167,7 +167,7 @@ module Invidious::Routes::API::Manifest
# /api/manifest/hls_playlist/* # /api/manifest/hls_playlist/*
def self.get_hls_playlist(env) def self.get_hls_playlist(env)
response = YT_POOL.get(env.request.path) response = YT_POOL.client &.get(env.request.path)
if response.status_code != 200 if response.status_code != 200
haltf env, status_code: response.status_code haltf env, status_code: response.status_code
@ -223,7 +223,7 @@ module Invidious::Routes::API::Manifest
# /api/manifest/hls_variant/* # /api/manifest/hls_variant/*
def self.get_hls_variant(env) def self.get_hls_variant(env)
response = YT_POOL.get(env.request.path) response = YT_POOL.client &.get(env.request.path)
if response.status_code != 200 if response.status_code != 200
haltf env, status_code: response.status_code haltf env, status_code: response.status_code

View File

@ -106,7 +106,7 @@ module Invidious::Routes::API::V1::Videos
# Auto-generated captions often have cues that aren't aligned properly with the video, # Auto-generated captions often have cues that aren't aligned properly with the video,
# as well as some other markup that makes it cumbersome, so we try to fix that here # as well as some other markup that makes it cumbersome, so we try to fix that here
if caption.name.includes? "auto-generated" if caption.name.includes? "auto-generated"
caption_xml = YT_POOL.get(url).body caption_xml = YT_POOL.client &.get(url).body
settings_field = { settings_field = {
"Kind" => "captions", "Kind" => "captions",
@ -147,7 +147,7 @@ module Invidious::Routes::API::V1::Videos
query_params = uri.query_params query_params = uri.query_params
query_params["fmt"] = "vtt" query_params["fmt"] = "vtt"
uri.query_params = query_params uri.query_params = query_params
webvtt = YT_POOL.get(uri.request_target).body webvtt = YT_POOL.client &.get(uri.request_target).body
if webvtt.starts_with?("<?xml") if webvtt.starts_with?("<?xml")
webvtt = caption.timedtext_to_vtt(webvtt) webvtt = caption.timedtext_to_vtt(webvtt)
@ -300,7 +300,7 @@ module Invidious::Routes::API::V1::Videos
cache_annotation(id, annotations) cache_annotation(id, annotations)
end end
else # "youtube" else # "youtube"
response = YT_POOL.get("/annotations_invideo?video_id=#{id}") response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
if response.status_code != 200 if response.status_code != 200
haltf env, response.status_code haltf env, response.status_code

View File

@ -392,7 +392,7 @@ module Invidious::Routes::Channels
value = env.request.resource.split("/")[2] value = env.request.resource.split("/")[2]
body = "" body = ""
{"channel", "user", "c"}.each do |type| {"channel", "user", "c"}.each do |type|
response = YT_POOL.get("/#{type}/#{value}/live?disable_polymer=1") response = YT_POOL.client &.get("/#{type}/#{value}/live?disable_polymer=1")
if response.status_code == 200 if response.status_code == 200
body = response.body body = response.body
end end

View File

@ -92,7 +92,7 @@ module Invidious::Routes::Embed
return env.redirect url return env.redirect url
when "live_stream" when "live_stream"
response = YT_POOL.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}") response = YT_POOL.client &.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}")
video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"] video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"]
env.params.query.delete_all("channel") env.params.query.delete_all("channel")

View File

@ -9,10 +9,10 @@ module Invidious::Routes::ErrorRoutes
item = md["id"] item = md["id"]
# Check if item is branding URL e.g. https://youtube.com/gaming # Check if item is branding URL e.g. https://youtube.com/gaming
response = YT_POOL.get("/#{item}") response = YT_POOL.client &.get("/#{item}")
if response.status_code == 301 if response.status_code == 301
response = YT_POOL.get(URI.parse(response.headers["Location"]).request_target) response = YT_POOL.client &.get(URI.parse(response.headers["Location"]).request_target)
end end
if response.body.empty? if response.body.empty?
@ -40,7 +40,7 @@ module Invidious::Routes::ErrorRoutes
end end
# Check if item is video ID # Check if item is video ID
if item.match(/^[a-zA-Z0-9_-]{11}$/) && YT_POOL.head("/watch?v=#{item}").status_code != 404 if item.match(/^[a-zA-Z0-9_-]{11}$/) && YT_POOL.client &.head("/watch?v=#{item}").status_code != 404
env.response.headers["Location"] = url env.response.headers["Location"] = url
haltf env, status_code: 302 haltf env, status_code: 302
end end

View File

@ -160,9 +160,8 @@ module Invidious::Routes::Feeds
"default" => "http://www.w3.org/2005/Atom", "default" => "http://www.w3.org/2005/Atom",
} }
response = YT_POOL.get("/feeds/videos.xml?channel_id=#{ucid}") response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}")
return error_atom(404, NotFoundException.new("Channel does not exist.")) if response.status_code == 404 return error_atom(404, NotFoundException.new("Channel does not exist.")) if response.status_code == 404
rss = XML.parse(response.body) rss = XML.parse(response.body)
videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry| videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry|
@ -305,7 +304,7 @@ module Invidious::Routes::Feeds
end end
end end
response = YT_POOL.get("/feeds/videos.xml?playlist_id=#{plid}") response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
return error_atom(404, NotFoundException.new("Playlist does not exist.")) if response.status_code == 404 return error_atom(404, NotFoundException.new("Playlist does not exist.")) if response.status_code == 404
document = XML.parse(response.body) document = XML.parse(response.body)

View File

@ -12,7 +12,7 @@ module Invidious::Routes::Images
end end
begin begin
GGPHT_POOL.get(url, headers) do |resp| GGPHT_POOL.client &.get(url, headers) do |resp|
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
rescue ex rescue ex
@ -42,7 +42,7 @@ module Invidious::Routes::Images
end end
begin begin
ConnectionPool.get_ytimg_pool(authority).get(url, headers) do |resp| get_ytimg_pool(authority).client &.get(url, headers) do |resp|
env.response.headers["Connection"] = "close" env.response.headers["Connection"] = "close"
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
@ -65,7 +65,7 @@ module Invidious::Routes::Images
end end
begin begin
ConnectionPool.get_ytimg_pool("i9").get(url, headers) do |resp| get_ytimg_pool("i9").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
rescue ex rescue ex
@ -81,7 +81,7 @@ module Invidious::Routes::Images
end end
begin begin
YT_POOL.get(env.request.resource, headers) do |response| YT_POOL.client &.get(env.request.resource, headers) do |response|
env.response.status_code = response.status_code env.response.status_code = response.status_code
response.headers.each do |key, value| response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
@ -111,7 +111,7 @@ module Invidious::Routes::Images
if name == "maxres.jpg" if name == "maxres.jpg"
build_thumbnails(id).each do |thumb| build_thumbnails(id).each do |thumb|
thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg" thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg"
if ConnectionPool.get_ytimg_pool("i").head(thumbnail_resource_path, headers).status_code == 200 if get_ytimg_pool("i").client &.head(thumbnail_resource_path, headers).status_code == 200
name = thumb[:url] + ".jpg" name = thumb[:url] + ".jpg"
break break
end end
@ -127,7 +127,7 @@ module Invidious::Routes::Images
end end
begin begin
ConnectionPool.get_ytimg_pool("i").get(url, headers) do |resp| get_ytimg_pool("i").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
rescue ex rescue ex

View File

@ -464,7 +464,7 @@ module Invidious::Routes::Playlists
# Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos # Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos
def self.watch_videos(env) def self.watch_videos(env)
response = YT_POOL.get(env.request.resource) response = YT_POOL.client &.get(env.request.resource)
if url = response.headers["Location"]? if url = response.headers["Location"]?
url = URI.parse(url).request_target url = URI.parse(url).request_target
return env.redirect url return env.redirect url

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.com/) if !host.matches?(/[\w-]+\.(?:googlevideo|c\.youtube)\.com/)
return error_template(400, "Invalid \"host\" parameter.") return error_template(400, "Invalid \"host\" parameter.")
end end
@ -37,7 +37,8 @@ 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"]?
if range_header.nil? sq = query_params["sq"]?
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

View File

@ -293,6 +293,9 @@ 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"]? || ""
@ -328,13 +331,7 @@ module Invidious::Routes::Watch
env.params.query["title"] = filename env.params.query["title"] = filename
env.params.query["local"] = "true" env.params.query["local"] = "true"
if (CONFIG.invidious_companion.present?) return Invidious::Routes::VideoPlayback.latest_version(env)
video = get_video(video_id)
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
else
return Invidious::Routes::VideoPlayback.latest_version(env)
end
else else
return error_template(400, "Invalid label or itag") return error_template(400, "Invalid label or itag")
end end

View File

@ -16,11 +16,11 @@ module Invidious::Search
# Search a youtube channel # Search a youtube channel
# TODO: clean code, and rely more on YoutubeAPI # TODO: clean code, and rely more on YoutubeAPI
def channel(query : Query) : Array(SearchItem) def channel(query : Query) : Array(SearchItem)
response = YT_POOL.get("/channel/#{query.channel}") response = YT_POOL.client &.get("/channel/#{query.channel}")
if response.status_code == 404 if response.status_code == 404
response = YT_POOL.get("/user/#{query.channel}") response = YT_POOL.client &.get("/user/#{query.channel}")
response = YT_POOL.get("/c/#{query.channel}") if response.status_code == 404 response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404
initial_data = extract_initial_data(response.body) initial_data = extract_initial_data(response.body)
ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?) ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?)
raise ChannelSearchException.new(query.channel) if !ucid raise ChannelSearchException.new(query.channel) if !ucid

View File

@ -109,27 +109,20 @@ def extract_video_info(video_id : String)
params["reason"] = JSON::Any.new(reason) if reason params["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present? if !CONFIG.invidious_companion.present?
new_player_response = 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.")
# Don't use Android test suite client if po_token is passed because po_token doesn't players_fallback = [YoutubeAPI::ClientType::WebMobile, YoutubeAPI::ClientType::TvHtml5]
# work for Android test suite client. players_fallback.each do |player_fallback|
if reason.nil? && CONFIG.po_token.nil? client_config.client_type = player_fallback
# Fetch the video streams using an Android client in order to get the player_fallback_response = try_fetch_streaming_data(video_id, client_config)
# decrypted URLs and maybe fix throttling issues (#2194). See the if player_fallback_response && player_fallback_response["streamingData"]? &&
# following issue for an explanation about decrypted URLs: player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562 streaming_data = player_response["streamingData"].as_h
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
new_player_response = try_fetch_streaming_data(video_id, client_config) player_response["streamingData"] = JSON::Any.new(streaming_data)
end break
end
# Replace player response and reset reason end
if !new_player_response.nil?
# Preserve captions & storyboard data before replacement
new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
new_player_response["captions"] = player_response["captions"] if player_response["captions"]?
player_response = new_player_response
params.delete("reason")
end end
end end

View File

@ -0,0 +1,153 @@
# Mapping of subdomain => YoutubeConnectionPool
# This is needed as we may need to access arbitrary subdomains of ytimg
private YTIMG_POOLS = {} of String => YoutubeConnectionPool
struct YoutubeConnectionPool
property! url : URI
property! capacity : Int32
property! timeout : Float64
property pool : DB::Pool(HTTP::Client)
def initialize(url : URI, @capacity = 5, @timeout = 5.0)
@url = url
@pool = build_pool()
end
def client(&)
conn = pool.checkout
# Proxy needs to be reinstated every time we get a client from the pool
conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
begin
response = yield conn
rescue ex
conn.close
conn = make_client(url, force_resolve: true)
response = yield conn
ensure
pool.release(conn)
end
response
end
private def build_pool
options = DB::Pool::Options.new(
initial_pool_size: 0,
max_pool_size: capacity,
max_idle_pool_size: capacity,
checkout_timeout: timeout
)
DB::Pool(HTTP::Client).new(options) do
next make_client(url, force_resolve: true)
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)
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["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
request.headers["Accept-Language"] ||= "en-us,en;q=0.5"
# Preserve original cookies and add new YT consent cookie for EU servers
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"
if !CONFIG.cookies.empty?
request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
end
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true)
client = HTTP::Client.new(url)
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy && use_http_proxy
# Force the usage of a specific configured IP Family
if force_resolve
client.family = CONFIG.force_resolve
client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
end
client.before_request { |r| add_yt_headers(r) } if url.host.try &.ends_with?("youtube.com") || force_youtube_headers
client.read_timeout = 10.seconds
client.connect_timeout = 10.seconds
return client
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &)
client = make_client(url, region, force_resolve: force_resolve, use_http_proxy: use_http_proxy)
begin
yield client
ensure
client.close
end
end
def make_configured_http_proxy_client
# This method is only called when configuration for an HTTP proxy are set
config_proxy = CONFIG.http_proxy.not_nil!
return HTTP::Proxy::Client.new(
config_proxy.host,
config_proxy.port,
username: config_proxy.user,
password: config_proxy.password,
)
end
# Fetches a HTTP pool for the specified subdomain of ytimg.com
#
# Creates a new one when the specified pool for the subdomain does not exist
def get_ytimg_pool(subdomain)
if pool = YTIMG_POOLS[subdomain]?
return pool
else
LOGGER.info("ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"")
pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size)
YTIMG_POOLS[subdomain] = pool
return pool
end
end

View File

@ -639,13 +639,15 @@ module YoutubeAPI
LOGGER.trace("YoutubeAPI: POST data: #{data}") LOGGER.trace("YoutubeAPI: POST data: #{data}")
# Send the POST request # Send the POST request
body = YT_POOL.post(url, headers: headers, body: data.to_json) do |response| body = YT_POOL.client() do |client|
if response.status_code != 200 client.post(url, headers: headers, body: data.to_json) do |response|
raise InfoException.new("Error: non 200 status code. Youtube API returned \ if response.status_code != 200
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \ raise InfoException.new("Error: non 200 status code. Youtube API returned \
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.") status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
end
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
end end
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
end end
# Convert result to Hash # Convert result to Hash
@ -693,7 +695,7 @@ module YoutubeAPI
# Send the POST request # Send the POST request
begin begin
response = COMPANION_POOL.post(endpoint, headers: headers, body: data.to_json) response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json)
body = response.body body = response.body
if (response.status_code != 200) if (response.status_code != 200)
raise Exception.new( raise Exception.new(