kydokgetnada.com / 4anime

// ==UserScript==
// @name 4anime
// @namespace Violentmonkey Scripts
// @match https://4anime.to/*
// @grant none
// @license MIT
// @require https://cdn.jsdelivr.net/npm/@nano-sql/core@2.3.7/dist/nano-sql.min.js
// @require https://cdn.jsdelivr.net/npm/fasy
// @require https://cdn.jsdelivr.net/gh/kenwheeler/slick@1.8.1/slick/slick.min.js
// ==/UserScript==

var table = '4animeTo';
const validLocations = Object.freeze({
  home: 'home',
  anime: 'anime',
  episode: 'episode',
  nowhere: 'nowhere'
});

function whereAmI() {
  if (window.location.pathname == '/') {
    return validLocations.home;
  } else if (window.location.pathname.split('/')[1] == "anime") {
    return validLocations.anime;
  } else if (document.getElementsByTagName('video').length > 0) {
    return validLocations.episode;
  } else {
    return validLocations.nowhere;
  }
}


async function getAllAnimes() {
  var promise = await new Promise((resolve, reject) => {
      nSQL(table).query("select").exec().then((rows) => {
        resolve(rows);
      }).catch((err) => {
        throw err;
      })
    })
    .catch(err => {
      throw err
    });

  return promise;
  //return nSQL(table).query("select").exec();
}

async function getAnime(animeSlug) {
  var promise = await new Promise((resolve, reject) => {
      nSQL(table).query("select").where(["animeSlug", "=", animeSlug]).exec().then((rows) => {
        // console.log(rows);
        resolve(rows[0])
      }).catch((err) => {
        throw err;
      });
    })
    .catch(err => {
      throw err
    });
  return promise;
}

async function doesAnimeExistinDB(animeSlug) {
  var promise = await new Promise((resolve, reject) => {
      nSQL(table).query("select").where(["animeSlug", "=", animeSlug]).exec().then((rows) => {
        if (rows.length > 0) {
          resolve(true);
        } else {
          resolve(false);
        }
      }).catch((err) => {
        throw err;
      })
    })
    .catch(err => {
      throw err
    });

  return promise;
}

async function setData(animeSlug, episodeID, lastWatchedEpisode, lastWatchedEpisodeTime, episodeURL, animeTitle, animePosterURL) {
  console.log(animeTitle);
  var animeExists = await doesAnimeExistinDB(animeSlug);
  if (!animeExists) {
    fetch(document.querySelector('#titleleft').href).then(r=>r.text()).
    then(html=>{
        var doc = new DOMParser().parseFromString(html, "text/html");
        var animePosterURL = doc.querySelector("#details img").src;
        nSQL(table).query("upsert", {
          animeSlug,
          episodeID,
          lastWatchedEpisode,
          lastWatchedEpisodeTime,
          episodeURL,
          animePosterURL
        }).exec().catch((err) => {
          console.log(err)
        });
    });
  } else {
    var anime = await getAnime(animeSlug);
    var animeID = anime.id;
    nSQL(table)
      .query("upsert", {
        id:animeID,
        animeSlug,
        episodeID,
        lastWatchedEpisode,
        lastWatchedEpisodeTime,
        episodeURL,
        animeTitle
      })
      .exec();
  }
}

function insertAfter(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
function createBtn(text){
    var btn = document.createElement("BUTTON");
    btn.style.backgroundColor='#221f1f';
    btn.style.border='none';
    btn.style.color='#747474';
    btn.style.padding='20px';
    btn.style.marginTop='20px';
    btn.style.borderRadius='.25em';
    btn.style.cursor='pointer';
    btn.onmouseover = function() {
      this.style.backgroundColor = "#444040";
    }
    btn.onmouseout = function() {
      this.style.backgroundColor = "#221f1f";
    }
    btn.innerHTML = text;
    insertAfter(btn, document.querySelector('#description-mob p'));
    return btn;
}

async function asyncForEach(array, callback) {
  for (let index = 0; index < array.length; index++) {
    await callback(array[index], index, array);
  }
}

const wrapAll = (target, wrapper = document.createElement('div')) => {
  ;
  [...target.childNodes].forEach(child => wrapper.appendChild(child))
  target.appendChild(wrapper)
  return wrapper
}

async function start() {
  var currentLocation = whereAmI();
  
  if (currentLocation == validLocations.episode) {
    // figure out what anime is playing
    var animeSlug = document.querySelector("meta[property='og:url']").content.split("/")[3].split('-episode')[0];
    // what ep
    var urlParams = new URLSearchParams(document.querySelector("meta[property='og:url']").content.split("/").slice(-1)[0]);
    var episodeID = urlParams.get('id');
    var episodeNumber = document.querySelector('.active .active').innerHTML;
    // what time
    var videoTime;
    var videoElement = document.querySelector('video');
    
    //if currently playing anime exists and lastWatchedEpisode matches current episode resume
    var anime = await getAnime(animeSlug);
    if(anime) {
      var {lastWatchedEpisode} = anime;
      //resume
      if(lastWatchedEpisode == episodeNumber) {
        videoElement.currentTime = anime.lastWatchedEpisodeTime;
      }
    }
    
    window.setInterval(function() {
      if (videoElement.playing) {
        videoTime = videoElement.currentTime;
        setData(animeSlug, episodeID, episodeNumber, videoTime, window.location.href, document.querySelector("meta[property='og:title']").content.split('Epsiode')[0].trim());
      }
    }, 1000);
    
  } else if(currentLocation == validLocations.anime) {
    var animeSlug = document.querySelector("meta[property='og:url']").content.split("/").slice(-1)[0];
    var anime = await getAnime(animeSlug);
    //anime.episodeURL
    if(anime) {
      var btn = createBtn("Continue watching");
      var url = anime.episodeURL;
    } else {
      var btn = createBtn("Start Watching");
      var url = document.querySelector('#servers li:nth-child(1) a').href;
    }
    btn.addEventListener("click", ()=>{
      window.location.href = url;
    }); 
  } else if(currentLocation == validLocations.home) {
    var animes = await getAllAnimes();    
    
    var wrapper= document.createElement('div');
    
    insertAfter(wrapper, document.querySelector('.billboard'));
    var animeE= document.createElement('div');
    var content = `<div style="width:1174px; margin:auto;"> <a>Continue Watching</a> <div style="display:flex;flex-wrap: wrap; margin-top:10px;">
                  <style>
                    .cw:nth-child(7n+1){margin-right:0 !important}
                  </style>`;
    console.log(animeE.innerHTML);
    animes.forEach((anime, i) => {
      var marginLeft = '15px';
      content=  `
        ${content}
        <div class="cw" style="display:flex; flex-direction: column; width:154.717px; margin-right:${marginLeft}; padding-bottom:10px; white-space: nowrap; overflow: hidden;">
          <a href="${anime.episodeURL}" style="height:219.25px;">
            <img src="${anime.animePosterURL}" alt="" style="object-fit:cover; width: 100%; height: 100%">
            <center>
              <a href="${anime.episodeURL}">
                ${anime.animeTitle}
              </a>
            </center>
            <center>
              <a href="${anime.episodeURL}" style="color: #888888;">
                Episode ${anime.lastWatchedEpisode}
              </a>
            </center>
          </a>
        </div>
      `;
      
    });
    
    content = `${content} </div></div>`;
    animeE.innerHTML = content;
    
    insertAfter(animeE, wrapper);
  }
}

nSQL().createDatabase({
  id: "4Anime",
  mode: "PERM",
  tables: [{
    name: table,
    model: {
      "id:uuid": {
        pk: true
      },
      "animeSlug:string": {},
      "animeTitle:string": {},
      "episodeID:int": {},
      "episodeURL:string": {},
      "lastWatchedEpisode:int": {},
      "lastWatchedEpisodeTime:number": {},
      "animePosterURL:string": {},
    }
  }]
}).then(() => {
  start();
}).catch((err) => {
  console.log("erroe starting db", err);
})

Object.defineProperty(HTMLMediaElement.prototype, 'playing', {
  get: function() {
    return !!(this.currentTime > 0 && !this.paused && !this.ended && this.readyState > 2);
  }
})