diff --git a/README.md b/README.md index 4ca1bd7..137b991 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ yarn run build --universal --mac # 打包win yarn run build --win -# 打包win +# 打包linux yarn run build --linux ``` diff --git a/components.d.ts b/components.d.ts index 620980f..f1b4c41 100755 --- a/components.d.ts +++ b/components.d.ts @@ -8,6 +8,9 @@ export {} declare module 'vue' { export interface GlobalComponents { ElButton: typeof import('element-plus/es')['ElButton'] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] + ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElContainer: typeof import('element-plus/es')['ElContainer'] ElFooter: typeof import('element-plus/es')['ElFooter'] ElForm: typeof import('element-plus/es')['ElForm'] @@ -15,11 +18,11 @@ declare module 'vue' { ElHeader: typeof import('element-plus/es')['ElHeader'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] - ElLink: typeof import('element-plus/es')['ElLink'] ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] + ElPopover: typeof import('element-plus/es')['ElPopover'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] Footer: typeof import('./src/components/layout/Footer.vue')['default'] diff --git a/electron/main/cert.ts b/electron/main/cert.ts index 7ba48df..020569d 100755 --- a/electron/main/cert.ts +++ b/electron/main/cert.ts @@ -9,51 +9,62 @@ export function checkCertInstalled() { return fs.existsSync(CONFIG.INSTALL_CERT_FLAG) } -export async function installCert(checkInstalled = true) { - if (checkInstalled && checkCertInstalled()) { - return; +export function installCert(checkInstalled = true) { + try { + if (checkInstalled && checkCertInstalled()) { + return; + } + mkdirp.sync(path.dirname(CONFIG.INSTALL_CERT_FLAG)) + + if (process.platform === 'darwin') { + handleMacCertInstallation() + } else if (process.platform === 'win32') { + handleWindowsCertInstallation() + } else { + handleOtherCertInstallation() + } + } catch (e) { + handleOtherCertInstallation() } - mkdirp.sync(path.dirname(CONFIG.INSTALL_CERT_FLAG)) - - if (process.platform === 'darwin') { - return new Promise((resolve, reject) => { - clipboard.writeText( - `echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CONFIG.CERT_PUBLIC_PATH}" && touch ${CONFIG.INSTALL_CERT_FLAG} && echo "安装完成"`, - ) - dialog.showMessageBoxSync({ - type: "info", - message: `命令已复制到剪贴板,粘贴命令到终端并运行以安装并信任证书`, - }); - - reject() - }); - } else if (process.platform === 'linux') { - return new Promise((resolve, reject) => { - clipboard.writeText( - "https://github.com/putyy/res-downloader/blob/master/electron/res/keys/public.pem", - ) - dialog.showMessageBoxSync({ - type: "info", - message: `Linux系统请手动安装证书,已复制下载地址`, - }); - - reject() - }); +} + + +// MacOS 证书安装处理 +function handleMacCertInstallation() { + clipboard.writeText( + `echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CONFIG.CERT_PUBLIC_PATH}" && touch ${CONFIG.INSTALL_CERT_FLAG} && echo "安装完成"` + ); + + dialog.showMessageBoxSync({ + type: 'info', + message: '命令已复制到剪贴板,粘贴到终端并运行以安装并信任证书', + }); +} + +// Linux 证书安装处理 +function handleOtherCertInstallation() { + clipboard.writeText(CONFIG.CERT_PUBLIC_PATH); + + dialog.showMessageBoxSync({ + type: "info", + message: `请手动安装证书,证书文件路径:${CONFIG.CERT_PUBLIC_PATH} 已复制到剪贴板`, + }); +} + +// Windows 证书安装处理 +function handleWindowsCertInstallation() { + const result = spawn.sync(CONFIG.WIN_CERT_INSTALL_HELPER, [ + '-c', + '-add', + CONFIG.CERT_PUBLIC_PATH, + '-s', + 'root', + ]); + + if (result.stdout.toString().includes('Succeeded')) { + fs.writeFileSync(CONFIG.INSTALL_CERT_FLAG, ''); } else { - return new Promise((resolve: any, reject) => { - const result = spawn.sync(CONFIG.WIN_CERT_INSTALL_HELPER, [ - '-c', - '-add', - CONFIG.CERT_PUBLIC_PATH, - '-s', - 'root', - ]); - if (result.stdout.toString().indexOf('Succeeded') > -1) { - fs.writeFileSync(CONFIG.INSTALL_CERT_FLAG, '') - resolve() - } else { - reject() - } - }) + handleOtherCertInstallation(); } } + diff --git a/electron/main/index.ts b/electron/main/index.ts index dcb1580..dd647a1 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -7,6 +7,8 @@ import {closeProxy} from "./setProxy" import log from "electron-log" import path from 'path' import {spawn} from 'child_process' +import {startServer} from "./proxyServer"; +import fs from "fs"; // The built directory structure // @@ -56,6 +58,16 @@ const preload = join(__dirname, '../preload/index.js') const url = process.env.VITE_DEV_SERVER_URL const indexHtml = join(process.env.DIST, 'index.html') +global.videoList = {} +global.isStartProxy = false +global.isSettingProxy = false +global.resdConfig = { + save_dir: "", + quality: "-1", + proxy: "", + port: 8899, +} + // app.whenReady().then(createWindow) app.on('window-all-closed', () => { @@ -100,8 +112,10 @@ function createWindow() { mainWindow = new BrowserWindow({ title: 'Main window', icon: join(process.env.VITE_PUBLIC, 'favicon.ico'), - width: 800, - height: 600, + width: 1024, + minWidth: 960, + height: 768, + minHeight: 640, webPreferences: { preload, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production @@ -159,14 +173,11 @@ function createPreviewWindow(parent: BrowserWindow) { previewWin.setTitle("预览") previewWin.on("page-title-updated", (event) => { - // 阻止该事件 event.preventDefault() }) previewWin.on("close", (event) => { - // 不关闭窗口 event.preventDefault() - // 影藏窗口 previewWin.hide() }) } @@ -183,14 +194,12 @@ function createAria2Process() { aria2Path = path.join(CONFIG.EXECUTABLE_PATH, `./${process.platform}/aria2` + (CONFIG.IS_DEV ? `/${process.arch}` : '/') + "/aria2c"); aria2Conf = path.join(CONFIG.EXECUTABLE_PATH, `./${process.platform}/aria2/aria2.conf`) } - // 启动 aria2 - console.log("启动 aria2") aria2Process = spawn(aria2Path, [`--conf-path=${aria2Conf}`, `--rpc-listen-port=${CONFIG.ARIA_PORT}`], { windowsHide: false, stdio: CONFIG.IS_DEV ? 'pipe' : 'ignore' }); if(!aria2Process){ - console.log("启动 aria2 失败") + console.log("start aria2 error") } if (CONFIG.IS_DEV) { aria2Process.stdout.on('data', (data) => { @@ -200,16 +209,35 @@ function createAria2Process() { console.log(`aria2 error: ${data}`); }); } - console.log("aria2 成功启动") } catch (e) { console.log(`aria2 process start err`, e); } } +function initConfig(){ + const configPath = path.join(app.getPath('userData'), 'resd_config.json') + if (!fs.existsSync(configPath)) { + return + } + const buff = fs.readFileSync(configPath); + if (buff) { + try { + const jsonData = JSON.parse(buff) + global.resdConfig = Object.assign({}, global.resdConfig, jsonData) + if (!global.resdConfig.port) { + global.resdConfig.port = 8899 + } + } catch (parseErr) { + } + } +} + app.whenReady().then(() => { - initIPC() createWindow() createPreviewWindow(mainWindow) - createAria2Process() setWin(mainWindow, previewWin) + initConfig() + initIPC() + startServer(mainWindow) + createAria2Process() }) \ No newline at end of file diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index 4b988e1..4927e23 100755 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -7,10 +7,12 @@ import {hexMD5} from '../../src/common/md5' import {Aria2RPC} from './aria2Rpc' import fs from "fs" import urlTool from "url"; +import {closeProxy, setProxy} from "./setProxy"; +import path from 'path' let win: BrowserWindow let previewWin: BrowserWindow -let isStartProxy = false + const aria2RpcClient = new Aria2RPC() export default function initIPC() { @@ -21,23 +23,54 @@ export default function initIPC() { ipcMain.handle('invoke_init_app', (event, arg) => { // 开始 初始化应用 安装证书相关 - installCert(false).then(r => { - }) + installCert(false) + }) + + ipcMain.handle('invoke_set_config', (event, data) => { + const filePath = path.join(app.getPath('userData'), 'resd_config.json'); + fs.writeFile(filePath, JSON.stringify(data), ()=>{}) + global.resdConfig = Object.assign({}, global.resdConfig, data) + global.resdConfig.port = parseInt(global.resdConfig.port) + return true }) - ipcMain.handle('invoke_start_proxy', (event, arg) => { + ipcMain.handle('invoke_set_proxy', async (event, arg) => { // 启动代理服务 - if (isStartProxy) { - return + if (!global.isStartProxy) { + dialog.showMessageBoxSync({ + type: "error", + message: "代理未启动", + }); + return false } - isStartProxy = true - return startServer({ - win: win, - upstreamProxy: arg.upstream_proxy ? arg.upstream_proxy : "", - setProxyErrorCallback: err => { - console.log('setProxyErrorCallback', err) - }, - }) + try { + if (arg.proxy) { + await setProxy('127.0.0.1', global.resdConfig.port) + }else{ + await closeProxy('127.0.0.1', global.resdConfig.port) + } + return true + } catch (err) { + console.error(err); + dialog.showMessageBoxSync({ + type: "error", + message: err.toString(), + }); + return false + } + // let upstream_proxy = "" + // if (arg.upstream_proxy && !arg.upstream_proxy.includes(':8899')) { + // upstream_proxy = arg.upstream_proxy + // } + // + // global.isStartProxy = true + // return startServer({ + // win: win, + // upstreamProxy: upstream_proxy, + // setProxyErrorCallback: err => { + // console.log('setProxyErrorCallback', err) + // }, + // }) }) ipcMain.handle('invoke_select_down_dir', async (event, arg) => { @@ -72,7 +105,7 @@ export default function initIPC() { resolve(false); }); } - if(quality === "0" && data.decode_key){ + if (quality === "0" && data.decode_key) { const urlInfo = urlTool.parse(down_url, true); console.log('urlInfo', urlInfo) if (urlInfo.query["token"] && urlInfo.query["encfilekey"]) { @@ -103,18 +136,18 @@ export default function initIPC() { return new Promise((resolve, reject) => { - if (down_url.includes("douyin")) { - headers['Referer'] = down_url + if (data?.referer) { + headers['Referer'] = data?.referer } aria2RpcClient.addUri([down_url], save_path, fileName, headers).then((response) => { - let currentGid = response.result // 保存当前下载的 gid + let currentGid = response.result let progressIntervalId = null // // 开始定时查询下载进度 progressIntervalId = setInterval(() => { aria2RpcClient.tellStatus(currentGid).then((status) => { if (status.result.status !== "complete") { - const progress = aria2RpcClient.calculateDownloadProgress(status.result.bitfield); + const progress = aria2RpcClient.calculateDownloadProgress(status.result.bitfield) win?.webContents.send('on_down_file_schedule', {schedule: `已下载${progress}%`}) } else { clearInterval(progressIntervalId); @@ -123,26 +156,26 @@ export default function initIPC() { decodeWxFile(save_path_file, data.decode_key, save_path_file.replace(".mp4", "_wx.mp4")).then((res) => { fs.unlink(save_path_file, (err) => { }) - resolve(res); + resolve(res) }).catch((error) => { console.log("err:", error) resolve(false); - }); + }) } else { resolve({ fullFileName: save_path_file, - }); + }) } } }).catch((error) => { - console.error(error); - clearInterval(progressIntervalId); - resolve(false); + console.error(error) + clearInterval(progressIntervalId) + resolve(false) }); - }, 1000); + }, 1000) }).catch((error) => { console.log("err:", error) - resolve(false); + resolve(false) }); }); }); diff --git a/electron/main/proxyServer.ts b/electron/main/proxyServer.ts index a3929b3..4a120cb 100755 --- a/electron/main/proxyServer.ts +++ b/electron/main/proxyServer.ts @@ -1,7 +1,6 @@ import fs from 'fs' import log from 'electron-log' import CONFIG from './const' -import {setProxy} from './setProxy' import * as urlTool from "url" import {toSize, typeSuffix} from "./utils" // @ts-ignore @@ -10,10 +9,6 @@ import pkg from '../../package.json' const hoXy = require('hoxy') -const port = 8899 - -global.videoList = {} - if (process.platform === 'win32') { process.env.OPENSSL_BIN = CONFIG.OPEN_SSL_BIN_PATH process.env.OPENSSL_CONF = CONFIG.OPEN_SSL_CNF_PATH @@ -22,6 +17,7 @@ if (process.platform === 'win32') { const resObject = { url: "", url_sign: "", + referer: "", cover_url: "", file_format: "", platform: "", @@ -34,144 +30,144 @@ const resObject = { description: "" } -const vv = hexMD5(pkg.version) + (CONFIG.IS_DEV ? Math.random() :"") +const vv = hexMD5(pkg.version) + (CONFIG.IS_DEV ? Math.random() : "") -export async function startServer({win, upstreamProxy, setProxyErrorCallback = f => f,}) { - return new Promise(async (resolve: any, reject) => { - try { - const proxy = hoXy.createServer({ - upstreamProxy: upstreamProxy, - certAuthority: { - key: fs.readFileSync(CONFIG.CERT_PRIVATE_PATH), - cert: fs.readFileSync(CONFIG.CERT_PUBLIC_PATH), - }, +export function startServer(win) { + try { + let upstreamProxy = "" + if (global.resdConfig.proxy && !global.resdConfig.proxy.includes(':' + global.resdConfig.port)) { + upstreamProxy = global.resdConfig?.proxy + } + console.log("global.resdConfig.port:", global.resdConfig.port) + const proxy = hoXy.createServer({ + upstreamProxy: upstreamProxy, + certAuthority: { + key: fs.readFileSync(CONFIG.CERT_PRIVATE_PATH), + cert: fs.readFileSync(CONFIG.CERT_PUBLIC_PATH), + }, + }) + .listen(global.resdConfig.port, () => { + global.isStartProxy = true }) - .listen(port, async () => { - try { - await setProxy('127.0.0.1', port) - resolve() - } catch (err) { - console.error(err); - setProxyErrorCallback(err) - reject("请手动设置系统代理" + err.toString()) - } - }) - .on('error', err => { - setProxyErrorCallback(err) - reject('proxy service err: ' + err.toString()) - }) + .on('error', err => { + console.error("hoXy err:", err); + }) + intercept(proxy, win) + } catch (e) { + console.error("--------------proxy catch err--------------"); + } +} +function intercept(proxy, win) { + proxy.intercept( + { + phase: 'request', + hostname: 'res-downloader.666666.com', + as: 'json', + }, + (req, res) => { + res.string = 'ok' + res.statusCode = 200 + try { + if (req.json?.media?.length <= 0) { + return + } + const media = req.json?.media[0] + const url_sign: string = hexMD5(media.url) + if (!media?.decodeKey || global.videoList.hasOwnProperty(url_sign) === true) { + return + } + const urlInfo = urlTool.parse(media.url, true) + global.videoList[url_sign] = media.url + win.webContents.send('on_get_queue', Object.assign({}, resObject, { + url_sign: url_sign, + url: media.url + media.urlToken, + cover_url: media.coverUrl, + referer: "", + file_format: media.spec.map((res) => res.fileFormat).join('#'), + platform: urlInfo.hostname, + size: toSize(media.fileSize), + type: "video/mp4", + type_str: 'video', + decode_key: media.decodeKey, + description: req.json.description, + })) + } catch (e) { + log.log(e.toString()) + } + }, + ) - proxy.intercept( - { - phase: 'request', - hostname: 'res-downloader.666666.com', - as: 'json', - }, - (req, res) => { - res.string = 'ok' - res.statusCode = 200 - try { - if (req.json?.media?.length <= 0) { - return - } - const media = req.json?.media[0] - const url_sign: string = hexMD5(media.url) - if (!media?.decodeKey || global.videoList.hasOwnProperty(url_sign) === true) { - return - } - const urlInfo = urlTool.parse(media.url, true) - global.videoList[url_sign] = media.url + proxy.intercept( + { + phase: 'response', + hostname: 'channels.weixin.qq.com', + as: 'string', + }, + async (req, res) => { + if (req.url.includes('/web/pages/feed') || req.url.includes('/web/pages/home')) { + res.string = res.string.replaceAll('.js"', '.js?v=' + vv + '"') + res.statusCode = 200 + } + }, + ) + + proxy.intercept( + { + phase: 'response', + hostname: 'res.wx.qq.com', + as: 'string', + }, + async (req, res) => { + if (req.url.endsWith('.js?v=' + vv)) { + res.string = res.string.replaceAll('.js"', '.js?v=' + vv + '"'); + } + if (req.url.includes("web/web-finder/res/js/virtual_svg-icons-register.publish")) { + res.string = res.string.replace(/get\s*media\s*\(\)\s*\{/, ` + get media(){ + if(this.objectDesc){ + fetch("https://res-downloader.666666.com", { + method: "POST", + mode: "no-cors", + body: JSON.stringify(this.objectDesc), + }); + }; + `) + } + } + ); + + proxy.intercept( + { + phase: 'response', + }, + async (req, res) => { + try { + // 拦截响应 + const contentType = res?._data?.headers?.['content-type'] + const [resType, suffix] = typeSuffix(contentType) + if (resType) { + const url_sign: string = hexMD5(req.fullUrl()) + const res_url = req.fullUrl() + const urlInfo = urlTool.parse(res_url, true) + const contentLength = res?._data?.headers?.['content-length'] + if (global.videoList.hasOwnProperty(url_sign) === false) { + global.videoList[url_sign] = res_url + let referer = req?._data?.headers?.['referer'] win.webContents.send('on_get_queue', Object.assign({}, resObject, { + url: res_url, url_sign: url_sign, - url: media.url + media.urlToken, - cover_url: media.coverUrl, - file_format: media.spec.map((res)=> res.fileFormat).join('#'), + referer: referer ? referer : "", platform: urlInfo.hostname, - size: toSize(media.fileSize), - type: "video/mp4", - type_str: 'video', - decode_key: media.decodeKey, - description: req.json.description, + size: toSize(contentLength ? contentLength : 0), + type: contentType, + type_str: resType, })) - } catch (e) { - log.log(e.toString()) - } - }, - ) - - proxy.intercept( - { - phase: 'response', - hostname: 'channels.weixin.qq.com', - as: 'string', - }, - async (req, res) => { - if (req.url.includes('/web/pages/feed') || req.url.includes('/web/pages/home')) { - res.string = res.string.replaceAll('.js"', '.js?v=' + vv + '"') - res.statusCode = 200 - } - }, - ) - - proxy.intercept( - { - phase: 'response', - hostname: 'res.wx.qq.com', - as: 'string', - }, - async (req, res) => { - if (req.url.endsWith('.js?v=' + vv)) { - res.string = res.string.replaceAll('.js"', '.js?v=' + vv + '"'); - } - if (req.url.includes("web/web-finder/res/js/virtual_svg-icons-register.publish")) { - res.string = res.string.replace(/get\s*media\s*\(\)\s*\{/, ` - get media(){ - if(this.objectDesc){ - fetch("https://res-downloader.666666.com", { - method: "POST", - mode: "no-cors", - body: JSON.stringify(this.objectDesc), - }); - }; - `) } } - ); - - proxy.intercept( - { - phase: 'response', - }, - async (req, res) => { - try { - // 拦截响应 - const contentType = res?._data?.headers?.['content-type'] - const [resType, suffix] = typeSuffix(contentType) - if (resType) { - const url_sign: string = hexMD5(req.fullUrl()) - const res_url = req.fullUrl() - const urlInfo = urlTool.parse(res_url, true) - const contentLength = res?._data?.headers?.['content-length'] - if (global.videoList.hasOwnProperty(url_sign) === false) { - global.videoList[url_sign] = res_url - win.webContents.send('on_get_queue', Object.assign({}, resObject, { - url: res_url, - url_sign: url_sign, - platform: urlInfo.hostname, - size: toSize(contentLength ? contentLength : 0), - type: contentType, - type_str: resType, - })) - } - } - } catch (e) { - log.log(e.toString()) - } - }, - ) - } catch (e) { - log.log("--------------proxy catch err--------------", e) - } - }) + } catch (e) { + log.log("--------------proxy response err--------------", e) + } + }, + ) } \ No newline at end of file diff --git a/electron/main/setProxy.ts b/electron/main/setProxy.ts index 1b36148..2524271 100755 --- a/electron/main/setProxy.ts +++ b/electron/main/setProxy.ts @@ -38,7 +38,7 @@ export async function setProxy(host, port) { } else if (process.platform === 'linux') { dialog.showMessageBoxSync({ type: "info", - message: `请手动设置系统代理`, + message: `请手动设置系统代理 默认为: 127.0.0.1:8899`, }); return new Promise((resolve, reject) => {}) } else { diff --git a/electron/main/utils.ts b/electron/main/utils.ts index e6c8f5a..5c2e86d 100755 --- a/electron/main/utils.ts +++ b/electron/main/utils.ts @@ -99,7 +99,7 @@ function toSize(size: number) { } function typeSuffix(type: string) { - switch (type) { + switch (type ? type.toLowerCase() : type) { case "video/mp4": case "video/webm": case "video/ogg": @@ -111,7 +111,7 @@ function typeSuffix(type: string) { case "video/x-matroska": return ["video", ".mp4"]; case "audio/video": - case "video/x-flv": + case "video/x-flv": return ["live", ".mp4"]; case "image/png": case "image/webp": @@ -143,7 +143,7 @@ function typeSuffix(type: string) { case "audio/mp4;charset=UTF-8": return ["audio", ".mp3"]; case "application/vnd.apple.mpegurl": - case "application/x-mpegURL": + case "application/x-mpegurl": return ["m3u8", ".m3u8"]; case "application/pdf": return ["pdf", ".pdf"]; diff --git a/package.json b/package.json index 0d402ed..407a670 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "res-downloader", - "version": "2.1.3", + "version": "2.2.0", "main": "dist-electron/main/index.js", "description": "res-downloader(爱享素材下载器),支持视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐、qq短视频等", "homepage": "https://github.com/putyy/res-downloader", diff --git a/src/App.vue b/src/App.vue index 4d0a6d8..d10c89c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,8 +1,12 @@