AnimeWorld Scrobbling

Segna automaticamente gli episodi visualizzati su Trakt.TV

安装此脚本?
作者推荐脚本

您可能也喜欢BetterAnimeWorld

安装此脚本
  1. // ==UserScript==
  2. // @name AnimeWorld Scrobbling
  3. // @namespace https://www.pizidavi.altervista.org/
  4. // @description Segna automaticamente gli episodi visualizzati su Trakt.TV
  5. // @author pizidavi
  6. // @version 1.6.4
  7. // @copyright 2023, PIZIDAVI
  8. // @license MIT
  9. // @homepageURL https://www.pizidavi.altervista.org/AnimeWorldScrobbling/
  10. // @icon https://www.pizidavi.altervista.org/AnimeWorldScrobbling/favicon.png
  11. //
  12. // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.min.js
  13. // @require https://greasyfork.org/scripts/401626-notify-library/code/Notify%20Library.js
  14. // @match https://www.animeworld.ac/play/*
  15. //
  16. // @connect animeworld.ac
  17. // @connect api.trakt.tv
  18. // @connect api.themoviedb.org
  19. //
  20. // @grant GM_info
  21. // @grant GM_getValue
  22. // @grant GM_setValue
  23. // @grant GM_deleteValue
  24. // @grant GM_listValues
  25. // @grant GM_xmlhttpRequest
  26. // @grant GM_registerMenuCommand
  27. // @run-at document-end
  28. // ==/UserScript==
  29.  
  30. /* global GM_config */
  31.  
  32. (function($) {
  33. 'use strict';
  34.  
  35. const AnimeID = getAnimeID();
  36. const Trakt = GM_getValue(AnimeID, {});
  37.  
  38. GM_config.init({
  39. id: 'config',
  40. title: GM.info.script.name+' - Impostazioni',
  41. fields: {
  42. client_id: {
  43. label: 'Client ID',
  44. section: ['<a href="https://www.pizidavi.altervista.org/AnimeWorldScrobbling/#login" target="_blank">https://www.pizidavi.altervista.org/AnimeWorldScrobbling/#login</a>'],
  45. type: 'text',
  46. size: 70,
  47. default: ''
  48. },
  49. access_token: {
  50. label: 'Access Token',
  51. type: 'text',
  52. size: 70,
  53. default: ''
  54. },
  55. expires: {
  56. label: 'Expires Date',
  57. type: 'text',
  58. size: 70,
  59. default: ''
  60. },
  61. helper: {
  62. label: 'Mostra Helper',
  63. type: 'checkbox',
  64. default: true
  65. },
  66. auto_next_episode: {
  67. label: 'Auto Next-Episode',
  68. type: 'checkbox',
  69. default: false
  70. },
  71. save_episode_on_aw: {
  72. label: 'Save episode on Animeworld',
  73. section: ['Login obbligatorio!'],
  74. type: 'checkbox',
  75. default: false
  76. },
  77. delete: {
  78. label: 'Cancella',
  79. type: 'button',
  80. click: () => {
  81. deleteOne(AnimeID);
  82. GM_config.close();
  83. }
  84. }
  85. },
  86. 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;}',
  87. events: {
  88. init: () => {
  89. 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')) ))
  90. window.addEventListener('load', () => GM_config.open())
  91. },
  92. open: () => {
  93. if (new Date() >= new Date(GM_config.get('expires'))) {
  94. GM_config.set('access_token', '');
  95. GM_config.set('expires', '');
  96. }
  97.  
  98. if (!Trakt.slug)
  99. GM_config.fields['delete'].remove();
  100. },
  101. save: () => {
  102. if (!GM_config.get('client_id') || !GM_config.get('access_token') || !GM_config.get('expires')) {
  103. window.alert(GM.info.script.name + ': completa i campi mancanti');
  104. } else {
  105. window.alert(GM.info.script.name + ': salvato');
  106. GM_config.close();
  107. window.location.reload(false);
  108. }
  109. }
  110. }
  111. });
  112. GM_registerMenuCommand('Configure', () => GM_config.open());
  113.  
  114. /* --------------------- */
  115. const CLIENT_ID = GM_config.get('client_id');
  116. const ACCESS_TOKEN = GM_config.get('access_token');
  117. const EXPIRES = GM_config.get('expires');
  118.  
  119. const SHOW_HELPER = GM_config.get('helper');
  120. const AUTO_NEXT_EPISODE = GM_config.get('auto_next_episode');
  121. const SAVE_EPISODE_ON_AW = GM_config.get('save_episode_on_aw');
  122.  
  123. 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; }';
  124. 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>';
  125. 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>';
  126.  
  127. const section = $(TEMPLATE);
  128. $('#body #body-container').append(section);
  129.  
  130. const style = document.createElement('style');
  131. style.innerText = CSS;
  132. document.head.appendChild(style);
  133.  
  134. if(!CLIENT_ID || !ACCESS_TOKEN || !EXPIRES) {
  135. section.find('div.widget-body').html('Dati Trakt mancanti. Segui la <a href="https://www.pizidavi.altervista.org/AnimeWorldScrobbling/" target="_blank">guida</a>');
  136. return; }
  137. if(new Date() >= new Date(EXPIRES)) {
  138. section.find('div.widget-body').html('Access-Token Trakt Scaduto. <a href="https://www.pizidavi.altervista.org/AnimeWorldScrobbling/#login" target="_blank">Aggiorna</a>');
  139. return; }
  140. if(!AnimeID) {
  141. section.find('div.widget-body').html('Errore. AnimeID non trovato');
  142. return; }
  143.  
  144. section.find('input[type="text"]').val(Trakt.slug);
  145. section.find('input[type="number"]').val((Trakt.season || '1'));
  146. if(AUTO_NEXT_EPISODE)
  147. section.find('input[type="checkbox"]').attr('checked', '');
  148. if(SHOW_HELPER)
  149. section.find('#helper').css('display', 'block');
  150.  
  151. section.find('#watched').on('click', function() {
  152. const _this = $(this);
  153. const episodes_element = $('div.server ul a.active');
  154. const episodes = episodes_element.attr('data-base').split('-');
  155. var type = $('#main div.widget.info div.info > div.row > .meta:nth-child(1) dd:nth-child(2)').text().trim();
  156. type = (type == 'Movie' ? 'movies' : 'shows');
  157.  
  158. if(!Trakt.slug || !Trakt.season || !episodes.length) {
  159. new Notify({
  160. text: 'Errore',
  161. type: 'error'
  162. }).show();
  163. return;
  164. }
  165. _this.attr('disabled', '');
  166. _this.find('span.spinner-border').show();
  167.  
  168. if (SAVE_EPISODE_ON_AW)
  169. save_on_aw();
  170.  
  171. var joData = {};
  172. joData[type] = [
  173. {
  174. 'ids': {
  175. 'slug': Trakt.slug,
  176. },
  177. 'seasons': [
  178. {
  179. 'number': parseInt(Trakt.season),
  180. 'episodes': episodes.map(function(value, index) { // More episodes supported
  181. const date = new Date();
  182. date.setMinutes(date.getMinutes() - 21*(episodes.length-1 - index));
  183. return {
  184. 'watched_at': date.toJSON(),
  185. 'number': parseFloat(value)
  186. };
  187. })
  188. }
  189. ]
  190. }
  191. ];
  192.  
  193. request({
  194. method: 'POST',
  195. url: '/sync/history',
  196. data: joData,
  197. done: function(data, status) {
  198. _this.removeAttr('disabled');
  199. _this.find('span.spinner-border').hide();
  200. },
  201. success: function(data) {
  202. if(data.added[(type == 'movies' ? 'movies' : 'episodes')] > 0) {
  203. episodes_element.css('background-color', 'lightseagreen').css('color', '#fff');
  204. new Notify({
  205. text: (type == 'movies' ? 'Film' : 'Episodio')+' '+episodes.join('-')+' salvat'+(episodes.length > 1 ? 'i' : 'o'),
  206. type: 'success'
  207. }).show();
  208.  
  209. if(AUTO_NEXT_EPISODE || section.find('#autoNext').prop('checked')) {
  210. $('#controls > div.prevnext[data-value="next"]').click(); }
  211. if(section.find('#autoNext').prop('indeterminate')) {
  212. section.find('#autoNext').click().click();
  213. $('#controls > div.prevnext[data-value="next"]').click(); }
  214. }
  215. else {
  216. new Notify({
  217. text: 'Errore. '+(type == 'movies' ? 'Film' : 'Episodio')+' non trovato',
  218. type: 'error'
  219. }).show();
  220. }
  221. }
  222. });
  223.  
  224. });
  225.  
  226. section.find('#save-trakt').on('click', function() {
  227. const title = $('#main div.widget.info div.info > div.head h2').text();
  228. const slug = $(this).parent().parent().find('input[type="text"]').val();
  229. const season = $(this).parent().parent().find('input[type="number"]').val();
  230.  
  231. if(slug != '' && season != '') {
  232. Trakt.title = title.trim();
  233. Trakt.slug = slug.trim();
  234. Trakt.season = season;
  235. GM_setValue(AnimeID, Trakt);
  236.  
  237. section.find('button#watched').removeAttr('disabled');
  238.  
  239. new Notify({
  240. text: 'Dati salvati',
  241. type: 'success',
  242. timeout: 3000
  243. }).show();
  244. } else {
  245. new Notify({
  246. text: 'Completa tutti i campi',
  247. type: 'warn'
  248. }).show();
  249. }
  250. });
  251.  
  252. section.find('#autoNext').on('click', function(e) {
  253. const state = $(this).attr('data-state') || 'unchecked';
  254. if (state === 'unchecked') {
  255. $(this).prop('checked', false);
  256. $(this).prop('indeterminate', true);
  257.  
  258. $(this).attr('data-state', 'indeterminate');
  259. }
  260. else if (state === 'indeterminate') {
  261. $(this).prop('checked', true);
  262. $(this).prop('indeterminate', false);
  263.  
  264. $(this).attr('data-state', 'checked');
  265. }
  266. else if (state === 'checked') {
  267. $(this).prop('checked', false);
  268. $(this).prop('indeterminate', false);
  269.  
  270. $(this).attr('data-state', 'unchecked');
  271. }
  272. });
  273.  
  274. $(document).on('click', 'div.userbookmark li:not([data-value="watching"]):not([data-value="advanced"]):not(.divider)', function() {
  275. deleteOne(AnimeID);
  276. });
  277.  
  278.  
  279. if(Trakt.slug == undefined || Trakt.season == undefined) {
  280. section.find('#watched').attr('disabled', '');
  281.  
  282. var type = $('#main div.widget.info div.info > div.row > .meta:nth-child(1) dd:nth-child(2)').text().trim();
  283. if(type == 'Special' || type == 'OVA') {
  284. return; }
  285. type = (type == 'Movie' ? 'movie' : 'show');
  286.  
  287. var title = $('#main div.widget.info div.info > div.head h2').text().replace('(ITA)', '').replace('(TV)', '').trim();
  288. const season = parseInt(title.split(' ').at(-1)) || 1;
  289.  
  290. title = title.replace(season || '', '').trim();
  291.  
  292. request({
  293. method: 'GET',
  294. url: '/search/'+type+'?query='+encodeURI(title),
  295. success: function(data) {
  296. if(!data.length) { return; }
  297.  
  298. section.find('#trakt-results').show();
  299. $.each(data, function(index) {
  300. if(index >= 3) { return; }
  301.  
  302. const item = $(TEMPLATE_ITEM);
  303. item.attr('data-slug', this[this.type].ids.slug);
  304.  
  305. item.find('.info .name').text( (this[this.type].title.length > 45 ? this[this.type].title.substring(0, 45)+'...' : this[this.type].title));
  306. item.find('.info .name').attr('href', 'https://trakt.tv/'+this.type+'s/'+this[this.type].ids.slug);
  307. item.find('.info .name').attr('title', this[this.type].title);
  308. item.find('.info .year').text(this[this.type].year);
  309.  
  310. item.on('click', function(e) {
  311. if(e.target.tagName == 'A') { return; }
  312. const slug = $(this).attr('data-slug');
  313. section.find('input[type="text"]').val(slug);
  314. section.find('input[type="number"]').val(season);
  315. section.find('#save-trakt').click();
  316. });
  317. section.find('#trakt-results').append(item);
  318.  
  319. if(this[this.type].ids.tmdb != null) {
  320. item.attr('data-tmdb-id', this[this.type].ids.tmdb);
  321.  
  322. GM_xmlhttpRequest({
  323. method: 'GET',
  324. url: 'https://api.themoviedb.org/3/'+ (this.type == 'show' ? 'tv' : 'movie') +'/'+ this[this.type].ids.tmdb +'/images?api_key=52a23d06812ad987218e2e41ec6eb79c',
  325. onload: function() {
  326. if (this.readyState === 4 && (this.status === 200 || this.status === 201)) {
  327. const data = JSON.parse(this.responseText);
  328. if(data.posters.length > 0)
  329. 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');
  330. }
  331. }
  332. });
  333. }
  334. });
  335. }
  336. });
  337. }
  338.  
  339. function save_on_aw() {
  340. const CSRF_Token = document.querySelector('#csrf-token').content
  341. const id = document.querySelector('.watchlist-edit-modal[data-id]').getAttribute('data-id');
  342. const episodes = $('div.server ul a.active').attr('data-base').split('-');
  343. const episode = episodes[episodes.length-1];
  344.  
  345. const viewed = document.querySelector('#watchlist-edit-episodes').value;
  346. if (parseInt(viewed) >= parseInt(episode)) return;
  347.  
  348. const max = document.querySelector('#watchlist-edit-episodes').getAttribute('max');
  349. if (viewed == max) return;
  350.  
  351. const folder = document.querySelector('#watchlist-edit-folder').options[document.querySelector('#watchlist-edit-folder').options.selectedIndex].value;
  352. const rewatches = document.querySelector('#watchlist-edit-rewatches').value;
  353. const note = document.querySelector('#watchlist-edit-notes').value;
  354. const score = document.querySelector('#watchlist-edit-score').value;
  355.  
  356. var options = {};
  357. options.method = 'POST';
  358. options.url = '/api/watchlist/edit/'+id;
  359. options.data = 'folder='+folder+'&episodes='+episode+'&rewatches='+rewatches+'&notes='+note+'&vote='+score;
  360. options.headers = {
  361. 'Accept': 'application/json, text/javascript; q=0.01',
  362. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  363. 'referer': location.href,
  364. 'CSRF-Token': CSRF_Token
  365. };
  366. options.onload = function() {
  367. if (this.readyState === 4 && (this.status === 200 || this.status === 201)) {
  368. const response = JSON.parse(this.responseText);
  369. if (response.error === false) {
  370. document.querySelector('#watchlist-edit-episodes').value = episode;
  371. } else {
  372. console.error(GM.info.script.name, response);
  373. new Notify({
  374. text: 'Errore con AW',
  375. type: 'error'
  376. }).show();
  377. }
  378. } else if (this.readyState === 4) {
  379. console.error(GM.info.script.name, this.responseText);
  380. new Notify({
  381. text: 'Errore nella richiesta su AW',
  382. type: 'error'
  383. }).show();
  384. }
  385. }
  386. GM_xmlhttpRequest(options);
  387. }
  388.  
  389.  
  390. // Functions
  391. function request(options) {
  392. options.url = 'https://api.trakt.tv'+options.url;
  393. options.headers = {
  394. 'Content-Type': 'application/json',
  395. 'Authorization': 'Bearer '+ACCESS_TOKEN,
  396. 'trakt-api-version': '2',
  397. 'trakt-api-key': CLIENT_ID
  398. };
  399. options.data = JSON.stringify(options.data);
  400. options.onload = function() {
  401. if (this.readyState === 4) {
  402. if (typeof options.done == 'function') {
  403. options.done(this.responseText, this.status);
  404. }
  405.  
  406. if (this.status === 200 || this.status === 201) {
  407. options.success(JSON.parse(this.responseText));
  408. }
  409. else if (this.status === 403) {
  410. new Notify({
  411. text: 'Errore. API Key invalida',
  412. type: 'error',
  413. timeout: false
  414. }).show();
  415. }
  416. else if (this.status === 404) {
  417. new Notify({
  418. text: 'Errore. Elemento non trovato',
  419. type: 'error',
  420. timeout: false
  421. }).show();
  422. }
  423. else if (this.status >= 500 && this.status <= 522) {
  424. new Notify({
  425. text: 'Errore. Service Unavailable',
  426. type: 'error',
  427. timeout: false
  428. }).show();
  429. }
  430. else {
  431. new Notify({
  432. text: 'Errore nella richiesta. Ricarica la pagina',
  433. type: 'error',
  434. timeout: false
  435. }).show();
  436. console.error(GM.info.script.name, this.responseText);
  437. }
  438. }
  439. }
  440. GM_xmlhttpRequest(options);
  441. }
  442.  
  443. function getAnimeID() {
  444. const url = location.pathname;
  445. const start = url.indexOf('.')+1;
  446. const end = start + (url.substring(start).indexOf('/') >= 0 ? url.substring(start).indexOf('/') : url.substring(start).length);
  447. return url.substring(start, end) || undefined;
  448. }
  449.  
  450. function deleteOne(key) {
  451. if(GM_listValues().includes(key)) {
  452. GM_deleteValue(key);
  453.  
  454. section.find('input[type="text"]').val('');
  455. section.find('input[type="number"]').val('1');
  456. section.find('button#watched').attr('disabled', '');
  457.  
  458. Trakt.title = null;
  459. Trakt.slug = null;
  460. Trakt.season = null;
  461.  
  462. new Notify({
  463. text: 'Dati Trakt rimossi',
  464. type: 'success',
  465. timeout: 3000
  466. }).show();
  467.  
  468. return true;
  469. } else {
  470. return false;
  471. }
  472. }
  473. function deleteAll() {
  474. GM_listValues().forEach(function(key) {
  475. GM_deleteValue(key);
  476. });
  477. return true;
  478. }
  479.  
  480. })(jQuery);