MyFigureCollection: 图片下载器

下载原始图片

目前为 2022-05-05 提交的版本。查看 最新版本

// ==UserScript==
// @name        MyFigureCollection: 图片下载器
// @name:en     MyFigureCollection: Image Downloader
// @name:zh-CN  MyFigureCollection: 图片下载器
// @name:zh-TW  MyFigureCollection: 图片下载器
// @description 下载原始图片
// @description:en The original fullsize images downloader
// @description:zh-CN 下载原始图片
// @description:zh-TW 下载原始图片
// @namespace   Violentmonkey Scripts
// @match       https://myfigurecollection.net/picture/*
// @match       https://myfigurecollection.net/pictures.php
// @grant       GM_download
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @require     https://gf.qytechs.cn/scripts/444466-mini-mvvm/code/mini%20mvvm.js?version=1047057
// @license     GPL-3.0
// @compatible  Chrome
// @version     1.4.9
// @author      ayan0312
// ==/UserScript==
const TIME_OUT = 30 * 1000
const FILTER_URLS = [
    'https://static.myfigurecollection.net/ressources/nsfw.png',
    'https://static.myfigurecollection.net/ressources/spoiler.png'
]

const globalState = observe({
    group: GM_getValue('groupCount', 1),
    count: GM_getValue('picCount', 1),
    componentDownloadStatus: {},
    downloadStates: {
        total: 0,
        normal: 0,
        loading: 0,
        error: 0,
        timeout: 0,
        downloaded: 0,
    }
})

new Watcher(null, () => {
    return globalState.count
}, (newVal) => {
    GM_setValue('picCount', newVal)
})

new Watcher(null, () => {
    return globalState.group
}, (newVal) => {
    GM_setValue('groupCount', newVal)
    globalState.count = 0
})

new Watcher(null, () => {
    return globalState.componentDownloadStatus
}, (newVal) => {
    Object.assign(globalState.downloadStates, {
        total: 0,
        normal: 0,
        loading: 0,
        error: 0,
        timeout: 0,
        downloaded: 0,
    })
    const states = globalState.downloadStates
    Object.keys(newVal).forEach(key => {
        const status = newVal[key]
        if (states[status] != null) {
            states[status] += 1
            states.total += 1
        }
    })
}, true)

function beforeDownload() {
    if (GM_getValue('groupCount', 1) != globalState.group) {
        const curCount = GM_getValue('picCount', 1) + 1
        globalState.group = GM_getValue('groupCount', 1)
        globalState.count = curCount
    } else {
        globalState.group = GM_getValue('groupCount', 1)
        globalState.count = GM_getValue('picCount', 1) + 1
    }

    return { group: globalState.group, count: globalState.count }
}

const DownloadSequence = {
    template: `
    <span>Group: </span>
    <button v-on:click="decreaseGroup">-</button>
    <span style="margin:0 10px">{{global.group}}</span>
    <button v-on:click="increaseGroup">+</button>
    <span style="margin-left:10px">Item: </span>
    <button v-on:click="decreaseCount">-</button>
    <span style="margin:0 10px">{{global.count}}</span>
    <button v-on:click="increaseCount">+</button>
  `,
    data() {
        return {
            global: globalState,
        }
    },
    methods: {
        increaseCount() {
            this.global.count += 1
        },
        decreaseCount() {
            this.global.count -= 1
        },
        increaseGroup() {
            this.global.group += 1
        },
        decreaseGroup() {
            this.global.group -= 1
        }
    }
}

const REQEUST_BUTTON_STYLES = {
    normal: {},
    loading: { background: 'white', color: 'black', cursor: 'wait' },
    error: { background: 'red', color: 'white' },
    timeout: { background: 'yellow', color: 'black' },
    downloaded: { background: 'green', color: 'white' }
}

const DownloadButton = {
    template: `
      <button v-on:click="download" v-style="downloadBtnStyle">
        {{downloadedMsg}}
      </button>
    `,
    data() {
        return {
            oldStatus: 'normal',
            downloadStatus: 'normal' // 'normal' 'loading' 'error' 'timeout' 'downloaded'
        }
    },
    computed: {
        downloadBtnStyle() {
            return REQEUST_BUTTON_STYLES[this.downloadStatus]
        },
        downloadedMsg() {
            const messages = {
                normal: 'Download',
                loading: 'Downloading...',
                error: 'Failed',
                timeout: 'Timeout',
                downloaded: 'Redownload'
            }
            return messages[this.downloadStatus]
        }
    },
    watch: {
        downloadStatus(newStatus, oldStatus) {
            this.oldStatus = oldStatus
            globalState.componentDownloadStatus[this.cid] = newStatus
        }
    },
    created() {
        globalState.componentDownloadStatus[this.cid] = this.downloadStatus
    },
    destoryed() {
        delete globalState.componentDownloadStatus[this.cid]
    },
    methods: {
        download() {
            if (this.downloadStatus === 'loading') return
            refreshGroup()
            this.downloadStatus = 'loading'
            if (this.oldStatus !== 'error' && this.oldStatus !== 'timeout')
                this.value = beforeDownload()
            this.$emit('download', this.value)
        },
    }
}

const DownloadState = {
    template: `
    <div style="display:flex;flex-direction:row;padding:5px;flex-wrap:wrap">
      <div style="margin-right:15px;color:black">
        <span style="">Total:</span>
        <span>{{states.total}}</span>
      </div>
      <div style="margin-right:15px;color:green">
        <span style="">Downloaded:</span>
        <span>{{states.downloaded}}</span>
      </div>
      <div style="margin-right:15px;color:grey">
        <span style="">Downloading:</span>
        <span>{{states.loading}}</span>
      </div>
      <div style="margin-right:15px;color:brown">
        <span style="">Timeout:</span>
        <span>{{states.timeout}}</span>
      </div>
      <div style="color:red">
        <span>Failed:</span>
        <span>{{states.error}}</span>
      </div>
    </div>
  `,
    data() {
        return {
            states: globalState.downloadStates
        }
    },
    methods: {
        download(value) {
            this.$emit('download', value)
        }
    }
}

const PictureDownload = {
    components: {
        'download-button': DownloadButton,
        'download-sequence': DownloadSequence,
    },
    template: `
    <div>
      <download-sequence></download-sequence>
      <span v-show:downloaded>{{msg}}</span>
      <download-button v-ref="downloadButton" v-on:download="download"></download-button>
    </div>
  `,
    data() {
        return {
            group: 0,
            count: 0,
            downloaded: false
        }
    },
    computed: {
        msg() {
            return `Group: ${this.group} Item: ${this.count}`
        }
    },
    mounted() {
        const value = GM_getValue(window.location.href.split('&')[0])
        if (!value) return
        this.$refs.downloadButton.downloadStatus = value.downloadStatus
        this.$refs.downloadButton.value = {
            group: value.group,
            count: value.count
        }
        this.downloaded = true
        this.group = value.group
        this.count = value.count
    },
    methods: {
        download(value) {
            this.downloaded = true
            this.group = value.group
            this.count = value.count
            this.$emit('download', value)
        }
    }
}

function insertAfter(targetNode, afterNode) {
    const parentNode = afterNode.parentNode
    const beforeNode = afterNode.nextElementSibling
    if (beforeNode == null)
        parentNode.appendChild(targetNode)
    else
        parentNode.insertBefore(targetNode, beforeNode)
}

function mountVM(node, component) {
    const vm = new MVVMComponent(component)
    vm.$mount(node)
    return vm
}

function download({ vm, picture, group, count, origin }) {
    const values = origin.split('/')
    const fileType = values[values.length - 1].split('.')[1]
    const name = `${group}.${count}.${fileType}`
    const end = (status) => {
        return () => {
            vm.downloadStatus = status
            GM_setValue(picture.split('&')[0], { origin, group, count, downloadStatus: status })
        }
    }

    GM_download({
        url: origin,
        name,
        timeout: TIME_OUT,
        onload: end('downloaded'),
        onerror: end('error'),
        ontimeout: end('timeout'),
    })
}

function renderPictureExtension(objectMeta, thePicture) {
    const origin = thePicture.href
    const div = document.createElement('div')
    objectMeta.appendChild(div)
    const vm = mountVM(div, PictureDownload)
    vm.$on('download', (value) => {
        download({
            ...value,
            origin,
            picture: window.location.href,
            vm: vm.$refs.downloadButton,
        })
    })
}

const createPictruePreview = ({ thumb, origin, picture }) => {
    const commonStyle = {
        'margin': '10px 10px',
        'border': '1px solid #000',
        'padding': '10px',
        'border-radius': '5px',
        'background': '#fff',
        'transition': 'all 0.5s',
        'box-sizing': 'border-box'
    }

    return {
        components: {
            'download-button': DownloadButton
        },
        template: `
          <div v-style="containerStyle">
            <div style="margin-bottom:10px;display:flex;flex-direction:row;justify-content:center;align-items:center;width:100%;">
              <div style="margin:0 10px;flex-shrink:0"><img style="cursor:pointer" v-on:click="openPicturePage" [src]="thumb" /></div>
              <div style="flex-shrink:1" v-show="originalImage"><img style="width:100%" [src]="src" /></div>
            </div>
            <div style="display:flex;justify-content:center;align-items:center;flex-direction:column">
                <download-button v-ref="downloadButton" v-on:download="download"></download-button>
                <br v-show:downloaded />
                <div v-show:downloaded>
                  <span>Group:</span>
                  <span >{{group}}</span>
                  <span>Item:</span>
                  <span >{{count}}</span>
                </div>
                <br />
                <button v-on:click="toggle">{{msg}}</button>
                <br />
                <button v-show:refresh v-on:click="refreshOrigin" v-style="refreshBtnStyle">
                  {{refreshMsg}}
                </button>
            </div>
          </div>
      `,
        data() {
            return {
                thumb,
                origin,
                picture,
                refresh: FILTER_URLS.includes(origin),
                originalImage: false,
                group: 0,
                count: 0,
                downloaded: false,
                refreshStatus: 'normal' // 'normal' 'loading' 'error' 'timeout' 'downloaded'
            }
        },
        computed: {
            src() {
                return this.originalImage ? this.origin : this.thumb
            },
            msg() {
                return this.originalImage ? 'Close Preview' : 'Preview'
            },
            containerStyle() {
                return Object.assign({}, commonStyle, this.originalImage ? {
                    width: '100%'
                } : {})
            },
            refreshBtnStyle() {
                return REQEUST_BUTTON_STYLES[this.refreshStatus]
            },
            refreshMsg() {
                const messages = {
                    normal: 'Show Spoiler/NSFW',
                    loading: 'Showing...',
                    error: 'Failed',
                    timeout: 'Timeout',
                    downloaded: 'Reshow'
                }
                return messages[this.refreshStatus]
            }
        },
        mounted() {
            const value = GM_getValue(picture.split('&')[0])
            if (!value) return
            this.$refs.downloadButton.downloadStatus = value.downloadStatus
            this.$refs.downloadButton.value = {
                group: value.group,
                count: value.count
            }
            this.downloaded = true
            this.group = value.group
            this.count = value.count
        },
        methods: {
            download(value) {
                this.downloaded = true
                this.group = value.group
                this.count = value.count

                if (this.refresh && this.refreshStatus !== 'downloaded')
                    this.refreshOrigin()
                        .then(() => {
                            this.$emit('download', { ...value, origin: this.origin })
                        })
                else
                    this.$emit('download', { ...value, origin: this.origin })
            },
            toggle() {
                if (this.refresh && !this.originalImage && this.refreshStatus !== 'downloaded')
                    this.refreshOrigin()

                this.originalImage = !this.originalImage
            },
            refreshOrigin() {
                if (this.refreshStatus === 'loading') return
                this.refreshStatus = 'loading'
                return new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        url: this.picture,
                        responseType: 'document',
                        timeout: TIME_OUT,
                        onload: (data) => {
                            const doc = data.response
                            const a = doc.querySelector('.the-picture>a')
                            if (a) {
                                this.origin = a.href
                                const thumb = a.href.split('/')
                                thumb.splice(thumb.length - 1, 0, 'thumbnails')
                                this.thumb = thumb.join('/')
                                this.refreshStatus = 'downloaded'
                                resolve()
                                return
                            }
                            this.refreshStatus = 'error'
                            reject()
                        },
                        onerror: () => {
                            this.refreshStatus = 'error'
                            reject()
                        },
                        ontimeout: () => {
                            this.refreshStatus = 'timeout'
                            reject()
                        }
                    })
                })
            },
            openPicturePage() {
                window.open(picture)
            }
        }
    }
}

function parseOriginalImageURL(thumb_url) {
    let url = thumb_url
    if (thumb_url.indexOf('thumbnails/') > -1) {
        url = thumb_url.split('thumbnails/').join('')
    } else {
        const paths = thumb_url.split('pictures/')[1].split('/')
        if (paths.length > 2) {
            paths.splice(3, 1)
            url = [thumb_url.split('pictures/')[0], paths.join('/')].join('pictures/')
        }
    }

    return url
}

function getImageURLs(node) {
    const picture = node.querySelector('a').href
    const viewport = node.querySelector('.viewport')
    const thumb = viewport.style.background.split('"')[1]
    let origin = thumb
    if (FILTER_URLS.includes(origin))
        origin = thumb
    else
        origin = parseOriginalImageURL(origin)

    return { thumb, origin, picture }
}

function refreshGroup() {
    const itemId = (new URL(location.href)).searchParams.get('itemId')
    if (itemId && !GM_getValue(itemId)) {
        GM_setValue(itemId, true)

        if (globalState.count > 0)
            globalState.group += 1
    }
}

function renderPicturesExtension(thumbs) {
    function _initParentNode(parentNode) {
        parentNode.innerHTML = ''
        parentNode.style.setProperty('display', 'flex')
        parentNode.style.setProperty('flex-direction', 'row')
        parentNode.style.setProperty('flex-wrap', 'wrap')
        parentNode.style.setProperty('justify-content', 'center')
        parentNode.style.setProperty('align-items', 'center')
        const div1 = document.createElement('div')
        parentNode.parentNode.insertBefore(div1, parentNode)
        mountVM(div1, DownloadSequence)
        const div2 = document.createElement('div')
        const pageCount = document.querySelector('.listing-count-pages') || parentNode
        pageCount.parentNode.insertBefore(div2, pageCount)
        mountVM(div2, DownloadState)
    }

    const parentNode = thumbs[0].parentNode
    _initParentNode(parentNode)
    thumbs.forEach(thumb_node => {
        const imageURLs = getImageURLs(thumb_node)
        const preview = createPictruePreview(imageURLs)
        const div3 = document.createElement('div')
        parentNode.appendChild(div3)
        const previewVM = mountVM(div3, preview)
        previewVM.$on('download', (value) => {
            download({
                ...value,
                picture: imageURLs.picture,
                vm: previewVM.$refs.downloadButton,
            })
        })
    })
}

function render() {
    const objectMeta = document.querySelector('.object-meta')
    const thePicture = document.querySelector('.the-picture>a')
    if (objectMeta && thePicture)
        renderPictureExtension(objectMeta, thePicture)

    const thumbs = []
    document.querySelectorAll('.picture-icon.tbx-tooltip').forEach(thumb => {
        thumbs.push(thumb)
    })
    if (thumbs.length > 0)
        renderPicturesExtension(thumbs.reverse())
}

render()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址