Skip to content
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

Open
GlenLowland opened this issue Jun 15, 2023 · 47 comments
Open

Support Piped/Invidious #39

GlenLowland opened this issue Jun 15, 2023 · 47 comments

Comments

@GlenLowland
Copy link

Please add support for 3rd party front-ends, similar to SponsorBlock. Specifically, for Piped.

@codenyte
Copy link

Invidious would be nice as well, I use Piped myself, but it's always better to have multiple options

@ajayyy ajayyy changed the title Support Piped Support Piped/Invidious Jun 28, 2023
@ajayyy

This comment was marked as outdated.

@Minion3665

This comment was marked as outdated.

@ajayyy ajayyy pinned this issue Jul 6, 2023
@gBasil

This comment was marked as outdated.

@solisinvictum

This comment was marked as resolved.

@gBasil

This comment was marked as resolved.

@aggarwalsushant

This comment was marked as resolved.

@ajayyy

This comment was marked as outdated.

@gBasil
Copy link

gBasil commented Jul 17, 2023

I've modified Ajay's script again.

  1. I've added a simple polyfill for the fetch API that uses a GreaseMonkey API, which should get around CSP issues.
  2. I've added a function for converting Blobs to data URLs, as the 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.

@solisinvictum
Copy link

thanks! that works great.

@aggarwalsushant
Copy link

@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?

@aggarwalsushant
Copy link

Seems doesn't work with piped for now.

@ajayyy
Copy link
Owner

ajayyy commented Jul 19, 2023

For Piped

TeamPiped/Piped#2575 (comment)

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

@FireMasterK
Copy link

Piped now has native support for DeArrow everywhere with TeamPiped/Piped@9539d51.

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.

@ajayyy
Copy link
Owner

ajayyy commented Jul 21, 2023

@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 ajayyy/DeArrowThumbnailCache#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.

@FireMasterK
Copy link

I have ported an Alea implementation to Java. The random numbers generated from the seed don't seem to match however:

image

DeArrow: https://sponsor.ajay.app/api/branding?videoID=VyI-2c9KI1I

The randomTIme for this specific video is 0.16970540321403121

@ajayyy
Copy link
Owner

ajayyy commented Jul 24, 2023

@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

@solisinvictum
Copy link

@gBasil Script sadly stopped working today. Not working on Invidious anymore.

@Minion3665
Copy link

@gBasil Script sadly stopped working today. Not working on Invidious anymore.

that is a shame... what invidious instance are you using?

@solisinvictum
Copy link

@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
Copy link

gBasil commented Aug 15, 2023

@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.

@solisinvictum
Copy link

grafik

This is what is in the console.

@solisinvictum
Copy link

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)

@Minion3665
Copy link

Minion3665 commented Aug 15, 2023

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?)

@ghost
Copy link

ghost commented Aug 15, 2023

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.
image

This is all that's in browser console
image

@gBasil
Copy link

gBasil commented Aug 15, 2023

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`;
    }
})();

@solisinvictum
Copy link

Sry for the late response @gBasil

Your latest submitted script here, works again!

Great. Thanks!

@Sommerwiesel
Copy link

Sommerwiesel commented Sep 19, 2023

@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

Screenshot

@gBasil
Copy link

gBasil commented Sep 19, 2023

@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.

@Sommerwiesel
Copy link

Sommerwiesel commented Sep 19, 2023

@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

@gBasil
Copy link

gBasil commented Sep 20, 2023

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))

@BurntEmbers
Copy link

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.

@gBasil
Copy link

gBasil commented Jan 19, 2024

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?

@ajayyy
Copy link
Owner

ajayyy commented Jan 19, 2024

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.

@gBasil
Copy link

gBasil commented Jan 24, 2024

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?
image

@gBasil
Copy link

gBasil commented Feb 9, 2024

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`;
    }
})();

@codenyte
Copy link

codenyte commented Feb 9, 2024

Consider publishing the userscript on Greasyfork

@gBasil
Copy link

gBasil commented Feb 9, 2024

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.

@codenyte
Copy link

codenyte commented Feb 9, 2024

Right, I completely forgot about this

@shaedrich
Copy link

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.

@T3M1N4L
Copy link

T3M1N4L commented Jul 23, 2024

// ==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)
image

@filip2cz
Copy link

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) image

@gBasil
Copy link

gBasil commented Jul 26, 2024

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).

@shaedrich
Copy link

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?

@Euphoriyy
Copy link

Euphoriyy commented Aug 16, 2024

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`;
    }
})();

@Kladki
Copy link

Kladki commented Aug 30, 2024

Thanks for the script! One small area that is not covered though: the playlist videos while watching a video:
image

If that could be covered, then I would have no issues with the script other than the lack of submission.

@Kladki
Copy link

Kladki commented Sep 28, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

17 participants