Google News Enhanced via Gemini AI

Google News with AI-Generated Annotation via Gemini

当前为 2024-09-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @match https://news.google.com/*
  3. // @name Google News Enhanced via Gemini AI
  4. // @version 3.0
  5. // @license MIT
  6. // @namespace djshigel
  7. // @description Google News with AI-Generated Annotation via Gemini
  8. // @run-at document-end
  9. // @grant GM.setValue
  10. // @grant GM.getValue
  11. // ==/UserScript==
  12.  
  13. (async () => {
  14. let GEMINI_API_KEY = await GM.getValue("GEMINI_API_KEY") ;
  15. if (!GEMINI_API_KEY || !Object.keys(GEMINI_API_KEY).length) {
  16. GEMINI_API_KEY = window.prompt('Get Generative Language Client API key from Google AI Studio\nhttps://ai.google.dev/aistudio', '');
  17. await GM.setValue("GEMINI_API_KEY", GEMINI_API_KEY);
  18. }
  19. const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${GEMINI_API_KEY}`;
  20. const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  21. let consecutive429Count = 0;
  22.  
  23. // ########## Header ##########
  24. function insertHeaderStyle() {
  25. const $headerStyle = document.createElement('style');
  26. const header = document.querySelector('header[role="banner"]');
  27. $headerStyle.innerText = `
  28. @media screen and (max-height: 860px) {
  29. header[role="banner"] {
  30. position: absolute!important;
  31. margin-bottom : -${header.clientHeight}px;
  32. }
  33. }`;
  34. document.querySelector('head').appendChild($headerStyle);
  35. }
  36.  
  37. // ########## Load continuous page sections ##########
  38.  
  39. const loadContinuous = async () => {
  40. for (let i = 0; i < 20; i++) {
  41. await delay(100);
  42. let intersectionObservedElement = document.querySelector('main c-wiz > c-wiz ~ div[jsname]');
  43. if (!intersectionObservedElement) break;
  44. intersectionObservedElement.style.position = 'fixed' ;
  45. intersectionObservedElement.style.top = '0';
  46. }
  47. console.log(`loaded: ${document.querySelectorAll('main c-wiz > c-wiz').length} pages`);
  48. await delay(1000);
  49. };
  50.  
  51. // ########## Extract URL ##########
  52. const fetchRedirectPage = async (href) => {
  53. try {
  54. const response = await fetch(`${window.location.origin}${window.location.pathname.substring(0, window.location.pathname.indexOf('/', 3))}/read/${href}`)
  55. if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
  56. const responseText = await response.text();
  57. const parser = new DOMParser();
  58. return parser.parseFromString(responseText, 'text/html');
  59. } catch (error) {
  60. console.error('Error fetching redirect:', error);
  61. }
  62. }
  63.  
  64. function sendPostRequest(endPoint, param) {
  65. return new Promise((resolve, reject) => {
  66. var xhr = new XMLHttpRequest();
  67.  
  68. xhr.open("POST", endPoint, true);
  69. xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  70. xhr.onreadystatechange = function() {
  71. if (xhr.readyState === 4) {
  72. if (xhr.status === 200) {
  73. resolve(xhr.responseText.replace('httprm', ''));
  74. } else if (xhr.status === 400) {
  75. reject(Error(xhr.responseText));
  76. } else {
  77. reject(new Error("Request failed with status " + xhr.status + ": " + xhr.statusText));
  78. }
  79. }
  80. };
  81. xhr.send(param);
  82. });
  83. }
  84.  
  85. const getExtractedURL = async (href) => {
  86. href = href.replace('./read/', '').split('?')[0].split('_')[0];
  87. try {
  88. for (let i = 0; i < 3; i++) {
  89. const endPoint = `/_/DotsSplashUi/data/batchexecute?source-path=%2Fread%2F${href}`;
  90. const redirectPage = await fetchRedirectPage(href);
  91. if (!redirectPage.querySelector('script[data-id]') || !redirectPage.querySelector('c-wiz>div')) continue;
  92. let token = redirectPage.querySelector('script[data-id]').textContent.match(/[A-Za-z0-9]{25,35}:[0-9]{10,15}/);
  93. if (token) {
  94. token = token[0]
  95. } else {
  96. continue;
  97. }
  98. const signature = redirectPage.querySelector('c-wiz>div').getAttribute('data-n-a-sg');
  99. const timestamp = token.split(':')[1].substring(0,10);
  100. const param = `f.req=%5B%5B%5B%22Fbv4je%22%2C%22%5B%5C%22garturlreq%5C%22%2C%5B%5B%5C%22ja%5C%22%2C%5C%22JP%5C%22%2C%5B%5C%22FINANCE_TOP_INDICES%5C%22%2C%5C%22WEB_TEST_1_0_0%5C%22%5D%2Cnull%2Cnull%2C1%2C1%2C%5C%22JP%3Aja%5C%22%2Cnull%2C540%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2C0%2Cnull%2Cnull%2C%5B1529283084%2C281000000%5D%5D%2C%5C%22ja%5C%22%2C%5C%22JP%5C%22%2C1%2C%5B2%2C3%2C4%2C8%5D%2C1%2C0%2C%5C%22668194412%5C%22%2C0%2C0%2Cnull%2C0%5D%2C%5C%22${href}%5C%22%2C${timestamp}%2C%5C%22${signature}%5C%22%5D%22%2Cnull%2C%22generic%22%5D%5D%5D&at=${token}&`
  101. const response = await sendPostRequest(endPoint, param);
  102. const indexOfStartString = response.replace('httprm', '').indexOf('http');
  103. if (indexOfStartString == -1) continue;
  104. const lengthOfURL = response.substring(indexOfStartString).indexOf('\",') - 1;
  105. return response.substring(indexOfStartString, indexOfStartString + lengthOfURL)
  106. .replace(/\\\\u([a-fA-F0-9]{4})/g, (s, g) => String.fromCharCode(parseInt(g, 16)))
  107. .replace(/\\u([a-fA-F0-9]{4})/g, (s, g) => String.fromCharCode(parseInt(g, 16)));
  108. }
  109. } catch (error) {
  110. document.querySelector('#gemini-ticker').style.opacity = '0';
  111. console.error("URL decode error", error);
  112. return null;
  113. }
  114. };
  115.  
  116. // ########## Forecast ##########
  117. function getCurrentPosition() {
  118. return new Promise((resolve, reject) => {
  119. if (navigator.geolocation) {
  120. navigator.geolocation.getCurrentPosition(resolve, reject);
  121. } else {
  122. reject(new Error("Geolocation is not supported by this browser."));
  123. }
  124. });
  125. }
  126.  
  127. function getCityFromCoordinates(latitude, longitude) {
  128. const apiUrl = (new URL(location.href).searchParams.get('hl') == 'ja') ?
  129. `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${latitude}&longitude=${longitude}&localityLanguage=ja`:
  130. `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${latitude}&longitude=${longitude}&localityLanguage=en`;
  131. return fetch(apiUrl)
  132. .then(response => response.json())
  133. .then(data => data.city)
  134. .catch(error => {
  135. console.error('Error fetching the city data:', error);
  136. throw error;
  137. });
  138. }
  139.  
  140. async function getCity(position) {
  141. try {
  142. const latitude = position.coords.latitude;
  143. const longitude = position.coords.longitude;
  144. const city = await getCityFromCoordinates(latitude, longitude);
  145. return city;
  146. } catch (error) {
  147. document.querySelector('#gemini-ticker').style.opacity = '0';
  148. console.error('Error getting position or city:', error);
  149. throw error;
  150. }
  151. }
  152.  
  153.  
  154. const insertForecastElement = async (forecastLink) => {
  155. if (forecastLink) {
  156. const forecast = document.createElement('div');
  157. forecast.id = 'gemini-forecast';
  158. forecast.style.maxWidth = '320px';
  159. forecast.style.marginLeft = '16px';
  160. forecastLink.parentElement.parentElement.appendChild(forecast);
  161. }
  162. };
  163.  
  164. const processForecast = async () => {
  165. const forecastLink = document.querySelector('a[href*="https://weathernews.jp/"]') ||
  166. document.querySelector('a[href*="https://weather.com/"]');
  167. if (!forecastLink) return;
  168. let geo = '全国' ;
  169. let latitude = null;
  170. let longitude = null;
  171. try {
  172. const position = await getCurrentPosition();
  173. if (position && position.coords && position.coords.latitude && position.coords.longitude) {
  174. latitude = position.coords.latitude;
  175. longitude = position.coords.longitude;
  176. }
  177. geo = await getCity(position);
  178. } catch (error) {
  179. geo = '全国' ;
  180. }
  181. console.log(`forecast: ${geo}`);
  182. for (let attempt = 0; attempt < 3; attempt++) {
  183. try {
  184. document.querySelector('#gemini-ticker').style.opacity = '1';
  185. const response = (new URL(location.href).searchParams.get('hl') == 'ja') ?
  186. await fetch(apiUrl, {
  187. method: 'POST',
  188. headers: { 'Content-Type': 'application/json' },
  189. body: JSON.stringify({
  190. contents: [{
  191. parts: [{
  192. text: `私: URLに対し、次の手順に従ってステップバイステップで実行してください。
  193. 1 URLにアクセス出来なかった場合、結果を出力しない
  194. 2 ${(new Date).toString()}の天気に関する情報を抽出
  195. 3 どのように過ごすべきかを含め、200字程度に具体的に要約
  196. 4 タイトルや見出しを含めず、結果のみ出力
  197. ${geo}の情報: https://weathernews.jp/onebox/${latitude}/${longitude}/
  198. あなた:`
  199. }],
  200. }]
  201. }),
  202. }):
  203. await fetch(apiUrl, {
  204. method: 'POST',
  205. headers: { 'Content-Type': 'application/json' },
  206. body: JSON.stringify({
  207. contents: [{
  208. parts: [{
  209. text: `Me: Follow the steps below to execute step by step for each URL.
  210. 1 If the URL cannot be accessed, do not output the results
  211. 2 Extract weather information from ${(new Date).toString()}
  212. 3 Summarize in detail (about 200 characters) including how to spend the day
  213. 4 Output only the results, without titles or headings
  214. About ${geo}: https://weathernews.jp/onebox/${latitude}/${longitude}/
  215. You:`
  216. }],
  217. }]
  218. }),
  219. });
  220.  
  221. if (!response.ok) {
  222. if (response.status === 429) {
  223. consecutive429Count++;
  224. if (consecutive429Count >= 3) {
  225. console.warn("Too many requests. Pausing for a while...");
  226. await delay(10000);
  227. consecutive429Count = 0;
  228. continue;
  229. }
  230. } else {
  231. throw new Error('Network response was not ok');
  232. }
  233. } else {
  234. consecutive429Count = 0;
  235. }
  236.  
  237. const reader = response.body.getReader();
  238. let result = '', done = false, decoder = new TextDecoder();
  239. while (!done) {
  240. const { value, done: doneReading } = await reader.read();
  241. done = doneReading;
  242. if (value) result += decoder.decode(value, { stream: true });
  243. }
  244. result += decoder.decode();
  245.  
  246. const data = JSON.parse(result);
  247. if (!data.candidates[0]?.content?.parts[0]?.text) continue;
  248. let summary = data.candidates[0].content.parts[0].text.replace(/\*\*/g, '').replace(/##/g, '');
  249. if (summary.length < 80) {
  250. console.error('Summary is too short');
  251. return;
  252. }
  253. console.log(`forecast: ${summary}`);
  254.  
  255. insertForecastElement(forecastLink);
  256. let targetElement = document.querySelector('#gemini-forecast');
  257. if (!targetElement) {
  258. console.error('No target element found for summary insertion');
  259. return;
  260. }
  261.  
  262. let displayText = targetElement.textContent + ' ';
  263. for (const char of summary) {
  264. document.querySelector('#gemini-ticker').style.opacity = '1';
  265. displayText += char + '●';
  266. targetElement.textContent = displayText;
  267. await delay(1);
  268. displayText = displayText.slice(0, -1);
  269. document.querySelector('#gemini-ticker').style.opacity = '0';
  270. }
  271. targetElement.textContent = displayText;
  272. return;
  273. } catch (error) {
  274. document.querySelector('#gemini-ticker').style.opacity = '0';
  275. await delay(5000);
  276. console.error('Error:', error);
  277. }
  278. }
  279. };
  280.  
  281. // ########## Highlight ##########
  282. const insertHighlightElement = () => {
  283. const cWizElements = document.querySelector('aside>c-wiz') ?
  284. document.querySelectorAll('aside>c-wiz>*'):
  285. document.querySelector('main>c-wiz>c-wiz>c-wiz') ?
  286. document.querySelectorAll('main>c-wiz>*'):
  287. document.querySelectorAll('main>c-wiz>c-wiz, main>div>c-wiz, main>div>div>c-wiz');
  288. const validHolders = Array.from(document.querySelectorAll('c-wiz>section, c-wiz>section>div>div, main>div>c-wiz>c-wiz, main>c-wiz>c-wiz>c-wiz')).filter(element => {
  289. const backgroundColor = getComputedStyle(element).backgroundColor;
  290. return backgroundColor !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'transparent';
  291. });
  292. if (cWizElements.length >= 2) {
  293. const targetInsertPosition = cWizElements[1];
  294. const backgroundColor = getComputedStyle(validHolders[0]).backgroundColor;
  295. const cWizElement = document.createElement('c-wiz');
  296. cWizElement.id = 'gemini-highlight';
  297. cWizElement.style.marginBottom = '50px';
  298. cWizElement.style.width = '100%';
  299. cWizElement.innerHTML = (new URL(location.href).searchParams.get('hl') == 'ja') ?
  300. `<section style='margin-top: 20px'>
  301. <div style='
  302. font-size: 1.5em;
  303. margin-bottom: 10px;
  304. -webkit-background-clip: text!important;
  305. -webkit-text-fill-color: transparent;
  306. background: linear-gradient(to right, #4698e2, #c6657b);
  307. width: fit-content;' id='gemini-highlight-header'>
  308. Geminiによるハイライト
  309. </div>
  310. <div style='
  311. background-color: ${backgroundColor};
  312. padding: 16px;
  313. border-radius: 15px;' id='gemini-highlight-content'>
  314. </div>
  315. </section>`:
  316. `<section style='margin-top: 20px'>
  317. <div style='
  318. font-size: 1.5em;
  319. margin-bottom: 10px;
  320. -webkit-background-clip: text!important;
  321. -webkit-text-fill-color: transparent;
  322. background: linear-gradient(to right, #4698e2, #c6657b);
  323. width: fit-content;' id='gemini-highlight-header'>
  324. Highlight via Gemini
  325. </div>
  326. <div style='
  327. background-color: ${backgroundColor};
  328. padding: 16px;
  329. border-radius: 15px;' id='gemini-highlight-content'>
  330. </div>
  331. </section>`;
  332. targetInsertPosition.parentElement.insertBefore(cWizElement, targetInsertPosition);
  333. }
  334. };
  335.  
  336. const processHighlight = async (urls) => {
  337. for (let attempt = 0; attempt < 3; attempt++) {
  338. try {
  339. document.querySelector('#gemini-ticker').style.opacity = '1';
  340. const response = (new URL(location.href).searchParams.get('hl') == 'ja') ?
  341. await fetch(apiUrl, {
  342. method: 'POST',
  343. headers: { 'Content-Type': 'application/json' },
  344. body: JSON.stringify({
  345. contents: [{
  346. parts: [{
  347. text: `次に示す最新のニュースの中から最も重要なニュース1つに対し5文で深堀りをどうぞ。 ${urls}`
  348. }],
  349. }]
  350. }),
  351. }):
  352. await fetch(apiUrl, {
  353. method: 'POST',
  354. headers: { 'Content-Type': 'application/json' },
  355. body: JSON.stringify({
  356. contents: [{
  357. parts: [{
  358. text: `Below, please take a eight-sentence in-depth look at one of the most important recent news stories. ${urls}`
  359. }],
  360. }]
  361. }),
  362. });
  363.  
  364. if (!response.ok) {
  365. if (response.status === 429) {
  366. consecutive429Count++;
  367. if (consecutive429Count >= 3) {
  368. console.warn("Too many requests. Pausing for a while...");
  369. await delay(10000);
  370. consecutive429Count = 0;
  371. continue;
  372. }
  373. } else {
  374. throw new Error('Network response was not ok');
  375. }
  376. } else {
  377. consecutive429Count = 0;
  378. }
  379.  
  380. const reader = response.body.getReader();
  381. let result = '', done = false, decoder = new TextDecoder();
  382. while (!done) {
  383. const { value, done: doneReading } = await reader.read();
  384. done = doneReading;
  385. if (value) result += decoder.decode(value, { stream: true });
  386. }
  387. result += decoder.decode();
  388.  
  389. const data = JSON.parse(result);
  390. if (!data.candidates[0]?.content?.parts[0]?.text) continue;
  391. let summary = data.candidates[0].content.parts[0].text.replace(/\*\*/g, '').replace(/##/g, '');
  392. console.log(`highlights: ${summary}`);
  393.  
  394. insertHighlightElement();
  395. let targetElement = document.querySelector('#gemini-highlight-content');
  396. if (!targetElement) {
  397. console.error('No target element found for summary insertion');
  398. return;
  399. }
  400.  
  401. let displayText = targetElement.textContent + ' ';
  402. for (const char of summary) {
  403. document.querySelector('#gemini-ticker').style.opacity = '1';
  404. displayText += char + '●';
  405. targetElement.textContent = displayText;
  406. await delay(1);
  407. displayText = displayText.slice(0, -1);
  408. document.querySelector('#gemini-ticker').style.opacity = '0';
  409. }
  410. targetElement.textContent = displayText;
  411. return;
  412. } catch (error) {
  413. document.querySelector('#gemini-ticker').style.opacity = '0';
  414. await delay(5000);
  415. console.error('Error:', error);
  416. }
  417. }
  418. };
  419.  
  420. // ########## Article ##########
  421. const processArticle = async (article, links, title, url) => {
  422. try {
  423. document.querySelector('#gemini-ticker').style.opacity = '1';
  424. let summary = await GM.getValue(url);
  425. if (!summary || !Object.keys(summary).length) {
  426. const response = (new URL(location.href).searchParams.get('hl') == 'ja') ?
  427. await fetch(apiUrl, {
  428. method: 'POST',
  429. headers: { 'Content-Type': 'application/json' },
  430. body: JSON.stringify({
  431. contents: [{
  432. parts: [{
  433. text: `私: URLに対し、次の手順に従ってステップバイステップで実行してください。
  434. 1 URLにアクセス出来なかった場合、結果を出力しない
  435. 2 200字程度に学者のように具体的に要約
  436. 3 タイトルや見出しを含めず、結果のみを出力
  437. ${title}のURL: ${url}
  438. あなた:`
  439. }],
  440. }]
  441. }),
  442. }):
  443. await fetch(apiUrl, {
  444. method: 'POST',
  445. headers: { 'Content-Type': 'application/json' },
  446. body: JSON.stringify({
  447. contents: [{
  448. parts: [{
  449. text: `Me: Follow the steps below to execute step by step for each URL.
  450. 1 If the URL cannot be accessed, do not output the results
  451. 2 Summarize in 400 characters or so like an academic
  452. 3 Output only the results, without titles or headings
  453. ${title} with URL: ${url}
  454. You:`
  455. }],
  456. }]
  457. }),
  458. });
  459.  
  460. if (!response.ok) {
  461. if (response.status === 429) {
  462. consecutive429Count++;
  463. if (consecutive429Count >= 3) {
  464. console.warn("Too many requests. Pausing for a while...");
  465. await delay(30000);
  466. consecutive429Count = 0;
  467. return Promise.resolve();
  468. }
  469. } else {
  470. throw new Error('Network response was not ok');
  471. }
  472. } else {
  473. consecutive429Count = 0;
  474. }
  475.  
  476. const reader = response.body.getReader();
  477. let result = '', done = false, decoder = new TextDecoder();
  478. while (!done) {
  479. const { value, done: doneReading } = await reader.read();
  480. done = doneReading;
  481. if (value) result += decoder.decode(value, { stream: true });
  482. }
  483. result += decoder.decode();
  484.  
  485. const data = JSON.parse(result);
  486. if (!data.candidates[0]?.content?.parts[0]?.text) return Promise.resolve();
  487. summary = data.candidates[0].content.parts[0].text.replace(/\*\*/g, '').replace(/##/g, '');
  488.  
  489. if (summary.length >= 180) await GM.setValue(url, summary);
  490. }
  491. console.log(`summary: ${summary}`);
  492.  
  493. let targetElement = article.querySelector('time') || article.querySelector('span') || null;
  494. if (!targetElement || !targetElement.tagName) {
  495. const targetLinks = article.querySelectorAll('a[href*="./read/"]');
  496. const targetLink = targetLinks.length > 1 ? targetLinks[targetLinks.length - 1] : targetLinks[0];
  497. targetElement = document.createElement('span');
  498. targetElement.style.fontSize = '12px';
  499. targetElement.style.fontWeight = '200';
  500. targetElement.style.marginRight = '-90px';
  501. targetLink.parentElement.appendChild(targetElement);
  502. }
  503. if (targetElement.tagName === 'TIME') {
  504. targetElement.style.whiteSpace = 'pre-wrap';
  505. targetElement.style.alignSelf = 'end';
  506. targetElement.style.marginRight = '3px';
  507. targetElement.parentElement.style.height = 'auto';
  508. } else {
  509. targetElement.style.marginRight = '-60px';
  510. targetElement.style.whiteSpace = 'pre-wrap';
  511. }
  512. links.forEach(link => link.setAttribute('href', url));
  513.  
  514. let displayText = targetElement.textContent + ' ';
  515. const author = targetElement.parentElement.querySelector('hr ~ div > span');
  516. if (author) {
  517. const hr = targetElement.parentElement.querySelector('hr');
  518. if (hr) hr.remove();
  519. displayText += '• ' + author.textContent + ' ';
  520. author.remove();
  521. }
  522. for (const char of summary) {
  523. document.querySelector('#gemini-ticker').style.opacity = '1';
  524. displayText += char + '●';
  525. targetElement.textContent = displayText;
  526. await delay(1);
  527. displayText = displayText.slice(0, -1);
  528. document.querySelector('#gemini-ticker').style.opacity = '0';
  529. }
  530. targetElement.textContent = displayText;
  531. } catch (error) {
  532. document.querySelector('#gemini-ticker').style.opacity = '0';
  533. await delay(5000);
  534. console.error('Error:', error);
  535. }
  536. };
  537.  
  538. const throttledProcessArticle = async (article, links, title, url, interval) => {
  539. await delay(interval);
  540. return processArticle(article, links, title, url);
  541. };
  542.  
  543. // ########## Ticker ##########
  544. const insertTickerElement = () => {
  545. if (document.querySelector('#gemini-ticker')) return;
  546. const ticker = document.createElement('div');
  547. ticker.id = 'gemini-ticker';
  548. ticker.style.position = 'fixed';
  549. ticker.style.right = '20px';
  550. ticker.style.bottom = '10px';
  551. ticker.style.fontSize = '1.5em';
  552. ticker.style.color = '#77777777';
  553. ticker.style.transition = 'opacity .3s';
  554. ticker.style.zIndex = '100';
  555. ticker.innerHTML = '✦';
  556. document.querySelector('body').appendChild(ticker);
  557. };
  558. // ########## Settings ##########
  559. const insertSettingsElement = () => {
  560. if (document.querySelector('#gemini-api-settings') || !document.querySelector('a[href*="./settings/"]')) return;
  561. const settingsLink = document.createElement('div');
  562. settingsLink.id = 'gemini-api-settings';
  563. settingsLink.style.height = '64px';
  564. settingsLink.style.alignContent = 'center';
  565. settingsLink.innerHTML = (new URL(location.href).searchParams.get('hl') == 'ja') ?
  566. `<a style="height: 34px; font-size: 14px;">Google News Enhanced: Gemini APIキーの設定</a>`:
  567. `<a style="height: 34px; font-size: 14px;">Google News Enhanced: Setting for Gemini API key</a>`;
  568. document.querySelector('a[href*="./settings/"]').closest('main > div > div > div').appendChild(settingsLink);
  569. settingsLink.querySelector('a').addEventListener('click', async () => {
  570. const GEMINI_API_KEY = window.prompt('Get Generative Language Client API key from Google AI Studio\nhttps://ai.google.dev/aistudio', '');
  571. if (GEMINI_API_KEY != null) await GM.setValue("GEMINI_API_KEY", GEMINI_API_KEY);
  572. }, false);
  573. };
  574.  
  575. // ########## Main ##########
  576. insertHeaderStyle();
  577. insertTickerElement();
  578. await loadContinuous();
  579. for (let j = 0; j < 30 ; j++) {
  580. console.log(`######## attempt: ${j+1} ########`)
  581. insertSettingsElement();
  582. document.querySelector('#gemini-ticker').style.opacity = '1';
  583. const articles = Array.from(document.querySelectorAll('article'));
  584. const allLinks = Array.from(document.querySelectorAll('a[href*="./read/"]'));
  585. if (allLinks.length == 0) break;
  586.  
  587. const promiseArticles = articles.map(async (article, i) => {
  588. const links = Array.from(article.querySelectorAll('a[href*="./read/"]'));
  589. const targetLink = links.length > 1 ? links[links.length - 1] : links[0];
  590. if (!targetLink) return Promise.resolve();
  591. const href = targetLink.getAttribute('href');
  592. const title = targetLink.textContent;
  593. const url = await getExtractedURL(href);
  594. console.log(`title: ${title}`);
  595. console.log(`url: ${url}`);
  596. if (!url) return Promise.resolve();
  597.  
  598. return throttledProcessArticle(article, links, title, url, i * 500);
  599. });
  600.  
  601. await Promise.all(promiseArticles);
  602.  
  603. insertSettingsElement();
  604.  
  605. if (!document.querySelector('#gemini-forecast')) {
  606. await processForecast();
  607. await delay(1000);
  608. }
  609.  
  610. if (!document.querySelector('#gemini-highlight')) {
  611. const urls = articles.map(article => {
  612. const links = Array.from(article.querySelectorAll('a'));
  613. const targetLink = links.length > 1 ? links[links.length - 1] : links[0];
  614. if (!targetLink) return null;
  615. const href = targetLink.getAttribute('href');
  616. const title = targetLink.textContent;
  617. return `${title}: ${href}`;
  618. }).filter(Boolean).join(' ');
  619. console.log(`highlight: ${urls}`)
  620. await processHighlight(urls);
  621. await delay(1000);
  622. }
  623.  
  624. document.querySelector('#gemini-ticker').style.opacity = '0';
  625. await delay(1000);
  626. }
  627. document.querySelector('#gemini-ticker').style.opacity = '0';
  628. console.log('######## Ended up all ########')
  629. })();