Myanimelist.net - Kitsunekko.net auto-check

Requires manual updating of the subtitles list. Edit

As of 09.07.2021. See ბოლო ვერსია.

// ==UserScript==
// @name        Myanimelist.net - Kitsunekko.net auto-check
// @namespace   Violentmonkey Scripts
// @match       https://myanimelist.net/*
// @grant       none
// @version     1.0
// @author      -
// @description Requires manual updating of the subtitles list. Edit
// ==/UserScript==

console.log('userscript running')

let kitsunekkoSet = null

function formatTitle(title) {
  return title
    .trim()
    .toLowerCase()
    .replace(/[^a-zA-Z0-9]+/g, ' ')
    .replace(/ +/g, ' ')
}

function checkAnimeAvailability(name) {
  const formatted = formatTitle(name)
  return kitsunekkoSet.get(formatted)
}

window.checkAnimeAvailability = checkAnimeAvailability

function compileList(raw) {
  const rows = raw.split('\n')
  const titles = rows.map(row => (row.trim().match(/^(.*?)[\s]+[0-9]+ [a-z]+$/) || [])[1]).filter(a => a)
  
  const formatted = titles.map(title => {
    const formattedTitle = formatTitle(title)
    return [ formattedTitle, { orig: title, formatted: formattedTitle } ]
  })
  return new Map(formatted)
}



//---------------------------

function makeSubtitleLink(anime) {
  const query = anime.orig.replace(/ /g, '+')
  const href = `https://kitsunekko.net/dirlist.php?dir=subtitles%2Fjapanese%2F${query}%2F`
  
  const a = document.createElement('a')
  a.style = `
    font-size: 10px;
    font-weight: bold;
    background: red;
    color: white;
    border-radius: 20%;
    padding: 1.5px;
    margin: 0 0 0 3px;
  `;
  a.href = href
  a.appendChild(document.createTextNode('字幕'))
  return a;
}

function runCheckTitle(node) {
  const title = node.textContent
  const match = checkAnimeAvailability(title)
  if (!match) {
    console.log(`JP subtitles not found for "${title}"`)
    return null;
  }
  node.appendChild(makeSubtitleLink(match))
  return match
}

function runCheckAnimeThumb(node) {
  const title = node.querySelector('.title').textContent
  const match = checkAnimeAvailability(title)
  console.log('match:', match)
  if (!match) {
    console.log(`JP subtitles not found for "${title}"`)
    return null;
  }
  const subtitleLink = makeSubtitleLink(match)
  subtitleLink.style.position = "absolute";
  subtitleLink.style.zIndex = "9999";
  subtitleLink.style.marginTop = "3px";
  node.insertBefore(subtitleLink, node.firstChild)
  return match
}

function runCheckEpisodeThumb(node) {
  const title = node.querySelector('.external-link').textContent
  const match = checkAnimeAvailability(title)
  console.log('match:', match)
  if (!match) {
    console.log(`JP subtitles not found for "${title}"`)
    return null;
  }
  const subtitleLink = makeSubtitleLink(match)
  subtitleLink.style.position = "absolute";
  subtitleLink.style.zIndex = "9999";
  subtitleLink.style.marginTop = "3px";
  node.insertBefore(subtitleLink, node.firstChild)
  return match
}



function runChecks() {
  let matched = []
  
  const titles = [
    document.querySelectorAll('.title-name'),
    document.querySelectorAll('.title-english'),
    
    document.querySelectorAll('.ranking-unit .title'),
    document.querySelectorAll('.recommendations_h3 a:not([title=""])'),
    document.querySelectorAll('.reviews_h3 a'),
  ].map(nodelist => [...nodelist]).flat()
  matched = [ ...matched, ...titles.map(runCheckTitle) ]
  
  const animeThumbs = [...document.querySelectorAll('.btn-anime:not(.episode)')]
  matched = [ ...matched, ...animeThumbs.map(runCheckAnimeThumb) ]
  
  const episodeThumbs = [...document.querySelectorAll('.btn-anime.episode')]
  matched = [ ...matched, ...episodeThumbs.map(runCheckEpisodeThumb) ]
  
  matched = matched.filter(a => a)
  return matched
}


function setupKitsunekkoListInput({ empty=false }) {
  const openInputBtn = document.createElement('button')
  openInputBtn.appendChild(document.createTextNode('字幕設定'))
  
  const explanation = document.createElement('p')
  explanation.style = `
    padding: 1em;
    background: white;
    color: black;
  `
  explanation.innerHTML = `

  The values in this script needs to be updated manually.<br>
  1. Go to the <a href="https://kitsunekko.net/dirlist.php?dir=subtitles%2Fjapanese%2F">kitsunekko.net directory</a>.<br>
  2. Select and copy the list of anime links<br>
<br>
  e.g.: "<code>.hack G.U 	10 months<br>
3-gatsu no Lion 	5 months<br>
5-tou ni Naritai 	7 years<br>
07-Ghost 	3 weeks<br>
7SEEDS 	11 months<br>
11eyes 	11 months
...<code>"<br>
    <br>
  3. Paste in the box and submit.<br>
    <br>
    <br>
    To update the list again in the future, click on the <code>字幕設定</code> button in the footer.
  `
  
  const modalBg = document.createElement('div')
  modalBg.style = `
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;
    z-index: 9999999;
    background: rgba(0,0,0,0.5);
    display: none;
  `;
  modalBg.onclick = (ev) => { if (ev.target === modalBg) { closeModal(); } }
  
  const modal = document.createElement('div')
  modal.style = `
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
    
    display: flex;
    flex-flow: column nowrap;
  `
  modalBg.appendChild(modal)
  
  const input = document.createElement('textarea');
  const submit = document.createElement('button');
  submit.appendChild(document.createTextNode('Update subbed anime list'))
  submit.onclick = () => {
    const raw = input.value
    localStorage.setItem('kitsunekkoListRaw', raw);
    
    setTimeout(() => location.reload(), 500)
  }
  modal.appendChild(explanation)
  modal.appendChild(input)
  modal.appendChild(submit)
  
  function openModal() { modalBg.style.display = 'block'; }
  function closeModal() { modalBg.style.display = 'none'; }
  openInputBtn.onclick = openModal
  
  document.body.insertBefore(modalBg, document.body.firstElement)
  document.querySelector('.footer-link-block').appendChild(openInputBtn)
  
  if (empty) { openModal() }
}


function main() {
  const KITSUNEKKO_LIST_RAW = localStorage.getItem('kitsunekkoListRaw')
  const empty = KITSUNEKKO_LIST_RAW ? false : true
  setupKitsunekkoListInput({ empty: empty, })
  
  if (!empty) {  
    const subsMap = compileList(KITSUNEKKO_LIST_RAW)
    kitsunekkoSet = subsMap


    const matched = runChecks()
    setTimeout(() => {
      console.log(`Found ${matched.length} anime(s) with japanese subs:`, matched.map(anime => anime.orig))
    }, 500)
  }
  
}
setTimeout(main)