Nyaa.si - Load More Thumbnail

Load image from cover/screenshot links.

// ==UserScript==
// @name         Nyaa.si - Load More Thumbnail
// @name:zh-CN   Nyaa.si - 自动加载更多预览图
// @namespace    none
// @description  Load image from cover/screenshot links.
// @description:zh-CN  从封面/截图链接加载图片并显示。
// @icon         https://www.google.com/s2/favicons?sz=64&domain=sukebei.nyaa.si
// @version      0.9.6
// @license      MIT
// @author       York Wang
// @match        https://sukebei.nyaa.si/*
// @match        https://hentai-covers.site/*
// @match        https://www.dlsite.com/*
// @match        https://e-hentai.org/*
// @match        https://pics.dmm.co.jp/*
// @match        https://javtenshi.com/*
// @match        https://3xplanet.net/*
// @match        https://3xplanet.com/*
// @match        https://xpic.org/*
// @match        https://imgrock.pw/*
// @match        https://picrok.com/*
// @match        https://picbaron.com/*
// @match        https://imgbaron.com/*
// @match        https://kvador.com/*
// @match        https://kropic.com/*
// @match        https://imgsto.com/*
// @match        https://imgsen.com/*
// @match        https://imgstar.eu/*
// @match        https://picdollar.com/*
// @match        https://pics4you.net/*
// @match        https://silverpic.com/*
// @match        https://fotokiz.com/*
// @match        https://premalo.com/*
// @match        https://piczhq.com/*
// @match        https://trypicz.com/*
// @match        http://imglord.com/*
// @match        https://croea.com/*
// @match        https://imgtaxi.com/*
// @match        https://imgadult.com/*
// @match        https://imgdrive.net/*
// @match        https://xxxwebdlxxx.org/*
// @match        https://xxxwebdlxxx.top/*
// @match        https://uvonahaze.xyz/*
// @match        https://trans.firm.in/*
// @match        https://imgdawgknuttz.com/*
// @match        https://imagetwist.netlify.app/*
// @match        https://imagetwist.com/*
// @match        https://imagexport.com/*
// @match        https://imagenimage.com/*
// @match        https://hentai4free.net/*
// @match        https://pixhost.to/*
// @match        https://imgair.net/*
// @match        http://imgair.net/*
// @match        http://imgfrost.net/*
// @match        http://imgblaze.net/*
// @match        https://pig69.com/*
// @match        https://ai18.pics/*
// @match        https://porn4f.com/*
// @match        https://javball.com/*
// @match        https://ovabee.com/*
// @match        https://idol69.net/*
// @match        https://cnpics.org/*
// @match        https://cnxx.me/*
// @match        https://cosplay18.pics/*
// @run-at       document-end
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    function Handler(pattern, process, processNyaa) {
        this.pattern = pattern
        this.process = process
        this.processNyaa = processNyaa
    }
    Handler.prototype.canHandle = function(url) {
        return this.pattern.test(url)
    }
    Handler.prototype.handle = function(url) {
        this.process(href => {
            document.location.href = href
            unsafeWindow.top.postMessage({"LMT": href, "LMT_SRC": url}, '*')
        })
    }
    Handler.prototype.handleNyaa = function(url) {
        if(this.processNyaa) {
            this.processNyaa(url, href => {
                unsafeWindow.top.postMessage({"LMT": href, "LMT_SRC": url}, '*')
            })
        } else {
            unsafeWindow.LMT_Frame.src = url
        }
    }
    const handlers = []
    const addHandler = (pattern, process, processNyaa) => handlers.push(new Handler(pattern, process, processNyaa))

    addHandler(/^https?:\/\/(hentai-covers\.site)\/image\/\w+/, callback => {
        return false
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/id="image-main" src="(.+?)"/)
                if(src.length > 1) callback(src[1])
            }
        })
    })
    addHandler(/^https?:\/\/(www\.dlsite\.com)\/maniax\/work\/=\/product_id\/RJ\d+.html/, callback => {
        return false
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/twitter:image:src" content="(.+?)"/)
                if(src.length > 1) callback(src[1])
            }
        })
    })
    addHandler(/^https?:\/\/(e-hentai\.org)\/g\/\w+\/\w+\//, callback => {
        return false
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/url\((https:\/\/.+?)\) no-repeat/)
                if(src.length > 1) callback(src[1])
            }
        })
    })
    addHandler(/^https?:\/\/pics\.dmm\.co\.jp(\/[\w]+)+\.jpg.*/, callback => {
        callback(document.location.href.match(/^https?:\/\/pics\.dmm\.co\.jp(\/[\w]+)+\.jpg/)[0])
    })
    addHandler(/^https?:\/\/c\.fantia\.jp\/uploads\/product\/image\/\d+\/[\w-]+\.webp/, callback => {
        return false
    }, (url, callback) => {
        callback(url)
    })
    addHandler(/^https?:\/\/(iwtf1\.caching\.ovh|i\.postimg\.cc|cdn\.faleno\.net|img169\.com|qpic\.ws|img\.blr844\.com|3xplanetimg2\.com|pics4you\.net|www\.javbus\.com)(\/[\w-]+)+\.jpg$/, callback => {
        return false
    }, (url, callback) => {
        callback(url)
    })
    addHandler(/^https?:\/\/(javtenshi\.com|3xplanet\.net|3xplanet\.com)\/viewimage\/\d+\.html/, callback => {
        return false
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/scale\(this\);" src="(.+)/)
                if(src.length > 1) callback(src[1])
            }
        })
    })
    addHandler(/^https?:\/\/xpic\.org(\/\w+)+/, callback => {
        unsafeWindow.wuLu && unsafeWindow.wuLu()
        const img = document.querySelector('img.attachment-original.size-original')
        if(img) {
            callback(img.src)
        }
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/src="(.*)" class="attachment-original size-original"/)
                if(src.length > 1) callback(src[1])
            }
        })
    })
    addHandler(/^https?:\/\/(imgrock\.pw)(\/[\w\-]+)+(\.[\w\-]+)+/, callback => {
        // pause on CAPTCHA
        const iframe = document.querySelector('iframe')
        if(iframe && iframe.src.indexOf('captcha') > -1) return

        const img = document.querySelector('.picview')
        if(img) {
            callback(img.src)
        } else {
            const btns = document.querySelectorAll('input[name=fnext]')
            for(let i=0;i<btns.length;i++) {if(!btns[i].style.display) btns[i].click()}
            const forms = document.querySelectorAll('form')
            for(let i=0;i<forms.length;i++) {if(forms[i].hito) {forms[i].submit()}}
        }
    })
    addHandler(/^https?:\/\/(picrok\.com)(\/[\w\-]+)+\.php/, callback => {
        // pause on CAPTCHA
        const iframe = document.querySelector('iframe')
        if(iframe && iframe.src.indexOf('captcha') > -1) return

        const img = document.querySelector('.picview')
        if(img) {
            callback(img.src)
        } else {
            unsafeWindow.setTimeout(() => {
              const forms = document.querySelectorAll('form')
              const btns = document.querySelectorAll('form>button')
              // for(let i=0;i<btns.length;i++) {if(btns[i].style.display) forms[i-1].submit()}
            }, 5000)
        }
    })
    addHandler(/^https?:\/\/(picbaron\.com|imgbaron\.com|kvador\.com|kropic\.com|imgsto\.com|imgsen\.com|imgstar\.eu|picdollar\.com|pics4you\.net|silverpic\.com|fotokiz\.com|premalo\.com|piczhq\.com|trypicz\.com|imglord\.com)(\/.+)+(\.[\w\-]+)+/, callback => {
        const img = document.querySelector('.pic')
        if(img) {
            callback(img.src)
        } else {
            const form = document.querySelector('form')
            form && form.submit()
        }
    })
    addHandler(/^https?:\/\/(croea\.com)(\/\w+)+/, callback => {
        const img = document.querySelector('.pic')
        if(img) {
            callback(img.src)
        } else {
            const form = document.querySelector('form')
            form && form.submit()
        }
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/src="(.*)" class="pic img img-responsive"/)
                if(src.length > 1) {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        responseType: "blob",
                        url: src[1],
                        onload: res => {
                            const reader = new FileReader()
                            reader.onload = () => {
                                callback(reader.result)
                            }
                            reader.readAsDataURL(res.response)
                        }
                    })
                }
            }
        })
    })
    addHandler(/^https?:\/\/(imgtaxi\.com|imgadult\.com|imgdrive\.net)(\/\w+)+/, callback => {
        unsafeWindow.ctipops = []
        unsafeWindow.adbctipops = []
        const img = document.querySelector('img.centred') || document.querySelector('img.centred_resized')
        if(img) {
            callback(img.src)
        } else {
            unsafeWindow.setTimeout(() => {
                const btn = document.querySelector('.overlay_ad_link')
              if(btn) {
                btn.focus()
                btn.click()
              }
            }, 1000)
        }
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/og:image:secure_url" content="(.*)"/)
                if(src.length > 1) callback(src[1].replace('small','big'))
            }
        })
    })
    addHandler(/^https?:\/\/(xxxwebdlxxx\.org|xxxwebdlxxx\.top)(\/\w+)+/, callback => {
        unsafeWindow.ctipops = []
        unsafeWindow.adbctipops = []
        const img = document.querySelector('img.centred') || document.querySelector('img.centred_resized')
        if(img) {
            callback(img.src)
        } else {
            unsafeWindow.setTimeout(() => {
                const btn = document.querySelector('.overlay_ad_link')
              if(btn) {
                btn.focus()
                btn.click()
              }
            }, 1000)
        }
    })
    addHandler(/^https?:\/\/(uvonahaze\.xyz|trans\.firm\.in||imgdawgknuttz\.com)(\/\w+)+/, callback => {
        const img = document.querySelector('img.centred') || document.querySelector('img.centred_resized')
        if(img) {
            callback(img.src)
        } else {
            const btn = document.querySelector('input[name=imgContinue]')
            btn && btn.click()
        }
    })
    addHandler(/^https?:\/\/(imagetwist\.netlify\.app)(\/\w+)+/, callback => {
        if(unsafeWindow.targetURL) document.location.href = unsafeWindow.targetURL
        const btn = document.querySelector('a.btn')
        btn && btn.click()
    })
    addHandler(/^https?:\/\/(imagetwist\.com|imagexport\.com|imagenimage\.com)(\/\w+)+/, callback => {
        const img = document.querySelector('.img-responsive')
        if(img) {
            callback(img.src)
        }
    })
    addHandler(/^https?:\/\/hentai4free\.net(\/\w+)+/, callback => {
        unsafeWindow.wuLu && unsafeWindow.wuLu()
        const img = document.querySelector('#image-viewer-container>img')
        if(img) {
            callback(img.src)
        }
    })
    addHandler(/^https?:\/\/pixhost\.to(\/\w+)+/, callback => {
        const img = document.querySelector('img.image-img')
        if(img) {
            callback(img.src)
        } else {
            const btn = document.querySelector('a.continue')
            btn && btn.click()
        }
    })
    addHandler(/^https?:\/\/(imgair\.net|imgfrost\.net|imgblaze\.net)(\/\w+)$/, callback => {
        unsafeWindow.wuLu && unsafeWindow.wuLu()
        const img = document.querySelector('#newImgE')
        if(img) {
            callback(img.src)
        }
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url.replace(/^https?:\/\/(imgfrost\.net|imgblaze\.net)/, 'https://imgair.net'),
            onload: res => {
                const txt = res.responseText
                let title = txt.match(/<title>(.*)<\/title>/)
                if(!title) return false
                title = title[1].trim()
                const badChar = /[\[\(\$\^\.\]\*\\\?\+\{\}\\|\)]/gi
                const pattern = new RegExp("(http.*" + title.replace(badChar, (c) => `\\${c}`) + ")")
                const src = txt.match(pattern)
                if(src.length > 0) callback(src[0])
            }
        })
    })
    addHandler(/^https?:\/\/(pig69\.com|ai18\.pics|porn4f\.com|javball\.com|ovabee\.com|idol69\.net|cnpics\.org|cnxx\.me|cosplay18\.pics)(\/\w+)+/, callback => {
        const img = document.querySelector('#fileOriginalModal img')
        if(img) {
            callback(img.src)
        } else {
            const btn = document.querySelector('a.continue')
            btn && btn.click()
        }
    }, (url, callback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const src = res.responseText.match(/"https?:\/\/((pig69\.com|ai18\.pics|porn4f\.com|javball\.com|ovabee\.com|idol69\.net|cnpics\.org|cnxx\.me|cosplay18\.pics)\/upload\/Application\/storage\/app\/public\/uploads\/users\/.*)"/)
                if(src.length > 1) callback("http://"+src[1])
            }
        })
    })
    addHandler(/^https:\/\/manko\.fun\|/, callback => {
        return false
    }, (url, callback) => {
        if(/^https:\/\/sukebei\.nyaa\.si\/(\?.*)?$/.test(href)) {
            callback(url.substr(18))
        }
    })

    const href = document.location.href
    if(/^https?:\/\/(sukebei\.nyaa\.si).+/g.test(href)) {

        let LMT_Wrap, LMT_Frame, LMT_Loading, LMT_panel, LMT_img
        const panelWidth = 480
        const panelHeight = 480
        function createWrap(parent) {
            parent.parentNode.insertAdjacentHTML('afterend', '<div class="panel panel-default"><div class="panel-body" id="LMT_Wrap"></div></div>')
            LMT_Wrap = document.querySelector('#LMT_Wrap')

            LMT_Loading = document.createElement('div')
            LMT_Loading.textContent = 'Loading Images...'
            LMT_Wrap.appendChild(LMT_Loading)
        }
        function createPanel() {
            LMT_panel = document.createElement('div')
            LMT_panel.style.position = 'fixed'
            LMT_panel.style.top = '-1000px'
            LMT_panel.style.left = '-1000px'
            LMT_panel.style.backgroundColor = '#f5f5f5'
            LMT_panel.style.border = '1px solid #ddd'
            LMT_panel.style.borderRadius = '6px'
            LMT_panel.style.boxShadow = '0 1px 1px rgba(0,0,0,.05)'
            LMT_panel.style.width = `${panelWidth}px`
            LMT_panel.style.height = `${panelHeight}px`
            document.body.appendChild(LMT_panel)

            LMT_img = document.createElement('img')
            LMT_img.style.width = '100%'
            LMT_img.style.height = '100%'
            LMT_img.style.objectFit = 'scale-down'
            LMT_img.onerror = (e) => {
                console.log('onerror', e.target.src)
            }
            LMT_panel.appendChild(LMT_img)

            LMT_Frame = document.createElement('iframe')
            LMT_Frame.id = 'LMT_Frame'
            LMT_Frame.sandbox = 'allow-forms allow-scripts allow-same-origin'
            LMT_Frame.style.display = 'none'
            document.body.appendChild(LMT_Frame)
        }

        const imgList = []

        function process() {
            if(imgList.length) {
                let url = imgList.shift()
                url.handler.handleNyaa(url.href)
            } else {
                if(LMT_Wrap) {
                    LMT_Loading.innerText = ''
                    document.body.removeChild(LMT_Frame)

                    if(!Array.apply(null, document.querySelectorAll('#LMT_Wrap > img')).length) {
                        document.querySelector('.panel:has(#LMT_Wrap)').style.display = 'none'
                    }
                } else {
                    LMT_Frame.src = 'about:blank'
                }
            }
        }

        unsafeWindow.addEventListener('message', function (e) {
            if(!e.data.LMT) return false
            if(LMT_Wrap) {
                LMT_Frame.src = ''
                const img = document.createElement('img')
                img.src = e.data.LMT
                img.style['max-width'] = '100%'
                LMT_Wrap.appendChild(img)
            }
            if(e.data.LMT_SRC) {
                const url_src = e.data.LMT_SRC
                const a = document.querySelector(`a[data-lmt-src="${decodeURI(url_src)}"]`)
                if(a && !a.dataset.lmt) {
                    a.dataset.lmt = e.data.LMT
                    const span = document.createElement("span");
                    span.innerHTML='🖼️'
                    span.dataset.lmt = e.data.LMT
                    a.before(span)

                    if(/^https?:\/\/(sukebei\.nyaa\.si\/view\/).+/g.test(a.href)) {
                        const id = a.href.substr(a.href.lastIndexOf('/')+1)
                        saveThumb(id, e.data.LMT)
                    }
                }
            }
            process()
        })

        let windowWidth = unsafeWindow.innerWidth
        let windowHeight = unsafeWindow.innerHeight
        unsafeWindow.addEventListener('resize', function (e) {
            windowWidth = unsafeWindow.innerWidth
            windowHeight = unsafeWindow.innerHeight
        })
        unsafeWindow.addEventListener('mouseover', function (e) {
            const a = e.target
            if(a.dataset.lmt) {
                LMT_img.src = a.dataset.lmt
                if(e.clientX + panelWidth < windowWidth) {
                    LMT_panel.style.left = e.clientX+'px'
                } else {
                    LMT_panel.style.left = e.clientX-panelWidth+'px'
                }
                if(e.clientY + panelHeight < windowHeight) {
                    LMT_panel.style.top = e.clientY+'px'
                } else {
                    LMT_panel.style.top = e.clientY-panelHeight+'px'
                }
            }
        })
        unsafeWindow.addEventListener('mouseout', function (e) {
            const a = e.target
            if(a.dataset.lmt) {
                LMT_panel.style.top = '-1000px'
                LMT_panel.style.left = '-1000px'
                LMT_img.src = '/static/favicon.png'
            }
        })

        const CLOUD_URL = 'https://oc1.bigsm.art'
        const getThumbs = ids => {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `${CLOUD_URL}/thumbs/?ids=${ids}`,
                    onload: res => { resolve(JSON.parse(res.responseText)) },
                    onError: err => { resolve([]) }
                })
            })
        }
        const saveThumb = (id, thumb) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${CLOUD_URL}/thumb/${id}`,
                data: `{"url": "${thumb}"}`,
                headers: {
                    "Content-Type": "application/json"
                }
            })
        }

        if(/^https?:\/\/(sukebei\.nyaa\.si\/view\/).+/g.test(href)) {
            // Detail Page
            if(document.title === '429 Too Many Requests') {
                document.location.href = document.location.href
                return
            }
            const desc = document.querySelector('#torrent-description')
            const links = desc.querySelectorAll('a')
            if(!desc || !links) return

            for(let i = 0; i < links.length; i++) {
                if(!links[i].href) continue
                links[i].dataset.lmtSrc = decodeURI(links[i].href)
                handlers.forEach(h=>{h.canHandle(links[i].href) && imgList.push({href:links[i].href,handler:h})})
            }

            if(!LMT_Frame) {
                createWrap(desc)
            }
            createPanel()
            process()
        } else {
            // List Page
            // try to load thumb from cloud
            const links = document.querySelectorAll('.torrent-list>tbody>tr>td:nth-child(2)>a:last-child')
            const ids = Array.apply(null, links).map(a => a.href.substr(a.href.lastIndexOf('/')+1)).join(',')
            getThumbs(ids).then(thumbs => {
                for(let i in thumbs) {
                    if(!thumbs[i]) continue
                    links[i].dataset.lmt = thumbs[i]
                    const span = document.createElement("span");
                    span.innerHTML='🖼️'
                    span.dataset.lmt = thumbs[i]
                    links[i].before(span)
                }
            })

            document.querySelector('.torrent-list').addEventListener('mouseover', async (e) => {
                const a = e.target
                if(!a.dataset.lmt && !a.dataset.lmtSrc && a.href) {
                    // Serialize the requests, to avoid '429 Too Many Requests'
                    const unlock = await lock()
                    const detail = await getDetail(a.href)
                    unlock()
                    let desc = detail.responseText.match(/id="torrent-description">(.*?)<\/div>/)
                    if(!desc) return false
                    desc = desc[1].replaceAll('&#10;', '\n').replaceAll(')]', ' )]').replaceAll('\*\*', ' \*\* ')
                    let hrefs = desc.match(/(https?:\/\/[^\s\)]+)/g) || []
                    let info = detail.responseText.match(/noopener noreferrer nofollow" href="(https?:\/\/.+?)"/)
                    if(info) hrefs = [...hrefs, info[1]]
                    console.log(hrefs)

                    let flag = false
                    for (let i in hrefs) {
                        let href = hrefs[i]
                        // for links '[![Visit manko.fun](https://...)](https://manko.fun)'
                        if (href.trim() === 'https://manko.fun') {
                            href = desc.match(/(https?:\/\/.+?)\)\]\(https:\/\/manko\.fun\)/)
                            if(href) {
                                href = 'https://manko.fun|' + href[1].trim()
                            } else {
                                continue
                            }
                        }

                        for(let j in handlers) {
                            const h = handlers[j]
                            if(href.indexOf('nyaa') < 0 && h.canHandle(href)) {
                                a.dataset.lmtSrc = href
                                imgList.push({href:href,handler:h})
                                flag = true
                                break
                            }
                        }
                        if(flag) {
                            process()
                            break
                        }
                    }
                    if(!flag) {
                        a.dataset.lmtSrc = '#'
                    }
                }
            })
            createPanel()

            let isLock = false;
            let lockList = [];
            async function lock() {
                function unlock() {
                    let waitFunc = lockList.shift();
                    if (waitFunc) {
                        waitFunc.resolve(unlock);
                    } else {
                        isLock = false;
                    }
                }
                if (isLock) {
                    return new Promise((resolve, reject) => {
                        lockList.push({ resolve, reject });
                    });
                } else {
                    isLock = true;
                    return unlock;
                }
            }
            const getDetail = url => {
                return new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        onload: res => { resolve(res) },
                        onError: err => { reject(err) }
                    })
                })
            }
        }
    } else {
        // Image Host Websites
        handlers.forEach(h=>{h.canHandle(href) && h.handle(document.location.href)})
    }
})();