Pitchfork Reddit Comments

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

Tính đến 08-11-2025. Xem phiên bản mới nhất.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Pitchfork Reddit Comments
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Load and display Reddit comments from r/indieheads on Pitchfork album review pages.
// @author       TA
// @license      MIT
// @match        https://pitchfork.com/reviews/albums/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// TODO: not working: https://pitchfork.com/reviews/albums/jennifer-walton-daughters/
// Needs to take H1 album title for the match

(function() {
    'use strict';

    // --- Utility Functions (developed in previous steps) ---

    /**
     * Extracts the Album Name from the Pitchfork page.
     * @returns {string|null} The album name or null if not found.
     */
    function extractAlbumName() {
        let albumElement = document.querySelector('h1[data-testid="ContentHeaderHed"]');
        console.log(`Album element found: ${albumElement ? 'YES' : 'NO'}`);
        
        // Fallback selector if the main one doesn't work
        if (!albumElement) {
            albumElement = document.querySelector('h1[class*="SplitScreenContentHeaderHed"]');
            console.log(`Fallback album element found: ${albumElement ? 'YES' : 'NO'}`);
        }
        
        // Another fallback - any h1 with em tag
        if (!albumElement) {
            albumElement = document.querySelector('h1 em');
            if (albumElement) {
                albumElement = albumElement.closest('h1');
                console.log(`Second fallback album element found: ${albumElement ? 'YES' : 'NO'}`);
            }
        }
        
        if (!albumElement) {
            return null;
        }
        
        console.log(`Album element HTML: ${albumElement.innerHTML}`);
        
        // Check if album name is wrapped in <em> tags (common for album titles)
        const emElement = albumElement.querySelector('em');
        if (emElement) {
            const albumName = emElement.textContent.trim();
            console.log(`Album name from <em> tag: "${albumName}"`);
            return albumName;
        }
        
        // Fallback to regular text content if no <em> tag found
        const albumName = albumElement.textContent.trim();
        console.log(`Album name from textContent: "${albumName}"`);
        return albumName;
    }

    /**
     * Extracts the Artist Name(s) from the Pitchfork page.
     * @returns {string|string[]|null} The artist name(s) or null if not found.
     */
    function extractArtistName() {
        // Try multiple selector approaches to handle different class name patterns
        let artistElements = document.querySelectorAll('ul[class*="SplitScreenContentHeaderArtistWrapper"] div[class*="SplitScreenContentHeaderArtist"]');
        
        // Fallback selector if the first one doesn't work
        if (!artistElements.length) {
            artistElements = document.querySelectorAll('ul[class*="ArtistWrapper"] div[class*="Artist"]');
        }
        
        // Another fallback - look for any div inside ul that contains artist link
        if (!artistElements.length) {
            artistElements = document.querySelectorAll('ul a[href*="/artists/"] div');
        }
        
        console.log(`Artist elements found: ${artistElements.length}`);
        if (!artistElements.length) {
            return null;
        }
        const artists = Array.from(artistElements).map(el => {
            const artistName = el.textContent.trim();
            console.log(`Found artist element: "${artistName}"`);
            return artistName;
        });
        console.log(`All artists found: ${JSON.stringify(artists)}`);
        // Return a single string if only one artist, array if multiple
        const result = artists.length === 1 ? artists[0] : artists;
        console.log(`Final artist result: ${JSON.stringify(result)}`);
        return result;
    }

    /**
     * Formats artist and album names into Reddit search query strings.
     * Returns separate queries for FRESH ALBUM and ALBUM DISCUSSION threads.
     *
     * @param {string|string[]} artistName The name of the artist(s).
     * @param {string} albumName The name of the album.
     * @returns {Object} Object with freshAlbumQuery and albumDiscussionQuery properties.
     */
    function formatAlbumSearchQueries(artistName, albumName) {
        // If artistName is an array, join with ' & ' for the query
        const formattedArtist = Array.isArray(artistName) ? artistName.join(' & ') : artistName;

        // Create simpler queries that are more likely to match
        // Remove quotes and brackets which can cause search issues
        const freshAlbumQuery = `FRESH ALBUM ${formattedArtist} ${albumName}`;
        const albumDiscussionQuery = `ALBUM DISCUSSION ${formattedArtist} ${albumName}`;

        // Return both queries separately
        return {
            freshAlbumQuery,
            albumDiscussionQuery
        };
    }

    /**
     * Constructs a Reddit search URL for the r/indieheads subreddit's JSON API endpoint.
     * Cleans the query by removing problematic characters like slashes and ampersands.
     *
     * @param {string} query The search query string.
     * @returns {string} The constructed Reddit search JSON API URL.
     */
    function buildIndieHeadsSearchJsonUrl(query) {
        // Clean the query by removing slashes, ampersands, percent signs, and plus signs with spaces
        // that might interfere with the search functionality
        const cleanedQuery = query
            .replace(/[\/&%+]/g, ' ') // Replace slashes, ampersands, percent signs, and plus signs with spaces
            .replace(/\s+/g, ' ')     // Replace multiple spaces with a single space
            .trim();                  // Remove leading/trailing spaces

        const encodedQuery = encodeURIComponent(cleanedQuery);
        const searchUrl = `https://www.reddit.com/r/indieheads/search.json?q=${encodedQuery}&restrict_sr=on&sort=relevance&t=all`;
        return searchUrl;
    }

    /**
     * Identifies relevant Reddit thread URLs from search results based on title patterns.
     * Processes FRESH ALBUM and ALBUM DISCUSSION results separately.
     * Ensures no duplicate threads are added.
     *
     * @param {Array<Object>} freshAlbumResults The results from the FRESH ALBUM search.
     * @param {Array<Object>} albumDiscussionResults The results from the ALBUM DISCUSSION search.
     * @param {string} artistName The name of the artist(s).
     * @param {string} albumName The name of the album.
     * @returns {Array<Object>} An array of objects {title: string, url: string} for all matching threads.
     */
    function identifyRelevantThreads(freshAlbumResults, albumDiscussionResults, artist, albumName) {
        const relevantThreads = [];
        // Track URLs to avoid duplicates
        const addedUrls = new Set();
        
        // Normalize artist name for comparison (handle both string and array)
        const normalizedArtist = Array.isArray(artist) ? artist.join(' & ') : artist;
        const isSelfTitled = normalizedArtist.toLowerCase() === albumName.toLowerCase();
        
        // Fuzzy matching function for artist names to handle spelling variations
        function fuzzyMatchArtist(title, artistName) {
            // Exact match
            if (title.includes(artistName)) {
                return true;
            }
            
            // Handle common variations and spelling differences
            const artistLower = artistName.toLowerCase();
            const titleLower = title.toLowerCase();
            
            // Remove common suffixes/prefixes for comparison
            const cleanArtist = artistLower.replace(/^(the |a |an )/i, '');
            const cleanTitle = titleLower.replace(/^(the |a |an )/i, '');
            
            // Check if artist name is contained within title or vice versa
            if (cleanTitle.includes(cleanArtist) || cleanArtist.includes(cleanTitle)) {
                return true;
            }
            
            // Enhanced fuzzy matching with multiple strategies
            
            // Strategy 1: Handle character substitutions and common misspellings
            const substitutions = {
                'c': ['ch', 'k'],
                'k': ['c', 'ch'],
                's': ['z', 'x'],
                'z': ['s'],
                'x': ['s', 'z']
            };
            
            // Try character substitutions
            for (const [char, replacements] of Object.entries(substitutions)) {
                for (const replacement of replacements) {
                    const substitutedArtist = artistLower.replace(new RegExp(char, 'g'), replacement);
                    if (cleanTitle.includes(substitutedArtist)) {
                        console.log(`Fuzzy matched "${artistName}" to "${substitutedArtist}" in title: "${title}"`);
                        return true;
                    }
                }
            }
            
            // Strategy 2: Handle missing letters (e.g., "Daghters" → "Daughters")
            function matchWithMissingLetters(text, pattern) {
                // Split both into words for word-by-word comparison
                const textWords = text.split(/\s+/);
                const patternWords = pattern.split(/\s+/);
                
                // If different number of words, try matching the whole strings
                if (textWords.length !== patternWords.length) {
                    return matchSingleWordMissingLetters(text, pattern);
                }
                
                // Match each word pair
                for (let i = 0; i < textWords.length; i++) {
                    if (!matchSingleWordMissingLetters(textWords[i], patternWords[i])) {
                        return false;
                    }
                }
                return true;
            }
            
            function matchSingleWordMissingLetters(text, pattern) {
                // Allow up to 2 missing letters per word
                const maxMissing = Math.min(2, Math.floor(pattern.length * 0.3));
                
                let textIndex = 0;
                let patternIndex = 0;
                let missingCount = 0;
                
                while (textIndex < text.length && patternIndex < pattern.length) {
                    if (text[textIndex] === pattern[patternIndex]) {
                        textIndex++;
                        patternIndex++;
                    } else {
                        // Try skipping a character in the pattern (missing letter)
                        if (missingCount < maxMissing && patternIndex + 1 < pattern.length &&
                            text[textIndex] === pattern[patternIndex + 1]) {
                            patternIndex += 2; // Skip the missing letter in pattern
                            textIndex++;
                            missingCount++;
                        } else {
                            return false;
                        }
                    }
                }
                
                // Check if we've matched most of both strings
                const remainingPattern = pattern.length - patternIndex;
                return remainingPattern <= maxMissing && textIndex === text.length;
            }
            
            // Strategy 3: Handle double letter variations (e.g., "begining" → "beginning")
            function matchWithDoubleLetters(text, pattern) {
                // Split both into words for word-by-word comparison
                const textWords = text.split(/\s+/);
                const patternWords = pattern.split(/\s+/);
                
                // If different number of words, try matching the whole strings
                if (textWords.length !== patternWords.length) {
                    return matchSingleWordDoubleLetters(text, pattern);
                }
                
                // Match each word pair
                for (let i = 0; i < textWords.length; i++) {
                    if (!matchSingleWordDoubleLetters(textWords[i], patternWords[i])) {
                        return false;
                    }
                }
                return true;
            }
            
            function matchSingleWordDoubleLetters(text, pattern) {
                // Normalize double letters: convert consecutive identical letters to single
                function normalizeDoubleLetters(str) {
                    return str.replace(/(.)\1+/g, '$1');
                }
                
                const normalizedText = normalizeDoubleLetters(text);
                const normalizedPattern = normalizeDoubleLetters(pattern);
                
                // Check if normalized versions match
                if (normalizedText === normalizedPattern) {
                    return true;
                }
                
                // Also try with one version having double letters and other single
                // This handles cases like "begining" vs "beginning"
                const textWithPotentialDoubles = text.replace(/([bcdfghjklmnpqrstvwxyz])/g, '$1$1');
                const patternWithPotentialDoubles = pattern.replace(/([bcdfghjklmnpqrstvwxyz])/g, '$1$1');
                
                return normalizedText === normalizeDoubleLetters(patternWithPotentialDoubles) ||
                       normalizeDoubleLetters(textWithPotentialDoubles) === normalizedPattern;
            }
            
            // Apply enhanced fuzzy matching strategies
            // Check if title contains artist with missing letters
            if (matchWithMissingLetters(cleanTitle, cleanArtist)) {
                console.log(`Fuzzy matched "${artistName}" with missing letters in title: "${title}"`);
                return true;
            }
            
            // Check if title contains artist with double letter variations
            if (matchWithDoubleLetters(cleanTitle, cleanArtist)) {
                console.log(`Fuzzy matched "${artistName}" with double letter variations in title: "${title}"`);
                return true;
            }
            
            // Try the reverse: check if artist contains title variations
            if (matchWithMissingLetters(cleanArtist, cleanTitle)) {
                console.log(`Fuzzy matched "${artistName}" (reverse missing letters) in title: "${title}"`);
                return true;
            }
            
            if (matchWithDoubleLetters(cleanArtist, cleanTitle)) {
                console.log(`Fuzzy matched "${artistName}" (reverse double letters) in title: "${title}"`);
                return true;
            }
            
            return false;
        }
        
        // Helper function to find the best thread from search results
        const findBestThread = (results, threadType) => {
            if (!results || !Array.isArray(results) || results.length === 0) {
                console.log(`No ${threadType} search results found.`);
                return null;
            }

            console.log(`Processing ${results.length} ${threadType} search results.`);
            
            // Debug: Log all thread titles for this search type
            console.log(`All ${threadType} search results:`);
            results.forEach((item, index) => {
                if (item.kind === "t3" && item.data && item.data.title) {
                    console.log(`  ${index + 1}. "${item.data.title}"`);
                }
            });

            // Look for an exact match first
            for (const item of results) {
                if (item.kind === "t3" && item.data && item.data.title && item.data.permalink) {
                    const title = item.data.title;
                    const url = "https://www.reddit.com" + item.data.permalink;

                    // Skip if we've already added this URL
                    if (addedUrls.has(url)) {
                        console.log(`Skipping duplicate thread: "${title}"`);
                        continue;
                    }

                    const titleLower = title.toLowerCase();
                    const threadTypeLower = threadType.toLowerCase();
                    const albumNameLower = albumName.toLowerCase();
                    const artistLower = normalizedArtist.toLowerCase();

                    // Check if this is the right type of thread
                    // More flexible matching for thread types (handles brackets, case differences)
                    const isCorrectThreadType =
                        titleLower.includes(threadTypeLower) ||
                        titleLower.includes(threadTypeLower.replace(' ', '')) || // Remove spaces
                        titleLower.includes(`[${threadTypeLower}]`) || // Handle [THREAD TYPE] format
                        titleLower.includes(`${threadTypeLower}]`); // Handle [THREAD TYPE] without opening bracket
                    
                    if (isCorrectThreadType) {
                        // For self-titled albums, ensure we're not matching just the artist name
                        if (isSelfTitled) {
                            // Look for patterns that indicate this is about the album, not just the artist
                            const albumIndicators = ['album', 'record', 'lp', 'release', 'debut', 'sophomore', 'self-titled'];
                            const hasAlbumIndicator = albumIndicators.some(indicator => titleLower.includes(indicator));
                            
                            if (titleLower.includes(albumNameLower) &&
                                (hasAlbumIndicator || titleLower.includes(`${artistLower} - ${albumNameLower}`))) {
                                console.log(`Found ${threadType} thread (self-titled album): "${title}"`);
                                return { title, url };
                            }
                        } else {
                            // For non-self-titled albums, require BOTH artist and album name to be present
                            // Use fuzzy matching for artist name to handle spelling variations
                            const artistMatches = fuzzyMatchArtist(titleLower, artistLower);
                            if (titleLower.includes(albumNameLower) && artistMatches) {
                                console.log(`Found ${threadType} thread: "${title}"`);
                                return { title, url };
                            }
                        }
                    }
                }
            }

            // If no exact match, take the first result that contains the album name
            for (const item of results) {
                if (item.kind === "t3" && item.data && item.data.title && item.data.permalink) {
                    const title = item.data.title;
                    const url = "https://www.reddit.com" + item.data.permalink;

                    // Skip if we've already added this URL
                    if (addedUrls.has(url)) {
                        console.log(`Skipping duplicate thread: "${title}"`);
                        continue;
                    }

                    const titleLower = title.toLowerCase();
                    const albumNameLower = albumName.toLowerCase();
                    const artistLower = normalizedArtist.toLowerCase();

                    // For self-titled albums, apply additional filtering
                    if (isSelfTitled) {
                        const albumIndicators = ['album', 'record', 'lp', 'release', 'debut', 'sophomore', 'self-titled'];
                        const hasAlbumIndicator = albumIndicators.some(indicator => titleLower.includes(indicator));
                        
                        if (titleLower.includes(albumNameLower) && hasAlbumIndicator) {
                            console.log(`Found ${threadType} thread (self-titled album, partial match): "${title}"`);
                            return { title, url };
                        }
                    } else {
                        // For non-self-titled albums, require BOTH artist and album name to be present
                        if (titleLower.includes(albumNameLower) && titleLower.includes(artistLower)) {
                            console.log(`Found ${threadType} thread (partial match): "${title}"`);
                            return { title, url };
                        }
                    }
                }
            }

            console.log(`No matching ${threadType} thread found.`);
            return null;
        };

        // Find the best thread for each type
        const freshAlbumThread = findBestThread(freshAlbumResults, "FRESH ALBUM");

        // Add FRESH ALBUM thread if found
        if (freshAlbumThread) {
            relevantThreads.push(freshAlbumThread);
            addedUrls.add(freshAlbumThread.url); // Track the URL to avoid duplicates
            console.log(`Added FRESH ALBUM thread: "${freshAlbumThread.title}"`);
        }

        // Find ALBUM DISCUSSION thread
        const albumDiscussionThread = findBestThread(albumDiscussionResults, "ALBUM DISCUSSION");

        // Add ALBUM DISCUSSION thread if found and not a duplicate
        if (albumDiscussionThread && !addedUrls.has(albumDiscussionThread.url)) {
            relevantThreads.push(albumDiscussionThread);
            addedUrls.add(albumDiscussionThread.url);
            console.log(`Added ALBUM DISCUSSION thread: "${albumDiscussionThread.title}"`);
        } else if (albumDiscussionThread) {
            console.log(`Skipping duplicate ALBUM DISCUSSION thread: "${albumDiscussionThread.title}"`);
        }

        console.log(`Found ${relevantThreads.length} unique relevant threads`);
        return relevantThreads;
    }

    /**
     * Fetches comments from a given Reddit thread URL using the .json endpoint.
     * Note: This uses GM_xmlhttpRequest for cross-origin requests in Userscripts.
     *
     * @param {string} threadUrl The URL of the Reddit thread.
     * @returns {Promise<Array<Object>|null>} A promise that resolves with an array of comment data or null on error.
     */
    function fetchRedditComments(threadUrl) {
        console.log(`[fetchRedditComments] Attempting to fetch comments for: ${threadUrl}`);
        return new Promise((resolve, reject) => {
            // Append .json to the thread URL to get the JSON data
            const jsonUrl = threadUrl.endsWith('.json') ? threadUrl : threadUrl + '.json';

            console.log(`[fetchRedditComments] Requesting URL: ${jsonUrl}`);

            // Use GM_xmlhttpRequest for cross-origin requests
            GM_xmlhttpRequest({
                method: "GET",
                url: jsonUrl,
                onload: function(response) {
                    console.log(`[fetchRedditComments] Received response for ${jsonUrl}. Status: ${response.status}`);
                    try {
                        if (response.status === 200) {
                            console.log(`[fetchRedditComments] Response Text for ${jsonUrl}: ${response.responseText.substring(0, 500)}...`); // Log beginning of response
                            const data = JSON.parse(response.responseText);
                            console.log("[fetchRedditComments] Successfully parsed JSON response.");
                            // The JSON response for a thread includes two arrays: [submission, comments]
                            // We need the comments array (index 1)
                            if (data && data.length === 2 && data[1] && data[1].data && data[1].data.children) {
                                console.log(`[fetchRedditComments] Found comment data. Number of top-level items: ${data[1].data.children.length}`);
                                // Process the raw comment data to extract relevant info and handle replies
                                const comments = processComments(data[1].data.children);
                                console.log(`[fetchRedditComments] Processed comments. Total processed: ${comments.length}`);
                                resolve(comments);
                            } else {
                                console.error("[fetchRedditComments] Unexpected Reddit JSON structure:", data);
                                resolve(null); // Resolve with null for unexpected structure
                            }
                        } else {
                            console.error("[fetchRedditComments] Error fetching Reddit comments:", response.status, response.statusText);
                            resolve(null); // Resolve with null on HTTP error
                        }
                    } catch (e) {
                        console.error("[fetchRedditComments] Error parsing Reddit comments JSON:", e);
                        resolve(null); // Resolve with null on parsing error
                    }
                },
                onerror: function(error) {
                    console.error("[fetchRedditComments] GM_xmlhttpRequest error fetching Reddit comments:", error);
                    resolve(null); // Resolve with null on request error
                }
            });
        });
    }

    /**
     * Recursively processes raw Reddit comment data to extract relevant info and handle replies.
     * Filters out 'more' comments placeholders.
     *
     * @param {Array<Object>} rawComments The raw comment children array from Reddit API.
     * @returns {Array<Object>} An array of processed comment objects.
     */
    function processComments(rawComments) {
        const processed = [];
        if (!rawComments || !Array.isArray(rawComments)) {
            return processed;
        }

        for (const item of rawComments) {
            // Skip 'more' comments placeholders
            if (item.kind === 'more') {
                continue;
            }

            // Ensure it's a comment and has the necessary data
            if (item.kind === 't1' && item.data) {
                const commentData = item.data;
                const processedComment = {
                    author: commentData.author,
                    text: commentData.body,
                    score: commentData.score,
                    created_utc: commentData.created_utc,
                    replies: [] // Initialize replies array
                };

                // Recursively process replies if they exist
                if (commentData.replies && commentData.replies.data && commentData.replies.data.children) {
                    processedComment.replies = processComments(commentData.replies.data.children);
                }

                processed.push(processedComment);
            }
        }
        return processed;
    }

    // --- HTML Structures and Injection ---

    const REDDIT_COMMENTS_SECTION_HTML = `
        <div class="reddit-comments-section">
            <h3>Reddit Comments from r/indieheads</h3>
            <div class="reddit-comments-tabs">
                <!-- Tab buttons will be added here -->
            </div>
            <div class="reddit-comments-content">
                <!-- Comment content areas will be added here -->
                <!-- Each area will have a data-thread-id or similar to link to the tab -->
            </div>
        </div>
    `;

    /**
     * Injects HTML content after the last <hr> tag in the article and removes the <hr> tag.
     * If no <hr> tag is found, injects before div.disclaimer.
     * @param {string|HTMLElement} content The HTML string or HTMLElement to inject.
     */
    function injectAfterLastParagraph(content) {
        // Find the article element
        const article = document.querySelector('article');
        if (!article) {
            console.error('Article element not found for injection');
            return;
        }

        // Find all <hr> tags within the article
        const hrElements = Array.from(article.querySelectorAll('hr'));

        if (hrElements.length > 0) {
            // Get the last <hr> tag
            const lastHr = hrElements[hrElements.length - 1];

            // Insert content after the last <hr> tag
            if (typeof content === 'string') {
                lastHr.insertAdjacentHTML('afterend', content);
            } else {
                lastHr.insertAdjacentElement('afterend', content);
            }

            // Remove the <hr> tag after injecting our content
            lastHr.remove();
        } else {
            // Fallback: find div.disclaimer and inject before it
            const disclaimerDiv = article.querySelector('div.disclaimer');
            if (disclaimerDiv) {
                console.log('No <hr> found, injecting before div.disclaimer');
                // Insert content before the disclaimer div
                if (typeof content === 'string') {
                    disclaimerDiv.insertAdjacentHTML('beforebegin', content);
                } else {
                    disclaimerDiv.insertAdjacentElement('beforebegin', content);
                }
            } else {
                // Second fallback: find article with data-testid="ReviewPageArticle" and inject as last child
                const reviewArticle = document.querySelector('article[data-testid="ReviewPageArticle"] div[class^="GridWrapper"]:last-child div.grid-layout__content div.article__body > div > p:last-child');
                if (reviewArticle) {
                    console.log('No <hr> or div.disclaimer found, injecting as last child of ReviewPageArticle');
                    // Insert content as the last child of the review article
                    if (typeof content === 'string') {
                        reviewArticle.insertAdjacentHTML('beforeend', content);
                    } else {
                        reviewArticle.appendChild(content);
                    }
                } else {
                    console.error('No <hr> tags, div.disclaimer, or article[data-testid="ReviewPageArticle"] found for injection');
                }
            }
        }
    }

    // Function to render comments to HTML (basic structure)
    function renderCommentsHtml(comments, level = 0) {
        let html = `<ul class="reddit-comment-list level-${level}">`;
        if (!comments || comments.length === 0) {
            html += '<li>No comments found for this thread.</li>';
        } else {
            // Filter out deleted comments
            const validComments = comments.filter(comment => 
                comment.author !== "[deleted]" && comment.text !== "[deleted]"
            );

            if (validComments.length === 0) {
                html += '<li>No valid comments found for this thread.</li>';
            } else {
                validComments.forEach(comment => {
                    html += `<li class="reddit-comment">`;
                    
                    // Add collapse button for top-level comments
                    if (level === 0) {
                        html += `<div class="comment-meta">
                            <b>${comment.author}</b> (${comment.score} points)
                            <button class="comment-collapse-button">[−]</button>
                        </div>`;
                    } else {
                        html += `<div class="comment-meta"><b>${comment.author}</b> (${comment.score} points)</div>`;
                    }
                    
                    // Process comment text for special content
                    let processedText = comment.text;
                    
                    // Process Giphy embeds first
                    processedText = processedText.replace(/!\[gif\]\(giphy\|([a-zA-Z0-9]+)(?:\|downsized)?\)/g, (match, giphyId) => {
                        return `
                            <div class="giphy-embed-container">
                                <iframe src="https://giphy.com/embed/${giphyId}" 
                                    width="480" height="270" frameBorder="0" 
                                    class="giphy-embed" allowFullScreen></iframe>
                            </div>
                        `;
                    });
                    
                    // Process Reddit image links
                    processedText = processedText.replace(/(https:\/\/preview\.redd\.it\/[a-zA-Z0-9]+\.(jpeg|jpg|png|gif)\?[^\s)]+)/g, (match, imageUrl) => {
                        return `
                            <div class="reddit-image-container">
                                <img src="${imageUrl}" alt="Reddit Image" class="reddit-inline-image" />
                            </div>
                        `;
                    });
                    
                    // Process Markdown image syntax for Reddit images
                    processedText = processedText.replace(/!\[.*?\]\((https:\/\/preview\.redd\.it\/[a-zA-Z0-9]+\.(jpeg|jpg|png|gif)\?[^\s)]+)\)/g, (match, imageUrl) => {
                        return `
                            <div class="reddit-image-container">
                                <img src="${imageUrl}" alt="Reddit Image" class="reddit-inline-image" />
                            </div>
                        `;
                    });
                    
                    // Process basic Markdown formatting
                    
                    // Bold text
                    processedText = processedText.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
                    
                    // Italic text
                    processedText = processedText.replace(/\*([^*]+)\*/g, '<em>$1</em>');
                    
                    // Block quotes - simple implementation
                    processedText = processedText.replace(/^(>|>)\s*(.*?)$/gm, '<blockquote>$2</blockquote>');
                    
                    // Parse Markdown links
                    processedText = processedText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
                        return `<a href="${url}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
                    });
                    
                    // Parse plain URLs
                    processedText = processedText.replace(/(?<!["\'])(https?:\/\/[^\s<>[\]()'"]+)(?![^<]*>)/g, (match, url) => {
                        return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
                    });
                    
                    // Handle line breaks - simple approach
                    processedText = processedText.replace(/\n\n+/g, '</p><p>');
                    processedText = processedText.replace(/\n/g, '<br>');
                    
                    // Wrap in paragraph tags if not already
                    if (!processedText.startsWith('<p>') && 
                        !processedText.startsWith('<div') && 
                        !processedText.startsWith('<blockquote') &&
                        !processedText.includes('<div class="giphy-embed-container">') &&
                        !processedText.includes('<div class="reddit-image-container">')) {
                        processedText = `<p>${processedText}</p>`;
                    }
                    
                    html += `<div class="comment-body">${processedText}</div>`;
                    if (comment.replies && comment.replies.length > 0) {
                        html += renderCommentsHtml(comment.replies, level + 1);
                    }
                    html += `</li>`;
                });
            }
        }
        html += `</ul>`;
        return html;
    }

    function setupCommentCollapse() {
        document.querySelectorAll('.comment-collapse-button').forEach(button => {
            button.addEventListener('click', function() {
                const commentLi = this.closest('.reddit-comment');
                commentLi.classList.toggle('collapsed');
                
                // Update button text
                if (commentLi.classList.contains('collapsed')) {
                    this.textContent = '[+]';
                } else {
                    this.textContent = '[−]';
                }
            });
        });
    }

    // --- CSS Styles ---
    function injectStyles() {
        const styles = `
            @media (min-width: 2400px) {
                #main-content div[class^="GridWrapper"] {
                    max-width: 2000px;
                }
            }
            .reddit-comments-section {
                margin-top: 30px;
                padding: 0;
                border-top: 1px solid #ddd;
                font-family: inherit;
            }
            .reddit-comments-tabs {
                display: flex;
                flex-wrap: wrap;
                margin-bottom: 15px;
            }
            .reddit-tab-button {
                padding: 6px 8px 6px 12px;
                margin-right: 5px;
                margin-bottom: 5px;
                background: #f0f0f0;
                border: 1px solid #ccc;
                border-radius: 4px;
                cursor: pointer;
                font-size: 16px;
                letter-spacing: 0;
            }
            .reddit-tab-button:not(.active):hover {
                background: #f8f8f8;
            }
            .reddit-tab-button:hover, .reddit-tab-button:active, .reddit-tab-button:focus {
                text-decoration: none;
            }
            .reddit-tab-button.active {
                background: #e0e0e0;
                border-color: #aaa;
                font-weight: bold;
            }
            .reddit-comment-list {
                list-style-type: none;
                padding-left: 0;
            }
            .reddit-comment-list.level-0 {
                padding-left: 0;
                margin-left: 0;
            }
            .reddit-comment-list.level-1 {
                padding-left: 10px;
                border-left: 2px solid #eee;
            }
            .reddit-comment-list.level-2,
            .reddit-comment-list.level-3,
            .reddit-comment-list.level-4,
            .reddit-comment-list.level-5 {
                padding-left: 10px;
                border-left: 2px solid #f5f5f5;
            }
            .reddit-comment {
                margin-bottom: 15px;
            }
            .reddit-image-container {
                margin-top: 10px;
            }
            .comment-meta {
                font-size: .9em;
                margin-bottom: 5px;
                color: #666;
            }
            .comment-body {
                line-height: 1.5;
            }
            div.comment-body > p {
                padding: 0;
                margin: 0 0 1.125rem !important;
            }
            /* Markdown formatting styles */
            .comment-body strong {
                font-weight: 700;
            }
            .comment-body em {
                font-style: italic;
            }
            .comment-body blockquote {
                border-left: 3px solid #c5c1ad;
                margin: 8px 0;
                padding: 0 8px 0 12px;
                color: #646464;
                background-color: #f8f9fa;
            }
            /* Paragraph styling */
            .comment-body p {
                margin: .8em 0;
            }

            .comment-body p:first-child {
                margin-top: 0;
            }
            .comment-body p:last-child {
                margin-bottom: 0;
            }
            .comment-body blockquote p {
                margin: .4em 0;
            }
            .reddit-comment.collapsed .comment-body,
            .reddit-comment.collapsed .reddit-comment-list {
                display: none;
            }
            .reddit-comment.collapsed {
                opacity: 0.7;
            }
            .comment-collapse-button {
                background: none;
                border: none;
                color: #0079d3;
                cursor: pointer;
                font-size: 12px;
                margin-left: 5px;
                padding: 0;
            }
            .comment-collapse-button:hover {
                text-decoration: underline;
            }
        `;

        const styleElement = document.createElement('style');
        styleElement.textContent = styles;
        document.head.appendChild(styleElement);
    }


    // --- Main Execution Logic ---

    async function init() {
        console.log("Pitchfork Reddit Comments Userscript started.");

        // Inject CSS styles
        injectStyles();

        const artist = extractArtistName();
        const album = extractAlbumName();

        console.log(`Extracted Artist: ${JSON.stringify(artist)}`);
        console.log(`Extracted Album: ${JSON.stringify(album)}`);

        if (!artist || !album) {
            console.log("Could not extract artist or album name. Exiting.");
            return;
        }

        console.log(`Found Artist: ${artist}, Album: ${album}`);

        const queries = formatAlbumSearchQueries(artist, album);
        console.log(`Search queries:`, queries);

        // Make separate search requests for each query type
        const freshAlbumUrl = buildIndieHeadsSearchJsonUrl(queries.freshAlbumQuery);
        const albumDiscussionUrl = buildIndieHeadsSearchJsonUrl(queries.albumDiscussionQuery);

        console.log(`Fresh Album Search URL: ${freshAlbumUrl}`);
        console.log(`Album Discussion Search URL: ${albumDiscussionUrl}`);

        // Function to perform a search request
        const performSearch = (url) => {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    onload: function(response) {
                        try {
                            console.log(`[Search Request] Received response. Status: ${response.status}`);
                            if (response.status === 200) {
                                const searchData = JSON.parse(response.responseText);
                                console.log("[Search Request] Successfully parsed JSON response.");
                                if (searchData && searchData.data && searchData.data.children) {
                                    resolve(searchData.data.children);
                                } else {
                                    console.error("[Search Request] Unexpected Reddit search JSON structure:", searchData);
                                    resolve([]);
                                }
                            } else {
                                console.error("[Search Request] Error fetching Reddit search results:", response.status, response.statusText);
                                resolve([]);
                            }
                        } catch (e) {
                            console.error("[Search Request] Error parsing Reddit search JSON:", e);
                            resolve([]);
                        }
                    },
                    onerror: function(error) {
                        console.error("[Search Request] GM_xmlhttpRequest error fetching Reddit search results:", error);
                        resolve([]);
                    }
                });
            });
        };

        try {
            // Perform both searches in parallel
            const [freshAlbumResults, albumDiscussionResults] = await Promise.all([
                performSearch(freshAlbumUrl),
                performSearch(albumDiscussionUrl)
            ]);

            // Identify relevant threads from both result sets
            const relevantThreads = identifyRelevantThreads(
                freshAlbumResults,
                albumDiscussionResults,
                typeof artist === 'string' ? artist : artist.join(' & '), 
                album
            );

            if (relevantThreads.length === 0) {
                console.log("No relevant Reddit threads found.");
                const noThreadsMessage = document.createElement('p');
                noThreadsMessage.textContent = 'No relevant Reddit threads found for this review.';
                noThreadsMessage.style.fontStyle = 'italic';
                noThreadsMessage.style.marginTop = '20px'; // Add some spacing
                injectAfterLastParagraph(noThreadsMessage);
                return;
            }

            console.log(`Found ${relevantThreads.length} relevant thread(s):`, relevantThreads);

            // Inject the main comments section container
            injectAfterLastParagraph(REDDIT_COMMENTS_SECTION_HTML);

            const commentsSection = document.querySelector('.reddit-comments-section');
            const tabsArea = commentsSection.querySelector('.reddit-comments-tabs');
            const contentArea = commentsSection.querySelector('.reddit-comments-content');

            // Fetch comments and build tabs/content
            for (let i = 0; i < relevantThreads.length; i++) {
                const thread = relevantThreads[i];
                console.log(`Fetching comments for thread: ${thread.title} (${thread.url})`);
                const comments = await fetchRedditComments(thread.url);

                // Generate tab button
                const tabButton = document.createElement('button');
                tabButton.classList.add('reddit-tab-button');
                tabButton.textContent = thread.title + ' ';
                tabButton.setAttribute('data-thread-index', i);

                // Add a direct link icon that opens the Reddit thread in a new tab
                const linkIcon = document.createElement('a');
                linkIcon.href = thread.url;
                linkIcon.target = '_blank';
                linkIcon.rel = 'noopener noreferrer'; // Security best practice for target="_blank"
                linkIcon.innerHTML = '🔗';
                linkIcon.title = 'Open Reddit thread in new tab';
                linkIcon.style.fontSize = '0.8em';
                linkIcon.style.opacity = '0.7';
                linkIcon.style.textDecoration = 'none'; // Remove underline
                linkIcon.style.marginLeft = '5px';

                tabButton.appendChild(linkIcon);
                tabsArea.appendChild(tabButton);

                // Generate comment content area
                const threadContent = document.createElement('div');
                threadContent.classList.add('reddit-tab-content');
                threadContent.setAttribute('data-thread-index', i);
                threadContent.style.display = 'none'; // Hide by default

                if (comments) {
                    threadContent.innerHTML = renderCommentsHtml(comments);
                    // Set up collapse functionality for this tab's comments
                    setupCommentCollapse();
                } else {
                    threadContent.innerHTML = '<p>Could not load comments for this thread.</p>';
                }
                contentArea.appendChild(threadContent);

                // Activate the first tab and content by default
                if (i === 0) {
                    tabButton.classList.add('active');
                    threadContent.style.display = 'block';
                }
            }

            // Add event listeners for tab switching
            const tabButtons = tabsArea.querySelectorAll('.reddit-tab-button');
            const tabContents = contentArea.querySelectorAll('.reddit-tab-content');

            tabButtons.forEach(button => {
                button.addEventListener('click', () => {
                    const threadIndex = button.getAttribute('data-thread-index');

                    // Deactivate all tabs and hide all content
                    tabButtons.forEach(btn => btn.classList.remove('active'));
                    tabContents.forEach(content => content.style.display = 'none');

                    // Activate the clicked tab and show corresponding content
                    button.classList.add('active');
                    const activeContent = document.querySelector(`.reddit-tab-content[data-thread-index="${threadIndex}"]`);
                    activeContent.style.display = 'block';

                    // Re-initialize collapse functionality for the newly displayed tab
                    setupCommentCollapse();
                });
            });

        } catch (error) {
            console.error("Error during search process:", error);
        }
    }

    // Run the initialization function
    init();

})();