// ==UserScript==
// @name Trakt Search
// @name:it Ricerca Trakt
// @author Davide <iFelix18@protonmail.com>
// @namespace https://github.com/iFelix18
// @icon https://www.google.com/s2/favicons?sz=64&domain=trakt.tv
// @description Shows the results of a search on Trakt
// @description:it Mostra i risultati di una ricerca su Trakt
// @copyright 2021, Davide (https://github.com/iFelix18)
// @license MIT
// @version 1.2.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/gh/iFelix18/Userscripts@7abdd3baa19d3ec6c216587a226171d71a922469/lib/utils/utils.min.js
// @require https://cdn.jsdelivr.net/gh/iFelix18/Userscripts@4ad842f0cfa0e9abdfdf090ed566f696cddd56c6/lib/api/trakt.min.js
// @require https://cdn.jsdelivr.net/gh/iFelix18/Userscripts@5d7b61ed59c1dca1a186d7bb67e40706d70a1b26/lib/api/tmdb.min.js
// @require https://cdn.jsdelivr.net/npm/gm4-polyfill@1.0.1/gm4-polyfill.min.js#sha256-qmLl2Ly0/+2K+HHP76Ul+Wpy1Z41iKtzptPD1Nt8gSk=
// @require https://cdn.jsdelivr.net/npm/node-creation-observer@1.2.0/release/node-creation-observer-latest.js#sha256-OlRWIaZ5LD4UKqMHzIJ8Sc0ctSV2pTIgIvgppQRdNUU=
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js#sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=
// @require https://cdn.jsdelivr.net/npm/handlebars@4.7.7/dist/handlebars.min.js#sha256-ZSnrWNaPzGe8v25yP0S6YaMaDLMTDHC+4mHTw0xydEk=
// @match *://trakt.tv/*
// @connect api.trakt.tv
// @connect api.themoviedb.org
// @compatible chrome
// @compatible edge
// @compatible firefox
// @grant GM.getValue
// @grant GM.info
// @grant GM.registerMenuCommand
// @grant GM.setValue
// @grant GM.xmlHttpRequest
// @grant GM_getValue
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @run-at document-start
// @inject-into page
// ==/UserScript==
/* global $, GM_config, Handlebars, MonkeyUtils, NodeCreationObserver, TMDb, Trakt */
(() => {
//* GM_config
GM_config.init({
id: 'trakt-config',
title: `${GM.info.script.name} v${GM.info.script.version} Settings`,
fields: {
TraktClientID: {
label: 'Trakt Client ID',
section: ['Enter your Trakt Client ID', 'Get one at: https://trakt.tv/oauth/applications/new'],
labelPos: 'left',
type: 'text',
title: 'Your Trakt Client ID',
size: 70,
default: ''
},
TMDbApiKey: {
label: 'TMDb API Key',
section: ['Enter your TMDb API Key', 'Get one at: https://developers.themoviedb.org/3/'],
labelPos: 'left',
type: 'text',
title: 'Your TMDb API Key',
size: 70,
default: ''
},
logging: {
label: 'Logging',
section: ['Develop'],
labelPos: 'right',
type: 'checkbox',
default: false
},
debugging: {
label: 'Debugging',
labelPos: 'right',
type: 'checkbox',
default: false
}
},
css: ':root{--mainBackground:#343433;--background:#282828;--text:#fff}#trakt-config{background-color:var(--mainBackground);color:var(--text)}#trakt-config .section_header{background-color:var(--background);border-bottom:none;border:1px solid var(--background);color:var(--text)}#trakt-config .section_desc{background-color:var(--background);border-top:none;border:1px solid var(--background);color:var(--text)}#trakt-config .reset{color:var(--text)}',
events: {
init: () => {
if (!GM_config.isOpen && (GM_config.get('TraktClientID') === '' | GM_config.get('TMDbApiKey') === '')) {
window.addEventListener('load', () => GM_config.open())
}
},
save: () => {
if (GM_config.isOpen && (GM_config.get('TraktClientID') === '' | GM_config.get('TMDbApiKey') === '')) {
window.alert(`${GM.info.script.name}: check your settings and save`)
} else {
window.alert(`${GM.info.script.name}: settings saved`)
GM_config.close()
window.location.reload(false)
}
}
}
})
GM.registerMenuCommand('Configure', () => GM_config.open())
//* MonkeyUtils
const MU = new MonkeyUtils({
name: GM.info.script.name,
version: GM.info.script.version,
author: GM.info.script.author,
color: '#ed1c24',
logging: GM_config.get('logging')
})
MU.init('trakt-config')
//* Trakt API
const trakt = new Trakt({
clientID: GM_config.get('TraktClientID'),
debug: GM_config.get('debugging')
})
//* TMDb API
const tmdb = new TMDb({
apikey: GM_config.get('TMDbApiKey'),
language: ' ',
debug: GM_config.get('debugging')
})
//* Functions
/**
* Add style
*/
const addStyle = () => {
const css = '<style>#header-search .search-results{background:#333;display:none;max-width:427px}#header-search.open .search-results{display:block}.search-result{border-top:none;border:1px solid #666;display:flex;overflow:hidden;text-decoration:none!important}.search-result:hover{background-color:#222}.search-result-poster{float:left;height:auto;width:37.83333px}.search-result-text{align-items:center;display:flex;min-width:0;padding-left:12px;padding-right:12px}.search-result-type{background-color:#ed1c24;color:#fff;display:inline-block;flex-shrink:0;font-family:proxima nova semibold;font-size:11px;height:auto;margin-right:6px;text-align:center;text-transform:capitalize;width:7ch}.search-result-title{color:#fff;font-family:proxima nova;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-result-year{color:#999;flex-shrink:0;font-family:proxima nova;font-size:11px;margin-left:6px}</style>'
$('head').append(css)
}
/**
* Add template
*/
const addTemplate = () => {
const template = '<div class=search-results></div><script id=results-template type=text/x-handlebars-template>{{#each results}} <a class=search-result href={{link}} target=_self><img alt=poster class=search-result-poster src={{poster}}><div class=search-result-text><span class=search-result-type>{{this.type}} </span><span class=search-result-title>{{this.title}} </span><span class=search-result-year>{{this.year}}</span></div></a>{{/each}}</script>'
$('#header-search').append(template)
}
/**
* Returns id
* @param {object} element
* @param {string} type
* @returns {string}
*/
const id = (element, type) => {
switch (type) {
case 'episode': {
return element.show.ids.tmdb
}
default: {
return element[type].ids.tmdb
}
}
}
/**
* Returns title
* @param {object} element
* @param {string} type
* @returns {string}
*/
const title = (element, type) => {
switch (type) {
case 'episode': {
return `${element.show.title} ${element[type].season}x${element[type].number} "${element[type].title}"`
}
case 'person':
case 'list': {
return element[type].name
}
default: {
return element[type].title
}
}
}
/**
* Returns year
* @param {object} element
* @param {string} type
* @returns {string}
*/
const year = (element, type) => {
switch (type) {
case 'episode': {
return element.show.year
}
case 'list': {
return `${element[type].item_count} items`
}
default: {
return element[type].year
}
}
}
/**
* Returns link
* @param {object} element
* @param {string} type
* @returns {string}
*/
const link = (element, type) => {
switch (type) {
case 'episode': {
return `/shows/${element.show.ids.slug}/seasons/${element[type].season}/episodes/${element[type].number}`
}
case 'person': {
return `/people/${element[type].ids.slug}`
}
case 'list': {
return `/lists/${element[type].ids.trakt}`
}
default: {
return `/${type}s/${element[type].ids.slug}`
}
}
}
/**
* Returns poster link
* @param {object} response
* @returns {string}
*/
const poster = (response) => {
if (response.posters !== undefined && response.posters.length > 0) {
return `https://image.tmdb.org/t/p/w92${response.posters[0].file_path}`
} else if (response.profiles !== undefined && response.profiles.length > 0) {
return `https://image.tmdb.org/t/p/w92${response.profiles[0].file_path}`
} else {
return 'https://trakt.tv/assets/placeholders/thumb/poster-2561df5a41a5cb55c1d4a6f02d6532cf327f175bda97f4f813c18dea3435430c.png'
}
}
/**
* Returns all search results
* @param {string} type Search type
* @param {string} query Text query to search
* @returns {Promise}
*/
const search = (type, query) => {
let data = []
let resultsProcessed = 0
return new Promise((resolve, reject) => {
trakt.search(type, query, 'title').then((response) => {
response = response.slice(0, 6)
const length = response.length
if (length === 0) resolve()
for (const [index, element] of response.map((element) => element).entries()) {
const type = element.type
const score = element.score
const tmdbID = id(element, type)
tmdb.images((type === 'show' || type === 'episode') ? 'tv' : type, tmdbID).then((response) => {
data.push({
type: type,
score: score,
id: tmdbID,
title: title(element, type),
year: year(element, type),
link: link(element, type),
poster: poster(response),
index: index
})
resultsProcessed++
if (resultsProcessed === length) {
data = data.sort((a, b) => a.index - b.index)
resolve(data)
}
}).catch((error) => MU.error(error))
}
}).catch((error) => MU.error(error))
})
}
/**
* Get input
*/
const getInput = () => {
$('#header-search-query').attr('autocomplete', 'off').on('input', () => {
$('.search-results').empty()
const type = $('#header-search-type.shown .title').text().toLowerCase().replace(/(s)\b/g, '').replace(/\s&\s/g, ',').replace(/people/g, 'person').replace(/\s/g, '')
const query = $('#header-search-query.open').val()
if (type === 'user') return
if (query === '') return
search(type, query).then((response) => {
MU.log(response)
const template = Handlebars.compile($('#results-template').html())
const context = { results: response }
const compile = template(context)
$('.search-results').html(compile)
}).catch((error) => MU.error(error))
})
}
//* NodeCreationObserver
NodeCreationObserver.init('observed-search')
NodeCreationObserver.onCreation('#header-search', () => {
addStyle()
addTemplate()
getInput()
})
})()