Compare commits
214 Commits
v2.0.3-beta.2
...
v2.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 5461e74539 | |||
| 417484cb6d | |||
| 9aaffd3550 | |||
| 94759d48c7 | |||
| c0e343b696 | |||
| f18ac7c444 | |||
| f637610734 | |||
| 5e1046dd5b | |||
| f25584cb2f | |||
| 03999585d0 | |||
| 772c2b0300 | |||
| 3622e67a56 | |||
| c51630cdd6 | |||
| dab1c55a93 | |||
| 7b966bc988 | |||
| e5e46bee38 | |||
| 3365d96a03 | |||
| fb223cd493 | |||
| 60d49c4773 | |||
| e7aa4ae147 | |||
| c45be41ddc | |||
| 85b37facee | |||
| 90567009ef | |||
| 899afc156e | |||
| 9e33b7e488 | |||
| e7ba60a51f | |||
| 2a3ad325c2 | |||
| 7575b80d31 | |||
| 8bb9d119b5 | |||
| 5da3f2b1c7 | |||
| da6cbcfb75 | |||
| 3d7aff41ee | |||
| 2cc49e8679 | |||
| 0e961c0647 | |||
| 4d5757a6ce | |||
| 62ae0e5075 | |||
| 7513fe2882 | |||
| c945dc8360 | |||
| de7e0da045 | |||
| a706429395 | |||
| f26c9761c3 | |||
| 9424dd4c0e | |||
| 271294a8e7 | |||
| 6b2786292f | |||
| c36247b5e1 | |||
| c182afff2c | |||
| 1129d70dca | |||
| bde71d13f9 | |||
| 67b513d380 | |||
| 8d8d1ee238 | |||
| 7c10715607 | |||
| a939f06fcd | |||
| c50625b0c8 | |||
| bfa2d88935 | |||
| b04ea61e83 | |||
| 2a42a02f3d | |||
| 1e80278a1d | |||
| f38d9156c5 | |||
| 0f32864079 | |||
| f8fbecebc9 | |||
| 40c5656080 | |||
| ddf36a7091 | |||
| 258c67554d | |||
| 007ff31200 | |||
| ff75e3d94b | |||
| aaa18e0048 | |||
| 5c6f89a067 | |||
| c4efc4c293 | |||
| 2d955257e6 | |||
| 3cf84a2992 | |||
| 94a795fe69 | |||
| cc10ae8b2e | |||
| 63ed7cdaca | |||
| 064919e0a2 | |||
| 31b2652c9b | |||
| d080d709d1 | |||
| 0c301c79f2 | |||
| a1dbb6234f | |||
| 30248194af | |||
| 8f54ebeb3a | |||
| d8ef0b9eee | |||
| 6a3bfd071a | |||
| 849df04c05 | |||
| a40ae5ef82 | |||
| 00c535665f | |||
| 8b4a4d8ff8 | |||
| f9fecd541c | |||
| 93b1028498 | |||
| a0a3dba188 | |||
| 92ded00495 | |||
| bcbe258f60 | |||
| 660f9c7c9b | |||
| 8c5ef29894 | |||
| 612fc4c533 | |||
| 635377d227 | |||
| dc86ab8f4c | |||
| 36b63ad916 | |||
| e1b378811d | |||
| 37fb9bba75 | |||
| 848253c678 | |||
| b64e779ed1 | |||
| 046a62cde3 | |||
| 1a9e6efb90 | |||
| fd422c15ba | |||
| c338f76f8d | |||
| 4285b3f4f7 | |||
| d0109869a6 | |||
| d29ad44c54 | |||
| be61b07fea | |||
| e5e171fb6f | |||
| 805d97c3ca | |||
| 040222f259 | |||
| 09ed55eba4 | |||
| a6d4f8cf98 | |||
| d19b2394e8 | |||
| 036a99d84d | |||
| b022335af7 | |||
| a66c20c9d5 | |||
| f9fc135647 | |||
| fbbee9c114 | |||
| a65fbcbaf8 | |||
| 39b68b71d4 | |||
| 4d12a0a937 | |||
| 960973302e | |||
| 1a68b00bdd | |||
| dba73fc473 | |||
| 211f7314ce | |||
| 64d719a63a | |||
| 0304e6b569 | |||
| 190447a0bd | |||
| 497d1265f0 | |||
| 6d459e0692 | |||
| 45cde40cea | |||
| e9deb4c7f7 | |||
| 1abfc763dc | |||
| b84ddcc88d | |||
| 33f7951901 | |||
| d3354f9109 | |||
| d252816cc3 | |||
| 6e31904e95 | |||
| 2825d1f4c7 | |||
| fcb10c6137 | |||
| f564739578 | |||
| 40faf4b383 | |||
| affff353bd | |||
| f410fabff7 | |||
| 7d4b201b6e | |||
| a9ac0aa16b | |||
| 5419970080 | |||
| 506a573180 | |||
| 17b2450b43 | |||
| 2942e65a54 | |||
| 913a7bd9e5 | |||
| c5f2edd9de | |||
| 24d511e6b6 | |||
| 47f3336f9a | |||
| e0e6e5dbed | |||
| aa7f3e7bf7 | |||
| 065a6c45f7 | |||
| b13174dda1 | |||
| 9e6a931a41 | |||
| cdabdfd2f8 | |||
| b4cdbd7547 | |||
| 75fe603919 | |||
| 16ef9ae642 | |||
| 7db3162748 | |||
| 3f0e6d9856 | |||
| d87fc4a400 | |||
| ec6241b79e | |||
| 9b37398e9f | |||
| 3fca8694ee | |||
| 78efffa29b | |||
| 737ecbdadd | |||
| fa6ce22ea7 | |||
| 315c55c844 | |||
| d9ba60e1a5 | |||
| 7503322d01 | |||
| 2e5f2dc687 | |||
| e4afd27908 | |||
| 6fe5d5523e | |||
| 7335946ec2 | |||
| eb4576de9b | |||
| 1e41a9ee99 | |||
| dd5226ab47 | |||
| 989e5535eb | |||
| 65efeed692 | |||
| fcddb2d382 | |||
| bbc892ec81 | |||
| ca1c2821a7 | |||
| c8d4f763fd | |||
| a3d587af44 | |||
| ce2146cc89 | |||
| 8455f17517 | |||
| c075bdd4f9 | |||
| 65d88d0896 | |||
| 1c62640301 | |||
| dd6684c550 | |||
| 2e1a753583 | |||
| cbf6e4ecfb | |||
| 5af471fe1a | |||
| 0deb205faa | |||
| 73eaf0ecaa | |||
| 4791defbdd | |||
| 926249184b | |||
| 659b2755ef | |||
| e9d36edaa4 | |||
| fdcdbf27eb | |||
| 2f5cc2eb80 | |||
| c087fa4956 | |||
| ed33ec6532 | |||
| 2d96c2730c | |||
| 5bcec10be3 | |||
| 207bd15b14 | |||
| 00f0bfc84b |
@@ -0,0 +1,44 @@
|
||||
acodec
|
||||
adblocker
|
||||
ahooks
|
||||
antd
|
||||
aplus
|
||||
behaviour
|
||||
bili
|
||||
bilibili
|
||||
bodys
|
||||
cancle
|
||||
cliqz
|
||||
commitlint
|
||||
Conpty
|
||||
consola
|
||||
conventionalcommits
|
||||
datetime
|
||||
DDTHH
|
||||
execa
|
||||
idtype
|
||||
immer
|
||||
inversify
|
||||
KHTML
|
||||
languagedetector
|
||||
localforage
|
||||
mediago
|
||||
metas
|
||||
nocheck
|
||||
optimizelegibility
|
||||
pinia
|
||||
redownload
|
||||
Redownload
|
||||
reduxjs
|
||||
Sider
|
||||
stylelint
|
||||
svgz
|
||||
TDAPP
|
||||
tiptap
|
||||
typeorm
|
||||
vitepress
|
||||
vuedraggable
|
||||
vuejs
|
||||
waline
|
||||
watting
|
||||
Watting
|
||||
@@ -1,6 +1,4 @@
|
||||
APP_NAME=media-downloader
|
||||
APP_NAME=mediago
|
||||
APP_ID=mediago.ziying.site
|
||||
APP_COPYRIGHT=caorushizi
|
||||
APP_VERSION=2.0.3-beta.2
|
||||
|
||||
APP_SERVER_PORT=8433
|
||||
APP_VERSION=2.2.3
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
APP_TD_APPID=
|
||||
APP_CLARITY_APPID=
|
||||
APP_DOWNLOAD_DEBUG=true
|
||||
|
||||
GH_TOKEN=
|
||||
LOAD_DEVTOOLS=
|
||||
DEBUG_PLUGINS=true
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
APP_TD_APPID=
|
||||
APP_CLARITY_APPID=
|
||||
APP_DOWNLOAD_DEBUG=
|
||||
|
||||
GH_TOKEN=
|
||||
DEBUG_PLUGINS=false
|
||||
|
||||
+14
-14
@@ -23,41 +23,41 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest]
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
|
||||
# create steps
|
||||
steps:
|
||||
# step1: check out repository
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
# step2: install node env
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16.x"
|
||||
node-version: 20
|
||||
|
||||
# step3: install go env
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
# step4: install python env
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
go-version: "1.21.5"
|
||||
python-version: "3.10"
|
||||
|
||||
# step4: install pnpm env
|
||||
- uses: pnpm/action-setup@v2
|
||||
# step5: install pnpm env
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
standalone: true
|
||||
|
||||
- name: Build & release app
|
||||
run: pnpm run release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
|
||||
APP_CLARITY_APPID: ${{ secrets.APP_CLARITY_APPID}}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact
|
||||
path: packages/main/release/media-downloader-setup-*
|
||||
name: mediago-${{ matrix.os }}
|
||||
path: packages/main/release/mediago-setup-*
|
||||
|
||||
+13
-10
@@ -16,7 +16,6 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
BUILD_PATH: "."
|
||||
BUCKET: downloader-docs
|
||||
ENDPOINT: oss-cn-beijing.aliyuncs.com
|
||||
ACCESS_KEY: LTAI5tLckcUrBtj7bCiUYwWz
|
||||
@@ -28,22 +27,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16"
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: ${{ env.BUILD_PATH }}
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
|
||||
- name: Build with Vitepress
|
||||
run: pnpm run docs:build
|
||||
working-directory: ${{ env.BUILD_PATH }}
|
||||
|
||||
- name: Install Alibaba Cloud OSSUTIL
|
||||
run: wget http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64 && chmod +x ossutil64
|
||||
|
||||
- name: Configure Alibaba Cloud OSSUTIL
|
||||
run: ./ossutil64 config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
|
||||
|
||||
- name: Upload the web folder to the chosen OSS bucket
|
||||
run: ./ossutil64 --config-file .ossutilconfig cp ${{ github.workspace }}/docs/.vitepress/dist oss://${BUCKET} -r -f
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
node_modules
|
||||
.vscode
|
||||
*.local
|
||||
.idea
|
||||
.parcel-cache
|
||||
.vitepress
|
||||
*.tsno.mjs
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "internal"]
|
||||
path = internal
|
||||
url = https://github.com/caorushizi/mediago
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx --no -- commitlint --edit ${1}
|
||||
npm run commitlint ${1}
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx tsx ./scripts/pre-commit.ts
|
||||
@@ -0,0 +1,3 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run-script", "dev"],
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
},
|
||||
{
|
||||
"name": "Attach to Electron",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"address": "localhost",
|
||||
"restart": true,
|
||||
"timeout": 10000,
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
<img src="https://socialify.git.ci/caorushizi/m3u8-downloader/image?font=Inter&forks=1&issues=1&language=1&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Auto" alt="MediaDownloader"/>
|
||||
<img src="https://socialify.git.ci/caorushizi/mediago/image?font=Inter&forks=1&issues=1&language=1&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Auto" alt="MediaDownloader"/>
|
||||
|
||||
<div align="center">
|
||||
<h1>Media downloader</h1>
|
||||
<h1>MediaGo</h1>
|
||||
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/m3u8-downloader/discussions">Discussions</a>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/m3u8-downloader/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/m3u8-downloader/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/m3u8-downloader">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/m3u8-downloader">
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
@@ -25,18 +25,18 @@
|
||||
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
|
||||
|
||||
- **✅ 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
|
||||
- **📱 移动播放**: 可以轻松无缝的在PC和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- **📱 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- **⚡️ 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
|
||||
|
||||
## Quickstart
|
||||
|
||||
运行代码需要 node 、 pnpm 和 go,node、go 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
|
||||
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
|
||||
|
||||
## 运行代码
|
||||
|
||||
```shell
|
||||
# 代码下载
|
||||
git clone --recursive https://github.com/caorushizi/m3u8-downloader.git
|
||||
git clone https://github.com/caorushizi/mediago.git
|
||||
|
||||
# 安装依赖
|
||||
pnpm i
|
||||
@@ -50,15 +50,23 @@ pnpm release
|
||||
|
||||
## Releases
|
||||
|
||||
**v2.0.1(2023.7.9 发布)**
|
||||
**v2.2.0 (2024.5.22 发布)**
|
||||
|
||||
- [windows media-downloader v2.0.2](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.2/media-downloader-setup-2.0.2.exe)
|
||||
- [macos media-downloader v2.0.2](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.2/media-downloader-setup-2.0.2.dmg)
|
||||
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
|
||||
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
|
||||
|
||||
**v2.0.1(2023.7.1 发布)**
|
||||
**更新日志**
|
||||
|
||||
- [windows media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
- 支持下载直播流
|
||||
- 支持哔哩哔哩视频下载
|
||||
- 优化沉浸式嗅探流程
|
||||
- 支持下载控制台输出
|
||||
- 修复了一些 bug
|
||||
|
||||
**v2.0.2(2023.7.9 发布)**
|
||||
|
||||
- [windows mediago v2.0.2](https://github.com/caorushizi/mediago/releases/download/v2.0.2/media-downloader-setup-2.0.2.exe)
|
||||
- [macos mediago v2.0.2](https://github.com/caorushizi/mediago/releases/download/v2.0.2/media-downloader-setup-2.0.2.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
@@ -71,12 +79,6 @@ pnpm release
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
**更新日志**
|
||||
|
||||
## 软件截图
|
||||
|
||||

|
||||
@@ -93,6 +95,4 @@ pnpm release
|
||||
|
||||
## 鸣谢
|
||||
|
||||
N_m3u8DL-CLI 来自于 <https://github.com/nilaoda/N_m3u8DL-CLI>
|
||||
|
||||
mediago 来自于 <https://github.com/caorushizi/mediago>
|
||||
N_m3u8DL-RE 来自于 <https://github.com/nilaoda/N_m3u8DL-RE>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
export default {
|
||||
extends: ["@commitlint/config-conventional"], // extends can be nested
|
||||
parserPreset: "conventional-changelog-conventionalcommits",
|
||||
prompt: {
|
||||
settings: {},
|
||||
messages: {
|
||||
skip: ":skip",
|
||||
max: "upper %d chars",
|
||||
min: "%d chars at least",
|
||||
emptyWarning: "can not be empty",
|
||||
upperLimitWarning: "over limit",
|
||||
lowerLimitWarning: "below limit",
|
||||
},
|
||||
types: [
|
||||
{ value: "feat", name: "feat: ✨ A new feature", emoji: "✨ " },
|
||||
{ value: "fix", name: "fix: 🐛 A bug fix", emoji: "🐛 " },
|
||||
{
|
||||
value: "docs",
|
||||
name: "docs: 📝 Documentation only changes",
|
||||
emoji: "📝 ",
|
||||
},
|
||||
{
|
||||
value: "style",
|
||||
name: "style: 💄 Changes that do not affect the meaning of the code",
|
||||
emoji: "💄 ",
|
||||
},
|
||||
{
|
||||
value: "refactor",
|
||||
name: "refactor: 📦️ A code change that neither fixes a bug nor adds a feature",
|
||||
emoji: "📦️ ",
|
||||
},
|
||||
{
|
||||
value: "perf",
|
||||
name: "perf: 🚀 A code change that improves performance",
|
||||
emoji: "🚀 ",
|
||||
},
|
||||
{
|
||||
value: "test",
|
||||
name: "test: 🚨 Adding missing tests or correcting existing tests",
|
||||
emoji: "🚨 ",
|
||||
},
|
||||
{
|
||||
value: "build",
|
||||
name: "build: 🛠 Changes that affect the build system or external dependencies",
|
||||
emoji: "🛠 ",
|
||||
},
|
||||
{
|
||||
value: "ci",
|
||||
name: "ci: 🎡 Changes to our CI configuration files and scripts",
|
||||
emoji: "🎡 ",
|
||||
},
|
||||
{
|
||||
value: "chore",
|
||||
name: "chore: 🔨 Other changes that don't modify src or test files",
|
||||
emoji: "🔨 ",
|
||||
},
|
||||
{
|
||||
value: "revert",
|
||||
name: "revert: ⏪️ Reverts a previous commit",
|
||||
emoji: ":rewind:",
|
||||
},
|
||||
],
|
||||
useEmoji: true,
|
||||
confirmColorize: true,
|
||||
emojiAlign: "center",
|
||||
questions: {
|
||||
scope: {
|
||||
description:
|
||||
"What is the scope of this change (e.g. component or file name)",
|
||||
},
|
||||
subject: {
|
||||
description:
|
||||
"Write a short, imperative tense description of the change",
|
||||
},
|
||||
body: {
|
||||
description: "Provide a longer description of the change",
|
||||
},
|
||||
isBreaking: {
|
||||
description: "Are there any breaking changes?",
|
||||
},
|
||||
breakingBody: {
|
||||
description:
|
||||
"A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself",
|
||||
},
|
||||
breaking: {
|
||||
description: "Describe the breaking changes",
|
||||
},
|
||||
isIssueAffected: {
|
||||
description: "Does this change affect any open issues?",
|
||||
},
|
||||
issuesBody: {
|
||||
description:
|
||||
"If issues are closed, the commit requires a body. Please enter a longer description of the commit itself",
|
||||
},
|
||||
issues: {
|
||||
description: 'Add issue references (e.g. "fix #123", "re #123".)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"import": [
|
||||
"@cspell/dict-lorem-ipsum/cspell-ext.json"
|
||||
],
|
||||
"caseSensitive": false,
|
||||
"dictionaries": [
|
||||
"custom-words"
|
||||
],
|
||||
"dictionaryDefinitions": [
|
||||
{
|
||||
"name": "custom-words",
|
||||
"path": "./.cspell/custom-words.txt",
|
||||
"addWords": true
|
||||
}
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/lib/**",
|
||||
"**/docs/**",
|
||||
"**/stats.html"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { HeadConfig } from "vitepress";
|
||||
import { defineConfig } from "vitepress";
|
||||
import { HeadConfig, defineConfig } from "vitepress";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
@@ -22,7 +21,7 @@ if (!isDev) {
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "media-downloader",
|
||||
title: "MediaGo",
|
||||
description: "简单易用,快速下载",
|
||||
lastUpdated: true,
|
||||
head,
|
||||
@@ -57,7 +56,7 @@ export default defineConfig({
|
||||
|
||||
footer: {
|
||||
message: '<a href="https://beian.miit.gov.cn">豫ICP备20012967号-2</a>',
|
||||
copyright: "Copyright © 2019-present caorushizi",
|
||||
copyright: "Copyright © 2021-present caorushizi",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,7 +29,6 @@ function initWaline() {
|
||||
serverURL: "https://comments.ziying.site",
|
||||
dark: isDark.value,
|
||||
lang: "zh-CN",
|
||||
reaction: true,
|
||||
pageview: true,
|
||||
});
|
||||
}
|
||||
|
||||
+21
-4
@@ -8,15 +8,32 @@ outline: deep
|
||||
这篇文章是软件的简单说明,可以快速上手使用本本软件。
|
||||
|
||||
::: tip
|
||||
我建了一个 QQ 群,方便大家一起沟通交流: 574209001
|
||||
方便大家一起沟通交流, 可以加入 QQ 反馈群:
|
||||
|
||||
MediaGo 反馈群 1: 574209001
|
||||
|
||||
MediaGo 反馈群 2: 921619626
|
||||
:::
|
||||
|
||||
## 下载安装
|
||||
|
||||
**v2.2.0 (2024.5.22 发布)**
|
||||
|
||||
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
|
||||
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持下载直播流
|
||||
- 支持哔哩哔哩视频下载
|
||||
- 优化沉浸式嗅探流程
|
||||
- 支持下载控制台输出
|
||||
- 修复了一些 bug
|
||||
|
||||
**v2.0.1(2023.7.1 发布)**
|
||||
|
||||
- [windows media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
@@ -31,7 +48,7 @@ outline: deep
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
**更新日志**
|
||||
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ outline: deep
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos media-downloader v1.1.5 无~~
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos mediago v1.1.5 无~~
|
||||
|
||||
**更新日志**
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
-1
Submodule internal deleted from ca43a37335
+42
-17
@@ -1,36 +1,61 @@
|
||||
{
|
||||
"name": "electron-template",
|
||||
"name": "mediago",
|
||||
"version": "0.1.0",
|
||||
"description": "electron-template",
|
||||
"main": "index.js",
|
||||
"description": "mediago",
|
||||
"author": "caorushizi",
|
||||
"homepage": "https://downloader.caorushizi.cn/",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "zx scripts/dev.mjs && pnpm --parallel -F \"./packages/*\" run dev",
|
||||
"build": "zx scripts/build.mjs",
|
||||
"build:mobile": "pnpm -F mobile run build",
|
||||
"dev": "tsx scripts/dev.ts && pnpm --parallel -F \"./packages/*\" run dev",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"build:plugin": "pnpm -F plugin run build",
|
||||
"build:main": "pnpm -F main run build",
|
||||
"build:renderer": "pnpm -F renderer run build",
|
||||
"build:server": "zx scripts/build-server.mjs",
|
||||
"pack": "pnpm -F media-downloader run pack",
|
||||
"beta": "pnpm run build && pnpm -F mediago run pack",
|
||||
"release": "pnpm run build && pnpm -F main run release",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"types": "pnpm -F main run types"
|
||||
"types": "pnpm -F main run types",
|
||||
"types:watch": "pnpm -F main run types:watch",
|
||||
"prepare": "husky",
|
||||
"commit": "git-cz",
|
||||
"commitlint": "commitlint --edit",
|
||||
"spellcheck": "cspell \"packages/**/src/**/*.{ts,tsx}\"",
|
||||
"lint-staged": "pnpm --recursive -F \"./packages/*\" run lint-staged"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@cliqz/adblocker@1.26.15": "patches/@cliqz__adblocker@1.26.15.patch"
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-git"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@waline/client": "3.0.0-alpha.10",
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@commitlint/core": "^19.3.0",
|
||||
"@commitlint/cz-commitlint": "^19.2.0",
|
||||
"@commitlint/format": "^19.3.0",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@waline/client": "^3.2.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"vitepress": "1.0.0-rc.39",
|
||||
"vue": "^3.4.15",
|
||||
"zx": "^7.2.3"
|
||||
"cspell": "^8.8.3",
|
||||
"cz-git": "^1.9.2",
|
||||
"eslint": "^9.3.0",
|
||||
"globals": "^15.3.0",
|
||||
"husky": "^9.0.11",
|
||||
"tsx": "^4.11.0",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"vitepress": "1.2.2",
|
||||
"vue": "^3.4.27",
|
||||
"zx": "^8.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@cliqz/adblocker@1.27.3": "patches/@cliqz__adblocker@1.27.3.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
overrides: [],
|
||||
ignorePatterns: ["app"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint", "prettier"],
|
||||
rules: {
|
||||
"prettier/prettier": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
types/
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "media-downloader",
|
||||
"version": "2.0.3-beta.2",
|
||||
"name": "mediago",
|
||||
"version": "2.2.3",
|
||||
"description": "在线视频下载器",
|
||||
"main": "main/index.js",
|
||||
"author": "",
|
||||
"author": "caorushizi",
|
||||
"homepage": "https://downloader.caorushizi.cn/",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.26.15",
|
||||
"@cliqz/adblocker-electron-preload": "^1.26.15"
|
||||
"@cliqz/adblocker-electron": "^1.27.3",
|
||||
"@cliqz/adblocker-electron-preload": "^1.27.3",
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^28.1.4"
|
||||
"electron": "30.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,16 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: { globals: globals.node },
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export { dev, build, release } from "./scripts";
|
||||
+55
-44
@@ -2,65 +2,76 @@
|
||||
"name": "main",
|
||||
"version": "0.1.0",
|
||||
"description": "在线视频下载器",
|
||||
"type": "module",
|
||||
"main": "main/index.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-rebuild -f -w better-sqlite3",
|
||||
"dev": "cross-env NODE_ENV=development VITE_CJS_IGNORE_WARNING=true ts-node scripts/dev.ts",
|
||||
"build": "cross-env NODE_ENV=production ts-node scripts/build.ts",
|
||||
"pack": "cross-env NODE_ENV=development ts-node scripts/release.ts",
|
||||
"types": "tsc --emitDeclarationOnly",
|
||||
"release": "cross-env NODE_ENV=production ts-node scripts/release.ts"
|
||||
"postinstall": "electron-rebuild -f -w",
|
||||
"dev": "cross-env NODE_ENV=development gulp dev",
|
||||
"pack": "cross-env NODE_ENV=development gulp release",
|
||||
"build": "cross-env NODE_ENV=production gulp build",
|
||||
"release": "cross-env NODE_ENV=production gulp release",
|
||||
"types": "tsc",
|
||||
"types:watch": "tsc -w",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": "eslint"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.5.0",
|
||||
"@rushstack/eslint-patch": "^1.7.0",
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@types/electron-devtools-installer": "^2.2.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "^20.11.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/gulp": "^4.0.17",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
"consola": "^3.2.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.3.2",
|
||||
"electron": "^28.1.4",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"esbuild": "^0.19.11",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-n": "16.6.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"prebuild-install": "^7.1.1",
|
||||
"prettier": "3.2.4",
|
||||
"semver": "^7.5.4",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
"del": "^7.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "30.0.8",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-updater": "^6.1.8",
|
||||
"esbuild": "^0.21.4",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^9.3.0",
|
||||
"glob": "^10.4.1",
|
||||
"globals": "^15.3.0",
|
||||
"gulp": "^5.0.0",
|
||||
"prebuild-install": "^7.1.2",
|
||||
"semver": "^7.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.26.15",
|
||||
"@cliqz/adblocker-electron-preload": "^1.26.15",
|
||||
"@types/semver": "^7.5.6",
|
||||
"axios": "^1.6.5",
|
||||
"better-sqlite3": "^9.3.0",
|
||||
"@cliqz/adblocker-electron": "1.27.3",
|
||||
"@cliqz/adblocker-electron-preload": "1.27.3",
|
||||
"axios": "^1.7.2",
|
||||
"better-sqlite3": "^10.0.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dayjs": "^1.11.11",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-is-dev": "^3.0.1",
|
||||
"electron-log": "^5.0.3",
|
||||
"electron-store": "^8.1.0",
|
||||
"execa": "^8.0.1",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^9.0.0",
|
||||
"execa": "^9.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"inversify": "^6.0.2",
|
||||
"lint-staged": "^15.2.5",
|
||||
"lodash": "^4.17.21",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typeorm": "^0.3.19"
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"node-pty": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"typeorm": "0.3.20"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "plugin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^5.13.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"electron": "^28.1.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#MediagoRoot {
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
.app {
|
||||
z-index: 99999;
|
||||
.app-float-button {
|
||||
left: 30px;
|
||||
bottom: 30px;
|
||||
}
|
||||
.app-logo {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 3px;
|
||||
transform: scale(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FloatButton, Button, Dropdown, Space } from "antd";
|
||||
import "./App.scss";
|
||||
import logo from "./assets/logo.png";
|
||||
import { WebSource } from "./types";
|
||||
import { ipcRenderer } from "electron/renderer";
|
||||
|
||||
function App() {
|
||||
const [items, setItems] = useState<WebSource[]>([]);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const receiveMessage = (_: unknown, data: WebSource) => {
|
||||
setItems((item) => [...item, data]);
|
||||
setCount((c) => c + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer.on("webview-link-message", receiveMessage);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener("webview-link-message", receiveMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Dropdown
|
||||
placement="topLeft"
|
||||
menu={{
|
||||
items: items.map((item) => ({
|
||||
key: item.name,
|
||||
label: (
|
||||
<Space
|
||||
onClick={() => {
|
||||
ipcRenderer.invoke("add-download-item", {
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
type: item.type,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div>{item.name}</div>
|
||||
<Button type="link" style={{ padding: 0 }}>
|
||||
添加
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
})),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<FloatButton
|
||||
className="app-float-button"
|
||||
icon={<img className="app-logo" src={logo} alt="" />}
|
||||
shape="square"
|
||||
badge={{
|
||||
count,
|
||||
offset: [10, 0],
|
||||
}}
|
||||
onClick={() => {
|
||||
setCount(0);
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.scss";
|
||||
|
||||
export function mount() {
|
||||
const root = document.createElement("div");
|
||||
root.id = "MediagoRoot";
|
||||
document.body.appendChild(root);
|
||||
ReactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
mount();
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
export enum DownloadType {
|
||||
m3u8 = "m3u8",
|
||||
bilibili = "bilibili",
|
||||
}
|
||||
|
||||
export interface WebSource {
|
||||
url: string;
|
||||
type: DownloadType;
|
||||
name: string;
|
||||
headers?: string;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "./src/main.tsx"),
|
||||
name: "plugin",
|
||||
fileName: () => "index.js",
|
||||
formats: ["cjs"],
|
||||
},
|
||||
outDir: path.resolve(__dirname, "../../main/app/plugin"),
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
external: ["electron/renderer"],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
origin: "http://localhost:5173",
|
||||
},
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import {
|
||||
mainResolve,
|
||||
loadDotEnvDefined,
|
||||
copyResource,
|
||||
removeResource,
|
||||
} from "./utils";
|
||||
import { external } from "./config";
|
||||
|
||||
const mainDefined = loadDotEnvDefined();
|
||||
|
||||
removeResource([mainResolve("app/build")]);
|
||||
removeResource([mainResolve("app/bin")]);
|
||||
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
{
|
||||
from: mainResolve("bin"),
|
||||
to: mainResolve("app/bin"),
|
||||
},
|
||||
]);
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: [mainResolve("src/index.ts")],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: false,
|
||||
target: ["node16.13"],
|
||||
external,
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
...mainDefined,
|
||||
},
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: true,
|
||||
});
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: [mainResolve("src/preload.ts")],
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
target: ["chrome89"],
|
||||
sourcemap: false,
|
||||
external,
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
...mainDefined,
|
||||
},
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: true,
|
||||
});
|
||||
@@ -1,7 +1,138 @@
|
||||
export const external = [
|
||||
import { Configuration } from "electron-builder";
|
||||
import { Env, isDev, mainResolve } from "./utils";
|
||||
import esbuild from "esbuild";
|
||||
|
||||
const external = [
|
||||
"electron",
|
||||
"nock",
|
||||
"aws-sdk",
|
||||
"mock-aws-s3",
|
||||
"@cliqz/adblocker-electron-preload",
|
||||
"node-pty",
|
||||
];
|
||||
|
||||
function getConfig(): esbuild.BuildOptions {
|
||||
const getDefine = (): Record<string, string> => {
|
||||
if (isDev) {
|
||||
return {
|
||||
__bin__: `"${mainResolve("app/bin").replace(/\\/g, "\\\\")}"`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...Env.getInstance().loadDotEnvDefined(),
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
bundle: true,
|
||||
sourcemap: process.env.NODE_ENV === "development",
|
||||
external,
|
||||
define: getDefine(),
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: process.env.NODE_ENV === "production",
|
||||
};
|
||||
}
|
||||
|
||||
function buildOptions(
|
||||
entry: string,
|
||||
platform: esbuild.Platform,
|
||||
target: string
|
||||
): esbuild.BuildOptions {
|
||||
return {
|
||||
...getConfig(),
|
||||
entryPoints: [mainResolve(entry)],
|
||||
platform: platform,
|
||||
target: [target],
|
||||
};
|
||||
}
|
||||
|
||||
export function browserOptions(entry: string): esbuild.BuildOptions {
|
||||
return buildOptions(entry, "browser", "chrome89");
|
||||
}
|
||||
|
||||
export function nodeOptions(entry: string): esbuild.BuildOptions {
|
||||
return buildOptions(entry, "node", "node16.13");
|
||||
}
|
||||
|
||||
export function getReleaseConfig(): Configuration {
|
||||
return {
|
||||
productName: process.env.APP_NAME,
|
||||
buildVersion: process.env.APP_VERSION,
|
||||
appId: process.env.APP_ID,
|
||||
copyright: process.env.APP_COPYRIGHT,
|
||||
artifactName: "${productName}-setup-${arch}-${buildVersion}.${ext}",
|
||||
npmRebuild: true,
|
||||
directories: {
|
||||
output: "./release",
|
||||
},
|
||||
files: [
|
||||
{
|
||||
from: "./build",
|
||||
to: "./",
|
||||
},
|
||||
"./package.json",
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "./app/plugin",
|
||||
to: "plugin",
|
||||
},
|
||||
{
|
||||
from: "./app/bin/",
|
||||
to: "bin",
|
||||
},
|
||||
],
|
||||
win: {
|
||||
icon: "../assets/icon.ico",
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 410,
|
||||
y: 150,
|
||||
type: "link",
|
||||
path: "/Applications",
|
||||
},
|
||||
{
|
||||
x: 130,
|
||||
y: 150,
|
||||
type: "file",
|
||||
},
|
||||
],
|
||||
},
|
||||
mac: {
|
||||
icon: "../assets/icon.icns",
|
||||
target: [
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
linux: {
|
||||
category: "Utility",
|
||||
icon: "../assets/icon.icns",
|
||||
maintainer: "caorushizi <84996057@qq.com>",
|
||||
target: {
|
||||
target: "deb",
|
||||
arch: ["x64"],
|
||||
},
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowElevation: true,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||
import electron from "electron";
|
||||
import * as esbuild from "esbuild";
|
||||
import chokidar from "chokidar";
|
||||
import { loadDotEnvRuntime, mainResolve, log, copyResource } from "./utils";
|
||||
import { external } from "./config";
|
||||
import { build } from "vite";
|
||||
import path from "path";
|
||||
|
||||
let electronProcess: ChildProcessWithoutNullStreams | null = null;
|
||||
|
||||
process.env.NODE_ENV = "development";
|
||||
loadDotEnvRuntime();
|
||||
|
||||
async function copySource() {
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
{
|
||||
from: mainResolve("bin"),
|
||||
to: mainResolve("app/bin"),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const buildConfig: esbuild.BuildOptions = {
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: true,
|
||||
target: ["node20.9"],
|
||||
external,
|
||||
define: {
|
||||
// 开发环境中二进制可执行文件的路径
|
||||
__bin__: `"${mainResolve("bin", process.platform).replace(/\\/g, "\\\\")}"`,
|
||||
},
|
||||
plugins: [],
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
};
|
||||
|
||||
function startElectron() {
|
||||
const args = ["--inspect=5858", mainResolve("app/build/main/index.js")];
|
||||
|
||||
electronProcess = spawn(String(electron), args);
|
||||
|
||||
electronProcess.stdout.on("data", (data) => {
|
||||
log(String(data));
|
||||
});
|
||||
|
||||
electronProcess.stderr.on("data", (data) => {
|
||||
log(String(data));
|
||||
});
|
||||
}
|
||||
|
||||
function restartElectron() {
|
||||
if (electronProcess && electronProcess.pid) {
|
||||
if (process.platform === "darwin") {
|
||||
spawn("kill", ["-9", String(electronProcess.pid)]);
|
||||
} else {
|
||||
process.kill(electronProcess.pid);
|
||||
}
|
||||
electronProcess = null;
|
||||
startElectron();
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const mainContext = await esbuild.context({
|
||||
...buildConfig,
|
||||
entryPoints: [mainResolve("src/index.ts")],
|
||||
});
|
||||
|
||||
const preloadContext = await esbuild.context({
|
||||
...buildConfig,
|
||||
entryPoints: [mainResolve("src/preload.ts")],
|
||||
platform: "browser",
|
||||
});
|
||||
|
||||
const watcher = chokidar.watch("./src");
|
||||
watcher.on("change", async () => {
|
||||
await mainContext.rebuild();
|
||||
await preloadContext.rebuild();
|
||||
log("watch build succeed.");
|
||||
restartElectron();
|
||||
});
|
||||
|
||||
const pluginRoot = path.resolve(__dirname, "../plugin");
|
||||
const watcher2 = chokidar.watch(pluginRoot);
|
||||
watcher2.on("change", async () => {
|
||||
console.log("plugin changed");
|
||||
await build({ root: pluginRoot });
|
||||
restartElectron();
|
||||
});
|
||||
|
||||
try {
|
||||
await mainContext.rebuild();
|
||||
await preloadContext.rebuild();
|
||||
await copySource();
|
||||
await startElectron();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,98 @@
|
||||
import { deleteSync } from "del";
|
||||
import { ElectronApp, mainResolve, Env, isWin } from "./utils";
|
||||
import gulp from "gulp";
|
||||
import * as esbuild from "esbuild";
|
||||
import consola from "consola";
|
||||
import { browserOptions, nodeOptions, getReleaseConfig } from "./config";
|
||||
import semver from "semver";
|
||||
import pkg from "../app/package.json";
|
||||
import * as builder from "electron-builder";
|
||||
import { globSync } from "glob";
|
||||
import fs from "fs";
|
||||
|
||||
const env = Env.getInstance();
|
||||
env.loadDotEnvRuntime();
|
||||
|
||||
async function clean() {
|
||||
return deleteSync([
|
||||
mainResolve("app/build"),
|
||||
mainResolve("app/bin"),
|
||||
mainResolve("release"),
|
||||
]);
|
||||
}
|
||||
|
||||
async function copySqlite() {
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
const source = mainResolve("node_modules/better-sqlite3", path);
|
||||
const target = mainResolve("app", path);
|
||||
fs.cpSync(source, target, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyBin() {
|
||||
const source = mainResolve("bin", process.platform);
|
||||
const target = mainResolve("app/bin");
|
||||
fs.cpSync(source, target, { recursive: true });
|
||||
}
|
||||
|
||||
async function chmodBin() {
|
||||
// 遍历文件夹下的所有文件,将文件的权限设置为 777
|
||||
if (isWin) return;
|
||||
|
||||
const files = globSync(mainResolve("app/bin/*"));
|
||||
for (const file of files) {
|
||||
fs.chmodSync(file, 0o777);
|
||||
}
|
||||
}
|
||||
|
||||
const copy = gulp.parallel(copyBin, copySqlite);
|
||||
|
||||
async function watchTask() {
|
||||
const app = new ElectronApp();
|
||||
const main = await esbuild.context(nodeOptions("src/index.ts"));
|
||||
const preload = await esbuild.context(browserOptions("src/preload.ts"));
|
||||
|
||||
const watcher = gulp.watch(["./src"]);
|
||||
watcher
|
||||
.on("ready", async () => {
|
||||
await main.rebuild();
|
||||
await preload.rebuild();
|
||||
app.start();
|
||||
})
|
||||
.on("change", async () => {
|
||||
await main.rebuild();
|
||||
await preload.rebuild();
|
||||
app.restart();
|
||||
})
|
||||
.on("error", (error) => {
|
||||
consola.error(error);
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function buildTask() {
|
||||
await esbuild.build(nodeOptions("src/index.ts"));
|
||||
await esbuild.build(browserOptions("src/preload.ts"));
|
||||
}
|
||||
|
||||
async function pack() {
|
||||
if (semver.neq(process.env.APP_VERSION || "", pkg.version)) {
|
||||
throw new Error("请先同步构建版本和发布版本");
|
||||
}
|
||||
const config = getReleaseConfig();
|
||||
if (process.env.GH_TOKEN) {
|
||||
config.publish = {
|
||||
provider: "github",
|
||||
repo: "mediago",
|
||||
owner: "caorushizi",
|
||||
releaseType: "draft",
|
||||
};
|
||||
}
|
||||
await builder.build({ config });
|
||||
}
|
||||
|
||||
// 开发环境
|
||||
export const dev = gulp.series(clean, copy, chmodBin, watchTask);
|
||||
// 构建打包
|
||||
export const build = gulp.series(clean, copy, chmodBin, buildTask);
|
||||
// release
|
||||
export const release = gulp.series(pack);
|
||||
@@ -1,116 +0,0 @@
|
||||
import { loadDotEnvRuntime, log, mainResolve, removeResource } from "./utils";
|
||||
import * as builder from "electron-builder";
|
||||
import semver from "semver";
|
||||
import pkg from "../app/package.json";
|
||||
|
||||
removeResource([mainResolve("release")]);
|
||||
|
||||
loadDotEnvRuntime();
|
||||
|
||||
if (semver.neq(process.env.APP_VERSION || "", pkg.version)) {
|
||||
log("请先同步构建版本和发布版本");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const options: builder.Configuration = {
|
||||
productName: process.env.APP_NAME,
|
||||
buildVersion: process.env.APP_VERSION,
|
||||
appId: process.env.APP_ID,
|
||||
copyright: process.env.APP_COPYRIGHT,
|
||||
artifactName: "${productName}-setup-${buildVersion}.${ext}",
|
||||
directories: {
|
||||
output: "./release",
|
||||
},
|
||||
files: [
|
||||
{
|
||||
from: "./build",
|
||||
to: "./",
|
||||
},
|
||||
"./package.json",
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "./app/mobile",
|
||||
to: "mobile",
|
||||
},
|
||||
{
|
||||
from: "./app/plugin",
|
||||
to: "plugin",
|
||||
},
|
||||
{
|
||||
from: "./app/bin/${platform}/",
|
||||
to: "bin",
|
||||
},
|
||||
],
|
||||
win: {
|
||||
icon: "../assets/icon.ico",
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
},
|
||||
],
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 410,
|
||||
y: 150,
|
||||
type: "link",
|
||||
path: "/Applications",
|
||||
},
|
||||
{
|
||||
x: 130,
|
||||
y: 150,
|
||||
type: "file",
|
||||
},
|
||||
],
|
||||
},
|
||||
mac: {
|
||||
icon: "../assets/icon.icns",
|
||||
target: {
|
||||
target: "default",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
},
|
||||
linux: {},
|
||||
nsis: {
|
||||
oneClick: true,
|
||||
allowElevation: true,
|
||||
allowToChangeInstallationDirectory: false,
|
||||
installerIcon: "",
|
||||
uninstallerIcon: "",
|
||||
installerHeaderIcon: "",
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
shortcutName: "",
|
||||
include: "",
|
||||
script: "",
|
||||
},
|
||||
publish: {
|
||||
provider: "github",
|
||||
repo: "m3u8-downloader",
|
||||
owner: "caorushizi",
|
||||
releaseType: "prerelease",
|
||||
},
|
||||
};
|
||||
|
||||
async function start() {
|
||||
const target =
|
||||
process.env.NODE_ENV === "development"
|
||||
? builder.DIR_TARGET
|
||||
: builder.DEFAULT_TARGET;
|
||||
try {
|
||||
const targets =
|
||||
process.platform === "win32"
|
||||
? builder.Platform.WINDOWS.createTarget(target)
|
||||
: builder.Platform.MAC.createTarget(target);
|
||||
await builder.build({
|
||||
targets,
|
||||
config: options,
|
||||
});
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,79 +1,108 @@
|
||||
import { existsSync, cpSync, rmSync } from "fs";
|
||||
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import electron from "electron";
|
||||
|
||||
// FIXME: 有没有什么办法可以不用这么写?
|
||||
const con = console;
|
||||
export const log = con.log;
|
||||
const baseResolve = (...r) => resolve(__dirname, ...r);
|
||||
export const mainResolve = (...r: string[]) => baseResolve("..", ...r);
|
||||
export const rootResolve = (...r: string[]) => baseResolve("../../..", ...r);
|
||||
|
||||
export const mainResolve = (...r: any[]) => resolve(__dirname, "..", ...r);
|
||||
export const rootResolve = (...r: any[]) =>
|
||||
resolve(__dirname, "../../..", ...r);
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
log("当前的环境是: ", nodeEnv);
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
function loadEnv(path: string) {
|
||||
const result: Record<string, string> = {};
|
||||
export const isLinux = process.platform === "linux";
|
||||
export const isMac = process.platform === "darwin";
|
||||
export const isWin = process.platform === "win32";
|
||||
|
||||
const _loadEnv = (path: string) => {
|
||||
if (!existsSync(path)) {
|
||||
export class ElectronApp {
|
||||
process: ChildProcessWithoutNullStreams | null = null;
|
||||
|
||||
start() {
|
||||
const args = ["--inspect=5858", mainResolve("app/build/main/index.js")];
|
||||
|
||||
this.process = spawn(String(electron), args);
|
||||
|
||||
this.process.stdout.on("data", (data) => {
|
||||
consola.log(String(data));
|
||||
});
|
||||
|
||||
this.process.stderr.on("data", (data) => {
|
||||
consola.log(String(data));
|
||||
});
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.kill();
|
||||
this.start();
|
||||
}
|
||||
|
||||
kill() {
|
||||
if (this.process && this.process.pid) {
|
||||
if (isMac) {
|
||||
spawn("kill", ["-9", String(this.process.pid)]);
|
||||
} else {
|
||||
process.kill(this.process.pid);
|
||||
}
|
||||
this.process = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Env {
|
||||
env: Record<string, string> = {};
|
||||
nodeEnv = "";
|
||||
static instance: Env;
|
||||
|
||||
private constructor(nodeEnv = process.env.NODE_ENV) {
|
||||
const env = this.parseEnv(rootResolve(".env"));
|
||||
const modeEnv = this.parseEnv(rootResolve(`.env.${nodeEnv}`));
|
||||
const localEnv = this.parseEnv(rootResolve(`.env.${nodeEnv}.local`));
|
||||
|
||||
this.nodeEnv = nodeEnv || "development";
|
||||
this.env = { ...env, ...modeEnv, ...localEnv };
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!Env.instance) {
|
||||
Env.instance = new Env();
|
||||
}
|
||||
|
||||
return Env.instance;
|
||||
}
|
||||
|
||||
private parseEnv(path: string) {
|
||||
if (!fs.existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { error, parsed } = dotenv.config({ path });
|
||||
if (error != null || !parsed) {
|
||||
const parsed = dotenv.parse(fs.readFileSync(path));
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.keys(parsed).forEach((key) => {
|
||||
result[key] = parsed[key];
|
||||
return Object.keys(parsed).reduce((prev, curr) => {
|
||||
prev[curr] = parsed[curr];
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
loadDotEnvRuntime() {
|
||||
Object.keys(this.env).forEach((key) => {
|
||||
if (process.env[key] != null || !this.env[key]) return;
|
||||
process.env[key] = this.env[key];
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_loadEnv(path);
|
||||
_loadEnv(`${path}.local`);
|
||||
loadDotEnvDefined() {
|
||||
return Object.keys(this.env).reduce<Record<string, string>>((prev, cur) => {
|
||||
if (!cur.startsWith("APP_")) return prev;
|
||||
prev[`process.env.${[cur]}`] = JSON.stringify(this.env[cur]);
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadDotEnv() {
|
||||
const env = loadEnv(rootResolve(".env"));
|
||||
const envMode = loadEnv(rootResolve(`.env.${nodeEnv}`));
|
||||
|
||||
return { ...env, ...envMode };
|
||||
}
|
||||
|
||||
export function loadDotEnvRuntime() {
|
||||
const env = loadDotEnv();
|
||||
|
||||
Object.keys(env).forEach((key) => {
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = env[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function loadDotEnvDefined() {
|
||||
const env = loadDotEnv();
|
||||
|
||||
return Object.keys(env).reduce<Record<string, string>>((prev, cur) => {
|
||||
if (!cur.startsWith("APP_")) return prev;
|
||||
prev[`process.env.${[cur]}`] = JSON.stringify(env[cur]);
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function copyResource(resource: { from: string; to: string }[]) {
|
||||
resource.forEach((r) => {
|
||||
const { from, to } = r;
|
||||
cpSync(from, to, {
|
||||
recursive: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function removeResource(resource: string[]) {
|
||||
resource.forEach((r) => {
|
||||
rmSync(r, { recursive: true, force: true });
|
||||
});
|
||||
get isDev() {
|
||||
return this.nodeEnv === "development";
|
||||
}
|
||||
}
|
||||
|
||||
+37
-34
@@ -1,19 +1,18 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { DownloadStatus } from "./interfaces";
|
||||
import { TYPES } from "./types";
|
||||
import { DownloadStatus } from "./interfaces.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import { Menu, Tray, app, nativeImage, nativeTheme } from "electron";
|
||||
import TrayIcon from "./tray-icon.png";
|
||||
import path from "path";
|
||||
import MainWindow from "./windows/MainWindow";
|
||||
import ProtocolService from "./services/ProtocolService";
|
||||
import UpdateService from "./services/UpdateService";
|
||||
import IpcHandlerService from "./services/IpcHandlerService";
|
||||
import DatabaseService from "./services/DatabaseService";
|
||||
import WebviewService from "./services/WebviewService";
|
||||
import DevToolsService from "./services/DevToolsService";
|
||||
import StoreService from "./services/StoreService";
|
||||
import VideoRepository from "./repository/VideoRepository";
|
||||
import VideoService from "./services/VideoService";
|
||||
import MainWindow from "./windows/MainWindow.ts";
|
||||
import WebviewService from "./services/WebviewService.ts";
|
||||
import VideoRepository from "./repository/VideoRepository.ts";
|
||||
import ElectronDevtools from "./vendor/ElectronDevtools.ts";
|
||||
import ElectronStore from "./vendor/ElectronStore.ts";
|
||||
import ElectronUpdater from "./vendor/ElectronUpdater.ts";
|
||||
import TypeORM from "./vendor/TypeORM.ts";
|
||||
import ProtocolService from "./core/protocol.ts";
|
||||
import IpcHandlerService from "./core/ipc.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ElectronApp {
|
||||
@@ -21,37 +20,41 @@ export default class ElectronApp {
|
||||
@inject(TYPES.MainWindow)
|
||||
private readonly mainWindow: MainWindow,
|
||||
@inject(TYPES.ProtocolService)
|
||||
private readonly protocolService: ProtocolService,
|
||||
@inject(TYPES.UpdateService)
|
||||
private readonly updateService: UpdateService,
|
||||
private readonly protocol: ProtocolService,
|
||||
@inject(TYPES.ElectronUpdater)
|
||||
private readonly updater: ElectronUpdater,
|
||||
@inject(TYPES.IpcHandlerService)
|
||||
private readonly ipcHandler: IpcHandlerService,
|
||||
@inject(TYPES.DatabaseService)
|
||||
private readonly dataService: DatabaseService,
|
||||
private readonly ipc: IpcHandlerService,
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM,
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webview: WebviewService,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.DevToolsService)
|
||||
private readonly devTools: DevToolsService,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.VideoService)
|
||||
private readonly videoService: VideoService,
|
||||
@inject(TYPES.ElectronDevtools)
|
||||
private readonly devTools: ElectronDevtools,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore
|
||||
) {}
|
||||
|
||||
private async seriveInit(): Promise<void> {
|
||||
this.protocolService.create();
|
||||
await this.dataService.init();
|
||||
private async serviceInit(): Promise<void> {
|
||||
this.mainWindow.init();
|
||||
this.ipcHandler.init();
|
||||
this.updateService.init();
|
||||
this.webview.init(), this.devTools.init();
|
||||
this.videoService.init();
|
||||
}
|
||||
|
||||
private async vendorInit() {
|
||||
await this.db.init();
|
||||
this.updater.init();
|
||||
this.devTools.init();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.seriveInit();
|
||||
this.protocol.create();
|
||||
this.ipc.init();
|
||||
|
||||
// vendor
|
||||
await this.vendorInit();
|
||||
// service
|
||||
await this.serviceInit();
|
||||
|
||||
app.on("activate", () => {
|
||||
this.mainWindow.init();
|
||||
@@ -64,7 +67,7 @@ export default class ElectronApp {
|
||||
}
|
||||
|
||||
initAppTheme(): void {
|
||||
const theme = this.storeService.get("theme");
|
||||
const theme = this.store.get("theme");
|
||||
nativeTheme.themeSource = theme;
|
||||
}
|
||||
|
||||
@@ -90,7 +93,7 @@ export default class ElectronApp {
|
||||
const videoIds = videos.map((video) => video.id);
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
videoIds,
|
||||
DownloadStatus.Failed,
|
||||
DownloadStatus.Failed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { ConversionPagination, type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ConversionRepository from "../repository/ConversionRepository.ts";
|
||||
import { handle } from "../helper/decorator.ts";
|
||||
import { Conversion } from "../entity/Conversion.ts";
|
||||
import { IpcMainEvent } from "electron/main";
|
||||
|
||||
@injectable()
|
||||
export default class ConversionController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.ConversionRepository)
|
||||
private readonly conversionRepository: ConversionRepository
|
||||
) {}
|
||||
|
||||
@handle("get-conversions")
|
||||
async getConversions(e: IpcMainEvent, pagination: ConversionPagination) {
|
||||
return await this.conversionRepository.getConversions(pagination);
|
||||
}
|
||||
|
||||
@handle("add-conversion")
|
||||
async addConversion(e: IpcMainEvent, conversion: Conversion) {
|
||||
return await this.conversionRepository.addConversion(conversion);
|
||||
}
|
||||
|
||||
@handle("delete-conversion")
|
||||
async deleteConversion(e: IpcMainEvent, id: number) {
|
||||
return await this.conversionRepository.deleteConversion(id);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,58 @@
|
||||
import { IpcMainEvent } from "electron/main";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper";
|
||||
import { handle } from "../helper/index.ts";
|
||||
import {
|
||||
type Controller,
|
||||
DownloadItem,
|
||||
DownloadItemPagination,
|
||||
Task,
|
||||
DownloadStatus,
|
||||
} from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import { downloader } from "../helper";
|
||||
import MainWindow from "../windows/MainWindow";
|
||||
import StoreService from "../services/StoreService";
|
||||
import DownloadService from "../services/DownloadService";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import { existsSync } from "fs-extra";
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import DownloadService from "../services/DownloadService.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DownloadController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.DownloadService)
|
||||
private readonly downloadService: DownloadService,
|
||||
@inject(TYPES.MainWindow)
|
||||
private readonly mainWindow: MainWindow,
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webviewService: WebviewService
|
||||
) {}
|
||||
|
||||
@handle("show-download-dialog")
|
||||
async showDownloadDialog(e: IpcMainEvent, data: DownloadItem) {
|
||||
const image = await this.webviewService.captureView();
|
||||
this.webviewService.sendToWindow(
|
||||
"show-download-dialog",
|
||||
data,
|
||||
image.toDataURL()
|
||||
);
|
||||
}
|
||||
|
||||
@handle("add-download-item")
|
||||
async addDownloadItem(e: IpcMainEvent, video: DownloadItem) {
|
||||
async addDownloadItem(e: IpcMainEvent, video: Omit<DownloadItem, "id">) {
|
||||
const item = await this.videoRepository.addVideo(video);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
this.mainWindow.window?.webContents.send("download-item-notifier", item);
|
||||
this.mainWindow.send("download-item-notifier", item);
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("add-download-items")
|
||||
async addDownloadItems(e: IpcMainEvent, videos: DownloadItem[]) {
|
||||
async addDownloadItems(e: IpcMainEvent, videos: Omit<DownloadItem, "id">[]) {
|
||||
const items = await this.videoRepository.addVideos(videos);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
this.mainWindow.window?.webContents.send("download-item-notifier", items);
|
||||
this.mainWindow.send("download-item-notifier", items);
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -51,8 +62,15 @@ export default class DownloadController implements Controller {
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("edit-download-now")
|
||||
async editDownloadNow(e: IpcMainEvent, video: DownloadItem) {
|
||||
const item = await this.editDownloadItem(e, video);
|
||||
await this.startDownload(e, item.id);
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("download-now")
|
||||
async downloadNow(e: IpcMainEvent, video: DownloadItem) {
|
||||
async downloadNow(e: IpcMainEvent, video: Omit<DownloadItem, "id">) {
|
||||
// 添加下载项
|
||||
const item = await this.addDownloadItem(e, video);
|
||||
// 开始下载
|
||||
@@ -60,38 +78,30 @@ export default class DownloadController implements Controller {
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("download-items-now")
|
||||
async downloadItemsNow(e: IpcMainEvent, videos: Omit<DownloadItem, "id">[]) {
|
||||
// 添加下载项
|
||||
const items = await this.addDownloadItems(e, videos);
|
||||
// 开始下载
|
||||
items.forEach((item) => this.startDownload(e, item.id));
|
||||
return items;
|
||||
}
|
||||
|
||||
@handle("get-download-items")
|
||||
async getDownloadItems(e: IpcMainEvent, pagination: DownloadItemPagination) {
|
||||
const localDir = this.storeService.get("local");
|
||||
const videos = await this.videoRepository.findVideos(pagination);
|
||||
const newVideos = videos.list.map((video) => {
|
||||
if (video.status === DownloadStatus.Success) {
|
||||
return {
|
||||
...video,
|
||||
exist: existsSync(`${localDir}/${video.name}.mp4`),
|
||||
};
|
||||
}
|
||||
return video;
|
||||
});
|
||||
|
||||
return {
|
||||
total: videos.total,
|
||||
list: newVideos,
|
||||
};
|
||||
return videos;
|
||||
}
|
||||
|
||||
@handle("start-download")
|
||||
async startDownload(e: IpcMainEvent, vid: number) {
|
||||
// 查找将要下载的视频
|
||||
const video = await this.videoRepository.findVideo(vid);
|
||||
if (!video) {
|
||||
return Promise.reject("没有找到该视频");
|
||||
}
|
||||
const { name, url, headers, type } = video;
|
||||
const local = this.storeService.get("local");
|
||||
const local = this.store.get("local");
|
||||
|
||||
// 从配置中添加参数
|
||||
const deleteSegments = this.storeService.get("deleteSegments");
|
||||
const deleteSegments = this.store.get("deleteSegments");
|
||||
|
||||
const task: Task = {
|
||||
id: vid,
|
||||
@@ -103,7 +113,6 @@ export default class DownloadController implements Controller {
|
||||
headers,
|
||||
deleteSegments,
|
||||
},
|
||||
process: downloader,
|
||||
};
|
||||
await this.videoRepository.changeVideoStatus(vid, DownloadStatus.Watting);
|
||||
this.downloadService.addTask(task);
|
||||
|
||||
@@ -8,30 +8,32 @@ import {
|
||||
shell,
|
||||
clipboard,
|
||||
} from "electron";
|
||||
import { Favorite } from "../entity/Favorite";
|
||||
import { convertToAudio, db, workspace } from "../helper";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import { convertToAudio, db, workspace } from "../helper/index.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { AppStore, EnvPath } from "../main";
|
||||
import { AppStore, EnvPath } from "../main.ts";
|
||||
import path from "path";
|
||||
import { handle, getLocalIP } from "../helper";
|
||||
import { type Controller } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import { handle, getLocalIP } from "../helper/index.ts";
|
||||
import { type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import fs from "fs-extra";
|
||||
import MainWindow from "../windows/MainWindow";
|
||||
import BrowserWindow from "../windows/BrowserWindow";
|
||||
import PlayerWindow from "../windows/PlayerWindow";
|
||||
import StoreService from "../services/StoreService";
|
||||
import WebviewService from "../services/WebviewService";
|
||||
import FavoriteRepository from "../repository/FavoriteRepository";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
import BrowserWindow from "../windows/BrowserWindow.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
import FavoriteRepository from "../repository/FavoriteRepository.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import ConversionRepository from "../repository/ConversionRepository.ts";
|
||||
import { machineId } from "node-machine-id";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@injectable()
|
||||
export default class HomeController implements Controller {
|
||||
private sharedState: Record<string, any> = {};
|
||||
private sharedState: Record<string, unknown> = {};
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
@inject(TYPES.FavoriteRepository)
|
||||
private readonly favoriteRepository: FavoriteRepository,
|
||||
@inject(TYPES.MainWindow)
|
||||
@@ -40,10 +42,10 @@ export default class HomeController implements Controller {
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.BrowserWindow)
|
||||
private readonly browserWindow: BrowserWindow,
|
||||
@inject(TYPES.PlayerWindow)
|
||||
private readonly playerWindow: PlayerWindow,
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webviewService: WebviewService,
|
||||
@inject(TYPES.ConversionRepository)
|
||||
private readonly conversionRepository: ConversionRepository
|
||||
) {}
|
||||
|
||||
@handle("get-env-path")
|
||||
@@ -53,7 +55,7 @@ export default class HomeController implements Controller {
|
||||
dbPath: db,
|
||||
workspace: workspace,
|
||||
platform: process.platform,
|
||||
local: this.storeService.get("local"),
|
||||
local: this.store.get("local"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,15 +76,13 @@ export default class HomeController implements Controller {
|
||||
|
||||
@handle("get-app-store")
|
||||
getAppStore() {
|
||||
return this.storeService.store;
|
||||
return this.store.store;
|
||||
}
|
||||
|
||||
@handle("on-favorite-item-context-menu")
|
||||
async onFavoriteItemContextMenu(e: IpcMainEvent, id: number) {
|
||||
const send = (action: string) => {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return;
|
||||
window.webContents.send("favorite-item-event", {
|
||||
this.mainWindow.send("favorite-item-event", {
|
||||
action,
|
||||
payload: id,
|
||||
});
|
||||
@@ -118,7 +118,7 @@ export default class HomeController implements Controller {
|
||||
|
||||
if (!result.canceled) {
|
||||
const dir = result.filePaths[0];
|
||||
this.storeService.set("local", dir);
|
||||
this.store.set("local", dir);
|
||||
return dir;
|
||||
}
|
||||
return "";
|
||||
@@ -128,12 +128,12 @@ export default class HomeController implements Controller {
|
||||
async setAppStore(e: IpcMainEvent, key: keyof AppStore, val: any) {
|
||||
// useProxy
|
||||
if (key === "useProxy") {
|
||||
const proxy = this.storeService.get("proxy");
|
||||
const proxy = this.store.get("proxy");
|
||||
this.webviewService.setProxy(val, proxy);
|
||||
}
|
||||
// proxy
|
||||
if (key === "proxy") {
|
||||
const useProxy = this.storeService.get("useProxy");
|
||||
const useProxy = this.store.get("useProxy");
|
||||
useProxy && this.webviewService.setProxy(true, val);
|
||||
}
|
||||
// block
|
||||
@@ -148,8 +148,12 @@ export default class HomeController implements Controller {
|
||||
if (key === "isMobile") {
|
||||
this.webviewService.setUserAgent(val);
|
||||
}
|
||||
// privacy
|
||||
if (key === "privacy") {
|
||||
this.webviewService.setDefaultSession(val);
|
||||
}
|
||||
|
||||
this.storeService.set(key, val);
|
||||
this.store.set(key, val);
|
||||
}
|
||||
|
||||
@handle("open-dir")
|
||||
@@ -165,10 +169,7 @@ export default class HomeController implements Controller {
|
||||
@handle("on-download-list-context-menu")
|
||||
async downloadListContextMenu(e: IpcMainEvent, id: number) {
|
||||
const send = (action: string) => {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return;
|
||||
|
||||
window.webContents.send("download-item-event", {
|
||||
this.mainWindow.send("download-item-event", {
|
||||
action,
|
||||
payload: id,
|
||||
});
|
||||
@@ -178,7 +179,7 @@ export default class HomeController implements Controller {
|
||||
{
|
||||
label: "拷贝链接地址",
|
||||
click: () => {
|
||||
clipboard.writeText(item?.url || "");
|
||||
clipboard.writeText(item.url || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -214,10 +215,11 @@ export default class HomeController implements Controller {
|
||||
|
||||
@handle("convert-to-audio")
|
||||
async convertToAudio(e: IpcMainEvent, id: number) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
const local = this.storeService.get("local");
|
||||
const input = path.join(local, `${video?.name}.mp4`);
|
||||
const output = path.join(local, `${video?.name}.mp3`);
|
||||
const conversion = await this.conversionRepository.findConversion(id);
|
||||
const local = this.store.get("local");
|
||||
const input = conversion.path;
|
||||
const outName = `${path.basename(input, path.extname(input))}.mp3`;
|
||||
const output = path.join(local, outName);
|
||||
|
||||
const exist = await fs.exists(input);
|
||||
if (exist) {
|
||||
@@ -237,13 +239,7 @@ export default class HomeController implements Controller {
|
||||
// 关闭浏览器窗口
|
||||
this.browserWindow.hideWindow();
|
||||
// 修改设置中的属性
|
||||
this.storeService.set("openInNewWindow", false);
|
||||
}
|
||||
|
||||
@handle("open-player-window")
|
||||
async openPlayerWindow(event: IpcMainEvent, name: string) {
|
||||
// 打开播放器窗口
|
||||
this.playerWindow.openWindow(name);
|
||||
this.store.set("openInNewWindow", false);
|
||||
}
|
||||
|
||||
@handle("get-local-ip")
|
||||
@@ -260,4 +256,42 @@ export default class HomeController implements Controller {
|
||||
async setSharedState(event: IpcMainEvent, state: any) {
|
||||
this.sharedState = state;
|
||||
}
|
||||
|
||||
@handle("get-download-log")
|
||||
async getDownloadLog(event: IpcMainEvent, id: number) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
return video.log || "";
|
||||
}
|
||||
|
||||
@handle("select-file")
|
||||
async selectFile() {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return Promise.reject("未找到主窗口");
|
||||
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
properties: ["openFile"],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
return result.filePaths[0];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@handle("get-machine-id")
|
||||
async getMachineId() {
|
||||
try {
|
||||
const id = this.store.get("machineId");
|
||||
if (id) return id;
|
||||
const newId = await machineId();
|
||||
this.store.set("machineId", newId);
|
||||
return newId;
|
||||
} catch (e) {
|
||||
const id = this.store.get("machineId");
|
||||
if (id) return id;
|
||||
const newId = nanoid();
|
||||
this.store.set("machineId", newId);
|
||||
return newId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { IpcMainEvent } from "electron";
|
||||
import {
|
||||
IpcMainEvent,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuItemConstructorOptions,
|
||||
} from "electron";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper";
|
||||
import { type Controller } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import WebviewService from "../services/WebviewService";
|
||||
import StoreService from "../services/StoreService";
|
||||
import { handle } from "../helper/index.ts";
|
||||
import { type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import { SniffingHelper } from "../services/SniffingHelperService.ts";
|
||||
|
||||
@injectable()
|
||||
export default class WebviewController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webview: WebviewService,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
@inject(TYPES.SniffingHelper)
|
||||
private readonly sniffingHelper: SniffingHelper
|
||||
) {}
|
||||
|
||||
@handle("set-webview-bounds")
|
||||
@@ -21,10 +29,27 @@ export default class WebviewController implements Controller {
|
||||
}
|
||||
|
||||
@handle("webview-load-url")
|
||||
async browserViewLoadUrl(e: IpcMainEvent, url?: string): Promise<void> {
|
||||
async browserViewLoadUrl(e: IpcMainEvent, url: string): Promise<void> {
|
||||
await this.webview.loadURL(url);
|
||||
}
|
||||
|
||||
@handle("webview-url-contextmenu")
|
||||
async webviewUrlContextMenu() {
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: "复制",
|
||||
role: "copy",
|
||||
},
|
||||
{
|
||||
label: "粘贴",
|
||||
role: "paste",
|
||||
},
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
menu.popup();
|
||||
}
|
||||
|
||||
@handle("webview-go-back")
|
||||
async webviewGoBack(): Promise<boolean> {
|
||||
return this.webview.goBack();
|
||||
@@ -53,6 +78,16 @@ export default class WebviewController implements Controller {
|
||||
@handle("webview-change-user-agent")
|
||||
async webviewChangeUserAgent(e: IpcMainEvent, isMobile: boolean) {
|
||||
this.webview.setUserAgent(isMobile);
|
||||
this.storeService.set("isMobile", isMobile);
|
||||
this.store.set("isMobile", isMobile);
|
||||
}
|
||||
|
||||
@handle("plugin-ready")
|
||||
async pluginReady() {
|
||||
this.sniffingHelper.pluginReady();
|
||||
}
|
||||
|
||||
@handle("clear-webview-cache")
|
||||
async clearWebviewCache() {
|
||||
return this.webview.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { inject, injectable, multiInject } from "inversify";
|
||||
import { Controller } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import LoggerService from "./LoggerService";
|
||||
import { error, success } from "../helper/utils";
|
||||
import { Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import { error, success } from "../helper/utils.ts";
|
||||
|
||||
@injectable()
|
||||
export default class IpcHandlerService {
|
||||
constructor(
|
||||
@multiInject(TYPES.Controller)
|
||||
private readonly controllers: Controller[],
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
) {}
|
||||
|
||||
private registerIpc(
|
||||
@@ -43,7 +43,7 @@ export default class IpcHandlerService {
|
||||
}
|
||||
return success(res);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error("处理 ipc 事件时异常: ", e);
|
||||
this.logger.error(`处理 ipc [${channel}] 事件时异常: `, e);
|
||||
if (e instanceof Error) {
|
||||
return error(e.message);
|
||||
} else {
|
||||
@@ -0,0 +1,30 @@
|
||||
import { protocol } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { pathExists, readFile } from "fs-extra";
|
||||
import { injectable } from "inversify";
|
||||
import { join } from "path";
|
||||
import { URL } from "url";
|
||||
import { defaultScheme } from "../helper/index.ts";
|
||||
import mime from "mime-types";
|
||||
|
||||
@injectable()
|
||||
export default class ProtocolService {
|
||||
create(): void {
|
||||
if (isDev) return;
|
||||
|
||||
protocol.handle(defaultScheme, async (req) => {
|
||||
const pathName = new URL(req.url).pathname;
|
||||
let filePath = join(__dirname, "../renderer", pathName);
|
||||
const fileExist = await pathExists(filePath);
|
||||
if (!fileExist) {
|
||||
// 如果没有找到文件,直接返回 index.html , react history 模式
|
||||
filePath = join(__dirname, "../renderer/index.html");
|
||||
}
|
||||
const mimeType = mime.lookup(filePath);
|
||||
const data = await readFile(filePath);
|
||||
return new Response(data, {
|
||||
headers: { "Content-Type": mimeType || "text/html" },
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface Vendor {
|
||||
init(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity({
|
||||
name: "conversion",
|
||||
})
|
||||
export class Conversion {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: true,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
default: "",
|
||||
})
|
||||
path: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate?: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedDate?: Date;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DownloadStatus, DownloadType } from "../interfaces";
|
||||
import { DownloadStatus, DownloadType } from "../interfaces.ts";
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -17,6 +17,7 @@ export class Video {
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@@ -53,6 +54,12 @@ export class Video {
|
||||
})
|
||||
status: DownloadStatus;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
default: "",
|
||||
})
|
||||
log: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate: Date;
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { stripColors, biliDownloaderBin } from "../../helper";
|
||||
import { DownloadParams, DownloadProgress } from "../../interfaces";
|
||||
import { cmdr } from "./cmdr";
|
||||
|
||||
export const biliDownloader = async (params: DownloadParams): Promise<void> => {
|
||||
const { id, abortSignal, url, local, callback } = params;
|
||||
// const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
|
||||
const progressReg = /([\d.]+)%/g;
|
||||
const errorReg = /ERROR/g;
|
||||
const startDownloadReg = /保存文件名:/g;
|
||||
const isLiveReg = /检测到直播流/g;
|
||||
|
||||
const spawnParams = [url, "--work-dir", local];
|
||||
|
||||
await cmdr(biliDownloaderBin, spawnParams, {
|
||||
detached: true,
|
||||
shell: true,
|
||||
abortSignal,
|
||||
onMessage: (message) => {
|
||||
if (isLiveReg.test(message) || startDownloadReg.test(message)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive: false,
|
||||
cur: "",
|
||||
total: "",
|
||||
speed: "",
|
||||
});
|
||||
}
|
||||
|
||||
const log = stripColors(message);
|
||||
if (errorReg.test(log)) {
|
||||
throw new Error(log);
|
||||
}
|
||||
|
||||
const result = progressReg.exec(log);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, precentage, speed] = result;
|
||||
const cur = String(Number(precentage) * 10);
|
||||
if (cur === "0") {
|
||||
return;
|
||||
}
|
||||
|
||||
const total = "1000";
|
||||
// FIXME: 无法获取是否为直播流
|
||||
const progress: DownloadProgress = {
|
||||
id,
|
||||
type: "progress",
|
||||
cur,
|
||||
total,
|
||||
speed,
|
||||
isLive: false,
|
||||
};
|
||||
callback(progress);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
import { execa, Options } from "execa";
|
||||
import iconv from "iconv-lite";
|
||||
|
||||
export interface CmdrParams {
|
||||
abortSignal: AbortController;
|
||||
encoding?: string;
|
||||
onMessage?: (message: string) => void;
|
||||
onErrMessage?: (message: string) => void;
|
||||
}
|
||||
|
||||
export const cmdr = (
|
||||
binPath: string,
|
||||
args: string[],
|
||||
params: CmdrParams & Options,
|
||||
): Promise<void> => {
|
||||
const {
|
||||
abortSignal,
|
||||
encoding = "utf-8",
|
||||
onMessage,
|
||||
onErrMessage,
|
||||
...execOptions
|
||||
} = params;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = (data: Buffer, callback: (message: string) => void) => {
|
||||
const items = iconv.decode(data, encoding);
|
||||
items.split("\n").forEach((item) => {
|
||||
const message = item.trim();
|
||||
if (!message) return;
|
||||
try {
|
||||
callback(message);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const downloader = execa(binPath, args, {
|
||||
...execOptions,
|
||||
signal: abortSignal.signal,
|
||||
});
|
||||
|
||||
if (downloader.stdout && onMessage) {
|
||||
downloader.stdout.on("data", (data) => process(data, onMessage));
|
||||
}
|
||||
|
||||
if (downloader.stderr && onErrMessage) {
|
||||
downloader.stderr.on("data", (data) => process(data, onErrMessage));
|
||||
}
|
||||
|
||||
downloader.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
downloader.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("未知错误"));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { DownloadParams } from "../../interfaces";
|
||||
import { m3u8DownloaderWin32 } from "./m3u8-win32";
|
||||
import { m3u8DownloaderDarwin } from "./m3u8-darwin";
|
||||
import { biliDownloader } from "./bilibili";
|
||||
|
||||
export const downloader = (params: DownloadParams): Promise<void> => {
|
||||
if (params.type === "bilibili") {
|
||||
return biliDownloader(params);
|
||||
}
|
||||
|
||||
if (params.type === "m3u8") {
|
||||
if (process.platform === "win32") {
|
||||
return m3u8DownloaderWin32(params);
|
||||
} else {
|
||||
return m3u8DownloaderDarwin(params);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import { stripColors, m3u8DownloaderBin } from "../../helper";
|
||||
import { DownloadParams, DownloadProgress } from "../../interfaces";
|
||||
import { cmdr } from "./cmdr";
|
||||
|
||||
export const m3u8DownloaderDarwin = async (
|
||||
params: DownloadParams,
|
||||
): Promise<void> => {
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
proxy,
|
||||
} = params;
|
||||
// const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
|
||||
const progressReg = /([\d.]+)%/g;
|
||||
const errorReg = /ERROR/g;
|
||||
const startDownloadReg = /保存文件名:/g;
|
||||
const isLiveReg = /检测到直播流/g;
|
||||
|
||||
const spawnParams = [
|
||||
url,
|
||||
"--tmp-dir",
|
||||
local,
|
||||
"--save-dir",
|
||||
local,
|
||||
"--save-name",
|
||||
name,
|
||||
"--auto-select",
|
||||
];
|
||||
|
||||
if (headers) {
|
||||
const h: Record<string, unknown> = JSON.parse(headers);
|
||||
Object.entries(h).forEach(([k, v]) => {
|
||||
spawnParams.push("-H", `${k}: ${v}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSegments) {
|
||||
spawnParams.push("--del-after-done");
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
spawnParams.push("--custom-proxy", proxy);
|
||||
}
|
||||
|
||||
let isLive = false;
|
||||
await cmdr(m3u8DownloaderBin, spawnParams, {
|
||||
abortSignal,
|
||||
onMessage: (message) => {
|
||||
if (isLiveReg.test(message) || startDownloadReg.test(message)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive,
|
||||
cur: "",
|
||||
total: "",
|
||||
speed: "",
|
||||
});
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
const log = stripColors(message);
|
||||
|
||||
if (errorReg.test(log)) {
|
||||
throw new Error(log);
|
||||
}
|
||||
|
||||
const result = progressReg.exec(log);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, precentage, speed] = result;
|
||||
const cur = String(Number(precentage) * 10);
|
||||
if (cur === "0") {
|
||||
return;
|
||||
}
|
||||
|
||||
const total = "1000";
|
||||
// FIXME: 无法获取是否为直播流
|
||||
const progress: DownloadProgress = {
|
||||
id,
|
||||
type: "progress",
|
||||
cur,
|
||||
total,
|
||||
speed,
|
||||
isLive,
|
||||
};
|
||||
callback(progress);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import { formatHeaders, m3u8DownloaderBin } from "../../helper";
|
||||
import { DownloadParams, DownloadProgress } from "../../interfaces";
|
||||
import { cmdr } from "./cmdr";
|
||||
|
||||
export const m3u8DownloaderWin32 = async (
|
||||
params: DownloadParams,
|
||||
): Promise<void> => {
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
proxy,
|
||||
} = params;
|
||||
const progressReg = /Progress:\s(\d+)\/(\d+)\s\(.+?\).+?\((.+?\/s).*?\)/g;
|
||||
const isLiveReg = /识别为直播流, 开始录制/g;
|
||||
const startDownloadReg = /开始下载文件/g;
|
||||
|
||||
const spawnParams = [url, "--workDir", local, "--saveName", name];
|
||||
|
||||
if (headers) {
|
||||
spawnParams.push("--headers", formatHeaders(headers));
|
||||
}
|
||||
|
||||
if (deleteSegments) {
|
||||
spawnParams.push("--enableDelAfterDone");
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
spawnParams.push("--proxyAddress", proxy);
|
||||
}
|
||||
|
||||
let isLive = false;
|
||||
await cmdr(m3u8DownloaderBin, spawnParams, {
|
||||
abortSignal,
|
||||
onMessage: (message) => {
|
||||
if (isLiveReg.test(message) || startDownloadReg.test(message)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive,
|
||||
cur: "",
|
||||
total: "",
|
||||
speed: "",
|
||||
});
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
const result = progressReg.exec(message);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, cur, total, speed] = result;
|
||||
const progress: DownloadProgress = {
|
||||
id,
|
||||
type: "progress",
|
||||
cur,
|
||||
total,
|
||||
speed,
|
||||
isLive,
|
||||
};
|
||||
callback(progress);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "child_process";
|
||||
import { ffmpegPath } from "./variables";
|
||||
import { ffmpegPath } from "./variables.ts";
|
||||
|
||||
export const convertToAudio = async (
|
||||
input: string,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// 创建 axios 实例,并指定自定义的 HTTPS Agent
|
||||
const instance = axios.create({
|
||||
httpsAgent: agent,
|
||||
});
|
||||
|
||||
export default instance;
|
||||
@@ -1,4 +1,6 @@
|
||||
import os from "os";
|
||||
import fetch from "node-fetch";
|
||||
import https from "https";
|
||||
|
||||
export function getLocalIP() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
@@ -23,9 +25,17 @@ export function getLocalIP() {
|
||||
return localIP;
|
||||
}
|
||||
|
||||
export { sleep, stripColors, formatHeaders } from "./utils";
|
||||
export * from "./variables";
|
||||
export { on, handle } from "./decorator";
|
||||
export { downloader } from "./downloader";
|
||||
export { convertToAudio } from "./ffmpeg";
|
||||
export { default as http } from "./http";
|
||||
function fetchWrapper(url: string) {
|
||||
const options = {
|
||||
agent: new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
};
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
export { sleep, formatHeaders } from "./utils.ts";
|
||||
export * from "./variables.ts";
|
||||
export { on, handle } from "./decorator.ts";
|
||||
export { convertToAudio } from "./ffmpeg.ts";
|
||||
export { fetchWrapper as fetch };
|
||||
|
||||
@@ -4,20 +4,11 @@ export async function sleep(second = 1): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, second * 1000));
|
||||
}
|
||||
|
||||
export function stripColors(str: string) {
|
||||
// 匹配控制台颜色字符的正则表达式
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const colorRegex = /\x1b\[(\d+)(;\d+)*m/g;
|
||||
// 将所有颜色字符替换为空字符串
|
||||
return str.replace(colorRegex, "");
|
||||
}
|
||||
|
||||
export function formatHeaders(headersStr: string): string {
|
||||
const headers: Record<string, any> | null = JSON.parse(headersStr);
|
||||
export function formatHeaders(headers: Record<string, string>): string {
|
||||
if (!headers) return "";
|
||||
const formatted = Object.entries(headers)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join("|");
|
||||
.join("\n");
|
||||
return formatted;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,15 @@ const appPath = app.getAppPath();
|
||||
export const appData = app.getPath("appData");
|
||||
export const download = app.getPath("downloads");
|
||||
|
||||
export const isMac = process.platform === "darwin";
|
||||
export const isWin = process.platform === "win32";
|
||||
export enum Platform {
|
||||
Windows = "win32",
|
||||
MacOS = "darwin",
|
||||
Linux = "linux",
|
||||
}
|
||||
|
||||
export const isMac = process.platform === Platform.MacOS;
|
||||
export const isWin = process.platform === Platform.Windows;
|
||||
export const isLinux = process.platform === Platform.Linux;
|
||||
|
||||
if (!isDev) {
|
||||
global.__bin__ = resolve(appPath, "../bin");
|
||||
@@ -18,28 +25,27 @@ export function resolveStatic(path: string) {
|
||||
return resolve(appPath, relativePath, path);
|
||||
}
|
||||
export function resolveBin(path: string) {
|
||||
if (isWin) {
|
||||
path += ".exe";
|
||||
}
|
||||
return resolve(__bin__, path);
|
||||
}
|
||||
|
||||
export const appName = process.env.APP_NAME || "electron-template";
|
||||
export const appName = process.env.APP_NAME || "mediago";
|
||||
export const workspace = resolve(appData, appName);
|
||||
export const defaultScheme = "mediago";
|
||||
export const PERSIST_MEDIAGO = "persist:mediago";
|
||||
export const PERSIST_WEBVIEW = "persist:webview";
|
||||
export const PRIVACY_WEBVIEW = "webview";
|
||||
export const db = resolve(workspace, "app.db");
|
||||
|
||||
// bin path
|
||||
export const ffmpegPath = resolveBin("ffmpeg");
|
||||
export const biliDownloaderBin = resolveBin("BBDown");
|
||||
const downloaderBinName = isWin ? "N_m3u8DL-CLI" : "N_m3u8DL-RE";
|
||||
export const m3u8DownloaderBin = resolveBin(downloaderBinName);
|
||||
export const videoServerBin = resolveBin("server");
|
||||
export const m3u8DownloaderBin = resolveBin("N_m3u8DL-RE");
|
||||
|
||||
// mobile path
|
||||
export const mobilePath = resolveStatic("mobile");
|
||||
// plugin path
|
||||
export const pluginPath = resolveStatic("plugin/index.js");
|
||||
export const pluginStylePath = resolveStatic("plugin/style.css");
|
||||
|
||||
// user agent
|
||||
export const pcUA =
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "reflect-metadata";
|
||||
import { app, protocol } from "electron";
|
||||
import { defaultScheme } from "./helper";
|
||||
import { container } from "./inversify.config";
|
||||
import { TYPES } from "./types";
|
||||
import ElectronApp from "./app";
|
||||
import { defaultScheme } from "./helper/index.ts";
|
||||
import { container } from "./inversify.config.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import ElectronApp from "./app.ts";
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
const start = async (): Promise<void> => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Conversion } from "./entity/Conversion.ts";
|
||||
|
||||
export type Controller = Record<string | symbol, any>;
|
||||
|
||||
export interface DownloadItem {
|
||||
@@ -21,11 +23,21 @@ export interface DownloadItemPagination {
|
||||
filter?: DownloadFilter;
|
||||
}
|
||||
|
||||
export interface ConversionPagination {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface VideoResponse {
|
||||
total: number;
|
||||
list: DownloadItem[];
|
||||
}
|
||||
|
||||
export interface ConversionResponse {
|
||||
total: number;
|
||||
list: Conversion[];
|
||||
}
|
||||
|
||||
export enum DownloadStatus {
|
||||
Ready = "ready",
|
||||
Watting = "watting",
|
||||
@@ -38,14 +50,12 @@ export enum DownloadStatus {
|
||||
export type Task = {
|
||||
id: number;
|
||||
params: Omit<DownloadParams, "id" | "abortSignal" | "callback">;
|
||||
process: (params: DownloadParams) => Promise<void>;
|
||||
};
|
||||
|
||||
export interface DownloadProgress {
|
||||
id: number;
|
||||
type: string;
|
||||
cur: string;
|
||||
total: string;
|
||||
percent: string;
|
||||
speed: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import DownloadController from "./controller/DownloadController";
|
||||
import HomeController from "./controller/HomeController";
|
||||
import WebviewController from "./controller/WebviewController";
|
||||
import DownloadController from "./controller/DownloadController.ts";
|
||||
import HomeController from "./controller/HomeController.ts";
|
||||
import WebviewController from "./controller/WebviewController.ts";
|
||||
import { Container } from "inversify";
|
||||
import FavoriteRepository from "./repository/FavoriteRepository";
|
||||
import VideoRepository from "./repository/VideoRepository";
|
||||
import DatabaseService from "./services/DatabaseService";
|
||||
import DevToolsService from "./services/DevToolsService";
|
||||
import DownloadService from "./services/DownloadService";
|
||||
import StoreService from "./services/StoreService";
|
||||
import WebviewService from "./services/WebviewService";
|
||||
import ElectronApp from "./app";
|
||||
import { Controller } from "./interfaces";
|
||||
import IpcHandlerService from "./services/IpcHandlerService";
|
||||
import LoggerService from "./services/LoggerService";
|
||||
import ProtocolService from "./services/ProtocolService";
|
||||
import UpdateService from "./services/UpdateService";
|
||||
import { TYPES } from "./types";
|
||||
import MainWindow from "./windows/MainWindow";
|
||||
import PlayerWindow from "./windows/PlayerWindow";
|
||||
import BrowserWindow from "./windows/BrowserWindow";
|
||||
import VideoService from "./services/VideoService";
|
||||
import FavoriteRepository from "./repository/FavoriteRepository.ts";
|
||||
import VideoRepository from "./repository/VideoRepository.ts";
|
||||
import WebviewService from "./services/WebviewService.ts";
|
||||
import ElectronApp from "./app.ts";
|
||||
import { Controller } from "./interfaces.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import MainWindow from "./windows/MainWindow.ts";
|
||||
import BrowserWindow from "./windows/BrowserWindow.ts";
|
||||
import { SniffingHelper } from "./services/SniffingHelperService.ts";
|
||||
import DownloadService from "./services/DownloadService.ts";
|
||||
import ElectronLogger from "./vendor/ElectronLogger.ts";
|
||||
import ElectronUpdater from "./vendor/ElectronUpdater.ts";
|
||||
import TypeORM from "./vendor/TypeORM.ts";
|
||||
import ElectronDevtools from "./vendor/ElectronDevtools.ts";
|
||||
import ElectronStore from "./vendor/ElectronStore.ts";
|
||||
import IpcHandler from "./core/ipc.ts";
|
||||
import ProtocolService from "./core/protocol.ts";
|
||||
import ConversionController from "./controller/ConversionController.ts";
|
||||
import ConversionRepository from "./repository/ConversionRepository.ts";
|
||||
|
||||
const container = new Container({
|
||||
skipBaseClassChecks: true,
|
||||
@@ -30,33 +31,38 @@ const container = new Container({
|
||||
container.bind<ElectronApp>(TYPES.ElectronApp).to(ElectronApp);
|
||||
|
||||
// services
|
||||
container
|
||||
.bind<IpcHandlerService>(TYPES.IpcHandlerService)
|
||||
.to(IpcHandlerService);
|
||||
container.bind<ProtocolService>(TYPES.ProtocolService).to(ProtocolService);
|
||||
container.bind<UpdateService>(TYPES.UpdateService).to(UpdateService);
|
||||
container.bind<LoggerService>(TYPES.LoggerService).to(LoggerService);
|
||||
container.bind<StoreService>(TYPES.StoreService).to(StoreService);
|
||||
container.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseService);
|
||||
container.bind<WebviewService>(TYPES.WebviewService).to(WebviewService);
|
||||
container.bind<DownloadService>(TYPES.DownloadService).to(DownloadService);
|
||||
container.bind<DevToolsService>(TYPES.DevToolsService).to(DevToolsService);
|
||||
container.bind<VideoService>(TYPES.VideoService).to(VideoService);
|
||||
container.bind<SniffingHelper>(TYPES.SniffingHelper).to(SniffingHelper);
|
||||
|
||||
// windows
|
||||
container.bind<MainWindow>(TYPES.MainWindow).to(MainWindow);
|
||||
container.bind<BrowserWindow>(TYPES.BrowserWindow).to(BrowserWindow);
|
||||
container.bind<PlayerWindow>(TYPES.PlayerWindow).to(PlayerWindow);
|
||||
|
||||
// controller
|
||||
container.bind<Controller>(TYPES.Controller).to(HomeController);
|
||||
container.bind<Controller>(TYPES.Controller).to(WebviewController);
|
||||
container.bind<Controller>(TYPES.Controller).to(DownloadController);
|
||||
container.bind<Controller>(TYPES.Controller).to(ConversionController);
|
||||
|
||||
// repository
|
||||
container.bind<VideoRepository>(TYPES.VideoRepository).to(VideoRepository);
|
||||
container
|
||||
.bind<FavoriteRepository>(TYPES.FavoriteRepository)
|
||||
.to(FavoriteRepository);
|
||||
container
|
||||
.bind<ConversionRepository>(TYPES.ConversionRepository)
|
||||
.to(ConversionRepository);
|
||||
|
||||
// vendor
|
||||
container.bind<ElectronDevtools>(TYPES.ElectronDevtools).to(ElectronDevtools);
|
||||
container.bind<TypeORM>(TYPES.TypeORM).to(TypeORM);
|
||||
container.bind<ElectronUpdater>(TYPES.ElectronUpdater).to(ElectronUpdater);
|
||||
container.bind<ElectronLogger>(TYPES.ElectronLogger).to(ElectronLogger);
|
||||
container.bind<ElectronStore>(TYPES.ElectronStore).to(ElectronStore);
|
||||
|
||||
// core
|
||||
container.bind<ProtocolService>(TYPES.ProtocolService).to(ProtocolService);
|
||||
container.bind<IpcHandler>(TYPES.IpcHandlerService).to(IpcHandler);
|
||||
|
||||
export { container };
|
||||
|
||||
Vendored
+14
-1
@@ -1,5 +1,6 @@
|
||||
import { Rectangle } from "electron";
|
||||
import { type DownloadType } from "interfaces";
|
||||
import { AppLanguage, AppTheme } from "./types";
|
||||
|
||||
declare interface EnvPath {
|
||||
binPath: string;
|
||||
@@ -38,13 +39,25 @@ declare interface AppStore {
|
||||
browserBounds?: Rectangle;
|
||||
blockAds: boolean;
|
||||
// 主题
|
||||
theme: "system" | "light" | "dark";
|
||||
theme: AppTheme;
|
||||
// 使用浏览器插件
|
||||
useExtension: boolean;
|
||||
// 是否使用手机UA
|
||||
isMobile: boolean;
|
||||
// 最大同时下载数
|
||||
maxRunner: number;
|
||||
// 语言
|
||||
language: AppLanguage;
|
||||
// 是否显示终端
|
||||
showTerminal: boolean;
|
||||
// 隐私模式
|
||||
privacy: boolean;
|
||||
// 机器id
|
||||
machineId: string;
|
||||
// 下载代理设置
|
||||
downloadProxySwitch: boolean;
|
||||
// 自动更新
|
||||
autoUpgrade: boolean;
|
||||
}
|
||||
|
||||
declare interface BrowserStore {
|
||||
|
||||
+156
-66
@@ -1,12 +1,16 @@
|
||||
import { contextBridge, ipcRenderer, shell } from "electron";
|
||||
import { AppStore, BrowserStore, EnvPath } from "./main";
|
||||
import { type Favorite } from "./entity/Favorite";
|
||||
import { contextBridge, ipcRenderer } from "electron/renderer";
|
||||
import { shell } from "electron/common";
|
||||
import { AppStore, BrowserStore, EnvPath } from "./main.ts";
|
||||
import { type Favorite } from "./entity/Favorite.ts";
|
||||
import {
|
||||
ConversionPagination,
|
||||
ConversionResponse,
|
||||
VideoResponse,
|
||||
type DownloadItem,
|
||||
type DownloadItemPagination,
|
||||
} from "./interfaces";
|
||||
import { Video } from "./entity/Video";
|
||||
} from "./interfaces.ts";
|
||||
import { Video } from "./entity/Video.ts";
|
||||
import { Conversion } from "./entity/Conversion.ts";
|
||||
|
||||
const apiFunctions: Record<string, any> = {};
|
||||
|
||||
@@ -15,77 +19,163 @@ const apiKey = "electron";
|
||||
export type ElectronApi = typeof electronApi;
|
||||
|
||||
const electronApi = {
|
||||
getEnvPath: (): Promise<EnvPath> => ipcRenderer.invoke("get-env-path"),
|
||||
getFavorites: (): Promise<Favorite[]> => ipcRenderer.invoke("get-favorites"),
|
||||
addFavorite: (
|
||||
favorite: Omit<Favorite, "id" | "createdDate" | "updatedDate">,
|
||||
): Promise<Favorite> => ipcRenderer.invoke("add-favorite", favorite),
|
||||
removeFavorite: (id: number): Promise<void> =>
|
||||
ipcRenderer.invoke("remove-favorite", id),
|
||||
setWebviewBounds: (rect: Electron.Rectangle): Promise<void> =>
|
||||
ipcRenderer.invoke("set-webview-bounds", rect),
|
||||
webviewGoBack: (): Promise<boolean> => ipcRenderer.invoke("webview-go-back"),
|
||||
webviewReload: (): Promise<void> => ipcRenderer.invoke("webview-reload"),
|
||||
webviewLoadURL: (url?: string): Promise<void> =>
|
||||
ipcRenderer.invoke("webview-load-url", url),
|
||||
webviewGoHome: (): Promise<void> => ipcRenderer.invoke("webview-go-home"),
|
||||
getAppStore: (): Promise<AppStore> => ipcRenderer.invoke("get-app-store"),
|
||||
onSelectDownloadDir: (): Promise<string> =>
|
||||
ipcRenderer.invoke("select-download-dir"),
|
||||
setAppStore: (
|
||||
getEnvPath(): Promise<EnvPath> {
|
||||
return ipcRenderer.invoke("get-env-path");
|
||||
},
|
||||
getFavorites(): Promise<Favorite[]> {
|
||||
return ipcRenderer.invoke("get-favorites");
|
||||
},
|
||||
addFavorite(
|
||||
favorite: Omit<Favorite, "id" | "createdDate" | "updatedDate">
|
||||
): Promise<Favorite> {
|
||||
return ipcRenderer.invoke("add-favorite", favorite);
|
||||
},
|
||||
removeFavorite(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("remove-favorite", id);
|
||||
},
|
||||
setWebviewBounds(rect: Electron.Rectangle): Promise<void> {
|
||||
return ipcRenderer.invoke("set-webview-bounds", rect);
|
||||
},
|
||||
webviewGoBack(): Promise<boolean> {
|
||||
return ipcRenderer.invoke("webview-go-back");
|
||||
},
|
||||
webviewReload(): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-reload");
|
||||
},
|
||||
webviewLoadURL(url?: string): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-load-url", url);
|
||||
},
|
||||
webviewGoHome(): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-go-home");
|
||||
},
|
||||
getAppStore(): Promise<AppStore> {
|
||||
return ipcRenderer.invoke("get-app-store");
|
||||
},
|
||||
onSelectDownloadDir(): Promise<string> {
|
||||
return ipcRenderer.invoke("select-download-dir");
|
||||
},
|
||||
setAppStore(
|
||||
key: keyof AppStore,
|
||||
val: AppStore[keyof AppStore],
|
||||
): Promise<void> => ipcRenderer.invoke("set-app-store", key, val),
|
||||
openDir: (dir: string): Promise<void> => ipcRenderer.invoke("open-dir", dir),
|
||||
addDownloadItem: (video: Omit<DownloadItem, "id">): Promise<Video> =>
|
||||
ipcRenderer.invoke("add-download-item", video),
|
||||
addDownloadItems: (videos: DownloadItem[]): Promise<Video[]> =>
|
||||
ipcRenderer.invoke("add-download-items", videos),
|
||||
getDownloadItems: (p: DownloadItemPagination): Promise<VideoResponse> =>
|
||||
ipcRenderer.invoke("get-download-items", p),
|
||||
startDownload: (vid: number): Promise<void> =>
|
||||
ipcRenderer.invoke("start-download", vid),
|
||||
openUrl: (url: string): Promise<void> => ipcRenderer.invoke("open-url", url),
|
||||
stopDownload: (id: number): Promise<void> =>
|
||||
ipcRenderer.invoke("stop-download", id),
|
||||
onDownloadListContextMenu: (id: number): Promise<void> =>
|
||||
ipcRenderer.invoke("on-download-list-context-menu", id),
|
||||
onFavoriteItemContextMenu: (id: number): Promise<void> =>
|
||||
ipcRenderer.invoke("on-favorite-item-context-menu", id),
|
||||
deleteDownloadItem: (id: number): Promise<void> =>
|
||||
ipcRenderer.invoke("delete-download-item", id),
|
||||
convertToAudio: (id: number): Promise<void> =>
|
||||
ipcRenderer.invoke("convert-to-audio", id),
|
||||
rendererEvent: (channel: string, funcId: string, listener: any): void => {
|
||||
val: AppStore[keyof AppStore]
|
||||
): Promise<void> {
|
||||
return ipcRenderer.invoke("set-app-store", key, val);
|
||||
},
|
||||
async openDir(dir?: string): Promise<void> {
|
||||
if (!dir) return;
|
||||
return ipcRenderer.invoke("open-dir", dir);
|
||||
},
|
||||
addDownloadItem(video: Omit<DownloadItem, "id">): Promise<Video> {
|
||||
return ipcRenderer.invoke("add-download-item", video);
|
||||
},
|
||||
addDownloadItems(videos: Omit<DownloadItem, "id">[]): Promise<Video[]> {
|
||||
return ipcRenderer.invoke("add-download-items", videos);
|
||||
},
|
||||
getDownloadItems(p: DownloadItemPagination): Promise<VideoResponse> {
|
||||
return ipcRenderer.invoke("get-download-items", p);
|
||||
},
|
||||
startDownload(vid: number): Promise<void> {
|
||||
return ipcRenderer.invoke("start-download", vid);
|
||||
},
|
||||
openUrl(url: string): Promise<void> {
|
||||
return ipcRenderer.invoke("open-url", url);
|
||||
},
|
||||
stopDownload(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("stop-download", id);
|
||||
},
|
||||
onDownloadListContextMenu(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("on-download-list-context-menu", id);
|
||||
},
|
||||
onFavoriteItemContextMenu(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("on-favorite-item-context-menu", id);
|
||||
},
|
||||
deleteDownloadItem(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("delete-download-item", id);
|
||||
},
|
||||
convertToAudio(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("convert-to-audio", id);
|
||||
},
|
||||
rendererEvent(channel: string, funcId: string, listener: any): void {
|
||||
const key = `${channel}-${funcId}`;
|
||||
apiFunctions[key] = listener;
|
||||
ipcRenderer.on(channel, listener);
|
||||
},
|
||||
removeEventListener: (channel: string, funcId: string): void => {
|
||||
removeEventListener(channel: string, funcId: string): void {
|
||||
const key = `${channel}-${funcId}`;
|
||||
const fun = apiFunctions[key];
|
||||
ipcRenderer.removeListener(channel, fun);
|
||||
delete apiFunctions[key];
|
||||
},
|
||||
showBrowserWindow: (): Promise<void> =>
|
||||
ipcRenderer.invoke("show-browser-window"),
|
||||
webviewHide: (): Promise<void> => ipcRenderer.invoke("webview-hide"),
|
||||
webviewShow: (): Promise<void> => ipcRenderer.invoke("webview-show"),
|
||||
downloadNow: (video: DownloadItem): Promise<void> =>
|
||||
ipcRenderer.invoke("download-now", video),
|
||||
combineToHomePage: (store: BrowserStore): Promise<void> =>
|
||||
ipcRenderer.invoke("combine-to-home-page", store),
|
||||
editDownloadItem: (video: DownloadItem): Promise<void> =>
|
||||
ipcRenderer.invoke("edit-download-item", video),
|
||||
openPlayerWindow: (videoId: string): Promise<void> =>
|
||||
ipcRenderer.invoke("open-player-window", videoId),
|
||||
getLocalIP: (): Promise<string> => ipcRenderer.invoke("get-local-ip"),
|
||||
openBrowser: (url: string): Promise<void> => shell.openExternal(url),
|
||||
getSharedState: (): Promise<any> => ipcRenderer.invoke("get-shared-state"),
|
||||
setSharedState: (state: any): Promise<void> =>
|
||||
ipcRenderer.invoke("set-shared-state", state),
|
||||
setUserAgent: (isMobile: boolean): Promise<void> =>
|
||||
ipcRenderer.invoke("webview-change-user-agent", isMobile),
|
||||
showBrowserWindow(): Promise<void> {
|
||||
return ipcRenderer.invoke("show-browser-window");
|
||||
},
|
||||
webviewHide(): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-hide");
|
||||
},
|
||||
webviewShow(): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-show");
|
||||
},
|
||||
webviewUrlContextMenu(): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-url-contextmenu");
|
||||
},
|
||||
downloadNow(video: Omit<DownloadItem, "id">): Promise<void> {
|
||||
return ipcRenderer.invoke("download-now", video);
|
||||
},
|
||||
downloadItemsNow(videos: Omit<DownloadItem, "id">[]): Promise<void> {
|
||||
return ipcRenderer.invoke("download-items-now", videos);
|
||||
},
|
||||
editDownloadNow(video: DownloadItem): Promise<void> {
|
||||
return ipcRenderer.invoke("edit-download-now", video);
|
||||
},
|
||||
combineToHomePage(store: BrowserStore): Promise<void> {
|
||||
return ipcRenderer.invoke("combine-to-home-page", store);
|
||||
},
|
||||
editDownloadItem(video: DownloadItem): Promise<void> {
|
||||
return ipcRenderer.invoke("edit-download-item", video);
|
||||
},
|
||||
getLocalIP(): Promise<string> {
|
||||
return ipcRenderer.invoke("get-local-ip");
|
||||
},
|
||||
openBrowser(url: string): Promise<void> {
|
||||
return shell.openExternal(url);
|
||||
},
|
||||
selectFile(): Promise<string> {
|
||||
return ipcRenderer.invoke("select-file");
|
||||
},
|
||||
getSharedState(): Promise<any> {
|
||||
return ipcRenderer.invoke("get-shared-state");
|
||||
},
|
||||
setSharedState(state: any): Promise<void> {
|
||||
return ipcRenderer.invoke("set-shared-state", state);
|
||||
},
|
||||
setUserAgent(isMobile: boolean): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-change-user-agent", isMobile);
|
||||
},
|
||||
getDownloadLog(id: number): Promise<string> {
|
||||
return ipcRenderer.invoke("get-download-log", id);
|
||||
},
|
||||
showDownloadDialog(data: Omit<DownloadItem, "id">[]) {
|
||||
return ipcRenderer.invoke("show-download-dialog", data);
|
||||
},
|
||||
pluginReady() {
|
||||
return ipcRenderer.invoke("plugin-ready");
|
||||
},
|
||||
getConversions(
|
||||
pagination: ConversionPagination
|
||||
): Promise<ConversionResponse> {
|
||||
return ipcRenderer.invoke("get-conversions", pagination);
|
||||
},
|
||||
addConversion(conversion: Omit<Conversion, "id">): Promise<Conversion> {
|
||||
return ipcRenderer.invoke("add-conversion", conversion);
|
||||
},
|
||||
deleteConversion(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("delete-conversion", id);
|
||||
},
|
||||
getMachineId(): Promise<string> {
|
||||
return ipcRenderer.invoke("get-machine-id");
|
||||
},
|
||||
clearWebviewCache(): Promise<void> {
|
||||
return ipcRenderer.invoke("clear-webview-cache");
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld(apiKey, electronApi);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { ConversionPagination } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
import { Conversion } from "../entity/Conversion.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ConversionRepository {
|
||||
constructor(
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM
|
||||
) {}
|
||||
|
||||
async findConversion(id: number) {
|
||||
const repository = this.db.appDataSource.getRepository(Conversion);
|
||||
const conversion = await repository.findOneBy({ id });
|
||||
|
||||
if (!conversion) {
|
||||
throw new Error("没有找到该转换任务");
|
||||
}
|
||||
|
||||
return conversion;
|
||||
}
|
||||
|
||||
async getConversions(pagination: ConversionPagination) {
|
||||
const { current = 0, pageSize = 50 } = pagination;
|
||||
|
||||
const [items, count] = await this.db.appDataSource
|
||||
.getRepository(Conversion)
|
||||
.findAndCount({
|
||||
order: {
|
||||
createdDate: "ASC",
|
||||
},
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
return {
|
||||
total: count,
|
||||
list: items,
|
||||
};
|
||||
}
|
||||
|
||||
async addConversion(conversion: Conversion) {
|
||||
await this.db.appDataSource.getRepository(Conversion).save(conversion);
|
||||
}
|
||||
|
||||
async deleteConversion(id: number) {
|
||||
await this.db.appDataSource.getRepository(Conversion).delete({ id });
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import { Favorite } from "../entity/Favorite";
|
||||
import DatabaseService from "../services/DatabaseService";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
|
||||
@injectable()
|
||||
export default class FavoriteRepository {
|
||||
constructor(
|
||||
@inject(TYPES.DatabaseService)
|
||||
private readonly dataService: DatabaseService,
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM,
|
||||
) {}
|
||||
|
||||
async findFavorites(): Promise<Favorite[]> {
|
||||
return await this.dataService.manager.find(Favorite, {
|
||||
return await this.db.manager.find(Favorite, {
|
||||
order: {
|
||||
createdDate: "desc",
|
||||
},
|
||||
@@ -19,7 +19,7 @@ export default class FavoriteRepository {
|
||||
}
|
||||
|
||||
async addFavorite(favorite: Favorite): Promise<Favorite> {
|
||||
const exist = await this.dataService.manager.findOne(Favorite, {
|
||||
const exist = await this.db.manager.findOne(Favorite, {
|
||||
where: {
|
||||
url: favorite.url,
|
||||
},
|
||||
@@ -33,10 +33,10 @@ export default class FavoriteRepository {
|
||||
item.title = favorite.title;
|
||||
item.url = favorite.url;
|
||||
item.icon = favorite.icon;
|
||||
return await this.dataService.manager.save(item);
|
||||
return await this.db.manager.save(item);
|
||||
}
|
||||
|
||||
async removeFavorite(id: number): Promise<void> {
|
||||
await this.dataService.manager.getRepository(Favorite).delete(id);
|
||||
await this.db.manager.getRepository(Favorite).delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
import { Video } from "../entity/Video";
|
||||
import { Video } from "../entity/Video.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import {
|
||||
DownloadFilter,
|
||||
DownloadItem,
|
||||
DownloadItemPagination,
|
||||
DownloadStatus,
|
||||
} from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { In, Not } from "typeorm";
|
||||
import DatabaseService from "../services/DatabaseService";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
|
||||
@injectable()
|
||||
export default class VideoRepository {
|
||||
constructor(
|
||||
@inject(TYPES.DatabaseService)
|
||||
private readonly dataService: DatabaseService,
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM
|
||||
) {}
|
||||
|
||||
async addVideo(video: Omit<DownloadItem, "id">) {
|
||||
// 先判断有没有同名的视频
|
||||
const exist = await this.findVideoByName(video.name);
|
||||
if (exist) {
|
||||
throw new Error("视频名称已存在,请更换视频名称");
|
||||
}
|
||||
const item = new Video();
|
||||
item.name = video.name;
|
||||
item.url = video.url;
|
||||
item.type = video.type;
|
||||
video.headers && (item.headers = video.headers);
|
||||
return await this.dataService.manager.save(item);
|
||||
return await this.db.manager.save(item);
|
||||
}
|
||||
|
||||
async addVideos(videos: DownloadItem[]) {
|
||||
async addVideos(videos: Omit<DownloadItem, "id">[]) {
|
||||
const items = videos.map((video) => {
|
||||
const item = new Video();
|
||||
item.name = video.name;
|
||||
@@ -35,12 +40,12 @@ export default class VideoRepository {
|
||||
video.headers && (item.headers = video.headers);
|
||||
return item;
|
||||
});
|
||||
return await this.dataService.manager.save(items);
|
||||
return await this.db.manager.save(items);
|
||||
}
|
||||
|
||||
// 编辑视频
|
||||
async editVideo(video: DownloadItem) {
|
||||
const item = await this.dataService.appDataSource
|
||||
const item = await this.db.appDataSource
|
||||
.getRepository(Video)
|
||||
.findOneBy({ id: video.id });
|
||||
if (!item) {
|
||||
@@ -49,12 +54,12 @@ export default class VideoRepository {
|
||||
item.name = video.name;
|
||||
item.url = video.url;
|
||||
video.headers && (item.headers = video.headers);
|
||||
return await this.dataService.manager.save(item);
|
||||
return await this.db.manager.save(item);
|
||||
}
|
||||
|
||||
// 查找所有视频
|
||||
async findAllVideos() {
|
||||
return await this.dataService.appDataSource.getRepository(Video).find({
|
||||
return await this.db.appDataSource.getRepository(Video).find({
|
||||
order: {
|
||||
createdDate: "DESC",
|
||||
},
|
||||
@@ -72,14 +77,14 @@ export default class VideoRepository {
|
||||
? DownloadStatus.Success
|
||||
: Not(DownloadStatus.Success);
|
||||
|
||||
const [items, count] = await this.dataService.appDataSource
|
||||
const [items, count] = await this.db.appDataSource
|
||||
.getRepository(Video)
|
||||
.findAndCount({
|
||||
where: {
|
||||
status: filterCondition,
|
||||
},
|
||||
order: {
|
||||
createdDate: "DESC",
|
||||
createdDate: "ASC",
|
||||
},
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
@@ -91,39 +96,40 @@ export default class VideoRepository {
|
||||
}
|
||||
|
||||
async findVideo(id: number) {
|
||||
return this.dataService.appDataSource.getRepository(Video).findOneBy({
|
||||
id,
|
||||
});
|
||||
const repository = this.db.appDataSource.getRepository(Video);
|
||||
const video = await repository.findOneBy({ id });
|
||||
|
||||
if (!video) {
|
||||
throw new Error("没有找到该视频");
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
async findVideoByName(name: string) {
|
||||
return this.dataService.appDataSource.getRepository(Video).findOneBy({
|
||||
return this.db.appDataSource.getRepository(Video).findOneBy({
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
async changeVideoStatus(id: number | number[], status: DownloadStatus) {
|
||||
const ids = !Array.isArray(id) ? [id] : id;
|
||||
return this.dataService.appDataSource
|
||||
.createQueryBuilder()
|
||||
.update(Video)
|
||||
.set({ status })
|
||||
.where({ id: In(ids) })
|
||||
.execute();
|
||||
const videoRepository = this.db.appDataSource.getRepository(Video);
|
||||
const videos = await videoRepository.findBy({ id: In(ids) });
|
||||
for (const video of videos) {
|
||||
video.status = status;
|
||||
}
|
||||
await videoRepository.save(videos);
|
||||
}
|
||||
|
||||
async changeVideoIsLive(id: number | number[], isLive: boolean) {
|
||||
const ids = !Array.isArray(id) ? [id] : id;
|
||||
return this.dataService.appDataSource
|
||||
.createQueryBuilder()
|
||||
.update(Video)
|
||||
.set({ isLive })
|
||||
.where({ id: In(ids) })
|
||||
.execute();
|
||||
async changeVideoIsLive(id: number) {
|
||||
const video = await this.findVideo(id);
|
||||
video.isLive = true;
|
||||
return this.db.manager.save(video);
|
||||
}
|
||||
|
||||
async findWattingAndDownloadingVideos() {
|
||||
return await this.dataService.appDataSource.getRepository(Video).find({
|
||||
return await this.db.appDataSource.getRepository(Video).find({
|
||||
where: {
|
||||
status: In([DownloadStatus.Downloading, DownloadStatus.Watting]),
|
||||
},
|
||||
@@ -131,12 +137,23 @@ export default class VideoRepository {
|
||||
}
|
||||
|
||||
async deleteDownloadItem(id: number) {
|
||||
return await this.dataService.appDataSource.getRepository(Video).delete(id);
|
||||
return await this.db.appDataSource.getRepository(Video).delete(id);
|
||||
}
|
||||
|
||||
async findVideoByUrl(url: string) {
|
||||
return this.dataService.appDataSource.getRepository(Video).findOneBy({
|
||||
return this.db.appDataSource.getRepository(Video).findOneBy({
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
async appendDownloadLog(id: number, message: string) {
|
||||
const video = await this.findVideo(id);
|
||||
video.log = video.log ? `${video.log}\n${message}` : message;
|
||||
return await this.db.manager.save(video);
|
||||
}
|
||||
|
||||
async getDownloadLog(id: number) {
|
||||
const video = await this.findVideo(id);
|
||||
return video.log;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,108 @@ import {
|
||||
DownloadProgress,
|
||||
DownloadStatus,
|
||||
Task,
|
||||
} from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import LoggerServiceImpl from "./LoggerService";
|
||||
import StoreService from "./StoreService";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import {
|
||||
Platform,
|
||||
biliDownloaderBin,
|
||||
m3u8DownloaderBin,
|
||||
} from "../helper/index.ts";
|
||||
import * as pty from "node-pty";
|
||||
import stripAnsi from "strip-ansi";
|
||||
|
||||
interface DownloadContext {
|
||||
// 是否为直播
|
||||
isLive: boolean;
|
||||
// 下载进度
|
||||
percent: string;
|
||||
// 下载速度
|
||||
speed: string;
|
||||
// 是否已经 ready
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadOptions {
|
||||
abortSignal: AbortController;
|
||||
encoding?: string;
|
||||
onMessage?: (ctx: DownloadContext, message: string) => void;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Schema {
|
||||
args: Record<string, { argsName: string[] | null }>;
|
||||
consoleReg: {
|
||||
percent: string;
|
||||
speed: string;
|
||||
error: string;
|
||||
start: string;
|
||||
isLive: string;
|
||||
};
|
||||
bin: string;
|
||||
platform: string[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
const processList: Schema[] = [
|
||||
{
|
||||
type: "m3u8",
|
||||
platform: [Platform.MacOS, Platform.Linux, Platform.Windows],
|
||||
bin: m3u8DownloaderBin,
|
||||
args: {
|
||||
url: {
|
||||
argsName: null,
|
||||
},
|
||||
localDir: {
|
||||
argsName: ["--tmp-dir", "--save-dir"],
|
||||
},
|
||||
name: {
|
||||
argsName: ["--save-name"],
|
||||
},
|
||||
// headers: {
|
||||
// argsName: ["--headers"],
|
||||
// },
|
||||
deleteSegments: {
|
||||
argsName: ["--del-after-done"],
|
||||
},
|
||||
proxy: {
|
||||
argsName: ["--custom-proxy"],
|
||||
},
|
||||
__common__: {
|
||||
argsName: ["--no-log"],
|
||||
},
|
||||
},
|
||||
consoleReg: {
|
||||
percent: "([\\d.]+)%",
|
||||
speed: "([\\d.]+[GMK]Bps)",
|
||||
error: "ERROR",
|
||||
start: "保存文件名:",
|
||||
isLive: "检测到直播流",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "bilibili",
|
||||
platform: [Platform.Linux, Platform.MacOS, Platform.Windows],
|
||||
bin: biliDownloaderBin,
|
||||
args: {
|
||||
url: {
|
||||
argsName: null,
|
||||
},
|
||||
localDir: {
|
||||
argsName: ["--work-dir"],
|
||||
},
|
||||
},
|
||||
consoleReg: {
|
||||
speed: "([\\d.]+\\s[GMK]B/s)",
|
||||
percent: "([\\d.]+)%",
|
||||
error: "ERROR",
|
||||
start: "开始下载",
|
||||
isLive: "检测到直播流",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@injectable()
|
||||
export default class DownloadService extends EventEmitter {
|
||||
@@ -19,17 +116,15 @@ export default class DownloadService extends EventEmitter {
|
||||
|
||||
private limit: number;
|
||||
|
||||
private debug = process.env.APP_DOWNLOAD_DEBUG;
|
||||
|
||||
private signal: Record<number, AbortController> = {};
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerServiceImpl,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly storeService: ElectronStore
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -48,7 +143,6 @@ export default class DownloadService extends EventEmitter {
|
||||
|
||||
async stopTask(id: number) {
|
||||
if (this.signal[id]) {
|
||||
this.log(`taskId: ${id} stop`);
|
||||
this.signal[id].abort();
|
||||
}
|
||||
}
|
||||
@@ -57,11 +151,11 @@ export default class DownloadService extends EventEmitter {
|
||||
try {
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Downloading,
|
||||
DownloadStatus.Downloading
|
||||
);
|
||||
this.emit("download-start", task.id);
|
||||
|
||||
this.log(`taskId: ${task.id} start`);
|
||||
this.logger.info(`taskId: ${task.id} start`);
|
||||
const controller = new AbortController();
|
||||
this.signal[task.id] = controller;
|
||||
|
||||
@@ -83,34 +177,35 @@ export default class DownloadService extends EventEmitter {
|
||||
callback,
|
||||
};
|
||||
|
||||
const { proxy, useProxy } = this.storeService.store;
|
||||
if (useProxy) {
|
||||
const { proxy, downloadProxySwitch } = this.storeService.store;
|
||||
if (downloadProxySwitch && proxy) {
|
||||
params.proxy = proxy;
|
||||
}
|
||||
|
||||
await task.process(params);
|
||||
await this.process(params);
|
||||
delete this.signal[task.id];
|
||||
this.log(`taskId: ${task.id} success`);
|
||||
this.logger.info(`taskId: ${task.id} success`);
|
||||
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Success,
|
||||
DownloadStatus.Success
|
||||
);
|
||||
this.emit("download-success", task.id);
|
||||
} catch (err: any) {
|
||||
this.log(`taskId: ${task.id} failed`);
|
||||
if (err.name === "AbortError") {
|
||||
if (err.message === "AbortError") {
|
||||
this.logger.info(`taskId: ${task.id} stopped`);
|
||||
// 下载暂停
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Stopped,
|
||||
DownloadStatus.Stopped
|
||||
);
|
||||
this.emit("download-stop", task.id);
|
||||
} else {
|
||||
this.logger.info(`taskId: ${task.id} failed`);
|
||||
// 下载失败
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Failed,
|
||||
DownloadStatus.Failed
|
||||
);
|
||||
this.emit("download-failed", task.id, err);
|
||||
}
|
||||
@@ -144,9 +239,170 @@ export default class DownloadService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
log(...args: unknown[]) {
|
||||
if (this.debug) {
|
||||
this.logger.info(`[DownloadService] `, ...args);
|
||||
private _execa(
|
||||
binPath: string,
|
||||
args: string[],
|
||||
params: DownloadOptions
|
||||
): Promise<void> {
|
||||
const { abortSignal, onMessage, id } = params;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const ptyProcess = pty.spawn(binPath, args, {
|
||||
name: "xterm-color",
|
||||
cols: 500,
|
||||
rows: 500,
|
||||
useConpty: false,
|
||||
});
|
||||
|
||||
if (onMessage) {
|
||||
const ctx: DownloadContext = {
|
||||
ready: false,
|
||||
isLive: false,
|
||||
percent: "",
|
||||
speed: "",
|
||||
};
|
||||
ptyProcess.onData((data) => {
|
||||
try {
|
||||
this.emit("download-message", id, data);
|
||||
onMessage(ctx, stripAnsi(data));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
abortSignal.signal.addEventListener("abort", () => {
|
||||
ptyProcess.kill();
|
||||
reject(new Error("AbortError"));
|
||||
});
|
||||
|
||||
ptyProcess.onExit(({ exitCode }) => {
|
||||
if (exitCode === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("未知错误"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async downloader(params: DownloadParams, schema: Schema): Promise<void> {
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
proxy,
|
||||
} = params;
|
||||
|
||||
const spawnParams = [];
|
||||
for (const key of Object.keys(schema.args)) {
|
||||
const { argsName } = schema.args[key];
|
||||
if (key === "url") {
|
||||
argsName && spawnParams.push(...argsName);
|
||||
spawnParams.push(url);
|
||||
}
|
||||
if (key === "localDir") {
|
||||
argsName && argsName.forEach((i) => spawnParams.push(i, local));
|
||||
}
|
||||
if (key === "name") {
|
||||
argsName && argsName.forEach((i) => spawnParams.push(i, name));
|
||||
}
|
||||
|
||||
if (key === "headers") {
|
||||
if (headers) {
|
||||
const h: Record<string, unknown> = JSON.parse(headers);
|
||||
Object.entries(h).forEach(([k, v]) => {
|
||||
spawnParams.push("--header", `${k}: ${v}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "deleteSegments" && deleteSegments) {
|
||||
argsName && spawnParams.push(...argsName);
|
||||
}
|
||||
|
||||
if (key === "proxy" && proxy) {
|
||||
argsName && argsName.forEach((i) => spawnParams.push(i, proxy));
|
||||
}
|
||||
|
||||
if (key === "__common__") {
|
||||
argsName && spawnParams.push(...argsName);
|
||||
}
|
||||
}
|
||||
|
||||
const { consoleReg } = schema;
|
||||
const isLiveReg = RegExp(consoleReg.isLive, "g");
|
||||
const startDownloadReg = RegExp(consoleReg.start, "g");
|
||||
const errorReg = RegExp(consoleReg.error, "g");
|
||||
const speedReg = RegExp(consoleReg.speed, "g");
|
||||
const percentReg = RegExp(consoleReg.percent, "g");
|
||||
|
||||
const onMessage = (ctx: DownloadContext, message: string) => {
|
||||
// 解析是否为直播资源
|
||||
if (isLiveReg.test(message)) {
|
||||
ctx.isLive = true;
|
||||
}
|
||||
// 解析下载进度
|
||||
const [, percent] = percentReg.exec(message) || [];
|
||||
if (percent && Number(ctx.percent || 0) < Number(percent)) {
|
||||
ctx.percent = percent;
|
||||
}
|
||||
// 解析下载速度
|
||||
const [, speed] = speedReg.exec(message) || [];
|
||||
if (speed) {
|
||||
ctx.speed = speed;
|
||||
}
|
||||
|
||||
if (startDownloadReg.test(message)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive: ctx.isLive,
|
||||
speed: "",
|
||||
percent: "",
|
||||
});
|
||||
ctx.ready = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorReg.test(message)) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (ctx.ready && (ctx.percent || ctx.speed)) {
|
||||
callback({
|
||||
id,
|
||||
type: "progress",
|
||||
percent: ctx.percent || "",
|
||||
speed: ctx.speed || "",
|
||||
isLive: ctx.isLive,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.logger.debug("download params: ", spawnParams.join(" "));
|
||||
await this._execa(schema.bin, spawnParams, {
|
||||
id,
|
||||
abortSignal,
|
||||
onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
async process(params: DownloadParams): Promise<void> {
|
||||
const program = processList
|
||||
.filter((i) => i.platform.includes(process.platform))
|
||||
.filter((i) => i.type === params.type);
|
||||
|
||||
if (program.length === 0) {
|
||||
return Promise.reject(new Error("不支持的下载类型"));
|
||||
}
|
||||
|
||||
const schema = program[0];
|
||||
await this.downloader(params, schema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { protocol } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { pathExists, readFile } from "fs-extra";
|
||||
import { injectable } from "inversify";
|
||||
import { extname, join } from "path";
|
||||
import { URL } from "url";
|
||||
import { defaultScheme } from "../helper";
|
||||
|
||||
@injectable()
|
||||
export default class ProtocolService {
|
||||
create(): void {
|
||||
if (isDev) return;
|
||||
|
||||
protocol.registerBufferProtocol(
|
||||
defaultScheme,
|
||||
async (request, callback) => {
|
||||
let pathName = new URL(request.url).pathname;
|
||||
pathName = decodeURI(pathName);
|
||||
|
||||
const filePath = join(__dirname, "../renderer", pathName);
|
||||
const fileExist = await pathExists(filePath);
|
||||
|
||||
if (fileExist) {
|
||||
const data = await readFile(filePath);
|
||||
const extension = extname(pathName).toLowerCase();
|
||||
let mimeType = "";
|
||||
|
||||
if (extension === ".js") {
|
||||
mimeType = "text/javascript";
|
||||
} else if (extension === ".html") {
|
||||
mimeType = "text/html";
|
||||
} else if (extension === ".css") {
|
||||
mimeType = "text/css";
|
||||
} else if (extension === ".svg" || extension === ".svgz") {
|
||||
mimeType = "image/svg+xml";
|
||||
} else if (extension === ".json") {
|
||||
mimeType = "application/json";
|
||||
}
|
||||
|
||||
callback({ mimeType, data });
|
||||
} else {
|
||||
// 如果没有找到文件,直接返回 index.html , react history 模式
|
||||
const filePath = join(__dirname, "../renderer/index.html");
|
||||
const data = await readFile(filePath);
|
||||
callback({ mimeType: "text/html", data });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { DownloadType } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import EventEmitter from "events";
|
||||
import { OnSendHeadersListenerDetails, session } from "electron";
|
||||
import {
|
||||
PERSIST_WEBVIEW,
|
||||
PRIVACY_WEBVIEW,
|
||||
formatHeaders,
|
||||
} from "../helper/index.ts";
|
||||
|
||||
export interface SourceParams {
|
||||
url: string;
|
||||
documentURL: string;
|
||||
name: string;
|
||||
type: DownloadType;
|
||||
headers?: string;
|
||||
}
|
||||
|
||||
export interface SourceFilter {
|
||||
hosts?: RegExp[];
|
||||
matches?: RegExp[];
|
||||
type: DownloadType;
|
||||
schema?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface PageInfo {
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const filterList: SourceFilter[] = [
|
||||
{
|
||||
matches: [/\.m3u8/],
|
||||
type: DownloadType.m3u8,
|
||||
},
|
||||
{
|
||||
// TODO: 合集、列表、收藏夹
|
||||
hosts: [/^https?:\/\/(www\.)?bilibili.com\/video/],
|
||||
type: DownloadType.bilibili,
|
||||
schema: {
|
||||
name: "title",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@injectable()
|
||||
export class SniffingHelper extends EventEmitter {
|
||||
private pageInfo: PageInfo = { title: "", url: "" };
|
||||
private ready = false;
|
||||
private queue: SourceParams[] = [];
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
pluginReady() {
|
||||
this.ready = true;
|
||||
this.queue.forEach((item) => {
|
||||
this.emit("source", item);
|
||||
});
|
||||
}
|
||||
|
||||
update(pageInfo: PageInfo) {
|
||||
this.pageInfo = pageInfo;
|
||||
}
|
||||
|
||||
reset(pageInfo: PageInfo) {
|
||||
this.pageInfo = pageInfo;
|
||||
this.ready = false;
|
||||
this.queue = [];
|
||||
|
||||
listLoop: for (const filter of filterList) {
|
||||
if (filter.hosts) {
|
||||
for (const host of filter.hosts) {
|
||||
if (!host.test(pageInfo.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.send({
|
||||
url: pageInfo.url,
|
||||
documentURL: pageInfo.url,
|
||||
name: pageInfo.title,
|
||||
type: filter.type,
|
||||
});
|
||||
break listLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start(privacy: boolean = false) {
|
||||
const partition = privacy ? PRIVACY_WEBVIEW : PERSIST_WEBVIEW;
|
||||
const viewSession = session.fromPartition(partition);
|
||||
viewSession.webRequest.onSendHeaders(this.onSendHeaders);
|
||||
}
|
||||
|
||||
send = (item: SourceParams) => {
|
||||
this.logger.info(`在窗口中捕获视频链接: ${item.url}`);
|
||||
// 等待 DOM 中浮窗加载完成
|
||||
if (this.ready) {
|
||||
this.emit("source", item);
|
||||
} else {
|
||||
this.queue.push(item);
|
||||
}
|
||||
};
|
||||
|
||||
private onSendHeaders = (details: OnSendHeadersListenerDetails): void => {
|
||||
const { url, requestHeaders } = details;
|
||||
const { title, url: documentURL } = this.pageInfo;
|
||||
|
||||
listLoop: for (const filter of filterList) {
|
||||
if (filter.matches) {
|
||||
for (const match of filter.matches) {
|
||||
const u = new URL(url);
|
||||
if (!match.test(u.pathname)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.send({
|
||||
url,
|
||||
documentURL,
|
||||
name: title,
|
||||
type: filter.type,
|
||||
headers: formatHeaders(requestHeaders),
|
||||
});
|
||||
break listLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import isDev from "electron-is-dev";
|
||||
import LoggerService from "./LoggerService";
|
||||
|
||||
@injectable()
|
||||
export default class UpdateService {
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService) private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (isDev) return;
|
||||
|
||||
try {
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
autoUpdater.logger = this.logger.logger;
|
||||
await autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "自动更新完成",
|
||||
body: "下次重启时将会自动安装",
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.info("update error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { execa } from "execa";
|
||||
import { mobilePath, videoServerBin } from "../helper";
|
||||
import { TYPES } from "../types";
|
||||
import StoreService from "./StoreService";
|
||||
|
||||
@injectable()
|
||||
export default class VideoService {
|
||||
constructor(
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
) {}
|
||||
|
||||
init(): void {
|
||||
const local = this.storeService.get("local");
|
||||
// empty
|
||||
const child = execa(videoServerBin, [
|
||||
"--static-path",
|
||||
mobilePath,
|
||||
"--video-path",
|
||||
local,
|
||||
"--port",
|
||||
process.env.APP_SERVER_PORT || "",
|
||||
]);
|
||||
|
||||
child.stdout?.pipe(process.stdout);
|
||||
child.stderr?.pipe(process.stderr);
|
||||
}
|
||||
}
|
||||
@@ -1,308 +1,234 @@
|
||||
import { BrowserView, session } from "electron";
|
||||
import { DownloadType } from "../interfaces";
|
||||
import { Event, HandlerDetails, WebContentsView, session } from "electron";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import { TYPES } from "../types.ts";
|
||||
import isDev from "electron-is-dev";
|
||||
import {
|
||||
PERSIST_WEBVIEW,
|
||||
PRIVACY_WEBVIEW,
|
||||
fetch,
|
||||
mobileUA,
|
||||
pcUA,
|
||||
pluginPath,
|
||||
pluginStylePath,
|
||||
} from "../helper";
|
||||
} from "../helper/index.ts";
|
||||
import { ElectronBlocker } from "@cliqz/adblocker-electron";
|
||||
import fetch from "cross-fetch";
|
||||
import { WebSource } from "../main";
|
||||
import { load } from "cheerio";
|
||||
import LoggerService from "./LoggerService";
|
||||
import StoreService from "./StoreService";
|
||||
import MainWindow from "../windows/MainWindow";
|
||||
import BrowserWindow from "../windows/BrowserWindow";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
import BrowserWindow from "../windows/BrowserWindow.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import { SniffingHelper, SourceParams } from "./SniffingHelperService.ts";
|
||||
import { resolve } from "path";
|
||||
import { readFileSync } from "fs-extra";
|
||||
|
||||
interface SourceParams {
|
||||
url: string;
|
||||
requestId: string;
|
||||
headers: Record<string, any>;
|
||||
filter: SourceFilter;
|
||||
documentURL: string;
|
||||
}
|
||||
|
||||
interface SourceFilter {
|
||||
matches: RegExp[];
|
||||
type: DownloadType;
|
||||
handler: (this: WebviewService, params: SourceParams) => Promise<WebSource>;
|
||||
}
|
||||
|
||||
const filterList: SourceFilter[] = [
|
||||
{
|
||||
matches: [/\.m3u8/],
|
||||
type: DownloadType.m3u8,
|
||||
async handler(params) {
|
||||
const { url, headers } = params;
|
||||
const webContents = this.view.webContents;
|
||||
const title = webContents.getTitle();
|
||||
|
||||
return {
|
||||
url,
|
||||
type: DownloadType.m3u8,
|
||||
name: title || "没有获取到名称",
|
||||
headers: JSON.stringify(headers),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: 合集、列表、收藏夹
|
||||
matches: [/^https?:\/\/(www\.)?bilibili.com\/video/],
|
||||
type: DownloadType.bilibili,
|
||||
async handler(params) {
|
||||
const { url, requestId } = params;
|
||||
const response = await this.debugger.sendCommand(
|
||||
"Network.getResponseBody",
|
||||
{
|
||||
requestId,
|
||||
},
|
||||
);
|
||||
const $ = load(response.body);
|
||||
const title = $("title").text();
|
||||
|
||||
return {
|
||||
url,
|
||||
type: DownloadType.bilibili,
|
||||
name: title || "没有获取到名称",
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const styleText = readFileSync(pluginStylePath, "utf-8");
|
||||
|
||||
// FIXME: 需要重构
|
||||
@injectable()
|
||||
export default class WebviewService {
|
||||
public view: BrowserView;
|
||||
private view: WebContentsView | null = null;
|
||||
private blocker?: ElectronBlocker;
|
||||
private pageSources = new Set();
|
||||
requestMap: Record<string, SourceParams> = {};
|
||||
responseMap: Record<string, string[]> = {};
|
||||
private defaultSession: string;
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.MainWindow)
|
||||
private readonly mainWindow: MainWindow,
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
@inject(TYPES.BrowserWindow)
|
||||
private readonly browserWindow: BrowserWindow,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.SniffingHelper)
|
||||
private readonly sniffingHelper: SniffingHelper
|
||||
) {
|
||||
// 初始化 blocker
|
||||
this.initBlocker();
|
||||
|
||||
const { useProxy, proxy, privacy } = this.store.store;
|
||||
|
||||
this.sniffingHelper.start(privacy);
|
||||
this.sniffingHelper.on("source", this.onSource);
|
||||
|
||||
this.setDefaultSession(privacy, true);
|
||||
this.setProxy(useProxy, proxy);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.view = new BrowserView({
|
||||
this.view = new WebContentsView({
|
||||
webPreferences: {
|
||||
partition: PERSIST_WEBVIEW,
|
||||
preload: pluginPath,
|
||||
partition: this.defaultSession,
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
});
|
||||
this.view.setBackgroundColor("#fff");
|
||||
this.view.webContents.setAudioMuted(true);
|
||||
|
||||
const { useProxy, proxy, isMobile } = this.storeService.store;
|
||||
this.setProxy(useProxy, proxy);
|
||||
const { isMobile } = this.store.store;
|
||||
this.setUserAgent(isMobile);
|
||||
|
||||
this.view.webContents.on("dom-ready", async () => {
|
||||
this.pageSources.clear();
|
||||
this.view.webContents.insertCSS(styleText);
|
||||
const title = this.view.webContents.getTitle();
|
||||
const url = this.view.webContents.getURL();
|
||||
this.curWindow?.webContents.send("webview-dom-ready", { title, url });
|
||||
});
|
||||
this.view.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url === "about:blank") {
|
||||
// 兼容一些网站跳转到 about:blank
|
||||
this.view.webContents.once("will-redirect", async (event, url) => {
|
||||
this.loadURL(url, true);
|
||||
});
|
||||
} else {
|
||||
this.loadURL(url, true);
|
||||
}
|
||||
this.view.webContents.on("dom-ready", this.onDomReady);
|
||||
this.view.webContents.on("did-navigate", this.onDidNavigate);
|
||||
this.view.webContents.on("did-fail-load", this.onDidFailLoad);
|
||||
this.view.webContents.on("did-navigate-in-page", this.onDidNavigateInPage);
|
||||
this.view.webContents.setWindowOpenHandler(this.onOpenNewWindow);
|
||||
}
|
||||
|
||||
return { action: "deny" };
|
||||
});
|
||||
onDomReady = () => {
|
||||
if (!this.view) return;
|
||||
const pageInfo = this.getPageInfo();
|
||||
this.sniffingHelper.update(pageInfo);
|
||||
this.window.webContents.send("webview-dom-ready", pageInfo);
|
||||
};
|
||||
|
||||
onDidNavigate = async () => {
|
||||
if (!this.view) return;
|
||||
const pageInfo = this.getPageInfo();
|
||||
this.sniffingHelper.reset(pageInfo);
|
||||
this.window.webContents.send("webview-did-navigate", pageInfo);
|
||||
|
||||
try {
|
||||
this.debugger.attach("1.1");
|
||||
if (isDev && process.env.DEBUG_PLUGINS === "true") {
|
||||
const content =
|
||||
'function addScript(src){const script=document.createElement("script");script.src=src;script.type="module";document.body.appendChild(script)}addScript("http://localhost:8080/src/main.ts");';
|
||||
await this.view.webContents.executeJavaScript(content);
|
||||
} else {
|
||||
const content = readFileSync(pluginPath, "utf-8");
|
||||
await this.view.webContents.executeJavaScript(content);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error("Debugger attach failed : ", err);
|
||||
// empty
|
||||
}
|
||||
};
|
||||
|
||||
this.debugger.on("detach", (event, reason) => {
|
||||
this.logger.error("Debugger detached due to : ", reason);
|
||||
});
|
||||
onDidFailLoad = (e: Event, code: number, desc: string) => {
|
||||
this.window.webContents.send("webview-fail-load", { code, desc });
|
||||
};
|
||||
|
||||
this.debugger.on("message", async (event, method, params) => {
|
||||
if (method === "Network.requestWillBeSent") {
|
||||
const { requestId, documentURL } = params;
|
||||
const { url, headers } = params.request;
|
||||
onDidNavigateInPage = () => {
|
||||
if (!this.view) return;
|
||||
const pageInfo = this.getPageInfo();
|
||||
this.sniffingHelper.update(pageInfo);
|
||||
this.window.webContents.send("webview-did-navigate-in-page", pageInfo);
|
||||
};
|
||||
|
||||
for (const filter of filterList) {
|
||||
for (const match of filter.matches) {
|
||||
if (!match.test(url)) {
|
||||
continue;
|
||||
}
|
||||
onOpenNewWindow = ({ url }: HandlerDetails) => {
|
||||
this.loadURL(url);
|
||||
|
||||
if (this.pageSources.has(url)) {
|
||||
continue;
|
||||
}
|
||||
return { action: "deny" } as { action: "deny" };
|
||||
};
|
||||
|
||||
this.pageSources.add(url);
|
||||
this.responseMap[documentURL] = [];
|
||||
this.requestMap[requestId] = {
|
||||
url,
|
||||
headers,
|
||||
requestId,
|
||||
filter,
|
||||
documentURL,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
onSource = async (item: SourceParams) => {
|
||||
if (!this.view) return;
|
||||
// 这里需要判断是否使用浏览器插件
|
||||
const useExtension = this.store.get("useExtension");
|
||||
if (useExtension) {
|
||||
this.view.webContents.send("webview-link-message", item);
|
||||
} else {
|
||||
const video = await this.videoRepository.findVideoByName(item.name);
|
||||
if (video) {
|
||||
item.name = `${item.name}-${Date.now()}`;
|
||||
}
|
||||
if (method === "Network.loadingFinished") {
|
||||
const { requestId } = params;
|
||||
const sourceParams = this.requestMap[requestId];
|
||||
const res = await this.videoRepository.addVideo(item);
|
||||
const mainWebContents = this.mainWindow.window?.webContents;
|
||||
if (!mainWebContents) return;
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
mainWebContents.send("download-item-notifier", res);
|
||||
}
|
||||
};
|
||||
|
||||
if (sourceParams) {
|
||||
const { filter, url, documentURL } = sourceParams;
|
||||
const objUrl = new URL(url);
|
||||
|
||||
const { body, base64Encoded } = await this.debugger.sendCommand(
|
||||
"Network.getResponseBody",
|
||||
{
|
||||
requestId: params.requestId,
|
||||
},
|
||||
);
|
||||
if (this.responseMap[documentURL].length > 0) {
|
||||
const bodys = this.responseMap[documentURL];
|
||||
const exist = bodys.some((item) =>
|
||||
new RegExp(objUrl.pathname).test(item),
|
||||
);
|
||||
if (exist) return;
|
||||
}
|
||||
if (base64Encoded) {
|
||||
// base64 解码
|
||||
const bodyContent = Buffer.from(body, "base64").toString();
|
||||
this.responseMap[documentURL].push(bodyContent);
|
||||
} else {
|
||||
this.responseMap[documentURL].push(body);
|
||||
}
|
||||
|
||||
this.logger.info(`在窗口中捕获视频链接: ${url}`);
|
||||
// 这里需要判断是否使用浏览器插件
|
||||
const useExtension = this.storeService.get("useExtension");
|
||||
if (useExtension) {
|
||||
const webContents = this.view.webContents;
|
||||
const item = await filter.handler.call(this, sourceParams);
|
||||
webContents.send("webview-link-message", item);
|
||||
} else {
|
||||
const item = await filter.handler.call(this, sourceParams);
|
||||
const video = await this.videoRepository.findVideoByName(item.name);
|
||||
if (video) {
|
||||
item.name = `${item.name}-${Date.now()}`;
|
||||
}
|
||||
const res = await this.videoRepository.addVideo(item);
|
||||
const mainWebContents = this.mainWindow.window?.webContents;
|
||||
if (!mainWebContents) return;
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
mainWebContents.send("download-item-notifier", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.debugger.sendCommand("Network.enable");
|
||||
getPageInfo() {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
return {
|
||||
title: this.view.webContents.getTitle(),
|
||||
url: this.view.webContents.getURL(),
|
||||
};
|
||||
}
|
||||
|
||||
getBounds(): Electron.Rectangle {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
return this.view.getBounds();
|
||||
}
|
||||
|
||||
setAutoResize(options: Electron.AutoResizeOptions): void {
|
||||
this.view.setAutoResize(options);
|
||||
setBounds(bounds: Electron.Rectangle): void {
|
||||
if (!this.view) return;
|
||||
this.view.setBounds(bounds);
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string): void {
|
||||
if (!this.view) return;
|
||||
this.view.setBackgroundColor(color);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.curWindow?.setBrowserView(this.view);
|
||||
isDev && this.view.webContents.openDevTools();
|
||||
if (!this.view) return;
|
||||
this.window.contentView.addChildView(this.view);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.curWindow?.setBrowserView(null);
|
||||
isDev && this.view.webContents.closeDevTools();
|
||||
if (!this.view) return;
|
||||
this.window.contentView.removeChildView(this.view);
|
||||
}
|
||||
|
||||
setBounds(bounds: Electron.Rectangle): void {
|
||||
if (process.platform === "darwin") {
|
||||
bounds.y = bounds.y + 30;
|
||||
loadURL(url: string) {
|
||||
// 开始加载 url
|
||||
if (!this.view) {
|
||||
this.init();
|
||||
}
|
||||
this.view.setBounds(bounds);
|
||||
}
|
||||
if (!this.view) return;
|
||||
|
||||
async loadURL(url?: string, isNewWindow?: boolean) {
|
||||
const canGoBack = this.view.webContents.canGoBack();
|
||||
try {
|
||||
await this.view.webContents.loadURL(url || "");
|
||||
} catch (err: unknown) {
|
||||
this.logger.error("加载 url 时出现错误: ", err);
|
||||
throw err;
|
||||
} finally {
|
||||
if (!canGoBack && !isNewWindow) {
|
||||
this.view.webContents.clearHistory();
|
||||
}
|
||||
}
|
||||
// 1. 停止当前导航
|
||||
this.view.webContents.stop();
|
||||
|
||||
// 2. 加载新的 url
|
||||
this.view.webContents.loadURL(url);
|
||||
}
|
||||
|
||||
async goBack() {
|
||||
if (this.view.webContents.canGoBack()) {
|
||||
this.view.webContents.goBack();
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
const { webContents } = this.view;
|
||||
if (webContents.canGoBack()) {
|
||||
webContents.goBack();
|
||||
return true;
|
||||
} else {
|
||||
this.destroyView();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async reload() {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
this.view.webContents.reload();
|
||||
}
|
||||
|
||||
async goHome() {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
this.view.webContents.stop();
|
||||
this.view.webContents.clearHistory();
|
||||
this.destroyView();
|
||||
}
|
||||
|
||||
get curWindow() {
|
||||
get window() {
|
||||
if (this.browserWindow.window) return this.browserWindow.window;
|
||||
if (this.mainWindow.window) return this.mainWindow.window;
|
||||
return null;
|
||||
throw new Error("未找到当前窗口");
|
||||
}
|
||||
|
||||
private get session() {
|
||||
return session.fromPartition(PERSIST_WEBVIEW);
|
||||
return session.fromPartition(this.defaultSession);
|
||||
}
|
||||
|
||||
private enableProxy(proxy: string) {
|
||||
if (!proxy) {
|
||||
this.logger.error("[proxy] 代理地址不能为空");
|
||||
this.logger.error("[Proxy] 代理地址不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,12 +238,12 @@ export default class WebviewService {
|
||||
}
|
||||
|
||||
this.session.setProxy({ proxyRules: proxy });
|
||||
this.logger.info(`[proxy] 代理开启成功,代理地址为${proxy}`);
|
||||
this.logger.info(`[Proxy] 代理开启(${proxy})`);
|
||||
}
|
||||
|
||||
private disableProxy() {
|
||||
this.session.setProxy({ proxyRules: "" });
|
||||
this.logger.info("[proxy] 代理关闭成功");
|
||||
this.logger.info("[Proxy] 代理关闭");
|
||||
}
|
||||
|
||||
setProxy(useProxy: boolean, proxy: string): void {
|
||||
@@ -339,41 +265,84 @@ export default class WebviewService {
|
||||
async initBlocker() {
|
||||
this.blocker = await ElectronBlocker.fromPrebuiltAdsAndTracking(fetch);
|
||||
|
||||
const enableBlocking = this.storeService.get("blockAds");
|
||||
const enableBlocking = this.store.get("blockAds");
|
||||
this.setBlocking(enableBlocking);
|
||||
}
|
||||
|
||||
private enableBlocking() {
|
||||
if (!this.blocker) {
|
||||
this.logger.error("开启 blocker 失败,未初始化");
|
||||
this.logger.error("[AdBlocker] 开启失败(未初始化)");
|
||||
return;
|
||||
}
|
||||
this.blocker.enableBlockingInSession(this.session);
|
||||
this.logger.info("开启 blocker 成功");
|
||||
this.logger.info("[AdBlocker] 开启");
|
||||
}
|
||||
|
||||
private disableBlocking() {
|
||||
if (!this.blocker) {
|
||||
this.logger.error("关闭 blocker 失败,未初始化");
|
||||
this.logger.error("[AdBlocker] 关闭失败(未初始化)");
|
||||
return;
|
||||
}
|
||||
if (!this.blocker.isBlockingEnabled(this.session)) {
|
||||
return;
|
||||
}
|
||||
this.blocker.disableBlockingInSession(this.session);
|
||||
this.logger.info("关闭 blocker 成功");
|
||||
this.logger.info("[AdBlocker] 关闭");
|
||||
}
|
||||
|
||||
setUserAgent(isMobile?: boolean) {
|
||||
if (!this.view) return;
|
||||
if (isMobile) {
|
||||
this.view.webContents.setUserAgent(mobileUA);
|
||||
} else {
|
||||
this.view.webContents.setUserAgent(pcUA);
|
||||
}
|
||||
this.logger.info("设置 user-agent 成功", isMobile);
|
||||
this.logger.info(`[UA] 设置为${isMobile ? "移动端" : " pc 端"}`);
|
||||
}
|
||||
|
||||
get debugger() {
|
||||
return this.view.webContents.debugger;
|
||||
captureView(): Promise<Electron.NativeImage> {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
return this.view.webContents.capturePage();
|
||||
}
|
||||
|
||||
sendToWindow(channel: string, ...args: unknown[]) {
|
||||
this.window.webContents.send(channel, ...args);
|
||||
}
|
||||
|
||||
destroyView() {
|
||||
if (this.view && this.window) {
|
||||
this.view.webContents.close();
|
||||
this.window.contentView.removeChildView(this.view);
|
||||
}
|
||||
// FIXME: 为了避免内存泄漏,这里需要销毁 view
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
async clearCache() {
|
||||
await this.session.clearCache();
|
||||
await this.session.clearStorageData();
|
||||
}
|
||||
|
||||
setDefaultSession(isPrivacy = false, init = false) {
|
||||
this.logger.info(`[Session] ${isPrivacy ? "隐私" : "正常"}模式`);
|
||||
if (isPrivacy) {
|
||||
this.defaultSession = PRIVACY_WEBVIEW;
|
||||
} else {
|
||||
this.defaultSession = PERSIST_WEBVIEW;
|
||||
}
|
||||
|
||||
if (this.view) {
|
||||
const { useProxy, proxy } = this.store.store;
|
||||
this.destroyView();
|
||||
this.init();
|
||||
this.setProxy(useProxy, proxy);
|
||||
this.sniffingHelper.start(isPrivacy);
|
||||
}
|
||||
|
||||
if (!init) {
|
||||
this.window.webContents.send("change-privacy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,22 @@ export const TYPES = {
|
||||
// repository
|
||||
VideoRepository: Symbol.for("VideoRepository"),
|
||||
FavoriteRepository: Symbol.for("FavoriteRepository"),
|
||||
ConversionRepository: Symbol.for("ConversionRepository"),
|
||||
// windows
|
||||
BrowserWindow: Symbol.for("BrowserWindow"),
|
||||
PlayerWindow: Symbol.for("PlayerWindow"),
|
||||
MainWindow: Symbol.for("MainWindow"),
|
||||
// services
|
||||
WebviewService: Symbol.for("WebviewService"),
|
||||
DownloadService: Symbol.for("DownloadService"),
|
||||
DevToolsService: Symbol.for("DevToolsService"),
|
||||
VideoService: Symbol.for("VideoService"),
|
||||
StoreService: Symbol.for("StoreService"),
|
||||
SessionService: Symbol.for("SessionService"),
|
||||
DatabaseService: Symbol.for("DatabaseService"),
|
||||
ProtocolService: Symbol.for("ProtocolService"),
|
||||
UpdateService: Symbol.for("UpdateService"),
|
||||
LoggerService: Symbol.for("LoggerService"),
|
||||
SniffingHelper: Symbol.for("SniffingHelper"),
|
||||
// vendor
|
||||
ElectronLogger: Symbol.for("ElectronLogger"),
|
||||
ElectronUpdater: Symbol.for("ElectronUpdater"),
|
||||
ElectronStore: Symbol.for("ElectronStore"),
|
||||
ElectronDevtools: Symbol.for("ElectronDevtools"),
|
||||
TypeORM: Symbol.for("TypeORM"),
|
||||
};
|
||||
|
||||
export enum AppTheme {
|
||||
@@ -27,3 +28,9 @@ export enum AppTheme {
|
||||
Light = "light",
|
||||
Dark = "dark",
|
||||
}
|
||||
|
||||
export enum AppLanguage {
|
||||
System = "system",
|
||||
ZH = "zh",
|
||||
EN = "en",
|
||||
}
|
||||
|
||||
Vendored
+10
-9
@@ -1,20 +1,21 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import { TYPES } from "../types.ts";
|
||||
import isDev from "electron-is-dev";
|
||||
import installExtension, {
|
||||
import install, {
|
||||
REDUX_DEVTOOLS,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} from "electron-devtools-installer";
|
||||
import LoggerService from "./LoggerService";
|
||||
import ElectronLogger from "./ElectronLogger.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DevToolsService {
|
||||
export default class DevToolsService implements Vendor {
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
async init() {
|
||||
if (!isDev) {
|
||||
return;
|
||||
}
|
||||
@@ -25,9 +26,9 @@ export default class DevToolsService {
|
||||
|
||||
try {
|
||||
this.logger.debug("当前环境为开发环境,开始加载开发者工具");
|
||||
await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]);
|
||||
await install([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]);
|
||||
this.logger.debug("加载开发者工具成功");
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
this.logger.error("加载开发者工具失败", err);
|
||||
}
|
||||
}
|
||||
+10
-7
@@ -2,10 +2,11 @@ import logger, { Logger } from "electron-log";
|
||||
import { injectable } from "inversify";
|
||||
import dayjs from "dayjs";
|
||||
import path from "path";
|
||||
import { appName, workspace } from "../helper";
|
||||
import { appName, workspace } from "../helper/index.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
|
||||
@injectable()
|
||||
export default class LoggerService {
|
||||
export default class ElectronLogger implements Vendor {
|
||||
logger: Logger;
|
||||
|
||||
constructor() {
|
||||
@@ -13,23 +14,25 @@ export default class LoggerService {
|
||||
const logPath = path.resolve(workspace, `logs/${datetime}-${appName}.log`);
|
||||
logger.transports.console.format = "{h}:{i}:{s} {text}";
|
||||
logger.transports.file.getFile();
|
||||
logger.transports.file.resolvePath = () => logPath;
|
||||
logger.transports.file.resolvePathFn = () => logPath;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
info(...args: any[]) {
|
||||
info(...args: unknown[]) {
|
||||
return this.logger.info(...args);
|
||||
}
|
||||
|
||||
warn(...args: any[]) {
|
||||
warn(...args: unknown[]) {
|
||||
return this.logger.warn(...args);
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
error(...args: unknown[]) {
|
||||
return this.logger.error(...args);
|
||||
}
|
||||
|
||||
debug(...args: any[]) {
|
||||
debug(...args: unknown[]) {
|
||||
return this.logger.debug(...args);
|
||||
}
|
||||
|
||||
async init() {}
|
||||
}
|
||||
+12
-7
@@ -1,11 +1,12 @@
|
||||
import Store from "electron-store";
|
||||
import { injectable } from "inversify";
|
||||
import { AppStore } from "../main";
|
||||
import { download, workspace } from "../helper";
|
||||
import { AppTheme } from "../types";
|
||||
import { AppStore } from "../main.ts";
|
||||
import { download, workspace } from "../helper/index.ts";
|
||||
import { AppLanguage, AppTheme } from "../types.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
|
||||
@injectable()
|
||||
export default class StoreService extends Store<AppStore> {
|
||||
export default class StoreService extends Store<AppStore> implements Vendor {
|
||||
constructor() {
|
||||
super({
|
||||
name: "config",
|
||||
@@ -24,11 +25,15 @@ export default class StoreService extends Store<AppStore> {
|
||||
useExtension: false,
|
||||
isMobile: false,
|
||||
maxRunner: 2,
|
||||
language: AppLanguage.System,
|
||||
showTerminal: false,
|
||||
privacy: false,
|
||||
machineId: "",
|
||||
downloadProxySwitch: false,
|
||||
autoUpgrade: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
init(): void {
|
||||
// empty
|
||||
}
|
||||
async init() {}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types.ts";
|
||||
import isDev from "electron-is-dev";
|
||||
import ElectronLogger from "./ElectronLogger.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
import ElectronStore from "electron-store";
|
||||
|
||||
@injectable()
|
||||
export default class UpdateService implements Vendor {
|
||||
constructor(
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
if (isDev) return;
|
||||
|
||||
const { autoUpgrade } = this.store.store;
|
||||
if (!autoUpgrade) return;
|
||||
|
||||
try {
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
autoUpdater.logger = this.logger.logger;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
await autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "自动更新完成",
|
||||
body: "下次重启时将会自动安装",
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.info("update error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -1,31 +1,33 @@
|
||||
import { db } from "../helper";
|
||||
import { db } from "../helper/index.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { DataSource, EntityManager } from "typeorm";
|
||||
import { TYPES } from "../types";
|
||||
import { Video } from "../entity/Video";
|
||||
import { Favorite } from "../entity/Favorite";
|
||||
import LoggerService from "./LoggerService";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { Video } from "../entity/Video.ts";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import ElectronLogger from "./ElectronLogger.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
import { Conversion } from "../entity/Conversion.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DatabaseService {
|
||||
export default class DatabaseService implements Vendor {
|
||||
appDataSource: DataSource;
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger
|
||||
) {
|
||||
this.appDataSource = new DataSource({
|
||||
type: "better-sqlite3",
|
||||
database: db,
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
entities: [Video, Favorite],
|
||||
entities: [Video, Favorite, Conversion],
|
||||
migrations: [],
|
||||
subscribers: [],
|
||||
});
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
async init() {
|
||||
this.logger.info("数据库地址是: ", db);
|
||||
await this.appDataSource.initialize();
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import isDev from "electron-is-dev";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
import { TYPES } from "../types";
|
||||
import { TYPES } from "../types.ts";
|
||||
import _ from "lodash";
|
||||
import Window from "./window";
|
||||
import StoreService from "../services/StoreService";
|
||||
import Window from "../core/window.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
|
||||
@injectable()
|
||||
export default class BrowserWindow extends Window {
|
||||
@@ -13,8 +13,8 @@ export default class BrowserWindow extends Window {
|
||||
: "mediago://index.html/browser";
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
) {
|
||||
super({
|
||||
width: 1100,
|
||||
@@ -28,17 +28,15 @@ export default class BrowserWindow extends Window {
|
||||
},
|
||||
});
|
||||
|
||||
this.storeService.onDidChange("openInNewWindow", this.handleNewWindowsVal);
|
||||
this.storeService.onDidAnyChange(this.storeChange);
|
||||
this.store.onDidChange("openInNewWindow", this.handleNewWindowsVal);
|
||||
this.store.onDidAnyChange(this.storeChange);
|
||||
}
|
||||
|
||||
storeChange = (store: any) => {
|
||||
if (!this.window) return;
|
||||
// 向所有窗口发送通知
|
||||
this.window.webContents.send("store-change", store);
|
||||
storeChange = (store: unknown) => {
|
||||
this.send("store-change", store);
|
||||
};
|
||||
|
||||
handleNewWindowsVal = (newValue: any) => {
|
||||
handleNewWindowsVal = (newValue: unknown) => {
|
||||
if (!this.window) return;
|
||||
|
||||
// 向所有窗口发送通知
|
||||
@@ -53,7 +51,7 @@ export default class BrowserWindow extends Window {
|
||||
if (!this.window) return;
|
||||
|
||||
const bounds = this.window.getBounds();
|
||||
this.storeService.set("browserBounds", _.omit(bounds, ["x", "y"]));
|
||||
this.store.set("browserBounds", _.omit(bounds, ["x", "y"]));
|
||||
};
|
||||
|
||||
showWindow = () => {
|
||||
@@ -65,7 +63,7 @@ export default class BrowserWindow extends Window {
|
||||
this.window.show();
|
||||
isDev && this.window.webContents.openDevTools();
|
||||
|
||||
const browserBounds = this.storeService.get("browserBounds");
|
||||
const browserBounds = this.store.get("browserBounds");
|
||||
if (browserBounds) {
|
||||
this.window.setBounds(browserBounds);
|
||||
}
|
||||
@@ -76,4 +74,10 @@ export default class BrowserWindow extends Window {
|
||||
|
||||
this.window.close();
|
||||
};
|
||||
|
||||
send(channel: string, ...args: unknown[]) {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,28 @@ import { Menu, Notification, app } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
import { TYPES } from "../types";
|
||||
import { DownloadProgress } from "../interfaces";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { DownloadProgress } from "../interfaces.ts";
|
||||
import _ from "lodash";
|
||||
import Window from "./window";
|
||||
import LoggerService from "../services/LoggerService";
|
||||
import DownloadService from "../services/DownloadService";
|
||||
import StoreService from "../services/StoreService";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import Window from "../core/window.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import DownloadService from "../services/DownloadService.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import { isWin } from "../helper/index.ts";
|
||||
|
||||
@injectable()
|
||||
export default class MainWindow extends Window {
|
||||
url = isDev ? "http://localhost:8555/" : "mediago://index.html/";
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
@inject(TYPES.DownloadService)
|
||||
private readonly downloadService: DownloadService,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
) {
|
||||
super({
|
||||
width: 1100,
|
||||
@@ -41,14 +42,17 @@ export default class MainWindow extends Window {
|
||||
this.downloadService.on("download-success", this.onDownloadSuccess);
|
||||
this.downloadService.on("download-failed", this.onDownloadFailed);
|
||||
this.downloadService.on("download-start", this.onDownloadStart);
|
||||
this.downloadService.on("download-stop", this.onDownloadStart);
|
||||
this.storeService.onDidAnyChange(this.storeChange);
|
||||
this.downloadService.on("download-stop", this.onDownloadStop);
|
||||
this.downloadService.on("download-message", this.receiveMessage);
|
||||
this.store.onDidAnyChange(this.storeChange);
|
||||
app.on("second-instance", this.secondInstance);
|
||||
}
|
||||
|
||||
onDownloadReadyStart = ({ id, isLive }: { id: number; isLive: boolean }) => {
|
||||
this.videoRepository.changeVideoIsLive(id, isLive);
|
||||
this.send("change-video-is-live", { id, isLive });
|
||||
onDownloadReadyStart = async ({ id, isLive }: DownloadProgress) => {
|
||||
if (isLive) {
|
||||
await this.videoRepository.changeVideoIsLive(id);
|
||||
this.send("change-video-is-live", { id });
|
||||
}
|
||||
};
|
||||
|
||||
init(): void {
|
||||
@@ -62,7 +66,7 @@ export default class MainWindow extends Window {
|
||||
|
||||
this.window = this.create();
|
||||
|
||||
const mainBounds = this.storeService.get("mainBounds");
|
||||
const mainBounds = this.store.get("mainBounds");
|
||||
if (mainBounds) {
|
||||
this.window.setBounds(mainBounds);
|
||||
}
|
||||
@@ -75,12 +79,12 @@ export default class MainWindow extends Window {
|
||||
if (!this.window) return;
|
||||
|
||||
const bounds = this.window.getBounds();
|
||||
this.storeService.set("mainBounds", _.omit(bounds, ["x", "y"]));
|
||||
this.store.set("mainBounds", _.omit(bounds, ["x", "y"]));
|
||||
};
|
||||
|
||||
secondInstance = () => {
|
||||
if (!this.window) return;
|
||||
if (process.platform === "win32") {
|
||||
if (isWin) {
|
||||
if (this.window) {
|
||||
if (this.window.isMinimized()) {
|
||||
this.window.restore();
|
||||
@@ -94,7 +98,7 @@ export default class MainWindow extends Window {
|
||||
}
|
||||
};
|
||||
|
||||
storeChange = (store: any) => {
|
||||
storeChange = (store: unknown) => {
|
||||
// 向所有窗口发送通知
|
||||
this.send("store-change", store);
|
||||
};
|
||||
@@ -104,27 +108,27 @@ export default class MainWindow extends Window {
|
||||
};
|
||||
|
||||
onDownloadSuccess = async (id: number) => {
|
||||
const promptTone = this.storeService.get("promptTone");
|
||||
const promptTone = this.store.get("promptTone");
|
||||
if (promptTone) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
|
||||
new Notification({
|
||||
title: "下载成功",
|
||||
body: `${video?.name} 下载成功`,
|
||||
body: `${video.name} 下载成功`,
|
||||
}).show();
|
||||
}
|
||||
|
||||
this.send("download-success", id);
|
||||
};
|
||||
|
||||
onDownloadFailed = async (id: number, err: any) => {
|
||||
const promptTone = this.storeService.get("promptTone");
|
||||
onDownloadFailed = async (id: number, err: unknown) => {
|
||||
const promptTone = this.store.get("promptTone");
|
||||
if (promptTone) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
|
||||
new Notification({
|
||||
title: "下载失败",
|
||||
body: `${video?.name} 下载失败`,
|
||||
body: `${video.name} 下载失败`,
|
||||
}).show();
|
||||
}
|
||||
this.logger.error("下载失败:", err);
|
||||
@@ -139,7 +143,16 @@ export default class MainWindow extends Window {
|
||||
this.send("download-stop", id);
|
||||
};
|
||||
|
||||
send(channel: string, ...args: any[]) {
|
||||
receiveMessage = async (id: number, message: string) => {
|
||||
// 将日志写入数据库中
|
||||
await this.videoRepository.appendDownloadLog(id, message);
|
||||
const showTerminal = this.store.get("showTerminal");
|
||||
if (showTerminal) {
|
||||
this.send("download-message", id, message);
|
||||
}
|
||||
};
|
||||
|
||||
send(channel: string, ...args: unknown[]) {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send(channel, ...args);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import isDev from "electron-is-dev";
|
||||
import { injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
import Window from "./window";
|
||||
|
||||
@injectable()
|
||||
export default class PlayerWindowServiceImpl extends Window {
|
||||
url = isDev ? "http://localhost:8555/player" : "mediago://index.html/player";
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
width: 1100,
|
||||
minWidth: 1100,
|
||||
height: 680,
|
||||
minHeight: 680,
|
||||
show: false,
|
||||
frame: true,
|
||||
webPreferences: {
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openWindow = async (name: string) => {
|
||||
if (!this.window) {
|
||||
const url = new URL(this.url);
|
||||
url.searchParams.set("name", encodeURIComponent(name));
|
||||
this.url = url.toString();
|
||||
this.window = this.create();
|
||||
}
|
||||
|
||||
isDev && this.window.webContents.openDevTools();
|
||||
this.window.show();
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "CommonJS",
|
||||
"module": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
@@ -9,12 +9,14 @@
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strictPropertyInitialization": false,
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./types",
|
||||
"outDir": "app/output/build/main"
|
||||
"outDir": "app/output/build/main",
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier/skip-formatting",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
},
|
||||
rules: {
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
test-results/
|
||||
playwright-report/
|
||||
@@ -1,71 +0,0 @@
|
||||
# mobile-player
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run End-to-End Tests with [Playwright](https://playwright.dev)
|
||||
|
||||
```sh
|
||||
# Install browsers for the first run
|
||||
npx playwright install
|
||||
|
||||
# When testing on CI, must build the project first
|
||||
npm run build
|
||||
|
||||
# Runs the end-to-end tests
|
||||
npm run test:e2e
|
||||
# Runs the tests only on Chromium
|
||||
npm run test:e2e -- --project=chromium
|
||||
# Runs the tests of a specific file
|
||||
npm run test:e2e -- tests/example.spec.ts
|
||||
# Runs the tests in debug mode
|
||||
npm run test:e2e -- --debug
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
Vendored
-17
@@ -1,17 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
VanCell: typeof import('vant/es')['Cell']
|
||||
VanEmpty: typeof import('vant/es')['Empty']
|
||||
VanList: typeof import('vant/es')['List']
|
||||
VanNavBar: typeof import('vant/es')['NavBar']
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": ["./**/*"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// See here how to get started:
|
||||
// https://playwright.dev/docs/intro
|
||||
test("visits the app root url", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.locator("div.greetings > h1")).toHaveText("You did it!");
|
||||
});
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>media download</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "mobile",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev:mobile": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"pinia": "^2.1.4",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2",
|
||||
"xgplayer": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@tsconfig/node18": "^2.0.1",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-plugin-vue": "^9.15.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "~5.3.3",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.3.9",
|
||||
"vitest": "^0.32.2",
|
||||
"vue-tsc": "^1.8.1"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user