Last active
October 6, 2024 15:46
-
-
Save Kenya-West/e9b6e5483274d8d1ac287f8b8fad10d3 to your computer and use it in GitHub Desktop.
Inoreader get Habr stats - shows comments and rating counter for Habr articles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name InoReader get Habr stats | |
// @namespace http://tampermonkey.net/ | |
// @version 0.0.1 | |
// @description Shows comments and rating counter for Habr 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 = { | |
readerPaneArticleListMutationObserverLinked: false, | |
readerPaneArticleViewMutationObserverLinked: 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"; | |
const LOCAL_STORAGE_LISTVIEW_PARAMNAME="getHabrStatsListView"; | |
const LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME="getHabrStatsArticleView"; | |
/** | |
* 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) { | |
getHabrStatsInArticleList(node); | |
} | |
getHabrStatsInArticleView(node); | |
} | |
}); | |
} | |
} | |
}; | |
function registerCommands() { | |
let enableImageRestoreInListViewCommand; | |
let disableImageRestoreInListViewCommand; | |
let enableImageRestoreInArticleViewCommand; | |
let disableImageRestoreInArticleViewCommand; | |
const restoreImageListView = localStorage.getItem(LOCAL_STORAGE_LISTVIEW_PARAMNAME) ?? "false"; | |
const restoreImageArticleView = localStorage.getItem(LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME) ?? "true"; | |
if (restoreImageListView === "false") { | |
appState.restoreImagesInListView = false; | |
// @ts-ignore | |
enableImageRestoreInListViewCommand = GM_registerMenuCommand("Enable getting Habr stats in article list", () => { | |
localStorage.setItem(LOCAL_STORAGE_LISTVIEW_PARAMNAME, "true"); | |
appState.restoreImagesInListView = true; | |
if (enableImageRestoreInListViewCommand) { | |
unregisterAllCommands(); | |
registerCommands(); | |
} | |
}); | |
} else { | |
appState.restoreImagesInListView = true; | |
// @ts-ignore | |
disableImageRestoreInListViewCommand = GM_registerMenuCommand("Disable getting Habr stats in article list", () => { | |
localStorage.setItem(LOCAL_STORAGE_LISTVIEW_PARAMNAME, "false"); | |
appState.restoreImagesInListView = false; | |
if (disableImageRestoreInListViewCommand) { | |
unregisterAllCommands(); | |
registerCommands(); | |
} | |
}); | |
} | |
if (restoreImageArticleView === "false") { | |
appState.restoreImagesInArticleView = false; | |
// @ts-ignore | |
enableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Enable getting Habr stats in article view", () => { | |
localStorage.setItem(LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME, "true"); | |
appState.restoreImagesInArticleView = true; | |
if (enableImageRestoreInArticleViewCommand) { | |
unregisterAllCommands(); | |
registerCommands(); | |
} | |
}); | |
} else { | |
appState.restoreImagesInArticleView = true; | |
// @ts-ignore | |
disableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Disable getting Habr stats in article view", () => { | |
localStorage.setItem(LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME, "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 getHabrStatsInArticleList(node) { | |
/** | |
* @type {MutationObserver | undefined} | |
*/ | |
let tmObserverHabrStatsReaderPane; | |
const readerPane = document.body.querySelector("#reader_pane"); | |
if (readerPane) { | |
if (!appState.readerPaneArticleListMutationObserverLinked) { | |
appState.readerPaneArticleListMutationObserverLinked = 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 | |
tmObserverHabrStatsReaderPane = new MutationObserver(callback); | |
// Start observing the target node for configured mutations | |
tmObserverHabrStatsReaderPane.observe(readerPane, mutationObserverLocalConfig); | |
} | |
} else { | |
appState.readerPaneArticleListMutationObserverLinked = false; | |
tmObserverHabrStatsReaderPane?.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 habrLink = getHabrLink(element); | |
if (habrLink) { | |
const habrArticle = commonFetchHabrArticle(habrLink); | |
habrArticle.then(async (habrArticle) => { | |
const habrStats = commonGetHabrStats(habrArticle); | |
placeHabrStatsElement(element, habrStats); | |
}); | |
} | |
} | |
} | |
/** | |
* | |
* @param {Node & HTMLDivElement} node | |
* @returns {string | null} | |
*/ | |
function getHabrLink(node) { | |
const nodeElement = node; | |
/** | |
* @type {HTMLAnchorElement | null} | |
*/ | |
const habrLinkElement = nodeElement.querySelector(".article_tile_content_wraper > a[href*='habr.com']"); | |
return habrLinkElement?.href ?? null; | |
} | |
/** | |
* | |
* @param {HTMLDivElement} div | |
* @param {{ | |
* rating: number | string, | |
* comments: number | string, | |
* }} habrStats | |
* @returns {void} | |
*/ | |
function placeHabrStatsElement(div, habrStats) { | |
const html = `<div class=""> | |
<span class=""> | |
<center>${typeof(habrStats.rating) === 'number' ? habrStats.rating > 0 ? "+" : "-" : ""}${habrStats.rating} | comments: ${habrStats.comments}</center> | |
</span></div>`; | |
div.insertAdjacentHTML("beforeend", html); | |
} | |
} | |
// | |
// | |
// SECOND PART - RESTORE IMAGES IN ARTICLE VIEW | |
// | |
// | |
// | |
/** | |
* | |
* @param {Node} node | |
* @returns {void} | |
*/ | |
function getHabrStatsInArticleView(node) { | |
if (!appState.restoreImagesInArticleView && appState.readerPaneArticleViewMutationObserverLinked) { | |
return; | |
} | |
/** | |
* @type {HTMLDivElement} | |
*/ | |
// @ts-ignore | |
const nodeElement = node; | |
/** | |
* @type {HTMLDivElement | null} | |
*/ | |
let articleRoot = nodeElement?.querySelector(querySelectorPathArticleRoot); | |
if (articleRoot === null && nodeElement?.classList.contains("article_full_contents")) { | |
articleRoot = nodeElement; | |
} | |
if (articleRoot) { | |
appState.readerPaneArticleViewMutationObserverLinked = true; | |
const habrLink = getHabrLink(); | |
if (habrLink) { | |
const habrArticle = commonFetchHabrArticle(habrLink); | |
habrArticle.then(async (habrArticle) => { | |
const habrStats = commonGetHabrStats(habrArticle); | |
placeHabrStatsElement(habrStats); | |
appState.readerPaneArticleViewMutationObserverLinked = false; | |
}); | |
} else { | |
appState.readerPaneArticleViewMutationObserverLinked = false; | |
} | |
return; | |
} else { | |
appState.readerPaneArticleViewMutationObserverLinked = false; | |
} | |
/** | |
* | |
* @returns {string | null} | |
*/ | |
function getHabrLink() { | |
/** | |
* @type {HTMLDivElement} | |
*/ | |
// @ts-ignore | |
const nodeElement = articleRoot; | |
/** | |
* @type {HTMLAnchorElement | null} | |
*/ | |
const habrLinkElement = nodeElement.querySelector(".article_title.article_title_expanded_view > a.article_title_link[href*='habr.com']"); | |
return habrLinkElement?.href ?? null; | |
} | |
/** | |
* | |
* @param {{ | |
* rating: number | string, | |
* comments: number | string, | |
* }} habrStats | |
* @returns {void} | |
*/ | |
function placeHabrStatsElement(habrStats) { | |
/** | |
* @type {HTMLDivElement} | |
*/ | |
// @ts-ignore | |
const nodeElement = articleRoot?.querySelector(".article_footer_placeholder_middle"); | |
const html = ` | |
<div class="article_sub_title graylink_darker"> | |
<center> | |
${typeof habrStats.rating === "number" ? (habrStats.rating > 0 ? "+" : "-") : ""}${habrStats.rating} | comments: ${habrStats.comments} | |
</center> | |
</div> | |
`; | |
nodeElement.insertAdjacentHTML("beforebegin", html); | |
} | |
} | |
/** | |
* | |
* @param {string} habrLink | |
* @returns {Promise<Document>} | |
*/ | |
async function commonFetchHabrArticle(habrLink) { | |
// add ?embed=1 to the end of the telegramPostUrl by constructing URL object | |
const habrArticleUrlObject = new URL(habrLink); | |
const requestUrl = appConfig.corsProxies[3].prefixUrl ? appConfig.corsProxies[3].prefixUrl + habrArticleUrlObject.toString() : habrArticleUrlObject; | |
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 {{ | |
* rating: number | string, | |
* comments: number | string, | |
* }} imageUrl | |
*/ | |
function commonGetHabrStats(doc) { | |
/** | |
* @type {HTMLSpanElement | null} | |
*/ | |
const ratingElement = doc.querySelector(".tm-article-presenter__body .tm-votes-lever__score-counter[data-test-id='votes-score-counter']"); | |
const rating = Number(ratingElement?.innerText) ?? "N/A"; | |
/** | |
* @type {HTMLSpanElement | null} | |
*/ | |
const commentsElement = doc.querySelector(".tm-article-comments-counter-link__value"); | |
const comments = Number(commentsElement?.innerText) ?? "N/A"; | |
const habrStats = { | |
rating, | |
comments, | |
}; | |
return habrStats; | |
} | |
// Create an observer instance linked to the callback function | |
const tmObserverHabrStats = new MutationObserver(callback); | |
// Start observing the target node for configured mutations | |
tmObserverHabrStats.observe(targetNode, mutationObserverGlobalConfig); | |
registerCommands(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment