// Return YouTube Dislike - injected into YouTube pages
// Injects a compact dislike counter into YouTube watch/shorts pages.
try { console.info('[RYD] script injected into', location.hostname, 'url=', location.href); } catch {}
// Minimal CSS injected once
function injectStyles() {
if (document.getElementById('ryd-styles')) return;
const style = document.createElement('style');
style.id = 'ryd-styles';
style.textContent = `
.ryd-badge { display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border-radius:999px; font: 12px/1.2 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; color:#e8e8f0; background: rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.14); }
.ryd-badge .icon { width:14px; height:14px; display:inline-block; }
.ryd-badge .count { font-weight:600; }
.ryd-muted { opacity: .65 }
.ryd-floating-wrap { pointer-events: none; }
.ryd-floating-wrap .ryd-badge { pointer-events: auto; backdrop-filter: blur(6px); background: rgba(0,0,0,0.45); border-color: rgba(255,255,255,0.18); }
.ryd-fixed-wrap { position: fixed; left: 12px; bottom: 12px; z-index: 2147483647; pointer-events: none; }
.ryd-fixed-wrap .ryd-badge { pointer-events: auto; backdrop-filter: blur(6px); background: rgba(0,0,0,0.45); border-color: rgba(255,255,255,0.18); }
`;
if (document.head) document.head.appendChild(style); else document.documentElement.appendChild(style);
}
function nfmt(n) {
try { return new Intl.NumberFormat(undefined, { notation: 'compact' }).format(n); } catch { return String(n); }
}
function invokeIPC(channel, args) {
return new Promise((resolve, reject) => {
const id = Math.random().toString(36).substr(2, 9);
const message = { type: 'message', data: { channel, args: [args], id } };
const handler = (event) => {
if (event.data && event.data.type === 'message' && event.data.data && event.data.data.id === id) {
window.removeEventListener('message', handler);
const response = event.data.data.args[0];
if (response && response.ok) {
resolve(response);
} else {
reject(new Error(response ? response.error : 'IPC failed'));
}
}
};
window.addEventListener('message', handler);
window.postMessage(message, '*');
// Timeout after 10 seconds
setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error('IPC timeout'));
}, 10000);
});
}
async function fetchRyd(videoId) {
// Try IPC first to bypass CSP
try {
const res = await invokeIPC('return-youtube-dislike:get', { videoId });
if (res && res.ok) return res.data;
} catch (e) {
console.debug('[RYD] IPC failed, falling back to fetch:', e.message);
}
// Fallback to direct fetch
try {
const url = `https://returnyoutubedislikeapi.com/votes?videoId=${encodeURIComponent(videoId)}`;
const r = await fetch(url, { cache: 'no-store' });
if (!r.ok) return null;
return await r.json();
} catch (e) {
console.debug('[RYD] Fetch failed:', e);
return null;
}
}
function getVideoIdFromUrl(u) {
try {
const url = new URL(u);
if (url.hostname.includes('youtube.com') || url.hostname === 'youtu.be') {
// watch?v=ID
if (url.pathname === '/watch') return url.searchParams.get('v');
// shorts/ID
if (url.pathname.startsWith('/shorts/')) return url.pathname.split('/')[2] || null;
// youtu.be/ID
if (url.hostname === 'youtu.be') return url.pathname.slice(1) || null;
}
} catch {}
return null;
}
function findBadgeHost() {
// Primary: watch page actions container that holds the like/share buttons
const primarySelectors = [
'ytd-watch-metadata ytd-menu-renderer #top-level-buttons-computed',
'ytd-video-primary-info-renderer #top-level-buttons-computed',
'ytd-watch-metadata #top-row #actions',
'ytd-watch-metadata #actions',
'#actions-inner'
];
for (const sel of primarySelectors) {
const n = document.querySelector(sel);
if (n) return n;
}
// Fallback: if we can find the segmented like/dislike component, place next to it
const seg = document.querySelector('ytd-segmented-like-dislike-button-renderer');
if (seg && seg.parentElement) return seg.parentElement;
// Shorts: different overlay structure
const shortsSelectors = [
'ytd-reel-player-overlay-renderer #actions',
'ytd-reel-video-renderer #actions'
];
for (const sel of shortsSelectors) {
const n = document.querySelector(sel);
if (n) return n;
}
// Shadow DOM targeted probes (open shadow roots only)
const probeShadow = (tag, innerSel) => {
try {
const nodes = document.querySelectorAll(tag);
for (const el of nodes) {
if (el && el.shadowRoot) {
const found = el.shadowRoot.querySelector(innerSel);
if (found) return found;
}
}
} catch {}
return null;
};
// Actions under menu renderer
let deep = probeShadow('ytd-menu-renderer', '#top-level-buttons-computed');
if (deep) return deep;
// Watch metadata containers
deep = probeShadow('ytd-watch-metadata', '#top-row #actions');
if (deep) return deep;
deep = probeShadow('ytd-watch-metadata', '#actions');
if (deep) return deep;
// Shorts overlay
deep = probeShadow('ytd-reel-player-overlay-renderer', '#actions');
if (deep) return deep;
return null;
}
function ensureBadge(host) {
if (!host) return null;
let slot = host.querySelector('.ryd-badge');
if (!slot) {
slot = document.createElement('span');
slot.className = 'ryd-badge ryd-muted';
slot.innerHTML = `—`;
try { host.appendChild(slot); } catch {}
}
return slot;
}
function findPlayerOverlayHost() {
// As a fallback, attach to the player container and absolutely position the badge.
const containers = [
'#player',
'#movie_player',
'ytd-player',
'ytd-watch-flexy #player-container',
'ytd-watch-flexy #player'
];
for (const sel of containers) {
const n = document.querySelector(sel);
if (n) return n;
}
return null;
}
let lastVideoId = null;
let pending = 0;
let hostRetryTimer = null;
async function updateForCurrentUrl() {
const vid = getVideoIdFromUrl(location.href);
if (!vid) return;
if (vid !== lastVideoId) {
lastVideoId = vid;
pending++; // invalidate prior fetches
}
injectStyles();
const tryAttach = async () => {
let host = findBadgeHost();
if (!host) {
// Fallback: player overlay attachment
const player = findPlayerOverlayHost();
if (player) {
// Ensure the player can position children over video
try {
const st = player.style;
if (getComputedStyle(player).position === 'static') st.position = 'relative';
} catch {}
// Create a container to hold the floating badge in the player
let wrap = player.querySelector('.ryd-floating-wrap');
if (!wrap) {
wrap = document.createElement('div');
wrap.className = 'ryd-floating-wrap';
wrap.style.position = 'absolute';
wrap.style.left = '12px';
wrap.style.bottom = '12px';
wrap.style.zIndex = '2147483647';
player.appendChild(wrap);
}
host = wrap;
}
}
if (!host) { return false; }
try { console.debug('[RYD] attaching badge to', host.tagName || host.className || host.id || host); } catch {}
const badge = ensureBadge(host);
if (!badge) return false;
badge.classList.add('ryd-muted');
const ticket = ++pending;
const data = await fetchRyd(vid);
if (ticket !== pending || lastVideoId !== vid) return true; // outdated
if (!data) { const cnt = badge.querySelector('.count'); if (cnt) cnt.textContent = 'n/a'; return true; }
const dislikes = Number(data.dislikes || data.dislikeCount || 0);
const likes = Number(data.likes || data.likeCount || 0);
const ratio = likes + dislikes > 0 ? Math.round((dislikes / (likes + dislikes)) * 100) : 0;
const cnt = badge.querySelector('.count');
if (cnt) cnt.textContent = `${nfmt(dislikes)} 👎 (${ratio}%)`;
badge.title = `${dislikes.toLocaleString()} dislikes\n${likes.toLocaleString()} likes`;
badge.classList.remove('ryd-muted');
return true;
};
// Immediate attempt, then retry a few seconds while YouTube lays out
const okNow = await tryAttach();
if (okNow) return;
let tries = 0;
clearInterval(hostRetryTimer);
hostRetryTimer = setInterval(async () => {
tries++;
const done = await tryAttach();
if (done || tries > 60) { // up to ~30s for very slow layouts
clearInterval(hostRetryTimer);
hostRetryTimer = null;
if (!done) {
// Final fallback: fixed overlay attached to body
try {
let fixed = document.querySelector('.ryd-fixed-wrap');
if (!fixed) {
fixed = document.createElement('div');
fixed.className = 'ryd-fixed-wrap';
document.body.appendChild(fixed);
}
const badge = ensureBadge(fixed);
if (badge) {
badge.classList.add('ryd-muted');
const ticket = ++pending;
const data = await fetchRyd(vid);
if (ticket === pending && lastVideoId === vid) {
const dislikes = Number((data && (data.dislikes || data.dislikeCount)) || 0);
const likes = Number((data && (data.likes || data.likeCount)) || 0);
const ratio = likes + dislikes > 0 ? Math.round((dislikes / (likes + dislikes)) * 100) : 0;
const cnt = badge.querySelector('.count');
if (cnt) cnt.textContent = `${nfmt(dislikes)} 👎 (${ratio}%)`;
badge.title = `${dislikes.toLocaleString()} dislikes\n${likes.toLocaleString()} likes`;
badge.classList.remove('ryd-muted');
}
}
} catch {}
}
}
}, 500);
}
function observeUrlChanges() {
// Single-page app navigations
let last = location.href;
const mo = new MutationObserver(() => {
if (location.href !== last) { last = location.href; updateForCurrentUrl(); }
});
mo.observe(document, { subtree: true, childList: true });
window.addEventListener('yt-navigate-finish', updateForCurrentUrl, true);
window.addEventListener('popstate', updateForCurrentUrl, true);
window.addEventListener('yt-page-data-updated', updateForCurrentUrl, true);
}
document.addEventListener('readystatechange', () => { if (document.readyState === 'interactive') updateForCurrentUrl(); });
document.addEventListener('DOMContentLoaded', () => {
// Only act on YouTube
if (!/^(?:.*\.)?youtube\.com$/.test(location.hostname) && location.hostname !== 'youtu.be') return;
updateForCurrentUrl();
observeUrlChanges();
// Also schedule a couple of follow-up attempts after page scripts settle
setTimeout(updateForCurrentUrl, 1500);
setTimeout(updateForCurrentUrl, 3500);
});