Affiche Rottentomatoes meter sur WaWaCity

Affiche les scores de Rotten Tomatoes sur wawacity pour les films et les séries

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Affiche Rottentomatoes meter sur WaWaCity
// @description Affiche les scores de Rotten Tomatoes sur wawacity pour les films et les séries
// @namespace   ConnorMcLeod
// @grant       GM_xmlhttpRequest
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       unsafeWindow
// @grant       GM.xmlHttpRequest
// @grant       GM.setValue
// @grant       GM.getValue
// @require     https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js
// @license     GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @icon        https://raw.githubusercontent.com/hfg-gmuend/openmoji/master/color/72x72/1F345.png
// @version     1.1
// @connect     www.rottentomatoes.com
// @connect     algolia.net
// @connect     flixster.com
// @match       https://www.rottentomatoes.com/
// @include     /^https:\/\/www\.wawacity\.[^\/]+\/\?p=film&id=.*/
// @include     /^https:\/\/www\.wawacity\.[^\/]+\/\?p=serie&id=.*/
// ==/UserScript==

/*
 * This script is a simplified version of the original "Show Rottentomatoes meter" 
 * by cuzi, modified to work on and only on WaWaCity website.
 * 
 * Original script: https://greasyfork.org/en/scripts/35443-show-rottentomatoes-meter
 * Modifications: Removed support for all sites except WaWaCity, added comments, integrated display
 */

/* global GM, $, unsafeWindow */

// Configuration constants
const SCRIPT_NAME = 'Show Rottentomatoes meter'
const BASE_URL = 'https://www.rottentomatoes.com'
const ALGOLIA_URL = 'https://{domain}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent={agent}&x-algolia-api-key={sId}&x-algolia-application-id={aId}'
const ALGOLIA_AGENT = 'Algolia for JavaScript (4.12.0); Browser (lite)'
const FLIXSTER_EMS_URL = 'https://flixster.com/api/ems/v2/emsId/{emsId}'
const CACHE_EXPIRE_HOURS = 4

// Emoji constants for visual indicators
const EMOJI_TOMATO = String.fromCodePoint(0x1F345)
const EMOJI_GREEN_APPLE = String.fromCodePoint(0x1F34F)
const EMOJI_STRAWBERRY = String.fromCodePoint(0x1F353)
const EMOJI_POPCORN = '\uD83C\uDF7F'
const EMOJI_GREEN_SALAD = '\uD83E\uDD57'
const EMOJI_NAUSEATED = '\uD83E\uDD22'

// Current search context
const current = {
    type: null,
    query: null,
    year: null,
    fallbackTitles: null,
    fallbackIndex: 0
}

/**
 * Add CSS styles for integrated ratings display
 */
function addStyles() {
    if (document.getElementById('rt-wawacity-styles')) return;

    const style = document.createElement('style');
    style.id = 'rt-wawacity-styles';
    style.textContent = `
    .rt-ratings-container {
      margin: 5px 0;
    }
    
    .rt-ratings {
      display: flex;
      gap: 15px;
      align-items: center;
      flex-wrap: wrap;
    }
    
    .rt-score-badge {
      display: inline-flex;
      align-items: center;
      padding: 6px 12px;
      border-radius: 20px;
      font-size: 13px;
      font-weight: 600;
      color: white;
      text-decoration: none;
      box-shadow: 0 2px 6px rgba(0,0,0,0.15);
      transition: all 0.3s ease;
      cursor: pointer;
      min-width: 70px;
      justify-content: center;
    }
    
    .rt-score-badge:hover {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(0,0,0,0.2);
      text-decoration: none;
      color: white;
    }
    
    .rt-critics.fresh {
      background: linear-gradient(45deg, #ff6b6b, #ee5a24);
    }
    
    .rt-critics.certified-fresh {
      background: linear-gradient(45deg, #ff6b6b, #c0392b);
      position: relative;
    }
    
    .rt-critics.certified-fresh::before {
      content: "✓";
      position: absolute;
      top: -2px;
      right: 2px;
      font-size: 10px;
      color: #f1c40f;
    }
    
    .rt-critics.rotten {
      background: linear-gradient(45deg, #27ae60, #2ecc71);
    }
    
    .rt-critics.no-score {
      background: linear-gradient(45deg, #7f8c8d, #95a5a6);
    }
    
    .rt-audience.good {
      background: linear-gradient(45deg, #3498db, #2980b9);
    }
    
    .rt-audience.bad {
      background: linear-gradient(45deg, #e67e22, #d35400);
    }
    
    .rt-audience.no-score {
      background: linear-gradient(45deg, #7f8c8d, #95a5a6);
    }
    
    .rt-score-emoji {
      margin-right: 5px;
      font-size: 14px;
    }
    
    .rt-score-text {
      font-size: 12px;
      font-weight: 500;
      color: #2c3e50 !important;
    }
    
    .rt-link {
      color: #3498db !important;
      text-decoration: none;
      font-size: 11px;
      margin-left: 10px;
      opacity: 0.8;
      transition: opacity 0.3s ease;
    }
    
    .rt-link:hover {
      opacity: 1;
      text-decoration: none;
      color: #2980b9 !important;
    }
    
    @media (max-width: 768px) {
      .rt-ratings {
        flex-direction: column;
        gap: 8px;
        align-items: flex-start;
      }
      
      .rt-score-badge {
        min-width: 120px;
      }
    }
  `;
    document.head.appendChild(style);
}

/**
 * Calculate time elapsed since a given date
 * @param {Date} time - The time to compare with current time
 * @returns {string} Human readable time difference
 */
function minutesSince(time) {
    const seconds = ((new Date()).getTime() - time.getTime()) / 1000
    return seconds > 60 ? parseInt(seconds / 60) + ' min ago' : 'now'
}

/**
 * Update Algolia search credentials from RottenTomatoes website
 * This function extracts API keys needed for search functionality
 */
function updateAlgolia() {
    const algoliaSearch = {
        aId: null,
        sId: null
    }

    // Extract algolia credentials from RottenTomatoes global object
    if (unsafeWindow.RottenTomatoes &&
        'thirdParty' in unsafeWindow.RottenTomatoes &&
        'algoliaSearch' in unsafeWindow.RottenTomatoes.thirdParty) {
        if (typeof(unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.aId) === 'string' &&
            typeof(unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.sId) === 'string') {
            algoliaSearch.aId = unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.aId
            algoliaSearch.sId = unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.sId
        }
    }

    // Save credentials to storage
    if (algoliaSearch.aId) {
        GM.setValue('algoliaSearch', JSON.stringify(algoliaSearch))
    }
}

/**
 * Request additional movie/TV show data from Flixster EMS API
 * @param {string} emsId - The EMS ID for the content
 * @returns {Promise} Promise resolving to enhanced data
 */
function askFlixsterEMS(emsId) {
    return new Promise(function(resolve) {
        GM.getValue('flixsterEmsCache', '{}').then(function(s) {
            const flixsterEmsCache = JSON.parse(s)

            // Clean expired cache entries
            for (const prop in flixsterEmsCache) {
                const hoursDiff = ((new Date()).getTime() - (new Date(flixsterEmsCache[prop].time)).getTime()) / (1000 * 60 * 60)
                if (hoursDiff > CACHE_EXPIRE_HOURS) {
                    delete flixsterEmsCache[prop]
                }
            }

            // Return cached data if available
            if (emsId in flixsterEmsCache) {
                return resolve(flixsterEmsCache[emsId])
            }

            // Make API request for new data
            const url = FLIXSTER_EMS_URL.replace('{emsId}', encodeURIComponent(emsId))
            GM.xmlHttpRequest({
                method: 'GET',
                url,
                onload: function(response) {
                    let data = {}
                    try {
                        data = JSON.parse(response.responseText)
                    } catch (e) {
                        // Silent error handling
                    }

                    // Save to cache with timestamp
                    data.time = (new Date()).toJSON()
                    flixsterEmsCache[emsId] = data
                    GM.setValue('flixsterEmsCache', JSON.stringify(flixsterEmsCache))
                    resolve(data)
                },
                onerror: function(response) {
                    resolve(null)
                }
            })
        })
    })
}

/**
 * Enhance original data with additional Flixster information
 * @param {Object} orgData - Original movie/show data
 * @returns {Promise<Object>} Enhanced data object
 */
async function addFlixsterEMS(orgData) {
    const flixsterData = await askFlixsterEMS(orgData.emsId)
    if (!flixsterData || !('tomatometer' in flixsterData)) {
        return orgData
    }

    // Add certified fresh status
    if ('certifiedFresh' in flixsterData.tomatometer && flixsterData.tomatometer.certifiedFresh) {
        orgData.meterClass = 'certified_fresh'
    }

    // Add review counts and scores
    if ('numReviews' in flixsterData.tomatometer) {
        orgData.numReviews = flixsterData.tomatometer.numReviews
        if ('freshCount' in flixsterData.tomatometer) {
            orgData.freshCount = flixsterData.tomatometer.freshCount
        }
        if ('rottenCount' in flixsterData.tomatometer) {
            orgData.rottenCount = flixsterData.tomatometer.rottenCount
        }
    }

    // Add consensus and average score
    if ('consensus' in flixsterData.tomatometer) {
        orgData.consensus = flixsterData.tomatometer.consensus
    }
    if ('avgScore' in flixsterData.tomatometer) {
        orgData.avgScore = flixsterData.tomatometer.avgScore
    }

    // Add audience data
    if ('userRatingSummary' in flixsterData) {
        const userRating = flixsterData.userRatingSummary
        if ('scoresCount' in userRating) {
            orgData.audienceCount = userRating.scoresCount
        }
        if ('avgScore' in userRating) {
            orgData.audienceAvgScore = userRating.avgScore
        }
    }

    return orgData
}

/**
 * Calculate match quality between search query and result
 * @param {string} title - Movie/show title from search results
 * @param {number} year - Release year
 * @param {Set} currentSet - Set of words from current query
 * @returns {number} Match quality score (higher = better match)
 */
function matchQuality(title, year, currentSet) {
    // Exact matches get highest scores
    if (title === current.query && year === current.year) {
        return 104 + year
    }
    if (title.toLowerCase() === current.query.toLowerCase() && year === current.year) {
        return 103 + year
    }

    // Year-based matching
    if (title === current.query && current.year) {
        return 102 - Math.abs(year - current.year)
    }
    if (title.toLowerCase() === current.query.toLowerCase() && current.year) {
        return 101 - Math.abs(year - current.year)
    }

    // Partial matches
    if (title.startsWith(current.query)) {
        return 6
    }
    if (title.indexOf(current.query) !== -1) {
        return 4
    }
    if (title.toLowerCase().indexOf(current.query.toLowerCase()) !== -1) {
        return 2
    }

    // Word-based matching
    const titleSet = new Set(title.replace(/[^a-z ]/gi, ' ').split(' '))
    const intersectionSize = new Set([...titleSet].filter(x => currentSet.has(x))).size
    return intersectionSize - 20 + (year === current.year ? 1 : 0)
}

/**
 * Create modern badge for critics score
 * @param {Object} data - Movie/show rating data
 * @returns {string} HTML string for the critics badge
 */
function createCriticsBadge(data) {
    let emoji = EMOJI_TOMATO
    let score = '--'
    let className = 'no-score'
    let tooltip = 'Trouvé sur RT mais pas encore noté par les critiques'

    if (typeof data.meterScore === 'number') {
        score = data.meterScore + '%'

        if (data.meterClass === 'certified_fresh') {
            tooltip = `Certified Fresh : ${data.meterScore}% critiques`
        } else if (data.meterClass === 'fresh') {
            tooltip = `Fresh : ${data.meterScore}% critiques`
        } else if (data.meterClass === 'rotten') {
            tooltip = `Rotten : ${data.meterScore}% critiques`
        }

        // Add additional info to tooltip
        if (data.numReviews) {
            tooltip += ` (${data.numReviews} avis)`
        }
        // if (data.consensus) {
        // tooltip += `\n${data.consensus}`
        // }
        if (data.consensus) {
            const node = document.createElement('span')
            node.innerHTML = data.consensus
            tooltip += '\n' + node.textContent
        }

    }

    return `<span class="rt-score-badge rt-critics ${className}" title="${tooltip}">
    <span class="rt-score-emoji">${emoji}</span>
    <span class="rt-score-text">${score}</span>
  </span>`
}

/**
 * Create modern badge for audience score
 * @param {Object} data - Movie/show rating data
 * @returns {string} HTML string for the audience badge
 */
function createAudienceBadge(data) {
    if (!('audienceScore' in data) || data.audienceScore === null) {
        return `<span class="rt-score-badge rt-audience no-score" title="Trouvé sur RT mais pas encore noté par le public">
      <span class="rt-score-emoji">👥</span>
      <span class="rt-score-text">--</span>
    </span>`
    }

    let emoji = EMOJI_POPCORN
    let className = 'good'
    let tooltip = `Public : ${data.audienceScore}%`

    if (data.audienceClass === 'red_popcorn') {
        emoji = EMOJI_POPCORN
        className = 'good'
    } else if (data.audienceClass === 'green_popcorn') {
        emoji = EMOJI_GREEN_SALAD
        className = 'bad'
    }

    // Add additional info to tooltip
    if (data.audienceCount) {
        tooltip += ` (${data.audienceCount.toLocaleString()} votes)`
    }
    if (data.audienceAvgScore) {
        tooltip += `\nMoyenne : ${data.audienceAvgScore}/5 étoiles`
    }

    return `<span class="rt-score-badge rt-audience ${className}" title="${tooltip}">
    <span class="rt-score-emoji">${emoji}</span>
    <span class="rt-score-text">${data.audienceScore}%</span>
  </span>`
}

/**
 * Load movie/TV show ratings from Rotten Tomatoes API
 * @param {string} query - Search query (movie/show title)
 * @param {string} type - Content type ('movie' or 'tv')
 * @param {number} year - Release year (optional)
 */
async function loadMeter(query, type, year) {
    current.type = type
    current.query = query
    current.year = year

    // Get cached data
    const algoliaCache = JSON.parse(await GM.getValue('algoliaCache', '{}'))
    const algoliaSearch = JSON.parse(await GM.getValue('algoliaSearch', '{}'))

    // Clean expired cache entries
    for (const prop in algoliaCache) {
        const hoursDiff = ((new Date()).getTime() - (new Date(algoliaCache[prop].time)).getTime()) / (1000 * 60 * 60)
        if (hoursDiff > CACHE_EXPIRE_HOURS) {
            delete algoliaCache[prop]
        }
    }

    // Use cached response if available
    if (query in algoliaCache) {
        handleAlgoliaResponse(algoliaCache[query])
        return
    }

    // Check if API credentials are available
    if (!('aId' in algoliaSearch && 'sId' in algoliaSearch)) {
        integrateRatings('ALGOLIA_NOT_CONFIGURED', new Date())
        return
    }

    // Make API request
    const url = ALGOLIA_URL
        .replace('{domain}', algoliaSearch.aId.toLowerCase())
        .replace('{aId}', encodeURIComponent(algoliaSearch.aId))
        .replace('{sId}', encodeURIComponent(algoliaSearch.sId))
        .replace('{agent}', encodeURIComponent(ALGOLIA_AGENT))

    GM.xmlHttpRequest({
        method: 'POST',
        url,
        data: '{"requests":[{"indexName":"content_rt","query":"' + query.replace('"', '') + '","params":"hitsPerPage=20"}]}',
        onload: function(response) {
            // Cache the response
            response.time = (new Date()).toJSON()
            const newobj = {}
            for (const key in response) {
                newobj[key] = response[key]
            }
            newobj.responseText = response.responseText
            algoliaCache[query] = newobj
            GM.setValue('algoliaCache', JSON.stringify(algoliaCache))

            handleAlgoliaResponse(response)
        },
        onerror: function(response) {
            // Silent error handling
        }
    })
}

/**
 * Process the response from Algolia search API
 * @param {Object} response - API response object
 */
async function handleAlgoliaResponse(response) {
    const rawData = JSON.parse(response.responseText)

    // Filter results by content type
    const hits = rawData.results[0].hits.filter(hit => hit.type === current.type)

    // Convert API response to standardized format
    let results = []
    hits.forEach(function(hit) {
        const result = {
            name: hit.title,
            year: parseInt(hit.releaseYear),
            url: '/' + (current.type === 'tv' ? 'tv' : 'm') + '/' + ('vanity' in hit ? hit.vanity : hit.title.toLowerCase()),
            meterClass: null,
            meterScore: null,
            audienceClass: null,
            audienceScore: null,
            emsId: hit.emsId
        }

        // Extract Rotten Tomatoes ratings from API response
        if ('rottenTomatoes' in hit) {
            const rt = hit.rottenTomatoes
            if ('criticsIconUrl' in rt) {
                result.meterClass = rt.criticsIconUrl.match(/\/(\w+)\.png/)[1]
            }
            if ('criticsScore' in rt) {
                result.meterScore = rt.criticsScore
            }
            if ('audienceIconUrl' in rt) {
                result.audienceClass = rt.audienceIconUrl.match(/\/(\w+)\.png/)[1]
            }
            if ('audienceScore' in rt) {
                result.audienceScore = rt.audienceScore
            }
            if ('certifiedFresh' in rt && rt.certifiedFresh) {
                result.meterClass = 'certified_fresh'
            }
        }
        results.push(result)
    })

    // Calculate match quality for ALL results first
    const currentSet = new Set(current.query.replace(/[^a-z ]/gi, ' ').split(' '))
    results = results.map(result => ({
        ...result,
        matchQuality: matchQuality(result.name, result.year, currentSet)
    }))

    // Then sort by match quality
    results.sort(function(a, b) {
        return b.matchQuality - a.matchQuality
    })

    // Filter out results with very poor match quality
    const MIN_QUALITY_THRESHOLD = 50
    const goodResults = results.filter(result => result.matchQuality >= MIN_QUALITY_THRESHOLD)

    // Use filtered results instead of original results
    const finalResults = goodResults

    // If no results found
    if (finalResults.length === 0) {
        // Try fallback titles if available
        if (current.fallbackTitles && current.fallbackIndex < current.fallbackTitles.length - 1) {
            loadMeterWithFallback(current.fallbackTitles, current.type, current.year, current.fallbackIndex + 1)
            return
        } else {
            // No more fallbacks, show NOT_FOUND
            integrateRatings('NOT_FOUND', new Date(response.time))
            return
        }
    }

    // Enhance first result with additional data if it's highly rated
    if (finalResults.length > 0 && finalResults[0].meterScore && finalResults[0].meterScore >= 70) {
        finalResults[0] = await addFlixsterEMS(finalResults[0])
    }

    if (finalResults.length > 0) {
        integrateRatings(finalResults, new Date(response.time))
    } else {
        integrateRatings('NOT_FOUND', new Date(response.time))
    }
}

/**
 * Integrate the ratings directly into the WaWaCity page
 * @param {Array|string} arr - Array of movie/show data or error message
 * @param {Date} time - Timestamp of the data
 */
function integrateRatings(arr, time) {
    // Remove any existing ratings
    $('.rt-ratings-container').remove()

    // Find the detail list container
    const detailList = document.querySelector('.detail-list')
    if (!detailList) {
        return
    }

    // Handle configuration error
    if (arr === 'ALGOLIA_NOT_CONFIGURED') {
        const errorItem = document.createElement('li')
        errorItem.innerHTML = `
      <span>Rotten Tomatoes:</span>
      <b style="color: #e74c3c;">
        Configuration requise - 
        <a href="https://www.rottentomatoes.com/" target="_blank" style="color: #3498db;">
          Visitez RottenTomatoes.com
        </a>
      </b>
    `
        detailList.appendChild(errorItem)
        return
    }


    // Handle not found case
    if (arr === 'NOT_FOUND') {
        const contentType = current.type === 'movie' ? 'Film' : 'Série'
        const notFoundItem = document.createElement('li')
        notFoundItem.innerHTML = `
		<span>Rotten Tomatoes:</span>
		<b style="color: #95a5a6; font-style: italic;">
		  ${contentType} non trouvé${current.type === 'movie' ? '' : 'e'}
		</b>
	  `
        detailList.appendChild(notFoundItem)
        return
    }

    // Create the ratings display element
    const bestMatch = arr[0]
    const ratingsItem = document.createElement('li')
    ratingsItem.className = 'rt-ratings-container'

    const criticsBadge = createCriticsBadge(bestMatch)
    const audienceBadge = createAudienceBadge(bestMatch)
    const rtLink = `${BASE_URL}${bestMatch.url}`

    ratingsItem.innerHTML = `
    <span>Rotten Tomatoes:</span>
    <b>
      <div class="rt-ratings">
        ${criticsBadge}
        ${audienceBadge}
        <a href="${rtLink}" target="_blank" class="rt-link" title="Voir sur Rotten Tomatoes">
          ${bestMatch.name} (${bestMatch.year})
        </a>
      </div>
    </b>
  `

    // Insert after genres or before last item
    const genresItem = Array.from(detailList.children).find(li =>
        li.textContent.includes('Genre') || li.textContent.includes('Genres')
    )

    if (genresItem && genresItem.nextSibling) {
        detailList.insertBefore(ratingsItem, genresItem.nextSibling)
    } else {
        const lastItem = detailList.lastElementChild
        if (lastItem) {
            detailList.insertBefore(ratingsItem, lastItem)
        } else {
            detailList.appendChild(ratingsItem)
        }
    }
}

/**
 * Extract movie/TV show information from WaWaCity page with fallback titles
 * @returns {boolean} True if data was found and processed
 */
function extractWaWaCityData() {
    // Check if we're on a movie or TV show page
    const isTVShow = document.location.search.startsWith('?p=serie&id=')
    const isMovie = document.location.search.startsWith('?p=film&id=')

    if (!isTVShow && !isMovie) {
        return false
    }

    try {
        // Find the detail list container
        const detailList = document.querySelector('.detail-list')
        if (!detailList) {
            return false
        }

        const listItems = detailList.getElementsByTagName("li")
        let year = null
        let originalTitle = null

        // Extract original title and year from page details
        for (const item of listItems) {
            if (item.textContent.includes('Année:')) {
                const yearMatch = item.textContent.match(/Année:\s*(\d{4})/)
                if (yearMatch) {
                    year = parseInt(yearMatch[1])
                }
            } else if (item.textContent.includes('Titre original:')) {
                const titleMatch = item.textContent.match(/Titre original:\s*(.+)/)
                if (titleMatch) {
                    originalTitle = titleMatch[1].trim()
                }
            }
        }

        // Extract fallback title from page title block
        const titleBlocks = document.querySelectorAll('.wa-sub-block-title')
        let fallbackTitle = null
        for (const titleBlock of titleBlocks) {
            if (titleBlock.textContent.includes(originalTitle) ||
                titleBlock.closest('.wa-sub-block').querySelector('.detail-list')) {

                let blockText = titleBlock.textContent
                    .replace(/^\s*[A-Z]{2}\s+/, '') // Remove flag
                    .split(' - ')[0] // Take first part before any " - "
                    .replace(/\s*\[.*?\].*$/i, '') // Remove quality indicators in brackets
                    .trim()

                if (blockText && blockText !== originalTitle) {
                    fallbackTitle = blockText
                    break
                }
            }
        }

        // Extract fallback title from page title
        let pageTitle = null
        if (document.title) {
            const titleMatch = document.title.match(/Télécharger\s+(.+?)\s+gratuitement/)
            if (titleMatch) {
                let extractedTitle = titleMatch[1]
                    .split(' - ')[0] // Take first part before any " - "
                    .replace(/\s*\[.*?\].*$/i, '') // Remove quality indicators in brackets
                    .trim()

                if (extractedTitle && extractedTitle !== originalTitle && extractedTitle !== fallbackTitle) {
                    pageTitle = extractedTitle
                }
            }
        }

        // Try searches with different titles
        if (originalTitle || fallbackTitle || pageTitle) {
            const contentType = isTVShow ? 'tv' : 'movie'

            // Start with original title, then fallback titles
            const searchTitles = [originalTitle, fallbackTitle, pageTitle].filter(Boolean)

            loadMeterWithFallback(searchTitles, contentType, year, 0)
            return true
        }
    } catch (e) {
        // Silent error handling
    }

    return false
}

/**
 * Load meter with fallback titles if first search fails
 * @param {Array} titles - Array of titles to try
 * @param {string} type - Content type
 * @param {number} year - Release year
 * @param {number} index - Current title index
 */
async function loadMeterWithFallback(titles, type, year, index = 0) {
    if (index >= titles.length) {
        return
    }

    const title = titles[index]

    // Store fallback info for later use
    current.fallbackTitles = titles
    current.fallbackIndex = index

    await loadMeter(title, type, year)
}

/**
 * Main function to initialize the script
 */
function main() {
    // Add CSS styles
    addStyles()

    // Update Algolia credentials if on RottenTomatoes homepage
    if (document.location.href === 'https://www.rottentomatoes.com/') {
        updateAlgolia()
        return false
    }

    // Extract and process WaWaCity data
    if (document.location.hostname.includes('wawacity')) {
        return extractWaWaCityData()
    }

    return false
}

// Initialize script execution
(function() {
    const firstRunResult = main()
    let lastLocation = document.location.href
    let lastContent = document.body.innerText
    let retryCounter = 0

    /**
     * Handle page content changes and new page loads
     */
    function handlePageChange() {
        if (lastContent === document.body.innerText && retryCounter < 15) {
            // Content hasn't changed, retry after delay
            window.setTimeout(handlePageChange, 500)
            retryCounter++
        } else {
            // Content has changed, reset and try extraction
            lastContent = document.body.innerText
            retryCounter = 0
            const result = main()
            if (!result) {
                // No data found, try again later
                window.setTimeout(handlePageChange, 1000)
            }
        }
    }

    // Monitor for location changes (SPA navigation)
    window.setInterval(function() {
        if (document.location.href !== lastLocation) {
            lastLocation = document.location.href
            $('.rt-ratings-container').remove() // Remove existing ratings
            window.setTimeout(handlePageChange, 1000) // Wait for new content to load
        }
    }, 500)

    // Retry initial run if no data was found
    if (!firstRunResult) {
        window.setTimeout(main, 2000)
    }
})()