Open GlenLowland opened 1 year ago
Invidious would be nice as well, I use Piped myself, but it's always better to have multiple options
Do note that this implementation will only work in some cases, since it is relying on cached information existing.
It will only work if someone with the main extension has seen that video once previously.
I wrote a userscript for Invidious:
I updated your userscript for Invidious
// ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Adds support for DeArrow in Invidious
// @author You
// @match https://inv.vern.cc/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant none
// @author Macic-Dev
// @author Minion3665
// ==/UserScript==
(async function() {
'use strict';
/**
* 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) titleElement.textContent = cachedNewTitle;
if (thumbnailElement !== undefined && fetch.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = URL.createObjectURL(cachedNewThumbnail);
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let maxVotes = -1;
let maxVotesTitle = undefined;
for (const title of brandingAPIResponse.titles) {
if (title.votes > maxVotes) {
maxVotes = title.votes;
maxVotesTitle = title.title;
}
}
titleElement.textContent = maxVotesTitle ?? oldTitle;
}
{
let maxVotes = -1;
let maxVotesThumbnailTime = undefined;
let maxVotesThumbnailIsOriginal = false;
for (const thumbnail of brandingAPIResponse.thumbnails) {
if (thumbnail.votes > maxVotes) {
maxVotes = thumbnail.votes;
maxVotesThumbnailTime = thumbnail.timestamp;
maxVotesThumbnailIsOriginal = thumbnail.original;
}
if (maxVotesThumbnailIsOriginal) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${maxVotesThumbnailTime ?? brandingAPIResponse.randomTime}`);
thumbnailElement.src = URL.createObjectURL(await votedThumbnailAPIResponse.blob());
}
}
}
}
// 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`;
}
})();
Stuff I changed:
Let me know if you notice any issues with mine!
I wrote a userscript for Invidious:
I updated your userscript for Invidious
Stuff I changed:
* Document title changes on video pages * Ask the API rather than just trusting the cache * Update the thumbnail before pressing play on the video pages
Let me know if you notice any issues with mine!
I updated your userscript for Invidious
// ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.1
// @description Adds support for DeArrow in Invidious
// @author You
// @match http://yt.owo/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant none
// @author Macic-Dev
// @author Minion3665
// @author Basil
// ==/UserScript==
(async function() {
'use strict';
/**
* 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 && fetch.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = URL.createObjectURL(cachedNewThumbnail);
}
const brandingAPIResponse = await (await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`)).json();
{
let maxVotes = -1;
let maxVotesTitle = undefined;
for (const title of brandingAPIResponse.titles) {
if (title.votes > maxVotes) {
maxVotes = title.votes;
maxVotesTitle = title.title;
}
}
replaceTitle(titleElement, maxVotesTitle ?? oldTitle);
}
{
let maxVotes = -1;
let maxVotesThumbnailTime = undefined;
let maxVotesThumbnailIsOriginal = false;
for (const thumbnail of brandingAPIResponse.thumbnails) {
if (thumbnail.votes > maxVotes) {
maxVotes = thumbnail.votes;
maxVotesThumbnailTime = thumbnail.timestamp;
maxVotesThumbnailIsOriginal = thumbnail.original;
}
if (maxVotesThumbnailIsOriginal) {
thumbnailElement.src = oldThumbnail;
} else {
const votedThumbnailAPIResponse = await fetch(`https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${maxVotesThumbnailTime ?? brandingAPIResponse.randomTime}`);
thumbnailElement.src = URL.createObjectURL(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`;
}
})();
Stuff I changed:
/watch
page, it doesn't remove the button to switch between video and audio-only mode.Edit: It looks like Invidious does something with the title, so duplicate text may appear. Looking into it. Fixed.
Edit 2: Fixed another mistake.
Thanks for your afford to make this possible.
But i dont know, and didnt find a option or so in dearrow, where i could put this user script in.
Userscripts are managed with a different extension (like Violentmonkey), not DeArrow.
@gBasil seems your script doesn't work at all as of now (may be due to a change in invidious or what). I tried with violentmonkey in Brave and FF. Didn't make any changes to your script whatsoever.
I have correct some issues in the script:
[0]
if it is locked or has votes// ==UserScript==
// @name DeArrow for Invidious
// @namespace http://tampermonkey.net/
// @version 0.2.1
// @description Adds support for DeArrow in Invidious
// @author You
// @match http://yt.owo/*
// @icon https://dearrow.ajay.app/logo.svg
// @grant none
// @author Macic-Dev
// @author Minion3665
// @author Basil
// @author Ajay
// ==/UserScript==
(async function() {
'use strict';
/**
* 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 && fetch.status === 200) {
const cachedNewThumbnail = await cachedThumbnailAPIResponse.blob();
thumbnailElement.src = URL.createObjectURL(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 = URL.createObjectURL(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`;
}
})();
I've modified Ajay's script again.
fetch
API that uses a GreaseMonkey API, which should get around CSP issues.blob:
URLs from URL.createObjectURL
can be blocked by CSP.// ==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.
@ajayyy @gBasil worked great now. Thanks. 👏🏽 Obviously the counters in the Dearrow plugin won't rise as it's the ViolentMonkey doing the things now. Will this script work for Piped instances too?
Seems doesn't work with piped for now.
For Piped
https://github.com/TeamPiped/Piped/issues/2575#issuecomment-1641269041
Hi, we now have DeArrow support in a few places - feed, trending, and related videos. It's disabled by default but can be enabled in the preferences.
It looks like it doesn't support random thumbnails
Piped now has native support for DeArrow everywhere with https://github.com/TeamPiped/Piped/commit/9539d51126c0a53f10d084d3fdbc2c677fb6ac28.
It looks like it doesn't support random thumbnails
Is there any explanation on what this feature is supposed to do? The API documentation currently doesn't have any information on what the randomTime
is supposed to mean. My guess is that it is a random number between 0 and 1, and has to be multiplied with the video duration, and that has to be used as a thumbnail?
I've got another question - what happens when DeArrow doesn't have a video in its database? Piped fetches DeArrow content using k-anonymity
, and we have a lot of null
responses because it can't find a video.
@FireMasterK
Correct, there is this part in the documentation
Random time and video duration are used if you want to fallback thumbnails to a screenshot from a random time. To make thumbnail caching possible, this random time is consistent and produced by the server. The random time value is a number between 0 and 1 and must be multiplied by the video duration. For convenience, the API returns the video duration if it is known, but for most videos the server does not know the video duration. When the API returns a video duration of 0 or null, you must either not use this feature, or obtain the video duration using another method. The browser extension will fallback to fetching the video duration using other methods to always be able to use the feature.
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 https://github.com/ajayyy/DeArrowThumbnailCache/issues/4
As for what to do about videos not in the request due to using k-anonymity
requests, I'm not sure of a good solution. These numbers are generated using a seed random with the videoID as the seed. Specifically, they use the alea algorithm in https://github.com/davidbau/seedrandom/blob/released/lib/alea.js. When k-anonymity
is enabled in the browser extension, it falls back to finding the randomTime
value using this library on the client side when needed. I don't know a good solution for Piped though since it is not in JS, so cannot use that library. Maybe in this case it needs to fall back to an original thumbnail until a solution to that part is determined.
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 0.16970540321403121
@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.
@gBasil Script sadly stopped working today. Not working on Invidious anymore.
that is a shame... what invidious instance are you using?
@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.
@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.
This is what is in the console.
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)
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?)
and tell me if it fixes the issue?
Not the same person you were talking to, but this version of the script worked somewhat for me on a small personal instance while the previous version didn't (only found this thread yesterday).
It replaces the thumbnails correctly when one is available, but for any video that it can't find a user thumbnail for it won't replace it back with the original, does the script not do that or is that something going wrong somewhere? I'm on Firefox on Linux, using Violentmonkey, but the same thing happens with Tampermonkey, even if I use a freshly installed Google Chrome.
This is all that's in browser console
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. You should probably add a CSS rule with a max height or something like that.
![Screenshot](http://all-it.org/share/inv.png)
@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? I'd like to have that, too :D
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.
Also, can you share your ublock rule for blocking those pesky shorts? I'd like to have that, too :D
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:
owo##.pure-u-1.pure-u-md-1-4:not(:has(.thumbnail .length))
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.
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
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
Piped now has native support for DeArrow everywhere with TeamPiped/Piped@9539d51.
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`;
}
})();
Returns this (yewtube)
Maybe author of this script should make repo for it, so we can easily find latest version and pull request our changes?
// ==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`; } })();
Returns this (yewtube)
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).
Piped now has native support for DeArrow everywhere with TeamPiped/Piped@9539d51.
Well, for the frontend. However, as far as I know, one can't do any submissions yet. That would be quite helpful.
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`;
}
})();
Thanks for the script! One small area that is not covered though: the playlist videos while watching a video:
If that could be covered, then I would have no issues with the script other than the lack of submission.
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. Here is the script in case anyone finds a solution to this.
Please add support for 3rd party front-ends, similar to SponsorBlock. Specifically, for Piped.