Skip to content

Instantly share code, notes, and snippets.

@Kenya-West
Last active May 3, 2024 07:53
Show Gist options
  • Save Kenya-West/5f067902765af2911e9d2810cd691efa to your computer and use it in GitHub Desktop.
Save Kenya-West/5f067902765af2911e9d2810cd691efa to your computer and use it in GitHub Desktop.
InoReader autoplay video in card view - 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);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment