InoReader autoplay video in card view

Autoplays Telegram video generated by RSS-Bridge feed when user chooses "card view" (press `4`)

当前为 2024-05-02 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         InoReader autoplay video in card view
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  Autoplays Telegram video generated by RSS-Bridge feed when user chooses "card view" (press `4`)
// @author       Kenya-West
// @match        https://*.inoreader.com/feed*
// @match        https://*.inoreader.com/article*
// @match        https://*.inoreader.com/folder*
// @match        https://*.inoreader.com/starred*
// @match        https://*.inoreader.com/library*
// @match        https://*.inoreader.com/dashboard*
// @match        https://*.inoreader.com/web_pages*
// @match        https://*.inoreader.com/trending*
// @match        https://*.inoreader.com/commented*
// @match        https://*.inoreader.com/recent*
// @match        https://*.inoreader.com/search*
// @match        https://*.inoreader.com/channel*
// @match        https://*.inoreader.com/teams*
// @match        https://*.inoreader.com/dashboard*
// @match        https://*.inoreader.com/pocket*
// @match        https://*.inoreader.com/liked*
// @match        https://*.inoreader.com/tags*
// @icon         https://inoreader.com/favicon.ico?v=8
// @license      MIT
// ==/UserScript==
// @ts-check

(function () {
    "use strict";

    /**
     * @typedef {Object} appConfig
     * @property {Array<{
     *     prefixUrl: string,
     *     corsType: "direct" | "corsSh" | "corsAnywhere" | "corsFlare",
     *     token?: string,
     *     hidden?: boolean
     * }>} corsProxies
     */
    const appConfig = {
        corsProxies: [
            {
                prefixUrl: "https://corsproxy.io/?",
                corsType: "direct",
            },
            {
                prefixUrl: "https://proxy.cors.sh/",
                corsType: "corsSh",
                token: undefined,
                hidden: true,
            },
            {
                prefixUrl: "https://cors-anywhere.herokuapp.com/",
                corsType: "corsAnywhere",
                hidden: true,
            },
            {
                prefixUrl: "https://cors-1.kenyawest.workers.dev/?upstream_url=",
                corsType: "corsFlare",
            },
        ],
    };

    /**
     * Represents the application state.
     * @typedef {Object} AppState
     * @property {boolean} readerPaneMutationObserverLinked - Indicates whether the reader pane mutation observer is linked.
     * @property {Object} videoNowPlaying - Represents the currently playing video.
     * @property {HTMLVideoElement | null} videoNowPlaying.currentVideoElement - The current video element being played.
     * @property {function} videoNowPlaying.set - Sets the current video element and pauses the previous one.
     * @property {function} videoNowPlaying.get - Retrieves the current video element.
     */
    const appState = {
        readerPaneMutationObserverLinked: false,
        articleViewOpened: false,
        videoNowPlaying: {
            /**
             * Represents the currently playing video.
             * @type {HTMLVideoElement | null}
             */
            currentVideoElement: null,
            /**
             * 
             * @param {HTMLVideoElement | null} video 
             */
            set: (video) => {
                const previousVideo = appState.videoNowPlaying.currentVideoElement;
                appState.videoNowPlaying.currentVideoElement?.pause();
                appState.videoNowPlaying.currentVideoElement = video;
                appState.videoNowPlaying.currentVideoElement?.play();
            },
            /**
             * 
             * @returns {HTMLVideoElement | null}
             */
            get: () => {
                return appState.videoNowPlaying.currentVideoElement;
            },
            stopPlaying: () => {
                appState.videoNowPlaying.currentVideoElement?.pause();
                appState.videoNowPlaying.currentVideoElement = null;
            },
        },
    };

    // Select the node that will be observed for mutations
    const targetNode = document.body;

    // Options for the observer (which mutations to observe)
    const mutationObserverGlobalConfig = {
        attributes: false,
        childList: true,
        subtree: true,
    };

    const querySelectorPathArticleRoot = ".article_full_contents .article_content";

    /**
     * Callback function to execute when mutations are observed
     * @param {MutationRecord[]} mutationsList - List of mutations observed
     * @param {MutationObserver} observer - The MutationObserver instance
     */
    const callback = function (mutationsList, observer) {
        for (let i = 0; i < mutationsList.length; i++) {
            if (mutationsList[i].type === "childList") {
                mutationsList[i].addedNodes.forEach(function (node) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        autoplayVideoInArticleList(node);
                        stopVideoInArticleList();
                    }
                });
            }
        }
    };

    //
    //
    // FIRST PART - RESTORE IMAGES IN ARTICLE LIST
    //
    //
    //

    /**
     *
     * @param {Node} node
     * @returns {void}
     */
    function autoplayVideoInArticleList(node) {
        /**
         * @type {MutationObserver | undefined}
         */
        let tmObserverImageRestoreReaderPane;
        const readerPane = document.body.querySelector("#reader_pane");
        if (readerPane) {
            if (!appState.readerPaneMutationObserverLinked) {
                appState.readerPaneMutationObserverLinked = true;

                /**
                 * Callback function to execute when mutations are observed
                 * @param {MutationRecord[]} mutationsList - List of mutations observed
                 * @param {MutationObserver} observer - The MutationObserver instance
                 */
                const callback = function (mutationsList, observer) {
                    // filter mutations by having id on target and to have only unique id attribute values
                    let filteredMutations = mutationsList
                        // @ts-ignore
                        .filter((mutation) => mutation.target?.id.includes("article_"))
                        // @ts-ignore
                        .filter((mutation, index, self) => self.findIndex((t) => t.target?.id === mutation.target?.id) === index);

                    if (filteredMutations.length === 2) {
                        // check to have only two mutations: one that has .article_current class and one should not
                        const firstMutation = filteredMutations[0];
                        const secondMutation = filteredMutations[1];
                        // sort by abscence of .article_current class
                        filteredMutations = [firstMutation, secondMutation].sort((a, b) => {
                            // @ts-ignore
                            return a.target?.classList?.contains("article_current") ? 1 : -1;
                        });

                        // @ts-ignore
                        if (firstMutation.target?.classList?.contains("article_current") && !secondMutation.target?.classList?.contains("article_current")) {
                            filteredMutations = [];
                        }
                    }

                    for (let mutation of filteredMutations) {
                        if (mutation.type === "attributes") {
                            if (mutation.attributeName === "class") {
                                /**
                                 * @type {HTMLDivElement}
                                 */
                                // @ts-ignore
                                const target = mutation.target;

                                if (target.classList.contains("article_current") && target.querySelector(".article_tile_content_wraper [class*='icon-youtube']")) {
                                    const videoElement = checkVideoIsPlaced(target);
                                    if (!videoElement) {
                                        checkVideoExistingInTgPost(target).then((videoUrl) => {
                                            const videoElement = createVideoElement(videoUrl);
                                            placeVideo(target, videoElement);
                                            if (target.classList.contains("article_current")) {
                                                playVideo(videoElement);
                                            }
                                        });
                                    } else {
                                        playVideo(videoElement);
                                    }
                                } else if (!target.classList.contains("article_current") && target.querySelector(".article_tile_content_wraper [class*='icon-youtube']")) {
                                    if (checkVideoIsPlaced(target)) {
                                        /**
                                         * @type {HTMLVideoElement | null}
                                         */
                                        const videoElement = checkVideoIsPlaced(target);
                                        if (videoElement) {
                                            stopVideo(videoElement);
                                        }
                                    }
                                }

                                /**
                                 * 
                                 * @param {HTMLDivElement} article 
                                 * @returns {HTMLVideoElement | null}
                                 */
                                function checkVideoIsPlaced(article) {
                                    return article.querySelector(".article_tile_content_wraper > a[href*='t.me'] > video[src*='cdn-telegram.org']");
                                }

                                /**
                                 * 
                                 * @param {HTMLDivElement} target
                                 * @returns {Promise<string>}
                                 */
                                async function checkVideoExistingInTgPost(target) {
                                    const telegramPostUrl = commonGetTelegramPostUrl(target);
                                    if (telegramPostUrl) {
                                        return commonFetchTgPostEmbed(telegramPostUrl).then((tgPost) => {
                                            const videoUrl = commonGetVideoUrlFromTgPost(tgPost);
                                            if (videoUrl) {
                                                return videoUrl;
                                            } else {
                                                return Promise.reject("No video found in the telegram post");
                                            }
                                        });
                                    } else {
                                        return Promise.reject("No telegram post found in the article");
                                    }
                                }

                                /**
                                 * 
                                 * @param {string} videoUrl
                                 * @returns {HTMLVideoElement}
                                 */
                                function createVideoElement(videoUrl) {
                                    const videoElement = document.createElement("video");
                                    videoElement.src = videoUrl;
                                    videoElement.autoplay = false;
                                    videoElement.loop = true;
                                    videoElement.muted = false;
                                    videoElement.volume = 0.6;
                                    videoElement.style.width = "100%";
                                    videoElement.style.height = "100%";
                                    videoElement.style.pointerEvents = "none";
                                    videoElement.style.display = "none";

                                    return videoElement;
                                }

                                /**
                                 * 
                                 * @param {HTMLDivElement} article 
                                 * @param {HTMLVideoElement} videoElement 
                                 */
                                function placeVideo(article, videoElement) {
                                    /**
                                     * @type {HTMLAnchorElement | null}
                                     */
                                    const poster = article.querySelector(".article_tile_content_wraper > a[href*='t.me']");
                                    /**
                                     * @type {HTMLDivElement | null}
                                     */
                                    const cover = article.querySelector(".article_tile_content_wraper > a[href*='t.me'] > .article_tile_picture[style*='background-image']");
                                    if (poster) {
                                        poster.appendChild(videoElement);
                                        if (cover?.style) {
                                            cover.style.display = "none";
                                        }
                                        videoElement.style.display = "block";
                                    }
                                }

                                /**
                                 * 
                                 * @param {HTMLVideoElement} videoElement 
                                 */
                                function playVideo(videoElement) {
                                    const video = videoElement;
                                    if (video && !appState.articleViewOpened) {
                                        appState.videoNowPlaying.set(video);
                                    }
                                }

                                /**
                                 * 
                                 * @param {HTMLVideoElement} videoElement 
                                 */
                                function stopVideo(videoElement) {
                                    const video = videoElement;
                                    if (video) {
                                        video.pause();
                                    }
                                }
                            }
                        }
                    }
                };

                // Options for the observer (which mutations to observe)
                const mutationObserverLocalConfig = {
                    attributes: true,
                    attributeFilter: ["class"],
                    childList: false,
                    subtree: true,
                };

                // Create an observer instance linked to the callback function
                tmObserverImageRestoreReaderPane = new MutationObserver(callback);

                // Start observing the target node for configured mutations
                tmObserverImageRestoreReaderPane.observe(readerPane, mutationObserverLocalConfig);
            }
        } else {
            appState.readerPaneMutationObserverLinked = false;
            tmObserverImageRestoreReaderPane?.disconnect();
        }

        /**
         *
         * @param {Node} node
         */
        function start(node) {
            /**
             * @type {Node & HTMLDivElement}
             */
            // @ts-ignore
            const element = node;
            if (element.hasChildNodes() && element.id.includes("article_") && element.classList.contains("ar")) {
                const imageElement = getImageElement(element);
                if (imageElement) {
                    const telegramPostUrl = getTelegramPostUrl(element);
                    const imageUrl = getImageLink(imageElement);
                    if (imageUrl) {
                        testImageLink(imageUrl).then(async () => {
                            const tgPost = await commonFetchTgPostEmbed(telegramPostUrl);
                            await replaceImageSrc(imageElement, tgPost);
                            await placeMediaCount(element, tgPost);
                        });
                    }
                }
            }
        }

        /**
         *
         * @param {Node & HTMLDivElement} node
         * @returns {HTMLDivElement | null}
         */
        function getImageElement(node) {
            const nodeElement = node;
            /**
             * @type {HTMLDivElement | null}
             */
            const divImageElement = nodeElement.querySelector("a[href*='t.me'] > div[style*='background-image']");
            return divImageElement ?? null;
        }

        /**
         *
         * @param {Node & HTMLDivElement} node
         * @returns {string}
         */
        function getTelegramPostUrl(node) {
            if (!node) {
                return "";
            }
            return getFromNode(node) ?? "";

            /**
             *
             * @param {Node & HTMLDivElement} node
             * @returns {string}
             */
            function getFromNode(node) {
                /**
                 * @type {HTMLDivElement}
                 */
                // @ts-ignore
                const nodeElement = node;
                /**
                 * @type {HTMLAnchorElement | null}
                 */
                const ahrefElement = nodeElement.querySelector("a[href*='t.me']");
                const telegramPostUrl = ahrefElement?.href ?? "";
                // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it
                try {
                    return new URL(telegramPostUrl).origin + new URL(telegramPostUrl).pathname;
                } catch (error) {
                    return telegramPostUrl?.split("?")[0];
                }
            }
        }

        /**
         *
         * @param {HTMLDivElement} div
         */
        function getImageLink(div) {
            const backgroundImageUrl = div?.style.backgroundImage;
            return commonGetUrlFromBackgroundImage(backgroundImageUrl);
        }

        /**
         *
         * @param {string} imageUrl
         * @returns {Promise<void>}
         */
        function testImageLink(imageUrl) {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.src = imageUrl;
                img.onload = function () {
                    reject();
                };
                img.onerror = function () {
                    resolve();
                };
            });
        }

        /**
         *
         * @param {HTMLDivElement} div
         * @param {Document} tgPost
         * @returns {Promise<void>}
         */
        async function replaceImageSrc(div, tgPost) {
            const doc = tgPost;
            const imgLink = commonGetImgUrlsFromTgPost(doc) ?? [];
            if (imgLink?.length > 0) {
                try {
                    div.style.backgroundImage = `url(${imgLink})`;
                } catch (error) {
                    console.error(`Error parsing the HTML from the telegram post. Error: ${error}`);
                }
            } else {
                console.error("No image link found in the telegram post");
            }
        }

        /**
         *
         * @param {HTMLDivElement} node
         * @param {Document} tgPost
         */
        async function placeMediaCount(node, tgPost) {
            const mediaCount = commonGetImgUrlsFromTgPost(tgPost);
            if (mediaCount.length > 1) {
                placeElement(mediaCount.length);
            }

            /**
             * @param {string | number} total
             */
            function placeElement(total) {
                // Create the new element
                const mediaCountElement = document.createElement("span");
                mediaCountElement.className = "article_tile_comments";
                mediaCountElement.title = "";
                mediaCountElement.style.backgroundColor = "rgba(0,0,0,0.5)";
                mediaCountElement.style.padding = "0.1rem";
                mediaCountElement.style.borderRadius = "5px";
                mediaCountElement.style.marginLeft = "0.5rem";
                mediaCountElement.textContent = `1/${total}`;

                // Find the target wrapper
                let wrapper = node.querySelector(".article_tile_comments_wrapper.flex");

                // If the wrapper doesn't exist, create it
                if (!wrapper) {
                    wrapper = document.createElement("div");
                    wrapper.className = "article_tile_comments_wrapper flex";

                    // Find the parent element and append the new wrapper to it
                    const parent = node.querySelector(".article_tile_content_wraper");
                    if (parent) {
                        parent.appendChild(wrapper);
                    } else {
                        console.error("Parent element not found");
                        return;
                    }
                }

                // Append the new element to the wrapper
                wrapper.appendChild(mediaCountElement);
            }
        }
    }

    /**
     *
     * @param {string} telegramPostUrl
     * @returns {Promise<Document>}
     */
    async function commonFetchTgPostEmbed(telegramPostUrl) {
        // add ?embed=1 to the end of the telegramPostUrl by constructing URL object
        const telegramPostUrlObject = new URL(telegramPostUrl);
        telegramPostUrlObject.searchParams.append("embed", "1");

        const requestUrl = appConfig.corsProxies[3].prefixUrl ? appConfig.corsProxies[3].prefixUrl + telegramPostUrlObject.toString() : telegramPostUrlObject;
        const response = await fetch(requestUrl);
        try {
            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, "text/html");
            return Promise.resolve(doc);
        } catch (error) {
            console.error(`Error parsing the HTML from the telegram post. Error: ${error}`);
            return Promise.reject(error);
        }
    }

    /**
     *
     * @param {Document} doc
     * @returns {string[]} imageUrl
     */
    function commonGetImgUrlsFromTgPost(doc) {
        const imagesQuerySelectors = [
            ".tgme_widget_message_grouped_layer > a",
            "a[href^='https://t.me/'].tgme_widget_message_photo_wrap",
            ".tgme_widget_message_video_player[href^='https://t.me/'] > i[style*='background-image'].tgme_widget_message_video_thumb",
            ".tgme_widget_message_link_preview > i[style*='background-image'].link_preview_image",
        ];

        const imgUrls = [];

        for (let i = 0; i < imagesQuerySelectors.length; i++) {
            const images = doc.querySelectorAll(imagesQuerySelectors[i]);
            images.forEach((image) => {
                /**
                 * @type {HTMLAnchorElement}
                 */
                // @ts-ignore
                const element = image;
                const imageUrl = mediaElementParsingChooser(element);
                if (imageUrl) {
                    if (!imgUrls.includes(imageUrl)) {
                        imgUrls.push(imageUrl);
                    }
                }
            });
        }

        /**
         * @param {HTMLAnchorElement} element
         *
         * @returns {string | undefined} imageUrl
         */
        function mediaElementParsingChooser(element) {
            let link;

            if (element.classList?.contains("tgme_widget_message_photo_wrap") && element.href?.includes("https://t.me/")) {
                const url = getUrlFromPhoto(element);
                if (url) {
                    link = url;
                }
            } else if (element.classList?.contains("tgme_widget_message_video_thumb") && element.style.backgroundImage?.includes("cdn-telegram.org")) {
                const url = getUrlFromVideo(element);
                if (url) {
                    link = url;
                }
            } else if (element.classList?.contains("link_preview_image") && element.style.backgroundImage?.includes("cdn-telegram.org")) {
                const url = getUrlFromLinkPreview(element);
                if (url) {
                    link = url;
                }
            }

            return link;
        }

        /**
         *
         * @param {HTMLAnchorElement} element
         * @returns {string | undefined}
         */
        function getUrlFromPhoto(element) {
            const backgroundImageUrl = element?.style.backgroundImage;
            return commonGetUrlFromBackgroundImage(backgroundImageUrl);
        }

        /**
         *
         * @param {HTMLAnchorElement} element
         * @returns {string | undefined}
         */
        function getUrlFromVideo(element) {
            const backgroundImageUrl = element?.style.backgroundImage;
            return commonGetUrlFromBackgroundImage(backgroundImageUrl || "");
        }

        /**
         *
         * @param {HTMLElement} element
         * @returns
         */
        function getUrlFromLinkPreview(element) {
            const backgroundImageUrl = element?.style.backgroundImage;
            return commonGetUrlFromBackgroundImage(backgroundImageUrl);
        }

        return imgUrls;
    }

    

    //
    //
    // SECOND PART - STOP VIDEO IN ARTICLE LIST IF ARTICLE VIEW IS OPENED
    //
    //
    //

    function stopVideoInArticleList() {
        const articleRoot = document.querySelector(querySelectorPathArticleRoot);
        if (articleRoot) {
            appState.articleViewOpened = true;
            appState.videoNowPlaying.stopPlaying();
        } else {
            appState.articleViewOpened = false;
        }
    }

    /**
     *
     * @param {string} backgroundImageUrl
     * @returns {string | undefined}
     */
    function commonGetUrlFromBackgroundImage(backgroundImageUrl) {
        /**
         * @type {string | undefined}
         */
        let imageUrl;
        try {
            imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
        } catch (error) {
            imageUrl = backgroundImageUrl?.slice(5, -2);
        }

        if (!imageUrl || imageUrl == "undefined") {
            return;
        }

        if (!imageUrl?.startsWith("http")) {
            console.error(`The image could not be parsed. Image URL: ${imageUrl}`);
            return;
        }
        return imageUrl;
    }

    /**
     *
     * @param {Document} doc
     * @returns {string | undefined} imageUrl
     */
    function commonGetVideoUrlFromTgPost(doc) {
        /**
         * @type {HTMLVideoElement | null}
         */
        const video = doc.querySelector("video[src*='cdn-telegram.org']");
        const videoUrl = video?.src;
        return videoUrl;
    }

    /**
     *
     * @param {Node & HTMLDivElement} node
     * @returns {string}
     */
    function commonGetTelegramPostUrl(node) {
        if (!node) {
            return "";
        }
        return getFromNode(node) ?? "";

        /**
         *
         * @param {Node & HTMLDivElement} node
         * @returns {string}
         */
        function getFromNode(node) {
            /**
             * @type {HTMLDivElement}
             */
            // @ts-ignore
            const nodeElement = node;
            /**
             * @type {HTMLAnchorElement | null}
             */
            const ahrefElement = nodeElement.querySelector("a[href*='t.me']");
            const telegramPostUrl = ahrefElement?.href ?? "";
            // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it
            try {
                return new URL(telegramPostUrl).origin + new URL(telegramPostUrl).pathname;
            } catch (error) {
                return telegramPostUrl?.split("?")[0];
            }
        }
    }

    // Create an observer instance linked to the callback function
    const tmObserverImageRestore = new MutationObserver(callback);

    // Start observing the target node for configured mutations
    tmObserverImageRestore.observe(targetNode, mutationObserverGlobalConfig);
})();