Skip to content

Instantly share code, notes, and snippets.

@Kenya-West
Last active January 18, 2026 13:27
Show Gist options
  • Select an option

  • Save Kenya-West/5f067902765af2911e9d2810cd691efa to your computer and use it in GitHub Desktop.

Select an option

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.1.0
// @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
(async function () {
"use strict";
const querySelectorPathArticleRoot = ".article_full_contents .article_content";
const hasVideoElementQuerySelector = ".article_video_div";
const readerPane = document.querySelector("#reader_pane");
/**
* @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} 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 = {
articleViewOpened: false,
videoPlacingInProgress: false,
/**
* Represents the focused video element.
* @type {string | null}
*/
currentArticleId: null,
/**
* Represents the previously focused video element.
* @type {string | null}
*/
previousArticleId: null,
videoNowPlaying: {
/**
* Represents the currently playing video.
* @type {HTMLVideoElement | null}
*/
currentPlayElement: null,
/**
*
* @param {HTMLVideoElement | null} video
*/
set: (video) => {
if (video) {
const previousVideo = appState.videoNowPlaying.currentPlayElement;
if (previousVideo && previousVideo !== video) {
previousVideo.pause();
}
appState.videoNowPlaying.currentPlayElement = video;
appState.videoNowPlaying.currentPlayElement?.pause();
appState.videoNowPlaying.currentPlayElement
?.play()
.then(() => {})
.catch((error) => {
appState.videoNowPlaying.currentPlayElement = null;
console.error(`Error playing the video: ${error}`);
});
} else {
appState.videoNowPlaying.currentPlayElement = null;
}
},
/**
*
* @returns {HTMLVideoElement | null}
*/
get: () => {
return appState.videoNowPlaying.currentPlayElement;
},
stopPlaying: () => {
appState.videoNowPlaying.currentPlayElement?.pause();
appState.videoNowPlaying.currentPlayElement = null;
},
},
};
/**
* Checks if the element is an article and triggers video logic
* @param {Element} element
*/
function handleArticleMutation(element) {
// @ts-ignore
if (element.classList.contains("ar") && element.classList.contains("article_current") && element.id && element.querySelector(hasVideoElementQuerySelector)) {
// @ts-ignore
autoplayVideoInArticleList(element);
}
}
if (readerPane) {
// Handle existing articles
readerPane.querySelectorAll(".ar").forEach(handleArticleMutation);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
const target = /** @type {Element} */ (mutation.target);
// @ts-ignore
handleArticleMutation(target);
} else if (mutation.type === "childList") {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = /** @type {Element} */ (node);
// @ts-ignore
if (element.classList.contains("ar")) {
handleArticleMutation(element);
}
// Handle nested articles if any
// @ts-ignore
element.querySelectorAll?.(".ar").forEach(handleArticleMutation);
}
});
}
});
});
observer.observe(readerPane, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ["class"],
});
}
// Monitor for Article View opening/closing
const bodyObserver = new MutationObserver((mutations) => {
let checkNeeded = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
checkNeeded = true;
break;
}
}
if (checkNeeded) {
stopVideoInArticleListWhenArticleViewIsOpened();
}
});
bodyObserver.observe(document.body, {
childList: true,
subtree: true,
});
// Initial check
stopVideoInArticleListWhenArticleViewIsOpened();
//
// FIRST PART - RESTORE IMAGES IN ARTICLE LIST
//
//
//
/**
*
* @param {Node} node
* @returns {Promise<void>}
*/
async function autoplayVideoInArticleList(node) {
setCurrentFocusedArticleVideo(node);
/**
* @type {HTMLDivElement}
*/
// @ts-ignore
const target = node;
if (target.querySelector(hasVideoElementQuerySelector)) {
const videoElement = checkVideoIsPlaced(target);
if (!videoElement) {
if (!appState.videoPlacingInProgress) {
appState.videoPlacingInProgress = true;
try {
const videoUrl = await checkVideoExistingInTgPost(target);
if (videoUrl) {
let videoToPlay = findExistingVideo(target);
if (!videoToPlay) {
const videoElement = createVideoElement(videoUrl);
placeVideo(target, videoElement);
videoToPlay = videoElement;
}
if (target.id === appState.currentArticleId) {
if (videoToPlay && !appState.articleViewOpened) {
appState.videoNowPlaying.set(videoToPlay);
}
}
}
appState.videoPlacingInProgress = false;
} catch (error) {
appState.videoPlacingInProgress = false;
}
}
} else {
appState.videoNowPlaying.set(videoElement);
}
} else if (target.id !== appState.currentArticleId && target.querySelector(hasVideoElementQuerySelector)) {
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) {
const tgPost = await commonFetchTgPostEmbed(telegramPostUrl);
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.pointerEvents = "none";
videoElement.style.display = "none";
return videoElement;
}
/**
*
* @param {HTMLDivElement} article
* @returns {HTMLVideoElement | null}
*/
function findExistingVideo(article) {
/**
* @type {HTMLAnchorElement | null}
*/
const poster = article.querySelector(".article_tile_content_wraper > a[href*='t.me']");
if (poster) {
return poster.querySelector("video[src]");
}
return null;
}
/**
*
* @param {HTMLDivElement} article
* @param {HTMLVideoElement} videoElement
* @returns {HTMLVideoElement | null}
*/
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";
}
return videoElement;
}
/**
*
* @param {HTMLVideoElement} videoElement
*/
function stopVideo(videoElement) {
const video = videoElement;
if (video) {
video.pause();
}
}
/**
*
* @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} 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);
}
}
//
//
// SECOND PART - STOP VIDEO IN ARTICLE LIST IF ARTICLE VIEW IS OPENED
//
//
//
function stopVideoInArticleListWhenArticleViewIsOpened() {
const articleRoot = document.querySelector(querySelectorPathArticleRoot);
if (articleRoot) {
appState.articleViewOpened = true;
appState.videoNowPlaying.stopPlaying();
} else {
appState.articleViewOpened = false;
}
}
/**
* @param {Node} node
*/
function setCurrentFocusedArticleVideo(node) {
/**
* @type {Node & HTMLDivElement}
*/
// @ts-ignore
const element = node;
if (element.classList.contains("article_current")) {
appState.currentArticleId = element.id;
if (appState.previousArticleId && appState.previousArticleId !== appState.currentArticleId) {
// unbind arriveJS event listener
const previousArticleElement = document.getElementById(appState.previousArticleId);
if (previousArticleElement) {
// @ts-ignore
previousArticleElement.unbindArrive();
}
}
appState.previousArticleId = appState.currentArticleId;
} else {
appState.currentArticleId = null;
}
}
/**
*
* @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']") || doc.querySelector("video[src*='telesco.pe']");
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];
}
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment