// ==UserScript==
// @name External links on Trakt
// @name:de Externe Links auf Trakt
// @name:es Enlaces externos en Trakt
// @name:fr Liens externes sur Trakt
// @name:it Link esterni su Trakt
// @name:ru Внешние ссылки на Trakt
// @name:zh-CN 关于Trakt的外部链接
// @author Davide <iFelix18@protonmail.com>
// @namespace https://github.com/iFelix18
// @icon https://www.google.com/s2/favicons?sz=64&domain=https://trakt.tv
// @description Add more external links on Trakt
// @description:de Mehr externe Links auf Trakt hinzufügen
// @description:es Añadir más enlaces externos en Trakt
// @description:fr Ajouter plus de liens externes sur Trakt
// @description:it Aggiunge più link esterni su Trakt
// @description:ru Добавить больше внешних ссылок на Тракт
// @description:zh-CN 在Trakt上添加更多的外部链接
// @copyright 2022, Davide (https://github.com/iFelix18)
// @license MIT
// @version 1.4.1
// @homepage https://github.com/iFelix18/Trakt-Userscripts#readme
// @homepageURL https://github.com/iFelix18/Trakt-Userscripts#readme
// @supportURL https://github.com/iFelix18/Trakt-Userscripts/issues
// @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@43fd0fe4de1166f343883511e53546e87840aeaf/gm_config.min.js
// @require https://cdn.jsdelivr.net/npm/@ifelix18/utils@5.1.1/lib/index.min.js
// @require https://cdn.jsdelivr.net/npm/@ifelix18/wikidata@2.1.1/lib/index.min.js
// @require https://cdn.jsdelivr.net/npm/node-creation-observer@1.2.0/release/node-creation-observer-latest.min.js
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js
// @match *://trakt.tv/*
// @connect query.wikidata.org
// @compatible chrome
// @compatible edge
// @compatible firefox
// @compatible safari
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.deleteValue
// @grant GM.getValue
// @grant GM.listValues
// @grant GM.registerMenuCommand
// @grant GM.setValue
// @grant GM.xmlHttpRequest
// @run-at document-start
// @inject-into content
// ==/UserScript==
/* global $, GM_config, NodeCreationObserver, UserscriptUtils, Wikidata */
(() => {
//* Constants
const cachePeriod = 3_600_000
const id = GM.info.script.name.toLowerCase().replace(/\s/g, '-')
const title = `${GM.info.script.name} v${GM.info.script.version} Settings`
const fields = {
italy: {
label: 'Show Italian external sites',
section: ['Features'],
labelPos: 'left',
type: 'checkbox',
default: false
},
logging: {
label: 'Logging',
section: ['Develop'],
labelPos: 'left',
type: 'checkbox',
default: false
},
debugging: {
label: 'Debugging',
labelPos: 'left',
type: 'checkbox',
default: false
},
clearCache: {
label: 'Clear the cache',
type: 'button',
click: async () => {
const values = await GM.listValues()
for (const value of values) {
const cache = await GM.getValue(value) // get cache
if (cache.time) { GM.deleteValue(value) } // delete cache
}
UU.log('cache cleared')
GM_config.close()
}
}
}
//* GM_config
GM_config.init({
id,
title,
fields,
css: ':root{--font:"Montserrat",sans-serif;--background-grey:rgb(29, 29, 29);--black:rgb(0, 0, 0);--dark-grey:rgb(22, 22, 22);--grey:rgb(51, 51, 51);--red:rgb(237, 34, 36);--white:rgb(255, 255, 255)}#external-links-on-trakt *{color:var(--white)!important;font-family:var(--font)!important;font-size:14px!important;font-weight:400!important}#external-links-on-trakt{background:var(--background-grey)!important}#external-links-on-trakt .config_header{font-size:34px!important;line-height:1.1!important;text-shadow:0 0 20px var(--black)!important}#external-links-on-trakt .section_header_holder{background:var(--dark-grey)!important;border:1px solid var(--grey)!important;margin-bottom:1em!important}#external-links-on-trakt .section_header{background:var(--grey)!important;border:1px solid var(--grey)!important;padding:8px!important;text-align:left!important;text-transform:uppercase!important}#external-links-on-trakt .config_var{align-items:center!important;display:flex!important;margin:0!important;padding:15px!important}#external-links-on-trakt .field_label{margin-left:6px!important}#external-links-on-trakt button,#external-links-on-trakt input[type=button]{background:var(--grey)!important;border:1px solid transparent!important;padding:10px 16px!important}#external-links-on-trakt button:hover,#external-links-on-trakt input[type=button]:hover{filter:brightness(85%)!important}#external-links-on-trakt_buttons_holder button{background-color:var(--red)!important}#external-links-on-trakt .reset{margin-right:10px!important}',
events: {
init: () => {
window.addEventListener('load', () => { // add style
$('head').append('<style>@import url(https://fonts.googleapis.com/css2?family=Montserrat&display=swap);header#top-nav .navbar-nav.navbar-user:hover #user-menu{max-height:max-content}</style>')
})
if (GM.info.scriptHandler !== 'Userscripts') { //! Userscripts Safari: GM.registerMenuCommand is missing
GM.registerMenuCommand('Configure', () => GM_config.open())
}
},
save: () => {
window.alert(`${GM.info.script.name}: settings saved`)
GM_config.close()
window.location.reload(false)
}
}
})
//* NodeCreationObserver
NodeCreationObserver.init(id)
//* Utils
const UU = new UserscriptUtils({
name: GM.info.script.name,
version: GM.info.script.version,
author: GM.info.script.author,
logging: GM_config.get('logging')
})
UU.init(id)
//* Wikidata
const wikidata = new Wikidata({
debug: GM_config.get('debugging')
})
//* Functions
/**
* Adds a link to the menu to access the script configuration
*/
const addSettingsToMenu = () => {
const menu = `<li class=${id}><a href=""onclick=return!1>${GM.info.script.name}</a>`
$('#user-menu ul li.separator').last().after(menu)
$(`.${id}`).click(() => GM_config.open())
}
/**
* Clear old data from the cache
*/
const clearOldCache = async () => {
const values = await GM.listValues()
for (const value of values) {
const cache = await GM.getValue(value) // get cache
if ((Date.now() - cache.time) > 3_600_000) { GM.deleteValue(value) } // delete old cache
}
}
/**
* Returns IMDb ID
*
* @returns {string} IMDb ID
*/
const getID = () => {
return $('#info-wrapper .sidebar .external li a#external-link-imdb').attr('href').match(/tt\d+/)[0]
}
/**
* Returns item type
*
* @returns {string} Item type
*/
const getType = () => {
switch ($('meta[property="og:type"]').attr('content')) {
case 'video.movie':
return 'movie'
case 'video.tv_show':
return 'tv'
default:
break
}
}
/**
* Add external links
*
* @param {object} links Links data
*/
const addLinks = (links) => {
$.each(links, (site, link) => {
if ($(`#info-wrapper .sidebar .external li a#external-link-${site.toLowerCase()}`).length === 0 && link !== undefined && site !== 'Trakt') {
const externalLink = `<a target="_blank" id="external-link-${site.toLowerCase().replace(/\s/g, '_')}" href="${link.value}" data-original-title="" title="">${site}</a>`
$('#info-wrapper .sidebar .external li a:not(:has(i))').last().after(externalLink) // desktop
$('#info-wrapper .info .additional-stats li:last-child a:not([data-site]):not([data-method]):not([data-id])').last().after(externalLink).after(', ') // mobile
}
})
}
//* Script
$(document).ready(() => {
NodeCreationObserver.onCreation('body', () => {
addSettingsToMenu() // link settings to trakt menu
})
NodeCreationObserver.onCreation('.movies.show #info-wrapper .sidebar .external, .shows.show #info-wrapper .sidebar .external', async () => {
clearOldCache() // clear old cache
const id = getID() // get IMDb ID
if (!id) return // check if the ID exists
UU.log(`ID is '${id}'`)
const type = getType() // get ID type
const cache = await GM.getValue(id) // get cache
if (cache !== undefined && ((Date.now() - cache.time) < cachePeriod) && !GM_config.get('debugging')) { // cache valid
console.log(`${id} data from cache`)
addLinks(cache.worldwide) // add external links
if (GM_config.get('italy')) addLinks(cache.italy) // add external links
UU.log(cache.item)
} else { // cache not valid
console.log(`${id} data from Wikidata`)
const data = await wikidata.links(id, 'IMDb', type).then().catch(error => console.error(error))
const item = data.item
const links = data.links
const worldwide = Object.fromEntries(Object.entries(links).filter(([key, value]) => value ? value.country === 'worldwide' : false))
const italy = Object.fromEntries(Object.entries(links).filter(([key, value]) => value ? value.country === 'italy' : false))
GM.setValue(id, { worldwide, italy, item, time: Date.now() }) // set cache
addLinks(worldwide) // add external links
if (GM_config.get('italy')) addLinks(italy) // add external links
UU.log(item)
}
})
})
})()