Drudge Report Bias Indicator

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})();
Drudge Report Bias Indicator | Robomonkey