Greasy Fork

Greasy Fork is available in English.

GoFile 增强

GoFile 文件批量下载。批量导出下载链接。可以配合 AB Download Manager、IDM、Aria2 等下载器使用

当前为 2025-08-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GoFile 增强
// @name:en      GoFile Enhanced
// @namespace    https://github.com/ewigl/gofile-enhanced
// @version      0.6.5
// @description  GoFile 文件批量下载。批量导出下载链接。可以配合 AB Download Manager、IDM、Aria2 等下载器使用
// @description:en  Download Gofiles in batch. Support AB Download Manager, IDM and Aria2 related 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'

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

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

    // IDM EF2 File Formats, 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 ABDM_HOEMPAGE_URL = 'https://github.com/amir1376/ab-download-manager'

    const SUPPORTED_FORMATS = [
        { name: 'Direct', value: 'direct' },
        { name: 'ABDM', value: 'abdm' },
        { name: 'Aria2', value: 'rpc' },
        { name: 'IDM', value: 'ef2' },
    ]

    const GE_CONTAINER_ID = 'GofileEnhanced_Container'

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

    const I18N = {
        'zh-CN': {
            // Button
            downloadAll: '下载全部',
            downloadSelected: '下载选中',
            exportAll: '导出全部',
            exportSelected: '导出选中',
            sendAll: '发送全部',
            sendSelected: '发送选中',
            aria2RpcSettings: '配置 Aria2 RPC',
            // Notification
            noFileSelected: '未选中任何文件',
            noFileSelectedDescription: '请至少选中一个文件',
            noFiles: '没有文件可以下载',
            noFilesDescription: '没有可以下载的文件, 暂不支持文件夹下载',
            // ABDM
            abdmSettings: '配置 AB Download Manager',
            abdmPort: 'ABDM 端口',
            abdmSendSuccess: '下载任务已发送至 ABDM',
            abdmSendFailed: '下载任务未成功发送至 ABDM',
            // RPC
            rpcAddress: 'RPC 地址',
            rpcSecret: 'RPC 密钥',
            rpcDir: 'RPC 下载目录',
            rpcSendSuccess: '下载任务已成功发送至 Aria2',
            rpcSendFailed: '下载任务未成功发送至 Aria2',
            // Common
            cancel: '取消',
            checkPort: '请检查端口配置',
            config: '配置',
            connected: '已连接',
            connection: '连接',
            error: '错误',
            fail: '失败',
            notConfigured: '未配置',
            ok: '确定',
            port: '端口',
            reset: '重置',
            success: '成功',
            test: '测试',
            to: '为',
            unknownError: '未知错误',
            unSupportedFormat: '不支持的格式',
        },
        'en-US': {
            // Button
            downloadAll: 'Download All',
            downloadSelected: 'Download Selected',
            exportAll: 'Export All',
            exportSelected: 'Export Selected',
            sendAll: 'Send All',
            sendSelected: 'Send Selected',
            aria2RpcSettings: 'Aria2 RPC Settings',
            // Notification
            noFileSelected: 'No file selected',
            noFileSelectedDescription: 'Please select at least 1 file first',
            noFiles: 'No file can be downloaded',
            noFilesDescription: 'No file can be downloaded, folder download is not supported',
            // ABDM
            abdmSettings: 'AB Download Manager Settings',
            abdmPort: 'ABDM Port',
            abdmSendSuccess: 'Download task sent to ABDM',
            abdmSendFailed: 'Download task failed to send to ABDM',
            // RPC
            rpcAddress: 'RPC Address',
            rpcSecret: 'RPC Secret',
            rpcDir: 'RPC Dir',
            rpcSendSuccess: 'download task sent to Aria2',
            rpcSendFailed: 'download task failed to send to Aria2',
            // Common
            cancel: 'Cancel',
            checkPort: 'please check port configuration',
            config: 'Config',
            connected: 'connected',
            connection: 'connection',
            error: 'error',
            fail: 'failed',
            notConfigured: 'not configured',
            ok: 'OK',
            port: 'port',
            reset: 'Reset',
            success: 'Success',
            test: 'Test',
            to: 'to',
            unknownError: 'Unknown error',
            unSupportedFormat: 'Unsupported format',
        },
    }

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

    const ABDM_CONFIG = {
        abdmPort: 'abdm_port',
    }

    const DEFAULT_CONFIGS = {
        rpcSettings: [
            {
                name: ARIA2_RPC_CONFIG.rpcAddress,
                value: 'http://localhost:6800/jsonrpc',
            },
            {
                name: ARIA2_RPC_CONFIG.rpcSecret,
                value: '',
            },
            {
                name: ARIA2_RPC_CONFIG.rpcDir,
                value: '',
            },
        ],
        abdmSettings: [
            {
                name: ABDM_CONFIG.abdmPort,
                value: '15151',
            },
        ],
    }

    const ICON_CLASS = {
        gofileEnhanced: 'fa-brands fa-google-plus',
        downloadAll: 'fas fa-circle-down',
        downloadSelected: 'far fa-circle-down',
        exportAll: 'fas fa-file',
        exportSelected: 'far fa-file',
        sendAll: 'fas fa-paper-plane',
        sendSelected: 'far fa-paper-plane',
        abdmPort: 'fas fa-plug',
        rpcAddress: 'fa-link',
        rpcSecret: 'fa-key',
        rpcDir: 'fa-folder',
        folder: 'fas fa-folder',
        key: 'fas fa-key',
        link: 'fas fa-link',
        reset: 'fas fa-rotate-left',
        settings: 'fas fa-gear',
        test: 'fas fa-circle-nodes',
    }

    const utils = {
        getValue: (name) => GM_getValue(name),
        setValue(name, value) {
            GM_setValue(name, value)
        },
        // init default configs if not exists
        initDefaultConfig() {
            DEFAULT_CONFIGS.abdmSettings.forEach((item) => {
                utils.getValue(item.name) === undefined && utils.setValue(item.name, item.value)
            })
            DEFAULT_CONFIGS.rpcSettings.forEach((item) => {
                utils.getValue(item.name) === undefined && utils.setValue(item.name, item.value)
            })
        },
        // get translation by key
        getTranslation(key) {
            const lang = I18N[navigator.language] ? navigator.language : DEFAULT_LANGUAGE
            return I18N[lang][key] || key // fallback to key
        },
        // get token from cookie
        getToken: () => document.cookie,
        // Direct download related
        goDirectLink(links) {
            links.forEach((link) => {
                window.open(link, link)
            })
        },
        // ABDM related
        testABDMConnection() {
            const port = utils.getValue(ABDM_CONFIG.abdmPort)

            const connectedString = `${utils.getTranslation('abdmPort')} ${utils.getTranslation('connected')}`
            const connectionFailString = `${utils.getTranslation('abdmPort')} ${utils.getTranslation('connection')} ${utils.getTranslation('fail')}`
            const notConfiguredString = `${utils.getTranslation('abdmPort')} ${utils.getTranslation('notConfigured')}`

            if (port) {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `http://localhost:${port}/ping`,
                    onload: (response) => {
                        if (response.status === 200) {
                            createNotification(utils.getTranslation('success'), connectedString)
                        } else {
                            createNotification(utils.getTranslation('error'), connectionFailString, 'error')
                        }
                    },
                    onerror: (_error) => {
                        createNotification(utils.getTranslation('error'), `${connectionFailString}, ${utils.getTranslation('checkPort')}`, 'error')
                    },
                    onabort: () => {
                        createAlert('error', `${utils.getTranslation('unknownError')}, Aborted.`)
                    },
                })
            } else {
                createNotification('error', notConfiguredString, 'error')
            }
        },
        sendToABDM(tbdItems, cookie) {
            const port = utils.getValue(ABDM_CONFIG.abdmPort)

            if (!port) {
                return createNotification('error', `${utils.getTranslation('abdmPort')} ${utils.getTranslation('notConfigured')}`, 'error')
            }

            const postDatas = tbdItems.map((item) => {
                return {
                    downloadSource: {
                        link: item.link,
                        headers: {
                            cookie,
                        },
                    },
                    name: item.name,
                }
            })

            postDatas.forEach((data) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: `http://localhost:${port}/start-headless-download`,
                    data: JSON.stringify(data),
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    onload: (httpRes) => {
                        if (httpRes.status === 200) {
                            createNotification(utils.getTranslation('success'), `${data.name} ${utils.getTranslation('abdmSendSuccess')}`)
                        } else {
                            createNotification('error', `${utils.getTranslation('abdmSendFailed')} / ${httpRes.status} - ${httpRes.statusText}`, 'error')
                        }
                    },
                    onerror: (_error) => {
                        createNotification('error', `${utils.getTranslation('abdmSendFailed')}, ${utils.getTranslation('checkPort')}`, 'error')
                    },
                    onabort: () => {
                        createAlert('error', `${utils.getTranslation('unknownError')}, Aborted.`)
                    },
                })
            })
        },
        // Aria2 related
        getAria2RpcConfig() {
            return {
                address: utils.getValue(ARIA2_RPC_CONFIG.rpcAddress),
                secret: utils.getValue(ARIA2_RPC_CONFIG.rpcSecret),
                dir: utils.getValue(ARIA2_RPC_CONFIG.rpcDir).trim() === '' ? undefined : utils.getValue(ARIA2_RPC_CONFIG.rpcDir),
            }
        },
        resetRPCConfig() {
            DEFAULT_CONFIGS.rpcSettings.forEach((item) => {
                utils.setValue(item.name, item.value)
                createNotification(utils.getTranslation('success'), `${utils.getTranslation('reset')} ${item.name} ${utils.getTranslation('to')} "${item.value}"`)
            })
        },
        sendToRPC(fileLinks, cookie) {
            const { address, secret, dir } = utils.getAria2RpcConfig()

            const header = [`Cookie: ${cookie}`]

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

            // AJAX
            GM_xmlhttpRequest({
                method: 'POST',
                url: 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('error'), `${utils.getTranslation('rpcSendFailed')} / ${item.error.code} - ${item.error.message}`, 'error')
                                } else {
                                    createNotification(utils.getTranslation('success'), `${utils.getTranslation('rpcSendSuccess')} / ${item.result}`)
                                }
                            })
                        } catch (error) {
                            createNotification(utils.getTranslation('error'), `${error.toString()}`, 'error')
                        }
                    } else {
                        createNotification(utils.getTranslation('error'), `${utils.getTranslation('rpcSendFailed')} / ${httpRes.status} - ${httpRes.statusText}`, 'error')
                    }
                },
                onerror: (error) => {
                    createNotification(utils.getTranslation('error'), JSON.stringify(error), 'error')
                },
                onabort: () => {
                    createAlert('error', `${utils.getTranslation('unknownError')}, Aborted.`)
                },
            })
        },
        // IDM related
        downloadFile(content, format) {
            const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
            const url = URL.createObjectURL(blob)
            const link = document.createElement('a')
            link.href = url
            // generate file name by timestamp
            link.download = `${appdata.fileManager.mainContent.data.name} - ${new Date().getTime()}.${format.value}`
            link.click()
            URL.revokeObjectURL(url)
        },
        // DOM related
        getHrLine() {
            const hrLine = document.createElement('li')
            hrLine.classList.add('border-b', 'border-gray-700')
            return hrLine
        },
        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>
            `
        },
        getRegularButtons(format) {
            // Header Title
            const formatTitleElement = document.createElement('li')
            formatTitleElement.innerHTML = `
            <span class="flex items-center gap-2 text-blue-500 font-bold">
                <i class="${ICON_CLASS.gofileEnhanced}"></i>
                ${format.name}
            </span>
            `

            // buttonText
            let exportAllText, exportSelectedText, exportAllIconClass, exportSelectedIconClass

            switch (format.name) {
                case 'ABDM':
                case 'Aria2':
                    exportAllText = utils.getTranslation('sendAll')
                    exportAllIconClass = ICON_CLASS.sendAll
                    exportSelectedText = utils.getTranslation('sendSelected')
                    exportSelectedIconClass = ICON_CLASS.sendSelected
                    break
                case 'IDM':
                    exportAllText = utils.getTranslation('exportAll')
                    exportAllIconClass = ICON_CLASS.exportAll
                    exportSelectedText = utils.getTranslation('exportSelected')
                    exportSelectedIconClass = ICON_CLASS.exportSelected
                    break
                default:
                    exportAllText = utils.getTranslation('downloadAll')
                    exportAllIconClass = ICON_CLASS.downloadAll
                    exportSelectedText = utils.getTranslation('downloadSelected')
                    exportSelectedIconClass = ICON_CLASS.downloadSelected
                    break
            }

            // create export buttons
            const exportAllButton = document.createElement('li')
            const exportSelectedButton = document.createElement('li')

            // set innerHTML
            exportAllButton.innerHTML = this.getButtonTemplate(exportAllIconClass, exportAllText)
            exportSelectedButton.innerHTML = this.getButtonTemplate(exportSelectedIconClass, exportSelectedText)

            // add click event for each button
            exportAllButton.addEventListener('click', operations.handleExport.bind(null, false, format))
            exportSelectedButton.addEventListener('click', operations.handleExport.bind(null, true, format))

            return [formatTitleElement, exportAllButton, exportSelectedButton]
        },
        getCustomButtonDom(type, format) {
            // type: settings, reset, test...
            // format: direct, abdm, rpc, ef2...

            const iconClass = ICON_CLASS[type] || ICON_CLASS.settings
            const buttonText = format ? `${utils.getTranslation(type)} ${format.toUpperCase()}` : utils.getTranslation(type)

            return this.getButtonTemplate(iconClass, buttonText)
        },
        // DOM ABDM related
        getABDMButtons() {
            // ABDM settings button
            const abdmSettingsButton = document.createElement('div')
            abdmSettingsButton.innerHTML = utils.getCustomButtonDom('config', 'abdm')

            abdmSettingsButton.addEventListener('click', () => {
                createPopup({
                    title: utils.getTranslation('abdmSettings'),
                    content: utils.getConfigPanel('ABDM', ABDM_CONFIG, ABDM_HOEMPAGE_URL),
                    icon: 'fas fa-gears',
                })

                const form = document.forms['GofileEnhanced_Form_ABDM']

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

            const testABDMButton = document.createElement('div')
            testABDMButton.innerHTML = utils.getCustomButtonDom('test', 'abdm')
            testABDMButton.addEventListener('click', () => {
                utils.testABDMConnection()
            })

            return [abdmSettingsButton, testABDMButton]
        },
        // DOM Aria2 related
        getAria2Buttons() {
            // create rpc settings button
            const rpcSettingsButton = document.createElement('div')
            rpcSettingsButton.innerHTML = utils.getCustomButtonDom('config', 'rpc')
            // click rpc settings button to open modal
            rpcSettingsButton.addEventListener('click', () => {
                createPopup({
                    title: utils.getTranslation('aria2RpcSettings'),
                    content: utils.getConfigPanel('RPC', ARIA2_RPC_CONFIG, ARIA2_RPC_TUTORIAL_URL),
                    icon: 'fas fa-gears',
                })

                const form = document.forms['GofileEnhanced_Form_RPC']

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

            const rpcResetButton = document.createElement('div')
            rpcResetButton.innerHTML = utils.getCustomButtonDom('reset', 'rpc')
            rpcResetButton.addEventListener('click', () => {
                utils.resetRPCConfig()
            })

            return [rpcSettingsButton, rpcResetButton]
        },
        getButtonsByFormat(format) {
            let elements = this.getRegularButtons(format)

            switch (format.name) {
                case 'ABDM':
                    elements = [...elements, ...this.getABDMButtons()]
                    break
                case 'Aria2':
                    elements = [...elements, ...this.getAria2Buttons()]
                    break
                default:
                    break
            }

            return [this.getHrLine(), ...elements]
        },
        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 ${ICON_CLASS[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>
            `
        },
        getConfigPanel(ID, CONFIG, TITLE) {
            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="${TITLE}" target="_blank" rel="noopener noreferrer"> ${TITLE} </a>
                        </p>
                    </div>
                </div>

                <form id="GofileEnhanced_Form_${ID}" class="space-y-4">

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

                    <button
                        id="GofileEnhanced_${ID}_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>
            `
        },
    }

    const operations = {
        handleExport(selectMode, format) {
            const allFiles = appdata.fileManager.mainContent.data.children
            const selectedKeys = appdata.fileManager.contentsSelected

            // all file keys or selected file keys
            const fileKeys = Object.keys(selectMode ? selectedKeys : allFiles)

            // to be downloaded keys
            const tbdKeys = fileKeys.filter((key) => allFiles[key].type === FILE_TYPE)

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

            const cookie = utils.getToken()
            const tbdItems = tbdKeys.map((key) => allFiles[key])
            const tbdLinks = tbdKeys.map((key) => allFiles[key].link)

            switch (format.name) {
                case 'Direct':
                    utils.goDirectLink(tbdLinks)
                    break
                case 'ABDM':
                    utils.sendToABDM(tbdItems, cookie)
                    break
                case 'Aria2':
                    utils.sendToRPC(tbdLinks, cookie)
                    break
                case 'IDM':
                    const IDMLinks = tbdLinks
                        .map((link) => {
                            return `<${CRLF}${link}${CRLF}cookie: ${cookie}${CRLF}>${CRLF}`
                        })
                        .join('')
                    utils.downloadFile(IDMLinks, format)
                    break
                default:
                    createNotification(utils.getTranslation('error'), `${format.name} ${utils.getTranslation('unSupportedFormat')}`, 'error')
                    break
            }
        },
        // add buttons to sidebar
        addContainerToSidebar() {
            // create container
            const container = document.createElement('ul')
            container.id = GE_CONTAINER_ID
            // 'border-t', 'border-gray-700', 'mt-4',
            container.classList.add('pt-4', 'space-y-4')

            // append buttons to container
            SUPPORTED_FORMATS.forEach((format) => {
                utils.getButtonsByFormat(format).forEach((item) => {
                    container.appendChild(item)
                })
            })

            // append container to sidebar
            document.querySelector('#index_sidebar').appendChild(container)
        },
    }

    const main = {
        init() {
            utils.initDefaultConfig()

            // Observe changes in the DOM
            const observer = new MutationObserver((_mutations, _obs) => {
                // Check if the target node is available
                const container = document.getElementById(GE_CONTAINER_ID)

                // Check if the mainContent is available
                if (appdata.fileManager?.mainContent?.data) {
                    // Add buttons to sidebar
                    !container && operations.addContainerToSidebar()
                    // 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.')
            }
        },
    }

    // Script Entry Point
    main.init()
})()