Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8ac4182fc | |||
| 55b70d3e6d | |||
| a19765b0d3 | |||
| 5c7b65385a | |||
| b6b84269a3 | |||
| e44cb249d0 | |||
| 8bea071865 | |||
| a782652fca | |||
| 3a02803d3c | |||
| 263a08ab4c | |||
| ecdbd985ce | |||
| dd403564cc | |||
| 69d741b39a | |||
| 4c9c2e2d68 | |||
| 8660701340 | |||
| 341c5570af | |||
| ee12753818 | |||
| 055638278c |
@@ -1,6 +1,6 @@
|
||||
APP_NAME=media-downloader
|
||||
APP_ID=mediago.ziying.site
|
||||
APP_COPYRIGHT=caorushizi
|
||||
APP_VERSION=2.0.1
|
||||
APP_VERSION=2.0.2
|
||||
|
||||
APP_SERVER_PORT=8433
|
||||
|
||||
@@ -47,3 +47,8 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
path: packages/main/release/media-downloader-setup-*
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
electron_mirror=https://npm.taobao.org/mirrors/electron/
|
||||
registry=https://registry.npmmirror.com
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
package-manager=pnpm
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
# media-downloader
|
||||
|
||||
m3u8 视频在线提取工具 流媒体下载 m3u8 下载 桌面客户端 windows mac。
|
||||
可以直接在线获取 m3u8 链接地址,无需使用使用网络抓包,无需安装浏览器插件,可以直接带出请求标头……
|
||||
|
||||
模板: <https://github.com/caorushizi/electron-template>
|
||||
可以直接在线获取 m3u8 链接地址,无需使用使用网络抓包,无需安装浏览器插件,可以直接带出请求标头……详情请参考[使用帮助](https://downloader.caorushizi.cn/guides.html?form=github)
|
||||
|
||||
## 上手指南
|
||||
|
||||
以下指南将帮助你在本地机器上安装和运行该项目,进行开发和测试。关于如何将该项目部署到在线环境,请参考部署小节。
|
||||
|
||||
【使用帮助】[blog.ziying.site](https://downloader.caorushizi.cn/guides.html?form=github)
|
||||
|
||||
## 安装要求
|
||||
|
||||
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
|
||||
|
||||
+21
-14
@@ -1,24 +1,31 @@
|
||||
import { HeadConfig } from "vitepress";
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
const head: HeadConfig[] = [
|
||||
["link", { rel: "shortcut icon", href: "/favicon.svg" }],
|
||||
];
|
||||
if (!isDev) {
|
||||
head.push([
|
||||
"script",
|
||||
{},
|
||||
`var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?eefcbd14f0323044aa0ca678cd278381";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();`,
|
||||
]);
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "media-downloader",
|
||||
description: "简单易用,快速下载",
|
||||
lastUpdated: true,
|
||||
head: [
|
||||
["link", { rel: "shortcut icon", href: "/favicon.svg" }],
|
||||
[
|
||||
"script",
|
||||
{},
|
||||
`var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?eefcbd14f0323044aa0ca678cd278381";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();`,
|
||||
],
|
||||
],
|
||||
head,
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
|
||||
+21
-1
@@ -67,7 +67,27 @@ outline: deep
|
||||
|
||||

|
||||
|
||||
### 开始你的视频下载吧
|
||||
## 附加功能
|
||||
|
||||
1. 下载完成后可以在下载列表中点击转换音频按钮将视频转换为音频
|
||||
|
||||

|
||||
|
||||
2. 下载完成后播放视频
|
||||
|
||||
- PC 播放
|
||||
|
||||

|
||||
|
||||
- 移动端播放
|
||||
|
||||

|
||||
|
||||
3. 批量下载
|
||||
|
||||

|
||||
|
||||
## 开始你的视频下载吧
|
||||
|
||||
是不是很简单,快去下载你的视频吧
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
+6
-3
@@ -4,10 +4,13 @@
|
||||
"description": "electron-template",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "pnpm --parallel -F \"./packages/*\" run dev",
|
||||
"build": "pnpm --parallel -F \"./packages/*\" run build",
|
||||
"dev": "pnpm run build:mobile && pnpm --parallel -F \"./packages/*\" run dev",
|
||||
"build": "pnpm run build:mobile && pnpm run build:main && pnpm run build:renderer",
|
||||
"build:mobile": "pnpm -F mobile run build",
|
||||
"build:main": "pnpm -F main run build",
|
||||
"build:renderer": "pnpm -F renderer run build",
|
||||
"pack": "pnpm -F media-downloader run pack",
|
||||
"release": "pnpm run build && pnpm -F media-downloader run release",
|
||||
"release": "pnpm run build && pnpm -F main run release",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
|
||||
@@ -3,3 +3,4 @@ node_modules
|
||||
build
|
||||
dist
|
||||
bin/Logs
|
||||
release
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
output
|
||||
bin
|
||||
mobile
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "media-downloader",
|
||||
"version": "2.0.2",
|
||||
"description": "在线视频下载器",
|
||||
"main": "main/index.js",
|
||||
"scripts": {},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.26.6",
|
||||
"@cliqz/adblocker-electron-preload": "^1.26.6"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "media-downloader",
|
||||
"version": "2.0.1",
|
||||
"name": "main",
|
||||
"version": "0.1.0",
|
||||
"description": "在线视频下载器",
|
||||
"main": "main/index.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"rebuild": "electron-rebuild -f -w better-sqlite3",
|
||||
"dev": "cross-env NODE_ENV=development node script/dev.mjs",
|
||||
"build": "cross-env NODE_ENV=production node script/build.mjs",
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import { rmSync } from "node:fs";
|
||||
import { mainResolve, loadDotEnvDefined } from "./utils.mjs";
|
||||
import {
|
||||
mainResolve,
|
||||
loadDotEnvDefined,
|
||||
copyResource,
|
||||
removeResource,
|
||||
} from "./utils.mjs";
|
||||
|
||||
const mainDefined = loadDotEnvDefined();
|
||||
|
||||
rmSync(mainResolve("build"), { recursive: true, force: true });
|
||||
rmSync(mainResolve("dist"), { recursive: true, force: true });
|
||||
removeResource([mainResolve("app/build")]);
|
||||
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
]);
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: [
|
||||
@@ -28,7 +40,7 @@ esbuild.build({
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
...mainDefined,
|
||||
},
|
||||
outdir: mainResolve("build/main"),
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: true,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { spawn } from "child_process";
|
||||
import { cpSync } from "node:fs";
|
||||
import electron from "electron";
|
||||
import * as esbuild from "esbuild";
|
||||
import chokidar from "chokidar";
|
||||
import { loadDotEnvRuntime, mainResolve, log } from "./utils.mjs";
|
||||
import { loadDotEnvRuntime, mainResolve, log, copyResource } from "./utils.mjs";
|
||||
|
||||
let electronProcess = null;
|
||||
|
||||
@@ -11,21 +10,18 @@ process.env.NODE_ENV = "development";
|
||||
loadDotEnvRuntime();
|
||||
|
||||
async function copySource() {
|
||||
const oldSqlite3Path = mainResolve(
|
||||
"node_modules/better-sqlite3/build/Release"
|
||||
);
|
||||
const newSqlite3Path = mainResolve("build/Release");
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
cpSync(oldSqlite3Path, newSqlite3Path, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const oldBinPath = mainResolve("bin");
|
||||
const newBinPath = mainResolve("build/bin");
|
||||
|
||||
cpSync(oldBinPath, newBinPath, {
|
||||
recursive: true,
|
||||
});
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
{
|
||||
from: mainResolve("bin"),
|
||||
to: mainResolve("app/bin"),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
@@ -47,10 +43,10 @@ const ctx = await esbuild.context({
|
||||
],
|
||||
define: {
|
||||
// 开发环境中二进制可执行文件的路径
|
||||
__bin__: `"${mainResolve("bin").replace(/\\/g, "\\\\")}"`,
|
||||
__bin__: `"${mainResolve("bin", process.platform).replace(/\\/g, "\\\\")}"`,
|
||||
},
|
||||
plugins: [],
|
||||
outdir: mainResolve("build/main"),
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
});
|
||||
|
||||
@@ -71,7 +67,7 @@ watcher.on("change", async () => {
|
||||
});
|
||||
|
||||
function startElectron() {
|
||||
const args = ["--inspect=5858", mainResolve("build/main/index.js")];
|
||||
const args = ["--inspect=5858", mainResolve("app/build/main/index.js")];
|
||||
|
||||
electronProcess = spawn(String(electron), args);
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import builder from "electron-builder";
|
||||
import { readFileSync } from "node:fs";
|
||||
import semver from "semver";
|
||||
import { mainResolve, loadDotEnvRuntime, log } from "./utils.mjs";
|
||||
import {
|
||||
mainResolve,
|
||||
loadDotEnvRuntime,
|
||||
log,
|
||||
removeResource,
|
||||
} from "./utils.mjs";
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(mainResolve("./package.json")));
|
||||
removeResource([mainResolve("release")]);
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(mainResolve("./app/package.json")));
|
||||
loadDotEnvRuntime();
|
||||
|
||||
if (semver.neq(process.env.APP_VERSION, packageJson.version)) {
|
||||
@@ -11,20 +18,6 @@ if (semver.neq(process.env.APP_VERSION, packageJson.version)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const extraResources = [
|
||||
{
|
||||
from: "build/mobile",
|
||||
to: "mobile",
|
||||
},
|
||||
];
|
||||
if (process.platform === "win32") {
|
||||
// windows
|
||||
extraResources.push("bin/ffmpeg.exe", "bin/N_m3u8DL-CLI_v3.0.2.exe");
|
||||
} else {
|
||||
// mac
|
||||
extraResources.push("bin/N_m3u8DL-RE", "bin/ffmpeg");
|
||||
}
|
||||
|
||||
// Let's get that intellisense working
|
||||
/**
|
||||
* @type {import('electron-builder').Configuration}
|
||||
@@ -37,21 +30,25 @@ const options = {
|
||||
copyright: process.env.APP_COPYRIGHT,
|
||||
artifactName: "${productName}-setup-${buildVersion}.${ext}",
|
||||
directories: {
|
||||
output: "./dist",
|
||||
output: "./release",
|
||||
},
|
||||
files: [
|
||||
{
|
||||
from: "./build",
|
||||
to: "./",
|
||||
filter: ["**/*"],
|
||||
},
|
||||
"./package.json",
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "./node_modules/better-sqlite3/build/Release",
|
||||
to: "./build/Release",
|
||||
from: "./app/mobile",
|
||||
to: "mobile",
|
||||
},
|
||||
{
|
||||
from: "./app/bin/${platform}/",
|
||||
to: "bin",
|
||||
},
|
||||
],
|
||||
extraResources,
|
||||
win: {
|
||||
icon: "../assets/icon.ico",
|
||||
target: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, cpSync, rmSync } from "node:fs";
|
||||
import dotenv from "dotenv";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -8,8 +8,8 @@ const con = console;
|
||||
export const log = con.log;
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
export const mainResolve = (r) => resolve(__dirname, "..", r);
|
||||
export const rootResolve = (r) => resolve(__dirname, "../../..", r);
|
||||
export const mainResolve = (...r) => resolve(__dirname, "..", ...r);
|
||||
export const rootResolve = (...r) => resolve(__dirname, "../../..", ...r);
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
log("当前的环境是: ", nodeEnv);
|
||||
|
||||
@@ -63,3 +63,18 @@ export function loadDotEnvDefined() {
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function copyResource(resource) {
|
||||
resource.forEach((r) => {
|
||||
const { from, to } = r;
|
||||
cpSync(from, to, {
|
||||
recursive: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function removeResource(resource) {
|
||||
resource.forEach((r) => {
|
||||
rmSync(r, { recursive: true, force: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
StoreService,
|
||||
} from "./interfaces";
|
||||
import { TYPES } from "./types";
|
||||
import { app, nativeTheme } from "electron";
|
||||
import { Menu, Tray, app, nativeImage, nativeTheme } from "electron";
|
||||
import TrayIcon from "./tray-icon.png";
|
||||
import path from "path";
|
||||
|
||||
@injectable()
|
||||
export default class ElectronApp implements App {
|
||||
@@ -60,6 +62,8 @@ export default class ElectronApp implements App {
|
||||
|
||||
this.initAppTheme();
|
||||
this.resetDownloadStatus();
|
||||
|
||||
this.initTray();
|
||||
}
|
||||
|
||||
initAppTheme(): void {
|
||||
@@ -67,6 +71,21 @@ export default class ElectronApp implements App {
|
||||
nativeTheme.themeSource = theme;
|
||||
}
|
||||
|
||||
initTray() {
|
||||
const iconPath = path.resolve(__dirname, TrayIcon);
|
||||
const icon = nativeImage.createFromPath(iconPath);
|
||||
const tray = new Tray(icon);
|
||||
tray.setToolTip("在线视频下载");
|
||||
tray.addListener("click", () => {
|
||||
this.mainWindow.init();
|
||||
});
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: "显示主窗口", click: () => this.mainWindow.init() },
|
||||
{ label: "退出 app", role: "quit" },
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
// 如果重启后还有正在下载的视频,就将状态改成下载失败
|
||||
async resetDownloadStatus(): Promise<void> {
|
||||
// 重启后如果还有 downloading 状态的数据, 全部重置为失败
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuItemConstructorOptions,
|
||||
nativeTheme,
|
||||
shell,
|
||||
clipboard,
|
||||
} from "electron";
|
||||
import { Favorite } from "entity/Favorite";
|
||||
import { convertToAudio } from "helper";
|
||||
@@ -175,7 +176,14 @@ export default class HomeController implements Controller {
|
||||
payload: id,
|
||||
});
|
||||
};
|
||||
const item = await this.videoRepository.findVideo(id);
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: "拷贝链接地址",
|
||||
click: () => {
|
||||
clipboard.writeText(item?.url || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "选择",
|
||||
click: () => {
|
||||
|
||||
@@ -32,6 +32,13 @@ export class Video {
|
||||
})
|
||||
headers: string;
|
||||
|
||||
@Column({
|
||||
type: "boolean",
|
||||
default: false,
|
||||
nullable: false,
|
||||
})
|
||||
isLive: boolean;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execa } from "execa";
|
||||
import { macDownloaderPath, winDownloaderPath } from "./variables";
|
||||
import iconv from "iconv-lite";
|
||||
import { event, formatHeaders, formatString, stripColors } from "./utils";
|
||||
import { formatHeaders, stripColors } from "./utils";
|
||||
import { DownloadParams, DownloadProgress } from "interfaces";
|
||||
|
||||
export const spawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
@@ -13,17 +13,22 @@ export const spawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
};
|
||||
|
||||
const winSpawnDownload = async (params: DownloadParams): Promise<void> => {
|
||||
const { id, abortSignal, url, local, name, deleteSegments, headers } = params;
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
} = params;
|
||||
const progressReg = /Progress:\s(\d+)\/(\d+)\s\(.+?\).+?\((.+?\/s).*?\)/g;
|
||||
const isLiveReg = /识别为直播流, 开始录制/g;
|
||||
const startDownloadReg = /开始下载文件/g;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnParams = [
|
||||
formatString(url),
|
||||
"--workDir",
|
||||
formatString(local),
|
||||
"--saveName",
|
||||
formatString(name),
|
||||
];
|
||||
const spawnParams = [url, "--workDir", local, "--saveName", name];
|
||||
|
||||
if (headers) {
|
||||
spawnParams.push("--headers", formatHeaders(headers));
|
||||
@@ -34,11 +39,10 @@ const winSpawnDownload = async (params: DownloadParams): Promise<void> => {
|
||||
}
|
||||
|
||||
const downloader = execa(winDownloaderPath, spawnParams, {
|
||||
detached: true,
|
||||
shell: true,
|
||||
signal: abortSignal.signal,
|
||||
});
|
||||
|
||||
let isLive = false;
|
||||
downloader.stdout?.on("data", (data) => {
|
||||
const str = iconv.decode(Buffer.from(data), "gbk");
|
||||
str.split("\n").forEach((item) => {
|
||||
@@ -46,15 +50,33 @@ const winSpawnDownload = async (params: DownloadParams): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
process.env.NODE_ENV === "development" && console.log(item);
|
||||
if (isLiveReg.test(item) || startDownloadReg.test(item)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive,
|
||||
cur: "",
|
||||
total: "",
|
||||
speed: "",
|
||||
});
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
const result = progressReg.exec(item);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, cur, total, speed] = result;
|
||||
const progress: DownloadProgress = { id, cur, total, speed };
|
||||
event.emit("download-progress", progress);
|
||||
const progress: DownloadProgress = {
|
||||
id,
|
||||
type: "progress",
|
||||
cur,
|
||||
total,
|
||||
speed,
|
||||
isLive,
|
||||
};
|
||||
callback(progress);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,8 +95,21 @@ const winSpawnDownload = async (params: DownloadParams): Promise<void> => {
|
||||
};
|
||||
|
||||
const macSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
const { id, abortSignal, url, local, name, deleteSegments, headers } = params;
|
||||
const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
} = params;
|
||||
// const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
|
||||
const progressReg = /([\d.]+)%/g;
|
||||
const errorReg = /ERROR/g;
|
||||
const startDownloadReg = /保存文件名:/g;
|
||||
const isLiveReg = /检测到直播流/g;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnParams = [
|
||||
@@ -85,29 +120,52 @@ const macSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
local,
|
||||
"--save-name",
|
||||
name,
|
||||
"--auto-select",
|
||||
];
|
||||
|
||||
if (headers) {
|
||||
spawnParams.push("--headers", formatHeaders(headers));
|
||||
const h: Record<string, unknown> = JSON.parse(headers);
|
||||
Object.entries(h).forEach(([k, v]) => {
|
||||
spawnParams.push("-H", `${k}: ${v}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSegments) {
|
||||
spawnParams.push("--del-after-done");
|
||||
}
|
||||
|
||||
console.log("spawnParams", spawnParams);
|
||||
const downloader = execa(macDownloaderPath, spawnParams, {
|
||||
signal: abortSignal.signal,
|
||||
});
|
||||
|
||||
let isLive = false;
|
||||
downloader.stdout?.on("data", (data) => {
|
||||
const str = String(Buffer.from(data));
|
||||
str.split("\n").forEach((item) => {
|
||||
if (item.trim() == "") {
|
||||
return;
|
||||
}
|
||||
process.env.NODE_ENV === "development" && console.log(item);
|
||||
const result = progressReg.exec(stripColors(item));
|
||||
|
||||
if (isLiveReg.test(item) || startDownloadReg.test(item)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive,
|
||||
cur: "",
|
||||
total: "",
|
||||
speed: "",
|
||||
});
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
const log = stripColors(item);
|
||||
|
||||
if (errorReg.test(log)) {
|
||||
reject(new Error(log));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = progressReg.exec(log);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@@ -119,8 +177,16 @@ const macSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
}
|
||||
|
||||
const total = "1000";
|
||||
const progress: DownloadProgress = { id, cur, total, speed };
|
||||
event.emit("download-progress", progress);
|
||||
// FIXME: 无法获取是否为直播流
|
||||
const progress: DownloadProgress = {
|
||||
id,
|
||||
type: "progress",
|
||||
cur,
|
||||
total,
|
||||
speed,
|
||||
isLive,
|
||||
};
|
||||
callback(progress);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// 创建 axios 实例,并指定自定义的 HTTPS Agent
|
||||
const instance = axios.create({
|
||||
httpsAgent: agent,
|
||||
});
|
||||
|
||||
export default instance;
|
||||
@@ -28,3 +28,4 @@ export * from "./variables";
|
||||
export { on, handle } from "./decorator";
|
||||
export { spawnDownload } from "./download";
|
||||
export { convertToAudio } from "./ffmpeg";
|
||||
export { default as http } from "./http";
|
||||
|
||||
@@ -18,11 +18,7 @@ export function formatHeaders(headersStr: string): string {
|
||||
const formatted = Object.entries(headers)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join("|");
|
||||
return formatString(formatted);
|
||||
}
|
||||
|
||||
export function formatString(str: string) {
|
||||
return JSON.stringify(str);
|
||||
return formatted;
|
||||
}
|
||||
|
||||
export const event = new EventEmitter();
|
||||
|
||||
@@ -15,15 +15,14 @@ export const PERSIST_MEDIAGO = "persist:mediago";
|
||||
export const PERSIST_WEBVIEW = "persist:webview";
|
||||
export const db = path.resolve(workspace, "app.db");
|
||||
export const macDownloaderPath = path.resolve(__bin__, "N_m3u8DL-RE");
|
||||
export const winDownloaderPath = path.resolve(
|
||||
__bin__,
|
||||
"N_m3u8DL-CLI_v3.0.2.exe"
|
||||
);
|
||||
export const winDownloaderPath = path.resolve(__bin__, "N_m3u8DL-CLI.exe");
|
||||
export const ffmpegPath =
|
||||
process.platform === "win32"
|
||||
? path.resolve(__bin__, "ffmpeg.exe")
|
||||
: path.resolve(__bin__, "ffmpeg");
|
||||
export const mobilePath = path.resolve(app.getAppPath(), "../mobile");
|
||||
export const mobilePath = isDev
|
||||
? path.resolve(app.getAppPath(), "../../mobile")
|
||||
: path.resolve(app.getAppPath(), "../mobile");
|
||||
|
||||
// user agent
|
||||
export const pcUA =
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
declare module "*.svg";
|
||||
declare module "*.png";
|
||||
declare module "*.jpg";
|
||||
declare module "*.jpeg";
|
||||
declare module "*.gif";
|
||||
declare module "*.bmp";
|
||||
declare module "*.tiff";
|
||||
@@ -25,9 +25,7 @@ const start = async (): Promise<void> => {
|
||||
mediago.init();
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
// empty
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -93,6 +93,11 @@ export interface VideoRepository {
|
||||
findWattingAndDownloadingVideos: () => Promise<Video[]>;
|
||||
deleteDownloadItem: (id: number) => Promise<DeleteResult>;
|
||||
findAllVideos(): Promise<Video[]>;
|
||||
changeVideoIsLive: (
|
||||
id: number | number[],
|
||||
isLive: boolean
|
||||
) => Promise<UpdateResult>;
|
||||
findVideoByUrl: (url: string) => Promise<Video | null>;
|
||||
}
|
||||
|
||||
export interface FavoriteRepository {
|
||||
@@ -145,9 +150,11 @@ export type Task = {
|
||||
|
||||
export interface DownloadProgress {
|
||||
id: number;
|
||||
type: string;
|
||||
cur: string;
|
||||
total: string;
|
||||
speed: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadParams {
|
||||
@@ -158,6 +165,7 @@ export interface DownloadParams {
|
||||
headers: string;
|
||||
abortSignal: AbortController;
|
||||
deleteSegments?: boolean;
|
||||
callback: (progress: DownloadProgress) => void;
|
||||
}
|
||||
|
||||
export interface DevToolsService {
|
||||
|
||||
Vendored
+3
-3
@@ -16,7 +16,7 @@ declare interface EnvPath {
|
||||
|
||||
declare interface BrowserWindowInitialVal {
|
||||
url?: string;
|
||||
sourceList?: LinkMessage[];
|
||||
sourceList?: WebSource[];
|
||||
}
|
||||
|
||||
declare interface ElectronAPI {
|
||||
@@ -60,7 +60,7 @@ declare interface ElectronAPI {
|
||||
setUserAgent: (isMobile: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
declare interface LinkMessage {
|
||||
declare interface WebSource {
|
||||
url: string;
|
||||
name: string;
|
||||
headers: string;
|
||||
@@ -94,5 +94,5 @@ declare interface AppStore {
|
||||
|
||||
declare interface BrowserStore {
|
||||
url: string;
|
||||
sourceList: LinkMessage[];
|
||||
sourceList: WebSource[];
|
||||
}
|
||||
|
||||
@@ -96,6 +96,16 @@ export default class VideoRepositoryImpl implements VideoRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async changeVideoIsLive(id: number | number[], isLive: boolean) {
|
||||
const ids = !Array.isArray(id) ? [id] : id;
|
||||
return this.dataService.appDataSource
|
||||
.createQueryBuilder()
|
||||
.update(Video)
|
||||
.set({ isLive })
|
||||
.where({ id: In(ids) })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findWattingAndDownloadingVideos() {
|
||||
return await this.dataService.appDataSource.getRepository(Video).find({
|
||||
where: {
|
||||
@@ -107,4 +117,10 @@ export default class VideoRepositoryImpl implements VideoRepository {
|
||||
async deleteDownloadItem(id: number) {
|
||||
return await this.dataService.appDataSource.getRepository(Video).delete(id);
|
||||
}
|
||||
|
||||
async findVideoByUrl(url: string) {
|
||||
return this.dataService.appDataSource.getRepository(Video).findOneBy({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,16 @@ export default class DownloadServiceImpl
|
||||
...task.params,
|
||||
id: task.id,
|
||||
abortSignal: controller,
|
||||
callback: (progress) => {
|
||||
if (progress.type === "progress") {
|
||||
this.emit("download-progress", progress);
|
||||
} else if (progress.type === "ready") {
|
||||
this.emit("download-ready-start", progress);
|
||||
if (progress.isLive) {
|
||||
this.removeTask(progress.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
delete this.signal[task.id];
|
||||
this.log(`taskId: ${task.id} success`);
|
||||
@@ -97,11 +107,8 @@ export default class DownloadServiceImpl
|
||||
this.emit("download-failed", task.id, err);
|
||||
}
|
||||
} finally {
|
||||
// 处理当前正在活动的任务
|
||||
const doneId = this.active.findIndex((i) => i.id === task.id);
|
||||
this.active.splice(doneId, 1);
|
||||
// 处理完成的任务
|
||||
this.runTask();
|
||||
this.removeTask(task.id);
|
||||
|
||||
// 传输完成
|
||||
if (this.queue.length === 0 && this.active.length === 0) {
|
||||
// this.emit("download-finish");
|
||||
@@ -109,6 +116,16 @@ export default class DownloadServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
removeTask(id: number) {
|
||||
// 处理当前正在活动的任务
|
||||
const doneId = this.active.findIndex((i) => i.id === id);
|
||||
this.active.splice(doneId, 1);
|
||||
// 处理完成的任务
|
||||
if (this.active.length < this.limit) {
|
||||
this.runTask();
|
||||
}
|
||||
}
|
||||
|
||||
runTask() {
|
||||
while (this.active.length < this.limit && this.queue.length > 0) {
|
||||
const task = this.queue.shift();
|
||||
|
||||
@@ -10,14 +10,18 @@ export default class UpdateServiceImpl implements UpdateService {
|
||||
@inject(TYPES.LoggerService) private readonly logger: LoggerService
|
||||
) {}
|
||||
|
||||
init(): void {
|
||||
async init(): Promise<void> {
|
||||
if (isDev) return;
|
||||
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
autoUpdater.logger = this.logger.logger;
|
||||
autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "自动更新完成",
|
||||
body: "下次重启时将会自动安装",
|
||||
});
|
||||
try {
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
autoUpdater.logger = this.logger.logger;
|
||||
await autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "自动更新完成",
|
||||
body: "下次重启时将会自动安装",
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.info("update error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,14 +44,12 @@ export default class WebServiceImpl implements WebService {
|
||||
this.app.use(cors());
|
||||
this.app.use(range);
|
||||
this.app.use(serve(local));
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
this.app.use(
|
||||
serve(mobilePath, {
|
||||
extensions: ["html", "js", "css"],
|
||||
index: "index.html",
|
||||
})
|
||||
);
|
||||
}
|
||||
this.app.use(
|
||||
serve(mobilePath, {
|
||||
extensions: ["html", "js", "css"],
|
||||
index: "index.html",
|
||||
})
|
||||
);
|
||||
this.app.use(this.router.routes());
|
||||
this.app.use(this.router.allowedMethods());
|
||||
|
||||
|
||||
@@ -19,13 +19,14 @@ import { PERSIST_WEBVIEW, mobileUA, pcUA } from "helper/variables";
|
||||
import { ElectronBlocker } from "@cliqz/adblocker-electron";
|
||||
import fetch from "cross-fetch";
|
||||
import path from "path";
|
||||
import { LinkMessage } from "main";
|
||||
import { WebSource } from "main";
|
||||
|
||||
// FIXME: 需要重构
|
||||
@injectable()
|
||||
export default class WebviewServiceImpl implements WebviewService {
|
||||
public view: BrowserView;
|
||||
private blocker?: ElectronBlocker;
|
||||
private sources = new Set();
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.MainWindowService)
|
||||
@@ -58,6 +59,7 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
this.setUserAgent(isMobile);
|
||||
|
||||
this.view.webContents.on("dom-ready", () => {
|
||||
this.sources.clear();
|
||||
const title = this.view.webContents.getTitle();
|
||||
const url = this.view.webContents.getURL();
|
||||
this.curWindow?.webContents.send("webview-dom-ready", { title, url });
|
||||
@@ -114,9 +116,11 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
await this.view.webContents.loadURL(url || "");
|
||||
} catch (err: unknown) {
|
||||
this.logger.error("加载 url 时出现错误: ", err);
|
||||
}
|
||||
if (!canGoBack && !isNewWindow) {
|
||||
this.view.webContents.clearHistory();
|
||||
throw err;
|
||||
} finally {
|
||||
if (!canGoBack && !isNewWindow) {
|
||||
this.view.webContents.clearHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,34 +225,44 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
const detailsUrl = new URL(url);
|
||||
|
||||
if (sourceReg.test(detailsUrl.pathname)) {
|
||||
this.handleM3u8(details);
|
||||
try {
|
||||
this.handleM3u8(details);
|
||||
} catch (e) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
callback({});
|
||||
};
|
||||
|
||||
handleM3u8 = (details: OnBeforeSendHeadersListenerDetails): void => {
|
||||
handleM3u8 = async (
|
||||
details: OnBeforeSendHeadersListenerDetails
|
||||
): Promise<void> => {
|
||||
const { id, url } = details;
|
||||
|
||||
this.logger.info(`在窗口中捕获 m3u8 链接: ${url} id: ${id}`);
|
||||
const webContents = details.webContents;
|
||||
const linkMessage: LinkMessage = {
|
||||
|
||||
const source: WebSource = {
|
||||
url,
|
||||
name: webContents?.getTitle() || "没有获取到名称",
|
||||
headers: JSON.stringify(details.requestHeaders),
|
||||
};
|
||||
// 这里需要判断是否使用浏览器插件
|
||||
const useExtension = this.storeService.get("useExtension");
|
||||
if (useExtension) {
|
||||
this.view.webContents.send("webview-link-message", linkMessage);
|
||||
} else {
|
||||
this.videoRepository.addVideo(linkMessage).then((item) => {
|
||||
|
||||
if (!this.sources.has(source.url)) {
|
||||
this.sources.add(source.url);
|
||||
if (useExtension) {
|
||||
this.view.webContents.send("webview-link-message", source);
|
||||
} else {
|
||||
const item = await this.videoRepository.addVideo(source);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
this.mainWindow.window?.webContents.send(
|
||||
"download-item-notifier",
|
||||
item
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 625 B |
@@ -1,4 +1,4 @@
|
||||
import { LinkMessage } from "main";
|
||||
import { WebSource } from "main";
|
||||
import { IpcRendererEvent, ipcRenderer } from "electron/renderer";
|
||||
|
||||
// 创建浮窗容器元素
|
||||
@@ -44,10 +44,10 @@ overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const items: LinkMessage[] = [];
|
||||
const items: WebSource[] = [];
|
||||
ipcRenderer.on(
|
||||
"webview-link-message",
|
||||
(e: IpcRendererEvent, data: LinkMessage) => {
|
||||
(e: IpcRendererEvent, data: WebSource) => {
|
||||
floatingContainer.style.display = "block";
|
||||
|
||||
items.push(data);
|
||||
|
||||
@@ -43,7 +43,8 @@ export default class MainWindowServiceImpl
|
||||
},
|
||||
});
|
||||
|
||||
event.on("download-progress", this.onDownloadProgress);
|
||||
this.downloadService.on("download-ready-start", this.onDownloadReadyStart);
|
||||
this.downloadService.on("download-progress", this.onDownloadProgress);
|
||||
this.downloadService.on("download-success", this.onDownloadSuccess);
|
||||
this.downloadService.on("download-failed", this.onDownloadFailed);
|
||||
this.downloadService.on("download-start", this.onDownloadStart);
|
||||
@@ -52,6 +53,11 @@ export default class MainWindowServiceImpl
|
||||
app.on("second-instance", this.secondInstance);
|
||||
}
|
||||
|
||||
onDownloadReadyStart = ({ id, isLive }: { id: number; isLive: boolean }) => {
|
||||
this.videoRepository.changeVideoIsLive(id, isLive);
|
||||
this.send("change-video-is-live", { id, isLive });
|
||||
};
|
||||
|
||||
init(): void {
|
||||
if (this.window) {
|
||||
// 如果窗口已经存在,则直接显示
|
||||
@@ -96,20 +102,15 @@ export default class MainWindowServiceImpl
|
||||
};
|
||||
|
||||
storeChange = (store: any) => {
|
||||
if (!this.window) return;
|
||||
// 向所有窗口发送通知
|
||||
this.window.webContents.send("store-change", store);
|
||||
this.send("store-change", store);
|
||||
};
|
||||
|
||||
onDownloadProgress = (progress: DownloadProgress) => {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send("download-progress", progress);
|
||||
this.send("download-progress", progress);
|
||||
};
|
||||
|
||||
onDownloadSuccess = async (id: number) => {
|
||||
if (!this.window) return;
|
||||
|
||||
const promptTone = this.storeService.get("promptTone");
|
||||
if (promptTone) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
@@ -120,12 +121,10 @@ export default class MainWindowServiceImpl
|
||||
}).show();
|
||||
}
|
||||
|
||||
this.window.webContents.send("download-success", id);
|
||||
this.send("download-success", id);
|
||||
};
|
||||
|
||||
onDownloadFailed = async (id: number, err: any) => {
|
||||
if (!this.window) return;
|
||||
|
||||
const promptTone = this.storeService.get("promptTone");
|
||||
if (promptTone) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
@@ -136,18 +135,20 @@ export default class MainWindowServiceImpl
|
||||
}).show();
|
||||
}
|
||||
this.logger.error("下载失败:", err);
|
||||
this.window.webContents.send("download-failed", id);
|
||||
this.send("download-failed", id);
|
||||
};
|
||||
|
||||
onDownloadStart = async (id: number) => {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send("download-start", id);
|
||||
this.send("download-start", id);
|
||||
};
|
||||
|
||||
onDownloadStop = async (id: number) => {
|
||||
this.send("download-stop", id);
|
||||
};
|
||||
|
||||
send(channel: string, ...args: any[]) {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send("download-stop", id);
|
||||
};
|
||||
this.window.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "Node",
|
||||
"strictPropertyInitialization": false,
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"outDir": "build/main"
|
||||
"outDir": "app/output/build/main"
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:mobile": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
|
||||
@@ -15,11 +15,7 @@ const player = ref<Player | null>(null);
|
||||
const list = ref<VideoData[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
let baseUrl = location.href;
|
||||
if (import.meta.env.MODE === "development") {
|
||||
baseUrl = `http://${location.hostname}:8433/`;
|
||||
}
|
||||
const res = await axios.get(`${baseUrl}api/video-list`);
|
||||
const res = await axios.get(`/api/video-list`);
|
||||
if (videoRef.value) {
|
||||
const options: IPlayerOptions = {
|
||||
el: videoRef.value,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default defineConfig({
|
||||
port: 8556,
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "../main/build/mobile"),
|
||||
outDir: path.resolve(__dirname, "../main/app/mobile"),
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/react": "^18.2.13",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/sort-by": "^1.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
"@typescript-eslint/parser": "5.60.0",
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
Popover,
|
||||
QRCode,
|
||||
Dropdown,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import "./index.scss";
|
||||
import PageContainer from "../../components/PageContainer";
|
||||
import { useAsyncEffect, usePagination } from "ahooks";
|
||||
@@ -28,12 +28,12 @@ import {
|
||||
SyncOutlined,
|
||||
MobileOutlined,
|
||||
MoreOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectAppStore } from "../../store";
|
||||
import { tdApp } from "../../utils";
|
||||
import { increase } from "../../store/downloadSlice";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
enum DownloadFilter {
|
||||
list = "list",
|
||||
@@ -83,10 +83,8 @@ const HomePage: FC = () => {
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const isDev = import.meta.env.MODE === "development";
|
||||
const localIP = await getLocalIP();
|
||||
const port = isDev ? 8556 : import.meta.env.APP_SERVER_PORT;
|
||||
setBaseUrl(`http://${localIP}:${port}/`);
|
||||
setBaseUrl(`http://${localIP}:${import.meta.env.APP_SERVER_PORT}/`);
|
||||
}, []);
|
||||
|
||||
const onDownloadProgress = (e: any, progress: DownloadProgress) => {
|
||||
@@ -132,6 +130,10 @@ const HomePage: FC = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
const onChangeVideoIsLive = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
rendererEvent("download-progress", onDownloadProgress);
|
||||
rendererEvent("download-success", onDownloadSuccess);
|
||||
@@ -139,6 +141,7 @@ const HomePage: FC = () => {
|
||||
rendererEvent("download-start", onDownloadStart);
|
||||
rendererEvent("download-item-event", onDownloadMenuEvent);
|
||||
rendererEvent("download-item-notifier", onReceiveDownloadItem);
|
||||
rendererEvent("change-video-is-live", onChangeVideoIsLive);
|
||||
|
||||
return () => {
|
||||
removeEventListener("download-progress", onDownloadProgress);
|
||||
@@ -147,6 +150,7 @@ const HomePage: FC = () => {
|
||||
removeEventListener("download-start", onDownloadStart);
|
||||
removeEventListener("download-item-event", onDownloadMenuEvent);
|
||||
removeEventListener("download-item-notifier", onReceiveDownloadItem);
|
||||
removeEventListener("change-video-is-live", onChangeVideoIsLive);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -324,12 +328,12 @@ const HomePage: FC = () => {
|
||||
label: "播放视频",
|
||||
key: "play",
|
||||
icon: <PlayCircleOutlined />,
|
||||
disabled: curConverting,
|
||||
},
|
||||
{
|
||||
label: "转换为音频",
|
||||
key: "convert",
|
||||
icon: <SyncOutlined />,
|
||||
disabled: curConverting,
|
||||
},
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
@@ -362,10 +366,14 @@ const HomePage: FC = () => {
|
||||
} else if (item.status === DownloadStatus.Stopped) {
|
||||
tag = <Tag color="default">下载暂停</Tag>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{item.name}
|
||||
{tag}
|
||||
<Text>{item.name}</Text>
|
||||
<Space size={[0, 8]}>
|
||||
{tag}
|
||||
{item.isLive && <Tag color={"default"}>直播资源</Tag>}
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -490,6 +498,7 @@ const HomePage: FC = () => {
|
||||
]}
|
||||
/>
|
||||
</ModalForm>
|
||||
<Button onClick={() => openDir(appStore.local)}>打开文件夹</Button>
|
||||
</Space>
|
||||
}
|
||||
className="home-page"
|
||||
|
||||
@@ -44,7 +44,7 @@ const SettingPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderButtonLable = () => {
|
||||
const renderButtonLabel = () => {
|
||||
return (
|
||||
<Button onClick={onSelectDir} icon={<FolderOpenOutlined />}>
|
||||
选择文件夹
|
||||
@@ -52,30 +52,11 @@ const SettingPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderTooltipLable = () => {
|
||||
const renderTooltipLabel = (label: string, tooltip: string) => {
|
||||
return (
|
||||
<div className="item-label">
|
||||
<div className="item-label-text">代理开关</div>
|
||||
<Tooltip
|
||||
title={"该代理会对软件自带浏览器以及下载时生效"}
|
||||
placement={"right"}
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderExtensionLabel = () => {
|
||||
return (
|
||||
<div className="item-label">
|
||||
<div className="item-label-text">使用浏览器插件</div>
|
||||
<Tooltip
|
||||
title={
|
||||
"开启后浏览器嗅探到的资源将不会直接显示在下载列表中,所有资源嗅探会交给浏览器插件处理"
|
||||
}
|
||||
placement={"right"}
|
||||
>
|
||||
<div className="item-label-text">{label}</div>
|
||||
<Tooltip title={tooltip} placement={"right"}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -102,7 +83,7 @@ const SettingPage: React.FC = () => {
|
||||
formRef={formRef}
|
||||
layout="horizontal"
|
||||
submitter={false}
|
||||
labelCol={{ style: { width: "130px" } }}
|
||||
labelCol={{ style: { width: "140px" } }}
|
||||
labelAlign={"left"}
|
||||
colon={false}
|
||||
initialValues={settings}
|
||||
@@ -115,7 +96,7 @@ const SettingPage: React.FC = () => {
|
||||
disabled
|
||||
name="local"
|
||||
placeholder="请选择视频下载目录"
|
||||
label={renderButtonLable()}
|
||||
label={renderButtonLabel()}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="theme"
|
||||
@@ -139,7 +120,10 @@ const SettingPage: React.FC = () => {
|
||||
/>
|
||||
<ProFormSwitch
|
||||
name="useProxy"
|
||||
label={renderTooltipLable()}
|
||||
label={renderTooltipLabel(
|
||||
"代理开关",
|
||||
"该代理会对软件自带浏览器以及下载时生效"
|
||||
)}
|
||||
rules={[
|
||||
({ getFieldValue, setFieldValue }) => ({
|
||||
validator() {
|
||||
@@ -154,12 +138,21 @@ const SettingPage: React.FC = () => {
|
||||
/>
|
||||
<ProFormSwitch label="开启广告过滤" name="blockAds" />
|
||||
<ProFormSwitch label="以手机模式进入" name="isMobile" />
|
||||
<ProFormSwitch label={renderExtensionLabel()} name="useExtension" />
|
||||
<ProFormSwitch
|
||||
label={renderTooltipLabel(
|
||||
"使用沉浸式嗅探",
|
||||
"开启后沉浸式嗅探到的资源将不会直接显示在下载列表中,所有资源嗅探会交给浏览器插件处理"
|
||||
)}
|
||||
name="useExtension"
|
||||
/>
|
||||
</ProFormGroup>
|
||||
<ProFormGroup title="下载设置" direction={"vertical"}>
|
||||
<ProFormSwitch label="下载完成删除分片" name="deleteSegments" />
|
||||
<ProFormDigit
|
||||
label="最大同时下载数量"
|
||||
label={renderTooltipLabel(
|
||||
"最大同时下载数量",
|
||||
"直播录制不占用同时下载数量"
|
||||
)}
|
||||
name="maxRunner"
|
||||
min={1}
|
||||
max={10}
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
overflow: auto;
|
||||
.webview-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.webview-inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -13,7 +13,17 @@ import {
|
||||
StarOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useAsyncEffect, useRequest } from "ahooks";
|
||||
import { Avatar, Button, Form, Input, List, message, Space } from "antd";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
List,
|
||||
message,
|
||||
Space,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import PageContainer from "../../components/PageContainer";
|
||||
@@ -22,6 +32,7 @@ import { generateUrl, getFavIcon } from "../../utils";
|
||||
import "./index.scss";
|
||||
import { ModalForm, ProFormText } from "@ant-design/pro-components";
|
||||
import {
|
||||
BrowserStatus,
|
||||
PageMode,
|
||||
selectBrowserStore,
|
||||
setAppStore,
|
||||
@@ -43,7 +54,6 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
rendererEvent,
|
||||
removeEventListener,
|
||||
webviewGoBack,
|
||||
webviewReload,
|
||||
webwiewGoHome,
|
||||
onFavoriteItemContextMenu,
|
||||
combineToHomePage,
|
||||
@@ -67,13 +77,28 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
}, []);
|
||||
|
||||
const loadUrl = async (url: string) => {
|
||||
await webviewLoadURL(url);
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: url,
|
||||
mode: PageMode.Browser,
|
||||
})
|
||||
);
|
||||
try {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
mode: PageMode.Browser,
|
||||
status: BrowserStatus.Loading,
|
||||
})
|
||||
);
|
||||
await webviewLoadURL(url);
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: url,
|
||||
status: BrowserStatus.Loaded,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
status: BrowserStatus.Failed,
|
||||
errMsg: (err as any).message,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const goto = async () => {
|
||||
@@ -129,10 +154,6 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onClickReload = () => {
|
||||
webviewReload();
|
||||
};
|
||||
|
||||
const onClickEnter = async () => {
|
||||
if (!store.url) {
|
||||
return;
|
||||
@@ -213,13 +234,15 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
|
||||
// 渲染工具栏
|
||||
const renderToolbar = () => {
|
||||
const disabled =
|
||||
store.status !== BrowserStatus.Loaded || store.mode !== PageMode.Browser;
|
||||
return (
|
||||
<Space.Compact className="action-bar" block>
|
||||
<Button type="text" title="切换为手机模式" onClick={onSetDefaultUA}>
|
||||
{appStore.isMobile ? <MobileFilled /> : <MobileOutlined />}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={store.mode === PageMode.Default}
|
||||
disabled={disabled}
|
||||
title="首页"
|
||||
type="text"
|
||||
onClick={onClickGoHome}
|
||||
@@ -227,26 +250,21 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
<HomeOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={store.mode === PageMode.Default}
|
||||
disabled={disabled}
|
||||
title="回退"
|
||||
type="text"
|
||||
onClick={onClickGoBack}
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={store.mode === PageMode.Default}
|
||||
title="刷新"
|
||||
type="text"
|
||||
onClick={onClickReload}
|
||||
>
|
||||
<Button disabled={disabled} title="刷新" type="text" onClick={goto}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
title={curIsFavorite ? "取消收藏" : "收藏"}
|
||||
onClick={onClickAddFavorite}
|
||||
disabled={store.mode === PageMode.Default}
|
||||
disabled={disabled}
|
||||
>
|
||||
{curIsFavorite ? <StarFilled /> : <StarOutlined />}
|
||||
</Button>
|
||||
@@ -257,6 +275,9 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
const url = e.target.value;
|
||||
dispatch(setBrowserStore({ url }));
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
onKeyDown={onInputKeyDown}
|
||||
placeholder="请输入网址链接……"
|
||||
/>
|
||||
@@ -279,11 +300,24 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
|
||||
// 渲染浏览器面板
|
||||
const renderBrowserPanel = () => {
|
||||
return (
|
||||
<div className="webview-container">
|
||||
<WebView className="webview-inner" />
|
||||
</div>
|
||||
);
|
||||
let content = <div></div>;
|
||||
if (store.status === BrowserStatus.Loading) {
|
||||
content = <Spin />;
|
||||
} else if (store.status === BrowserStatus.Failed) {
|
||||
content = (
|
||||
<Empty description={store.errMsg || "加载失败"}>
|
||||
<Space>
|
||||
<Button type="primary" onClick={onClickGoHome}>
|
||||
返回首页
|
||||
</Button>
|
||||
<Button onClick={goto}>刷新</Button>
|
||||
</Space>
|
||||
</Empty>
|
||||
);
|
||||
} else if (store.status === BrowserStatus.Loaded) {
|
||||
content = <WebView className="webview-inner" />;
|
||||
}
|
||||
return <div className="webview-container">{content}</div>;
|
||||
};
|
||||
|
||||
// 渲染收藏 item
|
||||
|
||||
Vendored
+5
-1
@@ -12,6 +12,7 @@ declare interface DownloadItem {
|
||||
url: string;
|
||||
headers: string;
|
||||
status: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
declare interface VideoResponse {
|
||||
@@ -73,7 +74,7 @@ declare interface Favorite {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
declare interface LinkMessage {
|
||||
declare interface WebSource {
|
||||
url: string;
|
||||
name: string;
|
||||
headers: string;
|
||||
@@ -111,6 +112,8 @@ declare interface BrowserStore {
|
||||
mode: PageMode;
|
||||
url: string;
|
||||
title: string;
|
||||
status: BrowserStatus;
|
||||
errMsg?: string;
|
||||
}
|
||||
|
||||
declare interface DownloadProgress {
|
||||
@@ -118,6 +121,7 @@ declare interface DownloadProgress {
|
||||
cur: string;
|
||||
total: string;
|
||||
speed: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
interface ObjectConstructor {
|
||||
|
||||
@@ -9,10 +9,19 @@ export enum PageMode {
|
||||
Browser = "browser",
|
||||
}
|
||||
|
||||
export enum BrowserStatus {
|
||||
Default = "default",
|
||||
Loaded = "loaded",
|
||||
Loading = "loading",
|
||||
Failed = "failed",
|
||||
}
|
||||
|
||||
const initialState: BrowserStore = {
|
||||
mode: PageMode.Default,
|
||||
url: "",
|
||||
title: "",
|
||||
status: BrowserStatus.Default,
|
||||
errMsg: "",
|
||||
};
|
||||
|
||||
const convertPlainObject = (obj: unknown) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
||||
envDir: "../..",
|
||||
envPrefix: "APP",
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "../main/build/renderer"),
|
||||
outDir: path.resolve(__dirname, "../main/app/build/renderer"),
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+12
-3
@@ -1,4 +1,4 @@
|
||||
lockfileVersion: '6.1'
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
@@ -192,6 +192,15 @@ importers:
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.3
|
||||
|
||||
packages/main/app:
|
||||
dependencies:
|
||||
'@cliqz/adblocker-electron':
|
||||
specifier: ^1.26.6
|
||||
version: 1.26.6(electron@25.2.0)
|
||||
'@cliqz/adblocker-electron-preload':
|
||||
specifier: ^1.26.6
|
||||
version: 1.26.6(electron@25.2.0)
|
||||
|
||||
packages/mobile:
|
||||
dependencies:
|
||||
axios:
|
||||
@@ -332,10 +341,10 @@ importers:
|
||||
specifier: ^20.3.1
|
||||
version: 20.3.1
|
||||
'@types/react':
|
||||
specifier: ^18.2.13
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.13
|
||||
'@types/react-dom':
|
||||
specifier: ^18.2.6
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.6
|
||||
'@types/sort-by':
|
||||
specifier: ^1.2.0
|
||||
|
||||
Reference in New Issue
Block a user