AnimeWorld Skipper

Salta anime intro in AnimeWorld

Εγκατάσταση αυτού του κώδικαΒοήθεια
Κώδικας προτεινόμενος από τον δημιιουργό

Μπορεί, επίσης, να σας αρέσει ο κώδικας BetterAnimeWorld.

Εγκατάσταση αυτού του κώδικα
// ==UserScript==
// @name         AnimeWorld Skipper
// @namespace    https://github.com/pizidavi
// @icon         https://static.animeworld.so/assets/images/favicon/android-icon-192x192.png
// @description  Salta anime intro in AnimeWorld
// @author       pizidavi
// @version      1.1
// @copyright    2023, PIZIDAVI
// @license      MIT
// @require      https://cdn.jsdelivr.net/gh/soufianesakhi/node-creation-observer-js@edabdee1caaee6af701333a527a0afd95240aa3b/release/node-creation-observer-latest.min.js
// @require      https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.min.js
// @require      https://greasyfork.org/scripts/457460-aniskip/code/AniSkip.js
// @require      https://greasyfork.org/scripts/401626-notify-library/code/Notify%20Library.js
// @match        https://www.animeworld.so/play/*
// @connect      api.aniskip.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_getResourceText
// @run-at       document-body
// ==/UserScript==

/* global GM_config, NodeCreationObserver, AniSkip, Notify */

(function () {
  'use strict';

  //* GM_config
  GM_config.init({
    id: GM.info.script.name.toLowerCase().replace(/\s/g, '-'),
    title: `${GM.info.script.name} v${GM.info.script.version} Settings`,
    fields: {
      AniSkipUserId: {
        label: 'AniSkip User Id',
        section: ['AniSkip', 'Ottieni su: https://www.uuidgenerator.net/'],
        labelPos: 'left',
        type: 'text',
        title: 'Il tuo AniSkip userId',
        size: 70,
        default: ''
      },
      ManualSeekTime: {
        label: 'Secondi saltati con Salto Manuale (s)',
        section: ['Settings', 'Script settings'],
        type: 'int',
        default: 90
      },
      SeekWheel: {
        label: 'Millisecondi saltati con rotella del mouse su input nel Menu (ms)',
        type: 'int',
        default: 50
      },
      SubmittedSkip: {
        label: 'Times Skip inviati',
        section: ['Stats', '(solo lettura)'],
        type: 'int',
        default: 0,
        save: false
      }
    },
    events: {
      init: () => { },
      open: () => {
        GM_config.set('SubmittedSkip', GM_getValue('SubmittedSkip', 0));
      },
      save: () => {
        alert('Impostazioni salvate');
        GM_config.close()
        setTimeout(window.location.reload(false), 500);
      }
    }
  });
  GM_registerMenuCommand('Configure', () => GM_config.open());

  //* Const
  const CSS = `
  /* Skip Button */
  #player { display: flex; }
  .aws-btn {
    display: none;
    position: absolute;
    bottom: 6em;
    right: 2em;
    padding: .5em 1em .4em;
    font-size: 14px;
    border: 1px solid #bdc3c7;
    border-radius: 2px;
    color: #bdc3c7;
    background-color: rgba(0, 0, 0, .7);
    opacity: .35;
    z-index: 2147483648;
  }
  .aws-btn:hover {
    opacity: .75;
  }
  .aws-btn.active {
    display: block;
  }
  /* Menu */
  .aws-container {
    position: absolute;
    top: 1em;
    right: 1em;
    border: none;
  }
  .aws-container summary {
    cursor: pointer;
    color: white;
    background-color: rgba(35, 35, 35, 0.5);
    border-radius: 5px;
    padding: 1px 5px;
    user-select: none;
    opacity: 0.5;
    transition: opacity 0.3s;
  }
  .aws-container[open] summary,
  .aws-container summary:hover {
    opacity: 0.8;
  }
  .aws-container summary::-webkit-details-marker {
    display: none;
  }
  .aws-menu {
    position: absolute;
    right: 0;
    padding: 10px;
    border-radius: 3px;
    color: white;
    background-color: rgba(20, 20, 20, 0.95);
    z-index: 2;
    min-width: 300px;
  }
  .aws-menu h4,
  .aws-menu h5 {
    margin-top: 0;
    margin-bottom: 3px;
  }
  .aws-menu h5 {
    margin-bottom: 0;
  }
  .aws-menu hr {
    margin: 10px 0;
    padding: 0;
  }
  #aws-message {
    text-align: center;
    margin-bottom: 0;
  }
  #aws-reload-list {
    fill: white;
    width: 11px;
    margin-right: 2px;
  }
  .aws-skip-list {
    overflow-y: auto;
    max-height: 250px;
  }
  .aws-skip-list .skip-item {
    display: flex;
    align-items: center;
    padding: 5px;
    border-bottom: 1px solid #555;
  }
  .aws-skip-list .skip-item:last-child {
    border-bottom: none;
  }
  .aws-skip-list .skip-item .icon {
    display: block;
    height: 25px;
    width: 25px;
    margin-right: 10px;
    float: left;
    background-color: #bbb;
    border: 1px solid white;
    border-radius: 50%;
  }
  .aws-skip-list .skip-item .icon.op {
    background-color: green
  }
  .aws-skip-list .skip-item .icon.ed {
    background-color: gold
  }
  .aws-skip-list .skip-item .icon.mixed-op {
    background-color: #90ee90
  }
  .aws-skip-list .skip-item .icon.mixed-ed {
    background-color: #ff0
  }
  .aws-skip-list .skip-item .icon.recap {
    background-color: #add8e6
  }
  .aws-skip-list .skip-item .info .name {
    font-weight: 400;
    font-size: 1.1rem;
  }
  .aws-skip-list .skip-item .info p {
    margin: 0;
    font-size: .9rem;
  }
  .aws-skip-list .skip-item .action {
    display: flex;
    margin-left: auto;
  }
  .aws-skip-list .skip-item .action button {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 25px;
    aspect-ratio: 1;
    margin-right: 4px;
    padding: 0;
    border-radius: 25%;
  }
  .aws-skip-list .skip-item .action button:last-child {
    margin-right: 0;
  }
  .aws-skip-list .skip-item .action button svg {
    width: 12px;
  }
  `;
  const MENU = `
  <details id="aws" class="aws-container">
    <summary title="Apri/Chiudi menu" role="button">AW Skipper</summary>
    <div class="aws-menu">
      <h4>AnimeWorld Skipper</h4>
      <hr />
      <!-- Form -->
      <form>
        <h5>Add Skip Time</h5>
        <select class="form-control" name="aws-skip-type" style="margin-bottom:5px" required>
          <option value hidden selected>Seleziona una tipologia</option>
          <option value="op">Opening</option>
          <option value="ed">Ending</option>
          <option value="mixed-op">Mixed Epening</option>
          <option value="mixed-ed">Mixed Ending</option>
          <option value="recap">Recap</option>
        </select>
        <div class="row">
          <div class="col-sm-4" style="padding-right:0;">
            <input
              type="number"
              class="form-control"
              name="aws-start"
              value="0.000"
              placeholder="Start time"
              step="0.001"
              min="0"
              required
              data-aws-wheel
            >
            <small data-aws-goTo="now" role="button">+Ora</small>
          </div>
          <div class="col-sm-4" style="padding-right:0;">
            <input
              type="number"
              class="form-control"
              name="aws-end"
              value="0.000"
              placeholder="End time"
              step="0.001"
              min="0"
              required
              data-aws-wheel
            >
            <small data-aws-goTo="now" role="button">+Ora</small>
            <small data-aws-goTo="+90" role="button">+90s</small>
            <small data-aws-goTo="end" style="margin-left:auto;" role="button">+Fine</small>
          </div>
          <div class="col-sm-4">
            <button
              class="btn btn-block btn-primary"
              type="submit"
              disabled
            >Salva</button>
          </div>
        </div>
      </form>
      <!-- End form -->
      <hr />
      <!-- Skip list -->
      <h5>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="aws-reload-list" role="button">
          <path d="M463.5 224H472c13.3 0 24-10.7 24-24V72c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8H463.5z" />
        </svg>
        Skip Times
      </h5>
      <div id="aws-skip-list" class="aws-skip-list"><!-- Items --></div>
      <p id="aws-message" style="text-align:center;">Waiting for video metadata</p>
      <!-- End skip list -->
    </div>
  </details>
  `;
  const TEMPLATE_ITEM = `
  <div class="skip-item">
    <span class="icon"></span>
    <div class="info">
      <span class="name"></span>
      <p class="times">
        <span class="time-start" role="button" title="Go to start"></span>
        -
        <span class="time-end" role="button" title="Go to end"></span>
      </p>
    </div>
    <div class="action">
      <button class="btn btn-dark" data-aws-vote="upvote">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
          <path d="M313.4 32.9c26 5.2 42.9 30.5 37.7 56.5l-2.3 11.4c-5.3 26.7-15.1 52.1-28.8 75.2H464c26.5 0 48 21.5 48 48c0 25.3-19.5 46-44.3 47.9c7.7 8.5 12.3 19.8 12.3 32.1c0 23.4-16.8 42.9-38.9 47.1c4.4 7.2 6.9 15.8 6.9 24.9c0 21.3-13.9 39.4-33.1 45.6c.7 3.3 1.1 6.8 1.1 10.4c0 26.5-21.5 48-48 48H294.5c-19 0-37.5-5.6-53.3-16.1l-38.5-25.7C176 420.4 160 390.4 160 358.3V320 272 247.1c0-29.2 13.3-56.7 36-75l7.4-5.9c26.5-21.2 44.6-51 51.2-84.2l2.3-11.4c5.2-26 30.5-42.9 56.5-37.7zM32 192H96c17.7 0 32 14.3 32 32V448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32V224c0-17.7 14.3-32 32-32z" fill="black"></path>
        </svg>
      </button>
      <button class="btn" data-aws-vote="downvote">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
          <path d="M313.4 479.1c26-5.2 42.9-30.5 37.7-56.5l-2.3-11.4c-5.3-26.7-15.1-52.1-28.8-75.2H464c26.5 0 48-21.5 48-48c0-25.3-19.5-46-44.3-47.9c7.7-8.5 12.3-19.8 12.3-32.1c0-23.4-16.8-42.9-38.9-47.1c4.4-7.3 6.9-15.8 6.9-24.9c0-21.3-13.9-39.4-33.1-45.6c.7-3.3 1.1-6.8 1.1-10.4c0-26.5-21.5-48-48-48H294.5c-19 0-37.5 5.6-53.3 16.1L202.7 73.8C176 91.6 160 121.6 160 153.7V192v48 24.9c0 29.2 13.3 56.7 36 75l7.4 5.9c26.5 21.2 44.6 51 51.2 84.2l2.3 11.4c5.2 26 30.5 42.9 56.5 37.7zM32 320H96c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H32C14.3 32 0 46.3 0 64V288c0 17.7 14.3 32 32 32z"/>
        </svg>
      </button>
    </div>
  </div>
  `;

  const aniskip = new AniSkip({
    userId: GM_config.get('AniSkipUserId'),
    providerName: 'AnimeWorld'
  });
  const seekWheel = GM_config.get('SeekWheel', 50) / 1000;

  //* Script
  NodeCreationObserver.init('AnimeWorldSkipper');
  NodeCreationObserver.onCreation('#player iframe', function (iframe) {
    const malId = document.querySelector('#mal-button').getAttribute('href').split('/').at(-1);
    const episodeNumber = document.querySelector('div.server ul a.active').getAttribute('data-base').split('-')[0];

    iframe.addEventListener('load', function () {
      const content = this.contentDocument;
      const video = content?.querySelector('video');
      if (!video) return;

      injectStyle(CSS, content.head);
      const menu = injectHTML(MENU, video.parentNode);
      menu.querySelector('form [name="aws-start"]').onchange = function () {
        this.value = (parseFloat(this.value) || 0).toFixed(3);
        video.currentTime = this.value;
      };
      menu.querySelector('form [name="aws-end"]').onchange = function () {
        this.value = (parseFloat(this.value) || 0).toFixed(3);
        video.currentTime = this.value;
      };

      const loadskipdata = () => {
        const videoLength = video.duration;

        content.querySelectorAll('.aws-btn').forEach(element => {
          element.remove();
        });
        menu.querySelector('#aws-message').textContent = 'Loading...';
        menu.querySelector('#aws-skip-list').innerHTML = '';
        menu.querySelector('#aws-reload-list').onclick = () => {
          video.dispatchEvent(new Event('loadskipdata'));
        };

        menu.querySelector('form [name="aws-start"]').max = videoLength.toFixed(3);
        menu.querySelector('form [name="aws-end"]').max = videoLength.toFixed(3);
        menu.querySelector('form button[type="submit"]').disabled = false;

        menu.querySelectorAll('form [data-aws-goTo]').forEach(element => {
          const action = element.getAttribute('data-aws-goTo');
          element.onclick = function () {
            const value = (action === 'now' ? video.currentTime : action === '+90' ? video.currentTime+90 : videoLength).toFixed(3);
            this.parentNode.querySelector('input').value = value;
            this.parentNode.querySelector('input').dispatchEvent(new Event('change'));
          };
        });
        menu.querySelectorAll('form [data-aws-wheel]').forEach(element => {
          element.onwheel = function (e) {
            e.preventDefault();
            e.stopPropagation();
            let newValue = (parseFloat(this.value) || 0) + (e.deltaY >= 0 ? -seekWheel : seekWheel);
            if (newValue < 0)
              newValue = 0;
            else if (newValue > videoLength)
              newValue = videoLength;
            if (this.value !== newValue.toFixed(3)) {
              this.value = newValue.toFixed(3);
              this.dispatchEvent(new Event('change'));
            }
          };
        });

        aniskip.getSkipTimes(malId, episodeNumber, videoLength)
          .then(data => {
          console.log(data)
          menu.querySelector('#aws-message').textContent = '';

          data.forEach(item => {
            // Add skip-btn to video
            const skipButton = createSkipButton({
              id: item.skipId,
              text: 'Salta ' + aniskip.CategoryTitle[item.skipType]
            });
            skipButton.onclick = () => {
              video.currentTime = item.interval.endTime;
              video.focus();
            };
            skipButton.setAttribute('data-time-start', item.interval.startTime);
            skipButton.setAttribute('data-time-end', item.interval.endTime);
            video.parentNode.appendChild(skipButton);

            // Add skip-item to menu
            const skipItem = injectHTML(TEMPLATE_ITEM, menu.querySelector('#aws-skip-list'));
            skipItem.querySelector('.icon').classList.add(item.skipType);
            skipItem.querySelector('.name').textContent = aniskip.CategoryTitle[item.skipType];

            skipItem.querySelector('.times .time-start').textContent = item.interval.startTime.toFixed(1);
            skipItem.querySelector('.times .time-start').onclick = () => {
              video.currentTime = item.interval.startTime;
            };
            skipItem.querySelector('.times .time-end').textContent = item.interval.endTime.toFixed(1);
            skipItem.querySelector('.times .time-end').onclick = () => {
              video.currentTime = item.interval.endTime;
            };
            skipItem.querySelectorAll('.action [data-aws-vote]').forEach(element => {
              const action = element.getAttribute('data-aws-vote');
              element.onclick = function () {
                this.disabled = true;
                aniskip.vote(action, item.skipId)
                  .then(data => {
                  new Notify({
                    text: data.message || 'Votato!',
                    type: 'success'
                  }).show();
                })
                  .catch(data => {
                  console.error(data)
                  new Notify({
                    text: data.message || 'Errore',
                    type: 'error'
                  }).show();
                })
                  .finally(() => {
                  video.dispatchEvent(new Event('loadskipdata'));
                });
              }
            });
          });
        })
          .catch(response => {
          console.log(response)
          menu.querySelector('#aws-message').textContent = response.message || 'No skip time';

          // Add skip-btn to video
          const skipButton = createSkipButton({
            id: 'manual-skip',
            text: 'Salto Manuale',
            class: 'aws-btn manual active',
          });
          skipButton.onclick = function () {
            const seekTime = GM_config.get('ManualSeekTime', 90);
            const form = menu.querySelector('form');
            form.querySelector('[name="aws-start"]').value = video.currentTime.toFixed(3);
            form.querySelector('[name="aws-end"]').value = (video.currentTime + seekTime).toFixed(3);
            menu.setAttribute('open', true);
            video.pause();

            video.currentTime += seekTime;
            video.focus();
            this.remove();
          };
          video.parentNode.appendChild(skipButton);
        });

        menu.querySelector('form').onsubmit = function (e) {
          e.preventDefault();
          e.stopPropagation();
          const form = this;
          const skipType = form.querySelector('[name="aws-skip-type"]').value;
          const timeStart = parseFloat(form.querySelector('[name="aws-start"]').value);
          const timeEnd = parseFloat(form.querySelector('[name="aws-end"]').value);
          if (
            !skipType ||
            (!timeStart && timeStart !== 0) ||
            (!timeEnd && timeEnd !== 0) ||
            timeStart >= timeEnd ||
            timeStart < 0 ||
            timeEnd > videoLength
          ) {
            alert('Campi non validi');
            return;
          }

          form.querySelector('button[type="submit"]').disabled = true;
          aniskip.createSkipTime(
            malId,
            episodeNumber,
            {
              skipType: skipType,
              startTime: timeStart,
              endTime: timeEnd,
              episodeLength: videoLength
            }
          ).then(data => {
            form.querySelector('button[type="submit"]').disabled = false;
            form.reset();
            GM_setValue('SubmittedSkip', GM_getValue('SubmittedSkip', 0) + 1);
            video.dispatchEvent(new Event('loadskipdata'));
          }).catch(error => console.error(error));
        };
      };

      video.addEventListener('loadskipdata', loadskipdata);
      if (video.readyState > 0) {
        loadskipdata();
        video.play();
      } else
        video.onloadedmetadata = loadskipdata;

      // Show/Hide skipButton base of currentTime
      video.ontimeupdate = () => {
        content.querySelectorAll('.aws-btn:not(.manual)').forEach(element => {
          const startTime = element.getAttribute('data-time-start');
          const endTime = element.getAttribute('data-time-end');
          const active = video.currentTime >= startTime && video.currentTime < endTime;
          element.classList.toggle('active', active);
        });
        if (video.currentTime > video.duration / 3)
          content.querySelector('#manual-skip')?.remove();
      };

      video.oncanplay = function () {
        this.play();
        this.focus();
        this.oncanplay = null; // only at video start
      };

      video.onmouseenter = function () {
        if (!this.paused && !this.ended)
          this.focus();
      };

      content.onkeydown = e => {
        if (e.keyCode === 13) {
          const btn = content.querySelector('.aws-btn.active');
          if (btn) {
            e.preventDefault();
            btn.click();
          }
        }
      };

    }); // end iframe.addEventListener
  }); // end NodeCreationObserver.onCreation

  injectMeta('AnimeWorldSkipper');


  //* Functions
  function createSkipButton(options = {}) {
    return createElement('button', {
      text: options.text ?? 'Salta',
      class: options.class ?? 'aws-btn',
      ...options
    });
  }

  function createElement(tagName, options) {
    const element = document.createElement(tagName);
    element.textContent = options?.text;
    element.id = options?.id ?? '';
    element.classList = options?.class ?? '';
    element.title = options?.title ?? '';
    element.value = options?.value ?? '';
    element.style = options?.style ?? '';
    element.innerHTML = options?.html ?? element.innerHTML;
    return element;
  }

  function injectHTML(html, parent = document.body) {
    const tag = document.createElement('div');
    tag.innerHTML = html;
    return parent.appendChild(tag.childElementCount === 1 ? tag.firstElementChild : tag);
  }

  function injectStyle(style, parent = document.head) {
    const tag = document.createElement('style');
    tag.innerText = style;
    parent.appendChild(tag);
  }

  function injectMeta(name, content = '', parent = document.head) {
    const tag = document.createElement('meta');
    tag.name = name;
    tag.content = content;
    parent.appendChild(tag);
  }

})();