Affiche les scores de Rotten Tomatoes sur wawacity pour les films et les séries
// ==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)
}
})()