Skip to content

Support Piped/Invidious #39

Description

@GlenLowland

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

Activity

  1. codenyte commented on Jun 20, 2023

    @codenyte

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

  2. changed the title [-]Support Piped[/-] [+]Support Piped/Invidious[/+] on Jun 28, 2023
  3. ajayyy commented on Jun 28, 2023

    @ajayyy
  4. Minion3665 commented on Jul 6, 2023

    @Minion3665
  5. pinned this issue on Jul 6, 2023
  6. gBasil commented on Jul 6, 2023

    @gBasil
  7. solisinvictum commented on Jul 10, 2023

    @solisinvictum
  8. gBasil commented on Jul 11, 2023

    @gBasil
  9. conclusionary commented on Jul 15, 2023

    @conclusionary

    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.

    Oh just use a userscript manager extension/addon, I would recomend Violentmonkey since the project is 100% open-source.

  10. aggarwalsushant commented on Jul 17, 2023

    @aggarwalsushant
  11. ajayyy commented on Jul 17, 2023

    @ajayyy
  12. gBasil commented on Jul 17, 2023

    @gBasil

    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.

  13. solisinvictum commented on Jul 17, 2023

    @solisinvictum

    thanks! that works great.

  14. aggarwalsushant commented on Jul 17, 2023

    @aggarwalsushant

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

  15. aggarwalsushant commented on Jul 17, 2023

    @aggarwalsushant

    Seems doesn't work with piped for now.

  16. 28 remaining items

  17. shaedrich commented on May 7, 2024

    @shaedrich

    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.

  18. T3M1N4L commented on Jul 23, 2024

    @T3M1N4L
    // ==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

  19. filip2cz commented on Jul 26, 2024

    @filip2cz

    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

  20. gBasil commented on Jul 26, 2024

    @gBasil

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

  21. shaedrich commented on Jul 27, 2024

    @shaedrich

    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?

  22. Euphoriyy commented on Aug 16, 2024

    @Euphoriyy

    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`;
        }
    })();
  23. Kladki commented on Aug 30, 2024

    @Kladki

    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.

  24. Kladki commented on Sep 28, 2024

    @Kladki

    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.

  25. sysrqmagician commented on Dec 30, 2024

    @sysrqmagician

    I implemented basic DeArrow support in my extension for Invidious: extraneous.
    Slapped it together today; it has some additional features such as LocalStorage-based watched video markers and hiding videos based on duration/title. Contributions are welcome. Only tested on Firefox so far.

  26. beesarefriends commented on Dec 31, 2024

    @beesarefriends

    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.

    Would something like this work? Maybe add each video on the page to some list and check against that so as to avoid spamming the deArrow API but just running the main section of the code multiple times after a delay?

        let retries = 3;
        const intervalID = setInterval(_ => {
            console.log(retries)
            main();  //The main loop of the function, ie. the "videos on page" section
            retries--;
            if(retries == 0) clearInterval(intervalID);
        }, 2500); //some delay, here 2.5 seconds
  27. beesarefriends commented on Dec 31, 2024

    @beesarefriends

    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.

    Would something like this work? Maybe add each video on the page to some list and check against that so as to avoid spamming the deArrow API but just running the main section of the code multiple times after a delay?

        let retries = 3;
        const intervalID = setInterval(_ => {
            console.log(retries)
            main();  //The main loop of the function, ie. the "videos on page" section
            retries--;
            if(retries == 0) clearInterval(intervalID);
        }, 2500); //some delay, here 2.5 seconds

    In fact, here's a working version (with an option for formatting titles as lowercase with no emotes added, for myself)

  28. sysrqmagician commented on Dec 31, 2024

    @sysrqmagician

    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.

    Would something like this work? Maybe add each video on the page to some list and check against that so as to avoid spamming the deArrow API but just running the main section of the code multiple times after a delay?

        let retries = 3;
        const intervalID = setInterval(_ => {
            console.log(retries)
            main();  //The main loop of the function, ie. the "videos on page" section
            retries--;
            if(retries == 0) clearInterval(intervalID);
        }, 2500); //some delay, here 2.5 seconds

    The above might work, but you'd typically use a MutationObserver to intercept the playlist videos as they are being added to the DOM and do the required processing.
    I'm currently working on implementing this in extraneous.

    Edit: Correction - Not just a might. It does work, nice!

  29. beesarefriends commented on Dec 31, 2024

    @beesarefriends

    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.

    Would something like this work? Maybe add each video on the page to some list and check against that so as to avoid spamming the deArrow API but just running the main section of the code multiple times after a delay?

        let retries = 3;
        const intervalID = setInterval(_ => {
            console.log(retries)
            main();  //The main loop of the function, ie. the "videos on page" section
            retries--;
            if(retries == 0) clearInterval(intervalID);
        }, 2500); //some delay, here 2.5 seconds

    The above might work, but you'd typically use a MutationObserver to intercept the playlist videos as they are being added to the DOM and do the required processing. I'm currently working on implementing this in extraneous.

    Edit: Correction - Not just a might. It does work, nice!

    Just found out about MutationObservers actually! Updated the script with one that works and is a little less spammy, although ideally we wouldn't re-check every video on the page afterwards. That requires a bit of refactoring, though.

  30. Kladki commented on Dec 31, 2024

    @Kladki

    Nice that you found a solution. I have updated the script again now to use MutationObservers, but instead only update the playlist items instead of every video: https://gist.github.com/Kladki/101f0da6511906460252e26ea78d8ec3

  31. sysrqmagician commented on Jan 3, 2025

    @sysrqmagician

    Extraneous (repo, AMO) has been screened and is now available via the official Firefox Addon Store! It behaves as specified in the documentation and is reasonably robust. Contributions, feature requests or general feedback are very welcome.

    For Chromium, manual installation instructions are provided in the repository, although the DeArrow module is currently not working (issue 6). Chromium won't be officially supported, but should anyone be interested in using the plugin themselves, I'm not opposed to merging fixes for Chromium.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions