Pitchfork Reviews with r/indieheads Comments from Reddit

Load and display Reddit comments from r/indieheads on Pitchfork album review pages.

  1. // ==UserScript==
  2. // @name Pitchfork Reviews with r/indieheads Comments from Reddit
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Load and display Reddit comments from r/indieheads on Pitchfork album review pages.
  6. // @author TA
  7. // @license MIT
  8. // @match https://pitchfork.com/reviews/albums/*
  9. // @grant GM_xmlhttpRequest
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // --- Utility Functions (developed in previous steps) ---
  16.  
  17. /**
  18. * Extracts the Album Name from the Pitchfork page.
  19. * @returns {string|null} The album name or null if not found.
  20. */
  21. function extractAlbumName() {
  22. const albumElement = document.querySelector('h1[data-testid="ContentHeaderHed"]');
  23. return albumElement ? albumElement.textContent.trim() : null;
  24. }
  25.  
  26. /**
  27. * Extracts the Artist Name(s) from the Pitchfork page.
  28. * @returns {string|string[]|null} The artist name(s) or null if not found.
  29. */
  30. function extractArtistName() {
  31. const artistElements = document.querySelectorAll('ul[class*="SplitScreenContentHeaderArtistWrapper"] div[class*="SplitScreenContentHeaderArtist"]');
  32. if (!artistElements.length) {
  33. return null;
  34. }
  35. const artists = Array.from(artistElements).map(el => el.textContent.trim());
  36. // Return a single string if only one artist, array if multiple
  37. return artists.length === 1 ? artists[0] : artists;
  38. }
  39.  
  40. /**
  41. * Formats artist and album names into Reddit search query strings.
  42. * Returns separate queries for FRESH ALBUM and ALBUM DISCUSSION threads.
  43. *
  44. * @param {string|string[]} artistName The name of the artist(s).
  45. * @param {string} albumName The name of the album.
  46. * @returns {Object} Object with freshAlbumQuery and albumDiscussionQuery properties.
  47. */
  48. function formatAlbumSearchQueries(artistName, albumName) {
  49. // If artistName is an array, join with ' & ' for the query
  50. const formattedArtist = Array.isArray(artistName) ? artistName.join(' & ') : artistName;
  51.  
  52. // Create simpler queries that are more likely to match
  53. // Remove quotes and brackets which can cause search issues
  54. const freshAlbumQuery = `FRESH ALBUM ${formattedArtist} ${albumName}`;
  55. const albumDiscussionQuery = `ALBUM DISCUSSION ${formattedArtist} ${albumName}`;
  56.  
  57. // Return both queries separately
  58. return {
  59. freshAlbumQuery,
  60. albumDiscussionQuery
  61. };
  62. }
  63.  
  64. /**
  65. * Constructs a Reddit search URL for the r/indieheads subreddit's JSON API endpoint.
  66. * Cleans the query by removing problematic characters like slashes and ampersands.
  67. *
  68. * @param {string} query The search query string.
  69. * @returns {string} The constructed Reddit search JSON API URL.
  70. */
  71. function buildIndieHeadsSearchJsonUrl(query) {
  72. // Clean the query by removing slashes, ampersands, percent signs, and plus signs with spaces
  73. // that might interfere with the search functionality
  74. const cleanedQuery = query
  75. .replace(/[\/&%+]/g, ' ') // Replace slashes, ampersands, percent signs, and plus signs with spaces
  76. .replace(/\s+/g, ' ') // Replace multiple spaces with a single space
  77. .trim(); // Remove leading/trailing spaces
  78.  
  79. const encodedQuery = encodeURIComponent(cleanedQuery);
  80. const searchUrl = `https://www.reddit.com/r/indieheads/search.json?q=${encodedQuery}&restrict_sr=on&sort=relevance&t=all`;
  81. return searchUrl;
  82. }
  83.  
  84. /**
  85. * Identifies relevant Reddit thread URLs from search results based on title patterns.
  86. * Processes FRESH ALBUM and ALBUM DISCUSSION results separately.
  87. * Ensures no duplicate threads are added.
  88. *
  89. * @param {Array<Object>} freshAlbumResults The results from the FRESH ALBUM search.
  90. * @param {Array<Object>} albumDiscussionResults The results from the ALBUM DISCUSSION search.
  91. * @param {string} artistName The name of the artist(s).
  92. * @param {string} albumName The name of the album.
  93. * @returns {Array<Object>} An array of objects {title: string, url: string} for all matching threads.
  94. */
  95. function identifyRelevantThreads(freshAlbumResults, albumDiscussionResults, artist, albumName) {
  96. const relevantThreads = [];
  97. // Track URLs to avoid duplicates
  98. const addedUrls = new Set();
  99.  
  100. // Helper function to find the best thread from search results
  101. const findBestThread = (results, threadType) => {
  102. if (!results || !Array.isArray(results) || results.length === 0) {
  103. console.log(`No ${threadType} search results found.`);
  104. return null;
  105. }
  106.  
  107. console.log(`Processing ${results.length} ${threadType} search results.`);
  108.  
  109. // Look for an exact match first
  110. for (const item of results) {
  111. if (item.kind === "t3" && item.data && item.data.title && item.data.permalink) {
  112. const title = item.data.title;
  113. const url = "https://www.reddit.com" + item.data.permalink;
  114.  
  115. // Skip if we've already added this URL
  116. if (addedUrls.has(url)) {
  117. console.log(`Skipping duplicate thread: "${title}"`);
  118. continue;
  119. }
  120.  
  121. // Check if this is the right type of thread
  122. if (title.toLowerCase().includes(threadType.toLowerCase()) &&
  123. title.toLowerCase().includes(albumName.toLowerCase())) {
  124. console.log(`Found ${threadType} thread: "${title}"`);
  125. return { title, url };
  126. }
  127. }
  128. }
  129.  
  130. // If no exact match, take the first result that contains the album name
  131. for (const item of results) {
  132. if (item.kind === "t3" && item.data && item.data.title && item.data.permalink) {
  133. const title = item.data.title;
  134. const url = "https://www.reddit.com" + item.data.permalink;
  135.  
  136. // Skip if we've already added this URL
  137. if (addedUrls.has(url)) {
  138. console.log(`Skipping duplicate thread: "${title}"`);
  139. continue;
  140. }
  141.  
  142. if (title.toLowerCase().includes(albumName.toLowerCase())) {
  143. console.log(`Found ${threadType} thread (partial match): "${title}"`);
  144. return { title, url };
  145. }
  146. }
  147. }
  148.  
  149. console.log(`No matching ${threadType} thread found.`);
  150. return null;
  151. };
  152.  
  153. // Find the best thread for each type
  154. const freshAlbumThread = findBestThread(freshAlbumResults, "FRESH ALBUM");
  155.  
  156. // Add FRESH ALBUM thread if found
  157. if (freshAlbumThread) {
  158. relevantThreads.push(freshAlbumThread);
  159. addedUrls.add(freshAlbumThread.url); // Track the URL to avoid duplicates
  160. console.log(`Added FRESH ALBUM thread: "${freshAlbumThread.title}"`);
  161. }
  162.  
  163. // Find ALBUM DISCUSSION thread
  164. const albumDiscussionThread = findBestThread(albumDiscussionResults, "ALBUM DISCUSSION");
  165.  
  166. // Add ALBUM DISCUSSION thread if found and not a duplicate
  167. if (albumDiscussionThread && !addedUrls.has(albumDiscussionThread.url)) {
  168. relevantThreads.push(albumDiscussionThread);
  169. addedUrls.add(albumDiscussionThread.url);
  170. console.log(`Added ALBUM DISCUSSION thread: "${albumDiscussionThread.title}"`);
  171. } else if (albumDiscussionThread) {
  172. console.log(`Skipping duplicate ALBUM DISCUSSION thread: "${albumDiscussionThread.title}"`);
  173. }
  174.  
  175. console.log(`Found ${relevantThreads.length} unique relevant threads`);
  176. return relevantThreads;
  177. }
  178.  
  179. /**
  180. * Fetches comments from a given Reddit thread URL using the .json endpoint.
  181. * Note: This uses GM_xmlhttpRequest for cross-origin requests in Userscripts.
  182. *
  183. * @param {string} threadUrl The URL of the Reddit thread.
  184. * @returns {Promise<Array<Object>|null>} A promise that resolves with an array of comment data or null on error.
  185. */
  186. function fetchRedditComments(threadUrl) {
  187. console.log(`[fetchRedditComments] Attempting to fetch comments for: ${threadUrl}`);
  188. return new Promise((resolve, reject) => {
  189. // Append .json to the thread URL to get the JSON data
  190. const jsonUrl = threadUrl.endsWith('.json') ? threadUrl : threadUrl + '.json';
  191.  
  192. console.log(`[fetchRedditComments] Requesting URL: ${jsonUrl}`);
  193.  
  194. // Use GM_xmlhttpRequest for cross-origin requests
  195. GM_xmlhttpRequest({
  196. method: "GET",
  197. url: jsonUrl,
  198. onload: function(response) {
  199. console.log(`[fetchRedditComments] Received response for ${jsonUrl}. Status: ${response.status}`);
  200. try {
  201. if (response.status === 200) {
  202. console.log(`[fetchRedditComments] Response Text for ${jsonUrl}: ${response.responseText.substring(0, 500)}...`); // Log beginning of response
  203. const data = JSON.parse(response.responseText);
  204. console.log("[fetchRedditComments] Successfully parsed JSON response.");
  205. // The JSON response for a thread includes two arrays: [submission, comments]
  206. // We need the comments array (index 1)
  207. if (data && data.length === 2 && data[1] && data[1].data && data[1].data.children) {
  208. console.log(`[fetchRedditComments] Found comment data. Number of top-level items: ${data[1].data.children.length}`);
  209. // Process the raw comment data to extract relevant info and handle replies
  210. const comments = processComments(data[1].data.children);
  211. console.log(`[fetchRedditComments] Processed comments. Total processed: ${comments.length}`);
  212. resolve(comments);
  213. } else {
  214. console.error("[fetchRedditComments] Unexpected Reddit JSON structure:", data);
  215. resolve(null); // Resolve with null for unexpected structure
  216. }
  217. } else {
  218. console.error("[fetchRedditComments] Error fetching Reddit comments:", response.status, response.statusText);
  219. resolve(null); // Resolve with null on HTTP error
  220. }
  221. } catch (e) {
  222. console.error("[fetchRedditComments] Error parsing Reddit comments JSON:", e);
  223. resolve(null); // Resolve with null on parsing error
  224. }
  225. },
  226. onerror: function(error) {
  227. console.error("[fetchRedditComments] GM_xmlhttpRequest error fetching Reddit comments:", error);
  228. resolve(null); // Resolve with null on request error
  229. }
  230. });
  231. });
  232. }
  233.  
  234. /**
  235. * Recursively processes raw Reddit comment data to extract relevant info and handle replies.
  236. * Filters out 'more' comments placeholders.
  237. *
  238. * @param {Array<Object>} rawComments The raw comment children array from Reddit API.
  239. * @returns {Array<Object>} An array of processed comment objects.
  240. */
  241. function processComments(rawComments) {
  242. const processed = [];
  243. if (!rawComments || !Array.isArray(rawComments)) {
  244. return processed;
  245. }
  246.  
  247. for (const item of rawComments) {
  248. // Skip 'more' comments placeholders
  249. if (item.kind === 'more') {
  250. continue;
  251. }
  252.  
  253. // Ensure it's a comment and has the necessary data
  254. if (item.kind === 't1' && item.data) {
  255. const commentData = item.data;
  256. const processedComment = {
  257. author: commentData.author,
  258. text: commentData.body,
  259. score: commentData.score,
  260. created_utc: commentData.created_utc,
  261. replies: [] // Initialize replies array
  262. };
  263.  
  264. // Recursively process replies if they exist
  265. if (commentData.replies && commentData.replies.data && commentData.replies.data.children) {
  266. processedComment.replies = processComments(commentData.replies.data.children);
  267. }
  268.  
  269. processed.push(processedComment);
  270. }
  271. }
  272. return processed;
  273. }
  274.  
  275. // --- HTML Structures and Injection ---
  276.  
  277. const REDDIT_COMMENTS_SECTION_HTML = `
  278. <div class="reddit-comments-section">
  279. <h3>Reddit Comments from r/indieheads</h3>
  280. <div class="reddit-comments-tabs">
  281. <!-- Tab buttons will be added here -->
  282. </div>
  283. <div class="reddit-comments-content">
  284. <!-- Comment content areas will be added here -->
  285. <!-- Each area will have a data-thread-id or similar to link to the tab -->
  286. </div>
  287. </div>
  288. `;
  289.  
  290. /**
  291. * Injects HTML content after the last paragraph in the article.
  292. * @param {string|HTMLElement} content The HTML string or HTMLElement to inject.
  293. */
  294. function injectAfterLastParagraph(content) {
  295. // Find the article element
  296. const article = document.querySelector('article');
  297. if (!article) {
  298. console.error('Article element not found for injection');
  299. return;
  300. }
  301.  
  302. // Find all paragraphs within the article, excluding those with class "disclaimer"
  303. const paragraphs = Array.from(article.querySelectorAll('p')).filter(p =>
  304. !p.classList.contains('disclaimer') &&
  305. !p.closest('.disclaimer') // Also exclude paragraphs inside elements with class "disclaimer"
  306. );
  307.  
  308. if (paragraphs.length === 0) {
  309. console.error('No valid paragraphs found in article for injection');
  310. return;
  311. }
  312.  
  313. // Get the last paragraph
  314. const lastParagraph = paragraphs[paragraphs.length - 1];
  315.  
  316. // Insert content after the last paragraph
  317. if (typeof content === 'string') {
  318. lastParagraph.insertAdjacentHTML('afterend', content);
  319. } else {
  320. lastParagraph.insertAdjacentElement('afterend', content);
  321. }
  322. }
  323.  
  324. // Function to render comments to HTML (basic structure)
  325. function renderCommentsHtml(comments, level = 0) {
  326. let html = `<ul class="reddit-comment-list level-${level}">`;
  327. if (!comments || comments.length === 0) {
  328. html += '<li>No comments found for this thread.</li>';
  329. } else {
  330. // Filter out deleted comments
  331. const validComments = comments.filter(comment =>
  332. comment.author !== "[deleted]" && comment.text !== "[deleted]"
  333. );
  334.  
  335. if (validComments.length === 0) {
  336. html += '<li>No valid comments found for this thread.</li>';
  337. } else {
  338. validComments.forEach(comment => {
  339. html += `<li class="reddit-comment">`;
  340. // Add collapse button for top-level comments
  341. if (level === 0) {
  342. html += `<div class="comment-meta">
  343. <b>${comment.author}</b> (${comment.score} points)
  344. <button class="comment-collapse-button">[−]</button>
  345. </div>`;
  346. } else {
  347. html += `<div class="comment-meta"><b>${comment.author}</b> (${comment.score} points)</div>`;
  348. }
  349. // Process comment text for special content
  350. let processedText = comment.text;
  351. // Process Giphy embeds first
  352. processedText = processedText.replace(/!\[gif\]\(giphy\|([a-zA-Z0-9]+)(?:\|downsized)?\)/g, (match, giphyId) => {
  353. return `
  354. <div class="giphy-embed-container">
  355. <iframe src="https://giphy.com/embed/${giphyId}"
  356. width="480" height="270" frameBorder="0"
  357. class="giphy-embed" allowFullScreen></iframe>
  358. </div>
  359. `;
  360. });
  361. // Process Reddit image links
  362. processedText = processedText.replace(/(https:\/\/preview\.redd\.it\/[a-zA-Z0-9]+\.(jpeg|jpg|png|gif)\?[^\s)]+)/g, (match, imageUrl) => {
  363. return `
  364. <div class="reddit-image-container">
  365. <img src="${imageUrl}" alt="Reddit Image" class="reddit-inline-image" />
  366. </div>
  367. `;
  368. });
  369. // Process Markdown image syntax for Reddit images
  370. processedText = processedText.replace(/!\[.*?\]\((https:\/\/preview\.redd\.it\/[a-zA-Z0-9]+\.(jpeg|jpg|png|gif)\?[^\s)]+)\)/g, (match, imageUrl) => {
  371. return `
  372. <div class="reddit-image-container">
  373. <img src="${imageUrl}" alt="Reddit Image" class="reddit-inline-image" />
  374. </div>
  375. `;
  376. });
  377. // Process basic Markdown formatting
  378. // Bold text
  379. processedText = processedText.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  380. // Italic text
  381. processedText = processedText.replace(/\*([^*]+)\*/g, '<em>$1</em>');
  382. // Block quotes - simple implementation
  383. processedText = processedText.replace(/^(>|>)\s*(.*?)$/gm, '<blockquote>$2</blockquote>');
  384. // Parse Markdown links
  385. processedText = processedText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
  386. return `<a href="${url}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
  387. });
  388. // Parse plain URLs
  389. processedText = processedText.replace(/(?<!["\'])(https?:\/\/[^\s<>[\]()'"]+)(?![^<]*>)/g, (match, url) => {
  390. return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
  391. });
  392. // Handle line breaks - simple approach
  393. processedText = processedText.replace(/\n\n+/g, '</p><p>');
  394. processedText = processedText.replace(/\n/g, '<br>');
  395. // Wrap in paragraph tags if not already
  396. if (!processedText.startsWith('<p>') &&
  397. !processedText.startsWith('<div') &&
  398. !processedText.startsWith('<blockquote') &&
  399. !processedText.includes('<div class="giphy-embed-container">') &&
  400. !processedText.includes('<div class="reddit-image-container">')) {
  401. processedText = `<p>${processedText}</p>`;
  402. }
  403. html += `<div class="comment-body">${processedText}</div>`;
  404. if (comment.replies && comment.replies.length > 0) {
  405. html += renderCommentsHtml(comment.replies, level + 1);
  406. }
  407. html += `</li>`;
  408. });
  409. }
  410. }
  411. html += `</ul>`;
  412. return html;
  413. }
  414.  
  415. function setupCommentCollapse() {
  416. document.querySelectorAll('.comment-collapse-button').forEach(button => {
  417. button.addEventListener('click', function() {
  418. const commentLi = this.closest('.reddit-comment');
  419. commentLi.classList.toggle('collapsed');
  420. // Update button text
  421. if (commentLi.classList.contains('collapsed')) {
  422. this.textContent = '[+]';
  423. } else {
  424. this.textContent = '[−]';
  425. }
  426. });
  427. });
  428. }
  429.  
  430. // --- CSS Styles ---
  431. function injectStyles() {
  432. const styles = `
  433. @media (min-width: 2400px) {
  434. #main-content div[class^="GridWrapper"] {
  435. max-width: 2000px;
  436. }
  437. }
  438. .reddit-comments-section {
  439. margin-top: 30px;
  440. padding: 20px;
  441. border-top: 1px solid #ddd;
  442. font-family: inherit;
  443. }
  444. .reddit-comments-tabs {
  445. display: flex;
  446. flex-wrap: wrap;
  447. margin-bottom: 15px;
  448. }
  449. .reddit-tab-button {
  450. padding: 8px 12px;
  451. margin-right: 5px;
  452. margin-bottom: 5px;
  453. background: #f0f0f0;
  454. border: 1px solid #ccc;
  455. border-radius: 4px;
  456. cursor: pointer;
  457. }
  458. .reddit-tab-button:not(.active):hover {
  459. background: #f8f8f8;
  460. }
  461. .reddit-tab-button:hover, .reddit-tab-button:active, .reddit-tab-button:focus {
  462. text-decoration: none;
  463. }
  464. .reddit-tab-button.active {
  465. background: #e0e0e0;
  466. border-color: #aaa;
  467. font-weight: bold;
  468. }
  469. .reddit-comment-list {
  470. list-style-type: none;
  471. padding-left: 0;
  472. }
  473. .reddit-comment-list.level-0 {
  474. padding-left: 0;
  475. margin-left: 0;
  476. }
  477. .reddit-comment-list.level-1 {
  478. padding-left: 20px;
  479. border-left: 2px solid #eee;
  480. }
  481. .reddit-comment-list.level-2,
  482. .reddit-comment-list.level-3,
  483. .reddit-comment-list.level-4,
  484. .reddit-comment-list.level-5 {
  485. padding-left: 20px;
  486. border-left: 2px solid #f5f5f5;
  487. }
  488. .reddit-comment {
  489. margin-bottom: 15px;
  490. }
  491. .reddit-image-container {
  492. margin-top: 10px;
  493. }
  494. .comment-meta {
  495. font-size: .9em;
  496. margin-bottom: 5px;
  497. color: #666;
  498. }
  499. .comment-body {
  500. line-height: 1.5;
  501. }
  502. /* Markdown formatting styles */
  503. .comment-body strong {
  504. font-weight: 700;
  505. }
  506. .comment-body em {
  507. font-style: italic;
  508. }
  509. .comment-body blockquote {
  510. border-left: 3px solid #c5c1ad;
  511. margin: 8px 0;
  512. padding: 0 8px 0 12px;
  513. color: #646464;
  514. background-color: #f8f9fa;
  515. }
  516. /* Paragraph styling */
  517. .comment-body p {
  518. margin: .8em 0;
  519. }
  520.  
  521. .comment-body p:first-child {
  522. margin-top: 0;
  523. }
  524. .comment-body p:last-child {
  525. margin-bottom: 0;
  526. }
  527. .comment-body blockquote p {
  528. margin: .4em 0;
  529. }
  530. .reddit-comment.collapsed .comment-body,
  531. .reddit-comment.collapsed .reddit-comment-list {
  532. display: none;
  533. }
  534. .reddit-comment.collapsed {
  535. opacity: 0.7;
  536. }
  537. .comment-collapse-button {
  538. background: none;
  539. border: none;
  540. color: #0079d3;
  541. cursor: pointer;
  542. font-size: 12px;
  543. margin-left: 5px;
  544. padding: 0;
  545. }
  546. .comment-collapse-button:hover {
  547. text-decoration: underline;
  548. }
  549. `;
  550.  
  551. const styleElement = document.createElement('style');
  552. styleElement.textContent = styles;
  553. document.head.appendChild(styleElement);
  554. }
  555.  
  556.  
  557. // --- Main Execution Logic ---
  558.  
  559. async function init() {
  560. console.log("Pitchfork Reddit Comments Userscript started.");
  561.  
  562. // Inject CSS styles
  563. injectStyles();
  564.  
  565. const artist = extractArtistName();
  566. const album = extractAlbumName();
  567.  
  568. if (!artist || !album) {
  569. console.log("Could not extract artist or album name. Exiting.");
  570. return;
  571. }
  572.  
  573. console.log(`Found Artist: ${artist}, Album: ${album}`);
  574.  
  575. const queries = formatAlbumSearchQueries(artist, album);
  576. console.log(`Search queries:`, queries);
  577.  
  578. // Make separate search requests for each query type
  579. const freshAlbumUrl = buildIndieHeadsSearchJsonUrl(queries.freshAlbumQuery);
  580. const albumDiscussionUrl = buildIndieHeadsSearchJsonUrl(queries.albumDiscussionQuery);
  581.  
  582. console.log(`Fresh Album Search URL: ${freshAlbumUrl}`);
  583. console.log(`Album Discussion Search URL: ${albumDiscussionUrl}`);
  584.  
  585. // Function to perform a search request
  586. const performSearch = (url) => {
  587. return new Promise((resolve, reject) => {
  588. GM_xmlhttpRequest({
  589. method: "GET",
  590. url: url,
  591. onload: function(response) {
  592. try {
  593. console.log(`[Search Request] Received response. Status: ${response.status}`);
  594. if (response.status === 200) {
  595. const searchData = JSON.parse(response.responseText);
  596. console.log("[Search Request] Successfully parsed JSON response.");
  597. if (searchData && searchData.data && searchData.data.children) {
  598. resolve(searchData.data.children);
  599. } else {
  600. console.error("[Search Request] Unexpected Reddit search JSON structure:", searchData);
  601. resolve([]);
  602. }
  603. } else {
  604. console.error("[Search Request] Error fetching Reddit search results:", response.status, response.statusText);
  605. resolve([]);
  606. }
  607. } catch (e) {
  608. console.error("[Search Request] Error parsing Reddit search JSON:", e);
  609. resolve([]);
  610. }
  611. },
  612. onerror: function(error) {
  613. console.error("[Search Request] GM_xmlhttpRequest error fetching Reddit search results:", error);
  614. resolve([]);
  615. }
  616. });
  617. });
  618. };
  619.  
  620. try {
  621. // Perform both searches in parallel
  622. const [freshAlbumResults, albumDiscussionResults] = await Promise.all([
  623. performSearch(freshAlbumUrl),
  624. performSearch(albumDiscussionUrl)
  625. ]);
  626.  
  627. // Identify relevant threads from both result sets
  628. const relevantThreads = identifyRelevantThreads(
  629. freshAlbumResults,
  630. albumDiscussionResults,
  631. typeof artist === 'string' ? artist : artist.join(' & '),
  632. album
  633. );
  634.  
  635. if (relevantThreads.length === 0) {
  636. console.log("No relevant Reddit threads found.");
  637. const noThreadsMessage = document.createElement('p');
  638. noThreadsMessage.textContent = 'No relevant Reddit threads found for this review.';
  639. noThreadsMessage.style.fontStyle = 'italic';
  640. noThreadsMessage.style.marginTop = '20px'; // Add some spacing
  641. injectAfterLastParagraph(noThreadsMessage);
  642. return;
  643. }
  644.  
  645. console.log(`Found ${relevantThreads.length} relevant thread(s):`, relevantThreads);
  646.  
  647. // Inject the main comments section container
  648. injectAfterLastParagraph(REDDIT_COMMENTS_SECTION_HTML);
  649.  
  650. const commentsSection = document.querySelector('.reddit-comments-section');
  651. const tabsArea = commentsSection.querySelector('.reddit-comments-tabs');
  652. const contentArea = commentsSection.querySelector('.reddit-comments-content');
  653.  
  654. // Fetch comments and build tabs/content
  655. for (let i = 0; i < relevantThreads.length; i++) {
  656. const thread = relevantThreads[i];
  657. console.log(`Fetching comments for thread: ${thread.title} (${thread.url})`);
  658. const comments = await fetchRedditComments(thread.url);
  659.  
  660. // Generate tab button
  661. const tabButton = document.createElement('button');
  662. tabButton.classList.add('reddit-tab-button');
  663. tabButton.textContent = thread.title + ' ';
  664. tabButton.setAttribute('data-thread-index', i);
  665.  
  666. // Add a direct link icon that opens the Reddit thread in a new tab
  667. const linkIcon = document.createElement('a');
  668. linkIcon.href = thread.url;
  669. linkIcon.target = '_blank';
  670. linkIcon.rel = 'noopener noreferrer'; // Security best practice for target="_blank"
  671. linkIcon.innerHTML = '🔗';
  672. linkIcon.title = 'Open Reddit thread in new tab';
  673. linkIcon.style.fontSize = '0.8em';
  674. linkIcon.style.opacity = '0.7';
  675. linkIcon.style.textDecoration = 'none'; // Remove underline
  676. linkIcon.style.marginLeft = '5px';
  677.  
  678. tabButton.appendChild(linkIcon);
  679. tabsArea.appendChild(tabButton);
  680.  
  681. // Generate comment content area
  682. const threadContent = document.createElement('div');
  683. threadContent.classList.add('reddit-tab-content');
  684. threadContent.setAttribute('data-thread-index', i);
  685. threadContent.style.display = 'none'; // Hide by default
  686.  
  687. if (comments) {
  688. threadContent.innerHTML = renderCommentsHtml(comments);
  689. // Set up collapse functionality for this tab's comments
  690. setupCommentCollapse();
  691. } else {
  692. threadContent.innerHTML = '<p>Could not load comments for this thread.</p>';
  693. }
  694. contentArea.appendChild(threadContent);
  695.  
  696. // Activate the first tab and content by default
  697. if (i === 0) {
  698. tabButton.classList.add('active');
  699. threadContent.style.display = 'block';
  700. }
  701. }
  702.  
  703. // Add event listeners for tab switching
  704. const tabButtons = tabsArea.querySelectorAll('.reddit-tab-button');
  705. const tabContents = contentArea.querySelectorAll('.reddit-tab-content');
  706.  
  707. tabButtons.forEach(button => {
  708. button.addEventListener('click', () => {
  709. const threadIndex = button.getAttribute('data-thread-index');
  710.  
  711. // Deactivate all tabs and hide all content
  712. tabButtons.forEach(btn => btn.classList.remove('active'));
  713. tabContents.forEach(content => content.style.display = 'none');
  714.  
  715. // Activate the clicked tab and show corresponding content
  716. button.classList.add('active');
  717. const activeContent = document.querySelector(`.reddit-tab-content[data-thread-index="${threadIndex}"]`);
  718. activeContent.style.display = 'block';
  719.  
  720. // Re-initialize collapse functionality for the newly displayed tab
  721. setupCommentCollapse();
  722. });
  723. });
  724.  
  725. } catch (error) {
  726. console.error("Error during search process:", error);
  727. }
  728. }
  729.  
  730. // Run the initialization function
  731. init();
  732.  
  733. })();