您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Loads new images and videos from VK and Telegram in InoReader articles
// ==UserScript== // @name InoReader restore lost images and videos // @namespace http://tampermonkey.net/ // @version 0.0.10 // @description Loads new images and videos from VK and Telegram in InoReader articles // @author Kenya-West // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @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", }, ], }; const appState = { readerPaneMutationObserverLinked: false, restoreImagesInListView: false, restoreImagesInArticleView: false, }; // 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) { if (appState.restoreImagesInListView) { restoreImagesInArticleList(node); } runRestoreImagesInArticleView(node); } }); } } }; function registerCommands() { let enableImageRestoreInListViewCommand; let disableImageRestoreInListViewCommand; let enableImageRestoreInArticleViewCommand; let disableImageRestoreInArticleViewCommand; const restoreImageListView = localStorage.getItem("restoreImageListView") ?? "false"; const restoreImageArticleView = localStorage.getItem("restoreImageArticleView") ?? "true"; if (restoreImageListView === "false") { appState.restoreImagesInListView = false; // @ts-ignore enableImageRestoreInListViewCommand = GM_registerMenuCommand("Enable image restore in article list", () => { localStorage.setItem("restoreImageListView", "true"); appState.restoreImagesInListView = true; if (enableImageRestoreInListViewCommand) { unregisterAllCommands(); registerCommands(); } }); } else { appState.restoreImagesInListView = true; // @ts-ignore disableImageRestoreInListViewCommand = GM_registerMenuCommand("Disable image restore in article list", () => { localStorage.setItem("restoreImageListView", "false"); appState.restoreImagesInListView = false; if (disableImageRestoreInListViewCommand) { unregisterAllCommands(); registerCommands(); } }); } if (restoreImageArticleView === "false") { appState.restoreImagesInArticleView = false; // @ts-ignore enableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Enable image restore in article view", () => { localStorage.setItem("restoreImageArticleView", "true"); appState.restoreImagesInArticleView = true; if (enableImageRestoreInArticleViewCommand) { unregisterAllCommands(); registerCommands(); } }); } else { appState.restoreImagesInArticleView = true; // @ts-ignore disableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Disable image restore in article view", () => { localStorage.setItem("restoreImageArticleView", "false"); appState.restoreImagesInArticleView = false; if (disableImageRestoreInArticleViewCommand) { unregisterAllCommands(); registerCommands(); } }); } function unregisterCommand(command) { // @ts-ignore GM_unregisterMenuCommand(command); } function unregisterAllCommands() { // @ts-ignore GM_unregisterMenuCommand(enableImageRestoreInListViewCommand); // @ts-ignore GM_unregisterMenuCommand(disableImageRestoreInListViewCommand); // @ts-ignore GM_unregisterMenuCommand(enableImageRestoreInArticleViewCommand); // @ts-ignore GM_unregisterMenuCommand(disableImageRestoreInArticleViewCommand); } } // // // FIRST PART - RESTORE IMAGES IN ARTICLE LIST // // // /** * * @param {Node} node * @returns {void} */ function restoreImagesInArticleList(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) { for (let mutation of mutationsList) { if (mutation.type === "childList") { mutation.addedNodes.forEach(function (node) { if (node.nodeType === Node.ELEMENT_NODE) { setTimeout(() => { start(node); }, 500); } }); } } }; // Options for the observer (which mutations to observe) const mutationObserverLocalConfig = { attributes: false, childList: true, subtree: false, }; // 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); } } } // // // SECOND PART - RESTORE IMAGES IN ARTICLE VIEW // // // /** * * @param {Node} node * @returns {void} */ function runRestoreImagesInArticleView(node) { if (!appState.restoreImagesInArticleView) { return; } /** * @type {HTMLDivElement} */ // @ts-ignore const nodeElement = node; /** * @type {HTMLDivElement | null} */ const articleRoot = nodeElement?.querySelector(querySelectorPathArticleRoot); if (articleRoot) { getImageLink(articleRoot); getVideoLink(articleRoot); return; } /** * * @param {HTMLDivElement} articleRoot */ function getImageLink(articleRoot) { /** * @type {NodeListOf<HTMLAnchorElement> | null} */ const ahrefElementArr = articleRoot.querySelectorAll("a[href*='t.me']:has(img[data-original-src*='cdn-telegram.org'])"); const telegramPostUrl = commonGetTelegramPostUrl(node); ahrefElementArr.forEach((ahrefElement, index) => { /** * @type {HTMLImageElement | null} */ const img = ahrefElement.querySelector("img[data-original-src*='cdn-telegram.org']"); if (img && telegramPostUrl) { img.onerror = function () { replaceImageSrc(img, telegramPostUrl, index); }; } }); } /** * * @param {HTMLDivElement} articleRoot */ function getVideoLink(articleRoot) { /** * @type {NodeListOf<HTMLVideoElement> | null} */ const videos = articleRoot.querySelectorAll("video[poster*='cdn-telegram.org']"); videos?.forEach((video) => { /** * @type {HTMLSourceElement | null} */ const videoSource = video.querySelector("source"); const telegramPostUrl = commonGetTelegramPostUrl(node); if (videoSource && telegramPostUrl) { if (checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl)) { videoSource.onerror = function () { if (checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl)) { replaceVideoSrc(videoSource, telegramPostUrl).then(() => { if (checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl)) { video.load(); } }); } }; } } }); /** * * @param {string} telegramPostUrl * @returns */ function checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl) { if (document.querySelector(querySelectorPathArticleRoot) && commonGetTelegramPostUrl() === telegramPostUrl) { return true; } return false; } } /** * * @param {HTMLImageElement} img * @param {string} telegramPostUrl */ async function replaceImageSrc(img, telegramPostUrl, index = 0) { const doc = await commonFetchTgPostEmbed(telegramPostUrl); const imgLink = commonGetImgUrlsFromTgPost(doc); if (!imgLink) { return; } try { img.src = imgLink[index] ?? ""; img.setAttribute("data-original-src", imgLink[index] ?? ""); } catch (error) { console.error(`Error parsing the HTML from the telegram post. Error: ${error}`); } } /** * * @param {HTMLSourceElement} source * @param {string} telegramPostUrl * @returns {Promise<void>} */ async function replaceVideoSrc(source, telegramPostUrl) { const doc = await commonFetchTgPostEmbed(telegramPostUrl); const videoLink = commonGetVideoUrlFromTgPost(doc); try { source.src = videoLink ?? ""; return Promise.resolve(); } catch (error) { console.error(`Error parsing the HTML from the telegram post. Error: ${error}`); return Promise.reject(error); } } } /** * * @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; } /** * * @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 | undefined} node * @returns {string} */ function commonGetTelegramPostUrl(node = undefined) { return getFromArticleView() ?? getFromNode(node) ?? ""; /** * * @returns {string | undefined} */ function getFromArticleView() { /** * @type {HTMLAnchorElement | null} */ const element = document.querySelector(".article_title > a[href^='https://t.me/']"); return element?.href; } /** * * @param {Node | undefined} 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); registerCommands(); })();