Shows media bias ratings from AllSides next to each link on Drudge Report
Size
12.8 KB
Version
1.0.1
Created
Jan 28, 2026
Updated
6 days ago
1// ==UserScript==
2// @name Drudge Report Bias Indicator
3// @description Shows media bias ratings from AllSides next to each link on Drudge Report
4// @version 1.0.1
5// @match https://*.drudgereport.com/*
6// @icon https://www.drudgereport.com/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Drudge Report Bias Indicator: Starting...');
12
13 // Bias rating colors and icons
14 const BIAS_CONFIG = {
15 'Left': { color: '#0645AD', icon: '◀◀', label: 'Left' },
16 'Lean Left': { color: '#6495ED', icon: '◀', label: 'Lean Left' },
17 'Center': { color: '#9370DB', icon: '●', label: 'Center' },
18 'Lean Right': { color: '#FF6B6B', icon: '▶', label: 'Lean Right' },
19 'Right': { color: '#DC143C', icon: '▶▶', label: 'Right' }
20 };
21
22 // Cache for bias ratings
23 let biasCache = {};
24
25 // Extract domain from URL
26 function extractDomain(url) {
27 try {
28 const urlObj = new URL(url);
29 let domain = urlObj.hostname;
30 // Remove www. prefix
31 domain = domain.replace(/^www\./, '');
32 return domain;
33 } catch (e) {
34 console.error('Error extracting domain:', e);
35 return null;
36 }
37 }
38
39 // Fetch AllSides bias ratings
40 async function fetchBiasRatings() {
41 console.log('Fetching AllSides bias ratings...');
42
43 try {
44 const response = await GM.xmlhttpRequest({
45 method: 'GET',
46 url: 'https://www.allsides.com/media-bias/ratings',
47 responseType: 'text'
48 });
49
50 console.log('Response received, status:', response.status);
51 console.log('Response text length:', response.responseText ? response.responseText.length : 0);
52
53 const parser = new DOMParser();
54 const doc = parser.parseFromString(response.responseText, 'text/html');
55
56 const ratings = {};
57
58 // Parse the ratings table
59 const allRows = doc.querySelectorAll('tr');
60 console.log('Found rows:', allRows.length);
61
62 let foundCount = 0;
63 doc.querySelectorAll('tr').forEach(row => {
64 const nameLink = row.querySelector('a[href*="/news-source/"]');
65 const biasImg = row.querySelector('img[src*="bias"]');
66
67 if (nameLink && biasImg) {
68 foundCount++;
69 const sourceName = nameLink.textContent.trim().toLowerCase();
70 const biasRating = biasImg.alt;
71
72 // Store by source name
73 ratings[sourceName] = biasRating;
74
75 if (foundCount <= 5) {
76 console.log(`Found rating: ${sourceName} = ${biasRating}`);
77 }
78 }
79 });
80
81 console.log(`Total sources found: ${foundCount}`);
82
83 // If we didn't find any sources, log some debug info
84 if (foundCount === 0) {
85 console.log('No sources found. Checking page structure...');
86 console.log('Sample HTML:', response.responseText.substring(0, 500));
87 const allLinks = doc.querySelectorAll('a');
88 console.log('Total links found:', allLinks.length);
89 const allImages = doc.querySelectorAll('img');
90 console.log('Total images found:', allImages.length);
91 }
92
93 // Also try to get domain mappings by visiting some source pages
94 // For now, we'll use common domain mappings
95 const domainMappings = {
96 'nytimes.com': 'new york times',
97 'washingtonpost.com': 'washington post',
98 'wsj.com': 'wall street journal',
99 'foxnews.com': 'fox news',
100 'cnn.com': 'cnn',
101 'msnbc.com': 'msnbc',
102 'breitbart.com': 'breitbart',
103 'huffpost.com': 'huffpost',
104 'dailymail.co.uk': 'daily mail',
105 'theguardian.com': 'the guardian',
106 'bbc.com': 'bbc news',
107 'reuters.com': 'reuters',
108 'apnews.com': 'associated press',
109 'politico.com': 'politico',
110 'thehill.com': 'the hill',
111 'usatoday.com': 'usa today',
112 'nbcnews.com': 'nbc news',
113 'abcnews.go.com': 'abc news',
114 'cbsnews.com': 'cbs news',
115 'bloomberg.com': 'bloomberg',
116 'time.com': 'time',
117 'newsweek.com': 'newsweek',
118 'theatlantic.com': 'the atlantic',
119 'newyorker.com': 'the new yorker',
120 'vox.com': 'vox',
121 'slate.com': 'slate',
122 'nationalreview.com': 'national review',
123 'reason.com': 'reason',
124 'axios.com': 'axios',
125 'theintercept.com': 'the intercept',
126 'propublica.org': 'propublica',
127 'motherjones.com': 'mother jones',
128 'thedailybeast.com': 'the daily beast',
129 'nypost.com': 'new york post',
130 'washingtontimes.com': 'washington times',
131 'oann.com': 'one america news',
132 'newsmax.com': 'newsmax',
133 'thefederalist.com': 'the federalist',
134 'dailywire.com': 'daily wire',
135 'townhall.com': 'townhall',
136 'redstate.com': 'redstate',
137 'spectator.org': 'american spectator',
138 'theamericanconservative.com': 'the american conservative',
139 'jacobin.com': 'jacobin',
140 'commondreams.org': 'common dreams',
141 'truthout.org': 'truthout',
142 'thenation.com': 'the nation',
143 'thinkprogress.org': 'thinkprogress',
144 'mediamatters.org': 'media matters',
145 'freebeacon.com': 'washington free beacon',
146 'dailycaller.com': 'daily caller',
147 'mirror.co.uk': 'daily mirror',
148 'independent.co.uk': 'the independent',
149 'telegraph.co.uk': 'the telegraph',
150 'economist.com': 'the economist',
151 'ft.com': 'financial times',
152 'aljazeera.com': 'al jazeera',
153 'dw.com': 'deutsche welle',
154 'france24.com': 'france 24',
155 'scmp.com': 'south china morning post',
156 'japantimes.co.jp': 'japan times',
157 'thestar.com': 'toronto star',
158 'globeandmail.com': 'globe and mail',
159 'smh.com.au': 'sydney morning herald',
160 'abc.net.au': 'abc news australia',
161 'spiegel.de': 'der spiegel',
162 'lemonde.fr': 'le monde',
163 'elpais.com': 'el país',
164 'corriere.it': 'corriere della sera',
165 'nzherald.co.nz': 'new zealand herald',
166 'straitstimes.com': 'straits times',
167 'hindustantimes.com': 'hindustan times',
168 'timesofindia.com': 'times of india',
169 'dawn.com': 'dawn',
170 'nation.co.ke': 'daily nation',
171 'standardmedia.co.ke': 'the standard',
172 'thetimes.co.uk': 'the times',
173 'thesun.co.uk': 'the sun',
174 'express.co.uk': 'daily express',
175 'metro.co.uk': 'metro',
176 'msn.com': 'msn',
177 'yahoo.com': 'yahoo news',
178 'google.com': 'google news',
179 'x.com': 'x (twitter)',
180 'twitter.com': 'x (twitter)',
181 'facebook.com': 'facebook',
182 'instagram.com': 'instagram',
183 'tiktok.com': 'tiktok',
184 'youtube.com': 'youtube',
185 'reddit.com': 'reddit',
186 'medium.com': 'medium',
187 'substack.com': 'substack',
188 'ktsa.com': 'ktsa',
189 'wtop.com': 'wtop'
190 };
191
192 // Add domain mappings to ratings
193 for (const [domain, sourceName] of Object.entries(domainMappings)) {
194 if (ratings[sourceName]) {
195 ratings[domain] = ratings[sourceName];
196 }
197 }
198
199 console.log(`Loaded ${Object.keys(ratings).length} bias ratings`);
200 return ratings;
201
202 } catch (error) {
203 console.error('Error fetching bias ratings:', error);
204 return {};
205 }
206 }
207
208 // Get bias rating for a URL
209 function getBiasRating(url) {
210 const domain = extractDomain(url);
211 if (!domain) return null;
212
213 // Check exact domain match
214 if (biasCache[domain]) {
215 return biasCache[domain];
216 }
217
218 // Check if any cached key contains the domain or vice versa
219 for (const [key, value] of Object.entries(biasCache)) {
220 if (domain.includes(key) || key.includes(domain)) {
221 return value;
222 }
223 }
224
225 return null;
226 }
227
228 // Add bias indicator to a link
229 function addBiasIndicator(link) {
230 const href = link.getAttribute('href');
231 if (!href || href.startsWith('#') || href.startsWith('javascript:')) {
232 return;
233 }
234
235 // Skip if already processed
236 if (link.hasAttribute('data-bias-processed')) {
237 return;
238 }
239 link.setAttribute('data-bias-processed', 'true');
240
241 const biasRating = getBiasRating(href);
242 if (!biasRating || !BIAS_CONFIG[biasRating]) {
243 return;
244 }
245
246 const config = BIAS_CONFIG[biasRating];
247
248 // Create bias indicator
249 const indicator = document.createElement('span');
250 indicator.className = 'bias-indicator';
251 indicator.textContent = config.icon;
252 indicator.title = `AllSides Rating: ${config.label}`;
253 indicator.style.cssText = `
254 color: ${config.color};
255 font-weight: bold;
256 margin-left: 4px;
257 font-size: 0.9em;
258 cursor: help;
259 display: inline-block;
260 `;
261
262 // Insert after the link
263 link.parentNode.insertBefore(indicator, link.nextSibling);
264
265 console.log(`Added ${biasRating} indicator to: ${href}`);
266 }
267
268 // Process all links on the page
269 function processLinks() {
270 console.log('Processing links...');
271 const links = document.querySelectorAll('a[href]');
272 let processed = 0;
273
274 links.forEach(link => {
275 addBiasIndicator(link);
276 processed++;
277 });
278
279 console.log(`Processed ${processed} links`);
280 }
281
282 // Debounce function
283 function debounce(func, wait) {
284 let timeout;
285 return function executedFunction(...args) {
286 const later = () => {
287 clearTimeout(timeout);
288 func(...args);
289 };
290 clearTimeout(timeout);
291 timeout = setTimeout(later, wait);
292 };
293 }
294
295 // Initialize the extension
296 async function init() {
297 console.log('Initializing Drudge Report Bias Indicator...');
298
299 // Load cached ratings from storage
300 const cachedData = await GM.getValue('biasRatings', null);
301 const cacheTime = await GM.getValue('biasRatingsTime', 0);
302 const now = Date.now();
303
304 // Force fresh fetch for debugging (change to 24 * 60 * 60 * 1000 for production)
305 const cacheExpiry = 0; // Force fresh fetch every time for now
306
307 // Use cache if less than 24 hours old
308 if (cachedData && (now - cacheTime) < cacheExpiry) {
309 console.log('Using cached bias ratings');
310 biasCache = JSON.parse(cachedData);
311 console.log(`Loaded ${Object.keys(biasCache).length} ratings from cache`);
312 } else {
313 console.log('Fetching fresh bias ratings');
314 biasCache = await fetchBiasRatings();
315 await GM.setValue('biasRatings', JSON.stringify(biasCache));
316 await GM.setValue('biasRatingsTime', now);
317 }
318
319 // Process existing links
320 processLinks();
321
322 // Watch for new links being added
323 const observer = new MutationObserver(debounce(() => {
324 processLinks();
325 }, 500));
326
327 observer.observe(document.body, {
328 childList: true,
329 subtree: true
330 });
331
332 console.log('Drudge Report Bias Indicator: Ready!');
333 }
334
335 // Start when DOM is ready
336 if (document.readyState === 'loading') {
337 document.addEventListener('DOMContentLoaded', init);
338 } else {
339 init();
340 }
341
342})();