-
-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Piped/Invidious #39
Comments
Invidious would be nice as well, I use Piped myself, but it's always better to have multiple options |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as outdated.
This comment was marked as outdated.
I've modified Ajay's script again.
// ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.2
// @description Adds support for DeArrow in Invidious
// @author You
// @match https://yewtu.be/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant GM.xmlHttpRequest
// @author Macic-Dev
// @author Minion3665
// @author Basil
// @author Ajay
// ==/UserScript==
(async function() {
'use strict';
/**
* A simple fetch polyfill that uses the GreaseMonkey API
*
* @param {string} url - the URL to fetch
*/
function fetch(url) {
return new Promise(resolve => {
GM.xmlHttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: res => {
const headers = res.responseHeaders
.split('\r\n')
.reduce((prev, str) => {
const [key, val] = str.split(/: (.*)/s);
if (key === '') return prev;
prev[key.toLowerCase()] = val;
return prev;
}, {});
resolve({
headers: {
get: key => headers[key.toLowerCase()]
},
status: res.status,
blob: async () => res.response,
json: async () => JSON.parse(await new Response(res.response).text())
});
}
});
});
}
/**
* Converts a Blob to a data URL in order to get around CSP
*
* @param {Blob} blob - the URL to fetch
*/
function blobToDataURI(blob) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result);
});
}
/**
* Fetch data for a video, and then update the elements passed to reflect that
*
* @param {string} videoId - the YouTube ID of the video (the bit that comes after v= in watch URLs)
* @param {HTMLElement} titleElement - the element containing the video title which will be updated if DeArrow has a title
* @param {HTMLElement|undefined} thumbnailElement - the element containing the video thumbnail which will be updated if DeArrow has a thumbnail
*/
async function fetchAndUpdate(videoId, titleElement, thumbnailElement = undefined) {
const oldTitle = titleElement.textContent;
const oldThumbnail = thumbnailElement?.src;
const cachedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}`);
const cachedNewTitle = cachedThumbnailAPIResponse.headers.get('X-Title');
if (cachedNewTitle) replaceTitle(titleElement, cachedNewTitle);
if (thumbnailElement !== undefined && cachedThumbnailAPIResponse.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = await blobToDataURI(cachedNewThumbnail);
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let topTitle = brandingAPIResponse.titles[0];
let usedTitle = (topTitle && (topTitle.votes >= 0 || topTitle.locked)) ? topTitle : null;
replaceTitle(titleElement, usedTitle?.title ?? oldTitle);
}
{
let topThumbnail = brandingAPIResponse.thumbnails[0];
let usedThumbnail = (topThumbnail && (topThumbnail.votes >= 0 || topThumbnail.locked)) ? topThumbnail : null;
let randomTime = brandingAPIResponse.videoDuration ? brandingAPIResponse.videoDuration * brandingAPIResponse.randomTime : 0;
if (usedThumbnail && usedThumbnail.original) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${usedThumbnail?.timestamp ?? randomTime}`);
thumbnailElement.src = await blobToDataURI(await votedThumbnailAPIResponse.blob());
}
}
}
// Replaces the title of elements
function replaceTitle(element, text) {
if (element.nodeName === 'H1') {
for (const child of element.childNodes) {
if (child.nodeName !== '#text') continue;
child.textContent = text;
break;
}
} else {
element.textContent = text;
}
}
// Videos on page
const thumbnailDivs = document.querySelectorAll('div.thumbnail');
thumbnailDivs.forEach((div) => {
const videoUrl = new URL(div.parentNode.href);
const videoId = videoUrl.searchParams.get('v');
const titleP = [...div.parentNode.querySelectorAll('p')][1];
const thumbnailImg = div.querySelector('img.thumbnail');
void fetchAndUpdate(videoId, titleP, thumbnailImg);
});
// Current video
const currentUrl = new URL(document.URL);
if (currentUrl.pathname == '/watch') {
const currentId = currentUrl.searchParams.get('v');
const titleH = document.querySelector('h1');
const thumbnailDiv = document.querySelector('.vjs-poster');
await fetchAndUpdate(currentId, titleH, thumbnailDiv);
if (thumbnailDiv.src) thumbnailDiv.style.backgroundImage = `url(${thumbnailDiv.src})`;
document.title = `${titleH.textContent} - Invidious`;
}
})(); @aggarwalsushant This may fix your issue. Also, you have to change the URL in @match to the URL of whichever Invidious instance you're using. |
thanks! that works great. |
Seems doesn't work with piped for now. |
For Piped TeamPiped/Piped#2575 (comment)
It looks like it doesn't support random thumbnails |
Piped now has native support for DeArrow everywhere with TeamPiped/Piped@9539d51.
Is there any explanation on what this feature is supposed to do? The API documentation currently doesn't have any information on what the I've got another question - what happens when DeArrow doesn't have a video in its database? Piped fetches DeArrow content using |
Correct, there is this part in the documentation
If you'd like to wait on the thumbnail generator supporting the 0 to 1 numbers directly (which I plan to do), so you don't have to deal with getting the video duraton yourself, you can track ajayyy/DeArrowThumbnailCache#4 As for what to do about videos not in the request due to using |
I have ported an Alea implementation to Java. The random numbers generated from the seed don't seem to match however: DeArrow: https://sponsor.ajay.app/api/branding?videoID=VyI-2c9KI1I The randomTIme for this specific video is |
@FireMasterK nice! check on a video without SponsorBlock segments (they are the ones that wouldn't appear in the hash based API). If there are SponsorBlock segments, then it uses those to not choose a timestamp in a segment |
@gBasil Script sadly stopped working today. Not working on Invidious anymore. |
that is a shame... what invidious instance are you using? |
my own. but tested other instances (edited the script to instances url's). none of them working anymore. |
Strange, works for me on my own instance. Try checking the console in the browser dev tools and look for error messages. |
I tried now with Chrome, Firefox and Chromium (i self using librewolf). On Windows and Linux the same behaviour: Not working. (violentmonkey was the only one extension) |
please may you try // ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.3
// @description Adds support for DeArrow in Invidious
// @match https://yewtu.be/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant GM.xmlHttpRequest
// @author Basil, Macic-Dev, Minion3665, Ajay
// ==/UserScript==
(async function() {
'use strict';
/**
* A simple fetch polyfill that uses the GreaseMonkey API
*
* @param {string} url - the URL to fetch
*/
function fetch(url) {
return new Promise(resolve => {
GM.xmlHttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: res => {
const headers = res.responseHeaders
.split('\r\n')
.reduce((prev, str) => {
const [key, val] = str.split(/: (.*)/s);
if (key === '') return prev;
prev[key.toLowerCase()] = val;
return prev;
}, {});
resolve({
headers: {
get: key => headers[key.toLowerCase()]
},
status: res.status,
blob: async () => res.response,
json: async () => JSON.parse(await new Response(res.response).text())
});
}
});
});
}
/**
* Converts a Blob to a data URL in order to get around CSP
*
* @param {Blob} blob - the URL to fetch
*/
function blobToDataURI(blob) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result);
});
}
/**
* Fetch data for a video, and then update the elements passed to reflect that
*
* @param {string} videoId - the YouTube ID of the video (the bit that comes after v= in watch URLs)
* @param {HTMLElement} titleElement - the element containing the video title which will be updated if DeArrow has a title
* @param {HTMLElement|undefined} thumbnailElement - the element containing the video thumbnail which will be updated if DeArrow has a thumbnail
*/
async function fetchAndUpdate(videoId, titleElement, thumbnailElement = undefined) {
const oldTitle = titleElement.textContent;
const oldThumbnail = thumbnailElement?.src;
const cachedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}`);
const cachedNewTitle = cachedThumbnailAPIResponse.headers.get('X-Title');
if (cachedNewTitle) replaceTitle(titleElement, cachedNewTitle);
if (thumbnailElement !== undefined && cachedThumbnailAPIResponse.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = await blobToDataURI(cachedNewThumbnail);
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let topTitle = brandingAPIResponse.titles[0];
let usedTitle = (topTitle && (topTitle.votes >= 0 || topTitle.locked)) ? topTitle : null;
replaceTitle(titleElement, usedTitle?.title ?? oldTitle);
}
{
let topThumbnail = brandingAPIResponse.thumbnails[0];
let usedThumbnail = (topThumbnail && (topThumbnail.votes >= 0 || topThumbnail.locked)) ? topThumbnail : null;
let randomTime = brandingAPIResponse.videoDuration ? brandingAPIResponse.videoDuration * brandingAPIResponse.randomTime : 0;
if (usedThumbnail && usedThumbnail.original) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${usedThumbnail?.timestamp ?? randomTime}`);
thumbnailElement.src = await blobToDataURI(await votedThumbnailAPIResponse.blob());
}
}
}
// Replaces the title of elements
function replaceTitle(element, text) {
if (element.nodeName === 'H1') {
for (const child of element.childNodes) {
if (child.nodeName !== '#text') continue;
child.textContent = text;
break;
}
} else {
element.textContent = text;
}
}
// Videos on page
const thumbnailDivs = document.querySelectorAll('div.thumbnail');
thumbnailDivs.forEach((div) => {
const videoUrl = new URL(div.parentNode.href ?? div.querySelector('a').href);
const videoId = videoUrl.searchParams.get('v');
const titleP = [...div.parentNode.querySelectorAll('p')][1];
const thumbnailImg = div.querySelector('img.thumbnail');
void fetchAndUpdate(videoId, titleP, thumbnailImg);
});
// Current video
const currentUrl = new URL(document.URL);
if (currentUrl.pathname == '/watch') {
const currentId = currentUrl.searchParams.get('v');
const titleH = document.querySelector('h1');
const thumbnailDiv = document.querySelector('.vjs-poster');
await fetchAndUpdate(currentId, titleH, thumbnailDiv);
if (thumbnailDiv.src) thumbnailDiv.style.backgroundImage = `url(${thumbnailDiv.src})`;
document.title = `${titleH.textContent} - Invidious`;
}
})(); and tell me if it fixes the issue? Some instances appear to have their href placed differently (a version thing maybe?) |
So the issue seems to be that sometimes the Dearrow server responds with a Cloudflare 502 error and just returns an HTML page, presumably because it gets ratelimited by YouTube. I've updated it so that it only replaces the thumbnail if it receives an image. // ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.4
// @description Adds support for DeArrow in Invidious
// @match https://yewtu.be/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant GM.xmlHttpRequest
// @author Basil, Macic-Dev, Minion3665, Ajay
// ==/UserScript==
(async function() {
'use strict';
/**
* A simple fetch polyfill that uses the GreaseMonkey API
*
* @param {string} url - the URL to fetch
*/
function fetch(url) {
return new Promise(resolve => {
GM.xmlHttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: res => {
const headers = res.responseHeaders
.split('\r\n')
.reduce((prev, str) => {
const [key, val] = str.split(/: (.*)/s);
if (key === '') return prev;
prev[key.toLowerCase()] = val;
return prev;
}, {});
resolve({
headers: {
get: key => headers[key.toLowerCase()]
},
status: res.status,
blob: async () => res.response,
json: async () => JSON.parse(await new Response(res.response).text())
});
}
});
});
}
/**
* Converts a Blob to a data URL in order to get around CSP
*
* @param {Blob} blob - the URL to fetch
*/
function blobToDataURL(blob) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result);
});
}
/**
* Fetch data for a video, and then update the elements passed to reflect that
*
* @param {string} videoId - the YouTube ID of the video (the bit that comes after v= in watch URLs)
* @param {HTMLElement} titleElement - the element containing the video title which will be updated if DeArrow has a title
* @param {HTMLElement|undefined} thumbnailElement - the element containing the video thumbnail which will be updated if DeArrow has a thumbnail
*/
async function fetchAndUpdate(videoId, titleElement, thumbnailElement = undefined) {
const oldTitle = titleElement.textContent;
const oldThumbnail = thumbnailElement?.src;
const cachedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}`);
const cachedNewTitle = cachedThumbnailAPIResponse.headers.get('X-Title');
if (cachedNewTitle) replaceTitle(titleElement, cachedNewTitle);
if (thumbnailElement !== undefined && cachedThumbnailAPIResponse.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
const dataURL = await blobToDataURL(cachedNewThumbnail);
if (dataURL.startsWith('data:image/')) {
thumbnailElement.src = dataURL;
}
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let topTitle = brandingAPIResponse.titles[0];
let usedTitle = (topTitle && (topTitle.votes >= 0 || topTitle.locked)) ? topTitle : null;
replaceTitle(titleElement, usedTitle?.title ?? oldTitle);
}
{
let topThumbnail = brandingAPIResponse.thumbnails[0];
let usedThumbnail = (topThumbnail && (topThumbnail.votes >= 0 || topThumbnail.locked)) ? topThumbnail : null;
let randomTime = brandingAPIResponse.videoDuration ? brandingAPIResponse.videoDuration * brandingAPIResponse.randomTime : 0;
if (usedThumbnail && usedThumbnail.original) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${usedThumbnail?.timestamp ?? randomTime}`);
const dataURL = await blobToDataURL(await votedThumbnailAPIResponse.blob());
if (dataURL.startsWith('data:image/')) {
thumbnailElement.src = dataURL;
}
}
}
}
// Replaces the title of elements
function replaceTitle(element, text) {
if (element.nodeName === 'H1') {
for (const child of element.childNodes) {
if (child.nodeName !== '#text') continue;
child.textContent = text;
break;
}
} else {
element.textContent = text;
}
}
// Videos on page
const thumbnailDivs = document.querySelectorAll('div.thumbnail');
thumbnailDivs.forEach((div) => {
const videoUrl = new URL(div.parentNode.href ?? div.querySelector('a').href);
const videoId = videoUrl.searchParams.get('v');
const titleP = [...div.parentNode.querySelectorAll('p')][1];
const thumbnailImg = div.querySelector('img.thumbnail');
void fetchAndUpdate(videoId, titleP, thumbnailImg);
});
// Current video
const currentUrl = new URL(document.URL);
if (currentUrl.pathname == '/watch') {
const currentId = currentUrl.searchParams.get('v');
const titleH = document.querySelector('h1');
const thumbnailDiv = document.querySelector('.vjs-poster');
await fetchAndUpdate(currentId, titleH, thumbnailDiv);
if (thumbnailDiv.src) thumbnailDiv.style.backgroundImage = `url(${thumbnailDiv.src})`;
document.title = `${titleH.textContent} - Invidious`;
}
})(); |
Sry for the late response @gBasil Your latest submitted script here, works again! Great. Thanks! |
@gBasil One Problem with your userscript: The thumbnails for YouTube shorts is in portrait format and the enclosing container (div) changes to portrait as well, breaking the style of any feeds page. |
@Sommerwiesel Could you give me a link to a page (i.e. a channel page) so that I can reproduce that? I block the vertical Shorts videos with uBlock Origin, so I haven't run into this myself. |
@gBasil It's not a problem on channel pages because on there, shorts are in a separate category (videos and shorts) and thus, the different height does not break the style. It only happens on the feeds, where videos and shorts are mixed together. You can create an account on my instance if you'd like and test it with this channel in your subscription feeds, they release shorts regularly: https://invidious.nerdvpn.de/channel/UCchBatdUMZoMfJ3rIzgV84g Also, can you share your ublock rule for blocking those pesky shorts? |
The issue seems to be that Invidious returns the YouTube Shorts thumbnails with the same ratio as non-Shorts videos, meaning that it has the padding around it. However, DeArrow returns the thumbnail in the mobile orientation. I think the best solution would be for @ajayyy to do something on DeArrow's end (potentially by adding an endpoint that adds padding to the sides of the images?) in order to not have it break if an Invidious instance has custom styling.
It unfortunately isn't very reliable and ends up blocking streams and other things sometimes, because it just blocks videos that don't have a length. I wish there was an option to hide them entirely that didn't rely on hacks like these. But here it is:
|
The dearrow script no longer works, not even the latest version. Has anyone been updating this or not? With youtube's new hostility to adblockers and purposely making your CPU faster, I'd love to fully move over there now, and I have gotten way too used to dearrow decrapifying searches and titles. |
I've been running into issues as well. I was hoping there would be an official integration in DeArrow by now. @ajayyy Is this planned down the line, and if so, do you have any estimate as to when? |
The main issue would be integrating channel allowlists which uses a YouTube api to get the channel ID. I haven't thought of a good solution for that yet. |
@ajayyy I'm not entirely sure which ID, but doesn't Invidious have the channel ID (in the old format, at least) in the channel link under the video? |
Hey all, I've quickly updated the userscript to support what I believe is the new Invidious HTML layout. If you had errors previously, this'll hopefully fix them. Still hoping for an official integration though :P // ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.3
// @description Adds support for DeArrow in Invidious
// @match https://yewtu.be/*
// @match https://iv.ggtyler.dev/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant GM.xmlHttpRequest
// @author Macic-Dev
// @author Minion3665
// @author Basil
// @author Ajay
// ==/UserScript==
(async function() {
'use strict';
/**
* A simple fetch polyfill that uses the GreaseMonkey API
*
* @param {string} url - the URL to fetch
*/
function fetch(url) {
return new Promise(resolve => {
GM.xmlHttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: res => {
const headers = res.responseHeaders
.split('\r\n')
.reduce((prev, str) => {
const [key, val] = str.split(/: (.*)/s);
if (key === '') return prev;
prev[key.toLowerCase()] = val;
return prev;
}, {});
resolve({
headers: {
get: key => headers[key.toLowerCase()]
},
status: res.status,
blob: async () => res.response,
json: async () => JSON.parse(await new Response(res.response).text())
});
}
});
});
}
/**
* Converts a Blob to a data URL in order to get around CSP
*
* @param {Blob} blob - the URL to fetch
*/
function blobToDataURI(blob) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result);
});
}
/**
* Fetch data for a video, and then update the elements passed to reflect that
*
* @param {string} videoId - the YouTube ID of the video (the bit that comes after v= in watch URLs)
* @param {HTMLElement} titleElement - the element containing the video title which will be updated if DeArrow has a title
* @param {HTMLElement|undefined} thumbnailElement - the element containing the video thumbnail which will be updated if DeArrow has a thumbnail
*/
async function fetchAndUpdate(videoId, titleElement, thumbnailElement = undefined) {
const oldTitle = titleElement.textContent;
const oldThumbnail = thumbnailElement?.src;
const cachedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}`);
const cachedNewTitle = cachedThumbnailAPIResponse.headers.get('X-Title');
if (cachedNewTitle) replaceTitle(titleElement, cachedNewTitle);
if (thumbnailElement !== undefined && cachedThumbnailAPIResponse.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = await blobToDataURI(cachedNewThumbnail);
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let topTitle = brandingAPIResponse.titles[0];
let usedTitle = (topTitle && (topTitle.votes >= 0 || topTitle.locked)) ? topTitle : null;
replaceTitle(titleElement, usedTitle?.title ?? oldTitle);
}
{
let topThumbnail = brandingAPIResponse.thumbnails[0];
let usedThumbnail = (topThumbnail && (topThumbnail.votes >= 0 || topThumbnail.locked)) ? topThumbnail : null;
let randomTime = brandingAPIResponse.videoDuration ? brandingAPIResponse.videoDuration * brandingAPIResponse.randomTime : 0;
if (usedThumbnail && usedThumbnail.original) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${usedThumbnail?.timestamp ?? randomTime}`);
thumbnailElement.src = await blobToDataURI(await votedThumbnailAPIResponse.blob());
}
}
}
// Replaces the title of elements
function replaceTitle(element, text) {
if (element.nodeName === 'H1') {
for (const child of element.childNodes) {
if (child.nodeName !== '#text') continue;
child.textContent = text;
break;
}
} else {
element.textContent = text;
}
}
// Videos on page
const thumbnailDivs = document.querySelectorAll('div.thumbnail');
thumbnailDivs.forEach((div) => {
if (div.parentNode.nodeName === 'DIV') {
// First link. Selector could be better.
const link = div.querySelector('a');
// Make sure we aren't fetching channel URLs
if (link.href.startsWith('/channel/')) return;
const videoUrl = new URL(link.href);
const videoId = videoUrl.searchParams.get('v');
const titleP = div.parentNode.children[1].querySelector('p');
const thumbnailImg = div.querySelector('img.thumbnail');
fetchAndUpdate(videoId, titleP, thumbnailImg);
} else {
// Make sure we aren't fetching channel URLs
if (div.parentNode.href.startsWith('/channel/')) return;
const videoUrl = new URL(div.querySelector('a').href);
const videoId = videoUrl.searchParams.get('v');
const titleP = [...div.parentNode.querySelectorAll('p')][1];
const thumbnailImg = div.querySelector('img.thumbnail');
fetchAndUpdate(videoId, titleP, thumbnailImg);
}
});
// Current video
const currentUrl = new URL(document.URL);
if (currentUrl.pathname == '/watch') {
const currentId = currentUrl.searchParams.get('v');
const titleH = document.querySelector('h1');
const thumbnailDiv = document.querySelector('.vjs-poster');
await fetchAndUpdate(currentId, titleH, thumbnailDiv);
if (thumbnailDiv.src) thumbnailDiv.style.backgroundImage = `url(${thumbnailDiv.src})`;
document.title = `${titleH.textContent} - Invidious`;
}
})(); |
Consider publishing the userscript on Greasyfork |
@codenyte The issue is is that the userscript needs permissions to access all Invidious sites. I could theoretically write a detector and allow the userscript to access all sites, but I don't know of a good way to do the detection. |
Right, I completely forgot about this |
Well, for the frontend. However, as far as I know, one can't do any submissions yet. That would be quite helpful. |
// ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.3
// @description Adds support for DeArrow in Invidious
// @match https://yewtu.be/*
// @match https://iv.ggtyler.dev/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant GM.xmlHttpRequest
// @author Macic-Dev
// @author Minion3665
// @author Basil
// @author Ajay
// ==/UserScript==
(async function() {
'use strict';
/**
* A simple fetch polyfill that uses the GreaseMonkey API
*
* @param {string} url - the URL to fetch
*/
function fetch(url) {
return new Promise(resolve => {
GM.xmlHttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: res => {
const headers = res.responseHeaders
.split('\r\n')
.reduce((prev, str) => {
const [key, val] = str.split(/: (.*)/s);
if (key === '') return prev;
prev[key.toLowerCase()] = val;
return prev;
}, {});
resolve({
headers: {
get: key => headers[key.toLowerCase()]
},
status: res.status,
blob: async () => res.response,
json: async () => JSON.parse(await new Response(res.response).text())
});
}
});
});
}
/**
* Converts a Blob to a data URL in order to get around CSP
*
* @param {Blob} blob - the URL to fetch
*/
function blobToDataURI(blob) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result);
});
}
/**
* Fetch data for a video, and then update the elements passed to reflect that
*
* @param {string} videoId - the YouTube ID of the video (the bit that comes after v= in watch URLs)
* @param {HTMLElement} titleElement - the element containing the video title which will be updated if DeArrow has a title
* @param {HTMLElement|undefined} thumbnailElement - the element containing the video thumbnail which will be updated if DeArrow has a thumbnail
*/
async function fetchAndUpdate(videoId, titleElement, thumbnailElement = undefined) {
const oldTitle = titleElement.textContent;
const oldThumbnail = thumbnailElement?.src;
const cachedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}`);
const cachedNewTitle = cachedThumbnailAPIResponse.headers.get('X-Title');
if (cachedNewTitle) replaceTitle(titleElement, cachedNewTitle);
if (thumbnailElement !== undefined && cachedThumbnailAPIResponse.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = await blobToDataURI(cachedNewThumbnail);
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let topTitle = brandingAPIResponse.titles[0];
let usedTitle = (topTitle && (topTitle.votes >= 0 || topTitle.locked)) ? topTitle : null;
replaceTitle(titleElement, usedTitle?.title ?? oldTitle);
}
{
let topThumbnail = brandingAPIResponse.thumbnails[0];
let usedThumbnail = (topThumbnail && (topThumbnail.votes >= 0 || topThumbnail.locked)) ? topThumbnail : null;
let randomTime = brandingAPIResponse.videoDuration ? brandingAPIResponse.videoDuration * brandingAPIResponse.randomTime : 0;
if (usedThumbnail && usedThumbnail.original) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${usedThumbnail?.timestamp ?? randomTime}`);
thumbnailElement.src = await blobToDataURI(await votedThumbnailAPIResponse.blob());
}
}
}
// Replaces the title of elements
function replaceTitle(element, text) {
if (element.nodeName === 'H1') {
for (const child of element.childNodes) {
if (child.nodeName !== '#text') continue;
child.textContent = text;
break;
}
} else {
element.textContent = text;
}
}
// Videos on page
const thumbnailDivs = document.querySelectorAll('div.thumbnail');
thumbnailDivs.forEach((div) => {
if (div.parentNode.nodeName === 'DIV') {
// First link. Selector could be better.
const link = div.querySelector('a');
// Make sure we aren't fetching channel URLs
if (link.href.startsWith('/channel/')) return;
const videoUrl = new URL(link.href);
const videoId = videoUrl.searchParams.get('v');
const titleP = div.parentNode.children[1].querySelector('p');
const thumbnailImg = div.querySelector('img.thumbnail');
fetchAndUpdate(videoId, titleP, thumbnailImg);
} else {
// Make sure we aren't fetching channel URLs
if (div.parentNode.href.startsWith('/channel/')) return;
const videoUrl = new URL(div.querySelector('a').href);
const videoId = videoUrl.searchParams.get('v');
const titleP = [...div.parentNode.querySelectorAll('p')][1];
const thumbnailImg = div.querySelector('img.thumbnail');
fetchAndUpdate(videoId, titleP, thumbnailImg);
}
});
// Current video
const currentUrl = new URL(document.URL);
if (currentUrl.pathname == '/watch') {
const currentId = currentUrl.searchParams.get('v');
const titleH = document.querySelector('h1');
const thumbnailDiv = document.querySelector('.vjs-poster');
await fetchAndUpdate(currentId, titleH, thumbnailDiv);
if (thumbnailDiv.src) thumbnailDiv.style.backgroundImage = `url(${thumbnailDiv.src})`;
document.title = `${titleH.textContent} - Invidious`;
}
})(); |
Maybe author of this script should make repo for it, so we can easily find latest version and pull request our changes?
|
I'm not sure who the original author is, but maybe Ajay could? But it would be ideal if this was just integrated directly into DeArrow (I'm surprised it hasn't been added yet). |
Are there any plans to implement that? |
This update should fix any thumbnail-related issues: // ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.4
// @description Adds support for DeArrow in Invidious
// @match https://yewtu.be/*
// @match https://iv.ggtyler.dev/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant GM.xmlHttpRequest
// @author Macic-Dev
// @author Minion3665
// @author Basil
// @author Ajay
// @author Euphoriyy
// ==/UserScript==
(async function() {
'use strict';
/**
* A simple fetch polyfill that uses the GreaseMonkey API
*
* @param {string} url - the URL to fetch
*/
function fetch(url) {
return new Promise(resolve => {
GM.xmlHttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: res => {
const headers = res.responseHeaders
.split('\r\n')
.reduce((prev, str) => {
const [key, val] = str.split(/: (.*)/s);
if (key === '') return prev;
prev[key.toLowerCase()] = val;
return prev;
}, {});
resolve({
headers: {
get: key => headers[key.toLowerCase()]
},
status: res.status,
blob: async () => res.response,
json: async () => JSON.parse(await new Response(res.response).text())
});
}
});
});
}
/**
* Converts a Blob to a data URL in order to get around CSP
*
* @param {Blob} blob - the URL to fetch
*/
function blobToDataURI(blob) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result);
});
}
/**
* Fetch data for a video, and then update the elements passed to reflect that
*
* @param {string} videoId - the YouTube ID of the video (the bit that comes after v= in watch URLs)
* @param {HTMLElement} titleElement - the element containing the video title which will be updated if DeArrow has a title
* @param {HTMLElement|undefined} thumbnailElement - the element containing the video thumbnail which will be updated if DeArrow has a thumbnail
*/
async function fetchAndUpdate(videoId, titleElement, thumbnailElement = undefined) {
const oldTitle = titleElement.textContent;
const oldThumbnail = thumbnailElement?.src;
const cachedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}`);
const cachedNewTitle = cachedThumbnailAPIResponse.headers.get('X-Title');
if (cachedNewTitle) replaceTitle(titleElement, cachedNewTitle);
if (thumbnailElement !== undefined && cachedThumbnailAPIResponse.status === 200 && cachedThumbnailAPIResponse.headers.get("X-Timestamp") !== "0.0") {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = await blobToDataURI(cachedNewThumbnail);
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let topTitle = brandingAPIResponse.titles[0];
let usedTitle = (topTitle && (topTitle.votes >= 0 || topTitle.locked)) ? topTitle : null;
replaceTitle(titleElement, usedTitle?.title ?? oldTitle);
}
if (brandingAPIResponse.thumbnails.length > 0) {
let topThumbnail = brandingAPIResponse.thumbnails[0];
let usedThumbnail = (topThumbnail && (topThumbnail.votes >= 0 || topThumbnail.locked)) ? topThumbnail : null;
let randomTime = brandingAPIResponse.videoDuration ? brandingAPIResponse.videoDuration * brandingAPIResponse.randomTime : 0;
if (usedThumbnail && usedThumbnail.original) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${usedThumbnail?.timestamp ?? randomTime}`);
thumbnailElement.src = await blobToDataURI(await votedThumbnailAPIResponse.blob());
}
}
}
// Replaces the title of elements
function replaceTitle(element, text) {
if (element.nodeName === 'H1') {
for (const child of element.childNodes) {
if (child.nodeName !== '#text') continue;
child.textContent = text;
break;
}
} else {
element.textContent = text;
}
}
// Videos on page
const thumbnailDivs = document.querySelectorAll('div.thumbnail');
thumbnailDivs.forEach((div) => {
if (div.parentNode.nodeName === 'DIV') {
// First link. Selector could be better.
const link = div.querySelector('a');
// Make sure we aren't fetching channel URLs
if (link.href.startsWith('/channel/')) return;
const videoUrl = new URL(link.href);
const videoId = videoUrl.searchParams.get('v');
const titleP = div.parentNode.children[1].querySelector('p');
const thumbnailImg = div.querySelector('img.thumbnail');
fetchAndUpdate(videoId, titleP, thumbnailImg);
} else {
// Make sure we aren't fetching channel URLs
if (div.parentNode.href.startsWith('/channel/')) return;
const videoUrl = new URL(div.querySelector('a').href);
const videoId = videoUrl.searchParams.get('v');
const titleP = [...div.parentNode.querySelectorAll('p')][1];
const thumbnailImg = div.querySelector('img.thumbnail');
fetchAndUpdate(videoId, titleP, thumbnailImg);
}
});
// Current video
const currentUrl = new URL(document.URL);
if (currentUrl.pathname == '/watch') {
const currentId = currentUrl.searchParams.get('v');
const titleH = document.querySelector('h1');
const thumbnailDiv = document.querySelector('.vjs-poster');
await fetchAndUpdate(currentId, titleH, thumbnailDiv);
if (thumbnailDiv.src) thumbnailDiv.style.backgroundImage = `url(${thumbnailDiv.src})`;
document.title = `${titleH.textContent} - Invidious`;
}
})(); |
I have tried fixing this myself, and I have got it to work, with the catch of it only being when manually activated via context menu. This is because that playlist sidebar is loaded with javascript after the initial page load, meaning that the script is already finished executing before that sidebar loads in, preventing the thumbnails and titles from changing. |
Please add support for 3rd party front-ends, similar to SponsorBlock. Specifically, for Piped.
The text was updated successfully, but these errors were encountered: