Loads new images from VK and Telegram in InoReader articles
目前為
// ==UserScript==
// @name InoReader restore lost images
// @namespace http://tampermonkey.net/
// @version 0.0.1
// @description Loads new images 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*
// @icon https://inoreader.com/favicon.ico?v=8
// @license MIT
// ==/UserScript==
// @ts-check
(function () {
"use strict";
const appConfig = {
corsProxy: "https://corsproxy.io/?",
};
const appState = {
readerPaneExists: 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 mutation of mutationsList) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(function (node) {
if (node.nodeType === Node.ELEMENT_NODE) {
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);
}
}
/**
*
* @param {Node} node
* @returns {void}
*/
function restoreImagesInArticleList(node) {
const readerPane = document.body.querySelector("#reader_pane");
if (readerPane) {
if (!appState.readerPaneExists) {
appState.readerPaneExists = 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) {
if (appState.restoreImagesInListView) {
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
const tmObserverImageRestoreReaderPane = new MutationObserver(
callback
);
// Start observing the target node for configured mutations
tmObserverImageRestoreReaderPane.observe(
readerPane,
mutationObserverLocalConfig
);
}
} else {
appState.readerPaneExists = false;
}
/**
*
* @param {Node} node
*/
function start(node) {
const imageElement = getImageElement(node);
if (imageElement) {
const telegramPostUrl = getTelegramPostUrl(node);
const imageUrl = getImageLink(imageElement);
if (imageUrl) {
console.log(
`Found an image in the article list. Image URL: ${imageUrl}, Telegram post URL: ${telegramPostUrl}`
);
testImageLink(imageUrl).then(() => {
console.log(`Image loaded. Image URL: ${imageUrl}`);
replaceImageSrc(imageElement, telegramPostUrl);
console.log(`Replaced the image!`);
});
}
}
}
/**
*
* @param {Node} node
* @returns {HTMLDivElement | null}
*/
function getImageElement(node) {
/**
* @type {HTMLDivElement}
*/
// @ts-ignore
const nodeElement = node;
/**
* @type {HTMLDivElement | null}
*/
const divImageElement = nodeElement.querySelector(
"a[href*='t.me'] > div[style*='background-image']"
);
return divImageElement ?? null;
}
/**
*
* @param {Node} node
*/
function getTelegramPostUrl(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;
/**
* @type {string | undefined}
*/
let imageUrl;
try {
imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
} catch (error) {
imageUrl = backgroundImageUrl?.slice(5, -2);
}
if (!imageUrl?.startsWith("http")) {
console.error(
`The image could not be parsed. Image URL: ${imageUrl}`
);
return null;
}
return imageUrl;
}
/**
*
* @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 {string} telegramPostUrl
*/
async function replaceImageSrc(div, telegramPostUrl) {
const doc = await commonFetchTgPostEmbed(telegramPostUrl);
const imgLink = commonGetImgUrlFromTgPost(doc);
try {
div.style.backgroundImage = `url(${imgLink})`;
} catch (error) {
console.error(
`Error parsing the HTML from the telegram post. Error: ${error}`
);
}
}
}
/**
*
* @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);
return;
}
/**
*
* @param {HTMLDivElement} articleRoot
*/
function getImageLink(articleRoot) {
/**
* @type {HTMLAnchorElement[]}
*/
const ahrefElementArr = Array.from(
articleRoot.querySelectorAll("a[href*='t.me']")
);
/**
* @type {HTMLAnchorElement | null}
*/
const ahrefElement =
ahrefElementArr[1] ??
ahrefElementArr[2] ??
ahrefElementArr[0] ??
null;
/**
* @type {string | undefined} telegramPostUrl
*/
let telegramPostUrl = ahrefElement?.href ?? "";
// try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it
try {
telegramPostUrl =
new URL(telegramPostUrl).origin +
new URL(telegramPostUrl).pathname;
} catch (error) {
telegramPostUrl = telegramPostUrl?.split("?")[0];
}
articleRoot.querySelectorAll("img")?.forEach(
/**
*
* @param {HTMLImageElement} img
*/
function (img) {
const attributes = img.attributes;
const originalSrcLink =
attributes?.getNamedItem("data-original-src")?.value;
if (originalSrcLink?.includes("cdn-telegram.org")) {
img.onerror = function () {
if (telegramPostUrl) {
replaceImageSrc(img, telegramPostUrl);
} else {
console.error(
`The image could not be loaded and no Telegram post found. Telegram post URL: ${telegramPostUrl}. Img original src: ${originalSrcLink}`
);
}
};
}
}
);
}
/**
*
* @param {HTMLImageElement} img
* @param {string} telegramPostUrl
*/
async function replaceImageSrc(img, telegramPostUrl) {
const doc = await commonFetchTgPostEmbed(telegramPostUrl);
const imgLink = commonGetImgUrlFromTgPost(doc);
try {
img.src = imgLink ?? "";
img.setAttribute("data-original-src", imgLink ?? "");
} catch (error) {
console.error(
`Error parsing the HTML from the telegram post. Error: ${error}`
);
}
}
}
/**
*
* @param telegramPostUrl string
* @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.corsProxy
? appConfig.corsProxy +
encodeURIComponent(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 | undefined} imageUrl
*/
function commonGetImgUrlFromTgPost(doc) {
/**
* @type {HTMLAnchorElement | null}
*/
const img = doc.querySelector(
"a[href^='https://t.me/'].tgme_widget_message_photo_wrap"
);
// get background-image url from the style attribute
const backgroundImageUrl = img?.style.backgroundImage;
/**
* @type {string | undefined}
*/
let imageUrl;
try {
imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
} catch (error) {
imageUrl = backgroundImageUrl?.slice(5, -2);
}
// any better way?
if (!imageUrl?.startsWith("http")) {
console.error(
`The image could not be parsed. Image URL: ${imageUrl}`
);
return;
}
return imageUrl;
}
// 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();
})();