// ==UserScript==
// @name GoFile 增强
// @name:en GoFile Enhanced
// @namespace https://github.com/ewigl/gofile-enhanced
// @version 0.5.0
// @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/d/*
// @icon https://gofile.io/dist/img/favicon16.png
// @connect localhost
// @connect *
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @downloadURL none
// ==/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 })
// 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 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: '重置 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: 'Reset RPC settings',
// 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) => I18N[navigator.language || DEFAULT_LANGUAGE][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 ` ${buttonText} `
},
getFormInputItemTemplate(name, i18nKey) {
return `
`
},
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 `
`
},
}
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) {
createNotification(utils.getTranslation('fail'), error.toString(), 'error')
}
} else {
createNotification(
utils.getTranslation('fail'),
`${utils.getTranslation('rpcSendFailed')} / ${httpRes.status} - ${httpRes.statusText}`,
'error'
)
}
},
onerror: (error) => {
createNotification(utils.getTranslation('fail'), JSON.stringify(error), 'error')
},
onabort: () => {
createNotification(
utils.getTranslation('fail'),
utils.getTranslation('unknownError') + ' / (abort)',
'error'
)
},
})
},
addButtonsToSidebar() {
// boeder line
const hrLine = document.createElement('li')
hrLine.classList.add('border-b', 'border-gray-700')
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(),
})
)
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 class to container
container.classList.add('pt-4', 'space-y-4', 'border-gray-700')
// append buttons to container
container.append(
hrLine.cloneNode(true),
...buttonsForAll,
hrLine.cloneNode(true),
...buttonsForSelected,
hrLine.cloneNode(true),
rpcSettingsButton,
rpcResetButton
)
document.querySelector('#index_sidebar').appendChild(container)
},
addRPCSubmitEventListener() {
document.addEventListener('click', function (event) {
if (event.target.matches('#GofileEnhanced_RPC_Submit')) {
event.preventDefault()
const form = document.forms['GofileEnhanced_Form']
// To be optimized
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 main = {
init() {
// init RPC config
utils.initDefaultConfig()
// add aria2 rpc submit event listener
operations.addRPCSubmitEventListener()
// check if appdata.fileManager.mainContent.data is ready
let countTimeOut = 0
const interval = setInterval(() => {
if (appdata.fileManager.mainContent.data) {
operations.addButtonsToSidebar()
clearInterval(interval)
} else {
// 30s timeout, if appdata.fileManager.mainContent.data is still not ready
if (countTimeOut > 59) {
clearInterval(interval)
}
countTimeOut++
}
}, 500)
},
}
main.init()
})()