// ==UserScript==
// @name Trakt.tv | Actor Pronunciation Helper
// @description Adds a button on /people pages for fetching an audio recording of that person's name with the correct pronunciation from https://forvo.com
// @version 1.0.6
// @namespace https://github.com/Fenn3c401
// @author Fenn3c401
// @license GPL-3.0-or-later
// @homepageURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection#readme
// @supportURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection/issues
// @updateURL https://update.greasyfork.org/scripts/550069.meta.js
// @downloadURL https://raw.githubusercontent.com/Fenn3c401/Trakt.tv-Userscript-Collection/main/userscripts/dist/71cd9s61.user.js
// @icon https://trakt.tv/assets/logos/logomark.square.gradient-b644b16c38ff775861b4b1f58c1230f6a097a2466ab33ae00445a505c33fcb91.svg
// @match https://trakt.tv/*
// @match https://classic.trakt.tv/*
// @run-at document-start
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM.xmlHttpRequest
// @connect forvo.com
// ==/UserScript==
/* global moduleName */
'use strict';
let $;
const logger = {
_defaults: {
title: (typeof moduleName !== 'undefined' ? moduleName : GM_info.script.name).replace('Trakt.tv', 'Userscript'),
toast: true,
toastrOpt: { positionClass: 'toast-top-right', timeOut: 10000, progressBar: true },
toastrStyles: '#toast-container#toast-container a { color: #fff !important; border-bottom: dotted 1px #fff; }',
},
_print(fnConsole, fnToastr, msg = '', opt = {}) {
const { title = this._defaults.title, toast = this._defaults.toast, toastrOpt, toastrStyles = '', consoleStyles = '', data } = opt,
fullToastrMsg = `${msg}${data !== undefined ? ' See console for details.' : ''}`;
console[fnConsole](`%c${title}: ${msg}`, consoleStyles, ...(data !== undefined ? [data] : []));
if (toast) unsafeWindow.toastr?.[fnToastr](fullToastrMsg, title, { ...this._defaults.toastrOpt, ...toastrOpt });
},
info(msg, opt) { this._print('info', 'info', msg, opt) },
success(msg, opt) { this._print('info', 'success', msg, { consoleStyles: 'color:#00c853;', ...opt }) },
warning(msg, opt) { this._print('warn', 'warning', msg, opt) },
error(msg, opt) { this._print('error', 'error', msg, opt) },
};
addStyles();
document.addEventListener('turbo:load', () => {
if (!/^\/people\/[^\/]+(\/lists.*)?$/.test(location.pathname)) return;
$ ??= unsafeWindow.jQuery;
if (!$) return;
$(``
).appendTo($('#summary-wrapper .mobile-title h1')).tooltip({
title: 'Pronounce Name',
container: 'body',
placement: 'top',
html: true,
}).one('click', async function() {
$(this).tooltip('hide');
const $btnPronounceName = $(this),
name = $('body > [itemtype$="Person"] > meta[itemprop="name"]').attr('content') ?? $('#summary-wrapper .mobile-title > :last-child').text(); // fallback for /people//lists pages
unsafeWindow.showLoading?.();
const fullNameAudio = await fetchAudio(name);
const audios = fullNameAudio ? [fullNameAudio] : await Promise.all(name.split(/\s+/).map((namePart) => {
return /^\w\.?$/.test(namePart) ? new SpeechSynthesisUtterance(namePart) : fetchAudio(namePart).then((res) => res ?? new SpeechSynthesisUtterance(namePart));
}));
unsafeWindow.hideLoading?.();
if (audios.some((audio) => audio instanceof SpeechSynthesisUtterance)) {
audios.forEach((audio) => { if (audio instanceof SpeechSynthesisUtterance) audio.lang = 'en-US'; });
logger.warning(`Could not find a full pronunciation for "${name}" on ` +
`forvo.com. Falling back to TTS..`);
}
['ended', 'end'].forEach((type) => {
audios.slice(1).forEach((audio, i) => {
audios[i]?.addEventListener(type, () => audio.play ? audio.play() : speechSynthesis.speak(audio));
});
audios.at(-1).addEventListener(type, () => {
$btnPronounceName.find('.audio-animation').removeClass('in');
setTimeout(() => $btnPronounceName.find('.fa').addClass('in'), 150);
});
});
playAudios(audios, $btnPronounceName);
$btnPronounceName.on('click', () => playAudios(audios, $btnPronounceName));
});
}, { capture: true });
async function fetchAudio(query) {
const resp = await GM.xmlHttpRequest({ url: `https://forvo.com/search/${encodeURIComponent(query)}` }),
doc = new DOMParser().parseFromString(resp.responseText, 'text/html'),
audioHttpHost = $(doc).find('body > script').text().match(/_AUDIO_HTTP_HOST='(.+?)'/)?.[1],
audioPathsRaw = $(doc).find('[onclick^="Play"]').attr('onclick')?.match(/Play\([0-9]+,'(.*?)','(.*?)',(?:true|false),'(.*?)','(.*?)'/)?.slice(1),
audioPaths = audioPathsRaw?.map((pathRaw, i) => pathRaw && ['/mp3/', '/ogg/', '/audios/mp3/', '/audios/ogg/'][i] + atob(pathRaw)).filter(Boolean).reverse();
return audioPaths?.length ? $('')[0] : null;
}
function playAudios(audios, $btnPronounceName) {
$btnPronounceName.find('.fa').removeClass('in');
setTimeout(() => {
$btnPronounceName.find('.audio-animation').addClass('in');
audios.forEach((audio) => audio.load?.()); // for repeated playback; currentTime = 0 doesn't work for some audio files
speechSynthesis.cancel();
audios[0].play ? audios[0].play() : speechSynthesis.speak(audios[0]);
}, 150);
}
function addStyles() {
GM_addStyle(`
#btn-pronounce-name {
margin: 0 0 2px 7px;
position: relative;
height: 20px;
width: 20px;
vertical-align: middle;
display: inline-flex;
align-items: center;
justify-content: center;
border-style: none;
background-color: transparent;
}
#btn-pronounce-name .fa {
position: absolute;
font-size: 16px;
color: #aaa;
}
#btn-pronounce-name:hover .fa {
color: var(--link-color);
}
#btn-pronounce-name .audio-animation {
position: absolute;
height: 75%;
width: 75%;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
}
#btn-pronounce-name .audio-animation [class^="bar-"] {
flex: 1;
height: 100%;
border-radius: 3px;
background: linear-gradient(180deg, rgb(255 0 0), rgb(155 66 200));
transform: scaleY(0.2);
}
#btn-pronounce-name .in .bar-1 { animation: lineWave-1 .4s .3s infinite alternate; }
#btn-pronounce-name .in .bar-2 { animation: lineWave-2 .3s .2s infinite alternate; }
#btn-pronounce-name .in .bar-3 { animation: lineWave-3 .35s .25s infinite alternate; }
@keyframes lineWave-1 { from { transform: scaleY(0.24); } to { transform: scaleY(0.85); } }
@keyframes lineWave-2 { from { transform: scaleY(0.27); } to { transform: scaleY(0.98); } }
@keyframes lineWave-3 { from { transform: scaleY(0.24); } to { transform: scaleY(0.80); } }
`);
}