AnimeWorld Scrobbling

Segna automaticamente gli episodi visualizzati su Trakt.TV

Install this script?
Author's suggested script

You may also like BetterAnimeWorld.

Install this script
// ==UserScript==
// @name         AnimeWorld Scrobbling
// @namespace    https://www.pizidavi.altervista.org/
// @description  Segna automaticamente gli episodi visualizzati su Trakt.TV
// @author       pizidavi
// @version      1.6.3
// @copyright    2023, PIZIDAVI
// @license      MIT
// @homepageURL  https://www.pizidavi.altervista.org/AnimeWorldScrobbling/
// @icon         https://www.pizidavi.altervista.org/AnimeWorldScrobbling/favicon.png
//
// @require      https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.min.js
// @require      https://greasyfork.org/scripts/401626-notify-library/code/Notify%20Library.js
// @match        https://www.animeworld.so/play/*
//
// @connect      animeworld.so
// @connect      api.trakt.tv
// @connect      api.themoviedb.org
//
// @grant        GM_info
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @run-at       document-end
// ==/UserScript==

/* global GM_config */

(function($) {
  'use strict';

  const AnimeID = getAnimeID();
  const Trakt = GM_getValue(AnimeID, {});

  GM_config.init({
    id: 'config',
    title: GM.info.script.name+' - Impostazioni',
    fields: {
      client_id: {
        label: 'Client ID',
        section: ['<a href="https://www.pizidavi.altervista.org/AnimeWorldScrobbling/#login" target="_blank">https://www.pizidavi.altervista.org/AnimeWorldScrobbling/#login</a>'],
        type: 'text',
        size: 70,
        default: ''
      },
      access_token: {
        label: 'Access Token',
        type: 'text',
        size: 70,
        default: ''
      },
      expires: {
        label: 'Expires Date',
        type: 'text',
        size: 70,
        default: ''
      },
      helper: {
        label: 'Mostra Helper',
        type: 'checkbox',
        default: true
      },
      auto_next_episode: {
        label: 'Auto Next-Episode',
        type: 'checkbox',
        default: false
      },
      save_episode_on_aw: {
        label: 'Save episode on Animeworld',
        section: ['Login obbligatorio!'],
        type: 'checkbox',
        default: false
      },
      delete: {
        label: 'Cancella',
        type: 'button',
        click: () => {
          deleteOne(AnimeID);
          GM_config.close();
        }
      }
    },
    css: '#config{background-color:#343434;color:#fff} #config_header{margin-bottom:0.5em!important;margin-top:0.5em!important;} #config_delete_var{margin-top:2em !important;text-align:center;} #config .section_header{background-color:#282828;border:1px solid #282828;border-bottom:none;color:#fff;font-size:10pt}#config .section_desc{background-color:#282828;border:1px solid #282828;border-top:none;color:#fff;font-size:10pt}#config .reset{color:#fff}#config a{color:#fff}#config .section_header{margin-bottom: 1em;}',
    events: {
      init: () => {
        if (!GM_config.isOpen && (!GM_config.get('client_id') || !GM_config.get('access_token') || !GM_config.get('expires') || new Date() >= new Date(GM_config.get('expires')) ))
          window.addEventListener('load', () => GM_config.open())
      },
      open: () => {
        if (new Date() >= new Date(GM_config.get('expires'))) {
          GM_config.set('access_token', '');
          GM_config.set('expires', '');
        }

        if (!Trakt.slug)
          GM_config.fields['delete'].remove();
      },
      save: () => {
        if (!GM_config.get('client_id') || !GM_config.get('access_token') || !GM_config.get('expires')) {
          window.alert(GM.info.script.name + ': completa i campi mancanti');
        } else {
          window.alert(GM.info.script.name + ': salvato');
          GM_config.close();
          window.location.reload(false);
        }
      }
    }
  });
  GM_registerMenuCommand('Configure', () => GM_config.open());

  /* --------------------- */
  const CLIENT_ID = GM_config.get('client_id');
  const ACCESS_TOKEN = GM_config.get('access_token');
  const EXPIRES = GM_config.get('expires');

  const SHOW_HELPER = GM_config.get('helper');
  const AUTO_NEXT_EPISODE = GM_config.get('auto_next_episode');
  const SAVE_EPISODE_ON_AW = GM_config.get('save_episode_on_aw');

  const CSS = '#body .sidebar { float: right; width: 300px; position: relative; z-index: 1; } #trakt-results .item .info a.name::after { font-family: "Font Awesome 5 Free"; font-size: 9px; font-weight: 900; content: "\\f35d"; margin-left: 5px; vertical-align: super; }';
  const TEMPLATE = '<div id="trakt" class="sidebar"><div class="widget simple-film-list"><div class="widget-title"><div class="title">Trakt.TV</div></div><div class="widget-body"><div class="row mb-3"><div class="col-sm-10" style="padding-right: 0;"><button class="btn btn-primary btn-block" id="watched">Guardato<span class="spinner-border spinner-border-sm ml-2" role="status" style="display:none;"></span></button></div><div class="col-sm-2"><input type="checkbox" id="autoNext" style="margin-top: 8px;" title="Prossimo episodio automatico"></div></div><div class="row"><div class="col-sm-6" style=" padding-right: 0;"><input type="text" class="form-control" placeholder="Trakt Slug"><small id="helper" style="display:none;margin:0.3em 0px -5px 0.5em;"><a href="https://www.pizidavi.altervista.org/AnimeWorldScrobbling/#trakt" target="_blank" style="color:grey;">Dove lo trovo?</a></small></div><div class="col-sm-2" style=" padding-right: 0;"><input type="number" class="form-control" value="1" min="0" placeholder="Season"></div><div class="col-sm-4"><button id="save-trakt" class="btn btn-success btn-block">Salva</button></div></div><div id="trakt-results" class="mt-3" style="display:none;"><hr class="my-3"/><h5 class="mb-1">Risultati di ricerca su Trakt</h5></div></div></div></div>';
  const TEMPLATE_ITEM = '<div class="item" role="button" title="Click per selezionare questo risultato"><img src="#" class="thumb" style="opacity:0;"><div class="info"><a class="name" href="#" target="_blank"></a><p class="year mb-0"></p></div></div>';

  const section = $(TEMPLATE);
  $('#body #body-container').append(section);

  const style = document.createElement('style');
  style.innerText = CSS;
  document.head.appendChild(style);

  if(!CLIENT_ID || !ACCESS_TOKEN || !EXPIRES) {
    section.find('div.widget-body').html('Dati Trakt mancanti. Segui la <a href="https://www.pizidavi.altervista.org/AnimeWorldScrobbling/" target="_blank">guida</a>');
    return; }
  if(new Date() >= new Date(EXPIRES)) {
    section.find('div.widget-body').html('Access-Token Trakt Scaduto. <a href="https://www.pizidavi.altervista.org/AnimeWorldScrobbling/#login" target="_blank">Aggiorna</a>');
    return; }
  if(!AnimeID) {
    section.find('div.widget-body').html('Errore. AnimeID non trovato');
    return; }

  section.find('input[type="text"]').val(Trakt.slug);
  section.find('input[type="number"]').val((Trakt.season || '1'));
  if(AUTO_NEXT_EPISODE)
    section.find('input[type="checkbox"]').attr('checked', '');
  if(SHOW_HELPER)
    section.find('#helper').css('display', 'block');

  section.find('#watched').on('click', function() {
    const _this = $(this);
    const episodes_element = $('div.server ul a.active');
    const episodes = episodes_element.attr('data-base').split('-');
    var type = $('#main div.widget.info div.info > div.row > .meta:nth-child(1) dd:nth-child(2)').text().trim();
    type = (type == 'Movie' ? 'movies' : 'shows');

    if(!Trakt.slug || !Trakt.season || !episodes.length) {
      new Notify({
        text: 'Errore',
        type: 'error'
      }).show();
      return;
    }
    _this.attr('disabled', '');
    _this.find('span.spinner-border').show();

    if (SAVE_EPISODE_ON_AW)
      save_on_aw();

    var joData = {};
    joData[type] = [
      {
        'ids': {
          'slug': Trakt.slug,
        },
        'seasons': [
          {
            'number': parseInt(Trakt.season),
            'episodes': episodes.map(function(value, index) { // More episodes supported
              const date = new Date();
              date.setMinutes(date.getMinutes() - 21*(episodes.length-1 - index));
              return {
                'watched_at': date.toJSON(),
                'number': parseFloat(value)
              };
            })
          }
        ]
      }
    ];

    request({
      method: 'POST',
      url: '/sync/history',
      data: joData,
      done: function(data, status) {
        _this.removeAttr('disabled');
        _this.find('span.spinner-border').hide();
      },
      success: function(data) {
        if(data.added[(type == 'movies' ? 'movies' : 'episodes')] > 0) {
          episodes_element.css('background-color', 'lightseagreen').css('color', '#fff');
          new Notify({
            text: (type == 'movies' ? 'Film' : 'Episodio')+' '+episodes.join('-')+' salvat'+(episodes.length > 1 ? 'i' : 'o'),
            type: 'success'
          }).show();

          if(AUTO_NEXT_EPISODE || section.find('#autoNext').prop('checked')) {
            $('#controls > div.prevnext[data-value="next"]').click(); }
          if(section.find('#autoNext').prop('indeterminate')) {
            section.find('#autoNext').click().click();
            $('#controls > div.prevnext[data-value="next"]').click(); }
        }
        else {
          new Notify({
            text: 'Errore. '+(type == 'movies' ? 'Film' : 'Episodio')+' non trovato',
            type: 'error'
          }).show();
        }
      }
    });

  });

  section.find('#save-trakt').on('click', function() {
    const title = $('#main div.widget.info div.info > div.head h2').text();
    const slug = $(this).parent().parent().find('input[type="text"]').val();
    const season = $(this).parent().parent().find('input[type="number"]').val();

    if(slug != '' && season != '') {
      Trakt.title = title.trim();
      Trakt.slug = slug.trim();
      Trakt.season = season;
      GM_setValue(AnimeID, Trakt);

      section.find('button#watched').removeAttr('disabled');

      new Notify({
        text: 'Dati salvati',
        type: 'success',
        timeout: 3000
      }).show();
    } else {
      new Notify({
        text: 'Completa tutti i campi',
        type: 'warn'
      }).show();
    }
  });

  section.find('#autoNext').on('click', function(e) {
    const state = $(this).attr('data-state') || 'unchecked';
    if (state === 'unchecked') {
      $(this).prop('checked', false);
      $(this).prop('indeterminate', true);

      $(this).attr('data-state', 'indeterminate');
    }
    else if (state === 'indeterminate') {
      $(this).prop('checked', true);
      $(this).prop('indeterminate', false);

      $(this).attr('data-state', 'checked');
    }
    else if (state === 'checked') {
      $(this).prop('checked', false);
      $(this).prop('indeterminate', false);

      $(this).attr('data-state', 'unchecked');
    }
  });

  $(document).on('click', 'div.userbookmark li:not([data-value="watching"]):not([data-value="advanced"]):not(.divider)', function() {
    deleteOne(AnimeID);
  });


  if(Trakt.slug == undefined || Trakt.season == undefined) {
    section.find('#watched').attr('disabled', '');

    var type = $('#main div.widget.info div.info > div.row > .meta:nth-child(1) dd:nth-child(2)').text().trim();
    if(type == 'Special' || type == 'OVA') {
      return; }
    type = (type == 'Movie' ? 'movie' : 'show');

    var title = $('#main div.widget.info div.info > div.head h2').text().replace('(ITA)', '').replace('(TV)', '').trim();
    const season = parseInt(title.split(' ').at(-1)) || 1;

    title = title.replace(season || '', '').trim();

    request({
      method: 'GET',
      url: '/search/'+type+'?query='+encodeURI(title),
      success: function(data) {
        if(!data.length) { return; }

        section.find('#trakt-results').show();
        $.each(data, function(index) {
          if(index >= 3) { return; }

          const item = $(TEMPLATE_ITEM);
          item.attr('data-slug', this[this.type].ids.slug);

          item.find('.info .name').text( (this[this.type].title.length > 45 ? this[this.type].title.substring(0, 45)+'...' : this[this.type].title));
          item.find('.info .name').attr('href', 'https://trakt.tv/'+this.type+'s/'+this[this.type].ids.slug);
          item.find('.info .name').attr('title', this[this.type].title);
          item.find('.info .year').text(this[this.type].year);

          item.on('click', function(e) {
            if(e.target.tagName == 'A') { return; }
            const slug = $(this).attr('data-slug');
            section.find('input[type="text"]').val(slug);
            section.find('input[type="number"]').val(season);
            section.find('#save-trakt').click();
          });
          section.find('#trakt-results').append(item);

          if(this[this.type].ids.tmdb != null) {
            item.attr('data-tmdb-id', this[this.type].ids.tmdb);

            GM_xmlhttpRequest({
              method: 'GET',
              url: 'https://api.themoviedb.org/3/'+ (this.type == 'show' ? 'tv' : 'movie') +'/'+ this[this.type].ids.tmdb +'/images?api_key=52a23d06812ad987218e2e41ec6eb79c',
              onload: function() {
                if (this.readyState === 4 && (this.status === 200 || this.status === 201)) {
                  const data = JSON.parse(this.responseText);
                  if(data.posters.length > 0)
                    section.find('#trakt-results').find('[data-tmdb-id="'+data.id+'"] > img').attr('src', 'https://image.tmdb.org/t/p/w92'+data.posters[0].file_path).css('opacity', '1');
                }
              }
            });
          }
        });
      }
    });
  }

  function save_on_aw() {
    const CSRF_Token = document.querySelector('#csrf-token').content
    const id = document.querySelector('.watchlist-edit-modal[data-id]').getAttribute('data-id');
    const episodes = $('div.server ul a.active').attr('data-base').split('-');
    const episode = episodes[episodes.length-1];

    const viewed = document.querySelector('#watchlist-edit-episodes').value;
    if (parseInt(viewed) >= parseInt(episode)) return;

    const max = document.querySelector('#watchlist-edit-episodes').getAttribute('max');
    if (viewed == max) return;

    const folder = document.querySelector('#watchlist-edit-folder').options[document.querySelector('#watchlist-edit-folder').options.selectedIndex].value;
    const rewatches = document.querySelector('#watchlist-edit-rewatches').value;
    const note = document.querySelector('#watchlist-edit-notes').value;
    const score = document.querySelector('#watchlist-edit-score').value;

    var options = {};
    options.method = 'POST';
    options.url = '/api/watchlist/edit/'+id;
    options.data = 'folder='+folder+'&episodes='+episode+'&rewatches='+rewatches+'&notes='+note+'&vote='+score;
    options.headers = {
      'Accept': 'application/json, text/javascript; q=0.01',
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
      'referer': location.href,
      'CSRF-Token': CSRF_Token
    };
    options.onload = function() {
      if (this.readyState === 4 && (this.status === 200 || this.status === 201)) {
        const response = JSON.parse(this.responseText);
        if (response.error === false) {
          document.querySelector('#watchlist-edit-episodes').value = episode;
        } else {
          console.error(GM.info.script.name, response);
          new Notify({
            text: 'Errore con AW',
            type: 'error'
          }).show();
        }
      } else if (this.readyState === 4) {
        console.error(GM.info.script.name, this.responseText);
        new Notify({
          text: 'Errore nella richiesta su AW',
          type: 'error'
        }).show();
      }
    }
    GM_xmlhttpRequest(options);
  }


  // Functions
  function request(options) {
    options.url = 'https://api.trakt.tv'+options.url;
    options.headers = {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer '+ACCESS_TOKEN,
      'trakt-api-version': '2',
      'trakt-api-key': CLIENT_ID
    };
    options.data = JSON.stringify(options.data);
    options.onload = function() {
      if (this.readyState === 4) {
        if (typeof options.done == 'function') {
          options.done(this.responseText, this.status);
        }

        if (this.status === 200 || this.status === 201) {
          options.success(JSON.parse(this.responseText));
        }
        else if (this.status === 403) {
          new Notify({
            text: 'Errore. API Key invalida',
            type: 'error',
            timeout: false
          }).show();
        }
        else if (this.status === 404) {
          new Notify({
            text: 'Errore. Elemento non trovato',
            type: 'error',
            timeout: false
          }).show();
        }
        else if (this.status >= 500 && this.status <= 522) {
          new Notify({
            text: 'Errore. Service Unavailable',
            type: 'error',
            timeout: false
          }).show();
        }
        else {
          new Notify({
            text: 'Errore nella richiesta. Ricarica la pagina',
            type: 'error',
            timeout: false
          }).show();
          console.error(GM.info.script.name, this.responseText);
        }
      }
    }
    GM_xmlhttpRequest(options);
  }

  function getAnimeID() {
    const url = location.pathname;
    const start = url.indexOf('.')+1;
    const end = start + (url.substring(start).indexOf('/') >= 0 ? url.substring(start).indexOf('/') : url.substring(start).length);
    return url.substring(start, end) || undefined;
  }

  function deleteOne(key) {
    if(GM_listValues().includes(key)) {
      GM_deleteValue(key);

      section.find('input[type="text"]').val('');
      section.find('input[type="number"]').val('1');
      section.find('button#watched').attr('disabled', '');

      Trakt.title = null;
      Trakt.slug = null;
      Trakt.season = null;

      new Notify({
        text: 'Dati Trakt rimossi',
        type: 'success',
        timeout: 3000
      }).show();

      return true;
    } else {
      return false;
    }
  }
  function deleteAll() {
    GM_listValues().forEach(function(key) {
      GM_deleteValue(key);
    });
    return true;
  }

})(jQuery);