Shows media bias ratings from AllSides next to each link on Drudge Report
Size
20.8 KB
Version
1.1.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.1.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 // Since scraping isn't working, use hardcoded ratings based on AllSides data
94 // Add direct domain-to-rating mappings
95 const directRatings = {
96 // Left
97 'alternet.org': 'Left',
98 'commondreams.org': 'Left',
99 'jacobin.com': 'Left',
100 'motherjones.com': 'Left',
101 'thenation.com': 'Left',
102 'truthout.org': 'Left',
103 'mediamatters.org': 'Left',
104
105 // Lean Left
106 'nytimes.com': 'Lean Left',
107 'washingtonpost.com': 'Lean Left',
108 'cnn.com': 'Lean Left',
109 'msnbc.com': 'Lean Left',
110 'huffpost.com': 'Lean Left',
111 'politico.com': 'Lean Left',
112 'vox.com': 'Lean Left',
113 'slate.com': 'Lean Left',
114 'axios.com': 'Lean Left',
115 'theintercept.com': 'Lean Left',
116 'thedailybeast.com': 'Lean Left',
117 'theguardian.com': 'Lean Left',
118 'theatlantic.com': 'Lean Left',
119 'newyorker.com': 'Lean Left',
120 'propublica.org': 'Lean Left',
121 'nbcnews.com': 'Lean Left',
122 'abcnews.go.com': 'Lean Left',
123 'cbsnews.com': 'Lean Left',
124 'time.com': 'Lean Left',
125 'newsweek.com': 'Lean Left',
126 'thehill.com': 'Lean Left',
127 'usatoday.com': 'Lean Left',
128
129 // Center
130 'bbc.com': 'Center',
131 'reuters.com': 'Center',
132 'apnews.com': 'Center',
133 'bloomberg.com': 'Center',
134 'economist.com': 'Center',
135 'ft.com': 'Center',
136 'aljazeera.com': 'Center',
137
138 // Lean Right
139 'wsj.com': 'Lean Right',
140 'foxnews.com': 'Lean Right',
141 'nypost.com': 'Lean Right',
142 'washingtontimes.com': 'Lean Right',
143 'reason.com': 'Lean Right',
144 'nationalreview.com': 'Lean Right',
145 'theamericanconservative.com': 'Lean Right',
146
147 // Right
148 'breitbart.com': 'Right',
149 'dailywire.com': 'Right',
150 'newsmax.com': 'Right',
151 'oann.com': 'Right',
152 'thefederalist.com': 'Right',
153 'townhall.com': 'Right',
154 'redstate.com': 'Right',
155 'dailycaller.com': 'Right',
156 'freebeacon.com': 'Right',
157 'spectator.org': 'Right',
158
159 // Additional sources
160 'dailymail.co.uk': 'Right',
161 'mirror.co.uk': 'Lean Left',
162 'independent.co.uk': 'Lean Left',
163 'telegraph.co.uk': 'Lean Right',
164 'thetimes.co.uk': 'Center',
165 'thesun.co.uk': 'Lean Right',
166 'express.co.uk': 'Right',
167 'metro.co.uk': 'Lean Left',
168 'msn.com': 'Center',
169 'yahoo.com': 'Center',
170 'ktsa.com': 'Lean Right',
171 'wtop.com': 'Center'
172 };
173
174 // Merge direct ratings with any scraped ratings
175 Object.assign(ratings, directRatings);
176
177 // Also try to get domain mappings by visiting some source pages
178 // For now, we'll use common domain mappings
179 const domainMappings = {
180 'nytimes.com': 'new york times',
181 'washingtonpost.com': 'washington post',
182 'wsj.com': 'wall street journal',
183 'foxnews.com': 'fox news',
184 'cnn.com': 'cnn',
185 'msnbc.com': 'msnbc',
186 'breitbart.com': 'breitbart',
187 'huffpost.com': 'huffpost',
188 'dailymail.co.uk': 'daily mail',
189 'theguardian.com': 'the guardian',
190 'bbc.com': 'bbc news',
191 'reuters.com': 'reuters',
192 'apnews.com': 'associated press',
193 'politico.com': 'politico',
194 'thehill.com': 'the hill',
195 'usatoday.com': 'usa today',
196 'nbcnews.com': 'nbc news',
197 'abcnews.go.com': 'abc news',
198 'cbsnews.com': 'cbs news',
199 'bloomberg.com': 'bloomberg',
200 'time.com': 'time',
201 'newsweek.com': 'newsweek',
202 'theatlantic.com': 'the atlantic',
203 'newyorker.com': 'the new yorker',
204 'vox.com': 'vox',
205 'slate.com': 'slate',
206 'nationalreview.com': 'national review',
207 'reason.com': 'reason',
208 'axios.com': 'axios',
209 'theintercept.com': 'the intercept',
210 'propublica.org': 'propublica',
211 'motherjones.com': 'mother jones',
212 'thedailybeast.com': 'the daily beast',
213 'nypost.com': 'new york post',
214 'washingtontimes.com': 'washington times',
215 'oann.com': 'one america news',
216 'newsmax.com': 'newsmax',
217 'thefederalist.com': 'the federalist',
218 'dailywire.com': 'daily wire',
219 'townhall.com': 'townhall',
220 'redstate.com': 'redstate',
221 'spectator.org': 'american spectator',
222 'theamericanconservative.com': 'the american conservative',
223 'jacobin.com': 'jacobin',
224 'commondreams.org': 'common dreams',
225 'truthout.org': 'truthout',
226 'thenation.com': 'the nation',
227 'thinkprogress.org': 'thinkprogress',
228 'mediamatters.org': 'media matters',
229 'freebeacon.com': 'washington free beacon',
230 'dailycaller.com': 'daily caller',
231 'mirror.co.uk': 'daily mirror',
232 'independent.co.uk': 'the independent',
233 'telegraph.co.uk': 'the telegraph',
234 'economist.com': 'the economist',
235 'ft.com': 'financial times',
236 'aljazeera.com': 'al jazeera',
237 'dw.com': 'deutsche welle',
238 'france24.com': 'france 24',
239 'scmp.com': 'south china morning post',
240 'japantimes.co.jp': 'japan times',
241 'thestar.com': 'toronto star',
242 'globeandmail.com': 'globe and mail',
243 'smh.com.au': 'sydney morning herald',
244 'abc.net.au': 'abc news australia',
245 'spiegel.de': 'der spiegel',
246 'lemonde.fr': 'le monde',
247 'elpais.com': 'el país',
248 'corriere.it': 'corriere della sera',
249 'nzherald.co.nz': 'new zealand herald',
250 'straitstimes.com': 'straits times',
251 'hindustantimes.com': 'hindustan times',
252 'timesofindia.com': 'times of india',
253 'dawn.com': 'dawn',
254 'nation.co.ke': 'daily nation',
255 'standardmedia.co.ke': 'the standard',
256 'thetimes.co.uk': 'the times',
257 'thesun.co.uk': 'the sun',
258 'express.co.uk': 'daily express',
259 'metro.co.uk': 'metro',
260 'msn.com': 'msn',
261 'yahoo.com': 'yahoo news',
262 'google.com': 'google news',
263 'x.com': 'x (twitter)',
264 'twitter.com': 'x (twitter)',
265 'facebook.com': 'facebook',
266 'instagram.com': 'instagram',
267 'tiktok.com': 'tiktok',
268 'youtube.com': 'youtube',
269 'reddit.com': 'reddit',
270 'medium.com': 'medium',
271 'substack.com': 'substack',
272 'ktsa.com': 'ktsa',
273 'wtop.com': 'wtop'
274 };
275
276 // Add domain mappings to ratings
277 for (const [domain, sourceName] of Object.entries(domainMappings)) {
278 if (ratings[sourceName]) {
279 ratings[domain] = ratings[sourceName];
280 }
281 }
282
283 console.log(`Loaded ${Object.keys(ratings).length} bias ratings`);
284 return ratings;
285
286 } catch (error) {
287 console.error('Error fetching bias ratings:', error);
288 return {};
289 }
290 }
291
292 // Get bias rating for a URL
293 function getBiasRating(url) {
294 const domain = extractDomain(url);
295 if (!domain) return null;
296
297 // Check exact domain match
298 if (biasCache[domain]) {
299 return biasCache[domain];
300 }
301
302 // Check if any cached key contains the domain or vice versa
303 for (const [key, value] of Object.entries(biasCache)) {
304 if (domain.includes(key) || key.includes(domain)) {
305 return value;
306 }
307 }
308
309 return null;
310 }
311
312 // Add bias indicator to a link
313 function addBiasIndicator(link) {
314 const href = link.getAttribute('href');
315 if (!href || href.startsWith('#') || href.startsWith('javascript:')) {
316 return;
317 }
318
319 // Skip if already processed
320 if (link.hasAttribute('data-bias-processed')) {
321 return;
322 }
323 link.setAttribute('data-bias-processed', 'true');
324
325 const biasRating = getBiasRating(href);
326 if (!biasRating || !BIAS_CONFIG[biasRating]) {
327 return;
328 }
329
330 const config = BIAS_CONFIG[biasRating];
331
332 // Create bias indicator
333 const indicator = document.createElement('span');
334 indicator.className = 'bias-indicator';
335 indicator.textContent = config.icon;
336 indicator.title = `AllSides Rating: ${config.label}`;
337 indicator.style.cssText = `
338 color: ${config.color};
339 font-weight: bold;
340 margin-left: 4px;
341 font-size: 0.9em;
342 cursor: help;
343 display: inline-block;
344 `;
345
346 // Insert after the link
347 link.parentNode.insertBefore(indicator, link.nextSibling);
348
349 console.log(`Added ${biasRating} indicator to: ${href}`);
350 }
351
352 // Process all links on the page
353 function processLinks() {
354 console.log('Processing links...');
355 const links = document.querySelectorAll('a[href]');
356 let processed = 0;
357
358 links.forEach(link => {
359 addBiasIndicator(link);
360 processed++;
361 });
362
363 console.log(`Processed ${processed} links`);
364 }
365
366 // Create bias analysis summary panel
367 function createBiasSummary() {
368 // Count bias indicators by category
369 const indicators = document.querySelectorAll('.bias-indicator');
370 const counts = {
371 'Left': 0,
372 'Lean Left': 0,
373 'Center': 0,
374 'Lean Right': 0,
375 'Right': 0
376 };
377
378 indicators.forEach(indicator => {
379 const title = indicator.getAttribute('title');
380 const rating = title.replace('AllSides Rating: ', '');
381 if (counts.hasOwnProperty(rating)) {
382 counts[rating]++;
383 }
384 });
385
386 const total = indicators.length;
387 if (total === 0) {
388 console.log('No bias indicators found, skipping summary panel');
389 return;
390 }
391
392 // Calculate percentages
393 const percentages = {};
394 for (const [category, count] of Object.entries(counts)) {
395 percentages[category] = total > 0 ? Math.round((count / total) * 100) : 0;
396 }
397
398 // Remove existing summary if present
399 const existingSummary = document.getElementById('bias-summary-panel');
400 if (existingSummary) {
401 existingSummary.remove();
402 }
403
404 // Create summary panel
405 const summaryPanel = document.createElement('div');
406 summaryPanel.id = 'bias-summary-panel';
407 summaryPanel.style.cssText = `
408 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
409 color: white;
410 padding: 20px;
411 margin: 10px auto;
412 max-width: 800px;
413 border-radius: 10px;
414 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
415 font-family: Arial, sans-serif;
416 position: relative;
417 z-index: 1000;
418 `;
419
420 // Create title
421 const title = document.createElement('h3');
422 title.textContent = `Media Bias Analysis (${total} sources)`;
423 title.style.cssText = `
424 margin: 0 0 15px 0;
425 font-size: 18px;
426 font-weight: bold;
427 text-align: center;
428 `;
429 summaryPanel.appendChild(title);
430
431 // Create stats container
432 const statsContainer = document.createElement('div');
433 statsContainer.style.cssText = `
434 display: flex;
435 justify-content: space-around;
436 flex-wrap: wrap;
437 gap: 10px;
438 `;
439
440 // Add each category
441 for (const [category, count] of Object.entries(counts)) {
442 if (count === 0) continue;
443
444 const config = BIAS_CONFIG[category];
445 const percentage = percentages[category];
446
447 const statItem = document.createElement('div');
448 statItem.style.cssText = `
449 background: rgba(255, 255, 255, 0.2);
450 padding: 10px 15px;
451 border-radius: 8px;
452 text-align: center;
453 min-width: 120px;
454 backdrop-filter: blur(10px);
455 `;
456
457 const icon = document.createElement('div');
458 icon.textContent = config.icon;
459 icon.style.cssText = `
460 font-size: 24px;
461 margin-bottom: 5px;
462 color: ${config.color};
463 filter: brightness(1.5);
464 `;
465
466 const label = document.createElement('div');
467 label.textContent = category;
468 label.style.cssText = `
469 font-size: 12px;
470 margin-bottom: 5px;
471 opacity: 0.9;
472 `;
473
474 const value = document.createElement('div');
475 value.textContent = `${percentage}% (${count})`;
476 value.style.cssText = `
477 font-size: 16px;
478 font-weight: bold;
479 `;
480
481 statItem.appendChild(icon);
482 statItem.appendChild(label);
483 statItem.appendChild(value);
484 statsContainer.appendChild(statItem);
485 }
486
487 summaryPanel.appendChild(statsContainer);
488
489 // Insert at the top of the page
490 const body = document.body;
491 if (body.firstChild) {
492 body.insertBefore(summaryPanel, body.firstChild);
493 } else {
494 body.appendChild(summaryPanel);
495 }
496
497 console.log('Bias summary panel created');
498 }
499
500 // Debounce function
501 function debounce(func, wait) {
502 let timeout;
503 return function executedFunction(...args) {
504 const later = () => {
505 clearTimeout(timeout);
506 func(...args);
507 };
508 clearTimeout(timeout);
509 timeout = setTimeout(later, wait);
510 };
511 }
512
513 // Initialize the extension
514 async function init() {
515 console.log('Initializing Drudge Report Bias Indicator...');
516
517 // Load cached ratings from storage
518 const cachedData = await GM.getValue('biasRatings', null);
519 const cacheTime = await GM.getValue('biasRatingsTime', 0);
520 const now = Date.now();
521
522 // Force fresh fetch for debugging (change to 24 * 60 * 60 * 1000 for production)
523 const cacheExpiry = 0; // Force fresh fetch every time for now
524
525 // Use cache if less than 24 hours old
526 if (cachedData && (now - cacheTime) < cacheExpiry) {
527 console.log('Using cached bias ratings');
528 biasCache = JSON.parse(cachedData);
529 console.log(`Loaded ${Object.keys(biasCache).length} ratings from cache`);
530 } else {
531 console.log('Fetching fresh bias ratings');
532 biasCache = await fetchBiasRatings();
533 await GM.setValue('biasRatings', JSON.stringify(biasCache));
534 await GM.setValue('biasRatingsTime', now);
535 }
536
537 // Process existing links
538 processLinks();
539
540 // Create bias summary panel after a short delay to ensure all indicators are added
541 setTimeout(() => {
542 createBiasSummary();
543 }, 1000);
544
545 // Watch for new links being added
546 const observer = new MutationObserver(debounce(() => {
547 processLinks();
548 createBiasSummary();
549 }, 500));
550
551 observer.observe(document.body, {
552 childList: true,
553 subtree: true
554 });
555
556 console.log('Drudge Report Bias Indicator: Ready!');
557 }
558
559 // Start when DOM is ready
560 if (document.readyState === 'loading') {
561 document.addEventListener('DOMContentLoaded', init);
562 } else {
563 init();
564 }
565
566})();