pull/13/head
parent
3364b572d1
commit
5641d28f28
@ -1,4 +0,0 @@
|
||||
VITE_APP_TDID=
|
||||
VITE_APP_SENTRY_DSN=
|
||||
|
||||
NODE_ENV=development
|
@ -1,4 +0,0 @@
|
||||
VITE_APP_TDID=
|
||||
VITE_APP_SENTRY_DSN=
|
||||
|
||||
NODE_ENV=production
|
@ -1,6 +0,0 @@
|
||||
.eslintrc.js
|
||||
esbuild.config.js
|
||||
babel.config.js
|
||||
webpack.config.js
|
||||
script/
|
||||
vite.config.ts
|
@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "react"],
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"react-app",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
.bin
|
||||
node_modules
|
||||
build
|
||||
.electron
|
||||
dist
|
@ -1,31 +0,0 @@
|
||||
import Store from "electron-store/index";
|
||||
|
||||
import { BrowserWindow } from "electron";
|
||||
import { Windows } from "./utils/variables";
|
||||
|
||||
declare interface IWindowManager {
|
||||
create: (name: Windows) => Promise<BrowserWindow | null>;
|
||||
get: (name: Windows) => BrowserWindow | null;
|
||||
has: (name: Windows) => boolean;
|
||||
deleteById: (id: number) => void;
|
||||
}
|
||||
|
||||
declare interface IWindowListItem {
|
||||
url: string;
|
||||
options: () => Electron.BrowserWindowConstructorOptions;
|
||||
callback: (
|
||||
window: BrowserWindow,
|
||||
windowManager: IWindowManager
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export { IWindowManager, IWindowListItem };
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
__bin__: string;
|
||||
store: Store<AppStore>;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "app-main",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development node script/dev.js",
|
||||
"dist": "cross-env NODE_ENV=production node script/build.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"electron": "16.0.4",
|
||||
"esbuild": "^0.9.5",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/electron": "^2.5.4",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-log": "^4.3.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-store": "^7.0.2",
|
||||
"electron-updater": "^4.6.1",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob": "^7.1.6",
|
||||
"mediago-node": "workspace:^1.0.0",
|
||||
"moment": "^2.29.1",
|
||||
"nanoid": "^3.1.30",
|
||||
"qs": "^6.10.1",
|
||||
"semver": "^7.3.4",
|
||||
"sequelize": "^6.19.0",
|
||||
"spawn-args": "^0.2.0",
|
||||
"sqlite3": "^5.0.6"
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const { resolve } = require("path");
|
||||
|
||||
const rimraf = require("rimraf");
|
||||
|
||||
rimraf.sync(resolve(__dirname, "../build"));
|
||||
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
let envPath = resolve(__dirname, `../../../.env.${process.env.NODE_ENV}.local`);
|
||||
if (!fs.existsSync(envPath)) {
|
||||
envPath = resolve(__dirname, `../../../.env.${process.env.NODE_ENV}`);
|
||||
}
|
||||
|
||||
const { parsed } = require("dotenv").config({ path: envPath });
|
||||
|
||||
const mainDefined = Object.keys(parsed || {}).reduce((prev, cur) => {
|
||||
prev[`process.env.${[cur]}`] = JSON.stringify(parsed[cur]);
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
require("esbuild").build({
|
||||
entryPoints: [
|
||||
resolve(__dirname, "../src/index.ts"),
|
||||
resolve(__dirname, "../src/preload.ts"),
|
||||
],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: false,
|
||||
target: ["node16.13"],
|
||||
external: ["electron", "pg-hstore"],
|
||||
outdir: resolve(__dirname, "../dist"),
|
||||
loader: { ".png": "file" },
|
||||
define: {
|
||||
...mainDefined,
|
||||
},
|
||||
});
|
@ -1,92 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const { resolve, join } = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
const electron = require("electron");
|
||||
|
||||
let electronProcess = null;
|
||||
let manualRestart = false;
|
||||
|
||||
process.env.NODE_ENV = "development";
|
||||
|
||||
let envPath = resolve(__dirname, `../../../.env.${process.env.NODE_ENV}.local`);
|
||||
if (!fs.existsSync(envPath)) {
|
||||
envPath = resolve(__dirname, `../../../.env.${process.env.NODE_ENV}`);
|
||||
}
|
||||
|
||||
require("dotenv").config({ path: envPath });
|
||||
|
||||
function startMain() {
|
||||
return require("esbuild").build({
|
||||
entryPoints: [
|
||||
resolve(__dirname, "../src/index.ts"),
|
||||
resolve(__dirname, "../src/preload.ts"),
|
||||
],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: true,
|
||||
target: ["node16.13"],
|
||||
external: ["electron", "pg-hstore"],
|
||||
define: {
|
||||
// 开发环境中二进制可执行文件的路径
|
||||
__bin__: `"${resolve(__dirname, "../.bin").replace(/\\/g, "\\\\")}"`,
|
||||
},
|
||||
outdir: resolve(__dirname, "../dist"),
|
||||
loader: { ".png": "file" },
|
||||
watch: {
|
||||
onRebuild(error, result) {
|
||||
if (error) {
|
||||
console.error("watch build failed:", error);
|
||||
} else {
|
||||
console.log("watch build succeed.");
|
||||
if (electronProcess && electronProcess.kill) {
|
||||
manualRestart = true;
|
||||
process.kill(electronProcess.pid);
|
||||
electronProcess = null;
|
||||
startElectron();
|
||||
|
||||
setTimeout(() => {
|
||||
manualRestart = false;
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function startElectron() {
|
||||
let args = ["--inspect=5858", join(__dirname, "../dist/index.js")];
|
||||
|
||||
electronProcess = spawn(String(electron), args);
|
||||
|
||||
electronProcess.stdout.on("data", (data) => {
|
||||
electronLog(data, "blue");
|
||||
});
|
||||
|
||||
electronProcess.stderr.on("data", (data) => {
|
||||
electronLog(data, "red");
|
||||
});
|
||||
|
||||
electronProcess.on("close", () => {
|
||||
if (!manualRestart) process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
function electronLog(data, color) {
|
||||
let log = "";
|
||||
data = data.toString().split(/\r?\n/);
|
||||
data.forEach((line) => {
|
||||
if (line.trim()) log += `${line}\n`;
|
||||
});
|
||||
console.log(log);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await startMain();
|
||||
await startElectron();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit();
|
||||
}
|
||||
})();
|
@ -1,69 +0,0 @@
|
||||
import { windowManager } from "./window";
|
||||
import { BrowserView } from "electron";
|
||||
import { Sessions, Windows } from "../utils/variables";
|
||||
import { nanoid } from "nanoid";
|
||||
import { sessionList } from "./session";
|
||||
import logger from "./logger";
|
||||
|
||||
const createBrowserView = (): void => {
|
||||
const browserWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
|
||||
const view = new BrowserView({
|
||||
webPreferences: {
|
||||
partition: Sessions.PERSIST_MEDIAGO,
|
||||
},
|
||||
});
|
||||
browserWindow.setBrowserView(view);
|
||||
view.setBounds({ x: 0, y: 0, height: 0, width: 0 });
|
||||
|
||||
const { webContents } = view;
|
||||
if (process.env.NODE_ENV === "development") webContents.openDevTools();
|
||||
|
||||
webContents.on("dom-ready", () => {
|
||||
const title = webContents.getTitle();
|
||||
const url = webContents.getURL();
|
||||
|
||||
browserWindow.webContents.send("dom-ready", { title, url });
|
||||
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
webContents.loadURL(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
});
|
||||
|
||||
const filter = { urls: ["*://*/*"] };
|
||||
sessionList
|
||||
.get(Sessions.PERSIST_MEDIAGO)!
|
||||
.webRequest.onBeforeSendHeaders(
|
||||
filter,
|
||||
(
|
||||
details,
|
||||
callback: (beforeSendResponse: Electron.BeforeSendResponse) => void
|
||||
) => {
|
||||
const m3u8Reg = /\.m3u8$/;
|
||||
let cancel = false;
|
||||
const myURL = new URL(details.url);
|
||||
if (m3u8Reg.test(myURL.pathname)) {
|
||||
logger.info("在窗口中捕获 m3u8 链接: ", details.url);
|
||||
const { webContents: mainWindow } = windowManager.get(
|
||||
Windows.MAIN_WINDOW
|
||||
);
|
||||
const value: SourceUrl = {
|
||||
id: nanoid(),
|
||||
title: webContents.getTitle(),
|
||||
url: details.url,
|
||||
headers: details.requestHeaders,
|
||||
duration: 0,
|
||||
};
|
||||
mainWindow.send("m3u8-notifier", value);
|
||||
cancel = true;
|
||||
}
|
||||
callback({
|
||||
cancel,
|
||||
requestHeaders: details.requestHeaders,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default createBrowserView;
|
@ -1,95 +0,0 @@
|
||||
import Runner from "./runner";
|
||||
import glob from "glob";
|
||||
import { binDir } from "../utils/variables";
|
||||
import semver from "semver";
|
||||
import { pathExists } from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
export class Downloader {
|
||||
protected bin = ""; // 可执行文件地址
|
||||
protected args = ""; // runner 参数
|
||||
|
||||
constructor(public type: string) {}
|
||||
|
||||
handle(runner: Runner): void {
|
||||
runner.setDownloader(this);
|
||||
}
|
||||
|
||||
async parseArgs(args: Record<string, string>): Promise<void> {
|
||||
// empty
|
||||
}
|
||||
|
||||
getBin(): string {
|
||||
return this.bin;
|
||||
}
|
||||
|
||||
getArgs(): string {
|
||||
return this.args;
|
||||
}
|
||||
}
|
||||
|
||||
// mediago 下载器
|
||||
class MediaGoDownloader extends Downloader {
|
||||
constructor() {
|
||||
super("mediago");
|
||||
|
||||
this.bin = process.platform === "win32" ? "mediago" : "./mediago";
|
||||
}
|
||||
|
||||
async parseArgs(args: Record<string, string>): Promise<void> {
|
||||
this.args = Object.entries(args)
|
||||
.reduce((prev: string[], [key, value]) => {
|
||||
if (value) prev.push(`-${key} "${value}"`);
|
||||
return prev;
|
||||
}, [])
|
||||
.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
// N_m3u8DL-CLI 下载器
|
||||
class NM3u8DlCliDownloader extends Downloader {
|
||||
constructor() {
|
||||
super("N_m3u8DL-CLI");
|
||||
|
||||
const binNameList = glob.sync("N_m3u8DL-CLI*.exe", {
|
||||
cwd: binDir,
|
||||
});
|
||||
const [version] = binNameList
|
||||
.map((item) => /N_m3u8DL-CLI_v(.*).exe/.exec(item)?.[1] || "0.0.0")
|
||||
.filter((item) => semver.valid(item))
|
||||
.sort((a, b) => (semver.gt(a, b) ? -1 : 1));
|
||||
if (!version) throw new Error("没有找到 N_m3u8DL-CLI");
|
||||
this.bin = `N_m3u8DL-CLI_v${version}`;
|
||||
if (process.platform === "win32") {
|
||||
this.bin = `${this.bin}.exe`;
|
||||
}
|
||||
}
|
||||
|
||||
async parseArgs(args: Record<string, string>): Promise<void> {
|
||||
const binExist = await pathExists(path.resolve(binDir, this.bin));
|
||||
if (!binExist) throw new Error("没有找到 N_m3u8DL-CLI");
|
||||
|
||||
const argsStr = Object.entries(args)
|
||||
.reduce((prev: string[], [key, value]) => {
|
||||
if (key === "url") return prev;
|
||||
if (value && typeof value === "boolean") prev.push(`--${key}`);
|
||||
if (value && (typeof value === "string" || typeof value === "number"))
|
||||
prev.push(`--${key} "${value}"`);
|
||||
return prev;
|
||||
}, [])
|
||||
.join(" ");
|
||||
this.args = `"${args.url}" ${argsStr}`;
|
||||
}
|
||||
}
|
||||
|
||||
const createDownloader = (type: string): Downloader => {
|
||||
if (type === "mediago") {
|
||||
return new MediaGoDownloader();
|
||||
}
|
||||
if (type === "N_m3u8DL-CLI") {
|
||||
return new NM3u8DlCliDownloader();
|
||||
}
|
||||
throw new Error("暂不支持该下载方式");
|
||||
};
|
||||
|
||||
export { createDownloader };
|
@ -1,12 +0,0 @@
|
||||
import path from "path";
|
||||
import moment from "moment";
|
||||
import logger from "electron-log";
|
||||
import { workspace } from "../utils/variables";
|
||||
|
||||
const datetime = moment().format("YYYY-MM-DD");
|
||||
const logPath = path.resolve(workspace, `logs/${datetime}-mediago.log`);
|
||||
logger.transports.console.format = "{h}:{i}:{s} {text}";
|
||||
logger.transports.file.getFile();
|
||||
logger.transports.file.resolvePath = () => logPath;
|
||||
|
||||
export default logger;
|
@ -1,61 +0,0 @@
|
||||
import { spawn, SpawnOptions } from "child_process";
|
||||
import { workspace } from "../utils/variables";
|
||||
import argsBuilder from "spawn-args";
|
||||
import logger from "../core/logger";
|
||||
import { Downloader } from "./downloader";
|
||||
|
||||
// runner
|
||||
class Runner {
|
||||
private static instance: Runner;
|
||||
|
||||
private constructor(private downloader?: Downloader) {}
|
||||
|
||||
static getInstance(): Runner {
|
||||
if (!Runner.instance) {
|
||||
Runner.instance = new Runner();
|
||||
}
|
||||
return Runner.instance;
|
||||
}
|
||||
|
||||
setDownloader(downloader: Downloader): void {
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
run(options: SpawnOptions): Promise<void> {
|
||||
const command = this.downloader?.getBin();
|
||||
const args = this.downloader?.getArgs();
|
||||
|
||||
if (!command || !args) throw new Error("请先初始化downloader");
|
||||
logger.info("下载参数:", options.cwd, command, args);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnCommand = spawn(command, argsBuilder(args), {
|
||||
cwd: workspace,
|
||||
detached: true,
|
||||
shell: true,
|
||||
...options,
|
||||
});
|
||||
|
||||
spawnCommand.stdout?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.log(`stdout: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.stderr?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.error(`stderr: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.on("close", (code) => {
|
||||
if (code !== 0) reject(new Error(`调用 ${command} 可执行文件执行失败`));
|
||||
else resolve();
|
||||
});
|
||||
|
||||
spawnCommand.on("error", (err) => {
|
||||
console.error(`err: ${err}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Runner;
|
@ -1,10 +0,0 @@
|
||||
import { session, Session } from "electron";
|
||||
import { Sessions } from "../utils/variables";
|
||||
|
||||
const sessionList = new Map<Sessions, Session>();
|
||||
|
||||
function createSession(partition: Sessions): void {
|
||||
sessionList.set(partition, session.fromPartition(partition));
|
||||
}
|
||||
|
||||
export { sessionList, createSession };
|
@ -1,101 +0,0 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { Windows } from "../utils/variables";
|
||||
import { resolve } from "path";
|
||||
import { IWindowListItem, IWindowManager } from "../../main";
|
||||
|
||||
const windowList = new Map<Windows, IWindowListItem>();
|
||||
|
||||
windowList.set(Windows.MAIN_WINDOW, {
|
||||
url:
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:7789/main"
|
||||
: "mediago://index.html/main",
|
||||
options(): Electron.BrowserWindowConstructorOptions {
|
||||
return {
|
||||
width: 800,
|
||||
minWidth: 800,
|
||||
height: 600,
|
||||
minHeight: 600,
|
||||
show: false,
|
||||
frame: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
};
|
||||
},
|
||||
async callback(window) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
window.once("ready-to-show", () => {
|
||||
window.show();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
windowList.set(Windows.BROWSER_WINDOW, {
|
||||
url:
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:7789/browser"
|
||||
: "mediago://index.html/browser",
|
||||
options(): Electron.BrowserWindowConstructorOptions {
|
||||
return {
|
||||
width: 800,
|
||||
height: 600,
|
||||
show: false,
|
||||
frame: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
};
|
||||
},
|
||||
async callback(window) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
class Window implements IWindowManager {
|
||||
private windowMap: Map<Windows | string, BrowserWindow> = new Map();
|
||||
|
||||
private windowIdMap: Map<number, Windows | string> = new Map();
|
||||
|
||||
async create(name: Windows) {
|
||||
const windowConfig: IWindowListItem = windowList.get(name)!;
|
||||
const window = new BrowserWindow(windowConfig.options());
|
||||
const { id } = window;
|
||||
this.windowMap.set(name, window);
|
||||
this.windowIdMap.set(window.id, name);
|
||||
window.loadURL(windowConfig.url);
|
||||
windowConfig.callback(window, this);
|
||||
window.on("close", () => {
|
||||
this.deleteById(id);
|
||||
});
|
||||
return window;
|
||||
}
|
||||
|
||||
get(name: Windows) {
|
||||
return this.windowMap.get(name)!;
|
||||
}
|
||||
|
||||
has(name: Windows) {
|
||||
return this.windowMap.has(name);
|
||||
}
|
||||
|
||||
deleteById = (id: number) => {
|
||||
const name = this.windowIdMap.get(id);
|
||||
if (name) {
|
||||
this.windowMap.delete(name);
|
||||
this.windowIdMap.delete(id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const windowManager = new Window();
|
||||
|
||||
export { windowManager, windowList };
|
@ -1,17 +0,0 @@
|
||||
import { db } from "../utils/variables";
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: "sqlite",
|
||||
storage: db,
|
||||
});
|
||||
|
||||
export const Favorite = sequelize.define("Favorite", {
|
||||
title: DataTypes.STRING,
|
||||
url: DataTypes.STRING,
|
||||
});
|
||||
|
||||
export const Video = sequelize.define("Video", {
|
||||
title: DataTypes.STRING,
|
||||
url: DataTypes.STRING,
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
import { sequelize } from "./db";
|
||||
|
||||
(async function () {
|
||||
await sequelize.sync();
|
||||
})();
|
@ -1,16 +0,0 @@
|
||||
// import installExtension, {
|
||||
// REACT_DEVELOPER_TOOLS,
|
||||
// REDUX_DEVTOOLS,
|
||||
// } from "electron-devtools-installer";
|
||||
import logger from "../core/logger";
|
||||
|
||||
export default async function handleExtension(): Promise<void> {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
try {
|
||||
// await installExtension(REACT_DEVELOPER_TOOLS);
|
||||
// await installExtension(REDUX_DEVTOOLS);
|
||||
} catch (e) {
|
||||
logger.info(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
import { app, ipcMain, Menu } from "electron";
|
||||
import { failFn, successFn } from "../utils";
|
||||
import { windowManager } from "../core/window";
|
||||
import { binDir, Windows } from "../utils/variables";
|
||||
import Runner from "../core/runner";
|
||||
import { createDownloader } from "../core/downloader";
|
||||
import { downloader as mediaNode } from "mediago-node";
|
||||
|
||||
const handleIpc = (): void => {
|
||||
ipcMain.on("close-main-window", async () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
ipcMain.on("open-browser-window", (e, url) => {
|
||||
// 开始计算主窗口的位置
|
||||
const browserWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
const browserView = browserWindow.getBrowserView();
|
||||
browserView?.webContents.loadURL(url || "https://baidu.com");
|
||||
browserWindow.show();
|
||||
});
|
||||
|
||||
ipcMain.on("close-browser-window", () => {
|
||||
const browserWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
browserWindow.hide();
|
||||
});
|
||||
|
||||
ipcMain.on("set-browser-view-bounds", (e, rect) => {
|
||||
const currentWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
const view = currentWindow.getBrowserView();
|
||||
if (view) view.setBounds(rect);
|
||||
});
|
||||
|
||||
ipcMain.on("browser-view-go-back", (e) => {
|
||||
const currentWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
const view = currentWindow.getBrowserView();
|
||||
if (view) {
|
||||
const canGoBack = view.webContents.canGoBack();
|
||||
if (canGoBack) view.webContents.goBack();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("browser-view-reload", (e) => {
|
||||
const currentWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
const view = currentWindow.getBrowserView();
|
||||
if (view) view.webContents.reload();
|
||||
});
|
||||
|
||||
ipcMain.on("browser-view-load-url", (e, url: string) => {
|
||||
const currentWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
const view = currentWindow.getBrowserView();
|
||||
if (view) view.webContents.loadURL(url || "https://baidu.com");
|
||||
});
|
||||
|
||||
ipcMain.on("open-download-item-context-menu", (e, item: SourceItem) => {
|
||||
const mainWin = windowManager.get(Windows.MAIN_WINDOW);
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "详情",
|
||||
click: () => {
|
||||
e.sender.send("download-context-menu-detail", item);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "下载",
|
||||
click: () => {
|
||||
e.sender.send("download-context-menu-download", item);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "删除",
|
||||
click: () => {
|
||||
e.sender.send("download-context-menu-delete", item);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "清空列表",
|
||||
click: () => {
|
||||
e.sender.send("download-context-menu-clear-all");
|
||||
},
|
||||
},
|
||||
]);
|
||||
menu.popup({
|
||||
window: mainWin,
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("window-minimize", (e, name) => {
|
||||
let window;
|
||||
if (name === "main") {
|
||||
window = windowManager.get(Windows.MAIN_WINDOW);
|
||||
} else {
|
||||
window = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
}
|
||||
|
||||
window.minimize();
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"exec-command",
|
||||
async (event, exeFile: string, args: Record<string, string>) => {
|
||||
try {
|
||||
if (exeFile === "mediago") {
|
||||
await mediaNode({
|
||||
name: args["name"],
|
||||
path: args["path"],
|
||||
url: args["url"],
|
||||
});
|
||||
return successFn("success");
|
||||
}
|
||||
const runner = Runner.getInstance();
|
||||
const downloader = createDownloader(exeFile);
|
||||
downloader.handle(runner);
|
||||
await downloader.parseArgs(args);
|
||||
const result = await runner.run({ cwd: binDir });
|
||||
return successFn(result);
|
||||
} catch (e: any) {
|
||||
return failFn(-1, e.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle("get-bin-dir", async () => binDir);
|
||||
|
||||
ipcMain.handle("set-store", (e, key, value) => global.store.set(key, value));
|
||||
|
||||
ipcMain.handle("get-store", (e, key) => global.store.get(key));
|
||||
|
||||
ipcMain.handle("get-path", (e, name) => app.getPath(name));
|
||||
|
||||
ipcMain.handle("get-current-window", (e) => {
|
||||
const currentWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
return currentWindow.getBrowserView();
|
||||
});
|
||||
|
||||
ipcMain.handle("ipc:data", (e, { path: string, params: any }) => {
|
||||
console.log(123);
|
||||
});
|
||||
};
|
||||
|
||||
export default handleIpc;
|
@ -1,54 +0,0 @@
|
||||
import Store from "electron-store";
|
||||
import { Sessions, workspace } from "../utils/variables";
|
||||
import { sessionList } from "../core/session";
|
||||
import logger from "../core/logger";
|
||||
|
||||
export default function handleStore(): void {
|
||||
let exeFile = "";
|
||||
if (process.platform === "win32") {
|
||||
exeFile = "N_m3u8DL-CLI";
|
||||
} else {
|
||||
exeFile = "mediago";
|
||||
}
|
||||
|
||||
global.store = new Store<AppStore>({
|
||||
name: "config",
|
||||
cwd: workspace,
|
||||
fileExtension: "json",
|
||||
watch: true,
|
||||
defaults: {
|
||||
workspace: "",
|
||||
exeFile,
|
||||
tip: true,
|
||||
proxy: "",
|
||||
useProxy: false,
|
||||
statistics: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 设置软件代理
|
||||
const setProxy = (isInit?: boolean) => {
|
||||
try {
|
||||
const webviewSession = sessionList.get(Sessions.PERSIST_MEDIAGO)!;
|
||||
const proxy = global.store.get("proxy");
|
||||
const useProxy = global.store.get("useProxy");
|
||||
if (proxy && useProxy) {
|
||||
logger.info(
|
||||
`[proxy] ${isInit ? "初始化" : "开启"}成功,代理地址为${proxy}`
|
||||
);
|
||||
webviewSession.setProxy({ proxyRules: proxy });
|
||||
} else {
|
||||
if (!isInit) logger.info(`[proxy] 关闭成功`);
|
||||
webviewSession.setProxy({});
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(
|
||||
`[proxy] ${isInit ? "初始化" : ""}设置代理失败:\n${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
setProxy(true);
|
||||
|
||||
global.store.onDidChange("useProxy", setProxy);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import logger from "../core/logger";
|
||||
import { protocol } from "electron";
|
||||
import { defaultScheme } from "../utils/variables";
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: defaultScheme,
|
||||
privileges: {
|
||||
secure: true,
|
||||
standard: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export default function handleUpdater(): void {
|
||||
autoUpdater.logger = logger;
|
||||
autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "发现新版本",
|
||||
body: "已经下载完成,下次打开时安装~",
|
||||
});
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { windowManager } from "../core/window";
|
||||
import { Windows } from "../utils/variables";
|
||||
import createBrowserView from "../core/browser";
|
||||
|
||||
export default async function handleWindows(): Promise<void> {
|
||||
await Promise.all([
|
||||
windowManager.create(Windows.MAIN_WINDOW),
|
||||
await windowManager.create(Windows.BROWSER_WINDOW),
|
||||
]);
|
||||
await createBrowserView();
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import { app, BrowserWindow, crashReporter } from "electron";
|
||||
import handleIpc from "./helper/handleIpc";
|
||||
import { Sessions } from "./utils/variables";
|
||||
import handleStore from "./helper/handleStore";
|
||||
import handleExtension from "./helper/handleExtension";
|
||||
import handleWindows from "./helper/handleWindows";
|
||||
import * as Sentry from "@sentry/electron/dist/main";
|
||||
import { author, name } from "../package.json";
|
||||
import { createSession } from "./core/session";
|
||||
import handleProtocol from "./helper/handleProtocol";
|
||||
import handleUpdater from "./helper/handleUpdater";
|
||||
import "./db";
|
||||
|
||||
Sentry.init({ dsn: process.env.VITE_APP_SENTRY_DSN });
|
||||
|
||||
if (process.env.VITE_APP_SENTRY_DSN) {
|
||||
crashReporter.start({
|
||||
companyName: author,
|
||||
productName: name,
|
||||
ignoreSystemCrashHandler: true,
|
||||
submitURL: process.env.VITE_APP_SENTRY_DSN,
|
||||
});
|
||||
}
|
||||
|
||||
if (require("electron-squirrel-startup")) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
app.on("activate", async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
await handleWindows();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
handleUpdater();
|
||||
handleProtocol();
|
||||
createSession(Sessions.PERSIST_MEDIAGO);
|
||||
handleWindows();
|
||||
handleExtension();
|
||||
handleStore();
|
||||
handleIpc();
|
||||
});
|
@ -1,52 +0,0 @@
|
||||
import { contextBridge, dialog, ipcRenderer, shell } from "electron";
|
||||
import { resolve } from "path";
|
||||
|
||||
const apiKey = "electron";
|
||||
const api: ElectronApi = {
|
||||
store: {
|
||||
set(key, value) {
|
||||
return ipcRenderer.invoke("set-store", key, value);
|
||||
},
|
||||
get(key) {
|
||||
return ipcRenderer.invoke("get-store", key);
|
||||
},
|
||||
},
|
||||
isWindows: process.platform === "win32",
|
||||
isMacos: process.platform === "darwin",
|
||||
ipcExec: (exeFile, args) => ipcRenderer.invoke("exec-command", exeFile, args),
|
||||
openBinDir: async () => {
|
||||
const binDir = await ipcRenderer.invoke("get-bin-dir");
|
||||
await shell.openPath(binDir);
|
||||
},
|
||||
openPath: (workspace) => shell.openPath(workspace),
|
||||
openConfigDir: async () => {
|
||||
const appName =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "media downloader dev"
|
||||
: "media downloader";
|
||||
const appPath = await ipcRenderer.invoke("get-path", "appData");
|
||||
await shell.openPath(resolve(appPath, appName));
|
||||
},
|
||||
openExternal: (url, options) => shell.openExternal(url, options),
|
||||
openBrowserWindow: (url) => ipcRenderer.send("open-browser-window", url),
|
||||
closeBrowserWindow: () => ipcRenderer.send("close-browser-window"),
|
||||
getPath: (name) => ipcRenderer.invoke("get-path", name),
|
||||
showOpenDialog: (options) => {
|
||||
return dialog.showOpenDialog(options);
|
||||
},
|
||||
getBrowserView: () => ipcRenderer.invoke("get-current-window"),
|
||||
addEventListener: (channel, listener) => ipcRenderer.on(channel, listener),
|
||||
removeEventListener: (channel, listener) =>
|
||||
ipcRenderer.removeListener(channel, listener),
|
||||
setBrowserViewRect: (rect) =>
|
||||
ipcRenderer.send("set-browser-view-bounds", rect),
|
||||
closeMainWindow: () => ipcRenderer.send("close-main-window"),
|
||||
browserViewGoBack: () => ipcRenderer.send("browser-view-go-back"),
|
||||
browserViewReload: () => ipcRenderer.send("browser-view-reload"),
|
||||
browserViewLoadURL: (url) => ipcRenderer.send("browser-view-load-url", url),
|
||||
itemContextMenu: (item) =>
|
||||
ipcRenderer.send("open-download-item-context-menu", item),
|
||||
minimize: (name) => ipcRenderer.send("window-minimize", name),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld(apiKey, api);
|
@ -1 +0,0 @@
|
||||
declare module "spawn-args";
|
@ -1,8 +0,0 @@
|
||||
const successFn = (data: unknown): IpcResponse => ({ code: 0, msg: "", data });
|
||||
const failFn = (code: number, msg: string): IpcResponse => ({
|
||||
code,
|
||||
msg,
|
||||
data: null,
|
||||
});
|
||||
|
||||
export { successFn, failFn };
|
@ -1,29 +0,0 @@
|
||||
import { app } from "electron";
|
||||
import path, { resolve } from "path";
|
||||
|
||||
declare const __bin__: string;
|
||||
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
global.__bin__ = resolve(app.getAppPath(), "../.bin").replace(/\\/g, "\\\\");
|
||||
}
|
||||
|
||||
export const appData = app.getPath("appData");
|
||||
export const appName =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "media downloader dev"
|
||||
: "media downloader";
|
||||
export const workspace = path.resolve(appData, appName);
|
||||
export const defaultScheme = "mediago";
|
||||
|
||||
export enum Windows {
|
||||
MAIN_WINDOW = "MAIN_WINDOW",
|
||||
BROWSER_WINDOW = "BROWSER_WINDOW",
|
||||
}
|
||||
|
||||
export enum Sessions {
|
||||
PERSIST_MEDIAGO = "persist:mediago",
|
||||
}
|
||||
|
||||
export const binDir = __bin__;
|
||||
|
||||
export const db = path.resolve(workspace, "database.sqlite");
|
@ -1,23 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "./src",
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"src/types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"files": [
|
||||
"./main.d.ts",
|
||||
"../global.d.ts"
|
||||
]
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
.eslintrc.js
|
||||
esbuild.config.js
|
||||
babel.config.js
|
||||
webpack.config.js
|
||||
script/
|
||||
vite.config.ts
|
@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "react"],
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"react-app",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
.idea
|
||||
.bin
|
||||
.vscode
|
||||
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
build
|
||||
devtools/*
|
@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="referrer" content="never">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>在线视频下载</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,82 +0,0 @@
|
||||
{
|
||||
"name": "app-renderer",
|
||||
"version": "1.1.5",
|
||||
"description": "在线视频下载",
|
||||
"author": "caorushizi",
|
||||
"main": "dist/main/index.js",
|
||||
"license": "MIT",
|
||||
"homepage": "./",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development vite",
|
||||
"dist": "cross-env NODE_ENV=production tsc && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
"@ant-design/pro-form": "^1.49.6",
|
||||
"@ant-design/pro-table": "^2.59.2",
|
||||
"@chakra-ui/react": "^1.7.2",
|
||||
"@emotion/react": "^11",
|
||||
"@emotion/styled": "^11",
|
||||
"@sentry/electron": "^2.5.4",
|
||||
"@sentry/react": "^6.16.1",
|
||||
"@sentry/tracing": "^6.16.1",
|
||||
"antd": "^4.17.2",
|
||||
"classnames": "^2.3.1",
|
||||
"connected-react-router": "^6.9.1",
|
||||
"framer-motion": "^4",
|
||||
"history": "^4.10.1",
|
||||
"localforage": "^1.9.0",
|
||||
"m3u8-parser": "^4.7.0",
|
||||
"moment": "^2.29.1",
|
||||
"nanoid": "^3.1.30",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
"re-resizable": "^6.9.1",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-file-drop": "^3.1.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.6",
|
||||
"react-window": "^1.8.6",
|
||||
"redux": "^4.1.1",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-saga": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-eslint": "^8.0.1",
|
||||
"@types/glob": "^7.1.3",
|
||||
"@types/node": "^14.14.35",
|
||||
"@types/prop-types": "^15.7.5",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/react-router": "^5.1.16",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.1",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "16.0.4",
|
||||
"eslint": "^7.5.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-plugin-flowtype": "^5.2.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"eslint-plugin-react-hooks": "^4.0.8",
|
||||
"less": "^4.1.2",
|
||||
"prettier": "2.2.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.32.8",
|
||||
"typescript": "^4.2.4",
|
||||
"vite": "^2.5.0"
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,39 +0,0 @@
|
||||
$border-color: rgb(217, 217, 217);
|
||||
|
||||
.header-edit {
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.header-field-container {
|
||||
border: 1px solid $border-color;
|
||||
|
||||
.header-item-container {
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
border-right: 1px solid rgb(217, 217, 217);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.form-item-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
import "./index.scss";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { AutoComplete, Form, Input, Row, Col, Button } from "antd";
|
||||
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export interface HeaderEditProps {
|
||||
label: string;
|
||||
name: string;
|
||||
valuePropName?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface HeaderFieldInputProps {
|
||||
value?: Record<string, string>;
|
||||
onChange?: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ value: "Accept" },
|
||||
{ value: "Accept-Charset" },
|
||||
{ value: "Accept-Encoding" },
|
||||
{ value: "Accept-Language" },
|
||||
{ value: "Accept-Ranges" },
|
||||
{ value: "Authorization" },
|
||||
{ value: "Cache-Control" },
|
||||
{ value: "Connection" },
|
||||
{ value: "Cookie" },
|
||||
{ value: "Content-Length" },
|
||||
{ value: "Content-Type" },
|
||||
{ value: "Date" },
|
||||
{ value: "From" },
|
||||
{ value: "Host" },
|
||||
{ value: "If-Match" },
|
||||
{ value: "If-Modified-Since" },
|
||||
{ value: "If-None-Match" },
|
||||
{ value: "If-Range" },
|
||||
{ value: "If-Unmodified-Since" },
|
||||
{ value: "Max-Forwards" },
|
||||
{ value: "Pragma" },
|
||||
{ value: "Proxy-Authorization" },
|
||||
{ value: "Range" },
|
||||
{ value: "Referer" },
|
||||
{ value: "TE" },
|
||||
{ value: "Upgrade" },
|
||||
{ value: "User-Agent" },
|
||||
{ value: "Via" },
|
||||
{ value: "Warning" },
|
||||
];
|
||||
|
||||
interface FormItem {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const renderItem = (
|
||||
item: FormItem,
|
||||
onChange: (item: FormItem) => void,
|
||||
onDelete: (item: FormItem) => void
|
||||
) => {
|
||||
return (
|
||||
<Row className={"header-item-container"} key={item.id}>
|
||||
<Col span={8}>
|
||||
<AutoComplete
|
||||
value={item.key}
|
||||
style={{ width: "100%" }}
|
||||
options={options}
|
||||
dropdownMatchSelectWidth={false}
|
||||
filterOption={(inputValue, option) => {
|
||||
if (!option) return false;
|
||||
return (
|
||||
option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !==
|
||||
-1
|
||||
);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onChange({ ...item, key: value });
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
<Input
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChange({ ...item, value: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={1} className={"form-item-action"}>
|
||||
<DeleteOutlined
|
||||
style={{ color: "#F56C6C", cursor: "pointer" }}
|
||||
onClick={() => onDelete(item)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
// 编辑 header
|
||||
const HeaderFieldInput: FC<HeaderFieldInputProps> = ({ value, onChange }) => {
|
||||
const [formValues, setFormValues] = useState<FormItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const values = processHeader(value);
|
||||
setFormValues(values);
|
||||
}, []);
|
||||
|
||||
const processHeader = (value?: Record<string, string>) => {
|
||||
if (!value) return [];
|
||||
return Object.entries(value).map(([key, value]) => ({
|
||||
id: nanoid(),
|
||||
key,
|
||||
value,
|
||||
}));
|
||||
};
|
||||
|
||||
const postHeader = (values: FormItem[]) => {
|
||||
return values.reduce((cur: Record<string, string>, prev) => {
|
||||
if (!prev.key) return cur;
|
||||
|
||||
cur[prev.key] = prev.value;
|
||||
return cur;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const onInputChange = (item: FormItem): void => {
|
||||
const copiedFormValues = formValues.slice();
|
||||
|
||||
const changeItem = copiedFormValues.find((i) => i.id === item.id);
|
||||
if (!changeItem) return;
|
||||
|
||||
changeItem.key = item.key;
|
||||
changeItem.value = item.value;
|
||||
|
||||
setFormValues(copiedFormValues);
|
||||
onChange?.(postHeader(copiedFormValues));
|
||||
};
|
||||
|
||||
const onInputDelete = (item: FormItem) => {
|
||||
const changeItemIndex = formValues.findIndex((i) => i.id === item.id);
|
||||
if (changeItemIndex < 0) return;
|
||||
|
||||
const copiedFormValues = formValues.slice();
|
||||
copiedFormValues.splice(changeItemIndex, 1);
|
||||
setFormValues(copiedFormValues);
|
||||
onChange?.(postHeader(copiedFormValues));
|
||||
};
|
||||
|
||||
const onInputAdd = () => {
|
||||
const changedValue = [...formValues, { id: nanoid(), key: "", value: "" }];
|
||||
setFormValues(changedValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{formValues.length > 0 && (
|
||||
<Box className={"header-field-container"} mb={6}>
|
||||
{formValues.map((formItem) => {
|
||||
return renderItem(formItem, onInputChange, onInputDelete);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
<Box d={"flex"} justifyContent={"space-between"} alignItems={"center"}>
|
||||
<Box>{formValues.length <= 0 && "点击添加 header"}</Box>
|
||||
<Button
|
||||
size={"small"}
|
||||
type={"link"}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onInputAdd}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Http Header 编辑组件
|
||||
const HeaderEdit: FC<HeaderEditProps> = ({ label, valuePropName, name }) => {
|
||||
return (
|
||||
<Form.Item
|
||||
className={"header-edit"}
|
||||
name={name}
|
||||
label={label}
|
||||
valuePropName={valuePropName}
|
||||
>
|
||||
<HeaderFieldInput />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderEdit;
|
Binary file not shown.
Before Width: | Height: | Size: 209 B |
Binary file not shown.
Before Width: | Height: | Size: 355 B |
Binary file not shown.
Before Width: | Height: | Size: 82 B |
@ -1,105 +0,0 @@
|
||||
@import "../../main";
|
||||
|
||||
.window-tool-bar {
|
||||
-webkit-app-region: drag;
|
||||
padding-left: 10px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
height: 30px;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@include text-ellipsis();
|
||||
|
||||
.mac-btn {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin: 0 3px;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
|
||||
&.close {
|
||||
background: rgb(250, 100, 94);
|
||||
}
|
||||
|
||||
&.min {
|
||||
background: rgb(232, 189, 90);
|
||||
}
|
||||
|
||||
&.max {
|
||||
background: rgb(106, 187, 83);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.close {
|
||||
background-image: url(assets/btn-close.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 6px 6px;
|
||||
background-color: rgb(250, 100, 94);
|
||||
}
|
||||
|
||||
&.min {
|
||||
background-image: url(assets/btn-min.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 6px 2px;
|
||||
background-color: rgb(232, 189, 90);
|
||||
}
|
||||
|
||||
&.max {
|
||||
background-image: url(assets/btn-max.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 6px 6px;
|
||||
background-color: rgb(106, 187, 83);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
max-width: 400px;
|
||||
@include text-ellipsis();
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.btn {
|
||||
height: 29px;
|
||||
line-height: 30px;
|
||||
width: 45px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e5e5e5;
|
||||
}
|
||||
|
||||
&.close {
|
||||
&:hover {
|
||||
background: #e81123;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React, { FC, PropsWithChildren } from "react";
|
||||
import "./index.scss";
|
||||
import { CloseOutlined, MinusOutlined } from "@ant-design/icons";
|
||||
import useElectron from "../../hooks/electron";
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
onClose?: () => void;
|
||||
onMinimize?: () => void;
|
||||
}
|
||||
|
||||
const WindowToolBar: FC<PropsWithChildren<Props>> = ({
|
||||
onClose,
|
||||
onMinimize,
|
||||
color = "#fff",
|
||||
children,
|
||||
}) => {
|
||||
const { isMacos, isWindows } = useElectron();
|
||||
|
||||
return (
|
||||
<div className="window-tool-bar" style={{ background: color }}>
|
||||
<div className="window-tool-bar-left">
|
||||
{isMacos && <div className="mac-btn close" onClick={onClose} />}
|
||||
{isMacos && <div className="mac-btn min" onClick={onMinimize} />}
|
||||
</div>
|
||||
<div className="window-tool-bar-title">{children}</div>
|
||||
<div className="window-tool-bar-right">
|
||||
{isWindows && (
|
||||
<div className="btn" onClick={onMinimize}>
|
||||
<MinusOutlined />
|
||||
</div>
|
||||
)}
|
||||
{isWindows && (
|
||||
<div className="btn close" onClick={onClose}>
|
||||
<CloseOutlined />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WindowToolBar;
|
@ -1,5 +0,0 @@
|
||||
const useElectron = (): ElectronApi => {
|
||||
return window.electron;
|
||||
};
|
||||
|
||||
export default useElectron;
|
@ -1,41 +0,0 @@
|
||||
@mixin text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin scrollbar($c: rgba(0, 0, 0, 0.15)) {
|
||||
&::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $c;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin window-common {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-wrap {
|
||||
@include scrollbar;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: Alibaba-PuHuiTi-Regular;
|
||||
@include scrollbar();
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import React from "react";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Route } from "react-router";
|
||||
import tdApp from "./utils/td";
|
||||
import "antd/dist/antd.css";
|
||||
import { Provider } from "react-redux";
|
||||
import BrowserPage from "./nodes/browser";
|
||||
import MainPage from "./nodes/main";
|
||||
import "./main.scss";
|
||||
import store from "./store";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
|
||||
Sentry.init({
|
||||
dsn: String(import.meta.env.VITE_APP_SENTRY_DSN || ""),
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
tdApp.init();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<Route path="/main" component={MainPage} />
|
||||
<Route path="/browser" component={BrowserPage} />
|
||||
</Router>
|
||||
</Provider>,
|
||||
document.getElementById("root")
|
||||
);
|
@ -1,42 +0,0 @@
|
||||
.search-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 5px;
|
||||
|
||||
.search-inner {
|
||||
flex: 1;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
border: 1px solid #dcdfe6;
|
||||
outline: none;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 5px;
|
||||
|
||||
&:focus {
|
||||
border-color: #66afe9;
|
||||
outline: 0;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px rgba(102, 175, 233, 0.6);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e5e5e5;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
import React, { ChangeEvent } from "react";
|
||||
import "./index.scss";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ArrowRightOutlined,
|
||||
HomeOutlined,
|
||||
ReloadOutlined,
|
||||
StarFilled,
|
||||
StarOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import classNames from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
interface Props {
|
||||
onGoBack: () => void;
|
||||
onReload: () => void;
|
||||
onGoBackHome: () => void;
|
||||
handleEnter: () => void;
|
||||
url: string;
|
||||
onUrlChange: (url: string) => void;
|
||||
className: string;
|
||||
isFav: boolean;
|
||||
handleClickFav: () => void;
|
||||
}
|
||||
|
||||
const SearchBar: React.FC<Props> = (props) => {
|
||||
const {
|
||||
onGoBack,
|
||||
onReload,
|
||||
onGoBackHome,
|
||||
url,
|
||||
onUrlChange,
|
||||
className,
|
||||
handleEnter,
|
||||
isFav,
|
||||
handleClickFav,
|
||||
} = props;
|
||||
|
||||
// 搜索框变化
|
||||
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
onUrlChange(e.target.value);
|
||||
};
|
||||
|
||||
// 搜索框键盘事件
|
||||
const handleEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
handleEnter();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames("search-bar", className)}>
|
||||
<div className="btn">
|
||||
<ArrowLeftOutlined className="icon" onClick={onGoBack} />
|
||||
</div>
|
||||
<div className="btn">
|
||||
<ReloadOutlined className="icon" onClick={onReload} />
|
||||
</div>
|
||||
<div className="btn">
|
||||
<HomeOutlined className="icon home" onClick={onGoBackHome} />
|
||||
</div>
|
||||
<div className="btn" onClick={handleClickFav}>
|
||||
{isFav ? <StarFilled /> : <StarOutlined />}
|
||||
</div>
|
||||
<input
|
||||
className="search-inner"
|
||||
placeholder="请在此输入网址"
|
||||
type="text"
|
||||
value={url}
|
||||
onKeyPress={handleEnterPress}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<div className="btn">
|
||||
<ArrowRightOutlined className="button" onClick={handleEnter} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchBar.propTypes = {
|
||||
onGoBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SearchBar.defaultProps = {};
|
||||
|
||||
export default SearchBar;
|
@ -1,39 +0,0 @@
|
||||
@import "../../main";
|
||||
|
||||
@include window-common;
|
||||
|
||||
$playlist-width: 200px;
|
||||
|
||||
.browser-window {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.webview-container {
|
||||
flex: 1;
|
||||
width: 100vw;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
|
||||
.webview-nav {
|
||||
}
|
||||
|
||||
.webview-inner {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
#videoView {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f2f6fc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
import React, { FC, useEffect, useRef, useState } from "react";
|
||||
import "./index.scss";
|
||||
import { Spin } from "antd";
|
||||
import tdApp from "../../utils/td";
|
||||
import "antd/dist/antd.css";
|
||||
import { insertFav, isFavFunc, removeFav } from "../../utils/localforge";
|
||||
import WindowToolBar from "../../components/WindowToolBar";
|
||||
import SearchBar from "./elements/SearchBar";
|
||||
import onEvent from "../../utils/td-utils";
|
||||
import useElectron from "../../hooks/electron";
|
||||
|
||||
tdApp.init();
|
||||
|
||||
const computeRect = ({
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => ({
|
||||
x: Math.floor(left),
|
||||
y: Math.floor(top),
|
||||
width: Math.floor(width),
|
||||
height: Math.floor(height),
|
||||
});
|
||||
|
||||
const BrowserWindow: FC = () => {
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [isFav, setIsFav] = useState<boolean>(false);
|
||||
const webviewRef = useRef<HTMLDivElement>();
|
||||
const resizeObserver = useRef<ResizeObserver>();
|
||||
const {
|
||||
browserViewGoBack,
|
||||
browserViewReload,
|
||||
browserViewLoadURL,
|
||||
addEventListener,
|
||||
setBrowserViewRect,
|
||||
removeEventListener,
|
||||
closeBrowserWindow,
|
||||
minimize,
|
||||
} = useElectron();
|
||||
|
||||
useEffect(() => {
|
||||
initWebView();
|
||||
addEventListener("dom-ready", handleViewDOMReady);
|
||||
|
||||
return () => {
|
||||
setBrowserViewRect({ x: 0, y: 0, height: 0, width: 0 });
|
||||
removeEventListener("dom-ready", handleViewDOMReady);
|
||||
resizeObserver.current?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleViewDOMReady = async (
|
||||
e: Electron.IpcRendererEvent,
|
||||
{ url, title }: { url: string; title: string }
|
||||
): Promise<void> => {
|
||||
const isFav = await isFavFunc(url);
|
||||
setUrl(url);
|
||||
setTitle(title);
|
||||
setIsFav(isFav);
|
||||
document.title = title;
|
||||
};
|
||||
|
||||
const initWebView = () => {
|
||||
if (webviewRef.current) {
|
||||
const rect = computeRect(webviewRef.current.getBoundingClientRect());
|
||||
setBrowserViewRect(rect);
|
||||
|
||||
// 监控 webview 元素的大小
|
||||
resizeObserver.current = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
const viewRect = computeRect(entry.contentRect);
|
||||
viewRect.x += rect.x;
|
||||
viewRect.y += rect.y;
|
||||
setBrowserViewRect(viewRect);
|
||||
});
|
||||
|
||||
resizeObserver.current.observe(webviewRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onGoBack = () => {
|
||||
onEvent.browserPageGoBack();
|
||||
browserViewGoBack();
|
||||
};
|
||||
|
||||
const onReload = () => {
|
||||
onEvent.browserPageReload();
|
||||
browserViewReload();
|
||||
};
|
||||
|
||||
const onGoBackHome = () => {
|
||||
browserViewLoadURL();
|
||||
};
|
||||
|
||||
const onUrlChange = (url: string) => {
|
||||
setUrl(url);
|
||||
};
|
||||
|
||||
const handleEnter = () => {
|
||||
browserViewLoadURL(url);
|
||||
};
|
||||
|
||||
const handleClickFav = async () => {
|
||||
const isFav = await isFavFunc(url);
|
||||
if (isFav) {
|
||||
await removeFav({ title, url });
|
||||
} else {
|
||||
await insertFav({ title, url });
|
||||
}
|
||||
setIsFav((fav) => !fav);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="browser-window">
|
||||
<WindowToolBar
|
||||
onClose={() => {
|
||||
closeBrowserWindow();
|
||||
}}
|
||||
onMinimize={() => {
|
||||
minimize("");
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</WindowToolBar>
|
||||
<div className="webview-container">
|
||||
<SearchBar
|
||||
className="webview-nav"
|
||||
url={url}
|
||||
isFav={isFav}
|
||||
onUrlChange={onUrlChange}
|
||||
onGoBack={onGoBack}
|
||||
onReload={onReload}
|
||||
onGoBackHome={onGoBackHome}
|
||||
handleEnter={handleEnter}
|
||||
handleClickFav={handleClickFav}
|
||||
/>
|
||||
<div className="webview-inner">
|
||||
<div id="videoView" ref={webviewRef as any}>
|
||||
<Spin />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowserWindow;
|
@ -1,69 +0,0 @@
|
||||
@import "../../../main";
|
||||
|
||||
.download-list-container {
|
||||
.new-download-list {
|
||||
.list-item-container {
|
||||
cursor: pointer;
|
||||
|
||||
.list-item-inner {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.favorite-menu {
|
||||
.ant-dropdown-menu-title-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-drop {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
> .file-drop-target {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&.file-drop-dragging-over-frame {
|
||||
&::after {
|
||||
z-index: 10000;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&.file-drop-dragging-over-target {
|
||||
&::before {
|
||||
z-index: 10100;
|
||||
content: "拖拽到这里新建下载";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
border: 2px #00b3ff dashed;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.setting-form {
|
||||
padding: 20px;
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
@import "../../main";
|
||||
|
||||
@include window-common;
|
||||
|
||||
.main-window {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.main-window-tabs {
|
||||
flex: 1;
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
background: #4090f7;
|
||||
-webkit-app-region: drag;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
-webkit-app-region: no-drag;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
|
||||
.download-item {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
.ant-tabs-tab-btn {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
@include scrollbar;
|
||||
}
|
||||
}
|
||||
|
||||
.float-icon {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: 10px;
|
||||
height: 50px;
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #dcdfe6;
|
||||
padding: 0 12px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-drawer {
|
||||
.ant-drawer-body {
|
||||
@include scrollbar;
|
||||
}
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
import React, { FC, useEffect, useRef, useState } from "react";
|
||||
import "./index.scss";
|
||||
import { Badge, Button, message, Tabs } from "antd";
|
||||
import WindowToolBar from "../../components/WindowToolBar";
|
||||
import Setting from "../../nodes/main/elements/Setting";
|
||||
import { SourceStatus, SourceType } from "../../types";
|
||||
import {
|
||||
getVideos,
|
||||
insertVideo,
|
||||
updateVideoStatus,
|
||||
} from "../../utils/localforge";
|
||||
import audioSrc from "../../assets/tip.mp3";
|
||||
import onEvent from "../../utils/td-utils";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Settings, updateSettings } from "../../store/actions/settings.actions";
|
||||
import { AppState } from "../../store/reducers";
|
||||
import useElectron from "../../hooks/electron";
|
||||
import NewDownloadList from "../../nodes/main/elements/DownloadList";
|
||||
import { MainState, updateNotifyCount } from "../../store/actions/main.actions";
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import { helpUrl } from "../../utils/variables";
|
||||
import { IpcRendererEvent } from "electron";
|
||||
|
||||
const audio = new Audio(audioSrc);
|
||||
|
||||
enum TabKey {
|
||||
HomeTab = "1",
|
||||
SettingTab = "3",
|
||||
}
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const MainPage: FC = () => {
|
||||
const [tableData, setTableData] = useState<SourceItem[]>([]);
|
||||
const [activeKey, setActiveKey] = useState<TabKey>(TabKey.HomeTab);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector<AppState, Settings>((state) => state.settings);
|
||||
const countRef = useRef(0);
|
||||
const { notifyCount } = useSelector<AppState, MainState>(
|
||||
(state) => state.main
|
||||
);
|
||||
countRef.current = notifyCount;
|
||||
const { workspace, exeFile } = settings;
|
||||
const {
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
closeMainWindow,
|
||||
store,
|
||||
minimize,
|
||||
} = useElectron();
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
|
||||
addEventListener("m3u8-notifier", handleWebViewMessage);
|
||||
return () => {
|
||||
removeEventListener("m3u8-notifier", handleWebViewMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const initData = async () => {
|
||||
// 开始初始化表格数据
|
||||
const tableData = await getVideos();
|
||||
const initialSettings = await store.get();
|
||||
dispatch(updateSettings(initialSettings));
|
||||
setTableData(tableData);
|
||||
setActiveKey(
|
||||
initialSettings.workspace ? TabKey.HomeTab : TabKey.SettingTab
|
||||
);
|
||||
};
|
||||
|
||||
const handleWebViewMessage = async (
|
||||
e: IpcRendererEvent,
|
||||
source: SourceUrl
|
||||
): Promise<void> => {
|
||||
const item: SourceItem = {
|
||||
...source,
|
||||
exeFile,
|
||||
status: SourceStatus.Ready,
|
||||
type: SourceType.M3u8,
|
||||
directory: settings.workspace,
|
||||
createdAt: Date.now(),
|
||||
deleteSegments: true,
|
||||
};
|
||||
const sourceItem = await insertVideo(item);
|
||||
if (!sourceItem) return;
|
||||
const tableData = await getVideos();
|
||||
|
||||
setTableData(tableData);
|
||||
|
||||
dispatch(updateNotifyCount(countRef.current + 1));
|
||||
};
|
||||
|
||||
// 首页面板切换事件
|
||||
const onTabChange = (activeKey: TabKey): void => {
|
||||
if (activeKey === TabKey.HomeTab) {
|
||||
dispatch(updateNotifyCount(0));
|
||||
}
|
||||
};
|
||||
|
||||
// 打开使用帮助
|
||||
const openHelp = () => {
|
||||
onEvent.mainPageHelp();
|
||||
window.electron.openExternal(helpUrl);
|
||||
};
|
||||
|
||||
// 首页面板点击事件
|
||||
const onTabClick = async (activeKey: TabKey): Promise<void> => {
|
||||
if (!settings.workspace) {
|
||||
message.error("请选择本地路径");
|
||||
return;
|
||||
}
|
||||
if (activeKey === TabKey.SettingTab) {
|
||||
onEvent.toSettingPage();
|
||||
} else if (activeKey === TabKey.HomeTab) {
|
||||
onEvent.toMainPage();
|
||||
}
|
||||
setActiveKey(activeKey);
|
||||
};
|
||||
|
||||
// 切换视频源的 status
|
||||
const changeSourceStatus = async (
|
||||
source: SourceItem,
|
||||
status: SourceStatus
|
||||
): Promise<void> => {
|
||||
if (status === SourceStatus.Success && settings.tip) {
|
||||
await audio.play();
|
||||
}
|
||||
await updateVideoStatus(source, status);
|
||||
const tableData = await getVideos();
|
||||
|
||||
setTableData(tableData);
|
||||
};
|
||||
|
||||
// 更新表格的数据
|
||||
const updateTableData = async (): Promise<void> => {
|
||||
const videos = await getVideos();
|
||||
|
||||
setTableData(videos);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="main-window">
|
||||
<WindowToolBar
|
||||
color="#4090F7"
|
||||
onClose={() => {
|
||||
closeMainWindow();
|
||||
}}
|
||||
onMinimize={() => {
|
||||
minimize("main");
|
||||
}}
|
||||
/>
|
||||
<div className="main-window">
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
tabPosition="top"
|
||||
className="main-window-tabs"
|
||||
onChange={(value) => onTabChange(value as TabKey)}
|
||||
onTabClick={(key) => onTabClick(key as TabKey)}
|
||||
>
|
||||
<TabPane
|
||||
tab={
|
||||
<Badge className="download-item" count={notifyCount}>
|
||||
下载
|
||||
</Badge>
|
||||
}
|
||||
key={TabKey.HomeTab}
|
||||
>
|
||||
<NewDownloadList
|
||||
workspace={workspace}
|
||||
tableData={tableData}
|
||||
changeSourceStatus={changeSourceStatus}
|
||||
updateTableData={updateTableData}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="设置" key={TabKey.SettingTab}>
|
||||
<Setting />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="toolbar">
|
||||
<Button
|
||||
type={"link"}
|
||||
onClick={openHelp}
|
||||
icon={<QuestionCircleOutlined />}
|
||||
>
|
||||
使用帮助
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPage;
|
@ -1,17 +0,0 @@
|
||||
export interface MainState {
|
||||
notifyCount: number;
|
||||
}
|
||||
|
||||
export const UPDATE_NOTIFY_COUNT = "UPDATE_NOTIFY_COUNT";
|
||||
|
||||
export interface UpdateNotifyCount {
|
||||
type: typeof UPDATE_NOTIFY_COUNT;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export const updateNotifyCount = (count: number): UpdateNotifyCount => ({
|
||||
type: UPDATE_NOTIFY_COUNT,
|
||||
payload: count,
|
||||
});
|
||||
|
||||
export type MainUnionType = UpdateNotifyCount;
|
@ -1,22 +0,0 @@
|
||||
export interface Settings {
|
||||
workspace: string;
|
||||
tip: boolean;
|
||||
proxy: string;
|
||||
useProxy: boolean;
|
||||
exeFile: string;
|
||||
statistics: boolean; // 是否允许打点统计
|
||||
}
|
||||
|
||||
export const UPDATE_SETTINGS = "UPDATE_SETTINGS";
|
||||
|
||||
export interface UpdateSettings {
|
||||
type: typeof UPDATE_SETTINGS;
|
||||
payload: Partial<Settings>;
|
||||
}
|
||||
|
||||
export const updateSettings = (payload: Partial<Settings>): UpdateSettings => ({
|
||||
type: UPDATE_SETTINGS,
|
||||
payload,
|
||||
});
|
||||
|
||||
export type SettingsUnionType = UpdateSettings;
|
@ -1,14 +0,0 @@
|
||||
import { applyMiddleware, createStore } from "redux";
|
||||
import { routerMiddleware } from "connected-react-router";
|
||||
import { composeWithDevTools } from "redux-devtools-extension";
|
||||
import { createHashHistory } from "history";
|
||||
import createRootReducer from "./reducers";
|
||||
|
||||
export const history = createHashHistory();
|
||||
|
||||
const store = createStore(
|
||||
createRootReducer(history),
|
||||
composeWithDevTools(applyMiddleware(routerMiddleware(history)))
|
||||
);
|
||||
|
||||
export default store;
|
@ -1,25 +0,0 @@
|
||||
import { CombinedState, combineReducers, Reducer } from "redux";
|
||||
import { connectRouter, RouterState } from "connected-react-router";
|
||||
import { History } from "history";
|
||||
import settings from "./settings";
|
||||
import main from "./main";
|
||||
import { Settings } from "../actions/settings.actions";
|
||||
import { MainState } from "../actions/main.actions";
|
||||
|
||||
export interface AppState {
|
||||
router: RouterState;
|
||||
settings: Settings;
|
||||
main: MainState;
|
||||
}
|
||||
|
||||
const createRootReducer = (
|
||||
history: History
|
||||
): Reducer<CombinedState<AppState>> => {
|
||||
return combineReducers({
|
||||
router: connectRouter(history),
|
||||
settings,
|
||||
main,
|
||||
});
|
||||
};
|
||||
|
||||
export default createRootReducer;
|
@ -1,24 +0,0 @@
|
||||
import {
|
||||
MainState,
|
||||
MainUnionType,
|
||||
UPDATE_NOTIFY_COUNT,
|
||||
} from "../actions/main.actions";
|
||||
|
||||
const initialState: MainState = {
|
||||
notifyCount: 0,
|
||||
};
|
||||
|
||||
export default function main(
|
||||
state = initialState,
|
||||
action: MainUnionType
|
||||
): MainState {
|
||||
switch (action.type) {
|
||||
case UPDATE_NOTIFY_COUNT:
|
||||
return {
|
||||
...state,
|
||||
notifyCount: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import {
|
||||
Settings,
|
||||
SettingsUnionType,
|
||||
UPDATE_SETTINGS,
|
||||
} from "../actions/settings.actions";
|
||||
|
||||
const initialState: Settings = {
|
||||
workspace: "",
|
||||
exeFile: "",
|
||||
tip: true,
|
||||
proxy: "",
|
||||
useProxy: false,
|
||||
statistics: true,
|
||||
};
|
||||
|
||||
export default function settings(
|
||||
state = initialState,
|
||||
action: SettingsUnionType
|
||||
): Settings {
|
||||
switch (action.type) {
|
||||
case UPDATE_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export enum SourceStatus {
|
||||
Ready = "ready",
|
||||
Downloading = "downloading",
|
||||
Failed = "failed",
|
||||
Success = "success",
|
||||
}
|
||||
|
||||
export enum SourceType {
|
||||
M3u8 = "m3u8",
|
||||
M4s = "m4s",
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
const urlReg = /^(https?:\/\/(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+\.)+[a-zA-Z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/;
|
||||
export const isUrl = (url: string): boolean => urlReg.test(url);
|
@ -1,116 +0,0 @@
|
||||
import * as localforage from "localforage";
|
||||
import { SourceStatus } from "../types";
|
||||
|
||||
const keys = { videos: "videos", fav: "fav" };
|
||||
|
||||
const insertVideo = async (
|
||||
item: SourceItem
|
||||
): Promise<SourceItem | undefined> => {
|
||||
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
|
||||
// 首先查看数据库中是否存在
|
||||
if (!Array.isArray(videos)) videos = [];
|
||||
const isFav = videos.findIndex((video) => video.url === item.url) >= 0;
|
||||
if (isFav) return undefined;
|
||||
videos.unshift(item);
|
||||
await localforage.setItem(keys.videos, videos);
|
||||
return item;
|
||||
};
|
||||
|
||||
const getVideos = async (): Promise<SourceItem[]> => {
|
||||
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
|
||||
if (!Array.isArray(videos)) videos = [];
|
||||
return videos;
|
||||
};
|
||||
|
||||
const updateVideoStatus = async (
|
||||
source: SourceItem,
|
||||
status: SourceStatus
|
||||
): Promise<void> => {
|
||||
// fixme: 当数据量比较大的时候
|
||||
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
|
||||
if (!Array.isArray(videos)) videos = [];
|
||||
const findIndex = videos.findIndex((video) => source.id === video.id);
|
||||
if (findIndex >= 0) {
|
||||
videos.splice(findIndex, 1, { ...source, status });
|
||||
await localforage.setItem(keys.videos, videos);
|
||||
}
|
||||
};
|
||||
|
||||
const updateVideoTitle = async (source: SourceItem, title: string) => {
|
||||
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
|
||||
if (!Array.isArray(videos)) videos = [];
|
||||
const findIndex = videos.findIndex((video) => source.id === video.id);
|
||||
if (findIndex >= 0) {
|
||||
videos.splice(findIndex, 1, { ...source, title });
|
||||
await localforage.setItem(keys.videos, videos);
|
||||
}
|
||||
};
|
||||
|
||||
const updateVideoUrl = async (source: SourceItem, url: string) => {
|
||||
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
|
||||
if (!Array.isArray(videos)) videos = [];
|
||||
const findIndex = videos.findIndex((video) => source.id === video.id);
|
||||
if (findIndex >= 0) {
|
||||
videos.splice(findIndex, 1, { ...source, url });
|
||||
await localforage.setItem(keys.videos, videos);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeVideo = async (id: string) => {
|
||||
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
|
||||
if (!Array.isArray(videos)) videos = [];
|
||||
const favIndex = videos.findIndex((item) => item.id === id);
|
||||
if (favIndex >= 0) videos.splice(favIndex, 1);
|
||||
await localforage.setItem(keys.videos, videos);
|
||||
return videos;
|
||||
};
|
||||
|
||||
const removeVideos = async (ids: string[]): Promise<void> => {
|
||||
for (const id of ids) {
|
||||
await removeVideo(String(id));
|
||||
}
|
||||
};
|
||||
|
||||
const insertFav = async (fav: Fav): Promise<Fav[]> => {
|
||||
let favs = await localforage.getItem<Fav[]>(keys.fav);
|
||||
if (!Array.isArray(favs)) favs = [];
|
||||
const isFav = favs.findIndex((item) => item.url === fav.url) >= 0;
|
||||
if (isFav) return favs;
|
||||
favs.unshift(fav);
|
||||
await localforage.setItem(keys.fav, favs);
|
||||
return favs;
|
||||
};
|
||||
|
||||
const removeFav = async (fav: Fav): Promise<Fav[]> => {
|
||||
let favs = await localforage.getItem<Fav[]>(keys.fav);
|
||||
if (!Array.isArray(favs)) favs = [];
|
||||
const favIndex = favs.findIndex((item) => item.url === fav.url);
|
||||
if (favIndex >= 0) favs.splice(favIndex, 1);
|
||||
await localforage.setItem(keys.fav, favs);
|
||||
return favs;
|
||||
};
|
||||
|
||||
const isFavFunc = async (url: string): Promise<boolean> => {
|
||||
let favs = await localforage.getItem<Fav[]>(keys.fav);
|
||||
if (!Array.isArray(favs)) favs = [];
|
||||
return favs.findIndex((item) => item.url === url) >= 0;
|
||||
};
|
||||
|
||||
const getFavs = async (): Promise<Fav[]> => {
|
||||
const favs = await localforage.getItem<Fav[]>(keys.fav);
|
||||
if (!Array.isArray(favs)) return [];
|
||||
return favs;
|
||||
};
|
||||
|
||||
export {
|
||||
insertVideo,
|
||||
getVideos,
|
||||
updateVideoStatus,
|
||||
updateVideoTitle,
|
||||
updateVideoUrl,
|
||||
removeVideos,
|
||||
insertFav,
|
||||
isFavFunc,
|
||||
removeFav,
|
||||
getFavs,
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import tdApp from "../utils/td";
|
||||
|
||||
const onEvent = {
|
||||
browserPageGoBack: (): void => tdApp.onEvent("浏览器页面-点击返回按钮"),
|
||||
browserPageReload: (): void => tdApp.onEvent("浏览器页面-点击刷新按钮"),
|
||||
mainPageDownloadFail: (kv?: unknown): void =>
|
||||
tdApp.onEvent("下载页面-下载视频失败", kv),
|
||||
mainPageDownloadSuccess: (kv?: unknown): void =>
|
||||
tdApp.onEvent("下载页面-下载视频成功", kv),
|
||||
mainPageOpenBrowserPage: (): void => tdApp.onEvent("下载页面-打开浏览器页面"),
|
||||
mainPageNewSource: (): void => tdApp.onEvent("下载页面-新建下载"),
|
||||
mainPageHelp: (): void => tdApp.onEvent("下载页面-打开使用帮助"),
|
||||
tableStartDownload: (): void => tdApp.onEvent("资源表格-下载按钮"),
|
||||
tableReNewStatus: (): void => tdApp.onEvent("资源表格-重置状态"),
|
||||
toSettingPage: (): void => tdApp.onEvent("下载页面-点击切换设置页面"),
|
||||
toMainPage: (): void => tdApp.onEvent("下载页面-点击切换主页面"),
|
||||
favPageAddFav: (): void => tdApp.onEvent("收藏页面-添加收藏"),
|
||||
favPageOpenLink: (): void => tdApp.onEvent("收藏页面-打开链接"),
|
||||
favPageDeleteLink: (): void => tdApp.onEvent("收藏页面-删除链接"),
|
||||
addSourceDownload: (): void => tdApp.onEvent("新建下载-立即下载"),
|
||||
addSourceAddSource: (): void => tdApp.onEvent("新建下载-添加资源"),
|
||||
};
|
||||
|
||||
export default onEvent;
|
@ -1,32 +0,0 @@
|
||||
import { version } from "../../package.json";
|
||||
import store from "../store";
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
class TDEvent {
|
||||
appId?: string | boolean;
|
||||
vn: string;
|
||||
vc: string;
|
||||
|
||||
constructor() {
|
||||
this.appId = import.meta.env.VITE_APP_TDID;
|
||||
this.vn = isProd ? `${version}生产版` : `${version}开发版`;
|
||||
this.vc = `${version}`;
|
||||
}
|
||||
|
||||
init() {
|
||||
const script = document.createElement("script");
|
||||
script.src = `https://jic.talkingdata.com/app/h5/v1?appid=${this.appId}&vn=${this.vn}&vc=${this.vc}`;
|
||||
const headElement = document.getElementsByTagName("head")[0];
|
||||
headElement.appendChild(script);
|
||||
}
|
||||
|
||||
onEvent(eventId: string, mapKv: any = {}) {
|
||||
const { settings } = store.getState();
|
||||
if (settings.statistics) {
|
||||
window.TDAPP.onEvent(eventId, "", mapKv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new TDEvent();
|
@ -1,9 +0,0 @@
|
||||
const processHeaders = (headersStr: string): Record<string, string> =>
|
||||
headersStr.split("\n").reduce((prev: Record<string, string>, cur) => {
|
||||
const colonIndex = cur.indexOf(":");
|
||||
const key = cur.slice(0, colonIndex).trim();
|
||||
prev[key] = cur.slice(colonIndex + 1).trim();
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
export { processHeaders };
|
@ -1,39 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"noEmit": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"jsx": "react",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"files": [
|
||||
"./renderer.d.ts",
|
||||
"../global.d.ts"
|
||||
]
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import {defineConfig} from "vite";
|
||||
import {resolve} from "path";
|
||||
import reactRefresh from "@vitejs/plugin-react-refresh";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
server: {
|
||||
port: 7789,
|
||||
strictPort: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{find: /^types/, replacement: resolve(__dirname, "../src/types")},
|
||||
{find: /^~/, replacement: ""},
|
||||
],
|
||||
},
|
||||
envDir: "../../",
|
||||
plugins: [reactRefresh()],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
@ -1,6 +0,0 @@
|
||||
.eslintrc.js
|
||||
esbuild.config.js
|
||||
babel.config.js
|
||||
webpack.config.js
|
||||
script/
|
||||
vite.config.ts
|
@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "react"],
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"react-app",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
node_modules
|
||||
pack
|
||||
build
|
||||
.bin
|
||||
dist
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.3 KiB |
@ -1,22 +0,0 @@
|
||||
import { series, parallel, src, dest } from "gulp";
|
||||
import del from "del";
|
||||
|
||||
async function clean() {
|
||||
await del(["dist", "build", ".bin"]);
|
||||
}
|
||||
|
||||
function copyMain() {
|
||||
return src("../app-main/dist/**/*").pipe(dest("./dist/main"));
|
||||
}
|
||||
|
||||
function copyRenderer() {
|
||||
return src("../app-renderer/dist/**/*").pipe(dest("./dist/renderer"));
|
||||
}
|
||||
|
||||
function copyBin() {
|
||||
return src("../app-main/.bin/**/*").pipe(dest(".bin"));
|
||||
}
|
||||
|
||||
const source = series(clean, parallel(copyMain, copyRenderer, copyBin));
|
||||
|
||||
export { source };
|
@ -1,80 +0,0 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"description": "hello world ~",
|
||||
"main": "main/index.js",
|
||||
"scripts": {
|
||||
"pack": "gulp source && electron-builder --dir",
|
||||
"build": "gulp source && electron-builder"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"build": {
|
||||
"productName": "在线视频下载",
|
||||
"appId": "mediago.ziying.site",
|
||||
"copyright": "caorushizi",
|
||||
"artifactName": "media-downloader-setup-${version}.${ext}",
|
||||
"directories": {
|
||||
"output": "./build"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"from": "dist",
|
||||
"to": "./",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
},
|
||||
"./package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
".bin/**"
|
||||
],
|
||||
"win": {
|
||||
"icon": "assets/icon.ico",
|
||||
"requestedExecutionLevel": "requireAdministrator",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dmg": {
|
||||
"contents": []
|
||||
},
|
||||
"mac": {
|
||||
"icon": "build/icons/icon.icns"
|
||||
},
|
||||
"linux": {
|
||||
"icon": "build/icons"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowElevation": true,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "",
|
||||
"uninstallerIcon": "",
|
||||
"installerHeaderIcon": "",
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "",
|
||||
"include": "",
|
||||
"script": ""
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "^4.0.9",
|
||||
"@types/node": "^14.14.35",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"asar": "^3.1.0",
|
||||
"del": "^6.0.0",
|
||||
"electron": "16.0.4",
|
||||
"electron-builder": "^22.14.2",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-copy": "^4.0.1",
|
||||
"prettier": ">=1.13.0",
|
||||
"ts-node": "^10.7.0"
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "./src",
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
]
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
.eslintrc.js
|
||||
esbuild.config.js
|
||||
babel.config.js
|
||||
webpack.config.js
|
||||
script/
|
||||
vite.config.ts
|
||||
build
|
@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "react"],
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"react-app",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
};
|
@ -1 +0,0 @@
|
||||
build
|
@ -1,776 +0,0 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/t5eo7UJc.ts
|
||||
#EXTINF:2,
|
||||
/20220403/vYigLW9d/2000kb/hls/706YAUIb.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/cIakdl28.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/z86xtPYy.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/88YoVaqA.ts
|
||||
#EXTINF:1.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/83RyPrv4.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/1A1kqBnX.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/1I8etuwA.ts
|
||||
#EXTINF:3.533,
|
||||
/20220403/vYigLW9d/2000kb/hls/9SNKZYbG.ts
|
||||
#EXTINF:2.2,
|
||||
/20220403/vYigLW9d/2000kb/hls/PcZY4skP.ts
|
||||
#EXTINF:3.3,
|
||||
/20220403/vYigLW9d/2000kb/hls/GMpnFWSe.ts
|
||||
#EXTINF:2.033,
|
||||
/20220403/vYigLW9d/2000kb/hls/MarDDLjN.ts
|
||||
#EXTINF:4.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/uIFKckMw.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/TNJcOS5v.ts
|
||||
#EXTINF:3.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/MsQHIybb.ts
|
||||
#EXTINF:2,
|
||||
/20220403/vYigLW9d/2000kb/hls/9W72xh9e.ts
|
||||
#EXTINF:2.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/z1AdcblH.ts
|
||||
#EXTINF:4.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/c5gdXzYK.ts
|
||||
#EXTINF:1.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/mcs6Ba22.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Dwig7hZE.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/eDPIZBKy.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/MQ6iecaN.ts
|
||||
#EXTINF:2.233,
|
||||
/20220403/vYigLW9d/2000kb/hls/f4KKdTdo.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/3zE3ia0D.ts
|
||||
#EXTINF:4.833,
|
||||
/20220403/vYigLW9d/2000kb/hls/rvhv6jsi.ts
|
||||
#EXTINF:1.533,
|
||||
/20220403/vYigLW9d/2000kb/hls/sWw98TuL.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/dhasXTI9.ts
|
||||
#EXTINF:2.767,
|
||||
/20220403/vYigLW9d/2000kb/hls/48Wblrva.ts
|
||||
#EXTINF:4.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/UjIYPgYi.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Elz31cnU.ts
|
||||
#EXTINF:4.433,
|
||||
/20220403/vYigLW9d/2000kb/hls/Trkg0HM1.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/oBoElrl4.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/2l1dt2Gq.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/e9KaBuCZ.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/XP1OrEtk.ts
|
||||
#EXTINF:3.9,
|
||||
/20220403/vYigLW9d/2000kb/hls/LaPq6unu.ts
|
||||
#EXTINF:2.767,
|
||||
/20220403/vYigLW9d/2000kb/hls/EkOJwON5.ts
|
||||
#EXTINF:4.1,
|
||||
/20220403/vYigLW9d/2000kb/hls/sJXDpsjn.ts
|
||||
#EXTINF:1.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/zNss1HVO.ts
|
||||
#EXTINF:2.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/Kl383ASH.ts
|
||||
#EXTINF:3.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/LOSmsdUv.ts
|
||||
#EXTINF:4.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/WVJBjNYR.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/VatTTh74.ts
|
||||
#EXTINF:2.3,
|
||||
/20220403/vYigLW9d/2000kb/hls/wK3wT1Ad.ts
|
||||
#EXTINF:2.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/WyxC5cRs.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/vXw3Yvlw.ts
|
||||
#EXTINF:2.033,
|
||||
/20220403/vYigLW9d/2000kb/hls/j848QgUU.ts
|
||||
#EXTINF:3.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/vZuFRSC2.ts
|
||||
#EXTINF:2.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/khLlqDhl.ts
|
||||
#EXTINF:4.367,
|
||||
/20220403/vYigLW9d/2000kb/hls/yI6k6tqd.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/vLTUDGHT.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/1Qnlv9gs.ts
|
||||
#EXTINF:2.3,
|
||||
/20220403/vYigLW9d/2000kb/hls/9QftUk0b.ts
|
||||
#EXTINF:4.967,
|
||||
/20220403/vYigLW9d/2000kb/hls/nzVsWQyV.ts
|
||||
#EXTINF:1.6,
|
||||
/20220403/vYigLW9d/2000kb/hls/sGIf1Y1I.ts
|
||||
#EXTINF:3.9,
|
||||
/20220403/vYigLW9d/2000kb/hls/XdFIZr26.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/adJWu1dT.ts
|
||||
#EXTINF:1.933,
|
||||
/20220403/vYigLW9d/2000kb/hls/lv5VAZaV.ts
|
||||
#EXTINF:4.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/JRD6MhEx.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/La2zYgcl.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/vwONQNw1.ts
|
||||
#EXTINF:3.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/jOI1c4eE.ts
|
||||
#EXTINF:1.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/2UVVNmdL.ts
|
||||
#EXTINF:4.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/DXkr0vHc.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/p8HsK5r1.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/6psAHcxv.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/3WanAAgn.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/80kRDJm7.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/IsVVrQ2c.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/usHCnusH.ts
|
||||
#EXTINF:2.3,
|
||||
/20220403/vYigLW9d/2000kb/hls/8GE5mUvQ.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/e7rpEev2.ts
|
||||
#EXTINF:4.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/np8AfYjt.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/s58ONNZ7.ts
|
||||
#EXTINF:1.967,
|
||||
/20220403/vYigLW9d/2000kb/hls/E1LzBUcc.ts
|
||||
#EXTINF:3.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/7JbrmjAC.ts
|
||||
#EXTINF:2,
|
||||
/20220403/vYigLW9d/2000kb/hls/PUjhsE57.ts
|
||||
#EXTINF:3.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/5GEZl5TM.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/NVkKXuHv.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/YanyufXw.ts
|
||||
#EXTINF:2.367,
|
||||
/20220403/vYigLW9d/2000kb/hls/lqyMzd0i.ts
|
||||
#EXTINF:3.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/cEb6bEaj.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/z2otxYkV.ts
|
||||
#EXTINF:2.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/k9usPpiP.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/phuwiZUb.ts
|
||||
#EXTINF:1.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/uGlCPgmg.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/xOXtNxBM.ts
|
||||
#EXTINF:4.667,
|
||||
/20220403/vYigLW9d/2000kb/hls/zxJPoxiH.ts
|
||||
#EXTINF:1.567,
|
||||
/20220403/vYigLW9d/2000kb/hls/UFTUL5bw.ts
|
||||
#EXTINF:4.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/GP9fhyIe.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/IIk6Zjf6.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/s7KG1ykn.ts
|
||||
#EXTINF:3.567,
|
||||
/20220403/vYigLW9d/2000kb/hls/QQ0RAnzu.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/d5wMy6wS.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/uVrKf5EN.ts
|
||||
#EXTINF:4.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/kM7BtbLW.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/foO4jPuI.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/dAwe4jeq.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/ZgJd7IPo.ts
|
||||
#EXTINF:2.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/yXar6npQ.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/HKwZYm0Z.ts
|
||||
#EXTINF:3.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/6jo758Em.ts
|
||||
#EXTINF:1.533,
|
||||
/20220403/vYigLW9d/2000kb/hls/p2ynBVxJ.ts
|
||||
#EXTINF:4.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/pDDl66UB.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/JoOdQS33.ts
|
||||
#EXTINF:3.2,
|
||||
/20220403/vYigLW9d/2000kb/hls/hNoIosNP.ts
|
||||
#EXTINF:2.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/kVRiTkMM.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/mGjdMFeR.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/WxRyxs9c.ts
|
||||
#EXTINF:3.033,
|
||||
/20220403/vYigLW9d/2000kb/hls/cAxO7by8.ts
|
||||
#EXTINF:3.767,
|
||||
/20220403/vYigLW9d/2000kb/hls/BCCGIMrj.ts
|
||||
#EXTINF:3.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/qCGefmpt.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/wNGRPtQk.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/0DLbmv8H.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/mnDjL8FL.ts
|
||||
#EXTINF:4.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/q1DROtgj.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/eRdZZB3Q.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/206bn5gF.ts
|
||||
#EXTINF:4.3,
|
||||
/20220403/vYigLW9d/2000kb/hls/vJ4j9NlY.ts
|
||||
#EXTINF:1.567,
|
||||
/20220403/vYigLW9d/2000kb/hls/IrST8aJa.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/TB7EVpH4.ts
|
||||
#EXTINF:3,
|
||||
/20220403/vYigLW9d/2000kb/hls/pgSbTkRa.ts
|
||||
#EXTINF:3,
|
||||
/20220403/vYigLW9d/2000kb/hls/VUB5fnFj.ts
|
||||
#EXTINF:2.8,
|
||||
/20220403/vYigLW9d/2000kb/hls/6AUr1aug.ts
|
||||
#EXTINF:3.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/ITDpDQCm.ts
|
||||
#EXTINF:3.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/5vAVqiK6.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/hCeCEifT.ts
|
||||
#EXTINF:4.533,
|
||||
/20220403/vYigLW9d/2000kb/hls/KbXjtT7B.ts
|
||||
#EXTINF:1.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/m6eR5lKm.ts
|
||||
#EXTINF:4.367,
|
||||
/20220403/vYigLW9d/2000kb/hls/nRXGIeQf.ts
|
||||
#EXTINF:2.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/RWIDKv9z.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/2uXbwOhm.ts
|
||||
#EXTINF:3.833,
|
||||
/20220403/vYigLW9d/2000kb/hls/WEtIDeJC.ts
|
||||
#EXTINF:2.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/RjnjOOJ6.ts
|
||||
#EXTINF:3.667,
|
||||
/20220403/vYigLW9d/2000kb/hls/2rfYOB9U.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/08WxNMa8.ts
|
||||
#EXTINF:3.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/N7kYyBdX.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/ZWgswIQk.ts
|
||||
#EXTINF:4.1,
|
||||
/20220403/vYigLW9d/2000kb/hls/gCYj6e52.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/qSmuM4lg.ts
|
||||
#EXTINF:3.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/yhvvE5Bk.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/0QHYV2zk.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/n9y9RN31.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/4mz31ZeQ.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/GVZCXcqz.ts
|
||||
#EXTINF:1.233,
|
||||
/20220403/vYigLW9d/2000kb/hls/YoLD3h36.ts
|
||||
#EXTINF:3,
|
||||
/20220403/vYigLW9d/2000kb/hls/jNFKaJfU.ts
|
||||
#EXTINF:3.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/EiGpSpH1.ts
|
||||
#EXTINF:3.2,
|
||||
/20220403/vYigLW9d/2000kb/hls/C6NPhAr7.ts
|
||||
#EXTINF:2.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/JcbhSqUC.ts
|
||||
#EXTINF:3.967,
|
||||
/20220403/vYigLW9d/2000kb/hls/SfB27QCE.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/lmSGC6g4.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/TBKnedXe.ts
|
||||
#EXTINF:2.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/jQoEo9YD.ts
|
||||
#EXTINF:3.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/78bZkJHa.ts
|
||||
#EXTINF:3.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/Qfl8IKxW.ts
|
||||
#EXTINF:2.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/uiwmoB4k.ts
|
||||
#EXTINF:4.2,
|
||||
/20220403/vYigLW9d/2000kb/hls/pI3HdzcA.ts
|
||||
#EXTINF:3.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/QZE0nrzv.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/rucsUPbV.ts
|
||||
#EXTINF:2.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/fQ9zPDFJ.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/I49ZdCE2.ts
|
||||
#EXTINF:4.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/w88o7C7O.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/tPhaP4MD.ts
|
||||
#EXTINF:3.833,
|
||||
/20220403/vYigLW9d/2000kb/hls/KcL9c6AS.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/COYo22d3.ts
|
||||
#EXTINF:3,
|
||||
/20220403/vYigLW9d/2000kb/hls/De4iegyu.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/MjSMdjEL.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/HL7pfalc.ts
|
||||
#EXTINF:3.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/MjlHgW31.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/jCukIVpP.ts
|
||||
#EXTINF:3.233,
|
||||
/20220403/vYigLW9d/2000kb/hls/8fYCqQvk.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/FRXNetpO.ts
|
||||
#EXTINF:2.933,
|
||||
/20220403/vYigLW9d/2000kb/hls/poU9MWVi.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/bUVTl38I.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/pVvI9MlP.ts
|
||||
#EXTINF:1.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/POV4ldN7.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/LdTXKXnt.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/oigE7FnA.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/UUwFxEaL.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/plkrZi5A.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/dEi83VA8.ts
|
||||
#EXTINF:4.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/2ZtBZ1OT.ts
|
||||
#EXTINF:1.8,
|
||||
/20220403/vYigLW9d/2000kb/hls/3a1kQv30.ts
|
||||
#EXTINF:3.3,
|
||||
/20220403/vYigLW9d/2000kb/hls/EAjkkKMi.ts
|
||||
#EXTINF:3.367,
|
||||
/20220403/vYigLW9d/2000kb/hls/4ix5WdcO.ts
|
||||
#EXTINF:2.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/E6hteO4X.ts
|
||||
#EXTINF:3.767,
|
||||
/20220403/vYigLW9d/2000kb/hls/T98eZg0Y.ts
|
||||
#EXTINF:3.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/dVrGaseO.ts
|
||||
#EXTINF:1.8,
|
||||
/20220403/vYigLW9d/2000kb/hls/I4ro2Y7q.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Bkb3XPcT.ts
|
||||
#EXTINF:4.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/Lc9Xvz5E.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Rs0PjtYa.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/4Z6KRbur.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/IzDwiQ60.ts
|
||||
#EXTINF:2.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/T40wwc6R.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/hNRowS5S.ts
|
||||
#EXTINF:1.8,
|
||||
/20220403/vYigLW9d/2000kb/hls/zs0ydhMB.ts
|
||||
#EXTINF:4.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/FspYUTnx.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/YVD6Rcxg.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/xQsjCHsd.ts
|
||||
#EXTINF:4.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/qqbWySV6.ts
|
||||
#EXTINF:1.6,
|
||||
/20220403/vYigLW9d/2000kb/hls/V8EUVy0G.ts
|
||||
#EXTINF:2.1,
|
||||
/20220403/vYigLW9d/2000kb/hls/Wb9PjOnv.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/CoRxwdwa.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/EJFlAgRs.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/bHHktyEW.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/p8utXick.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/JUWfx8tt.ts
|
||||
#EXTINF:4.933,
|
||||
/20220403/vYigLW9d/2000kb/hls/6sDSWC0Z.ts
|
||||
#EXTINF:1.933,
|
||||
/20220403/vYigLW9d/2000kb/hls/TG5YLkkO.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/D6Clcho3.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Y7hvdg8W.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/LN4zhFhI.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/MG0PwXNW.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/DyPIHdYq.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/lHYhmKmB.ts
|
||||
#EXTINF:4.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/ngtEs8B2.ts
|
||||
#EXTINF:1.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/ZzGpxumG.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/849F3bgP.ts
|
||||
#EXTINF:4,
|
||||
/20220403/vYigLW9d/2000kb/hls/c9hYG9gp.ts
|
||||
#EXTINF:2.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/QJyyMuLB.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/8v01BmlR.ts
|
||||
#EXTINF:4.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/hTYb4V2O.ts
|
||||
#EXTINF:1.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/wpPS4hyQ.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/AnHB37yl.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/HooV3tle.ts
|
||||
#EXTINF:3.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/c1ZabaQ4.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/IAEbI36r.ts
|
||||
#EXTINF:2.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/8DCVlJ09.ts
|
||||
#EXTINF:1.8,
|
||||
/20220403/vYigLW9d/2000kb/hls/N8HMMeea.ts
|
||||
#EXTINF:3.967,
|
||||
/20220403/vYigLW9d/2000kb/hls/ujR8ZE0c.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/nlr8Z3ej.ts
|
||||
#EXTINF:4.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/cDOZl4An.ts
|
||||
#EXTINF:2.3,
|
||||
/20220403/vYigLW9d/2000kb/hls/AT2UaUb0.ts
|
||||
#EXTINF:2.1,
|
||||
/20220403/vYigLW9d/2000kb/hls/N2QXjQKF.ts
|
||||
#EXTINF:4.367,
|
||||
/20220403/vYigLW9d/2000kb/hls/Y4XQ0wlW.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/5FKB7VvR.ts
|
||||
#EXTINF:3.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/Cp7G4tnT.ts
|
||||
#EXTINF:3.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/Xpjt0KT7.ts
|
||||
#EXTINF:2.833,
|
||||
/20220403/vYigLW9d/2000kb/hls/aDrTftmH.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/H9SwidJJ.ts
|
||||
#EXTINF:4.1,
|
||||
/20220403/vYigLW9d/2000kb/hls/EpFEVBxf.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/jofCvprP.ts
|
||||
#EXTINF:1.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/USGDuXs0.ts
|
||||
#EXTINF:4.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/GS5Z5Dn4.ts
|
||||
#EXTINF:1.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/pWgRxqg7.ts
|
||||
#EXTINF:4.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/G7kPs4q2.ts
|
||||
#EXTINF:1.6,
|
||||
/20220403/vYigLW9d/2000kb/hls/QcbRQdRa.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/8C6ICMZa.ts
|
||||
#EXTINF:4.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/ntDmICwe.ts
|
||||
#EXTINF:2.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/8uyUmTSa.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/17WuZT5j.ts
|
||||
#EXTINF:5.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/azio7ymn.ts
|
||||
#EXTINF:2.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/HmKIMZ38.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/qYmRSheZ.ts
|
||||
#EXTINF:3.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/JfSqS2p1.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/dO3m0Gcb.ts
|
||||
#EXTINF:4.833,
|
||||
/20220403/vYigLW9d/2000kb/hls/aLqyInZX.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/88WlZZta.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/x12pOqep.ts
|
||||
#EXTINF:2.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/sTTXk7wt.ts
|
||||
#EXTINF:2.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/gWfuXgeh.ts
|
||||
#EXTINF:2.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/zXhcG1rm.ts
|
||||
#EXTINF:3.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/a9qx5uqU.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/KDVBrUOU.ts
|
||||
#EXTINF:2.933,
|
||||
/20220403/vYigLW9d/2000kb/hls/sFu615hg.ts
|
||||
#EXTINF:4.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/PwqBhfan.ts
|
||||
#EXTINF:2.2,
|
||||
/20220403/vYigLW9d/2000kb/hls/URqyyHoO.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/qRJXlroA.ts
|
||||
#EXTINF:4.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/SbSlhZs7.ts
|
||||
#EXTINF:1.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/sBp1Bn9Y.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/YoH7n2PH.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/UnCkyZPa.ts
|
||||
#EXTINF:2.567,
|
||||
/20220403/vYigLW9d/2000kb/hls/4cEwTOZA.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/ZU6NJ4P5.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/NwusKC8v.ts
|
||||
#EXTINF:3.8,
|
||||
/20220403/vYigLW9d/2000kb/hls/Ql1GWZbd.ts
|
||||
#EXTINF:4.033,
|
||||
/20220403/vYigLW9d/2000kb/hls/5LNsvGuV.ts
|
||||
#EXTINF:1.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/uol5nkqV.ts
|
||||
#EXTINF:4.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/toUQdDo3.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Qhq6CDit.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Qu2mp7Zt.ts
|
||||
#EXTINF:2.967,
|
||||
/20220403/vYigLW9d/2000kb/hls/TxvvehEs.ts
|
||||
#EXTINF:3.1,
|
||||
/20220403/vYigLW9d/2000kb/hls/jMTmR52L.ts
|
||||
#EXTINF:3.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/6gRVrgWj.ts
|
||||
#EXTINF:3.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/6Mx1AU1v.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/q0jFU4q7.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/8z7NXZPM.ts
|
||||
#EXTINF:2.033,
|
||||
/20220403/vYigLW9d/2000kb/hls/F8kPMK7T.ts
|
||||
#EXTINF:3.667,
|
||||
/20220403/vYigLW9d/2000kb/hls/YvltMOS5.ts
|
||||
#EXTINF:3.6,
|
||||
/20220403/vYigLW9d/2000kb/hls/Pf8PEtwV.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/9AZDXiqJ.ts
|
||||
#EXTINF:2.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/vVOBge9M.ts
|
||||
#EXTINF:2.833,
|
||||
/20220403/vYigLW9d/2000kb/hls/CDNXcqYX.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/9teEclZh.ts
|
||||
#EXTINF:3.233,
|
||||
/20220403/vYigLW9d/2000kb/hls/6wu1gO1g.ts
|
||||
#EXTINF:0.767,
|
||||
/20220403/vYigLW9d/2000kb/hls/DV7FuJUl.ts
|
||||
#EXTINF:4.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/QWAcbFDZ.ts
|
||||
#EXTINF:2.033,
|
||||
/20220403/vYigLW9d/2000kb/hls/xCpatSok.ts
|
||||
#EXTINF:4.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/Cjijx2K1.ts
|
||||
#EXTINF:2.733,
|
||||
/20220403/vYigLW9d/2000kb/hls/xefX89bl.ts
|
||||
#EXTINF:1.933,
|
||||
/20220403/vYigLW9d/2000kb/hls/qyOB6n9j.ts
|
||||
#EXTINF:4.633,
|
||||
/20220403/vYigLW9d/2000kb/hls/elGUbEV9.ts
|
||||
#EXTINF:2.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/1IctsAZd.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/4eKwnRrh.ts
|
||||
#EXTINF:2.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/DO8V12fQ.ts
|
||||
#EXTINF:3.667,
|
||||
/20220403/vYigLW9d/2000kb/hls/2bD4lO1T.ts
|
||||
#EXTINF:2.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/KUIUSkHS.ts
|
||||
#EXTINF:2.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/D2hSmagi.ts
|
||||
#EXTINF:3,
|
||||
/20220403/vYigLW9d/2000kb/hls/zvMrAEWw.ts
|
||||
#EXTINF:3.2,
|
||||
/20220403/vYigLW9d/2000kb/hls/L6ezNikF.ts
|
||||
#EXTINF:3.967,
|
||||
/20220403/vYigLW9d/2000kb/hls/h4oqgSrs.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/usiLXpUz.ts
|
||||
#EXTINF:2.933,
|
||||
/20220403/vYigLW9d/2000kb/hls/VkNDxXfT.ts
|
||||
#EXTINF:3.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/dAimbXwS.ts
|
||||
#EXTINF:2.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/mLzNkCZH.ts
|
||||
#EXTINF:5.133,
|
||||
/20220403/vYigLW9d/2000kb/hls/USW5GRpG.ts
|
||||
#EXTINF:2.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/vT5RdHa5.ts
|
||||
#EXTINF:1.6,
|
||||
/20220403/vYigLW9d/2000kb/hls/lxa7oiyZ.ts
|
||||
#EXTINF:3.033,
|
||||
/20220403/vYigLW9d/2000kb/hls/s46GuRws.ts
|
||||
#EXTINF:3.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/0t0ILxZ7.ts
|
||||
#EXTINF:3.067,
|
||||
/20220403/vYigLW9d/2000kb/hls/ES50FHWS.ts
|
||||
#EXTINF:2.667,
|
||||
/20220403/vYigLW9d/2000kb/hls/qDBd5sVG.ts
|
||||
#EXTINF:3.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/NmB1NVWk.ts
|
||||
#EXTINF:4.367,
|
||||
/20220403/vYigLW9d/2000kb/hls/pXR21QtL.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Sp8MSXV9.ts
|
||||
#EXTINF:2.267,
|
||||
/20220403/vYigLW9d/2000kb/hls/ExkUp7NY.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/VNngmCbL.ts
|
||||
#EXTINF:2.6,
|
||||
/20220403/vYigLW9d/2000kb/hls/89vtqMT9.ts
|
||||
#EXTINF:4.167,
|
||||
/20220403/vYigLW9d/2000kb/hls/LNpaEr0d.ts
|
||||
#EXTINF:3.1,
|
||||
/20220403/vYigLW9d/2000kb/hls/0AgtCtTx.ts
|
||||
#EXTINF:3.567,
|
||||
/20220403/vYigLW9d/2000kb/hls/pmAPFU8t.ts
|
||||
#EXTINF:2.467,
|
||||
/20220403/vYigLW9d/2000kb/hls/FtrAo7ah.ts
|
||||
#EXTINF:2,
|
||||
/20220403/vYigLW9d/2000kb/hls/1RSbJZ9X.ts
|
||||
#EXTINF:4.7,
|
||||
/20220403/vYigLW9d/2000kb/hls/VnPeyyBm.ts
|
||||
#EXTINF:1.567,
|
||||
/20220403/vYigLW9d/2000kb/hls/NGflc9RC.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/wEcox3yS.ts
|
||||
#EXTINF:3.667,
|
||||
/20220403/vYigLW9d/2000kb/hls/UdgU8sQf.ts
|
||||
#EXTINF:3.833,
|
||||
/20220403/vYigLW9d/2000kb/hls/9RNPIQ2P.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/5CksFmhj.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/AARAigWU.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/AIpauOyQ.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/9pCUQQpw.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/11zX4AfT.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/RMtWPll1.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/gWeYPHA9.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/Pt9ym9HZ.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/X7Zgem11.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/yi8Yv40G.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/PnzpT119.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/yGjO9Ukb.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/fYExhh2G.ts
|
||||
#EXTINF:4.333,
|
||||
/20220403/vYigLW9d/2000kb/hls/0eUZ7jcR.ts
|
||||
#EXTINF:1.867,
|
||||
/20220403/vYigLW9d/2000kb/hls/mZxCVRiB.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/2DXCtLmF.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/n8bKlOws.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/8cQhQKDi.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/LlUoqGZ8.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/p2LXnrBu.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/xNpRMaVU.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/n3QTIBfH.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/uRoQXgmn.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/ryEykUOu.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/CdVFcu3e.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/dP2sAgZM.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/C6t9Ig7a.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/BhVQpNJY.ts
|
||||
#EXTINF:2.4,
|
||||
/20220403/vYigLW9d/2000kb/hls/W6wTfyec.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/8QQmYQ1r.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/7hIpyLtI.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/LoCyT5An.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/ZV3PuHLC.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/b99PvX7a.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/MfjLbkNd.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/ZLwBINbj.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/af1E5p03.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/CxcQQyxo.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/ys6Xl87w.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/PhJ4KsxI.ts
|
||||
#EXTINF:5,
|
||||
/20220403/vYigLW9d/2000kb/hls/HV5JzWv9.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/3yHs5ME9.ts
|
||||
#EXTINF:1.9,
|
||||
/20220403/vYigLW9d/2000kb/hls/CrNtkjJf.ts
|
||||
#EXTINF:2.5,
|
||||
/20220403/vYigLW9d/2000kb/hls/kEO3FqH7.ts
|
||||
#EXTINF:4.133,
|
||||
/20220403/vYigLW9d/2000kb/hls/RnBL53av.ts
|
||||
#EXT-X-ENDLIST
|
@ -1,71 +0,0 @@
|
||||
declare module "m3u8-parser";
|
||||
|
||||
declare interface Manifest {
|
||||
allowCache: boolean;
|
||||
endList: boolean;
|
||||
mediaSequence: number;
|
||||
discontinuitySequence: number;
|
||||
playlistType: string;
|
||||
custom: unknown;
|
||||
playlists?: Playlist[];
|
||||
mediaGroups: {
|
||||
AUDIO: {
|
||||
"GROUP-ID": {
|
||||
NAME: {
|
||||
default: boolean;
|
||||
autoselect: boolean;
|
||||
language: string;
|
||||
uri: string;
|
||||
instreamId: string;
|
||||
characteristics: string;
|
||||
forced: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
VIDEO: unknown;
|
||||
"CLOSED-CAPTIONS": unknown;
|
||||
SUBTITLES: unknown;
|
||||
};
|
||||
dateTimeString: string;
|
||||
dateTimeObject: Date;
|
||||
targetDuration: number;
|
||||
totalDuration: number;
|
||||
discontinuityStarts: [number];
|
||||
segments?: Segment[];
|
||||
}
|
||||
|
||||
declare interface Playlist {
|
||||
attributes: unknown;
|
||||
uri: string;
|
||||
timeline: number;
|
||||
}
|
||||
|
||||
declare interface Segment {
|
||||
byterange: {
|
||||
length: number;
|
||||
offset: number;
|
||||
};
|
||||
duration: number;
|
||||
attributes: unknown;
|
||||
discontinuity: number;
|
||||
uri: string;
|
||||
timeline: number;
|
||||
key: {
|
||||
method: string;
|
||||
uri: string;
|
||||
iv: string;
|
||||
};
|
||||
map: {
|
||||
uri: string;
|
||||
byterange: {
|
||||
length: number;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
"cue-out": string;
|
||||
"cue-out-cont": string;
|
||||
"cue-in": string;
|
||||
custom: unknown;
|
||||
}
|
||||
|
||||
declare module "spawn-args";
|
@ -1,5 +0,0 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "mediago-node",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"server": "node script/server.js",
|
||||
"start": "tsc && node build/index.js",
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"trasform": {},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/lru-cache": "^7.6.1",
|
||||
"@types/node": "^14.14.35",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"eslint": "^7.5.0",
|
||||
"jest": "^27.5.1",
|
||||
"koa": "^2.13.4",
|
||||
"koa-static": "^5.0.0",
|
||||
"prettier": "2.2.1",
|
||||
"ts-jest": "^27.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"fs-extra": "^10.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.8.1",
|
||||
"m3u8-parser": "^4.7.0",
|
||||
"nanoid": "^3.1.30",
|
||||
"spawn-args": "^0.2.0"
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
const serve = require('koa-static');
|
||||
const Koa = require('koa');
|
||||
const app = new Koa();
|
||||
const path = require('path')
|
||||
|
||||
app.use(serve(path.resolve(__dirname, '../example')));
|
||||
|
||||
app.listen(3000);
|
||||
console.log('listening on port 3000');
|
@ -1,17 +0,0 @@
|
||||
import LRU from "lru-cache";
|
||||
|
||||
const options = {
|
||||
max: 500,
|
||||
maxSize: 5000,
|
||||
sizeCalculation: () => {
|
||||
return 1;
|
||||
},
|
||||
ttl: 1000 * 60 * 5,
|
||||
allowStale: false,
|
||||
updateAgeOnGet: false,
|
||||
updateAgeOnHas: false,
|
||||
};
|
||||
|
||||
const cache = new LRU(options);
|
||||
|
||||
export default cache;
|
@ -1,72 +0,0 @@
|
||||
import axios, { AxiosProxyConfig } from "axios";
|
||||
import { Agent } from "https";
|
||||
import { move, pathExists, createWriteStream } from "fs-extra";
|
||||
|
||||
const httpReg = /^https?:\/\//;
|
||||
const isAbsReg = /^\//;
|
||||
|
||||
export default class Downloader {
|
||||
constructor(private proxy?: AxiosProxyConfig) {}
|
||||
|
||||
static buildUrl(uri: string, baseUrl: string): string {
|
||||
let url: string;
|
||||
if (httpReg.test(uri)) {
|
||||
url = uri;
|
||||
} else {
|
||||
const m3u8 = new URL(baseUrl);
|
||||
if (isAbsReg.test(uri)) {
|
||||
m3u8.pathname = uri;
|
||||
} else {
|
||||
const pathArr = m3u8.pathname.split("/");
|
||||
pathArr.pop();
|
||||
pathArr.push(uri);
|
||||
m3u8.pathname = pathArr.join("/");
|
||||
}
|
||||
url = m3u8.toString();
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async fetch(url: string): Promise<Buffer> {
|
||||
const resp = await axios.get(url, {
|
||||
httpsAgent: new Agent({
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
proxy: this.proxy,
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async do(url: string, output: string, transforms: any[]): Promise<void> {
|
||||
const exist = await pathExists(output);
|
||||
if (exist) return;
|
||||
|
||||
const tmpFile = `${output}.tmp`;
|
||||
|
||||
const writer = createWriteStream(tmpFile);
|
||||
const resp = await axios.get(url, {
|
||||
httpsAgent: new Agent({
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
responseType: "stream",
|
||||
proxy: this.proxy,
|
||||
});
|
||||
|
||||
let pipeline = resp.data;
|
||||
transforms.forEach((t) => {
|
||||
pipeline = pipeline.pipe(t);
|
||||
});
|
||||
pipeline.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on("finish", async () => {
|
||||
await move(tmpFile, output);
|
||||
resolve();
|
||||
});
|
||||
writer.on("error", reject);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export class Task {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
status: "pending" | "retry" = "pending";
|
||||
retryCount = 0;
|
||||
lastFailedTime?: number;
|
||||
runner: () => Promise<void>;
|
||||
|
||||
constructor(runner: () => Promise<void>) {
|
||||
this.id = nanoid();
|
||||
this.timestamp = Date.now();
|
||||
this.runner = runner;
|
||||
}
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
import EventEmitter from "events";
|
||||
import { Task } from "./Task";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
interface TaskOptions {
|
||||
limit?: number;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
type RunnerStatus = "initial" | "running" | "suspended" | "terminated";
|
||||
|
||||
export class TaskRunner extends EventEmitter {
|
||||
// 下载状态
|
||||
private status: RunnerStatus = "initial";
|
||||
|
||||
// 暂停时的队列
|
||||
private staging: Task[] = [];
|
||||
|
||||
// 全部的任务列表
|
||||
private queue: Task[] = [];
|
||||
|
||||
// 当前正在处理的任务
|
||||
private active: Task[] = [];
|
||||
|
||||
// 最大处理的任务数
|
||||
private readonly limit: number;
|
||||
|
||||
private readonly debug: boolean;
|
||||
|
||||
private runTaskThrottle;
|
||||
|
||||
constructor(options?: TaskOptions) {
|
||||
super();
|
||||
|
||||
const { limit = 5, debug = false } = options || {};
|
||||
this.limit = limit;
|
||||
this.debug = debug;
|
||||
this.runTaskThrottle = throttle(this.runTask, 200);
|
||||
}
|
||||
|
||||
pauseTask(): void {
|
||||
if (this.status === "running") {
|
||||
this.staging = this.queue.slice();
|
||||
this.queue = [];
|
||||
this.status = "suspended";
|
||||
}
|
||||
}
|
||||
|
||||
resumeTask(): void {
|
||||
if (this.status === "suspended") {
|
||||
this.queue = this.staging;
|
||||
this.staging = [];
|
||||
this.runTaskThrottle();
|
||||
}
|
||||
}
|
||||
|
||||
stopTask(): void {
|
||||
if (this.status === "running") {
|
||||
this.queue = [];
|
||||
this.status = "terminated";
|
||||
}
|
||||
}
|
||||
|
||||
public addTask(task: Task, immediate?: boolean): void {
|
||||
this.queue.push(task);
|
||||
if (immediate) {
|
||||
this.status = "running";
|
||||
this.runTaskThrottle();
|
||||
}
|
||||
}
|
||||
|
||||
public run(): void {
|
||||
this.status = "running";
|
||||
this.runTaskThrottle();
|
||||
}
|
||||
|
||||
private async execute(task: Task) {
|
||||
try {
|
||||
await task.runner();
|
||||
// 任务执行成功
|
||||
this.log(
|
||||
`执行 ${task.id} 任务成功,目前队列中有 ${
|
||||
this.queue.length + this.active.length - 1
|
||||
} 条任务。`
|
||||
);
|
||||
} catch (err) {
|
||||
// 任务执行失败
|
||||
task.status = "retry";
|
||||
task.retryCount += 1;
|
||||
task.lastFailedTime = Date.now();
|
||||
if (this.status === "running") {
|
||||
this.queue.push(task);
|
||||
} else if (this.status === "suspended") {
|
||||
this.staging.push(task);
|
||||
}
|
||||
this.log(`开始执行 ${task.id} 执行失败,失败 ${task.retryCount} 次。`);
|
||||
this.log("错误信息是:", (err as any).message);
|
||||
} finally {
|
||||
// 处理当前正在活动的任务
|
||||
const doneId = this.active.findIndex((i) => i.id === task.id);
|
||||
this.active.splice(doneId, 1);
|
||||
// 处理完成的任务
|
||||
this.runTaskThrottle();
|
||||
// 传输完成
|
||||
if (this.queue.length === 0 && this.active.length === 0) {
|
||||
this.emit("done");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private runTask() {
|
||||
if (this.status === "suspended" || this.status === "terminated") {
|
||||
return;
|
||||
}
|
||||
|
||||
while (this.active.length < this.limit && this.queue.length > 0) {
|
||||
const task = this.queue.shift();
|
||||
// 如果任务队列中没有任务,进行下一次循环
|
||||
if (!task) continue;
|
||||
|
||||
if (task.status === "pending") {
|
||||
// 如果任务是 pending 状态直接执行任务
|
||||
this.active.push(task);
|
||||
this.execute(task);
|
||||
} else if (task.status === "retry" && task.lastFailedTime) {
|
||||
// 如果当前的任务是已经失败过
|
||||
// 1. 判断重试的次数是不是大于15次
|
||||
// 2. 判断当前执行的时间是否超过5s
|
||||
if (task.lastFailedTime - Date.now() / 1000 <= 5) {
|
||||
// fixme: 失败后重试
|
||||
this.queue.push(task);
|
||||
} else if (task.retryCount < 15) {
|
||||
this.active.push(task);
|
||||
this.execute(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private log(...args: unknown[]) {
|
||||
if (this.debug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
import path from "path";
|
||||
import { ensureDir, pathExists, writeFile } from "fs-extra";
|
||||
import { concatVideo, parseManifest } from "../utils";
|
||||
import { Task } from "./Task";
|
||||
import Downloader from "./Downloader";
|
||||
import { AxiosProxyConfig, AxiosRequestHeaders } from "axios";
|
||||
import { TaskRunner } from "./TaskRunner";
|
||||
import { CipherGCMTypes, createDecipheriv } from "crypto";
|
||||
import cache from "./Cache";
|
||||
|
||||
export default class Workspace {
|
||||
m3u8Path: string;
|
||||
fileList: string;
|
||||
cacheDir: string;
|
||||
videoPath: string;
|
||||
manifest?: Manifest;
|
||||
segments?: Segment[];
|
||||
|
||||
runner: TaskRunner;
|
||||
downloader: Downloader;
|
||||
|
||||
constructor(
|
||||
private m3u8Url: string,
|
||||
private baseDir: string,
|
||||
private videoName: string,
|
||||
proxy?: AxiosProxyConfig,
|
||||
headers?: AxiosRequestHeaders
|
||||
) {
|
||||
const cacheDir = path.resolve(baseDir, videoName);
|
||||
this.m3u8Path = path.resolve(cacheDir, "raw.m3u8");
|
||||
this.fileList = path.resolve(cacheDir, "fileList.txt");
|
||||
this.cacheDir = cacheDir;
|
||||
this.videoPath = path.resolve(baseDir, `${videoName}.mp4`);
|
||||
|
||||
this.runner = new TaskRunner({
|
||||
limit: 15,
|
||||
debug: true,
|
||||
});
|
||||
this.downloader = new Downloader(proxy);
|
||||
}
|
||||
|
||||
async prepare(): Promise<void> {
|
||||
if (await pathExists(this.videoPath)) {
|
||||
throw new Error("视频文件已经存在");
|
||||
}
|
||||
|
||||
await ensureDir(this.cacheDir);
|
||||
|
||||
await this.prepareSegments();
|
||||
|
||||
await this.prepareSegmentTasks();
|
||||
}
|
||||
|
||||
private async prepareSegments() {
|
||||
const data = await this.downloader.fetch(this.m3u8Url);
|
||||
this.manifest = await parseManifest(String(data));
|
||||
|
||||
const { playlists } = this.manifest || {};
|
||||
if (playlists && playlists.length > 0) {
|
||||
// todo: 选择 playlist
|
||||
const [playlist] = playlists;
|
||||
const url = Downloader.buildUrl(playlist.uri, this.m3u8Url);
|
||||
|
||||
const data = await this.downloader.fetch(url);
|
||||
this.manifest = await parseManifest(String(data));
|
||||
}
|
||||
|
||||
this.segments = this.manifest.segments;
|
||||
}
|
||||
|
||||
private async prepareSegmentTasks(): Promise<void> {
|
||||
let fileListContent = "";
|
||||
|
||||
if (!this.segments) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [index, item] of Object.entries(this.segments)) {
|
||||
const dest = path.resolve(this.cacheDir, `${index}.ts`);
|
||||
fileListContent += `file '${dest}'\n`;
|
||||
|
||||
const sign = item?.key?.uri;
|
||||
if (item.key) {
|
||||
if (!cache.has(sign)) {
|
||||
const keyUrl = Downloader.buildUrl(item.key.uri, this.m3u8Url);
|
||||
const key = await this.downloader.fetch(keyUrl);
|
||||
cache.set(sign, key);
|
||||
}
|
||||
}
|
||||
|
||||
const task = new Task(async () => {
|
||||
const url = Downloader.buildUrl(item.uri, this.m3u8Url);
|
||||
|
||||
const transforms = [];
|
||||
if (cache.has(sign)) {
|
||||
const method = `${item.key.method}-cbc`.toLowerCase() as CipherGCMTypes;
|
||||
const iv =
|
||||
item.key.iv || Buffer.from(`${index}`.padStart(32, "0"), "hex");
|
||||
const key = cache.get(sign) as string;
|
||||
const transform = createDecipheriv(method, key, iv);
|
||||
transforms.push(transform);
|
||||
}
|
||||
|
||||
await this.downloader.do(url, dest, transforms);
|
||||
});
|
||||
this.runner.addTask(task);
|
||||
}
|
||||
|
||||
await writeFile(this.fileList, fileListContent);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.runner.run();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.runner.on("done", async () => {
|
||||
await concatVideo(this.fileList, this.videoPath);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { isUrl } from "./utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import Workspace from "./core/Workspace";
|
||||
import { AxiosProxyConfig, AxiosRequestHeaders } from "axios";
|
||||
|
||||
interface DownloaderOptions {
|
||||
url: string;
|
||||
name?: string;
|
||||
path?: string;
|
||||
proxy?: AxiosProxyConfig;
|
||||
headers?: AxiosRequestHeaders;
|
||||
}
|
||||
|
||||
export async function downloader(opts: DownloaderOptions): Promise<void> {
|
||||
let { name, path: pathStr } = opts;
|
||||
const { url, proxy, headers } = opts;
|
||||
if (!name) name = nanoid(5);
|
||||
if (!pathStr) pathStr = `${__dirname}/videos`;
|
||||
|
||||
if (!isUrl(url)) {
|
||||
console.error("url 不是合法的url");
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = new Workspace(url, pathStr, name, proxy, headers);
|
||||
await workspace.prepare();
|
||||
|
||||
await workspace.run();
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { spawn } from "child_process";
|
||||
import argsBuilder from "spawn-args";
|
||||
import { Parser } from "m3u8-parser";
|
||||
|
||||
export async function spawnRunner(
|
||||
command: string,
|
||||
args: string,
|
||||
opts?: unknown
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnCommand = spawn("ffmpeg", argsBuilder(args, opts));
|
||||
|
||||
spawnCommand.stdout?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.log(`stdout: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.stderr?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.error(`stderr: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error("执行失败"));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function isUrl(urlStr: string): boolean {
|
||||
return /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?/.test(urlStr);
|
||||
}
|
||||
|
||||
export async function concatVideo(
|
||||
filelist: string,
|
||||
video: string
|
||||
): Promise<void> {
|
||||
const args = `-f concat -safe 0 -i "${filelist}" -acodec copy -vcodec copy "${video}"`;
|
||||
await spawnRunner("ffmpeg", args, { removequotes: "always" });
|
||||
}
|
||||
|
||||
export async function parseManifest(rawM3u8: string): Promise<Manifest> {
|
||||
const parser = new Parser();
|
||||
parser.push(rawM3u8);
|
||||
parser.end();
|
||||
|
||||
return parser.manifest as Manifest;
|
||||
}
|
||||
|
||||
export async function sleep(duration = 0): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, duration);
|
||||
});
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { downloader } from "../src";
|
||||
|
||||
jest.setTimeout(1000000);
|
||||
|
||||
test("downloader", async () => {
|
||||
const params = {
|
||||
url: "https://ukzy.ukubf3.com/20220409/WtaJj2Hy/index.m3u8",
|
||||
path: "C:\\Users\\caorushizi\\Desktop\\test-desktop",
|
||||
name: "斗罗大陆 1x",
|
||||
};
|
||||
|
||||
await downloader(params);
|
||||
});
|
||||
|
||||
test("downloader 1", async () => {
|
||||
const params = {
|
||||
url: "https://iqiyi.sd-play.com/20211017/vQZfIgIp/index.m3u8",
|
||||
path: "C:\\Users\\caorushizi\\Desktop\\test-desktop",
|
||||
name: "斗罗大陆 11",
|
||||
};
|
||||
|
||||
await downloader(params);
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "./src",
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"src/types"
|
||||
],
|
||||
"outDir": "./build",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "build"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"files": [
|
||||
"./index.d.ts"
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
vite.config.ts
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"standard-with-typescript",
|
||||
"prettier"
|
||||
],
|
||||
"overrides": [
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"project": [
|
||||
"./tsconfig.json"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue