Drudge Report Bias Indicator

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