GoFile Enhanced

Export files' download link. Use along with IDM, aria2 and similar downloaders.

// ==UserScript==
// @name         GoFile 增强
// @name:en      GoFile Enhanced
// @namespace    https://github.com/ewigl/gofile-enhanced
// @version      0.5.2
// @description  在 GoFile 文件下载页面添加亿个按钮,导出文件下载链接。配合 IDM、aria2 等下载器使用。
// @description:en Export files' download link. Use along with IDM, aria2 and similar downloaders.
// @author       Licht
// @license      MIT
// @homepage     https://github.com/ewigl/gofile-enhanced
// @match        http*://gofile.io/*
// @icon         https://gofile.io/dist/img/favicon16.png
// @connect      localhost
// @connect      *
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// ==/UserScript==

;(function () {
    'use strict'

    // New Api
    // appdata: literally, app data

    // function createNotification(title, message, type = 'success', duration = 3000)
    // function createPopup({ title, content, icon = null, backgroundOpacity = true, showCloseButton = true })
    // function createAlert(type, content)

    // IDM Exported Format (support CRLF(\r\n) only):
    // <
    // url
    // cookie: accountToken=ABCDEFG
    // >

    // constants
    const DEFAULT_LANGUAGE = 'en-US'

    const CRLF = '\r\n'

    const ARIA2_RPC_TUTORIAL_URL = 'https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface'

    const EXPORT_FORMAT = {
        // plain text
        txt: 'txt',
        // IDM
        ef2: 'ef2',
        // aria2
        aria2: 'aria2',
    }

    const GE_CONTAINER_ID = 'GofileEnhanced_Container'

    // const FOLDER_TYPE = 'folder'
    const FILE_TYPE = 'file'

    const I18N = {
        'zh-CN': {
            // Button
            allToTXT: '全部链接 -> TXT',
            selectedToTXT: '选中链接 -> TXT',
            allToEF2: '全部链接 -> IDM',
            selectedToEF2: '选中链接 -> IDM',
            allToARIA2: '全部链接 -> Aria2',
            selectedToARIA2: '选中链接 -> Aria2',
            aria2RpcSettings: '配置 Aria2 RPC',
            aria2RpcReset: '重置 Aria2 RPC',
            // Notification
            noFileSelected: '未选中任何文件',
            noFileSelectedDescription: '请先选中文件',
            noFiles: '没有文件可以下载',
            noFilesDescription: '没有可以下载的文件 暂不支持文件夹下载',
            // RPC
            rpcSendSuccess: '已通过 RPC 发送至 Aria2 下载',
            rpcSendFailed: '通过 RPC 发送至 Aria2 失败',
            unknownError: '未知错误',
            // RPC Settings
            rpcAddress: 'RPC 地址',
            rpcSecret: 'RPC 密钥',
            rpcDir: 'RPC 下载目录',
            // Common
            ok: '确定',
            cancel: '取消',
            success: '成功',
            fail: '失败',
            reset: '重置',
            to: '为',
        },
        'en-US': {
            // Button
            allToTXT: 'All links -> TXT',
            selectedToTXT: 'Selected links -> TXT',
            allToEF2: 'All links -> IDM',
            selectedToEF2: 'Selected links -> IDM',
            allToARIA2: 'All links -> Aria2',
            selectedToARIA2: 'Selected links -> Aria2',
            aria2RpcSettings: 'Aria2 RPC Settings',
            aria2RpcReset: 'Aria2 RPC Reset',
            // Notification
            noFileSelected: 'No file selected',
            noFileSelectedDescription: 'Please select files first',
            noFiles: 'No files can be downloaded',
            noFilesDescription: 'No files can be downloaded, folder download is not supported, yet',
            // RPC
            rpcSendSuccess: 'RPC send success',
            rpcSendFailed: 'RPC send failed',
            unknownError: 'Unknown error',
            // RPC Settings
            rpcAddress: 'RPC address',
            rpcSecret: 'RPC secret',
            rpcDir: 'RPC dir',
            // Common
            ok: 'OK',
            cancel: 'Cancel',
            success: 'Success',
            fail: 'Fail',
            reset: 'Reset',
            to: 'to',
        },
    }

    const ARIA2_RPC_CONFIG_KEY = {
        rpcAddress: 'aria2_rpc_address',
        rpcSecret: 'aria2_rpc_secret',
        rpcDir: 'aria2_rpc_dir',
    }

    const ARIA2_RPC_CONFIG_ICONS = {
        rpcAddress: 'fa-link',
        rpcSecret: 'fa-key',
        rpcDir: 'fa-folder',
    }

    const DEFAULT_CONFIG = {
        rpcSettings: [
            {
                name: ARIA2_RPC_CONFIG_KEY.rpcAddress,
                value: 'http://localhost:6800/jsonrpc',
            },
            {
                name: ARIA2_RPC_CONFIG_KEY.rpcSecret,
                value: '',
            },
            {
                name: ARIA2_RPC_CONFIG_KEY.rpcDir,
                value: '',
            },
        ],
    }

    const ICON_CLASS = {
        allToTXT: 'fas fa-clone',
        allToEF2: 'fas fa-paper-plane',
        allToARIA2: 'fas fa-circle-down',
        selectedToTXT: 'far fa-clone',
        selectedToEF2: 'far fa-paper-plane',
        selectedToARIA2: 'far fa-circle-down',
        aria2RpcSettings: 'fas fa-gear',
        aria2RpcReset: 'fas fa-rotate-left',
    }

    const utils = {
        getValue: (name) => GM_getValue(name),
        setValue(name, value) {
            GM_setValue(name, value)
        },
        initDefaultConfig() {
            DEFAULT_CONFIG.rpcSettings.forEach((item) => {
                utils.getValue(item.name) === undefined && utils.setValue(item.name, item.value)
            })
        },
        getTranslation(key) {
            const lang = I18N[navigator.language] ? navigator.language : DEFAULT_LANGUAGE
            return I18N[lang][key] || key // fallback to key
        },
        getToken: () => document.cookie,
        getAria2RpcConfig() {
            return {
                address: utils.getValue(ARIA2_RPC_CONFIG_KEY.rpcAddress),
                secret: utils.getValue(ARIA2_RPC_CONFIG_KEY.rpcSecret),
                dir:
                    utils.getValue(ARIA2_RPC_CONFIG_KEY.rpcDir).trim() === ''
                        ? undefined
                        : utils.getValue(ARIA2_RPC_CONFIG_KEY.rpcDir),
            }
        },
        resetRPCConfig() {
            DEFAULT_CONFIG.rpcSettings.forEach((item) => {
                utils.setValue(item.name, item.value)
                createNotification(
                    utils.getTranslation('success'),
                    `${utils.getTranslation('reset')} ${item.name} ${utils.getTranslation('to')} "${item.value}"`
                )
            })
            // for each DEFAULT_CONFIG.rpcSettings
        },
        downloadFile(links, format = 'txt') {
            const blob = new Blob([links], { type: 'text/plain;charset=utf-8' })
            const url = URL.createObjectURL(blob)
            const link = document.createElement('a')
            link.href = url
            // generate file neme by timestamp
            link.download = `${appdata.fileManager.mainContent.data.name} - ${new Date().getTime()}.${format}`
            link.click()
            URL.revokeObjectURL(url)
        },
        getButtonTemplate(iconClass, buttonText) {
            return `<a href="javascript:void(0)" id="index_GofileEnhanced" class="hover:text-blue-500 flex items-center gap-2" aria-label="${buttonText}"><i class="${iconClass}"></i> ${buttonText} </a>`
        },
        getFormInputItemTemplate(name, i18nKey) {
            return `
            <div class="space-y-2">
                <label for="${name}" class="block text-sm font-medium text-gray-300">
                    ${utils.getTranslation(i18nKey)}
                </label>
                <div class="relative">
                    <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                        <i class="fas ${ARIA2_RPC_CONFIG_ICONS[i18nKey]} text-gray-400"></i>
                    </div>
                    <input 
                        type="text" 
                        id="${name}" 
                        name="${name}" 
                        class="w-full pl-10 pr-3 py-2 bg-gray-700 rounded-lg border border-gray-600 focus:ring-2
                            focus:ring-blue-500 focus:border-blue-500 focus:outline-none transition duration-200 text-white placeholder-gray-400"
                        value="${utils.getValue(name)}"
                    >
                </div>
            </div>
            `
        },
        getButtonDom(config) {
            const { selectMode = false, format = 'txt' } = config

            const buttonText = utils.getTranslation(
                selectMode ? 'selectedTo' + format.toUpperCase() : 'allTo' + format.toUpperCase()
            )
            const iconClass = selectMode
                ? ICON_CLASS['selectedTo' + format.toUpperCase()]
                : ICON_CLASS['allTo' + format.toUpperCase()]

            const button = document.createElement('li')
            button.innerHTML = this.getButtonTemplate(iconClass, buttonText)
            // add click event for each button
            button.addEventListener('click', operations.exportToFile.bind(null, selectMode, format))
            return button
        },
        getRPCButtonDom(type = 'settings') {
            const buttonText = utils.getTranslation(type === 'settings' ? 'aria2RpcSettings' : 'aria2RpcReset')
            const iconClass = type === 'settings' ? ICON_CLASS.aria2RpcSettings : ICON_CLASS.aria2RpcReset

            return this.getButtonTemplate(iconClass, buttonText)
        },
        getRPCSettingsDom() {
            return `
            <div class="space-y-4">
                <div class="bg-blue-900 bg-opacity-20 border border-blue-800 rounded-lg p-4">
                    <div class="flex items-center space-x-3">
                        <i class="fas fa-info-circle text-blue-400 text-xl"></i>
                        <p class="text-gray-300 text-sm">
                            <a href="${ARIA2_RPC_TUTORIAL_URL}" target="_blank" rel="noopener noreferrer"> ${ARIA2_RPC_TUTORIAL_URL} </a>
                        </p>
                    </div>
                </div>

                <form id="GofileEnhanced_Form" class="space-y-4">

                ${Object.keys(ARIA2_RPC_CONFIG_KEY)
                    .map((key) => this.getFormInputItemTemplate(ARIA2_RPC_CONFIG_KEY[key], key))
                    .join('')}

                    <button
                        id="GofileEnhanced_RPC_Submit"
                        type="submit"
                        class="w-full py-3 bg-blue-600 rounded-lg hover:bg-blue-700 transition duration-300 
                            ease-in-out text-center text-white font-semibold flex items-center justify-center space-x-2"
                    >
                        <i class="fas fa-check"></i>
                        <span> ${utils.getTranslation('ok')} </span>
                    </button>
                </form>
            </div>
            `
        },
        getHrLine() {
            const hrLine = document.createElement('li')
            hrLine.classList.add('border-b', 'border-gray-700')
            return hrLine
        },
    }

    const operations = {
        exportToFile(selectMode = false, format = 'txt') {
            const objectKeys = Object.keys(
                selectMode ? appdata.fileManager.contentsSelected : appdata.fileManager.mainContent.data.children
            )

            const fileKeys = objectKeys.filter((key) => appdata.fileManager.mainContent.data.children[key].type === FILE_TYPE)

            if (fileKeys.length === 0) {
                return createNotification(
                    selectMode ? utils.getTranslation('noFileSelected') : utils.getTranslation('noFiles'),
                    selectMode ? utils.getTranslation('noFileSelectedDescription') : utils.getTranslation('noFilesDescription'),
                    'warning'
                )
            }

            if (format === EXPORT_FORMAT.aria2) {
                return operations.sendToRPC(fileKeys.map((key) => appdata.fileManager.mainContent.data.children[key].link))
            }

            const formatMap = {
                [EXPORT_FORMAT.ef2]: (item) => `<${CRLF}${item.link}${CRLF}cookie: ${utils.getToken()}${CRLF}>${CRLF}`,
                [EXPORT_FORMAT.txt]: (item) => `${item.link}${CRLF}`,
            }

            const links = fileKeys
                .map((key) => {
                    const item = appdata.fileManager.mainContent.data.children[key]
                    return formatMap[format](item)
                })
                .join('')

            utils.downloadFile(links, format)
        },
        sendToRPC: async (fileLinks = []) => {
            const rpcConfig = utils.getAria2RpcConfig()

            const rpcData = fileLinks.map((link) => {
                return {
                    id: new Date().getTime(),
                    jsonrpc: '2.0',
                    method: 'aria2.addUri',
                    params: [
                        `token:${rpcConfig.secret}`,
                        [link],
                        {
                            header: [`Cookie: ${utils.getToken()}`],
                            dir: rpcConfig.dir,
                        },
                    ],
                }
            })

            GM_xmlhttpRequest({
                method: 'POST',
                url: rpcConfig.address,
                data: JSON.stringify(rpcData),
                onload: (httpRes) => {
                    if (httpRes.status === 200) {
                        try {
                            const responseArray = JSON.parse(httpRes.response)

                            responseArray.forEach((item) => {
                                if (item.error) {
                                    createNotification(
                                        utils.getTranslation('fail'),
                                        `${utils.getTranslation('rpcSendFailed')} / ${item.error.code} - ${item.error.message}`,
                                        'error'
                                    )
                                } else {
                                    createNotification(
                                        utils.getTranslation('success'),
                                        `${utils.getTranslation('rpcSendSuccess')} / ${item.result}`
                                    )
                                }
                            })
                        } catch (error) {
                            createAlert('error', error.toString())
                        }
                    } else {
                        createAlert(
                            'error',
                            `${utils.getTranslation('rpcSendFailed')} / ${httpRes.status} - ${httpRes.statusText}`
                        )
                    }
                },
                onerror: (error) => {
                    // createNotification(utils.getTranslation('fail'), JSON.stringify(error), 'error')
                    createAlert('error', JSON.stringify(error))
                },
                onabort: () => {
                    createAlert('error', utils.getTranslation('unknownError') + ' / (abort)')
                },
            })
        },
        addButtonsToSidebar() {
            // boeder line

            const buttonForAllConfigs = [
                { selectMode: false, format: EXPORT_FORMAT.txt },
                { selectMode: false, format: EXPORT_FORMAT.ef2 },
                { selectMode: false, format: EXPORT_FORMAT.aria2 },
            ]

            const buttonsForSelectedConfigs = [
                { selectMode: true, format: EXPORT_FORMAT.txt },
                { selectMode: true, format: EXPORT_FORMAT.ef2 },
                { selectMode: true, format: EXPORT_FORMAT.aria2 },
            ]

            // map buttons (except aria2) to get button dom element
            const buttonsForAll = buttonForAllConfigs.map((config) => utils.getButtonDom(config))
            const buttonsForSelected = buttonsForSelectedConfigs.map((config) => utils.getButtonDom(config))

            // create rpc settings button
            const rpcSettingsButton = document.createElement('div')
            rpcSettingsButton.innerHTML = utils.getRPCButtonDom()
            // click rpc settings button to open modal
            rpcSettingsButton.addEventListener('click', () => {
                createPopup({
                    title: utils.getTranslation('aria2RpcSettings'),
                    content: utils.getRPCSettingsDom(),
                    icon: 'fas fa-gears',
                })

                const form = document.forms['GofileEnhanced_Form']

                if (form) {
                    form.addEventListener('submit', (event) => {
                        event.preventDefault()
                        Object.keys(ARIA2_RPC_CONFIG_KEY).forEach((key) => {
                            utils.setValue(ARIA2_RPC_CONFIG_KEY[key], form.elements[ARIA2_RPC_CONFIG_KEY[key]].value)
                        })
                        closePopup()
                    })
                }
            })

            const rpcResetButton = document.createElement('div')
            rpcResetButton.innerHTML = utils.getRPCButtonDom('reset')
            // click aria2 rpc reset button to reset rpc config
            rpcResetButton.addEventListener('click', () => {
                utils.resetRPCConfig()
            })

            const container = document.createElement('ul')
            // add id
            container.id = GE_CONTAINER_ID
            // add class to container
            container.classList.add('pt-4', 'space-y-4', 'border-gray-700')
            // append buttons to container
            container.append(
                utils.getHrLine(),
                ...buttonsForAll,
                utils.getHrLine(),
                ...buttonsForSelected,
                utils.getHrLine(),
                rpcSettingsButton,
                rpcResetButton
            )
            document.querySelector('#index_sidebar').appendChild(container)
        },
    }

    const main = {
        init() {
            // init RPC config
            utils.initDefaultConfig()

            // observe changes to the DOM
            const observer = new MutationObserver((_mutations, _obs) => {
                const container = document.getElementById(GE_CONTAINER_ID)

                if (appdata.fileManager?.mainContent?.data) {
                    !container && operations.addButtonsToSidebar()
                    // Stop observing
                    // obs.disconnect()
                } else {
                    // remove GofileEnhanced_Container
                    container && container.remove()
                }
            })

            // Ovserve the target node "#index_main", which is in the DOM initially.
            const targetNode = document.getElementById('index_main')
            const config = { childList: true, subtree: true }
            if (targetNode) {
                observer.observe(targetNode, config)
            } else {
                console.log('#index_main not found.')
            }
        },
    }

    main.init()
})()