The Information | Find Alternative Articles

Displays a combined list of alternative articles from Google News and Bing News

  1. // ==UserScript==
  2. // @name The Information | Find Alternative Articles
  3. // @namespace http://your.namespace
  4. // @version 1.2
  5. // @icon https://www.google.com/s2/favicons?domain=theinformation.com&sz=64
  6. // @description Displays a combined list of alternative articles from Google News and Bing News
  7. // @author UniverseDev
  8. // @license MIT
  9. // @match https://www.theinformation.com/articles/*
  10. // @grant GM.xmlHttpRequest
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const pageTitle = document.title;
  17. const articleTitle = pageTitle.split(' | ')[0];
  18. const googleRssUrl = `https://news.google.com/rss/search?q=${encodeURIComponent(articleTitle)}`;
  19. const bingRssUrl = `https://www.bing.com/news/search?q=${encodeURIComponent(articleTitle)}&format=rss`;
  20.  
  21. const container = document.createElement('div');
  22. container.style.cssText = `
  23. position: fixed;
  24. top: 150px;
  25. right: 10px;
  26. width: 300px;
  27. max-height: 80vh;
  28. overflow-y: auto;
  29. background: white;
  30. padding: 15px;
  31. font-family: Arial, sans-serif;
  32. z-index: 9999;
  33. `;
  34. container.innerHTML = '<h3 style="margin: 0 0 15px 0; color: #000; font-size: 18px; font-weight: bold;">Alternative Articles</h3><p style="margin: 0; color: #666;">Loading...</p>';
  35. document.body.appendChild(container);
  36.  
  37. function parseGoogleItem(item) {
  38. return {
  39. title: item.querySelector('title')?.textContent || '',
  40. url: item.querySelector('link')?.textContent || '',
  41. source: item.querySelector('source')?.textContent || 'Unknown',
  42. date: item.querySelector('pubDate')?.textContent || ''
  43. };
  44. }
  45.  
  46. function parseBingItem(item) {
  47. const description = item.querySelector('description')?.textContent || '';
  48. const actualUrlMatch = description.match(/href="([^"]+)"/);
  49. const actualUrl = actualUrlMatch ? actualUrlMatch[1] : item.querySelector('link')?.textContent || '';
  50. const publication = item.getElementsByTagName('news:publication')[0];
  51. const source = publication?.getElementsByTagName('news:name')[0]?.textContent || 'Unknown';
  52. return {
  53. title: item.querySelector('title')?.textContent || '',
  54. url: actualUrl,
  55. source: source,
  56. date: item.querySelector('pubDate')?.textContent || ''
  57. };
  58. }
  59.  
  60. function normalizeUrl(url) {
  61. try {
  62. const urlObj = new URL(url);
  63. return urlObj.hostname + urlObj.pathname;
  64. } catch (e) {
  65. return url;
  66. }
  67. }
  68.  
  69. function fetchRss(url, parseItem) {
  70. return new Promise(resolve => {
  71. GM.xmlHttpRequest({
  72. method: 'GET',
  73. url: url,
  74. onload: function(response) {
  75. if (response.status === 200) {
  76. const xmlText = response.responseText;
  77. const parser = new DOMParser();
  78. const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
  79. const items = xmlDoc.querySelectorAll('item');
  80. const parsedArticles = Array.from(items).map(parseItem);
  81. resolve({ articles: parsedArticles, error: null });
  82. } else {
  83. resolve({ articles: [], error: `Error loading articles from ${url}` });
  84. }
  85. },
  86. onerror: function(error) {
  87. resolve({ articles: [], error: error.message });
  88. }
  89. });
  90. });
  91. }
  92.  
  93. Promise.all([
  94. fetchRss(googleRssUrl, parseGoogleItem),
  95. fetchRss(bingRssUrl, parseBingItem)
  96. ]).then(results => {
  97. const allArticles = results.flatMap(result => result.articles);
  98. const errors = results.map(result => result.error).filter(Boolean);
  99.  
  100. const filteredArticles = allArticles.filter(article =>
  101. !article.url.toLowerCase().includes('theinformation.com') &&
  102. !article.source.toLowerCase().includes('the information')
  103. );
  104.  
  105. const uniqueArticles = Array.from(new Map(filteredArticles.map(article => [normalizeUrl(article.url), article])).values());
  106.  
  107. uniqueArticles.sort((a, b) => {
  108. const dateA = a.date ? new Date(a.date) : new Date(0);
  109. const dateB = b.date ? new Date(b.date) : new Date(0);
  110. return dateB - dateA;
  111. });
  112.  
  113. const topArticles = uniqueArticles.slice(0, 10);
  114.  
  115. if (topArticles.length > 0) {
  116. const articlesHtml = topArticles.map(article => `
  117. <div style="margin-bottom: 20px;">
  118. <a href="${article.url}" target="_blank" style="display: block; color: #000; font-weight: bold; font-size: 16px; text-decoration: none; margin-bottom: 5px;">${article.title}</a>
  119. <small style="color: #666; font-size: 12px;">${article.source} - ${article.date ? new Date(article.date).toLocaleDateString() : 'Unknown date'}</small>
  120. </div>
  121. `).join('');
  122. container.innerHTML = `
  123. <h3 style="margin: 0 0 15px 0; color: #000; font-size: 18px; font-weight: bold;">Alternative Articles <a href="#" style="float: right; font-size: 14px; color: #666; text-decoration: none;" id="close-alternatives">✕</a></h3>
  124. ${articlesHtml}
  125. `;
  126. } else {
  127. container.innerHTML = '<h3 style="margin: 0 0 15px 0; color: #000; font-size: 18px; font-weight: bold;">Alternative Articles</h3><p style="margin: 0; color: #666;">No articles found.</p>';
  128. }
  129.  
  130. container.querySelector('#close-alternatives')?.addEventListener('click', (e) => {
  131. e.preventDefault();
  132. container.style.display = 'none';
  133. });
  134.  
  135. if (errors.length > 0) {
  136. const errorHtml = errors.map(error => `<p style="margin: 15px 0 10px 0; color: #666;">${error}</p>`).join('');
  137. container.insertAdjacentHTML('beforeend', errorHtml);
  138. }
  139. });
  140. })();