diff --git a/README.md b/README.md
index 01fc6eb..ed340c1 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
-## res-downloader(爱享素材下载器) 【[加入群聊](https://qm.qq.com/q/mfDMSpCxQ4)】
+## res-downloader
+### 爱享素材下载器【[加入群聊](https://qm.qq.com/q/mfDMSpCxQ4)】
🎯 基于 [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue.git)
📦 操作简单、可获取不同类型的资源
-🖥️ 支持Win10、Win11、Mac
-🌐 支持视频、音频、图片、m3u8等网络资源下载
+🖥️ 支持Win10、Win11、Mac、Linux
+🌐 支持视频、音频、图片、m3u8、直播流等常见网络资源拦截
💪 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载
👼 支持设置代理以获取特殊网络下的资源
@@ -21,7 +22,7 @@

## 常见问题
-下载慢、大视频下载失败
+下载慢、大视频下载失败(最新版本以内置aria2下载器)
> 推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密(视频号)” 按钮
>> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
@@ -39,7 +40,8 @@ Win7无法使用
>> MAC: /Users/你的用户名称/.res-downloader@putyy/res-downloader-installed.lock
>> Win: C:\Users\Admin\.res-downloader@putyy/res-downloader-installed.lock
-其他问题请留言 https://github.com/putyy/res-downloader/issues
+其他问题
+[github](https://github.com/putyy/res-downloader/issues) 、 [爱享论坛](https://s.gowas.cn/d/4089)
## 二次开发
> ps: 打包慢的问题可以参考 https://www.putyy.com/articles/87
@@ -59,5 +61,8 @@ yarn run build --universal --mac
yarn run build --win
```
+## 实现&初衷
+通过代理网络抓包拦截响应,筛选出有用的资源, 同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的,只不过这些软件需要手动进行筛选,对于小白用户上手还是有点难度,本软件对部分资源做了特殊处理,更适合大众用户,所以就有了本项目。
+
## 免责声明
本软件用于学习研究使用,若因使用本软件造成的一切法律责任均与本人无关!
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 98a0525..dcb1580 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -144,6 +144,8 @@ function createPreviewWindow(parent: BrowserWindow) {
parent: parent,
width: 600,
height: 400,
+ minWidth: 600,
+ minHeight: 400,
show: false,
// paintWhenInitiallyHidden: false,
webPreferences: {
diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts
index d1a1174..4b988e1 100755
--- a/electron/main/ipc.ts
+++ b/electron/main/ipc.ts
@@ -1,11 +1,12 @@
import {ipcMain, dialog, BrowserWindow, app, shell} from 'electron'
import {startServer} from './proxyServer'
import {installCert, checkCertInstalled} from './cert'
-import {decodeWxFile, suffix, getCurrentDateTimeFormatted} from './utils'
+import {decodeWxFile, typeSuffix, getCurrentDateTimeFormatted} from './utils'
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
import {Aria2RPC} from './aria2Rpc'
import fs from "fs"
+import urlTool from "url";
let win: BrowserWindow
let previewWin: BrowserWindow
@@ -71,7 +72,16 @@ export default function initIPC() {
resolve(false);
});
}
- if (quality !== "-1" && data.decode_key && data.file_format) {
+ if(quality === "0" && data.decode_key){
+ const urlInfo = urlTool.parse(down_url, true);
+ console.log('urlInfo', urlInfo)
+ if (urlInfo.query["token"] && urlInfo.query["encfilekey"]) {
+ down_url = urlInfo.protocol + "//" + urlInfo.hostname + urlInfo.pathname.replace("251/20302", "251/20304") +
+ "?encfilekey=" + urlInfo.query["encfilekey"] +
+ "&token=" + urlInfo.query["token"]
+ console.log("down_url:", down_url)
+ }
+ } else if (quality !== "-1" && data.decode_key && data.file_format) {
const format = data.file_format.split('#');
const qualityMap = [
format[0],
@@ -81,7 +91,7 @@ export default function initIPC() {
down_url += "&X-snsvideoflag=" + qualityMap[quality];
}
let fileName = data?.description ? data.description.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '') : hexMD5(down_url);
- fileName = fileName + "_" + getCurrentDateTimeFormatted() + suffix(data.type)
+ fileName = fileName + "_" + getCurrentDateTimeFormatted() + typeSuffix(data.type)[1]
let save_path_file = `${save_path}/${fileName}`
if (process.platform === 'win32') {
save_path_file = `${save_path}\\${fileName}`
diff --git a/electron/main/proxyServer.ts b/electron/main/proxyServer.ts
index fc40ffd..b214313 100755
--- a/electron/main/proxyServer.ts
+++ b/electron/main/proxyServer.ts
@@ -3,7 +3,7 @@ import log from 'electron-log'
import CONFIG from './const'
import {setProxy} from './setProxy'
import * as urlTool from "url"
-import {toSize} from "./utils"
+import {toSize, typeSuffix} from "./utils"
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
import pkg from '../../package.json'
@@ -146,87 +146,24 @@ export async function startServer({win, upstreamProxy, setProxyErrorCallback = f
async (req, res) => {
try {
// 拦截响应
- const ctype = res?._data?.headers?.['content-type']
- const url_sign: string = hexMD5(req.fullUrl())
- const res_url = req.fullUrl()
- const urlInfo = urlTool.parse(res_url, true)
- switch (ctype) {
- case "video/mp4":
- case "video/webm":
- case "video/ogg":
- case "video/x-msvideo":
- case "video/mpeg":
- case "video/quicktime":
- case "video/x-ms-wmv":
- case "video/x-flv":
- case "video/3gpp":
- case "video/x-matroska":
- 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(res?._data?.headers?.['content-length'] ?? 0),
- type: ctype,
- type_str: 'video',
- }))
- }
- break;
- case "image/png":
- case "image/webp":
- case "image/jpeg":
- case "image/jpg":
- case "image/svg+xml":
- case "image/gif":
- case "image/avif":
- case "image/bmp":
- case "image/tiff":
- case "image/x-icon":
- case "image/heic":
- case "image/vnd.adobe.photoshop":
+ 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: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0,
- type: ctype,
- type_str: 'image',
+ size: toSize(contentLength ? contentLength : 0),
+ type: contentType,
+ type_str: resType,
}))
- break
- case "audio/mpeg":
- case "audio/wav":
- case "audio/aiff":
- case "audio/x-aiff":
- case "audio/aac":
- case "audio/ogg":
- case "audio/flac":
- case "audio/midi":
- case "audio/x-midi":
- case "audio/x-ms-wma":
- case "audio/opus":
- case "audio/webm":
- case "audio/mp4":
- win.webContents.send('on_get_queue', Object.assign({}, resObject, {
- url: res_url,
- url_sign: url_sign,
- platform: urlInfo.hostname,
- size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0,
- type: ctype,
- type_str: 'audio',
- }))
- break
- case "application/vnd.apple.mpegurl":
- case "application/x-mpegURL":
- win.webContents.send('on_get_queue', Object.assign({}, resObject, {
- url: res_url,
- url_sign: url_sign,
- platform: urlInfo.hostname,
- size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0,
- type: ctype,
- type_str: 'm3u8',
- }))
- break
+ }
}
} catch (e) {
log.log(e.toString())
diff --git a/electron/main/utils.ts b/electron/main/utils.ts
index 0452d33..280c994 100755
--- a/electron/main/utils.ts
+++ b/electron/main/utils.ts
@@ -1,8 +1,9 @@
import fs from 'fs'
-import {Transform } from 'stream'
+import {Transform} from 'stream'
import {getDecryptionArray} from '../wxjs/decrypt.js'
const axios = require('axios')
+
function xorTransform(decryptionArray) {
let processedBytes = 0;
return new Transform({
@@ -32,7 +33,7 @@ function downloadFile(url, decodeKey, fullFileName, progressCallback) {
},
}
- if (url.includes("douyin")){
+ if (url.includes("douyin")) {
config.headers['Referer'] = url
}
@@ -57,7 +58,7 @@ function downloadFile(url, decodeKey, fullFileName, progressCallback) {
});
}),
);
- }else{
+ } else {
data.pipe(
fs.createWriteStream(fullFileName).on('finish', () => {
resolve({
@@ -97,7 +98,7 @@ function toSize(size: number) {
return size + 'b'
}
-function suffix(type: string) {
+function typeSuffix(type: string) {
switch (type) {
case "video/mp4":
case "video/webm":
@@ -106,23 +107,25 @@ function suffix(type: string) {
case "video/mpeg":
case "video/quicktime":
case "video/x-ms-wmv":
- case "video/x-flv":
case "video/3gpp":
case "video/x-matroska":
- return ".mp4";
+ return ["video", ".mp4"];
+ case "audio/video":
+ case "video/x-flv":
+ return ["live", ".mp4"];
case "image/png":
case "image/webp":
case "image/jpeg":
case "image/jpg":
- case "image/svg+xml":
case "image/gif":
case "image/avif":
case "image/bmp":
case "image/tiff":
- case "image/x-icon":
case "image/heic":
+ case "image/x-icon":
+ case "image/svg+xml":
case "image/vnd.adobe.photoshop":
- return ".png";
+ return ["image", ".png"];
case "audio/mpeg":
case "audio/wav":
case "audio/aiff":
@@ -136,12 +139,23 @@ function suffix(type: string) {
case "audio/opus":
case "audio/webm":
case "audio/mp4":
- return ".mp3";
+ return ["audio", ".mp3"];
case "application/vnd.apple.mpegurl":
case "application/x-mpegURL":
- return ".m3u8";
+ return ["m3u8", ".m3u8"];
+ case "application/pdf":
+ return ["pdf", ".pdf"];
+ case "application/vnd.ms-powerpoint":
+ case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
+ return ["ppt", ".ppt"];
+ case "application/vnd.ms-excel":
+ case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
+ return ["xls", ".xls"];
+ case "application/msword":
+ case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
+ return ["doc", ".doc"];
}
- return ""
+ return ["", ""]
}
function getCurrentDateTimeFormatted() {
@@ -157,4 +171,4 @@ function getCurrentDateTimeFormatted() {
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
-export {downloadFile, toSize, decodeWxFile, suffix, getCurrentDateTimeFormatted}
+export {downloadFile, toSize, decodeWxFile, typeSuffix, getCurrentDateTimeFormatted}
diff --git a/src/components/layout/Footer.vue b/src/components/layout/Footer.vue
index 1b0cd8f..8dc95d1 100755
--- a/src/components/layout/Footer.vue
+++ b/src/components/layout/Footer.vue
@@ -2,53 +2,17 @@
import {ipcRenderer} from 'electron'
import pkg from '../../../package.json'
const v = pkg.version
-const jump = (scene: number)=>{
- switch (scene) {
- case 1:
- ipcRenderer.invoke('invoke_open_default_browser', {
- url: "https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian"
- })
- break;
- case 2:
- ipcRenderer.invoke('invoke_open_default_browser', {
- url: "https://s.gowas.cn"
- })
- break;
- case 3:
- ipcRenderer.invoke('invoke_open_default_browser', {
- url: "https://i.gowas.cn"
- })
- break;
- case 4:
- ipcRenderer.invoke('invoke_open_default_browser', {
- url: "https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian"
- })
- break;
- case 5:
- ipcRenderer.invoke('invoke_open_default_browser', {
- url: "https://www.ais.do/ivi/rr2GaZ"
- })
- break;
- case 6:
- ipcRenderer.invoke('invoke_open_default_browser', {
- url: "https://github.com/putyy/res-downloader"
- })
- break;
- }
+const jump = (url: string)=>{
+ ipcRenderer.invoke('invoke_open_default_browser', {
+ url: url
+ })
}
div.line
a.item 当前版本: {{v}}
- a.item 站长邮箱: gowas.work@gmail.com
- a.item(@click="jump(1)") 获取更新
- a.item(@click="jump(4)") 问题反馈
-div.line
- a.item 推荐:
- a.item(@click="jump(5)") Ai助手(免费)
- a.item(@click="jump(2)") 网盘资源
- a.item(@click="jump(3)") 图片无损压缩
- a.item(@click="jump(6)") 软件源码
+ a.item(@click="jump('https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian')") 获取更新&问题反馈
+ a.item(@click="jump('https://github.com/putyy/res-downloader')") 软件源码
diff --git a/src/views/About.vue b/src/views/About.vue
index 2cd1254..bb1fef0 100644
--- a/src/views/About.vue
+++ b/src/views/About.vue
@@ -30,6 +30,7 @@ const str = "使用方法\n" +
" 2. 软件首页选择要获取的资源类型(默认选中的视频)\n" +
" 3. 打开要捕获的源, 如:视频号、网页、小程序等等\n" +
" 4. 返回软件首页即可看到要下载的资源\n" +
+ " 5. 直播流复制的链接如何使用?可以使用obs或者ffmpeg命令\n" +
"常见问题\n" +
" 1. 无法拦截获取\n" +
" 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899\n" +
@@ -56,8 +57,7 @@ div.about
el-button(@click="jump(3)") 获取更新
div 4. 问题反馈
el-button(@click="jump(4)") 点击前往
- div.more
- pre {{str}}
+ div.more {{str}}
@@ -74,7 +74,9 @@ div.about
white-space: pre-wrap;
}
.more{
-
+ width: 100%; /* 设置容器宽度 */
+ white-space: pre-wrap;
+ overflow-wrap: break-word; /* 允许在单词边界内换行 */
}
}
diff --git a/src/views/Index.vue b/src/views/Index.vue
index f5672ed..7d9d8c6 100755
--- a/src/views/Index.vue
+++ b/src/views/Index.vue
@@ -20,24 +20,51 @@ interface resData {
const tableData = ref([])
-const resType = ref({
- video: true,
- audio: true,
- image: false,
- m3u8: false
-})
-
const isInitApp = ref(false)
const multipleTableRef = ref>()
const multipleSelection = ref([])
const loading = ref()
+const resType = ref(["all"])
+const typeOptions = ref([
+ {
+ value: "all",
+ label: "全部",
+ },
+ {
+ value: "image",
+ label: "图片",
+ }, {
+ value: "audio",
+ label: "音频"
+ }, {
+ value: "video",
+ label: "视频"
+ }, {
+ value: "m3u8",
+ label: "m3u8"
+ }, {
+ value: "live",
+ label: "直播流"
+ }, {
+ value: "xls",
+ label: "文档"
+ }, {
+ value: "doc",
+ label: "doc"
+ }, {
+ value: "pdf",
+ label: "pdf"
+ }
+])
+
+const tableHeight = ref(400)
onMounted(() => {
- let resTypeCache = localStorageCache.get("res-type")
+ let resTypeCache = localStorageCache.get("res-type-arr")
if (resTypeCache) {
- resType.value = resTypeCache
+ resType.value = resTypeCache.split(",")
}
let tableDataCache = localStorageCache.get("res-table-data")
@@ -47,7 +74,7 @@ onMounted(() => {
ipcRenderer.on('on_get_queue', (res, data) => {
// @ts-ignore
- if (resType.value.hasOwnProperty(data.type_str) && resType.value[data.type_str]) {
+ if (resType.value.includes("all") || resType.value.includes(data.type_str)) {
tableData.value.push(data)
localStorageCache.set("res-table-data", tableData.value, -1)
}
@@ -79,6 +106,8 @@ onMounted(() => {
})
loading.value.close()
})
+ window.addEventListener("resize", handleResize);
+ handleResize()
})
onUnmounted(() => {
@@ -92,9 +121,14 @@ onUnmounted(() => {
})
watch(resType, (res, res1) => {
- localStorageCache.set("res-type", resType.value, -1)
+ localStorageCache.set("res-type-arr", resType.value.join(","), -1)
}, {deep: true})
+const handleResize = () => {
+ const height = document.documentElement.clientHeight || window.innerHeight;
+ tableHeight.value = height - 115
+}
+
const handleSelectionChange = (val: resData[]) => {
multipleSelection.value = val
}
@@ -270,55 +304,63 @@ const handleInitApp = () => {
el-container.container
- el-header
- el-row
- div
- el-button(type="primary" @click="handleBatchDown") 批量下载
- el-button(v-if="isInitApp" @click="handleInitApp")
- el-icon
- Promotion
- p 安装检测(如果看到此按钮说明软件安装未完成则需要手动点击此按钮)
- el-button(@click="handleClear")
- el-icon
- Delete
- p 清空列表
- el-button(@click="resType.video=!resType.video" :type="resType.video ? 'primary' : 'info'" ) 视频
- el-button(@click="resType.audio=!resType.audio" :type="resType.audio ? 'primary' : 'info'" ) 音频
- el-button(@click="resType.image=!resType.image" :type="resType.image ? 'primary' : 'info'" ) 图片
- el-button(@click="resType.m3u8=!resType.m3u8" :type="resType.m3u8 ? 'primary' : 'info'" ) m3u8
- a(style="color: red") 点击左边选项,选择需要拦截的资源类型
- el-main
- el-table(ref="multipleTableRef" @selection-change="handleSelectionChange" :data="tableData" max-height="100%" stripe)
- el-table-column(type="selection")
- el-table-column(label="预览" show-overflow-tooltip width="150px")
- template(#default="scope")
- div.show_res
- video.video(v-if="scope.row.type_str === 'video'" :src="scope.row.url" controls preload="none")
- img.img(v-if="scope.row.type_str === 'image'" :src="scope.row.url" crossorigin="anonymous")
- audio.audio(v-if="scope.row.type_str === 'audio'" controls preload="none")
- source(:src="scope.row.url" :type="scope.row.type")
- div {{scope.row.description}}
- el-table-column(prop="type_str" label="类型" show-overflow-tooltip)
- el-table-column(prop="platform" label="主机地址")
- el-table-column(prop="size" label="资源大小")
- el-table-column(prop="save_path" label="保存目录")
- el-table-column(prop="progress_bar" label="下载进度")
- el-table-column(label="操作" width="135px" )
- template(#default="scope")
- div.actions
- template(v-if="scope.row.type_str !== 'm3u8'" )
- el-button(link type="primary" @click="handleDown(scope.$index, scope.row)") {{scope.row.decode_key || scope.row.decryptor_array ? "解密下载(视频号)" : "下载"}}
- el-button(v-if="scope.row.decode_key || scope.row.decryptor_array" link type="primary" @click="decodeWxFile(scope.$index)") 视频解密(视频号)
- el-button(link type="primary" @click="handlePreview(scope.$index, scope.row)") 窗口预览
- el-button(link type="primary" @click="handleCopy(scope.row.url)") 复制链接
- el-button(link type="primary" @click="handleDel(scope.$index)") 删除
- el-button(v-if="scope.row.save_path" link type="primary" @click="openFileDir(scope.$index)") 打开文件目录
+ el-header(style="display:flex;align-items: center")
+ el-button(type="primary" @click="handleBatchDown") 批量下载
+ el-button(v-if="isInitApp" @click="handleInitApp")
+ el-icon
+ Promotion
+ p 安装检测(如果看到此按钮说明软件安装未完成则需要手动点击此按钮)
+ el-button(@click="handleClear")
+ el-icon
+ Delete
+ p 清空列表
+ el-select(
+ v-model="resType"
+ multiple
+ collapse-tags
+ collapse-tags-tooltip
+ :max-collapse-tags="3"
+ placeholder="资源拦截类型"
+ style="width: auto;min-width:130px"
+ )
+ el-option(v-for="item in typeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value")
+ el-table(ref="multipleTableRef" @selection-change="handleSelectionChange" :data="tableData" :height="tableHeight" max-height="100%" stripe)
+ el-table-column(type="selection")
+ el-table-column(label="预览" show-overflow-tooltip width="150px")
+ template(#default="scope")
+ div.show_res
+ video.video(v-if="scope.row.type_str === 'video'" :src="scope.row.url" controls preload="none")
+ img.img(v-if="scope.row.type_str === 'image'" :src="scope.row.url" crossorigin="anonymous")
+ audio.audio(v-if="scope.row.type_str === 'audio'" controls preload="none")
+ source(:src="scope.row.url" :type="scope.row.type")
+ div(v-if="scope.row.type_str !== 'video' && scope.row.type_str !== 'image' && scope.row.type_str !== 'audio'") {{scope.row.type_str}}类型无法预览
+ div {{scope.row.description}}
+ el-table-column(prop="type_str" label="类型" show-overflow-tooltip)
+ el-table-column(prop="platform" label="主机地址")
+ el-table-column(prop="size" label="资源大小")
+ el-table-column(prop="save_path" label="保存目录" width="135px" :show-overflow-tooltip="true")
+ el-table-column(prop="progress_bar" label="下载进度")
+ el-table-column(label="操作" width="135px" )
+ template(#default="scope")
+ div.actions
+ template(v-if="scope.row.type_str !== 'm3u8' && scope.row.type_str !== 'live'" )
+ el-button(link type="primary" @click="handleDown(scope.$index, scope.row)") {{scope.row.decode_key || scope.row.decryptor_array ? "解密下载(视频号)" : "下载"}}
+ el-button(v-if="scope.row.decode_key || scope.row.decryptor_array" link type="primary" @click="decodeWxFile(scope.$index)") 视频解密(视频号)
+ el-button(link type="primary" @click="handlePreview(scope.$index, scope.row)") 窗口预览
+ el-button(link type="primary" @click="handleCopy(scope.row.url)") 复制链接
+ el-button(link type="primary" @click="handleDel(scope.$index)") 删除
+ el-button(v-if="scope.row.save_path" link type="primary" @click="openFileDir(scope.$index)") 打开文件目录