hybridsong / Enhanced SauceNao

// ==UserScript==
// %genload_req%
// @name           Enhanced SauceNao
// @namespace      https://github.com/hybridsong/saucenao
// @version        2018.12.12.2017.10.04
// @author         hybridsong
// @license        GPL-3.0-only
// @icon           https://secure.gravatar.com/avatar/187e65a2ca69335f7483a4f19812050a
// @match          *://saucenao.com/*
// @run-at         document-end
// @grant          GM_listValues
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_deleteValue
// @grant          GM_xmlhttpRequest
// ==/UserScript==

/*
 *  Enhanced SauceNao
 *  \hybridsong/
 */

(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define([], factory);
  } else if (typeof exports !== "undefined") {
    factory();
  } else {
    var mod = {
      exports: {}
    };
    factory();
    global.FileSaver = mod.exports;
  }
})(this, function () {
  "use strict";

  /*
  * FileSaver.js
  * A saveAs() FileSaver implementation.
  *
  * By Eli Grey, http://eligrey.com
  *
  * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
  * source  : http://purl.eligrey.com/github/FileSaver.js
  */
  // The one and only way of getting global scope in all environments
  // https://stackoverflow.com/q/3277182/1008999
  var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0;

  function bom(blob, opts) {
    if (typeof opts === 'undefined') opts = {
      autoBom: false
    };else if (typeof opts !== 'object') {
      console.warn('Depricated: Expected third argument to be a object');
      opts = {
        autoBom: !opts
      };
    } // prepend BOM for UTF-8 XML and text/* types (including HTML)
    // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF

    if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
      return new Blob([String.fromCharCode(0xFEFF), blob], {
        type: blob.type
      });
    }

    return blob;
  }

  function download(url, name, opts) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.responseType = 'blob';

    xhr.onload = function () {
      saveAs(xhr.response, name, opts);
    };

    xhr.onerror = function () {
      console.error('could not download file');
    };

    xhr.send();
  }

  function corsEnabled(url) {
    var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker

    xhr.open('HEAD', url, false);
    xhr.send();
    return xhr.status >= 200 && xhr.status <= 299;
  } // `a.click()` doesn't work for all browsers (#465)


  function click(node) {
    try {
      node.dispatchEvent(new MouseEvent('click'));
    } catch (e) {
      var evt = document.createEvent('MouseEvents');
      evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
      node.dispatchEvent(evt);
    }
  }

  var saveAs = _global.saveAs || // probably in some web worker
  typeof window !== 'object' || window !== _global ? function saveAs() {}
  /* noop */
  // Use download attribute first if possible (#193 Lumia mobile)
  : 'download' in HTMLAnchorElement.prototype ? function saveAs(blob, name, opts) {
    var URL = _global.URL || _global.webkitURL;
    var a = document.createElement('a');
    name = name || blob.name || 'download';
    a.download = name;
    a.rel = 'noopener'; // tabnabbing
    // TODO: detect chrome extensions & packaged apps
    // a.target = '_blank'

    if (typeof blob === 'string') {
      // Support regular links
      a.href = blob;

      if (a.origin !== location.origin) {
        corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank');
      } else {
        click(a);
      }
    } else {
      // Support blobs
      a.href = URL.createObjectURL(blob);
      setTimeout(function () {
        URL.revokeObjectURL(a.href);
      }, 4E4); // 40s

      setTimeout(function () {
        click(a);
      }, 0);
    }
  } // Use msSaveOrOpenBlob as a second approach
  : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) {
    name = name || blob.name || 'download';

    if (typeof blob === 'string') {
      if (corsEnabled(blob)) {
        download(blob, name, opts);
      } else {
        var a = document.createElement('a');
        a.href = blob;
        a.target = '_blank';
        setTimeout(function () {
          click(a);
        });
      }
    } else {
      navigator.msSaveOrOpenBlob(bom(blob, opts), name);
    }
  } // Fallback to using FileReader and a popup
  : function saveAs(blob, name, opts, popup) {
    // Open a popup immediately do go around popup blocker
    // Mostly only avalible on user interaction and the fileReader is async so...
    popup = popup || open('', '_blank');

    if (popup) {
      popup.document.title = popup.document.body.innerText = 'downloading...';
    }

    if (typeof blob === 'string') return download(blob, name, opts);
    var force = blob.type === 'application/octet-stream';

    var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;

    var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);

    if ((isChromeIOS || force && isSafari) && typeof FileReader === 'object') {
      // Safari doesn't allow downloading of blob urls
      var reader = new FileReader();

      reader.onloadend = function () {
        var url = reader.result;
        url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;');
        if (popup) popup.location.href = url;else location = url;
        popup = null; // reverse-tabnabbing #460
      };

      reader.readAsDataURL(blob);
    } else {
      var URL = _global.URL || _global.webkitURL;
      var url = URL.createObjectURL(blob);
      if (popup) popup.location = url;else location.href = url;
      popup = null; // reverse-tabnabbing #460

      setTimeout(function () {
        URL.revokeObjectURL(url);
      }, 4E4); // 40s
    }
  };
  _global.saveAs = saveAs.saveAs = saveAs;

  if (typeof module !== 'undefined') {
    module.exports = saveAs;
  }
});

let main_style = "\
        #ES-images { position: fixed; top: 0; right: 5px; width: 125px; height: 100%; overflow-y: auto; z-index:1; display:none }\
        #ES-total { position: fixed; bottom: 5; right: 50px; background: #1d1d1d; padding: 0 5px; opacity: 0.5; z-index:1 }\
        #ES-namefix { position: fixed; bottom: 25; right: 50px; background: #1d1d1d; padding: 0 5px; opacity: 0.5; z-index:1 }\
        #ES-results { position: absolute; top:0; height: 100%; background: #1d1d1d; display: none }\
    "

let head = document.querySelector('head'),
    style = document.createElement("style")

style.type = "text/css"
style.append(main_style)

head.append(style)

'use strict'

let doc_url = new URL(document.location.href),
    NF_useboard = "danbooru",
    NF_delimiter = "_",
    NF_debug = true

const
    hosts = [
        'www.pixiv.net',
        'danbooru.donmai.us',
        'chan.sankakucomplex.com',
        'yande.re',
    ]

function createObjectURL(object) {
    return (window.URL) ? window.URL.createObjectURL(object) : window.webkitURL.createObjectURL(object);
}

function revokeObjectURL(url) {
    return (window.URL) ? window.URL.revokeObjectURL(url) : window.webkitURL.revokeObjectURL(url);
}

function filename (str)
{
   var name = new String(str).substring(str.lastIndexOf('/') + 1);
   return name;
}

function extname (path) {
    var filename = path.split('\\').pop().split('/').pop();
    return filename.substr(( Math.max(0, filename.lastIndexOf(".")) || Infinity) + 1);
}

if (doc_url.pathname == '/') { // root

let base = document.createElement("base")
base.target = '_self'
document.head.append(base)

let mount_scripts = () => {
    if (!document.getElementById('mounted_scripts')) {
        let scripts = results_container.querySelectorAll('script')
        for (let i=0; i<scripts.length; i++) {
            let script = document.createElement("script")
            script.textContent = scripts[i].innerText
            script.id = 'mounted_scripts'
            document.body.append(script)
            scripts[i].remove()
        }
        downloadJSAtOnload() // load jQuery
    }
}

let adjust_results_page = () => {
    base.target = '_blank'
    results_container.querySelector('#Search').remove()
}

const
    get_link = (host) => {
        let links = results_container.querySelectorAll('\
                .result:not(.hidden) .resultmiscinfo a,\
                .result:not(.hidden) .resultcontentcolumn>a\
            ')

        for (let link of links) {
            let m = link.href.match(/\:\/\/(.+?)\//)
            if (m && m[1]) {
                if (host == m[1])
                    return link
            }
        }
    }

let xhr = new XMLHttpRequest(),
    get_results = (file, img_btn, next) => {
        //return
        formData = new FormData()
        formData.append('file', file)
        formData.append('url', '')
        formData.append('frame', 1)
        formData.append('hide', 0)
        formData.append('database', 999)

        xhr.open("POST", '/search.php')
        xhr.onreadystatechange = function() {
            if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
                if (!img_btn.searched) {
                    img_btn.searched = 1
                    total_searched++
                    total.update()
                }

                img_btn.style.opacity = 0.5
                results.style.display = 'block'
                GM_setValue(file.name, true)

                results_container.innerHTML = xhr.response
                mount_scripts()
                adjust_results_page()

                if (next) next()
            }
            messages.innerText = `${xhr.statusText} (${xhr.status})`
        }
        xhr.send(formData)
    }

let total_searched = 0,
    total_images = 0

let image_list = null,
    load_images = (files) => {
        images.style.display = 'block'

        total_images = 0
        total_searched = 0
        image_list = []
        next_state.i = 0

        if(files.length) {
            for(let i=0; i<files.length; i++) {
                var src = createObjectURL(files[i])
                var image = new Image()
                image.src = src
                image.style.width = 100
                image.searched = 0

                if (GM_getValue(files[i].name)) {
                    total_searched++
                    total.update()
                    image.searched = 1
                    image.style.opacity = 0.5
                }

                image.onclick = function () {
                    next_state.i = i+1
                    this.style.opacity = 0.9
                    get_results(files[i], this)
                }

                images.append(image)
                total_images++
                total.update()

                image_list[i] = {
                    file: files[i],
                    image:image
                }
            }
        }
    }

let namefix_state = {
    i: 0,
    save: function (id, file) {
        if (id)
            saveAs(file, `${file.name.split('.')[0]}${NF_delimiter}${NF_useboard}${NF_delimiter}${id}.${extname(file.name)}`)
        else
            saveAs(file, `${file.name.split('.')[0]}${NF_delimiter}search${NF_delimiter}failed.${extname(file.name)}`)
    },
    proc: function () {
        if (this.i == image_list.length) return

        let { file, image } = image_list[this.i]

        if (!image.searched) {
            image.style.opacity = 0.9
            get_results(file, image, () => {
                let links = results_container.querySelectorAll('.result:not(.hidden) .resultmiscinfo a'),
                    urls = [],
                    id = null

                links.forEach((link, i) => {
                    let m = link.href.match(/\:\/\/(.+?)\//)
                    if (m && m[1]) {
                        let host = m[1]
                        if (host == 'danbooru.donmai.us')
                            urls.push(link.href)
                    }
                })

                if (urls.length) {
                    let m = urls[0].match(/\/(\d+)/)
                    id = m ? m[1] : null
                }

                if (NF_debug)
                    console.log(urls, links, id)

                this.save(id, file)
                setTimeout(() => this.proc(), 2000)
            })
        } else {
            if (this.i < image_list.length)
                setTimeout(() => this.proc(), 0)
        }

        this.i++
    }
}

let next_state = {
    i: 0,
    loading: 0,
    load: function () {
        if (this.i == image_list.length || this.loading) return

        let { file, image } = image_list[this.i]

        images.scrollTop = image.offsetTop
        image.style.opacity = 0.9
        this.loading = 1

        get_results(file, image, () => {
            this.i++
            this.loading = 0
        })
    }
}

let select_button = document.createElement('input')
select_button.multiple = true
select_button.type = 'file'
select_button.style.width = '80px'
select_button.onchange = function () { load_images(this.files) }

document.querySelector('form').insertBefore(select_button, document.querySelector('form p'))
document.querySelector('#Search').style.width = '465px'

let images = document.createElement('div')

images.id = "ES-images"

let total = document.createElement('div')

total.id = "ES-total"

total.update = () => {
    total.innerText = `${total_images}/${total_searched}`
}

total.onclick = () => {
    if (confirm("Clean"))
        clean()
}

images.append(total)

let namefix = document.createElement('div')

namefix.id = "ES-namefix"

namefix.innerText = 'NF'

namefix.onclick = () => {
    if (confirm("Name Fix")) {
        namefix.onclick = null
        namefix_state.proc()
    }
}

images.append(namefix)

document.body.append(images)

let results = document.createElement('div')

results.id = 'ES-results'

let widget = document.createElement('div')

widget.style.position = 'fixed'
widget.style.bottom = 0
widget.style.right = '135px'
widget.style.color = '#666'

results.append(widget)

let messages = document.createElement('div')

widget.append(messages)

let back = document.createElement('div')

back.append('Back')
back.onclick = () => {
    results.style.display = 'none'
}

widget.append(back)

let results_container = document.createElement('div')

results.append(results_container)

document.body.append(results)

window.onbeforeunload = () => {
    if (total_searched == total_images && image_list.length)
        clean()
}

let clean = () => {
    let vals = GM_listValues()
    for (let i=0; i<vals.length; i++)
        GM_deleteValue(vals[i])
}

document.onpaste = e => {
    load_images(e.clipboardData.files)
    next_state.load()
}

document.onkeydown = (e) => {
    let key = String.fromCharCode(e.keyCode)
    switch (key) {
        case 'N':
            next_state.load()
            break
    }

    if (key.match(/\d/)) {
        const link = get_link(hosts[key-1])
        if (link)
            link.click()
    }
 }

} // root