// ==UserScript==
// @name GoFile 增强
// @name:en GoFile Enhanced
// @namespace https://github.com/ewigl/gofile-enhanced
// @version 0.6.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'
// Api
// appdata: literally, app data
// Funcs
// function createNotification(title, message, type = 'success', duration = 3000)
// function createPopup({ title, content, icon = null, backgroundOpacity = true, showCloseButton = true })
// function createAlert(type, content)
// Formats
//
// 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 SUPPORTED_FORMATS = [
{ name: 'Direct', value: 'direct' },
{ name: 'IDM', value: 'ef2' },
{ name: 'Aria2', value: 'rpc' },
]
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: '配置 RPC',
aria2RpcReset: '重置 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
downloadAll: 'Download All',
downloadSelected: 'Download Selected',
exportAll: 'Export All',
exportSelected: 'Export Selected',
sendAll: 'Send All',
sendSelected: 'Send Selected',
aria2RpcSettings: 'RPC Settings',
aria2RpcReset: '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 = {
gofileEnhanced: 'fa-brands fa-google-plus',
exportAll: 'fas fa-circle-down',
exportSelected: '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)
},
// init default configs if not exists
initDefaultConfig() {
DEFAULT_CONFIG.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,
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
},
goDirectLink(links) {
links.forEach((link) => {
window.open(link, link)
})
},
downloadFile(links, format) {
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 name by timestamp
link.download = `${appdata.fileManager.mainContent.data.name} - ${new Date().getTime()}.${format.value}`
link.click()
URL.revokeObjectURL(url)
},
sendToRPC: async (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,
},
],
}
})
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('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)')
},
})
},
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
switch (format.name) {
case 'IDM':
exportAllText = utils.getTranslation('exportAll')
exportSelectedText = utils.getTranslation('exportSelected')
break
case 'Aria2':
exportAllText = utils.getTranslation('sendAll')
exportSelectedText = utils.getTranslation('sendSelected')
break
default:
exportAllText = utils.getTranslation('downloadAll')
exportSelectedText = utils.getTranslation('downloadSelected')
break
}
// create export buttons
const exportAllButton = document.createElement('li')
const exportSelectedButton = document.createElement('li')
// set innerHTML
exportAllButton.innerHTML = this.getButtonTemplate(ICON_CLASS.exportAll, exportAllText)
exportSelectedButton.innerHTML = this.getButtonTemplate(ICON_CLASS.exportSelected, 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]
},
getAria2Buttons() {
// create rpc settings button
const rpcSettingsButton = document.createElement('div')
rpcSettingsButton.innerHTML = utils.getRPCButtonDom('settings')
// 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()
})
return [rpcSettingsButton, rpcResetButton]
},
getRPCButtonDom(type) {
const buttonText = utils.getTranslation(type === 'settings' ? 'aria2RpcSettings' : 'aria2RpcReset')
const iconClass = type === 'settings' ? ICON_CLASS.aria2RpcSettings : ICON_CLASS.aria2RpcReset
return this.getButtonTemplate(iconClass, buttonText)
},
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>
`
},
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>
`
},
getButtonsByFormat(format) {
let elements = this.getRegularButtons(format)
switch (format.name) {
case 'IDM':
break
case 'Aria2':
elements = [...elements, ...this.getAria2Buttons()]
break
default:
break
}
return [this.getHrLine(), ...elements]
},
}
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 tbdLinks = tbdKeys.map((key) => allFiles[key].link)
switch (format.name) {
case 'Direct':
utils.goDirectLink(tbdLinks)
break
case 'IDM':
const IDMLinks = tbdLinks
.map((link) => {
return `<${CRLF}${link}${CRLF}cookie: ${cookie}${CRLF}>${CRLF}`
})
.join('')
utils.downloadFile(IDMLinks, format)
break
case 'Aria2':
utils.sendToRPC(tbdLinks, cookie)
break
default:
console.log('Unsupported format.')
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()
})()