Compare commits

...

228 Commits

Author SHA1 Message Date
士子☀️ 635377d227 Merge pull request #153 from caorushizi/feat/webcontentsView
Feat/webcontents view
2024-05-22 21:11:38 +08:00
caorushizi dc86ab8f4c feat: release 2.2.0 2024-05-22 21:09:25 +08:00
caorushizi 36b63ad916 fix: 🐛 bilibili 2024-05-22 20:56:18 +08:00
士子☀️ e1b378811d Merge pull request #152 from caorushizi/feat/webcontentsView
fix: 🐛  .env
2024-05-22 19:30:00 +08:00
caorushizi 37fb9bba75 fix: 🐛 .env 2024-05-22 19:28:19 +08:00
士子☀️ 848253c678 Merge pull request #151 from caorushizi/feat/webcontentsView
Feat/webcontents view
2024-05-22 18:19:33 +08:00
caorushizi b64e779ed1 feat: release 2.2.0-beta.4 2024-05-22 18:18:07 +08:00
caorushizi 046a62cde3 fix: 🐛 load url 2024-05-22 18:16:38 +08:00
caorushizi 1a9e6efb90 feat: use webcontents view and capture page 2024-05-22 15:15:39 +08:00
士子☀️ fd422c15ba Merge pull request #150 from caorushizi/fix/addForm
fix: 🐛  fix run release scripts
2024-05-22 04:04:18 +08:00
caorushizi c338f76f8d fix: 🐛 fix run release scripts 2024-05-22 04:03:32 +08:00
士子☀️ 4285b3f4f7 Merge pull request #149 from caorushizi/fix/addForm
feat:   支持直播视频下载
2024-05-22 03:51:41 +08:00
caorushizi d0109869a6 feat: 支持直播视频下载 2024-05-22 03:50:38 +08:00
士子☀️ d29ad44c54 Merge pull request #148 from caorushizi/fix/addForm
fix: 🐛  float button position
2024-05-22 01:02:50 +08:00
caorushizi be61b07fea fix: 🐛 float button position 2024-05-22 01:01:38 +08:00
士子☀️ e5e171fb6f Merge pull request #147 from caorushizi/fix/addForm
fix: 🐛  release 2.2.0-beta.3
2024-05-22 00:33:59 +08:00
caorushizi 805d97c3ca fix: 🐛 release 2.2.0-beta.3 2024-05-22 00:32:35 +08:00
士子☀️ 040222f259 Merge pull request #146 from caorushizi/fix/addForm
fix: 🐛  fix browser window event
2024-05-21 22:52:39 +08:00
caorushizi 09ed55eba4 fix: 🐛 fix browser window event 2024-05-21 22:51:40 +08:00
士子☀️ a6d4f8cf98 Merge pull request #145 from caorushizi/fix/addForm
fix: 🐛  release 2.2.0-beta.2
2024-05-21 02:38:48 +08:00
caorushizi d19b2394e8 fix: 🐛 release 2.2.0-beta.2 2024-05-21 02:37:40 +08:00
士子☀️ 036a99d84d Merge pull request #143 from caorushizi/fix/video
Fix/video
2024-05-14 12:22:24 +08:00
caorushizi b022335af7 feat: release 2.2.0-beta.1 2024-05-14 12:21:21 +08:00
caorushizi a66c20c9d5 feat: 修复页面中下载嗅探问题 2024-05-14 12:19:47 +08:00
caorushizi f9fc135647 fix: 🐛 修复 dom-ready 执行的顺序 2024-05-13 09:49:34 +08:00
士子☀️ fbbee9c114 Merge pull request #142 from caorushizi/fix/docs
Fix/docs
2024-05-13 09:42:35 +08:00
caorushizi a65fbcbaf8 fix: 🐛 修复插件展现运行的时机 2024-05-13 09:41:06 +08:00
士子☀️ 39b68b71d4 refactor: 📦️ modify mediago protocol, upgrade adblocker 2024-05-13 01:51:13 +08:00
士子☀️ 4d12a0a937 Merge pull request #140 from caorushizi/fix/docs
ci: 🎡  github actions
2024-05-02 19:01:16 +08:00
士子☀️ 960973302e ci: 🎡 github actions 2024-05-02 18:58:01 +08:00
士子☀️ 1a68b00bdd Merge pull request #139 from caorushizi/fix/docs
ci: 🎡  modify github actions
2024-05-02 18:32:22 +08:00
士子☀️ dba73fc473 ci: 🎡 modify github actions 2024-05-02 18:30:24 +08:00
士子☀️ 211f7314ce Merge pull request #138 from caorushizi/fix/docs
docs: 📝  remove reaction
2024-05-02 18:18:19 +08:00
士子☀️ 64d719a63a docs: 📝 remove reaction 2024-05-02 18:17:19 +08:00
士子☀️ 0304e6b569 Merge pull request #135 from caorushizi/feat/mediago
Feat/mediago
2024-04-05 23:46:23 +08:00
士子☀️ 190447a0bd feat: release v2.2.0-beta.0 2024-04-05 23:44:13 +08:00
士子☀️ 497d1265f0 chore: 🔨 add lint-staged 2024-04-05 23:42:35 +08:00
士子☀️ 6d459e0692 feat: rename mediago 2024-04-05 23:09:30 +08:00
士子☀️ 45cde40cea chore: 🔨 fix pre-commit 2024-04-05 22:36:13 +08:00
士子☀️ e9deb4c7f7 chore: 🔨 use tsx instead of ts-node 2024-04-05 22:02:59 +08:00
caorushizi 1abfc763dc docs: 📝 update waline client 2024-03-26 14:31:00 +08:00
caorushizi b84ddcc88d docs: 📝 update vitepress 2024-03-26 13:02:22 +08:00
士子☀️ 33f7951901 refactor: 📦️ remove submodule 2024-03-20 23:23:09 +08:00
士子☀️ d3354f9109 Merge pull request #125 from caorushizi/feat/player
refactor: 📦️  remove tsno
2024-03-20 19:32:06 +08:00
caorushizi d252816cc3 refactor: 📦️ remove tsno 2024-03-20 19:29:27 +08:00
士子☀️ 6e31904e95 Merge pull request #124 from caorushizi/feat/player
refactor: 📦️  remove video player
2024-03-20 16:59:12 +08:00
caorushizi 2825d1f4c7 refactor: 📦️ remove video player 2024-03-20 16:50:58 +08:00
士子☀️ fcb10c6137 Merge pull request #118 from caorushizi/feat/player
fix: 🐛  catch video
2024-03-17 18:04:27 +08:00
士子☀️ f564739578 fix: 🐛 catch video 2024-03-17 18:03:24 +08:00
士子☀️ 40faf4b383 Merge pull request #117 from caorushizi/feat/player
fix: 🐛  fix mobile video size
2024-03-17 17:46:01 +08:00
士子☀️ affff353bd fix: 🐛 fix mobile video size 2024-03-17 17:45:04 +08:00
士子☀️ f410fabff7 Merge pull request #116 from caorushizi/feat/player
fix: 🐛  fix video container
2024-03-17 17:22:13 +08:00
士子☀️ 7d4b201b6e fix: 🐛 fix video container 2024-03-17 17:21:10 +08:00
士子☀️ a9ac0aa16b Merge pull request #115 from caorushizi/2.1.0-beta.1
feat:   v2.1.0-beta.1
2024-03-17 16:09:16 +08:00
士子☀️ 5419970080 feat: v2.1.0-beta.1 2024-03-17 16:08:34 +08:00
士子☀️ 506a573180 Merge pull request #114 from caorushizi/feat/dialog
fix: 🐛 按照获取时间正序排列
2024-03-17 02:03:43 +08:00
士子☀️ 17b2450b43 feat: use download dialog 2024-03-17 02:01:32 +08:00
士子☀️ 2942e65a54 fix: 🐛 按照获取时间正序排列 2024-03-17 00:17:47 +08:00
士子☀️ 913a7bd9e5 Merge pull request #113 from caorushizi/feat/dialog
feat:   show download dialog
2024-03-17 00:12:31 +08:00
士子☀️ c5f2edd9de feat: show download dialog 2024-03-17 00:09:34 +08:00
士子☀️ 24d511e6b6 Merge pull request #112 from caorushizi/feat/plugins
feat:  bilibili plguins
2024-03-10 19:23:15 +08:00
士子☀️ 47f3336f9a feat: 2.1.0-beta.0 2024-03-10 19:22:30 +08:00
士子☀️ e0e6e5dbed feat: bilibili plguins 2024-03-10 19:19:27 +08:00
士子☀️ aa7f3e7bf7 Merge pull request #111 from caorushizi/feat/terminal
feat:  use terminal
2024-03-10 16:43:04 +08:00
caorushizi 065a6c45f7 feat: terminal 2024-03-10 16:40:25 +08:00
士子☀️ b13174dda1 feat: use terminal 2024-03-10 16:19:16 +08:00
士子☀️ 9e6a931a41 Merge pull request #109 from caorushizi/feat/terminal
feat:  terminal
2024-03-09 08:57:23 +08:00
士子☀️ cdabdfd2f8 feat: layout style 2024-03-09 01:06:49 +08:00
士子☀️ b4cdbd7547 feat: show terminal 2024-03-09 00:05:15 +08:00
caorushizi 75fe603919 feat: terminal 2024-03-08 21:35:05 +08:00
士子☀️ 16ef9ae642 Merge pull request #104 from caorushizi/feat/actions
feat:   add vscode debug config
2024-02-27 23:13:35 +08:00
士子☀️ 7db3162748 feat: add vscode debug config 2024-02-27 23:11:37 +08:00
士子☀️ 3f0e6d9856 Merge pull request #102 from caorushizi/feat/actions
feat:   update actions version
2024-02-27 01:04:51 +08:00
士子☀️ d87fc4a400 feat: update actions version 2024-02-27 01:03:26 +08:00
士子☀️ ec6241b79e Merge pull request #101 from caorushizi/fix/node-pty
fix: 🐛  cannot find module 'node-pty'
2024-02-27 00:50:42 +08:00
士子☀️ 9b37398e9f fix: 🐛 cannot find module 'node-pty' 2024-02-27 00:49:05 +08:00
士子☀️ 3fca8694ee fix: 🐛 filename 2024-02-25 23:39:24 +08:00
士子☀️ 78efffa29b Merge pull request #95 from caorushizi/dev/terminal
Dev/terminal
2024-02-25 22:40:02 +08:00
士子☀️ 737ecbdadd fix: 🐛 build player 2024-02-25 22:37:34 +08:00
士子☀️ fa6ce22ea7 Merge pull request #94 from caorushizi/dev/terminal
Dev/terminal
2024-02-25 22:22:36 +08:00
士子☀️ 315c55c844 feat: terminal 2024-02-25 18:28:37 +08:00
士子☀️ d9ba60e1a5 feat: terminal 2024-02-25 17:31:58 +08:00
士子☀️ 7503322d01 feat: terminal 2024-02-25 13:36:14 +08:00
士子☀️ 2e5f2dc687 Merge pull request #91 from caorushizi/dev/plugins
style: 💄 eslint
2024-02-24 23:28:27 +08:00
士子☀️ e4afd27908 fix: 🐛 add tsconfig 2024-02-24 23:19:09 +08:00
士子☀️ 6fe5d5523e feat: remove player page. use node-pty 2024-02-24 22:50:03 +08:00
士子☀️ 7335946ec2 refactor: 📦️ refactoring the project directory structure 2024-02-23 19:32:06 +08:00
士子☀️ eb4576de9b feat: 使用 node 伪终端 2024-02-21 22:17:02 +08:00
士子☀️ 1e41a9ee99 feat: node 伪终端获取详细日志 2024-02-21 21:44:12 +08:00
士子☀️ dd5226ab47 refactor: 📦️ downloader 2024-02-12 09:56:07 +08:00
士子☀️ 989e5535eb feat: new player 2024-02-12 01:55:11 +08:00
士子☀️ 65efeed692 style: 💄 eslint 2024-02-11 23:18:45 +08:00
士子☀️ fcddb2d382 Merge pull request #88 from caorushizi/dev/plugins
fix: 🐛  types
2024-02-11 18:13:57 +08:00
士子☀️ bbc892ec81 fix: 🐛 types 2024-02-11 18:08:51 +08:00
士子☀️ ca1c2821a7 Merge pull request #87 from caorushizi/dev/plugins
fix: 🐛  types
2024-02-11 18:02:50 +08:00
士子☀️ c8d4f763fd fix: 🐛 types 2024-02-11 18:00:08 +08:00
士子☀️ a3d587af44 Merge pull request #85 from caorushizi/dev/plugins
feat:  new plugin
2024-02-11 17:00:28 +08:00
士子☀️ ce2146cc89 feat: add plugin 2024-02-11 16:59:02 +08:00
士子☀️ 8455f17517 style: 💄 somthing 2024-02-11 10:45:32 +08:00
士子☀️ c075bdd4f9 feat: new plugin 2024-02-09 22:18:35 +08:00
士子☀️ 65d88d0896 Merge pull request #82 from caorushizi/dev/i18n
feat: display language
2024-02-09 17:01:55 +08:00
士子☀️ 1c62640301 style: 💄 add lint-staged 2024-02-09 17:00:48 +08:00
士子☀️ dd6684c550 feat: build plugin
build plugin with lit.
2024-02-09 15:54:22 +08:00
士子☀️ 2e1a753583 test 2024-02-09 15:41:34 +08:00
士子☀️ cbf6e4ecfb test 2024-02-09 15:26:20 +08:00
士子☀️ 5af471fe1a plugin 2024-02-09 13:23:52 +08:00
士子☀️ 0deb205faa plugin 2024-02-09 00:11:40 +08:00
士子☀️ 73eaf0ecaa plugin 2024-02-09 00:10:32 +08:00
士子☀️ 4791defbdd sniffing helper 2024-02-08 18:23:49 +08:00
士子☀️ 926249184b i18n 2024-02-08 15:12:08 +08:00
士子☀️ 659b2755ef feat: display language 2024-02-04 20:15:43 +08:00
士子☀️ e9d36edaa4 v2.0.3-beta.3 2024-01-22 23:01:56 +08:00
士子☀️ fdcdbf27eb dev tools 2024-01-22 22:25:29 +08:00
士子☀️ 2f5cc2eb80 Merge branch 'master' of https://github.com/caorushizi/m3u8-downloader 2024-01-21 19:11:42 +08:00
士子☀️ c087fa4956 webview 2024-01-21 19:10:11 +08:00
士子☀️ ed33ec6532 actions 2024-01-21 17:54:37 +08:00
士子☀️ 2d96c2730c .npmrc 2024-01-21 17:41:59 +08:00
士子☀️ 5bcec10be3 docs 2024-01-21 17:18:02 +08:00
士子☀️ 207bd15b14 .npmrc 2024-01-21 17:07:41 +08:00
士子☀️ 00f0bfc84b os version 2024-01-21 17:06:59 +08:00
士子☀️ 7bc3f83c88 update electron. 2024-01-21 16:56:59 +08:00
士子☀️ 1af2803c22 eslint 2024-01-21 01:20:02 +08:00
士子☀️ fd6a81abfd package version 2024-01-21 00:01:09 +08:00
士子☀️ a383093914 Merge pull request #71 from caorushizi/dependabot/npm_and_yarn/typescript-eslint/parser-6.19.0
build(deps-dev): bump @typescript-eslint/parser from 5.60.0 to 6.19.0
2024-01-20 23:29:33 +08:00
士子☀️ 13656429af Merge pull request #70 from caorushizi/dependabot/npm_and_yarn/better-sqlite3-9.3.0
build(deps): bump better-sqlite3 from 8.7.0 to 9.3.0
2024-01-20 23:29:11 +08:00
dependabot[bot] bc34881b38 build(deps-dev): bump @typescript-eslint/parser from 5.60.0 to 6.19.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.60.0 to 6.19.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-20 15:11:08 +00:00
dependabot[bot] e3b11cb087 build(deps): bump better-sqlite3 from 8.7.0 to 9.3.0
Bumps [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) from 8.7.0 to 9.3.0.
- [Release notes](https://github.com/WiseLibs/better-sqlite3/releases)
- [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v8.7.0...v9.3.0)

---
updated-dependencies:
- dependency-name: better-sqlite3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-20 15:11:00 +00:00
士子☀️ 8df78ca377 Merge pull request #61 from caorushizi/dependabot/npm_and_yarn/typescript-5.3.3
build(deps-dev): bump typescript from 5.1.6 to 5.3.3
2024-01-20 23:08:49 +08:00
士子☀️ f12919c6bc Update README.md 2024-01-16 23:02:29 +08:00
士子☀️ 00df324ae7 Merge pull request #66 from caorushizi/readme
readme
2024-01-15 22:25:23 +08:00
士子☀️ f14f9ffc1b readme 2024-01-15 22:23:40 +08:00
dependabot[bot] a388b725bc build(deps-dev): bump typescript from 5.1.6 to 5.3.3
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.1.6 to 5.3.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.1.6...v5.3.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-14 07:37:58 +00:00
caorushizi e64eaeefef macos 2024-01-14 15:35:15 +08:00
士子☀️ 6830d69b62 build script. 2024-01-14 12:09:08 +08:00
士子☀️ 8f609a8e93 checkout 2024-01-14 10:42:54 +08:00
士子☀️ 99c23f47c5 setup go 2024-01-14 10:36:14 +08:00
士子☀️ 95f9e20f39 go build 2024-01-14 01:34:08 +08:00
士子☀️ df1e664bc9 Merge pull request #60 from caorushizi/dev0114
add plugin
2024-01-14 01:25:05 +08:00
士子☀️ fec30a6453 add plugin 2024-01-14 01:24:24 +08:00
士子☀️ a048211486 add plugin 2024-01-14 01:14:02 +08:00
士子☀️ 465e9b2988 add plugin 2024-01-13 23:19:19 +08:00
士子☀️ af1ff19cb2 add web plugin 2024-01-13 20:28:58 +08:00
士子☀️ eae470f2ec video server 2024-01-13 12:24:52 +08:00
士子☀️ 1933799e32 feat: video list. 2024-01-13 01:03:46 +08:00
士子☀️ c7977bf7b4 Merge pull request #55 from caorushizi/dependabot/npm_and_yarn/electron-25.8.4
build(deps-dev): bump electron from 25.2.0 to 25.8.4
2023-12-29 16:40:43 +08:00
士子☀️ ce8407ce46 Merge pull request #58 from caorushizi/dependabot/npm_and_yarn/axios-1.6.0
build(deps): bump axios from 1.4.0 to 1.6.0
2023-12-29 16:40:01 +08:00
dependabot[bot] 5d313df190 build(deps): bump axios from 1.4.0 to 1.6.0
Bumps [axios](https://github.com/axios/axios) from 1.4.0 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.4.0...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-11 13:41:05 +00:00
dependabot[bot] c560a09904 build(deps-dev): bump electron from 25.2.0 to 25.8.4
Bumps [electron](https://github.com/electron/electron) from 25.2.0 to 25.8.4.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v25.2.0...v25.8.4)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-05 17:37:26 +00:00
士子☀️ df66a92989 Merge pull request #46 from caorushizi/feat/v2.1.0
m3u8
2023-08-22 14:49:01 +08:00
士子☀️ 52c9611e49 m3u8 2023-08-07 23:54:19 +08:00
士子☀️ 4018112a1d 优化二进制调用逻辑 2023-08-02 21:26:46 +08:00
士子☀️ 633a257e65 优化表单 2023-08-01 23:49:22 +08:00
caorushizi 0666d41226 工程优化 2023-07-29 14:14:37 +08:00
士子☀️ c2cfe55e24 Merge pull request #43 from caorushizi/dev/2.1.0 2023-07-29 13:43:37 +08:00
士子☀️ cb7fbddd7a 工程优化 2023-07-29 13:41:16 +08:00
士子☀️ 135d9fed1b 工程优化 2023-07-29 10:39:27 +08:00
士子☀️ 267aedc594 工程优化 2023-07-29 09:24:11 +08:00
士子☀️ 325d0da930 工程优化 2023-07-29 00:28:01 +08:00
士子☀️ 615bc74d15 Merge pull request #42 from caorushizi/dev/2.1.0 2023-07-27 17:59:32 +08:00
士子☀️ 1ca6935d36 download form. 2023-07-24 23:49:10 +08:00
士子☀️ 3bfb7139d3 download form. 2023-07-22 12:56:56 +08:00
士子☀️ 14d44765e5 form name 2023-07-22 00:03:58 +08:00
士子☀️ 807dd2d920 request 2023-07-21 23:39:02 +08:00
士子☀️ c9a0a7f7c2 request 2023-07-21 23:31:07 +08:00
士子☀️ a3eea6d501 v2.1.0 2023-07-20 23:25:33 +08:00
士子☀️ 4ce7dcac1b Merge pull request #38 from caorushizi/dev 2023-07-18 23:51:35 +08:00
士子☀️ 096762f24c some thing 2023-07-18 23:49:03 +08:00
士子☀️ baf358d223 something 2023-07-18 23:44:22 +08:00
士子☀️ e72973346b add: get web request. 2023-07-12 00:30:15 +08:00
士子☀️ 41258ecee8 add: get web request response. 2023-07-11 22:48:14 +08:00
士子☀️ b6a5be152a Merge pull request #37 from caorushizi/dev0709 2023-07-09 15:39:55 +08:00
士子☀️ d5d45c4935 fix: download use proxy. 2023-07-09 15:38:48 +08:00
士子☀️ 81eb4ecf09 Merge pull request #36 from caorushizi/dev0709 2023-07-09 15:09:23 +08:00
士子☀️ 5a4cc8c449 bin dir 2023-07-09 15:07:57 +08:00
士子☀️ e7ab664310 release: v2.0.2 2023-07-09 14:54:21 +08:00
士子☀️ 3cb2fa27b9 release: v2.0.2 2023-07-09 14:18:41 +08:00
士子☀️ e29e41aa4c release: v2.0.2 2023-07-09 14:01:15 +08:00
士子☀️ cc62613640 Merge pull request #35 from caorushizi/release/v2.0.2 2023-07-09 13:53:19 +08:00
士子☀️ 2720bcc685 release: v2.0.2 2023-07-09 13:52:40 +08:00
士子☀️ c8ac4182fc Merge pull request #34 from caorushizi/release/v2.0.2 2023-07-09 13:25:36 +08:00
士子☀️ 55b70d3e6d release: v2.0.2 2023-07-09 13:24:06 +08:00
士子☀️ a19765b0d3 fix: browser loading status. 2023-07-08 23:05:13 +08:00
士子☀️ 5c7b65385a remove postinstall 2023-07-08 16:34:34 +08:00
caorushizi b6b84269a3 fix: mac app. 2023-07-08 15:55:12 +08:00
士子☀️ e44cb249d0 Merge pull request #30 from caorushizi/feat/v2.0.2 2023-07-08 12:11:51 +08:00
士子☀️ 8bea071865 release size. 2023-07-08 12:00:50 +08:00
caorushizi a782652fca fix: tray icon. 2023-07-06 22:49:11 +08:00
caorushizi 3a02803d3c fix: mac download. 2023-07-06 22:16:28 +08:00
士子☀️ 263a08ab4c Merge pull request #29 from caorushizi/feat/v2.0.2 2023-07-06 00:32:55 +08:00
士子☀️ ecdbd985ce feat: tray context menu. 2023-07-06 00:32:05 +08:00
士子☀️ dd403564cc docs: addition function. 2023-07-06 00:15:57 +08:00
士子☀️ 69d741b39a Merge pull request #28 from caorushizi/dev 2023-07-05 23:53:59 +08:00
士子☀️ 4c9c2e2d68 add: tray icon. 2023-07-05 23:52:38 +08:00
士子☀️ 8660701340 download list. 2023-07-05 22:48:41 +08:00
士子☀️ 341c5570af support live mode. 2023-07-05 01:13:29 +08:00
士子☀️ ee12753818 Update README.md 2023-07-04 12:56:52 +08:00
士子☀️ 055638278c feat: something 2023-07-04 07:52:43 +08:00
士子☀️ bf06ff6482 docs: dark mode. 2023-07-02 15:32:34 +08:00
士子☀️ 459a5bc5b7 docs: comments dark theme. 2023-07-02 14:08:04 +08:00
士子☀️ a93f18037c release: v2.0.1 2023-07-02 13:50:22 +08:00
士子☀️ 67b5218002 release: v2.0.1 2023-07-02 13:38:46 +08:00
士子☀️ d3c745dda4 docs: update. 2023-07-02 13:19:31 +08:00
士子☀️ e694818f8f docs: support dark mode. 2023-07-02 13:10:30 +08:00
士子☀️ 729d7645e1 docs 2023-07-02 12:30:58 +08:00
士子☀️ 795e21df9c docs: content done. 2023-07-02 01:22:14 +08:00
士子☀️ bdc15e6c50 feat: max runner. 2023-07-01 23:02:42 +08:00
士子☀️ c3398ec572 baidu 2023-07-01 22:08:48 +08:00
士子☀️ 6b38b348fc actions 2023-07-01 21:37:43 +08:00
士子☀️ d650529842 actions 2023-07-01 21:32:21 +08:00
士子☀️ 2094408f52 actions 2023-07-01 21:28:26 +08:00
士子☀️ 89cb8b3a3a actions 2023-07-01 21:24:36 +08:00
士子☀️ 7a2d546972 actions. 2023-07-01 21:21:57 +08:00
士子☀️ 69e801ba2b Merge pull request #25 from caorushizi/dev
Dev
2023-07-01 21:16:54 +08:00
士子☀️ 14edc27ed1 add: docs and deploy. 2023-07-01 21:14:45 +08:00
士子☀️ 6f7fa7f067 add: docs 2023-07-01 20:15:40 +08:00
caorushizi a5a1c02e10 update: mac downloader 2023-07-01 19:01:51 +08:00
士子☀️ ce08cbce4b feat: some feature. 2023-07-01 18:12:35 +08:00
士子☀️ e03a5d38bb video header & web extension 2023-07-01 14:40:49 +08:00
士子☀️ 720db5a476 video headers & params format 2023-07-01 01:27:04 +08:00
士子☀️ e97783b08e Merge branch 'dev' of https://github.com/caorushizi/m3u8-downloader into dev 2023-06-30 00:45:34 +08:00
士子☀️ 12566f8668 add webview preload script. 2023-06-30 00:44:47 +08:00
caorushizi d686819417 npmrc 2023-06-29 19:26:47 +08:00
士子☀️ ea7b4a77af feat: prettier & web extension. 2023-06-28 23:53:46 +08:00
士子☀️ 6427a06879 test: do somthing. 2023-06-27 23:54:43 +08:00
士子☀️ f2f7a50e95 refactor: window service 2023-06-27 22:22:42 +08:00
caorushizi fbc57ebfca fix: webContents is null. 2023-06-27 11:46:55 +08:00
士子☀️ 26a5681dab feat: web extension. 2023-06-26 23:34:54 +08:00
士子☀️ 4640ff51e8 feat: add web extension. 2023-06-26 00:59:36 +08:00
203 changed files with 15957 additions and 10244 deletions
+39
View File
@@ -0,0 +1,39 @@
behaviour
commitlint
conventionalcommits
optimizelegibility
pinia
tiptap
vuedraggable
vuejs
TDAPP
reduxjs
bilibili
localforage
ahooks
antd
languagedetector
redownload
Redownload
watting
Watting
cancle
Sider
inversify
datetime
typeorm
bili
mediago
nocheck
metas
DDTHH
Conpty
execa
bodys
cliqz
adblocker
acodec
KHTML
svgz
stylelint
consola
+2 -4
View File
@@ -1,6 +1,4 @@
APP_NAME=media-downloader
APP_NAME=mediago
APP_ID=mediago.ziying.site
APP_COPYRIGHT=caorushizi
APP_VERSION=2.0.0
APP_SERVER_PORT=8433
APP_VERSION=2.2.0
+2 -1
View File
@@ -1,5 +1,6 @@
APP_TD_APPID=
APP_DOWNLOAD_DEBUG=true
APP_CLARITY_APPID=
GH_TOKEN=
LOAD_DEVTOOLS=
DEBUG_PLUGINS=true
+2 -1
View File
@@ -1,4 +1,5 @@
APP_TD_APPID=
APP_DOWNLOAD_DEBUG=
APP_CLARITY_APPID=
GH_TOKEN=
DEBUG_PLUGINS=false
+1
View File
@@ -0,0 +1 @@
+20
View File
@@ -0,0 +1,20 @@
/* eslint-env node */
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
overrides: [],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint"],
rules: {},
};
+1 -1
View File
@@ -5,7 +5,7 @@
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
+19 -6
View File
@@ -1,7 +1,7 @@
# build.yml
# Workflow's name
name: Build
name: Build Downloader App.
# Workflow's trigger
on:
@@ -23,21 +23,28 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
os: [windows-latest, macos-12]
# create steps
steps:
# step1: check out repository
- name: Check out git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# 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
- uses: pnpm/action-setup@v2
# step4: install python env
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
# step5: install pnpm env
- uses: pnpm/action-setup@v3
with:
version: latest
run_install: true
@@ -47,3 +54,9 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
APP_CLARITY_APPID: ${{ secrets.APP_CLARITY_APPID}}
- uses: actions/upload-artifact@v4
with:
name: mediago-${{ matrix.os }}
path: packages/main/release/mediago-setup-*
+52
View File
@@ -0,0 +1,52 @@
name: Deploy Downloader Docs.
on:
push:
branches: ["master"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
env:
BUCKET: downloader-docs
ENDPOINT: oss-cn-beijing.aliyuncs.com
ACCESS_KEY: LTAI5tLckcUrBtj7bCiUYwWz
ACCESS_KEY_SECRET: ${{ secrets.ACCESS_KEY_SECRET }}
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v3
with:
version: latest
run_install: true
- name: Build with Vitepress
run: pnpm run docs:build
- 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
+3 -1
View File
@@ -1,4 +1,6 @@
node_modules
.vscode
*.local
.idea
.parcel-cache
.vitepress
*.tsno.mjs
+1 -1
View File
@@ -10,4 +10,4 @@
}
]
}
}
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}
npm run commitlint ${1}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx tsx ./scripts/pre-commit.ts
+3 -2
View File
@@ -1,2 +1,3 @@
registry=https://registry.npmmirror.com/
electron_mirror=https://npm.taobao.org/mirrors/electron/
registry=https://registry.npmmirror.com
electron_mirror=https://npmmirror.com/mirrors/electron/
package-manager=pnpm
+3
View File
@@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage
+1
View File
@@ -0,0 +1 @@
{}
+23
View File
@@ -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}"
}
]
}
+61 -24
View File
@@ -1,47 +1,84 @@
# media-downloader
<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"/>
m3u8 视频在线提取工具 流媒体下载 m3u8 下载 桌面客户端 windows mac。
可以直接在线获取 m3u8 链接地址,无需使用使用网络抓包,无需安装浏览器插件,可以直接带出请求标头……
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<br>
<br>
模板: <https://github.com/caorushizi/electron-template>
<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>
以下指南将帮助你在本地机器上安装和运行该项目,进行开发和测试。关于如何将该项目部署到在线环境,请参考部署小节。
## Intro
【使用帮助】[blog.ziying.site](https://ziying.site/post/media-downloader-how-to-use/?from=github)
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
## 安装要求
- **✅&nbsp; 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
- **📱&nbsp; 移动播放**: 可以轻松无缝的在PC和移动设备之前切换,下载完成后即可使用手机观看视频。
- **⚡️&nbsp; 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
## Quickstart
运行代码需要 node 、 pnpm 和 go,node、go 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
## 运行代码
安装依赖 `pnpm i`
```shell
# 代码下载
git clone https://github.com/caorushizi/mediago.git
开发环境 `pnpm run dev`
# 安装依赖
pnpm i
打包运行 `pnpm run release`
# 开发环境
pnpm dev
## 下载链接
# 打包运行
pnpm release
```
v2.0.0-beta.5 添加视频播放功能,支持移动设备播放
## Releases
- 【2023.06.18 v2.0.0-beta.6 windows】[media-downloader-setup-2.0.0-beta.6.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.6/media-downloader-setup-2.0.0-beta.6.exe)
- 【2023.06.18 v2.0.0-beta.6 macos】[media-downloader-windows-v2.0.0-beta.6.dmg](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.6/media-downloader-setup-2.0.0-beta.6.dmg)
- 【2023.06.18 v2.0.0-beta.5 windows】[media-downloader-setup-2.0.0-beta.5.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.5/media-downloader-setup-2.0.0-beta.5.exe)
- 【2023.06.18 v2.0.0-beta.5 macos】[media-downloader-windows-v2.0.0-beta.5.dmg](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.5/media-downloader-setup-2.0.0-beta.5.dmg)
- 【2023.04.01 v2.0.0-beta.4 windows】 [media-downloader-windows-v2.0.0-beta.4.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.4/media-downloader-setup-2.0.0-beta.4.exe)
- 【2023.04.01 v2.0.0-beta.4 macos】 [media-downloader-windows-v2.0.0-beta.4.dmg](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.4/media-downloader-setup-2.0.0-beta.4.dmg)
- 【2022.01.08 v1.1.5】 [media-downloader-windows-v1.1.5.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.5.exe)
**v2.0.22023.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)
**更新日志**
- 暗黑模式
- 更多下载配置
- 支持请求标头自动带入
- 支持开启广告过滤
- 支持开启沉浸式嗅探
- 支持切换手机和 PC 模式
- 支持修改同时下载数量
- 修复了一些 bug
**v1.1.52022.2.5 发布)**
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
**更新日志**
## 软件截图
![首页](https://raw.githubusercontent.com/caorushizi/m3u8-downloader/master/screenshot/home-page.png)
![首页](https://static.ziying.site/v2.0.3-beta-home.png)
![设置页面](https://raw.githubusercontent.com/caorushizi/m3u8-downloader/master/screenshot/setting-page.png)
![设置页面](https://static.ziying.site/v2.0.3-beta-settings.png)
![资源提取](https://raw.githubusercontent.com/caorushizi/m3u8-downloader/master/screenshot/browser-page.png)
![资源提取](https://static.ziying.site/v2.0.3-beta-extract.png)
## 技术栈
+100
View File
@@ -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
View File
@@ -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"
]
}
+2
View File
@@ -0,0 +1,2 @@
.vitepress/dist
.vitepress/cache
+62
View File
@@ -0,0 +1,62 @@
import { HeadConfig, defineConfig } from "vitepress";
const isDev = process.env.NODE_ENV === "development";
const head: HeadConfig[] = [
["link", { rel: "shortcut icon", href: "/favicon.svg" }],
];
if (!isDev) {
head.push([
"script",
{},
`var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?eefcbd14f0323044aa0ca678cd278381";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();`,
]);
}
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "MediaGo",
description: "简单易用,快速下载",
lastUpdated: true,
head,
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Home", link: "/" },
{ text: "教程", link: "/guides" },
],
sidebar: [
{
text: "开始",
items: [
{ text: "快速开始", link: "/guides" },
{ text: "使用说明", link: "/documents" },
],
},
{
text: "其他",
items: [
{ text: "旧版本", link: "/history" },
{ text: "意见收集", link: "/proposal" },
{ text: "支持列表", link: "/list" },
],
},
],
socialLinks: [
{ icon: "github", link: "https://github.com/caorushizi/m3u8-downloader" },
],
footer: {
message: '<a href="https://beian.miit.gov.cn">豫ICP备20012967号-2</a>',
copyright: "Copyright © 2021-present caorushizi",
},
},
});
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { onMounted, watch, ref, watchEffect } from "vue";
import { init, WalineInstance } from "@waline/client";
import "@waline/client/waline.css";
import { useData, useRoute } from "vitepress";
const route = useRoute();
const { isDark } = useData();
const commentsRef = ref<WalineInstance | null>(null);
watchEffect(() => {
commentsRef.value?.update({
dark: isDark.value,
});
});
watch(
() => route.path,
() => {
commentsRef.value?.destroy();
initWaline();
}
);
function initWaline() {
commentsRef.value = init({
el: "#waline",
serverURL: "https://comments.ziying.site",
dark: isDark.value,
lang: "zh-CN",
pageview: true,
});
}
onMounted(() => {
initWaline();
});
</script>
<template>
<div id="waline">hello world</div>
</template>
<style scoped>
#waline {
margin-top: 20px;
}
</style>
+13
View File
@@ -0,0 +1,13 @@
import { h } from "vue";
import Theme from "vitepress/theme";
import "./style/var.css";
import Comments from "./components/Comments.vue";
export default {
...Theme,
Layout() {
return h(Theme.Layout, null, {
"doc-after": () => h(Comments),
});
},
};
+32
View File
@@ -0,0 +1,32 @@
:root {
/* 标题 */
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(
135deg,
#5e9ef3 10%,
#2a82f6 100%
);
/* 图标背景 */
--vp-home-hero-image-background-image: linear-gradient(
135deg,
#5e9ef3 10%,
#2a82f6 100%
);
--vp-home-hero-image-filter: blur(150px);
/* brand按钮 */
--vp-button-brand-border: #5e9ef3;
--vp-button-brand-text: #f2f7fd;
--vp-button-brand-bg: #2a82f6;
--vp-button-brand-active-border: #5e9ef3;
--vp-button-brand-active-text: #fff;
--vp-button-brand-active-bg: #409eff;
--vp-button-brand-hover-border: #5e9ef3;
--vp-button-brand-hover-text: #fff;
--vp-button-brand-hover-bg: #2a82f6;
--vp-c-brand: #2a82f6;
}
+83
View File
@@ -0,0 +1,83 @@
---
layout: doc
outline: deep
---
# 使用说明
这个页面主要讲解的是设置页面的参数的含义
![step 1](./images/documents-step1.png)
## 基础设置
::: tip
下载器全局设置
:::
1. 选择文件夹: 视频下载的路径
2. 下载器主题: 支持浅色和深色模式
3. 在新窗口中打开浏览器: 开启了之后浏览器页面会独立出一个窗口。
![step 1](./images/documents-step2.png)
4. 下载完成提示: 开启之后下载完成会出现系统提示
## 浏览器设置
::: tip
浏览器窗口相关的设置
:::
1. 代理设置: 填写自己的代理地址
2. 代理开关: 打开开关后浏览器默认会走填写的代理地址,如果代理地址这个开关是打不开的
3. 开启广告过滤: 打开开关后会过滤页面中的广告
4. 以手机模式进入: 打开后会将浏览器伪装成手机浏览器,请求移动端的站点(左侧是开启手机模式的截图)
![step 2](./images/documents-step3.png)
5. 使用浏览器插件
- 打开状态: 从浏览器中嗅探到的资源不会自动添加到下载列表,需要手动点击页面下方的下载的链接来添加视频到下载列表 **(后面会有很强大的功能哦)**
![step 3](./images/documents-step4.png)
- 关闭状态: 浏览器中嗅探到的资源会自动添加到下载列表
## 下载设置
::: tip
和下载相关的设置
:::
1. 下载完成删除分片: 开启后下载完成后会将分片文件删除
2. 最大同时下载数量: 最多同时可以下载多少视频文件,目前最大是 10,最小是 1
3. 更多操作
- 配置文件目录: 软件数据库路径、日志等等路径
- 可执行文件目录: 下载器二进制文件的路径
- 本地存储路径: 下载视频的本地路径
4. 当前版本: 当前软件版本信息
## 其他问题
**Q: 关于直播视频的下载**
A: 软件支持直播下载,软件目前没有好的办法区分直播流,所以将所有的下载控制台开启。用户自行分辨下载数据源
**Q: 关于 MacOS 版本**
A: 目前 MacOS 版本属于非稳定版本,2023 年 5 月发布。需要在 Mac 的安全设置中打开来自身份不明开发者的 Mac App
![QA1](./images/documents-qa1.png)
**Q: 关于旧版本**
A: 1.1.5 版本确实发布了很长时间,经过大量用户的验证,比较稳定。所以想要使用旧版软件的话请访问[此链接](/history.html)
**Q: 关于 Win7 用户**
A: v2.0.0 之后的版本已经放弃不支持 Win7 如果需要在 win7 上面使用同样需要下载 1.1.5 版本
+96
View File
@@ -0,0 +1,96 @@
---
layout: doc
outline: deep
---
# 快速开始
这篇文章是软件的简单说明,可以快速上手使用本本软件。
::: tip
我建了一个 QQ 群,方便大家一起沟通交流: 574209001
:::
## 下载安装
**v2.0.12023.7.1 发布)**
- [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)
**更新日志**
- 暗黑模式
- 更多下载配置
- 支持请求标头自动带入
- 支持开启广告过滤
- 支持开启沉浸式嗅探
- 支持切换手机和 PC 模式
- 支持修改同时下载数量
- 修复了一些 bug
**v1.1.52022.2.5 发布)**
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
**更新日志**
- 支持视频下载
## 操作说明
### 视频自动嗅探
1. 选择【资源提取】
![step 1](./images/guides-step1.png)
2. 进入视频的网址
![step 2](./images/guides-step2.png)
3. 开始下载,即可下载视频
![step 3](./images/guides-step3.png)
### 手动下载
1. 点击页面右上角的【新建下载】
![step 1](./images/guides-step4.png)
2. 在新建下载弹窗中填入【视频名称】和【m3u8 地址】即可
![step 2](./images/guides-step5.png)
3. 在列表中点击下载,即可下载视频
![step 3](./images/guides-step3.png)
## 附加功能
1. 下载完成后可以在下载列表中点击转换音频按钮将视频转换为音频
![step 1](./images/addition-step1.png)
2. 下载完成后播放视频
- PC 播放
![step 2](./images/addition-step2.png)
- 移动端播放
![step 3](./images/addition-step3.png)
3. 批量下载
![step 3](./images/addition-step4.png)
## 开始你的视频下载吧
是不是很简单,快去下载你的视频吧
::: warning
本软件仅可用于学习交流
:::
+21
View File
@@ -0,0 +1,21 @@
---
layout: doc
outline: deep
---
# 旧版本
::: info
这里是旧版本的永久链接,如果有需求的话,可以在这里下载
**这个链接的软件不会自动升级, 可放心使用**
:::
**v1.1.52022.2.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 无~~
**更新日志**
- 支持视频下载
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+30
View File
@@ -0,0 +1,30 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "在线视频下载"
text: "简单易用,快速下载"
tagline: 简单易学,无需抓包,不需要安装插件
image:
src: /home.png
alt: home
actions:
- theme: brand
text: 快速开始
link: /guides
- theme: alt
text: 使用说明
link: /documents
features:
- icon: ⏩
title: 无需抓包
details: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
- icon: 📱
title: 移动播放
details: 可以轻松无缝的在PC和移动设备之前切换,下载完成后即可使用手机观看视频。
- icon: ⚡️
title: 支持批量下载
details: 支持同时下载多个视频和直播资源,高速带宽不闲置。
---
+22
View File
@@ -0,0 +1,22 @@
---
layout: doc
outline: deep
---
# 支持列表
::: tip
大家有什么网站想要支持的在这里提
:::
**模板**
---
- 网站首页: xxx
- 视频(直播)列表页: xxx
- 视频(直播)页: xxx
---
支持完成后我会在评论区回复
+16
View File
@@ -0,0 +1,16 @@
---
layout: doc
outline: deep
---
# 意见收集
::: info
这里手机大家使用软件的问题,或者意见建议
:::
如果软件报错,需要描述清楚在什么网站下载什么视频的时候报错
如果是建议请描述清楚你的需求
如果是意见请尽量提出来,我会尽量满足大家的需求
+40
View File
@@ -0,0 +1,40 @@
<svg t="1686326276609" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="6962" xmlns:xlink="http://www.w3.org/1999/xlink"
width="200" height="200">
<path
d="M110.933333 42.666667h716.8A68.266667 68.266667 0 0 1 896 110.933333v802.133334a68.266667 68.266667 0 0 1-68.266667 68.266666H110.933333A68.266667 68.266667 0 0 1 42.666667 913.066667V110.933333A68.266667 68.266667 0 0 1 110.933333 42.666667z"
fill="#111314" fill-opacity=".72" p-id="6963"></path>
<path
d="M853.333333 42.666667h85.333334a68.266667 68.266667 0 0 1 68.266666 68.266666v802.133334A68.266667 68.266667 0 0 1 938.666667 981.333333h-85.333334a68.266667 68.266667 0 0 0 68.266667-68.266666V110.933333A68.266667 68.266667 0 0 0 853.333333 42.666667z"
fill="#CACBCC" p-id="6964"></path>
<path
d="M162.133333 128h614.4a34.133333 34.133333 0 0 1 34.133334 34.133333v699.733334a34.133333 34.133333 0 0 1-34.133334 34.133333H162.133333a34.133333 34.133333 0 0 1-34.133333-34.133333V162.133333a34.133333 34.133333 0 0 1 34.133333-34.133333z"
fill="#FFFFFF" p-id="6965"></path>
<path
d="M187.733333 170.666667h563.2c9.386667 0 17.066667 7.68 17.066667 17.066666v221.866667a17.066667 17.066667 0 0 1-17.066667 17.066667H187.733333a17.066667 17.066667 0 0 1-17.066666-17.066667V187.733333c0-9.386667 7.68-17.066667 17.066666-17.066666z"
fill="#EDEEF0" p-id="6966"></path>
<path
d="M256 298.666667m17.066667 0l136.533333 0q17.066667 0 17.066667 17.066666l0 136.533334q0 17.066667-17.066667 17.066666l-136.533333 0q-17.066667 0-17.066667-17.066666l0-136.533334q0-17.066667 17.066667-17.066666Z"
fill="#FFC13C" p-id="6967"></path>
<path
d="M341.333333 392.533333a34.133333 34.133333 0 1 1 0-68.266666 34.133333 34.133333 0 0 1 0 68.266666z m65.109334 56.192H276.224C276.224 426.666667 276.224 401.066667 341.333333 401.066667s65.109333 25.6 65.109334 47.658666z"
fill="#FFFFFF" p-id="6968"></path>
<path
d="M469.333333 341.333333m17.066667 0l136.533333 0q17.066667 0 17.066667 17.066667l0 8.533333q0 17.066667-17.066667 17.066667l-136.533333 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
fill="#5BADFF" p-id="6969"></path>
<path
d="M512 554.666667m17.066667 0l179.2 0q17.066667 0 17.066666 17.066666l0 51.2q0 17.066667-17.066666 17.066667l-179.2 0q-17.066667 0-17.066667-17.066667l0-51.2q0-17.066667 17.066667-17.066666Z"
fill="#EDEEF0" p-id="6970"></path>
<path
d="M512 682.666667m17.066667 0l179.2 0q17.066667 0 17.066666 17.066666l0 51.2q0 17.066667-17.066666 17.066667l-179.2 0q-17.066667 0-17.066667-17.066667l0-51.2q0-17.066667 17.066667-17.066666Z"
fill="#EDEEF0" p-id="6971"></path>
<path
d="M213.333333 554.666667m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066666l0 8.533334q0 17.066667-17.066666 17.066666l-221.866667 0q-17.066667 0-17.066667-17.066666l0-8.533334q0-17.066667 17.066667-17.066666Z"
fill="#EDEEF0" p-id="6972"></path>
<path
d="M213.333333 640m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066667l0 8.533333q0 17.066667-17.066666 17.066667l-221.866667 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
fill="#EDEEF0" p-id="6973"></path>
<path
d="M213.333333 725.333333m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066667l0 8.533333q0 17.066667-17.066666 17.066667l-221.866667 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
fill="#EDEEF0" p-id="6974"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+48 -8
View File
@@ -1,20 +1,60 @@
{
"name": "electron-template",
"name": "mediago",
"version": "0.1.0",
"description": "electron-template",
"main": "index.js",
"description": "mediago",
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm --parallel -F \"./packages/*\" run dev",
"build": "pnpm --parallel -F \"./packages/*\" run build",
"pack": "pnpm -F media-downloader run pack",
"release": "pnpm run build && pnpm -F media-downloader run release"
"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",
"prerelease": "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: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",
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"devDependencies": {
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@commitlint/cz-commitlint": "^18.6.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@waline/client": "^3.1.3",
"commitizen": "^4.3.0",
"cross-env": "^7.0.3",
"cspell": "^8.3.2",
"cz-git": "^1.8.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"husky": "^9.0.10",
"prettier": "3.2.5",
"tsx": "^4.7.1",
"vitepress": "1.0.1",
"vue": "^3.4.15",
"zx": "^7.2.3"
},
"pnpm": {
"patchedDependencies": {
"@cliqz/adblocker@1.26.6": "patches/@cliqz__adblocker@1.26.6.patch"
"@cliqz/adblocker@1.27.3": "patches/@cliqz__adblocker@1.27.3.patch"
}
}
}
+1
View File
@@ -0,0 +1 @@
./types
+4 -2
View File
@@ -6,16 +6,18 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended",
],
overrides: [],
ignorePatterns: ["app"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "prettier"],
plugins: ["@typescript-eslint"],
rules: {
"prettier/prettier": "warn",
"@typescript-eslint/no-explicit-any": "warn",
},
};
+2
View File
@@ -3,3 +3,5 @@ node_modules
build
dist
bin/Logs
release
types
+1
View File
@@ -0,0 +1 @@
types/
+1 -1
View File
@@ -1 +1 @@
{}
{}
+5
View File
@@ -0,0 +1,5 @@
node_modules
output
bin
mobile
plugin
+16
View File
@@ -0,0 +1,16 @@
{
"name": "mediago",
"version": "2.2.0",
"description": "在线视频下载器",
"main": "main/index.js",
"author": "",
"license": "MIT",
"dependencies": {
"@cliqz/adblocker-electron": "^1.27.3",
"@cliqz/adblocker-electron-preload": "^1.27.3",
"node-pty": "^1.0.0"
},
"devDependencies": {
"electron": "30.0.6"
}
}
+58 -55
View File
@@ -1,74 +1,77 @@
{
"name": "media-downloader",
"version": "2.0.0",
"name": "main",
"version": "0.1.0",
"description": "在线视频下载器",
"main": "main/index.js",
"scripts": {
"postinstall": "electron-builder install-app-deps",
"rebuild": "electron-rebuild -f -w better-sqlite3",
"dev": "cross-env NODE_ENV=development node script/dev.mjs",
"build": "cross-env NODE_ENV=production node script/build.mjs",
"pack": "cross-env NODE_ENV=development node script/release.mjs",
"release": "cross-env NODE_ENV=production node script/release.mjs"
"postinstall": "electron-rebuild -f -w",
"dev": "cross-env NODE_ENV=development VITE_CJS_IGNORE_WARNING=true tsx scripts/dev.ts",
"build": "cross-env NODE_ENV=production tsx scripts/build.ts",
"pack": "cross-env NODE_ENV=development tsx scripts/release.ts",
"types": "tsc",
"types:watch": "tsc -w",
"release": "cross-env NODE_ENV=production tsx scripts/release.ts",
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@electron/rebuild": "^3.2.13",
"@rushstack/eslint-patch": "^1.3.2",
"@types/fs-extra": "^11.0.1",
"@types/koa": "^2.13.6",
"@types/koa-range": "^0.3.2",
"@types/koa-send": "^4.1.3",
"@types/koa__cors": "^4.0.0",
"@types/koa__router": "^12.0.0",
"@types/lodash": "^4.14.195",
"@types/node": "^20.3.1",
"@typescript-eslint/eslint-plugin": "5.60.0",
"@typescript-eslint/parser": "5.60.0",
"@electron/rebuild": "^3.6.0",
"@rushstack/eslint-patch": "^1.7.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.14.202",
"@types/mime-types": "^2.1.4",
"@types/node": "^20.11.5",
"@types/node-fetch": "^2.6.11",
"@types/semver": "^7.5.6",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"chokidar": "^3.5.3",
"consola": "^3.2.3",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"electron": "^25.2.0",
"electron-builder": "^24.4.0",
"electron-updater": "^6.1.1",
"esbuild": "^0.18.6",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard-with-typescript": "^35.0.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.0.0",
"eslint-plugin-prettier": "^4.2.1",
"dotenv": "^16.3.2",
"electron": "30.0.6",
"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": "2.8.8",
"semver": "^7.5.2",
"typescript": "^5.1.3"
"prettier": "3.2.5",
"semver": "^7.5.4",
"tsx": "^4.7.1",
"typescript": "^5.3.3",
"vite": "^5.0.12"
},
"dependencies": {
"@cliqz/adblocker-electron": "^1.26.6",
"@cliqz/adblocker-electron-preload": "^1.26.6",
"@koa/cors": "^4.0.0",
"@koa/router": "^12.0.0",
"@types/koa-static": "^4.0.2",
"axios": "^1.4.0",
"better-sqlite3": "^8.4.0",
"cross-fetch": "^3.1.6",
"dayjs": "^1.11.8",
"@cliqz/adblocker-electron": "^1.27.3",
"@cliqz/adblocker-electron-preload": "^1.27.3",
"axios": "^1.6.5",
"better-sqlite3": "^9.3.0",
"cheerio": "1.0.0-rc.12",
"dayjs": "^1.11.10",
"electron-devtools-installer": "^3.2.0",
"electron-is-dev": "^2.0.0",
"electron-log": "^4.4.8",
"electron-is-dev": "^3.0.1",
"electron-log": "^5.0.3",
"electron-store": "^8.1.0",
"fs-extra": "^11.1.1",
"glob": "^10.3.0",
"iconv-lite": "^0.6.3",
"inversify": "^6.0.1",
"koa": "^2.14.2",
"koa-range": "^0.3.0",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"execa": "^8.0.1",
"fs-extra": "^11.2.0",
"inversify": "^6.0.2",
"lint-staged": "^15.2.2",
"lodash": "^4.17.21",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.17"
"mime-types": "^2.1.35",
"node-fetch": "^3.3.2",
"node-pty": "^1.0.0",
"reflect-metadata": "^0.2.1",
"strip-ansi": "^7.1.0",
"typeorm": "^0.3.19"
}
}
-30
View File
@@ -1,30 +0,0 @@
import * as esbuild from "esbuild";
import { rmSync } from "node:fs";
import { mainResolve, loadDotEnvDefined } from "./utils.mjs";
const mainDefined = loadDotEnvDefined();
rmSync(mainResolve("build"), { recursive: true, force: true });
rmSync(mainResolve("dist"), { recursive: true, force: true });
esbuild.build({
entryPoints: [mainResolve("src/index.ts"), mainResolve("src/preload.ts")],
bundle: true,
platform: "node",
sourcemap: false,
target: ["node16.13"],
external: [
"electron",
"nock",
"aws-sdk",
"mock-aws-s3",
"@cliqz/adblocker-electron-preload",
],
define: {
"process.env.NODE_ENV": '"production"',
...mainDefined,
},
outdir: mainResolve("build/main"),
loader: { ".png": "file" },
minify: true,
});
-90
View File
@@ -1,90 +0,0 @@
import { spawn } from "child_process";
import { cpSync } from "node:fs";
import electron from "electron";
import * as esbuild from "esbuild";
import chokidar from "chokidar";
import { loadDotEnvRuntime, mainResolve, log } from "./utils.mjs";
let electronProcess = null;
process.env.NODE_ENV = "development";
loadDotEnvRuntime();
async function copySource() {
const oldSqlite3Path = mainResolve(
"node_modules/better-sqlite3/build/Release"
);
const newSqlite3Path = mainResolve("build/Release");
cpSync(oldSqlite3Path, newSqlite3Path, {
recursive: true,
});
const oldBinPath = mainResolve("bin");
const newBinPath = mainResolve("build/bin");
cpSync(oldBinPath, newBinPath, {
recursive: true,
});
}
const ctx = await esbuild.context({
entryPoints: [mainResolve("src/index.ts"), mainResolve("src/preload.ts")],
bundle: true,
platform: "node",
sourcemap: true,
target: ["node16.13"],
external: [
"electron",
"mock-aws-s3",
"aws-sdk",
"nock",
"@cliqz/adblocker-electron-preload",
],
define: {
// 开发环境中二进制可执行文件的路径
__bin__: `"${mainResolve("bin").replace(/\\/g, "\\\\")}"`,
},
plugins: [],
outdir: mainResolve("build/main"),
loader: { ".png": "file" },
});
const watcher = chokidar.watch("./src");
watcher.on("change", async () => {
await ctx.rebuild();
log("watch build succeed.");
if (electronProcess && electronProcess.kill) {
if (process.platform === "darwin") {
spawn("kill", ["-9", electronProcess.pid]);
} else {
process.kill(electronProcess.pid);
}
electronProcess = null;
startElectron();
}
});
function startElectron() {
const args = ["--inspect=5858", mainResolve("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));
});
}
try {
await ctx.rebuild();
await copySource();
await startElectron();
} catch (e) {
console.error(e);
process.exit();
}
-122
View File
@@ -1,122 +0,0 @@
import builder from "electron-builder";
import { readFileSync } from "node:fs";
import semver from "semver";
import { mainResolve, loadDotEnvRuntime, log } from "./utils.mjs";
const packageJson = JSON.parse(readFileSync(mainResolve("./package.json")));
loadDotEnvRuntime();
if (semver.neq(process.env.APP_VERSION, packageJson.version)) {
log("请先同步构建版本和发布版本");
process.exit(0);
}
const extraResources = [
{
from: "build/mobile",
to: "mobile",
},
];
if (process.platform === "win32") {
// windows
extraResources.push("bin/ffmpeg.exe", "bin/N_m3u8DL-CLI_v3.0.2.exe");
} else {
// mac
extraResources.push("bin/N_m3u8DL-RE", "bin/ffmpeg");
}
// Let's get that intellisense working
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
const options = {
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: "./dist",
},
files: [
{
from: "./build",
to: "./",
filter: ["**/*"],
},
"./package.json",
{
from: "./node_modules/better-sqlite3/build/Release",
to: "./build/Release",
},
],
extraResources,
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",
},
};
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);
}
-65
View File
@@ -1,65 +0,0 @@
import { existsSync } from "node:fs";
import dotenv from "dotenv";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
// FIXME: 有没有什么办法可以不用这么写?
const con = console;
export const log = con.log;
const __dirname = dirname(fileURLToPath(import.meta.url));
export const mainResolve = (r) => resolve(__dirname, "..", r);
export const rootResolve = (r) => resolve(__dirname, "../../..", r);
const nodeEnv = process.env.NODE_ENV;
log("当前的环境是: ", nodeEnv);
function loadEnv(path) {
const result = {};
const _loadEnv = (path) => {
if (!existsSync(path)) {
return null;
}
const { error, parsed } = dotenv.config({ path });
if (error != null) {
return null;
}
Object.keys(parsed).forEach((key) => {
result[key] = parsed[key];
});
};
_loadEnv(path);
_loadEnv(`${path}.local`);
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((prev, cur) => {
if (!cur.startsWith("APP_")) return prev;
prev[`process.env.${[cur]}`] = JSON.stringify(env[cur]);
return prev;
}, {});
}
+59
View File
@@ -0,0 +1,59 @@
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,
plugins: [],
});
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,
});
+8
View File
@@ -0,0 +1,8 @@
export const external = [
"electron",
"nock",
"aws-sdk",
"mock-aws-s3",
"@cliqz/adblocker-electron-preload",
"node-pty",
];
+101
View File
@@ -0,0 +1,101 @@
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import electron from "electron";
import * as esbuild from "esbuild";
import chokidar from "chokidar";
import { loadDotEnvRuntime, mainResolve, copyResource } from "./utils";
import { external } from "./config";
import consola from "consola";
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) => {
consola.log(String(data));
});
electronProcess.stderr.on("data", (data) => {
consola.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();
consola.log("watch build succeed.");
restartElectron();
});
try {
await mainContext.rebuild();
await preloadContext.rebuild();
await copySource();
await startElectron();
} catch (e) {
console.error(e);
process.exit();
}
}
start();
+115
View File
@@ -0,0 +1,115 @@
import { loadDotEnvRuntime, mainResolve, removeResource } from "./utils";
import * as builder from "electron-builder";
import semver from "semver";
import pkg from "../app/package.json";
import consola from "consola";
removeResource([mainResolve("release")]);
loadDotEnvRuntime();
if (semver.neq(process.env.APP_VERSION || "", pkg.version)) {
consola.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}",
// FIXME: 这里屏蔽 node-pty 自动重构,因为会导致打包失败
npmRebuild: false,
directories: {
output: "./release",
},
files: [
{
from: "./build",
to: "./",
},
"./package.json",
],
extraResources: [
{
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: "mediago",
owner: "caorushizi",
releaseType: "release",
},
};
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) {
consola.log(e);
}
}
start();
+72
View File
@@ -0,0 +1,72 @@
import { existsSync, cpSync, rmSync } from "fs";
import dotenv from "dotenv";
import { resolve } from "path";
import consola from "consola";
import fs from "fs-extra";
export const mainResolve = (...r: string[]) => resolve(__dirname, "..", ...r);
export const rootResolve = (...r: string[]) =>
resolve(__dirname, "../../..", ...r);
const nodeEnv = process.env.NODE_ENV;
consola.log("当前的环境是: ", nodeEnv);
function loadEnv(path: string) {
const result: Record<string, string> = {};
if (!existsSync(path)) {
return null;
}
const parsed = dotenv.parse(fs.readFileSync(path));
if (!parsed) {
return null;
}
Object.keys(parsed).forEach((key) => {
result[key] = parsed[key];
});
return result;
}
function loadDotEnv() {
const env = loadEnv(rootResolve(".env"));
const envMode = loadEnv(rootResolve(`.env.${nodeEnv}`));
const envModeLocal = loadEnv(rootResolve(`.env.${nodeEnv}.local`));
return { ...env, ...envMode, ...envModeLocal };
}
export function loadDotEnvRuntime() {
const env = loadDotEnv();
Object.keys(env).forEach((key) => {
if (process.env[key] != null || !env[key]) return;
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 });
});
}
+60 -40
View File
@@ -1,58 +1,61 @@
import { inject, injectable } from "inversify";
import {
DatabaseService,
DownloadStatus,
IpcHandlerService,
MainWindowService,
ProtocolService,
UpdateService,
VideoRepository,
WebviewService,
type App,
WebService,
DevToolsService,
StoreService,
} from "./interfaces";
import { DownloadStatus } from "./interfaces";
import { TYPES } from "./types";
import { app, nativeTheme } from "electron";
import { Menu, Tray, app, nativeImage, nativeTheme } from "electron";
import TrayIcon from "./tray-icon.png";
import path from "path";
import MainWindow from "./windows/MainWindow";
import WebviewService from "./services/WebviewService";
import VideoRepository from "./repository/VideoRepository";
import ElectronDevtools from "./vendor/ElectronDevtools";
import ElectronStore from "./vendor/ElectronStore";
import ElectronUpdater from "./vendor/ElectronUpdater";
import TypeORM from "./vendor/TypeORM";
import ProtocolService from "./core/protocol";
import IpcHandlerService from "./core/ipc";
@injectable()
export default class ElectronApp implements App {
export default class ElectronApp {
constructor(
@inject(TYPES.MainWindowService)
private readonly mainWindow: MainWindowService,
@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.WebService)
private readonly webService: WebService,
@inject(TYPES.StoreService)
private readonly storeService: StoreService
@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.webService.init();
this.webview.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();
@@ -60,13 +63,30 @@ export default class ElectronApp implements App {
this.initAppTheme();
this.resetDownloadStatus();
this.initTray();
}
initAppTheme(): void {
const theme = this.storeService.get("theme");
const theme = this.store.get("theme");
nativeTheme.themeSource = theme;
}
initTray() {
const iconPath = path.resolve(__dirname, TrayIcon);
const icon = nativeImage.createFromPath(iconPath);
const tray = new Tray(icon);
tray.setToolTip("在线视频下载");
tray.addListener("click", () => {
this.mainWindow.init();
});
const contextMenu = Menu.buildFromTemplate([
{ label: "显示主窗口", click: () => this.mainWindow.init() },
{ label: "退出 app", role: "quit" },
]);
tray.setContextMenu(contextMenu);
}
// 如果重启后还有正在下载的视频,就将状态改成下载失败
async resetDownloadStatus(): Promise<void> {
// 重启后如果还有 downloading 状态的数据, 全部重置为失败
@@ -74,7 +94,7 @@ export default class ElectronApp implements App {
const videoIds = videos.map((video) => video.id);
await this.videoRepository.changeVideoStatus(
videoIds,
DownloadStatus.Failed
DownloadStatus.Failed,
);
}
}
@@ -1,44 +1,61 @@
import { IpcMainEvent } from "electron/main";
import { inject, injectable } from "inversify";
import { handle } from "../helper/decorator";
import { handle } from "../helper";
import {
StoreService,
LoggerService,
type Controller,
VideoRepository,
DownloadItem,
DownloadItemPagination,
DownloadService,
Task,
DownloadStatus,
} from "../interfaces";
import { TYPES } from "../types";
import { spawnDownload } from "helper";
import MainWindowServiceImpl from "services/MainWindowServiceImpl";
import MainWindow from "../windows/MainWindow";
import ElectronStore from "../vendor/ElectronStore";
import DownloadService from "../services/DownloadService";
import VideoRepository from "../repository/VideoRepository";
import WebviewService from "../services/WebviewService";
@injectable()
export default class DownloadController implements Controller {
constructor(
@inject(TYPES.LoggerService)
private readonly logger: LoggerService,
@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.MainWindowService)
private readonly mainWindow: MainWindowServiceImpl
@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) {
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[]) {
const items = await this.videoRepository.addVideos(videos);
// 这里向页面发送消息,通知页面更新
this.mainWindow.send("download-item-notifier", items);
return items;
}
@handle("edit-download-item")
async editDownloadItem(e: IpcMainEvent, video: DownloadItem) {
const item = await this.videoRepository.editVideo(video);
@@ -56,31 +73,30 @@ export default class DownloadController implements Controller {
@handle("get-download-items")
async getDownloadItems(e: IpcMainEvent, pagination: DownloadItemPagination) {
return await this.videoRepository.findVideos(pagination);
const videos = await this.videoRepository.findVideos(pagination);
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 } = video;
const local = this.storeService.get("local");
const { name, url, headers, type } = video;
const local = this.store.get("local");
// 从配置中添加参数
const deleteSegments = this.storeService.get("deleteSegments");
const deleteSegments = this.store.get("deleteSegments");
const task: Task = {
id: vid,
params: {
url,
type,
local,
name,
headers,
deleteSegments,
},
process: spawnDownload,
};
await this.videoRepository.changeVideoStatus(vid, DownloadStatus.Watting);
this.downloadService.addTask(task);
+47 -49
View File
@@ -6,49 +6,41 @@ import {
MenuItemConstructorOptions,
nativeTheme,
shell,
clipboard,
} from "electron";
import { Favorite } from "entity/Favorite";
import { convertToAudio } from "helper";
import { db, workspace } from "helper/variables";
import { Favorite } from "../entity/Favorite";
import { convertToAudio, db, workspace } from "../helper";
import { inject, injectable } from "inversify";
import { AppStore, EnvPath } from "main";
import { AppStore, EnvPath } from "../main";
import path from "path";
import { handle, getLocalIP } from "../helper";
import {
StoreService,
LoggerService,
type Controller,
FavoriteRepository,
MainWindowService,
VideoRepository,
BrowserWindowService,
PlayerWindowService,
WebviewService,
} from "../interfaces";
import { type Controller } from "../interfaces";
import { TYPES } from "../types";
import fs from "fs-extra";
import MainWindow from "../windows/MainWindow";
import BrowserWindow from "../windows/BrowserWindow";
import ElectronStore from "../vendor/ElectronStore";
import WebviewService from "../services/WebviewService";
import FavoriteRepository from "../repository/FavoriteRepository";
import VideoRepository from "../repository/VideoRepository";
@injectable()
export default class HomeController implements Controller {
private sharedState: Record<string, any> = {};
constructor(
@inject(TYPES.LoggerService)
private readonly logger: LoggerService,
@inject(TYPES.StoreService)
private readonly storeService: StoreService,
@inject(TYPES.ElectronStore)
private readonly store: ElectronStore,
@inject(TYPES.FavoriteRepository)
private readonly favoriteRepository: FavoriteRepository,
@inject(TYPES.MainWindowService)
private readonly mainWindow: MainWindowService,
@inject(TYPES.MainWindow)
private readonly mainWindow: MainWindow,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository,
@inject(TYPES.BrowserWindowService)
private readonly browserWindow: BrowserWindowService,
@inject(TYPES.PlayerWindowService)
private readonly playerWindow: PlayerWindowService,
@inject(TYPES.BrowserWindow)
private readonly browserWindow: BrowserWindow,
@inject(TYPES.WebviewService)
private readonly webviewService: WebviewService
private readonly webviewService: WebviewService,
) {}
@handle("get-env-path")
@@ -58,7 +50,7 @@ export default class HomeController implements Controller {
dbPath: db,
workspace: workspace,
platform: process.platform,
local: this.storeService.get("local"),
local: this.store.get("local"),
};
}
@@ -79,15 +71,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,
});
@@ -123,7 +113,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 "";
@@ -133,12 +123,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
@@ -149,8 +139,12 @@ export default class HomeController implements Controller {
if (key === "theme") {
nativeTheme.themeSource = val;
}
// isMobile
if (key === "isMobile") {
this.webviewService.setUserAgent(val);
}
this.storeService.set(key, val);
this.store.set(key, val);
}
@handle("open-dir")
@@ -166,15 +160,19 @@ 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,
});
};
const item = await this.videoRepository.findVideo(id);
const template: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: "拷贝链接地址",
click: () => {
clipboard.writeText(item.url || "");
},
},
{
label: "选择",
click: () => {
@@ -209,9 +207,9 @@ 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 local = this.store.get("local");
const input = path.join(local, `${video.name}.mp4`);
const output = path.join(local, `${video.name}.mp3`);
const exist = await fs.exists(input);
if (exist) {
@@ -231,13 +229,7 @@ export default class HomeController implements Controller {
// 关闭浏览器窗口
this.browserWindow.hideWindow();
// 修改设置中的属性
this.storeService.set("openInNewWindow", false);
}
@handle("open-player-window")
async openPlayerWindow(event: IpcMainEvent, id: number) {
// 打开播放器窗口
this.playerWindow.openWindow(id);
this.store.set("openInNewWindow", false);
}
@handle("get-local-ip")
@@ -254,4 +246,10 @@ 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 || "";
}
}
@@ -1,16 +1,26 @@
import { IpcMainEvent } from "electron";
import {
IpcMainEvent,
Menu,
MenuItem,
MenuItemConstructorOptions,
} from "electron";
import { inject, injectable } from "inversify";
import { handle } from "../helper/decorator";
import { LoggerService, type Controller, WebviewService } from "../interfaces";
import { handle } from "../helper";
import { type Controller } from "../interfaces";
import { TYPES } from "../types";
import WebviewService from "../services/WebviewService";
import ElectronStore from "../vendor/ElectronStore";
import { SniffingHelper } from "../services/SniffingHelperService";
@injectable()
export default class WebviewController implements Controller {
constructor(
@inject(TYPES.LoggerService)
private readonly logger: LoggerService,
@inject(TYPES.WebviewService)
private readonly webview: WebviewService
private readonly webview: WebviewService,
@inject(TYPES.ElectronStore)
private readonly store: ElectronStore,
@inject(TYPES.SniffingHelper)
private readonly sniffingHelper: SniffingHelper,
) {}
@handle("set-webview-bounds")
@@ -23,6 +33,23 @@ export default class WebviewController implements Controller {
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();
@@ -47,4 +74,15 @@ export default class WebviewController implements Controller {
async webviewGoHome() {
await this.webview.goHome();
}
@handle("webview-change-user-agent")
async webviewChangeUserAgent(e: IpcMainEvent, isMobile: boolean) {
this.webview.setUserAgent(isMobile);
this.store.set("isMobile", isMobile);
}
@handle("plugin-ready")
async pluginReady() {
this.sniffingHelper.pluginReady();
}
}
+64
View File
@@ -0,0 +1,64 @@
import { ipcMain } from "electron";
import { inject, injectable, multiInject } from "inversify";
import { Controller } from "../interfaces";
import { TYPES } from "../types";
import ElectronLogger from "../vendor/ElectronLogger";
import { error, success } from "../helper/utils";
@injectable()
export default class IpcHandlerService {
constructor(
@multiInject(TYPES.Controller)
private readonly controllers: Controller[],
@inject(TYPES.ElectronLogger)
private readonly logger: ElectronLogger,
) {}
private registerIpc(
controller: Controller,
propertyKey: string | symbol,
): void {
const property = controller[propertyKey];
if (typeof property !== "function") return;
const channel: string = Reflect.getMetadata(
"ipc-channel",
controller,
propertyKey,
);
if (!channel) return;
const ipcMethod: "on" | "handle" = Reflect.getMetadata(
"ipc-method",
controller,
propertyKey,
);
if (!ipcMethod) return;
ipcMain[ipcMethod](channel, async (...args: unknown[]) => {
try {
let res = property.call(controller, ...args);
if (res.then) {
res = await res;
}
return success(res);
} catch (e: unknown) {
this.logger.error(`处理 ipc [${channel}] 事件时异常: `, e);
if (e instanceof Error) {
return error(e.message);
} else {
return error(String(e));
}
}
});
}
init(): void {
for (const controller of this.controllers) {
const Class = Object.getPrototypeOf(controller);
Object.getOwnPropertyNames(Class).forEach((propertyKey) =>
this.registerIpc(controller, propertyKey),
);
}
}
}
+30
View File
@@ -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";
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" },
});
});
}
}
+3
View File
@@ -0,0 +1,3 @@
export interface Vendor {
init(): Promise<void>;
}
+43
View File
@@ -0,0 +1,43 @@
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
import isDev from "electron-is-dev";
export default class Window {
window: BrowserWindow | null = null;
options: BrowserWindowConstructorOptions;
url: string;
constructor(options: BrowserWindowConstructorOptions) {
this.options = options;
}
create() {
if (!this.url) {
throw new Error("url is required");
}
const window = new BrowserWindow(this.options);
void window.loadURL(this.url);
window.once("ready-to-show", this.readyToShow);
window.on("close", this.windowClose);
return window;
}
readyToShow = () => {
if (!this.window) return;
this.window.show();
isDev && this.window.webContents.openDevTools();
};
windowClose = () => {
if (!this.window) return;
// 防止 webview 同时被销毁
this.window.setBrowserView(null);
// 销毁窗口
this.window = null;
};
}
+28 -1
View File
@@ -1,4 +1,4 @@
import { DownloadStatus } from "interfaces";
import { DownloadStatus, DownloadType } from "../interfaces";
import {
Column,
CreateDateColumn,
@@ -17,15 +17,36 @@ export class Video {
@Column({
type: "text",
nullable: true,
unique: true,
})
name: string;
@Column({
type: "text",
nullable: false,
default: DownloadType.m3u8,
})
type: DownloadType;
@Column({
type: "text",
nullable: false,
})
url: string;
@Column({
type: "text",
nullable: true,
})
headers: string;
@Column({
type: "boolean",
default: false,
nullable: false,
})
isLive: boolean;
@Column({
type: "text",
nullable: false,
@@ -33,6 +54,12 @@ export class Video {
})
status: DownloadStatus;
@Column({
type: "text",
default: "",
})
log: string;
@CreateDateColumn()
createdDate: Date;
-120
View File
@@ -1,120 +0,0 @@
import { spawn } from "child_process";
import { ffmpegPath, macDownloaderPath, winDownloaderPath } from "./variables";
import iconv from "iconv-lite";
import { event, stripColors } from "./utils";
import { DownloadParams, DownloadProgress } from "interfaces";
export const spawnDownload = (params: DownloadParams): Promise<void> => {
if (process.platform === "win32") {
return winSpawnDownload(params);
} else {
return macSpawnDownload(params);
}
};
const winSpawnDownload = (params: DownloadParams): Promise<void> => {
const { id, abortSignal, url, local, name, deleteSegments } = params;
const progressReg = /Progress:\s(\d+)\/(\d+)\s\(.+?\).+?\((.+?\/s).*?\)/g;
return new Promise((resolve, reject) => {
const spawnParams = [url, "--workDir", local, "--saveName", name];
if (deleteSegments) {
spawnParams.push("--enableDelAfterDone");
}
const downloader = spawn(winDownloaderPath, spawnParams, {
signal: abortSignal.signal,
});
downloader.stdout.on("data", (data) => {
const str = iconv.decode(Buffer.from(data), "gbk");
str.split("\n").forEach((item) => {
if (item.trim() == "") {
return;
}
const result = progressReg.exec(item);
if (!result) {
return;
}
const [, cur, total, speed] = result;
const progress: DownloadProgress = { id, cur, total, speed };
event.emit("download-progress", progress);
});
});
downloader.on("error", (err) => {
reject(err);
});
downloader.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error("未知错误"));
}
});
});
};
const macSpawnDownload = (params: DownloadParams): Promise<void> => {
const { id, abortSignal, url, local, name, deleteSegments } = params;
const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
return new Promise((resolve, reject) => {
const spawnParams = [
url,
"--tmp-dir",
local,
"--save-dir",
local,
"--save-name",
name,
];
if (deleteSegments) {
spawnParams.push("--del-after-done");
}
const downloader = spawn(macDownloaderPath, spawnParams, {
signal: abortSignal.signal,
});
downloader.stdout.on("data", (data) => {
const str = String(Buffer.from(data));
str.split("\n").forEach((item) => {
if (item.trim() == "") {
return;
}
const result = progressReg.exec(stripColors(item));
if (!result) {
return;
}
const [, precentage, speed] = result;
const cur = String(Number(precentage) * 10);
if (cur === "0") {
return;
}
const total = "1000";
const progress: DownloadProgress = { id, cur, total, speed };
event.emit("download-progress", progress);
});
});
downloader.on("error", (err) => {
reject(err);
});
downloader.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error("未知错误"));
}
});
});
};
+1 -1
View File
@@ -3,7 +3,7 @@ import { ffmpegPath } from "./variables";
export const convertToAudio = async (
input: string,
output: string
output: string,
): Promise<void> => {
return new Promise((resolve, reject) => {
const ffmpeg = spawn(ffmpegPath, [
+14 -3
View File
@@ -1,4 +1,6 @@
import os from "os";
import fetch from "node-fetch";
import https from "https";
export function getLocalIP() {
const interfaces = os.networkInterfaces();
@@ -11,7 +13,7 @@ export function getLocalIP() {
// 过滤出 IPv4 地址且非回环地址
const filteredIface = iface.filter(
(details) => details.family === "IPv4" && !details.internal
(details) => details.family === "IPv4" && !details.internal,
);
if (filteredIface.length > 0) {
@@ -23,8 +25,17 @@ export function getLocalIP() {
return localIP;
}
export { sleep } from "./utils";
function fetchWrapper(url: string) {
const options = {
agent: new https.Agent({
rejectUnauthorized: false,
}),
};
return fetch(url, options);
}
export { sleep, formatHeaders } from "./utils";
export * from "./variables";
export { on, handle } from "./decorator";
export { spawnDownload } from "./download";
export { convertToAudio } from "./ffmpeg";
export { fetchWrapper as fetch };
+29 -6
View File
@@ -4,12 +4,35 @@ 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);
if (!headers) return "";
const formatted = Object.entries(headers)
.map(([key, value]) => `${key}:${value}`)
.join("|");
return formatted;
}
export const event = new EventEmitter();
export interface IpcResponse {
code: number;
message: string;
data: Record<string, any> | null;
}
export function success(data: Record<string, any>): IpcResponse {
return {
code: 0,
message: "success",
data,
};
}
export function error(message = "fail"): IpcResponse {
return {
code: -1,
message,
data: null,
};
}
+39 -17
View File
@@ -1,26 +1,48 @@
import { app } from "electron";
import isDev from "electron-is-dev";
import path from "path";
import { resolve } from "path";
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";
if (!isDev) {
global.__bin__ = path.resolve(app.getAppPath(), "../bin");
global.__bin__ = resolve(appPath, "../bin");
}
export const appData = app.getPath("appData");
export const appName = process.env.APP_NAME || "electron-template";
export const workspace = path.resolve(appData, appName);
export function resolveStatic(path: string) {
const relativePath = isDev ? "../.." : "..";
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 || "mediago";
export const workspace = resolve(appData, appName);
export const defaultScheme = "mediago";
export const download = app.getPath("downloads");
export const PERSIST_MEDIAGO = "persist:mediago";
export const PERSIST_WEBVIEW = "persist:webview";
export const db = path.resolve(workspace, "app.db");
export const macDownloaderPath = path.resolve(__bin__, "N_m3u8DL-RE");
export const winDownloaderPath = path.resolve(
__bin__,
"N_m3u8DL-CLI_v3.0.2.exe"
);
export const ffmpegPath =
process.platform === "win32"
? path.resolve(__bin__, "ffmpeg.exe")
: path.resolve(__bin__, "ffmpeg");
export const mobilePath = path.resolve(app.getAppPath(), "../mobile");
export const db = resolve(workspace, "app.db");
// bin path
const downloaderBinName = isWin ? "N_m3u8DL-CLI" : "N_m3u8DL-RE";
export const ffmpegPath = resolveBin("ffmpeg");
export const biliDownloaderBin = resolveBin("BBDown");
export const m3u8DownloaderBin = resolveBin(downloaderBinName);
export const videoServerBin = resolveBin("server");
// plugin path
export const pluginPath = resolveStatic("plugin/index.js");
// user agent
export const pcUA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
export const mobileUA =
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36";
+7
View File
@@ -0,0 +1,7 @@
declare module "*.svg";
declare module "*.png";
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.gif";
declare module "*.bmp";
declare module "*.tiff";
+4 -6
View File
@@ -1,9 +1,9 @@
import "reflect-metadata";
import { app, protocol } from "electron";
import { defaultScheme } from "./helper/variables";
import { type App } from "./interfaces";
import { defaultScheme } from "./helper";
import { container } from "./inversify.config";
import { TYPES } from "./types";
import ElectronApp from "./app";
const gotTheLock = app.requestSingleInstanceLock();
const start = async (): Promise<void> => {
@@ -21,13 +21,11 @@ const start = async (): Promise<void> => {
},
]);
await app.whenReady();
const mediago = container.get<App>(TYPES.App);
const mediago = container.get<ElectronApp>(TYPES.ElectronApp);
mediago.init();
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
// empty
});
};
+17 -114
View File
@@ -1,69 +1,13 @@
import { type BrowserWindow } from "electron";
import { ElectronLog } from "electron-log";
import Store from "electron-store";
import { Favorite } from "entity/Favorite";
import { Video } from "entity/Video";
import EventEmitter from "events";
import { AppStore } from "main";
import { DataSource, EntityManager, UpdateResult, DeleteResult } from "typeorm";
export interface MainWindowService {
show: boolean;
window: BrowserWindow | null;
init: () => void;
}
export interface PlayerWindowService {
openWindow: (id: number) => void;
}
export interface BrowserWindowService {
show: boolean;
window: BrowserWindow | null;
showWindow: () => void;
hideWindow: () => void;
}
export interface App {
init: () => void;
}
export interface IpcHandlerService {
init: () => void;
}
export interface ProtocolService {
create: () => void;
}
export interface UpdateService {
init: () => void;
}
export interface DatabaseService {
manager: EntityManager;
appDataSource: DataSource;
init: () => void;
}
export type Controller = Record<string | symbol, any>;
export interface LoggerService {
logger: ElectronLog;
info: (...args: any[]) => void;
error: (...args: any[]) => void;
warn: (...args: any[]) => void;
debug: (...args: any[]) => void;
}
export interface StoreService extends Store<AppStore> {
init: () => void;
}
export interface DownloadItem {
id?: number;
id: number;
type: DownloadType;
name: string;
url: string;
headers?: string;
status?: DownloadStatus;
isLive?: boolean;
}
export enum DownloadFilter {
@@ -82,46 +26,6 @@ export interface VideoResponse {
list: DownloadItem[];
}
export interface VideoRepository {
addVideo: (video: DownloadItem) => Promise<Video>;
editVideo: (video: DownloadItem) => Promise<Video>;
findVideos: (pagiantion: DownloadItemPagination) => Promise<VideoResponse>;
findVideo: (id: number) => Promise<Video | null>;
changeVideoStatus: (
id: number | number[],
status: DownloadStatus
) => Promise<UpdateResult>;
findWattingAndDownloadingVideos: () => Promise<Video[]>;
deleteDownloadItem: (id: number) => Promise<DeleteResult>;
findAllVideos(): Promise<Video[]>;
}
export interface FavoriteRepository {
findFavorites: () => Promise<Favorite[]>;
addFavorite: (favorite: Favorite) => Promise<Favorite>;
removeFavorite: (id: number) => Promise<void>;
}
export interface WebviewService {
init: () => void;
getBounds: () => Electron.Rectangle;
setAutoResize: (options: Electron.AutoResizeOptions) => void;
setBackgroundColor: (color: string) => void;
setBounds: (bounds: Electron.Rectangle) => void;
loadURL: (url?: string) => Promise<void>;
goBack: () => Promise<boolean>;
reload: () => Promise<void>;
goHome: () => Promise<void>;
hide: () => void;
show: () => void;
setProxy: (useProxy: boolean, proxy: string) => void;
setBlocking: (enableBlocking: boolean) => void;
}
export interface WebService {
init: () => void;
}
export enum DownloadStatus {
Ready = "ready",
Watting = "watting",
@@ -131,33 +35,32 @@ export enum DownloadStatus {
Failed = "failed",
}
export interface DownloadService extends EventEmitter {
addTask: (task: Task) => Promise<void>;
stopTask: (id: number) => Promise<void>;
}
export type Task = {
id: number;
params: Omit<DownloadParams, "id" | "abortSignal">;
process: (params: DownloadParams) => Promise<void>;
params: Omit<DownloadParams, "id" | "abortSignal" | "callback">;
};
export interface DownloadProgress {
id: number;
cur: string;
total: string;
type: string;
percent: string;
speed: string;
isLive: boolean;
}
export enum DownloadType {
m3u8 = "m3u8",
bilibili = "bilibili",
}
export interface DownloadParams {
id: number;
type: DownloadType;
url: string;
local: string;
name: string;
headers?: string;
abortSignal: AbortController;
proxy?: string;
deleteSegments?: boolean;
}
export interface DevToolsService {
init: () => Promise<void>;
callback: (progress: DownloadProgress) => void;
}
+44 -64
View File
@@ -1,82 +1,62 @@
import DownloadController from "controller/DownloadController";
import HomeController from "controller/HomeController";
import WebviewController from "controller/WebviewController";
import DownloadController from "./controller/DownloadController";
import HomeController from "./controller/HomeController";
import WebviewController from "./controller/WebviewController";
import { Container } from "inversify";
import FavoriteRepositoryImpl from "repository/favoriteRepositoryImpl";
import VideoRepositoryImpl from "repository/videoRepositoryImpl";
import BrowserWindowServiceImpl from "services/BrowserWindowServiceImpl";
import DatabaseServiceImpl from "services/DatabaseServiceImpl";
import DevToolsServiceImpl from "services/DevToolsServiceImpl";
import DownloadServiceImpl from "services/DownloadServiceImpl";
import StoreServiceImpl from "services/StoreServiceImpl";
import WebviewServiceImpl from "services/WebviewServiceImpl";
import FavoriteRepository from "./repository/FavoriteRepository";
import VideoRepository from "./repository/VideoRepository";
import WebviewService from "./services/WebviewService";
import ElectronApp from "./app";
import {
App,
StoreService,
Controller,
IpcHandlerService,
LoggerService,
MainWindowService,
ProtocolService,
UpdateService,
VideoRepository,
DatabaseService,
FavoriteRepository,
WebviewService,
BrowserWindowService,
DevToolsService,
WebService,
PlayerWindowService,
} from "./interfaces";
import IpcHandlerServiceImpl from "./services/IpcHandlerServiceImpl";
import LoggerServiceImpl from "./services/LoggerServiceImpl";
import MainWindowServiceImpl from "./services/MainWindowServiceImpl";
import ProtocolServiceImpl from "./services/ProtocolServiceImpl";
import UpdateServiceImpl from "./services/UpdateServiceImpl";
import { Controller } from "./interfaces";
import { TYPES } from "./types";
import WebServiceImpl from "services/WebServiceImpl";
import PlayerWindowServiceImpl from "services/PlayerWindowServiceImpl";
import MainWindow from "./windows/MainWindow";
import BrowserWindow from "./windows/BrowserWindow";
import { SniffingHelper } from "./services/SniffingHelperService";
import DownloadService from "./services/DownloadService";
import ElectronLogger from "./vendor/ElectronLogger";
import ElectronUpdater from "./vendor/ElectronUpdater";
import TypeORM from "./vendor/TypeORM";
import ElectronDevtools from "./vendor/ElectronDevtools";
import ElectronStore from "./vendor/ElectronStore";
import IpcHandler from "./core/ipc";
import ProtocolService from "./core/protocol";
const container = new Container({
skipBaseClassChecks: true,
defaultScope: "Singleton",
autoBindInjectable: true,
});
container
.bind<MainWindowService>(TYPES.MainWindowService)
.to(MainWindowServiceImpl);
container
.bind<BrowserWindowService>(TYPES.BrowserWindowService)
.to(BrowserWindowServiceImpl);
container.bind<App>(TYPES.App).to(ElectronApp);
container
.bind<IpcHandlerService>(TYPES.IpcHandlerService)
.to(IpcHandlerServiceImpl);
container.bind<ProtocolService>(TYPES.ProtocolService).to(ProtocolServiceImpl);
container.bind<UpdateService>(TYPES.UpdateService).to(UpdateServiceImpl);
container.bind<LoggerService>(TYPES.LoggerService).to(LoggerServiceImpl);
container.bind<StoreService>(TYPES.StoreService).to(StoreServiceImpl);
container.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseServiceImpl);
container.bind<WebviewService>(TYPES.WebviewService).to(WebviewServiceImpl);
container
.bind<DownloadServiceImpl>(TYPES.DownloadService)
.to(DownloadServiceImpl);
container.bind<DevToolsService>(TYPES.DevToolsService).to(DevToolsServiceImpl);
container.bind<WebService>(TYPES.WebService).to(WebServiceImpl);
container
.bind<PlayerWindowService>(TYPES.PlayerWindowService)
.to(PlayerWindowServiceImpl);
// === controller
container.bind<ElectronApp>(TYPES.ElectronApp).to(ElectronApp);
// services
container.bind<WebviewService>(TYPES.WebviewService).to(WebviewService);
container.bind<DownloadService>(TYPES.DownloadService).to(DownloadService);
container.bind<SniffingHelper>(TYPES.SniffingHelper).to(SniffingHelper);
// windows
container.bind<MainWindow>(TYPES.MainWindow).to(MainWindow);
container.bind<BrowserWindow>(TYPES.BrowserWindow).to(BrowserWindow);
// controller
container.bind<Controller>(TYPES.Controller).to(HomeController);
container.bind<Controller>(TYPES.Controller).to(WebviewController);
container.bind<Controller>(TYPES.Controller).to(DownloadController);
// === repository
container.bind<VideoRepository>(TYPES.VideoRepository).to(VideoRepositoryImpl);
// repository
container.bind<VideoRepository>(TYPES.VideoRepository).to(VideoRepository);
container
.bind<FavoriteRepository>(TYPES.FavoriteRepository)
.to(FavoriteRepositoryImpl);
.to(FavoriteRepository);
// 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 };
+22 -52
View File
@@ -1,10 +1,6 @@
import { Video, Rectangle } from "electron";
import { type Favorite } from "entity/Favorite";
import {
DownloadItem,
DownloadItemPagination,
VideoResponse,
} from "interfaces";
import { Rectangle } from "electron";
import { type DownloadType } from "interfaces";
import { AppLanguage, AppTheme } from "./types";
declare interface EnvPath {
binPath: string;
@@ -16,52 +12,14 @@ declare interface EnvPath {
declare interface BrowserWindowInitialVal {
url?: string;
sourceList?: LinkMessage[];
sourceList?: WebSource[];
}
declare interface ElectronAPI {
getEnvPath: () => Promise<EnvPath>;
getFavorites: () => Promise<Favorite>;
addFavorite: (favorite: Favorite) => Promise<Favorite>;
removeFavorite: (id: number) => Promise<void>;
setWebviewBounds: (rect: Electron.Rectangle) => Promise<void>;
webviewLoadURL: (url?: string) => Promise<void>;
webviewGoBack: () => Promise<boolean>;
webviewReload: () => Promise<void>;
webwiewGoHome: () => Promise<void>;
getAppStore: () => Promise<AppStore>;
onSelectDownloadDir: () => Promise<string>;
setAppStore: (key: keyof AppStore, val: any) => Promise<void>;
openDir: (dir: string) => Promise<void>;
addDownloadItem: (video: DownloadItem) => Promise<Video>;
getDownloadItems: (
pagiantion: DownloadItemPagination
) => Promise<VideoResponse>;
startDownload: (vid: number) => Promise<void>;
openUrl: (url: string) => Promise<void>;
stopDownload: (id: number) => Promise<void>;
onDownloadListContextMenu: (id: number) => Promise<void>;
onFavoriteItemContextMenu: (id: number) => Promise<void>;
deleteDownloadItem: (id: number) => Promise<void>;
convertToAudio: (id: number) => Promise<void>;
rendererEvent: (channel: string, funcId: string, listener: any) => void;
removeEventListener: (channel: string, funcId: string) => void;
showBrowserWindow: () => Promise<void>;
webviewHide: () => Promise<void>;
webviewShow: () => Promise<void>;
downloadNow: (video: DownloadItem) => Promise<void>;
combineToHomePage: (initialVal: BrowserStore) => Promise<void>;
editDownloadItem: (video: DownloadItem) => Promise<void>;
openPlayerWindow: (videoId: number) => Promise<void>;
getLocalIP: () => Promise<string>;
openBrowser: (url: string) => Promise<void>;
getSharedState: () => Promise<any>;
setSharedState: (state: any) => Promise<void>;
}
declare interface LinkMessage {
declare interface WebSource {
url: string;
title: string;
type: DownloadType;
name: string;
headers?: string;
}
declare interface AppStore {
@@ -81,10 +39,22 @@ 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;
}
declare interface BrowserStore {
url: string;
sourceList: LinkMessage[];
sourceList: WebSource[];
}
+139 -43
View File
@@ -1,58 +1,154 @@
import { contextBridge, ipcRenderer, shell } from "electron";
import { ElectronAPI } from "./main";
import { contextBridge, ipcRenderer } from "electron/renderer";
import { shell } from "electron/common";
import { AppStore, BrowserStore, EnvPath } from "./main";
import { type Favorite } from "./entity/Favorite";
import {
VideoResponse,
type DownloadItem,
type DownloadItemPagination,
} from "./interfaces";
import { Video } from "./entity/Video";
const apiFunctions: Record<string, any> = {};
const apiKey = "electron";
const handleApi: ElectronAPI = {
getEnvPath: () => ipcRenderer.invoke("get-env-path"),
getFavorites: () => ipcRenderer.invoke("get-favorites"),
addFavorite: (favorite) => ipcRenderer.invoke("add-favorite", favorite),
removeFavorite: (id) => ipcRenderer.invoke("remove-favorite", id),
setWebviewBounds: (rect) => ipcRenderer.invoke("set-webview-bounds", rect),
webviewGoBack: () => ipcRenderer.invoke("webview-go-back"),
webviewReload: () => ipcRenderer.invoke("webview-reload"),
webviewLoadURL: (url?: string) => ipcRenderer.invoke("webview-load-url", url),
webwiewGoHome: () => ipcRenderer.invoke("webview-go-home"),
getAppStore: () => ipcRenderer.invoke("get-app-store"),
onSelectDownloadDir: () => ipcRenderer.invoke("select-download-dir"),
setAppStore: (key, val) => ipcRenderer.invoke("set-app-store", key, val),
openDir: (dir) => ipcRenderer.invoke("open-dir", dir),
addDownloadItem: (video) => ipcRenderer.invoke("add-download-item", video),
getDownloadItems: (p) => ipcRenderer.invoke("get-download-items", p),
startDownload: (vid) => ipcRenderer.invoke("start-download", vid),
openUrl: (url: string) => ipcRenderer.invoke("open-url", url),
stopDownload: (id) => ipcRenderer.invoke("stop-download", id),
onDownloadListContextMenu: (id) =>
ipcRenderer.invoke("on-download-list-context-menu", id),
onFavoriteItemContextMenu: (id) =>
ipcRenderer.invoke("on-favorite-item-context-menu", id),
deleteDownloadItem: (id) => ipcRenderer.invoke("delete-download-item", id),
convertToAudio: (id) => ipcRenderer.invoke("convert-to-audio", id),
rendererEvent: (channel, funcId, listener) => {
export type ElectronApi = typeof electronApi;
const electronApi = {
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> {
return ipcRenderer.invoke("set-app-store", key, val);
},
openDir(dir: string): Promise<void> {
return ipcRenderer.invoke("open-dir", dir);
},
addDownloadItem(video: Omit<DownloadItem, "id">): Promise<Video> {
return ipcRenderer.invoke("add-download-item", video);
},
addDownloadItems(videos: DownloadItem[]): 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, funcId) => {
removeEventListener(channel: string, funcId: string): void {
const key = `${channel}-${funcId}`;
const fun = apiFunctions[key];
ipcRenderer.removeListener(channel, fun);
delete apiFunctions[key];
},
showBrowserWindow: () => ipcRenderer.invoke("show-browser-window"),
webviewHide: () => ipcRenderer.invoke("webview-hide"),
webviewShow: () => ipcRenderer.invoke("webview-show"),
downloadNow: (video) => ipcRenderer.invoke("download-now", video),
combineToHomePage: (store) =>
ipcRenderer.invoke("combine-to-home-page", store),
editDownloadItem: (video) => ipcRenderer.invoke("edit-download-item", video),
openPlayerWindow: (videoId) =>
ipcRenderer.invoke("open-player-window", videoId),
getLocalIP: () => ipcRenderer.invoke("get-local-ip"),
openBrowser: (url: string) => shell.openExternal(url),
getSharedState: () => ipcRenderer.invoke("get-shared-state"),
setSharedState: (state) => ipcRenderer.invoke("set-shared-state", state),
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);
},
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);
},
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);
},
downloadItem(data: Omit<DownloadItem, "id">) {
return ipcRenderer.invoke("add-download-item", data);
},
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");
},
};
contextBridge.exposeInMainWorld(apiKey, handleApi);
contextBridge.exposeInMainWorld(apiKey, electronApi);
@@ -0,0 +1,42 @@
import { inject, injectable } from "inversify";
import { TYPES } from "../types";
import { Favorite } from "../entity/Favorite";
import TypeORM from "../vendor/TypeORM";
@injectable()
export default class FavoriteRepository {
constructor(
@inject(TYPES.TypeORM)
private readonly db: TypeORM,
) {}
async findFavorites(): Promise<Favorite[]> {
return await this.db.manager.find(Favorite, {
order: {
createdDate: "desc",
},
});
}
async addFavorite(favorite: Favorite): Promise<Favorite> {
const exist = await this.db.manager.findOne(Favorite, {
where: {
url: favorite.url,
},
});
if (exist) {
throw new Error("网址已经存在");
}
const item = new Favorite();
item.title = favorite.title;
item.url = favorite.url;
item.icon = favorite.icon;
return await this.db.manager.save(item);
}
async removeFavorite(id: number): Promise<void> {
await this.db.manager.getRepository(Favorite).delete(id);
}
}
@@ -0,0 +1,159 @@
import { Video } from "../entity/Video";
import { inject, injectable } from "inversify";
import {
DownloadFilter,
DownloadItem,
DownloadItemPagination,
DownloadStatus,
} from "../interfaces";
import { TYPES } from "../types";
import { In, Not } from "typeorm";
import TypeORM from "../vendor/TypeORM";
@injectable()
export default class VideoRepository {
constructor(
@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.db.manager.save(item);
}
async addVideos(videos: DownloadItem[]) {
const items = videos.map((video) => {
const item = new Video();
item.name = video.name;
item.url = video.url;
item.type = video.type;
video.headers && (item.headers = video.headers);
return item;
});
return await this.db.manager.save(items);
}
// 编辑视频
async editVideo(video: DownloadItem) {
const item = await this.db.appDataSource
.getRepository(Video)
.findOneBy({ id: video.id });
if (!item) {
throw new Error("视频不存在");
}
item.name = video.name;
item.url = video.url;
video.headers && (item.headers = video.headers);
return await this.db.manager.save(item);
}
// 查找所有视频
async findAllVideos() {
return await this.db.appDataSource.getRepository(Video).find({
order: {
createdDate: "DESC",
},
});
}
async findVideos(pagination: DownloadItemPagination) {
const {
current = 0,
pageSize = 50,
filter = DownloadFilter.list,
} = pagination;
const filterCondition =
filter === DownloadFilter.done
? DownloadStatus.Success
: Not(DownloadStatus.Success);
const [items, count] = await this.db.appDataSource
.getRepository(Video)
.findAndCount({
where: {
status: filterCondition,
},
order: {
createdDate: "ASC",
},
skip: (current - 1) * pageSize,
take: pageSize,
});
return {
total: count,
list: items,
};
}
async findVideo(id: number) {
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.db.appDataSource.getRepository(Video).findOneBy({
name,
});
}
async changeVideoStatus(id: number | number[], status: DownloadStatus) {
const ids = !Array.isArray(id) ? [id] : id;
return this.db.appDataSource
.createQueryBuilder()
.update(Video)
.set({ status })
.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.db.appDataSource.getRepository(Video).find({
where: {
status: In([DownloadStatus.Downloading, DownloadStatus.Watting]),
},
});
}
async deleteDownloadItem(id: number) {
return await this.db.appDataSource.getRepository(Video).delete(id);
}
async findVideoByUrl(url: string) {
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;
}
}
@@ -1,48 +0,0 @@
import { inject, injectable } from "inversify";
import {
DatabaseService,
FavoriteRepository,
LoggerService,
} from "../interfaces";
import { TYPES } from "../types";
import { Favorite } from "entity/Favorite";
@injectable()
export default class FavoriteRepositoryImpl implements FavoriteRepository {
constructor(
@inject(TYPES.DatabaseService)
private readonly dataService: DatabaseService,
@inject(TYPES.LoggerService)
private readonly logger: LoggerService
) {}
async findFavorites(): Promise<Favorite[]> {
return await this.dataService.manager.find(Favorite, {
order: {
createdDate: "desc",
},
});
}
async addFavorite(favorite: Favorite): Promise<Favorite> {
const exist = await this.dataService.manager.findOne(Favorite, {
where: {
url: favorite.url,
},
});
if (exist) {
throw new Error("网址已经存在");
}
const item = new Favorite();
item.title = favorite.title;
item.url = favorite.url;
item.icon = favorite.icon;
return await this.dataService.manager.save(item);
}
async removeFavorite(id: number): Promise<void> {
await this.dataService.manager.getRepository(Favorite).delete(id);
}
}
@@ -1,109 +0,0 @@
import { Video } from "../entity/Video";
import { inject, injectable } from "inversify";
import {
DatabaseService,
DownloadFilter,
DownloadItem,
DownloadItemPagination,
DownloadStatus,
LoggerService,
VideoRepository,
} from "../interfaces";
import { TYPES } from "../types";
import { In, Not } from "typeorm";
@injectable()
export default class VideoRepositoryImpl implements VideoRepository {
constructor(
@inject(TYPES.DatabaseService)
private readonly dataService: DatabaseService,
@inject(TYPES.LoggerService)
private readonly logger: LoggerService
) {}
async addVideo(video: DownloadItem) {
const item = new Video();
item.name = video.name;
item.url = video.url;
return await this.dataService.manager.save(item);
}
// 编辑视频
async editVideo(video: DownloadItem) {
const item = await this.dataService.appDataSource
.getRepository(Video)
.findOneBy({ id: video.id });
if (!item) {
throw new Error("视频不存在");
}
item.name = video.name;
item.url = video.url;
return await this.dataService.manager.save(item);
}
// 查找所有视频
async findAllVideos() {
return await this.dataService.appDataSource.getRepository(Video).find({
order: {
createdDate: "DESC",
},
});
}
async findVideos(pagination: DownloadItemPagination) {
const {
current = 0,
pageSize = 50,
filter = DownloadFilter.list,
} = pagination;
const filterCondition =
filter === DownloadFilter.done
? DownloadStatus.Success
: Not(DownloadStatus.Success);
const [items, count] = await this.dataService.appDataSource
.getRepository(Video)
.findAndCount({
where: {
status: filterCondition,
},
order: {
createdDate: "DESC",
},
skip: (current - 1) * pageSize,
take: pageSize,
});
return {
total: count,
list: items,
};
}
async findVideo(id: number) {
return this.dataService.appDataSource.getRepository(Video).findOneBy({
id,
});
}
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();
}
async findWattingAndDownloadingVideos() {
return await this.dataService.appDataSource.getRepository(Video).find({
where: {
status: In([DownloadStatus.Downloading, DownloadStatus.Watting]),
},
});
}
async deleteDownloadItem(id: number) {
return await this.dataService.appDataSource.getRepository(Video).delete(id);
}
}
@@ -1,83 +0,0 @@
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import { resolve } from "path";
import { TYPES } from "../types";
import { BrowserWindowService, StoreService } from "../interfaces";
import _ from "lodash";
@injectable()
export default class BrowserWindowServiceImpl implements BrowserWindowService {
window: BrowserWindow | null = null;
private options: BrowserWindowConstructorOptions;
constructor(
@inject(TYPES.StoreService)
private readonly storeService: StoreService
) {
this.options = {
width: 1100,
minWidth: 1100,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: resolve(__dirname, "./preload.js"),
},
};
}
get show() {
return !!this.window && !this.window.isDestroyed();
}
private create() {
const window = new BrowserWindow(this.options);
const url = isDev
? "http://localhost:8555/browser"
: "mediago://index.html/browser";
void window.loadURL(url);
this.storeService.onDidChange("openInNewWindow", (newValue) => {
// 向所有窗口发送通知
if (newValue === false) {
if (window && !window.isDestroyed()) {
window.close();
}
}
});
window.on("resized", this.handleResize);
return window;
}
handleResize = () => {
if (!this.window) return;
const bounds = this.window.getBounds();
this.storeService.set("browserBounds", _.omit(bounds, ["x", "y"]));
};
showWindow = () => {
if (!this.window || this.window.isDestroyed()) {
this.window = this.create();
}
this.window.show();
isDev && this.window.webContents.openDevTools();
const browserBounds = this.storeService.get("browserBounds");
if (browserBounds) {
this.window.setBounds(browserBounds);
}
};
hideWindow = () => {
if (!this.window) return;
this.window.close();
};
}
@@ -0,0 +1,429 @@
import EventEmitter from "events";
import { inject, injectable } from "inversify";
import {
DownloadParams,
DownloadProgress,
DownloadStatus,
Task,
} from "../interfaces";
import { TYPES } from "../types";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
import VideoRepository from "../repository/VideoRepository";
import { biliDownloaderBin, m3u8DownloaderBin } from "../helper";
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: ["win32"],
bin: m3u8DownloaderBin,
args: {
url: {
argsName: null,
},
localDir: {
argsName: ["--workDir"],
},
name: {
argsName: ["--saveName"],
},
// headers: {
// argsName: ["--headers"],
// },
deleteSegments: {
argsName: ["--enableDelAfterDone"],
},
proxy: {
argsName: ["--proxyAddress"],
},
},
consoleReg: {
percent: "([\\d.]+)%",
speed: "([\\d.]+\\s[GMK]B/s)",
error: "ERROR",
start: "开始下载文件|开始录制",
isLive: "识别为直播流, 开始录制",
},
},
{
type: "m3u8",
platform: ["darwin"],
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"],
},
},
consoleReg: {
percent: "([\\d.]+)%",
speed: "([\\d.]+[GMK]Bps)",
error: "ERROR",
start: "保存文件名:",
isLive: "检测到直播流",
},
},
{
type: "bilibili",
platform: ["win32", "darwin"],
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 {
private queue: Task[] = [];
private active: Task[] = [];
private limit: number;
private signal: Record<number, AbortController> = {};
constructor(
@inject(TYPES.ElectronLogger)
private readonly logger: ElectronLogger,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository,
@inject(TYPES.ElectronStore)
private readonly storeService: ElectronStore,
) {
super();
const maxRunner = this.storeService.get("maxRunner");
this.limit = maxRunner;
this.storeService.onDidChange("maxRunner", (maxRunner) => {
maxRunner && (this.limit = maxRunner);
});
}
async addTask(task: Task) {
this.queue.push(task);
this.runTask();
}
async stopTask(id: number) {
if (this.signal[id]) {
this.logger.info(`taskId: ${id} stop`);
this.signal[id].abort();
}
}
async execute(task: Task) {
try {
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Downloading,
);
this.emit("download-start", task.id);
this.logger.info(`taskId: ${task.id} start`);
const controller = new AbortController();
this.signal[task.id] = controller;
const callback = (progress: DownloadProgress) => {
if (progress.type === "progress") {
this.emit("download-progress", progress);
} else if (progress.type === "ready") {
this.emit("download-ready-start", progress);
if (progress.isLive) {
this.removeTask(progress.id);
}
}
};
const params: DownloadParams = {
...task.params,
id: task.id,
abortSignal: controller,
callback,
};
const { proxy, useProxy } = this.storeService.store;
if (useProxy) {
params.proxy = proxy;
}
await this.process(params);
delete this.signal[task.id];
this.logger.info(`taskId: ${task.id} success`);
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Success,
);
this.emit("download-success", task.id);
} catch (err: any) {
this.logger.info(`taskId: ${task.id} failed`);
if (err.message === "AbortError") {
// 下载暂停
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Stopped,
);
this.emit("download-stop", task.id);
} else {
// 下载失败
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Failed,
);
this.emit("download-failed", task.id, err);
}
} finally {
this.removeTask(task.id);
// 传输完成
if (this.queue.length === 0 && this.active.length === 0) {
// this.emit("download-finish");
}
}
}
removeTask(id: number) {
// 处理当前正在活动的任务
const doneId = this.active.findIndex((i) => i.id === id);
this.active.splice(doneId, 1);
// 处理完成的任务
if (this.active.length < this.limit) {
this.runTask();
}
}
runTask() {
while (this.active.length < this.limit && this.queue.length > 0) {
const task = this.queue.shift();
if (task) {
this.active.push(task);
this.execute(task);
}
}
}
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));
}
}
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);
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,117 +0,0 @@
import EventEmitter from "events";
import { inject, injectable } from "inversify";
import {
DownloadService,
DownloadStatus,
Task,
VideoRepository,
} from "../interfaces";
import { TYPES } from "../types";
import LoggerServiceImpl from "./LoggerServiceImpl";
@injectable()
export default class DownloadServiceImpl
extends EventEmitter
implements DownloadService
{
private queue: Task[] = [];
private active: Task[] = [];
private limit = 2;
private debug = process.env.APP_DOWNLOAD_DEBUG;
private signal: Record<number, AbortController> = {};
constructor(
@inject(TYPES.LoggerService)
private readonly logger: LoggerServiceImpl,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository
) {
super();
}
async addTask(task: Task) {
this.queue.push(task);
this.runTask();
}
async stopTask(id: number) {
if (this.signal[id]) {
this.log(`taskId: ${id} stop`);
this.signal[id].abort();
}
}
async execute(task: Task) {
try {
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Downloading
);
this.emit("download-start", task.id);
this.log(`taskId: ${task.id} start`);
const controller = new AbortController();
this.signal[task.id] = controller;
await task.process({
...task.params,
id: task.id,
abortSignal: controller,
});
delete this.signal[task.id];
this.log(`taskId: ${task.id} success`);
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Success
);
this.emit("download-success", task.id);
} catch (err: any) {
this.log(`taskId: ${task.id} failed`);
if (err.name === "AbortError") {
// 下载暂停
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Stopped
);
this.emit("download-stop", task.id);
} else {
// 下载失败
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Failed
);
this.emit("download-failed", task.id, err);
}
} finally {
// 处理当前正在活动的任务
const doneId = this.active.findIndex((i) => i.id === task.id);
this.active.splice(doneId, 1);
// 处理完成的任务
this.runTask();
// 传输完成
if (this.queue.length === 0 && this.active.length === 0) {
// this.emit("download-finish");
}
}
}
runTask() {
while (this.active.length < this.limit && this.queue.length > 0) {
const task = this.queue.shift();
if (task) {
this.active.push(task);
this.execute(task);
}
}
}
log(...args: any[]) {
if (this.debug) {
this.logger.info(`[DownloadService] `, ...args);
}
}
}
@@ -1,76 +0,0 @@
import { ipcMain } from "electron";
import { inject, injectable, multiInject } from "inversify";
import { Controller, IpcHandlerService, LoggerService } from "../interfaces";
import { TYPES } from "../types";
@injectable()
export default class IpcHandlerServiceImpl implements IpcHandlerService {
constructor(
@multiInject(TYPES.Controller)
private readonly controllers: Controller[],
@inject(TYPES.LoggerService)
private readonly logger: LoggerService
) {}
private registerIpc(controller: Controller, property: string | symbol): void {
const fun = controller[property];
const channel: string = Reflect.getMetadata(
"ipc-channel",
controller,
property
);
const method: "on" | "handle" = Reflect.getMetadata(
"ipc-method",
controller,
property
);
if (typeof fun === "function" && method && channel) {
ipcMain[method](channel, async (...args: any[]) => {
try {
const handler = fun.bind(controller);
let res = handler(...args);
if (res.then) {
res = await res;
}
return this.success(res);
} catch (e: any) {
this.logger.error("处理 ipc 事件时异常: ", e);
if (e instanceof Error) {
return this.error(e.message);
} else if (typeof e === "string") {
return this.error(e);
} else {
return this.error(String(e));
}
}
});
}
}
success(data: any) {
return {
code: 0,
message: "success",
data,
};
}
error(message = "fail") {
return {
code: -1,
message,
data: null,
};
}
private bindIpcMethods(controller: Controller): void {
const Class = Object.getPrototypeOf(controller);
Object.getOwnPropertyNames(Class).forEach((property) =>
this.registerIpc(controller, property as never)
);
}
init(): void {
this.controllers.forEach((controller) => this.bindIpcMethods(controller));
}
}
@@ -1,172 +0,0 @@
import {
BrowserWindow,
BrowserWindowConstructorOptions,
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,
DownloadService,
LoggerService,
MainWindowService,
StoreService,
VideoRepository,
} from "../interfaces";
import { event } from "helper/utils";
import _ from "lodash";
@injectable()
export default class MainWindowServiceImpl implements MainWindowService {
private readonly options: BrowserWindowConstructorOptions;
window: BrowserWindow | null = null;
constructor(
@inject(TYPES.LoggerService)
private readonly logger: LoggerService,
@inject(TYPES.DownloadService)
private readonly downloadService: DownloadService,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository,
@inject(TYPES.StoreService)
private readonly storeService: StoreService
) {
this.options = {
width: 1100,
minWidth: 1100,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: resolve(__dirname, "./preload.js"),
},
};
}
create(): BrowserWindow {
if (this.window && !this.window.isDestroyed()) {
return this.window;
}
const window = new BrowserWindow(this.options);
const url = isDev ? "http://localhost:8555/" : "mediago://index.html/";
void window.loadURL(url);
window.once("ready-to-show", this.readyToShow);
event.on("download-progress", this.onDownloadProgress);
this.downloadService.on("download-success", this.onDownloadSuccess);
this.downloadService.on("download-failed", this.onDownloadFailed);
this.downloadService.on("download-start", this.onDownloadStart);
this.downloadService.on("download-stop", this.onDownloadStart);
this.storeService.onDidAnyChange((store) => {
// 向所有窗口发送通知
window.webContents.send("store-change", store);
});
// 处理当前窗口改变大小
window.on("resized", this.handleResize);
app.on("second-instance", () => {
if (process.platform === "win32") {
if (window) {
if (window.isMinimized()) {
window.restore();
}
if (window.isVisible()) {
window.focus();
} else {
window.show();
}
}
}
});
return window;
}
get show() {
return !!this.window && !this.window.isDestroyed();
}
init(): void {
Menu.setApplicationMenu(null);
this.window = this.create();
}
handleResize = () => {
if (!this.window) return;
const bounds = this.window.getBounds();
this.storeService.set("mainBounds", _.omit(bounds, ["x", "y"]));
};
readyToShow = () => {
if (!this.window) return;
this.window.show();
isDev && this.window.webContents.openDevTools();
const mainBounds = this.storeService.get("mainBounds");
if (mainBounds) {
this.window.setBounds(mainBounds);
}
};
onDownloadProgress = (progress: DownloadProgress) => {
if (!this.window) return;
this.window.webContents.send("download-progress", progress);
};
onDownloadSuccess = async (id: number) => {
if (!this.window) return;
const promptTone = this.storeService.get("promptTone");
if (promptTone) {
const video = await this.videoRepository.findVideo(id);
new Notification({
title: "下载成功",
body: `${video?.name} 下载成功`,
}).show();
}
this.window.webContents.send("download-success", id);
};
onDownloadFailed = async (id: number, err: any) => {
if (!this.window) return;
const promptTone = this.storeService.get("promptTone");
if (promptTone) {
const video = await this.videoRepository.findVideo(id);
new Notification({
title: "下载失败",
body: `${video?.name} 下载失败`,
}).show();
}
this.logger.error("下载失败:", err);
this.window.webContents.send("download-failed", id);
};
onDownloadStart = async (id: number) => {
if (!this.window) return;
this.window.webContents.send("download-start", id);
};
onDownloadStop = async (id: number) => {
if (!this.window) return;
this.window.webContents.send("download-stop", id);
};
}
@@ -1,54 +0,0 @@
import { BrowserWindow, BrowserWindowConstructorOptions, Menu } from "electron";
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import { resolve } from "path";
import { PlayerWindowService, VideoRepository } from "../interfaces";
import { TYPES } from "types";
@injectable()
export default class PlayerWindowServiceImpl implements PlayerWindowService {
private options: BrowserWindowConstructorOptions;
private window: BrowserWindow | null = null;
constructor(
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository
) {
this.options = {
width: 1100,
minWidth: 1100,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: resolve(__dirname, "./preload.js"),
},
};
}
create(): BrowserWindow {
const window = new BrowserWindow(this.options);
Menu.setApplicationMenu(null);
const url = isDev
? "http://localhost:8555/player"
: "mediago://index.html/player";
void window.loadURL(url);
return window;
}
openWindow = async (id: number) => {
if (!this.window || this.window.isDestroyed()) {
this.window = this.create();
}
const video = await this.videoRepository.findVideo(id);
if (!video) throw new Error("video not found");
isDev && this.window.webContents.openDevTools();
this.window.webContents.send("open-player-window", video.id);
this.window.show();
};
}
@@ -1,51 +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/variables";
import { ProtocolService } from "../interfaces";
@injectable()
export default class ProtocolServiceImpl implements 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,134 @@
import { inject, injectable } from "inversify";
import { DownloadType } from "../interfaces";
import { TYPES } from "../types";
import ElectronLogger from "../vendor/ElectronLogger";
import EventEmitter from "events";
import { session } from "electron";
import { PERSIST_WEBVIEW } from "../helper";
import {
CallbackResponse,
OnBeforeRequestListenerDetails,
} from "electron/main";
export interface SourceParams {
url: string;
documentURL: string;
name: string;
type: DownloadType;
}
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);
});
}
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() {
const viewSession = session.fromPartition(PERSIST_WEBVIEW);
viewSession.webRequest.onBeforeRequest(this.requestWillBeSent.bind(this));
}
send = (item: SourceParams) => {
this.logger.info(`在窗口中捕获视频链接: ${item.url}`);
// 等待 DOM 中浮窗加载完成
if (this.ready) {
this.emit("source", item);
} else {
this.queue.push(item);
}
};
private requestWillBeSent(
details: OnBeforeRequestListenerDetails,
callback: (response: CallbackResponse) => void,
): void {
const { url } = 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,
});
break listLoop;
}
}
}
callback({});
}
}
@@ -1,23 +0,0 @@
import { autoUpdater } from "electron-updater";
import { inject, injectable } from "inversify";
import { LoggerService, type UpdateService } from "../interfaces";
import { TYPES } from "../types";
import isDev from "electron-is-dev";
@injectable()
export default class UpdateServiceImpl implements UpdateService {
constructor(
@inject(TYPES.LoggerService) private readonly logger: LoggerService
) {}
init(): void {
if (isDev) return;
autoUpdater.disableWebInstaller = true;
autoUpdater.logger = this.logger.logger;
autoUpdater.checkForUpdatesAndNotify({
title: "自动更新完成",
body: "下次重启时将会自动安装",
});
}
}
@@ -1,84 +0,0 @@
import { StoreService, VideoRepository, WebService } from "../interfaces";
import { inject, injectable } from "inversify";
import Koa, { Context } from "koa";
import Router from "@koa/router";
import serve from "koa-static";
import range from "koa-range";
import { TYPES } from "types";
import { glob } from "glob";
import send from "koa-send";
import cors from "@koa/cors";
import { getLocalIP, mobilePath } from "helper";
@injectable()
export default class WebServiceImpl implements WebService {
private readonly app: Koa;
private readonly router: Router;
private readonly baseUrl: string;
constructor(
@inject(TYPES.StoreService)
private readonly storeService: StoreService,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository
) {
this.app = new Koa();
this.router = new Router();
this.baseUrl = `http://${getLocalIP()}:${
process.env.APP_SERVER_PORT
}/video/`;
}
async init(): Promise<void> {
this.router.get("/api/video-list", this.videoList);
this.router.get("/video/:filename", this.serveVideo);
const local = this.storeService.get("local");
this.app.use(cors());
this.app.use(range);
this.app.use(serve(local));
if (process.env.NODE_ENV === "production") {
this.app.use(
serve(mobilePath, {
extensions: ["html", "js", "css"],
index: "index.html",
})
);
}
this.app.use(this.router.routes());
this.app.use(this.router.allowedMethods());
this.app.listen(process.env.APP_SERVER_PORT);
}
private videoList = async (ctx: Context) => {
const local = this.storeService.get("local");
const mp4Files = await glob("*.mp4", {
cwd: local,
});
const videoList = await this.videoRepository.findAllVideos();
const res = mp4Files
.map((file) => {
const name = file.split(".").shift();
const video = videoList.find((video) => video.name === name);
if (!video) return null;
return {
id: video.id,
name: video.name,
url: `${this.baseUrl}${file}`,
};
})
.filter(Boolean);
ctx.body = res;
};
private serveVideo = async (ctx: Context) => {
const { filename } = ctx.params;
const local = this.storeService.get("local");
const file = filename;
await send(ctx, file, { root: local, index: false });
};
}
@@ -0,0 +1,256 @@
import { WebContentsView, session } from "electron";
import { inject, injectable } from "inversify";
import { TYPES } from "../types";
import isDev from "electron-is-dev";
import { PERSIST_WEBVIEW, fetch, mobileUA, pcUA, pluginPath } from "../helper";
import { ElectronBlocker } from "@cliqz/adblocker-electron";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
import MainWindow from "../windows/MainWindow";
import BrowserWindow from "../windows/BrowserWindow";
import VideoRepository from "../repository/VideoRepository";
import { SniffingHelper } from "./SniffingHelperService";
import { resolve } from "path";
import { readFileSync } from "fs-extra";
@injectable()
export default class WebviewService {
public view: WebContentsView;
private blocker?: ElectronBlocker;
constructor(
@inject(TYPES.MainWindow)
private readonly mainWindow: MainWindow,
@inject(TYPES.ElectronLogger)
private readonly logger: ElectronLogger,
@inject(TYPES.BrowserWindow)
private readonly browserWindow: BrowserWindow,
@inject(TYPES.ElectronStore)
private readonly store: ElectronStore,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository,
@inject(TYPES.SniffingHelper)
private readonly sniffingHelper: SniffingHelper,
) {
// 初始化 blocker
this.initBlocker();
}
async init(): Promise<void> {
this.view = new WebContentsView({
webPreferences: {
partition: PERSIST_WEBVIEW,
preload: resolve(__dirname, "./preload.js"),
},
});
this.view.setBackgroundColor("#fff");
this.webContents.setAudioMuted(true);
const { useProxy, proxy, isMobile } = this.store.store;
this.setProxy(useProxy, proxy);
this.setUserAgent(isMobile);
this.webContents.on("dom-ready", async () => {
const baseInfo = {
title: this.webContents.getTitle(),
url: this.webContents.getURL(),
};
this.sniffingHelper.reset(baseInfo);
this.window.webContents.send("webview-dom-ready", baseInfo);
try {
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.webContents.executeJavaScript(content);
} else {
const content = readFileSync(pluginPath, "utf-8");
await this.webContents.executeJavaScript(content);
}
} catch (err) {
// empty
}
});
// 兼容网站在当前页面中打开
this.webContents.setWindowOpenHandler(({ url }) => {
this.loadURL(url, true);
return { action: "deny" };
});
this.sniffingHelper.start();
this.sniffingHelper.on("source", async (item) => {
// 这里需要判断是否使用浏览器插件
const useExtension = this.store.get("useExtension");
if (useExtension) {
this.webContents.send("webview-link-message", item);
} else {
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);
}
});
}
getBounds(): Electron.Rectangle {
return this.view.getBounds();
}
setBackgroundColor(color: string): void {
this.view.setBackgroundColor(color);
}
show() {
this.window.contentView.addChildView(this.view);
isDev && this.webContents.openDevTools();
}
hide() {
this.window.contentView.removeChildView(this.view);
isDev && this.webContents.closeDevTools();
}
setBounds(bounds: Electron.Rectangle): void {
this.view.setBounds(bounds);
}
async loadURL(url?: string, isNewWindow?: boolean) {
const canGoBack = this.webContents.canGoBack();
try {
this.webContents.stop();
await this.webContents.loadURL(url || "");
} catch (err: unknown) {
this.logger.error("加载 url 时出现错误: ", err);
throw err;
} finally {
if (!canGoBack && !isNewWindow) {
this.webContents.clearHistory();
}
}
}
async goBack() {
if (this.webContents.canGoBack()) {
this.webContents.goBack();
return true;
} else {
return false;
}
}
async reload() {
this.webContents.reload();
}
async goHome() {
this.webContents.stop();
this.webContents.clearHistory();
}
get window() {
if (this.browserWindow.window) return this.browserWindow.window;
if (this.mainWindow.window) return this.mainWindow.window;
throw new Error("未找到当前窗口");
}
private get session() {
return session.fromPartition(PERSIST_WEBVIEW);
}
private enableProxy(proxy: string) {
if (!proxy) {
this.logger.error("[proxy] 代理地址不能为空");
return;
}
// 处理 proxy 地址的合法性
if (!/https?:\/\//.test(proxy)) {
proxy = `http://${proxy}`;
}
this.session.setProxy({ proxyRules: proxy });
this.logger.info(`[proxy] 代理开启成功,代理地址为${proxy}`);
}
private disableProxy() {
this.session.setProxy({ proxyRules: "" });
this.logger.info("[proxy] 代理关闭成功");
}
setProxy(useProxy: boolean, proxy: string): void {
if (useProxy) {
this.enableProxy(proxy);
} else {
this.disableProxy();
}
}
setBlocking(enableBlocking: boolean): void {
if (enableBlocking) {
this.enableBlocking();
} else {
this.disableBlocking();
}
}
async initBlocker() {
this.blocker = await ElectronBlocker.fromPrebuiltAdsAndTracking(fetch);
const enableBlocking = this.store.get("blockAds");
this.setBlocking(enableBlocking);
}
private enableBlocking() {
if (!this.blocker) {
this.logger.error("开启 blocker 失败,未初始化");
return;
}
this.blocker.enableBlockingInSession(this.session);
this.logger.info("开启 blocker 成功");
}
private disableBlocking() {
if (!this.blocker) {
this.logger.error("关闭 blocker 失败,未初始化");
return;
}
if (!this.blocker.isBlockingEnabled(this.session)) {
return;
}
this.blocker.disableBlockingInSession(this.session);
this.logger.info("关闭 blocker 成功");
}
setUserAgent(isMobile?: boolean) {
if (isMobile) {
this.webContents.setUserAgent(mobileUA);
} else {
this.webContents.setUserAgent(pcUA);
}
this.logger.info("设置 user-agent 成功", isMobile);
}
get debugger() {
return this.webContents.debugger;
}
get webContents() {
return this.view.webContents;
}
captureView(): Promise<Electron.NativeImage> {
return this.view.webContents.capturePage();
}
sendToWindow(channel: string, ...args: unknown[]) {
this.window.webContents.send(channel, ...args);
}
}

Some files were not shown because too many files have changed in this diff Show More