您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Autoplays Telegram video generated by RSS-Bridge feed when user chooses "card view" (press `4`)
// ==UserScript== // @name InoReader autoplay video in card view // @namespace http://tampermonkey.net/ // @version 0.0.2 // @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 {boolean} articleViewOpened - Indicates whether the article view is opened. * @property {boolean} videoPlacingInProgress - Indicates whether the video placing is in progress. * @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, videoPlacingInProgress: 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) { if (!appState.videoPlacingInProgress) { appState.videoPlacingInProgress = true; checkVideoExistingInTgPost(target) .then((videoUrl) => { const videoElement = createVideoElement(videoUrl); placeVideo(target, videoElement); if (target.classList.contains("article_current")) { playVideo(videoElement); } }) .finally(() => { appState.videoPlacingInProgress = false; }); } } 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); })();