Greasy Fork is available in English.

Stats for Trakt

Adds stats on Trakt

Installer dette script?
Skaberens foreslåede script

Du vil måske også kunne lide Ratings on Trakt

Installer dette script
// ==UserScript==
// @name            Stats for Trakt
// @name:it         Statistiche per Trakt
// @author          Davide <iFelix18@protonmail.com>
// @namespace       https://github.com/iFelix18
// @icon            https://www.google.com/s2/favicons?sz=64&domain=trakt.tv
// @description     Adds stats on Trakt
// @description:it  Aggiunge statistiche a Trakt
// @copyright       2019, Davide (https://github.com/iFelix18)
// @license         MIT
// @version         3.2.0
// @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/greasemonkey/gm4-polyfill@a834d46afcc7d6f6297829876423f58bb14a0d97/gm4-polyfill.min.js
// @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/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/jquery.scrollto@2.1.3/jquery.scrollTo.min.js#sha256-HGSZhocOCEHviq7s3a917LyjMaqXB75C7kLVDqlMfdc=
// @require         https://cdn.jsdelivr.net/npm/gasparesganga-jquery-loading-overlay@2.1.7/dist/loadingoverlay.min.js#sha256-jLFv9iIrIbqKULHpqp/jmePDqi989pKXOcOht3zgRcw=
// @require         https://cdn.jsdelivr.net/npm/chart.js@3.6.0/dist/chart.min.js#sha256-wkfHADWHH09UG8GShuNFDT8rVBiaj2rEYkZuch9eRg8=
// @require         https://cdn.jsdelivr.net/npm/chartjs-plugin-trendline@1.0.0/dist/chartjs-plugin-trendline.min.js#sha256-BsEcRlL6Xhw7pL2HLf/60nO9rt5eg9qyS1ID2XWPVGw=
// @require         https://cdn.jsdelivr.net/npm/progressbar.js@1.1.0/dist/progressbar.min.js#sha256-c83qPqBpH5rEFQvgyTfcLufqoQIFFoqE5B71yeBXhLc=
// @match           *://trakt.tv/*
// @connect         api.trakt.tv
// @compatible      chrome
// @compatible      edge
// @compatible      firefox
// @grant           GM.deleteValue
// @grant           GM.getValue
// @grant           GM.info
// @grant           GM.listValues
// @grant           GM.registerMenuCommand
// @grant           GM.setValue
// @grant           GM.xmlHttpRequest
// @grant           GM_deleteValue
// @grant           GM_getValue
// @grant           GM_info
// @grant           GM_listValues
// @grant           GM_registerMenuCommand
// @grant           GM_setValue
// @grant           GM_xmlhttpRequest
// @run-at          document-start
// @inject-into     page
// ==/UserScript==

/* global $, GM_config, MonkeyUtils, NodeCreationObserver, ProgressBar, 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: ''
      },
      logging: {
        label: 'Logging',
        section: ['Develop'],
        labelPos: 'right',
        type: 'checkbox',
        default: false
      },
      debugging: {
        label: 'Debugging',
        labelPos: 'right',
        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
          }

          MU.log('cache cleared')
          GM_config.close()
        }
      }
    },
    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') === '') {
          window.addEventListener('load', () => GM_config.open())
        }
      },
      save: () => {
        if (GM_config.isOpen && GM_config.get('TraktClientID') === '') {
          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')
  })

  //* Constants
  const cachePeriod = 3_600_000 // 1 hours
  const loading = $('<div>', {
    css: { /* cSpell: disable-next-line */
      'font-family': 'varela round,helvetica neue,Helvetica,Arial,sans-serif',
      'font-size': '14px',
      'text-align': 'center',
      'white-space': 'nowrap'
    },
    class: 'statsLoading',
    text: 'Loading stats...'
  })

  //* Functions
  /**
   * Returns a normalized episodes and season numbers by adding a zero to individual numbers: 1 => 01
   * @param {number} number Episode o season number
   * @returns {number}
   */
  const normalize = (number) => {
    return (number < 10 ? '0' : '') + number
  }

  /**
   * Capitalize first letter
   * @param {string} string
   * @returns {string}
   */
  const capitalizeFirstLetter = (string) => {
    return (string.charAt(0).toUpperCase() + string.slice(1)).trim()
  }

  /**
   * Returns a color
   * @param {number} index Datasets length
   * @returns {string}
   */
  const color = (index) => {
    const colors = [
      'rgb(204, 51, 63)',
      'rgb(0, 160, 176)',
      'rgb(235, 104, 65)',
      'rgb(106, 74, 60)',
      'rgb(237, 201, 81)',
      'rgb(171, 62, 91)',
      'rgb(179, 204, 87)',
      'rgb(239, 116, 111)',
      'rgb(62, 65, 71)',
      'rgb(255, 190, 64)',
      'rgb(123, 59, 59)'
    ]
    return colors[index % colors.length]
  }

  /**
   * Returns Trakt ID
   * @returns {number}
   */
  const getID = () => {
    const type = $('.btn-list[data-type]').data('type')
    const id = $(`.btn-list[data-${type}-id]`).data(`${type}-id`)
    return id
  }

  /**
   * Returns all episodes ratings in a show
   * @param {number} id Trakt ID
   * @returns {Promise}
   */
  const getEpisodesRatings = async (id) => {
    const cache = await GM.getValue(id) // get cache
    let data = []
    let episodesProcessed = 0
    return new Promise((resolve, reject) => {
      if (cache !== undefined && ((Date.now() - cache.time) < cachePeriod)) { // cache valid
        resolve((cache.data))
        MU.log('data from cache')
      } else { // cache not valid
        trakt.showSummary(id).then((response) => { // gets details for a show from Trakt
          const episodesAired = response.aired_episodes
          trakt.seasonSummary(id).then((response) => { // gets all seasons for a show from Trakt
            for (const season of response.map((season) => season).filter((season) => season.number !== 0)) { // for each season
              trakt.seasonsSeason(id, season.number).then((response) => { // gets all episodes for a specific season of a show from Trakt
                for (const episode of response.map((episode) => episode)) { // for each episode
                  trakt.episodeSummary(id, episode.season, episode.number).then((response) => { // gets rating for an episode from Trakt
                    data.push({
                      season: response.season,
                      episode: response.number,
                      first_aired: response.first_aired,
                      title: response.title,
                      rating: response.rating,
                      votes: response.votes
                    })
                    episodesProcessed++
                    if (episodesProcessed === episodesAired) { // got all ratings for all aired episodes
                      data = data.sort((a, b) => new Date(a.first_aired) - new Date(b.first_aired))
                      GM.setValue(id, { data, time: Date.now() }) // set cache
                      resolve(data)
                      MU.log('data from Trakt')
                    }
                  }).catch((error) => MU.error(error))
                }
              }).catch((error) => MU.error(error))
            }
          }).catch((error) => MU.error(error))
        }).catch((error) => MU.error(error))
      }
    })
  }

  /**
   * Returns your people progress
   * @returns {Promise}
   */
  const getPeopleProgress = () => {
    const data = []
    return new Promise((resolve, reject) => {
      $('.tab-links a').each((index, element) => {
        let role = $(element).data('role')
        const items = $(`.posters[data-role="${role}"] .grid-item[data-released!="0"]`).length
        const watched = $(`.posters[data-role="${role}"] .grid-item[data-released!="0"] .watch.selected`).length
        const progress = Math.round(((watched / items) ? (watched / items) : 0) * 100)
        role = capitalizeFirstLetter($(`.tab-links a[data-role="${role}"] h3`).clone().children().remove().end().text())
        data.push({
          role: role,
          items: items,
          watched: watched,
          progress: progress
        })
      })
      resolve(data)
    })
  }

  /**
   * Returns a datasets
   * @param {Object} data Episodes ratings
   * @returns {Array}
   */
  const scatterDatasets = (data) => {
    let datasets = []

    // eslint-disable-next-line unicorn/no-array-reduce, unicorn/prefer-object-from-entries
    data = data.reduce((data, { season, episode, title, rating, votes }, key) => {
      (data[season - 1] = data[season - 1] || []).push({
        x: key,
        y: rating
      })
      return data
    }, {})

    for (const key of Object.keys(data).map((season) => season)) {
      (datasets = datasets || []).push({
        label: `Season ${Number.parseFloat(key) + 1}`,
        data: data[key],
        backgroundColor: color(datasets.length),
        trendlineLinear: {
          style: color(datasets.length),
          lineStyle: 'solid',
          width: 2
        }
      })
    }

    return datasets
  }

  /**
   * Add scatter chart html structure to the page
   */
  const addChartStructure = () => {
    const html = '<div id=stats><div class=row><div class=col-md-12><h2><span><strong>Stats</strong></span></h2><div style=clear:both></div><div class="col-md-12 statsContainer"><canvas id=episodesRatingsChart></canvas></div><div style=clear:both></div></div></div></div>'
    $(html).insertBefore($('#recent-episodes'))
  }

  /**
   * Add progress bar html structure to the page
   */
  const addProgressBarStructure = () => {
    const html = '<div id=stats><div class=row><div><h2><span><strong>Stats</strong></span></h2><div style=clear:both></div><div class="col-lg-8 col-md-7 statsContainer"><div id=peopleProgressBar></div></div><div style=clear:both></div></div></div></div>'
    $(html).insertBefore($('#credits'))
  }

  /**
   * Add stats to sidebar menu
   * @param {number} child
   */
  const addToMenu = (child) => {
    $(`#info-wrapper .sidebar .sections li:nth-child(${child}) a`).parent().after('<li><a href="#stats">Stats</a></li>')
    $('#info-wrapper .sidebar .sections li a[href="#stats"]').click((event) => {
      event.preventDefault()
      $.scrollTo('#stats', 500, {
        offset: -70
      })
    })
  }

  /**
   * Add chart to the page
   * @param {Object} data Episodes ratings
   */
  const addScatterChart = (data) => {
    // eslint-disable-next-line no-unused-vars, no-undef
    const myChart = new Chart($('#episodesRatingsChart'), {
      type: 'scatter',
      data: {
        datasets: scatterDatasets(data)
      },
      options: {
        scales: {
          x: {
            display: true,
            ticks: {
              display: false
            },
            title: {
              display: true,
              text: 'Episode',
              font: { /* cSpell: disable-next-line */
                family: 'varela round, helvetica neue, Helvetica, Arial, sans-serif',
                size: 14,
                weight: 'normal',
                lineHeight: 'normal'
              }
            }
          },
          y: {
            display: true,
            title: {
              display: true,
              text: 'Rating',
              font: { /* cSpell: disable-next-line */
                family: 'varela round, helvetica neue, Helvetica, Arial, sans-serif',
                size: 14,
                weight: 'normal',
                lineHeight: 'normal'
              }
            }
          }
        },
        plugins: {
          title: {
            display: false,
            position: 'top',
            fontSize: 14, /* cSpell: disable-next-line */
            fontFamily: 'varela round, helvetica neue, Helvetica, Arial, sans-serif',
            fontStyle: 'normal',
            padding: 5,
            lineHeight: 'normal',
            text: 'Episodes ratings'
          },
          tooltip: {
            callbacks: {
              label: (context) => {
                const episode = data[context.parsed.x]
                return [`s${normalize(episode.season)}e${normalize(episode.episode)} - ${episode.title}`, '', `Rating: ${episode.rating}`, `Votes:  ${episode.votes}`]
              }
            }
          }
        }
      }
    })
  }

  /**
   * Add progress bar to the page
   * @param {Object} data People progress
   */
  const addProgressBar = (data) => {
    for (const role of data) {
      const progressbar = new ProgressBar.Line('#peopleProgressBar', {
        color: '#ed1c24',
        strokeWidth: 2,
        trailColor: '#530d0d',
        text: {
          style: {
            color: 'inherit',
            margin: '1px 0 5px', /* cSpell: disable-next-line */
            font: '14px varela round, helvetica neue, Helvetica, Arial, sans-serif'
          }
        }
      })
      progressbar.set(role.progress / 100)
      progressbar.setText(`${role.role}: watched ${role.watched} (${role.progress}%) out of a total of ${role.items} released items.`)
    }
  }

  /**
   * Remove progress bar
   */
  const removeProgressBar = () => {
    $('#peopleProgressBar').children().remove()
  }

  //* NodeCreationObserver
  NodeCreationObserver.init('observed-stats')
  NodeCreationObserver.onCreation('.shows.show', () => { // show page
    $(document).ready(() => {
      const id = getID() // Trakt ID
      if (!id) return
      addChartStructure() // add chart structure
      addToMenu(2) // add stats to the menu
      $('.statsContainer').LoadingOverlay('show', { // show loading
        image: '',
        custom: loading
      })
      getEpisodesRatings(id).then((response) => { // get episodes ratings
        $('.statsContainer').LoadingOverlay('hide', true) // hide loading
        addScatterChart(response) // add chart
      }).catch((error) => MU.error(error))
    })
  })
  NodeCreationObserver.onCreation('.people.show', () => { // people page
    $(document).ready(() => {
      addProgressBarStructure() // add progress bar structure
      addToMenu(1) // add stats to the menu
      $('.statsContainer').LoadingOverlay('show', { // show loading
        image: '',
        custom: loading
      })
      getPeopleProgress().then((response) => { // get people progress
        $('.statsContainer').LoadingOverlay('hide', true) // hide loading
        addProgressBar(response) // add progress bar
      }).catch((error) => MU.error(error))
    })
  })
  NodeCreationObserver.onCreation('.people.show #toast-container .toast.toast-success', () => { // people page
    $(document).ready(() => {
      $('.statsContainer').LoadingOverlay('show', { // show loading
        image: '',
        custom: loading
      })
      removeProgressBar()
      getPeopleProgress().then((response) => { // get people progress
        $('.statsContainer').LoadingOverlay('hide', true) // hide loading
        addProgressBar(response) // add progress bar
      }).catch((error) => MU.error(error))
    })
  })
})()