Compare commits

...

210 Commits

Author SHA1 Message Date
士子☀️ 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
士子☀️ b3b58680e0 release: v2.0.0 2023-06-24 15:29:27 +08:00
士子☀️ d1b75df87d Merge pull request #22 from caorushizi/development
fix: mac active.
2023-06-23 22:10:35 +08:00
caorushizi 4b4e1b9f54 fix: mac active. 2023-06-23 22:05:47 +08:00
士子☀️ 8ebb550270 Merge pull request #21 from caorushizi/development
release: v2.0.0-beta.7
2023-06-23 21:52:50 +08:00
士子☀️ 6b93954246 release: v2.0.0-beta.7 2023-06-23 21:48:46 +08:00
士子☀️ 550d7ebd4b release: v2.0.0-beta.7 2023-06-23 21:18:29 +08:00
caorushizi 3f331e5d3d feat: package.json 2023-06-23 21:12:54 +08:00
士子☀️ 020f921af8 release v2.0.0-beta.6 2023-06-23 10:44:58 +08:00
士子☀️ efa72ce12b release: v2.0.0-beta.6 2023-06-23 09:39:41 +08:00
士子☀️ 38ead633a5 release: v2.0.0-beta.6 2023-06-23 09:29:50 +08:00
士子☀️ 3eb1d0ae99 fix: get video source & dark mode 2023-06-22 21:39:43 +08:00
士子☀️ 7bed4ed335 update dependencies version. 2023-06-22 16:23:48 +08:00
士子☀️ b357d41f23 add: dark mode. 2023-06-22 15:58:48 +08:00
士子☀️ 3834603b0a update dependencies version. 2023-06-22 12:40:33 +08:00
士子☀️ f38ba108a3 add adblocker 2023-06-22 11:33:37 +08:00
士子☀️ 7ceda3d73f readme 2023-06-21 23:53:03 +08:00
士子☀️ aee57b6f98 feat: set dev tool load. 2023-06-20 23:28:47 +08:00
士子☀️ f9d49bd55f feat: restore browser state. 2023-06-20 22:49:07 +08:00
士子☀️ 3bb082473a Merge branch 'master' of https://github.com/caorushizi/m3u8-downloader 2023-06-20 22:01:23 +08:00
士子☀️ 31b6726fe5 fix: direct download. 2023-06-20 22:01:05 +08:00
士子☀️ 348b41b0dd Update README.md 2023-06-19 23:11:36 +08:00
士子☀️ d4bbfa0516 release: beta5 2023-06-18 18:38:38 +08:00
士子☀️ b2af9b3d25 release: 2.0.0-beta.5 2023-06-18 18:25:26 +08:00
caorushizi ded93a4f0b test 2023-06-18 18:04:59 +08:00
caorushizi d43f57565b feat: add video player. 2023-06-18 17:59:39 +08:00
caorushizi 34cf661fb2 feat: add video player 2023-06-18 17:05:05 +08:00
士子☀️ 91cfb883b6 feat: add video player. 2023-06-18 15:06:15 +08:00
士子☀️ 1cb7deb376 player 2023-06-17 21:24:54 +08:00
caorushizi 70933c63bb pack 2023-06-17 19:29:25 +08:00
caorushizi 5723b9edd0 player 2023-06-17 14:15:50 +08:00
士子☀️ 65f37a9c66 main 2023-06-17 13:08:45 +08:00
士子☀️ 57516793ab main 2023-06-17 02:42:41 +08:00
士子☀️ afecc5ac12 rename url 2023-06-17 01:16:53 +08:00
士子☀️ 3331564155 Merge pull request #20 from caorushizi/dev-server
feat: add video server
2023-06-17 00:13:54 +08:00
士子☀️ 19ce7b9906 add 2023-06-17 00:12:54 +08:00
士子☀️ 40f39340f0 add mobile. 2023-06-16 23:20:20 +08:00
士子☀️ f085bcacf7 add mobile 2023-06-11 02:29:53 +08:00
士子☀️ dbd4e73b6e feat: add mobile player. 2023-06-10 17:23:31 +08:00
士子☀️ b35e6b3315 feat: add video player. 2023-06-10 16:56:02 +08:00
士子☀️ 3c603a67dd feat: add video player 2023-06-10 11:33:35 +08:00
士子☀️ 8ff5abf5d8 feat: add video server 2023-06-08 01:22:59 +08:00
士子☀️ 0fdbd39393 fix: windows address url. 2023-06-07 01:02:22 +08:00
caorushizi df93770128 test 2023-06-04 00:40:39 +08:00
caorushizi b61a935068 test 2023-06-03 23:07:42 +08:00
士子☀️ 95e96b1057 优化素材提取页交互,优化打点逻辑,优化页面切换时的问题 2023-05-28 19:18:36 +08:00
士子☀️ 3dd325a280 Update README.md 2023-05-06 09:54:08 +08:00
士子☀️ bbe922df74 Update README.md 2023-05-06 09:53:03 +08:00
caorushizi e31eddfd2e 2.0.0-beta.4 2023-05-05 11:25:10 +08:00
caorushizi bbb9f5798b fix: peocess.env 2023-05-05 10:20:10 +08:00
caorushizi b634cf056d fix: env file. 2023-05-05 09:45:38 +08:00
士子☀️ 2ede22b50a github action pnpm version 2023-05-05 01:15:29 +08:00
士子☀️ a4efd83f7a build and release 2023-05-05 01:10:57 +08:00
caorushizi c3ebced763 macos pack 2023-05-04 23:32:03 +08:00
caorushizi 6cb6621df1 platform: add macos 2023-05-04 23:16:13 +08:00
士子☀️ 4e5febba8b something 2023-04-22 19:47:10 +08:00
士子☀️ 1fb787d4ea 手动添加链接;某些视频网站跳转 about:blank ;修改下载名称 2023-04-22 17:29:26 +08:00
士子☀️ f24ddaa55e add favorite 2023-04-22 14:33:57 +08:00
士子☀️ 66542a0003 talking data version 2023-04-17 23:04:17 +08:00
士子☀️ f8a8e1d3e6 不展示menu 2023-04-15 22:22:34 +08:00
士子☀️ c1873b8c6d 在新页面中打开浏览器 2023-04-15 21:30:15 +08:00
士子☀️ 07dceb2704 在新页面中打开浏览器 2023-04-15 21:05:07 +08:00
士子☀️ ceec042ffd 在新页面中打开浏览器 2023-04-15 16:45:39 +08:00
士子☀️ 2a47f255ee 升级antd,下载完成后删除原始文件 2023-04-14 00:04:44 +08:00
士子☀️ 1f899e6c6d env 2023-04-02 15:07:56 +08:00
士子☀️ f0686bb18c release 2023-04-01 20:44:51 +08:00
士子☀️ b321dc06e9 app version 2023-04-01 19:11:55 +08:00
士子☀️ 5f7c6e846b updater 2023-04-01 11:03:19 +08:00
士子☀️ 2d71475a48 Merge branch 'master' of https://github.com/caorushizi/m3u8-downloader 2023-04-01 10:55:55 +08:00
士子☀️ cfdeb4a1a8 updater 2023-04-01 10:55:36 +08:00
士子☀️ 5d3abdefdc Create .github/dependabot.yml 2023-04-01 10:16:21 +08:00
士子☀️ abcff22f3e 转换为音频 2023-03-31 23:27:45 +08:00
士子☀️ 7d4ca4d758 ipcrenderer 事件监听 2023-03-31 21:01:07 +08:00
caorushizi 9b44b2f31f 2.0 优化功能 2023-03-31 20:27:28 +08:00
士子☀️ 55bd8da894 feature: 完善首页、视频获取逻辑 2023-03-31 00:37:18 +08:00
士子☀️ e8c7abaa2f 修复事件监听&完善下载页面交互 2023-03-30 01:05:02 +08:00
士子☀️ 1702b9beac update 2023-03-28 23:49:41 +08:00
士子☀️ de25052980 暂停下载逻辑 2023-03-27 23:47:10 +08:00
士子☀️ 03700ed4bb readme 2023-03-26 20:48:18 +08:00
士子☀️ e0147a4a6e readme 2023-03-26 20:41:55 +08:00
士子☀️ 281fd07e23 Merge pull request #15 from caorushizi/dev
Dev
2023-03-26 20:34:14 +08:00
士子☀️ 1d7a0e62e5 download. 2023-03-26 20:29:20 +08:00
士子☀️ e4dcdb9422 download. 2023-03-26 19:22:29 +08:00
士子☀️ f035bb09b4 download. 2023-03-26 17:59:54 +08:00
士子☀️ 869c6497e9 download. 2023-03-26 15:54:43 +08:00
士子☀️ 151ed1ce0d download list progress. 2023-03-26 01:12:49 +08:00
士子☀️ bab349da9b download list. 2023-03-25 20:17:02 +08:00
士子☀️ ac8f9d2955 home page. 2023-03-25 16:00:06 +08:00
士子☀️ 0f2670069f readme. 2023-03-25 12:15:38 +08:00
士子☀️ 0af3d32337 download list. 2023-03-25 12:13:30 +08:00
士子☀️ f5a22f8393 config page. 2023-03-25 11:39:40 +08:00
士子☀️ 769a82e7ed setting form. 2023-03-25 00:08:23 +08:00
caorushizi 3985af0042 something 2023-03-24 20:09:18 +08:00
士子☀️ dca0e0a84b add electron extensions. 2023-03-24 00:29:27 +08:00
士子☀️ 233296ba3e receive 2023-03-23 00:22:37 +08:00
士子☀️ 2bc4fe0592 readme 2023-03-22 00:13:24 +08:00
士子☀️ c22245e296 source extract. 2023-03-22 00:11:15 +08:00
士子☀️ 29de2fc81c source extract. 2023-03-20 01:09:08 +08:00
士子☀️ bd27aefe8a source extract. 2023-03-20 00:50:08 +08:00
士子☀️ 722cabba57 source extract. 2023-03-19 19:23:35 +08:00
士子☀️ 3468c8d5e6 new branch. 2023-03-19 12:16:37 +08:00
caorushizi b3cc83f8cd new ui 2023-01-14 12:34:26 +08:00
caorushizi f6a1ca03ac new ui 2023-01-13 11:48:44 +08:00
士子☀️ 516ac72c3e feature: 修改页面提示逻辑 2022-12-28 00:07:53 +08:00
士子☀️ c068bde0a8 feature: 新增插件 2022-12-26 23:38:04 +08:00
士子☀️ ca3ed53a59 feature: 样式交互优化 2022-12-25 23:59:01 +08:00
士子☀️ 04c05ee15e feture: 优化样式交互 2022-12-25 20:48:31 +08:00
士子☀️ 60d27be739 style: eslint 修正 2022-12-25 15:11:06 +08:00
士子☀️ 259fe5f447 style: eslint 修正 2022-12-25 14:30:24 +08:00
士子☀️ 8d9b7fc9dc style: eslint 修正 2022-12-25 13:22:18 +08:00
士子☀️ d5d282147d style: eslint 修正 2022-12-24 17:51:28 +08:00
士子☀️ 08f7f66154 revert: 删除mediago-node 2022-12-23 22:15:26 +08:00
士子☀️ f702f847e6 style: 修正代码提交规范 2022-12-23 22:11:12 +08:00
士子☀️ b9b87cf354 master 2022-12-22 23:14:59 +08:00
士子☀️ a81b9a37ab master 2022-12-22 16:09:58 +08:00
士子☀️ 64174ef374 master 2022-12-18 23:20:34 +08:00
士子☀️ a8b3a9ee1a master 2022-12-17 00:30:53 +08:00
diaozhenyuan 7be1a7907d master 2022-12-16 21:14:14 +08:00
diaozhenyuan 05d207f34d master 2022-12-16 18:49:58 +08:00
diaozhenyuan 26be46b038 master 2022-12-16 18:02:24 +08:00
士子☀️ 47826f1f72 master 2022-12-15 23:27:59 +08:00
士子☀️ f7428f811a master 2022-12-12 22:55:24 +08:00
士子☀️ 3aa8577a38 master 2022-12-11 19:10:44 +08:00
士子☀️ 8b7965dbcb master 2022-12-11 02:54:46 +08:00
士子☀️ 006cb4b13c master 2022-12-10 19:14:22 +08:00
士子☀️ ecb8d6b387 master 2022-12-10 18:52:10 +08:00
士子☀️ b57b05fe1d master 2022-12-10 18:23:47 +08:00
士子☀️ 1e9969857d Merge pull request #13 from caorushizi/dev
Dev
2022-12-10 18:01:03 +08:00
士子☀️ af89dd2694 dev 2022-12-06 23:04:38 +08:00
士子☀️ e418e05ab1 dev 2022-11-29 23:34:31 +08:00
士子☀️ a8d8a7aca7 dev 2022-11-29 23:31:30 +08:00
士子☀️ 4c7226cda1 dev 2022-11-26 00:15:27 +08:00
士子☀️ 91b5ee4423 dev 2022-11-26 00:15:05 +08:00
士子☀️ f8d7148b9e Dev 2022-11-08 00:07:41 +08:00
士子☀️ 0f0687b5c9 dev 2022-11-06 23:36:28 +08:00
士子☀️ 1384cd4c6e dev 2022-11-06 12:41:35 +08:00
士子☀️ 4c5c78d0e4 dev 2022-11-06 12:15:07 +08:00
士子☀️ 551abb4496 dev 2022-10-30 21:49:32 +08:00
士子☀️ 8af8824c3e dev 2022-10-30 20:20:32 +08:00
士子☀️ c01c085ac1 dev 2022-10-30 01:46:39 +08:00
士子☀️ 8668acdd6d dev 2022-10-27 00:13:10 +08:00
士子☀️ 03567ed36d 重构 2022-10-24 00:10:10 +08:00
caorushizi dcfea2cfb3 整理项目文件 2022-05-01 01:01:05 +08:00
caorushizi 3c4e6c8240 整理项目文件 2022-04-30 23:23:38 +08:00
caorushizi 9e93426c38 2 2022-04-20 23:53:45 +08:00
caorushizi 5c698e2e9b 1 2022-04-19 23:34:58 +08:00
caorushizi b174e14fbd process 2022-04-19 23:27:09 +08:00
caorushizi 19e84cd415 downloader 2022-04-19 01:13:12 +08:00
caorushizi 73a9b3fd72 downloader 2022-04-19 01:12:33 +08:00
caorushizi 64a938879d downloader 2022-04-17 23:15:38 +08:00
caorushizi 66b9a9677b 重构文件目录 2022-04-17 01:55:37 +08:00
士子☀️ 39616244d9 mono repo. 2022-04-11 23:14:48 +08:00
士子☀️ 990846515f Update README.md 2022-02-06 21:32:25 +08:00
caorushizi 94777aead3 update readme.md 2022-02-05 16:59:13 +08:00
caorushizi 5275d3bfd3 删除视频数据打点 2022-02-05 16:43:06 +08:00
士子☀️ 73ae0ad083 Update README.md 2022-01-27 11:38:36 +08:00
caorushizi 5ed6e84a14 Merge branch 'master' of https://github.com/caorushizi/m3u8-downloader 2022-01-27 00:22:22 +08:00
caorushizi ed45cd8d73 README.md 2022-01-27 00:21:36 +08:00
士子☀️ f393b99cb5 Update README.md 2022-01-27 00:07:56 +08:00
caorushizi 6638600ffa 修复开发环境中没有注入环境变量导致无法运行 2022-01-26 23:53:54 +08:00
士子☀️ 4d5cae3b0c Update build.js 2022-01-26 13:38:31 +08:00
caorushizi e846390d04 添加数据统计许可 2022-01-25 23:52:07 +08:00
236 changed files with 16969 additions and 4429 deletions
+15
View File
@@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
+6
View File
@@ -0,0 +1,6 @@
APP_NAME=media-downloader
APP_ID=mediago.ziying.site
APP_COPYRIGHT=caorushizi
APP_VERSION=2.0.2
APP_SERVER_PORT=8433
+5 -1
View File
@@ -1 +1,5 @@
NODE_ENV=development
APP_TD_APPID=
APP_DOWNLOAD_DEBUG=true
GH_TOKEN=
LOAD_DEVTOOLS=
+4 -1
View File
@@ -1 +1,4 @@
NODE_ENV=production
APP_TD_APPID=
APP_DOWNLOAD_DEBUG=
GH_TOKEN=
-1
View File
@@ -1 +0,0 @@
NODE_ENV=staging
-6
View File
@@ -1,6 +0,0 @@
.eslintrc.js
esbuild.config.js
babel.config.js
webpack.config.js
script/
vite.config.ts
-19
View File
@@ -1,19 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es6: true,
node: true,
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "react"],
extends: [
"plugin:@typescript-eslint/recommended",
"react-app",
"plugin:prettier/recommended",
],
rules: {
"@typescript-eslint/ban-ts-comment": "warn",
"prettier/prettier": "error",
},
};
+11
View File
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
+54
View File
@@ -0,0 +1,54 @@
# build.yml
# Workflow's name
name: Build Downloader App.
# Workflow's trigger
on:
push:
branches:
- master
# Workflow's jobs
jobs:
# job's id
release:
# job's name
name: build and release electron app
# the type of machine to run the job on
runs-on: ${{ matrix.os }}
# create a build matrix for jobs
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
# create steps
steps:
# step1: check out repository
- name: Check out git repository
uses: actions/checkout@v3
# step2: install node env
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: "16.x"
- uses: pnpm/action-setup@v2
with:
version: latest
run_install: true
- name: Build & release app
run: pnpm run release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
- uses: actions/upload-artifact@v3
with:
name: artifact
path: packages/main/release/media-downloader-setup-*
+49
View File
@@ -0,0 +1,49 @@
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:
BUILD_PATH: "."
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@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "16"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
working-directory: ${{ env.BUILD_PATH }}
- name: Build with Vitepress
run: pnpm run docs:build
working-directory: ${{ env.BUILD_PATH }}
- name: Install Alibaba Cloud OSSUTIL
run: wget http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64 && chmod +x ossutil64
- name: Configure Alibaba Cloud OSSUTIL
run: ./ossutil64 config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
- name: Upload the web folder to the chosen OSS bucket
run: ./ossutil64 --config-file .ossutilconfig cp ${{ github.workspace }}/docs/.vitepress/dist oss://${BUCKET} -r -f
+2 -10
View File
@@ -1,13 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
.vscode
*.local
.idea
.bin
.vscode
yarn.lock
yarn-error.log
build
devtools/*
.parcel-cache
+13
View File
@@ -0,0 +1,13 @@
{
"extends": [
"development"
],
"hints": {
"axe/text-alternatives": [
"default",
{
"image-alt": "off"
}
]
}
}
+3
View File
@@ -0,0 +1,3 @@
registry=https://registry.npmmirror.com
electron_mirror=https://npmmirror.com/mirrors/electron/
package-manager=pnpm
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 士子☀️
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+51 -19
View File
@@ -1,32 +1,64 @@
# media-downloader
m3u8 视频在线提取工具 流媒体下载 m3u8下载 桌面客户端 windows mac
# 功能简介
m3u8 视频在线提取工具 流媒体下载 m3u8 下载 桌面客户端 windows mac。
可以直接在线获取 m3u8 链接地址,无需使用使用网络抓包,无需安装浏览器插件,可以直接带出请求标头……详情请参考[使用帮助](https://downloader.caorushizi.cn/guides.html?form=github)
可以直接在线获取 m3u8 链接地址,无需使用使用网络抓包,无需安装浏览器插件,可以直接带出请求标头……
## 上手指南
# 下载链接
以下指南将帮助你在本地机器上安装和运行该项目,进行开发和测试。关于如何将该项目部署到在线环境,请参考部署小节。
- [v1.0.3 windows](http://static.ziying.site/media-downloader-1.0.3%20Setup.exe)
- [v1.0.2 windows](http://static.ziying.site/media-downloader-1.0.2%20Setup.exe)
- [v1.0.1 windows](http://static.ziying.site/media-downloader-1.0.1%20Setup.exe)
- [v1.0.0 windows](http://static.ziying.site/media-downloader-1.0.0%20Setup.exe)
## 安装要求
# 更新日志 2021.04.18
1. 新增视频批量下载功能;
2. 添加收藏功能;
3. 更新了界面样式;
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
# 软件截图
## 运行代码
![首页](http://static.ziying.site/2021-04-29-home.png)
安装依赖 `pnpm i`
![浏览器页面](http://static.ziying.site/downloader-browser.png)
开发环境 `pnpm run dev`
![收藏页面](http://static.ziying.site/2021-04-29-fav.png)
打包运行 `pnpm run release`
![设置页面](http://static.ziying.site/2021-04-29-setting.png)
## 下载链接
# 使用 Vite & esBuild 构建
**v2.0.12023.7.1 发布)**
![vite esbuild](http://static.ziying.site/vite+esbuild.png)
- [windows media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
- [macos media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
**更新日志**
- 暗黑模式
- 更多下载配置
- 支持请求标头自动带入
- 支持开启广告过滤
- 支持开启沉浸式嗅探
- 支持切换手机和 PC 模式
- 支持修改同时下载数量
- 修复了一些 bug
**v1.1.52022.2.5 发布)**
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
**更新日志**
## 软件截图
![首页](https://raw.githubusercontent.com/caorushizi/m3u8-downloader/master/screenshot/home-page.png)
![设置页面](https://raw.githubusercontent.com/caorushizi/m3u8-downloader/master/screenshot/setting-page.png)
![资源提取](https://raw.githubusercontent.com/caorushizi/m3u8-downloader/master/screenshot/browser-page.png)
## 技术栈
- vite <https://cn.vitejs.dev>
- antd <https://ant.design>
- electron <https://www.electronjs.org>
## 鸣谢
N_m3u8DL-CLI 来自于 <https://github.com/nilaoda/N_m3u8DL-CLI>
mediago 来自于 <https://github.com/caorushizi/mediago>
+2
View File
@@ -0,0 +1,2 @@
.vitepress/dist
.vitepress/cache
+63
View File
@@ -0,0 +1,63 @@
import { HeadConfig } from "vitepress";
import { defineConfig } from "vitepress";
const isDev = process.env.NODE_ENV === "development";
const head: HeadConfig[] = [
["link", { rel: "shortcut icon", href: "/favicon.svg" }],
];
if (!isDev) {
head.push([
"script",
{},
`var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?eefcbd14f0323044aa0ca678cd278381";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();`,
]);
}
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "media-downloader",
description: "简单易用,快速下载",
lastUpdated: true,
head,
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 © 2019-present caorushizi",
},
},
});
@@ -0,0 +1,50 @@
<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",
reaction: true,
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),
});
},
};
+30
View File
@@ -0,0 +1,30 @@
: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-hover-border: #5e9ef3;
--vp-button-brand-hover-text: #fff;
--vp-button-brand-hover-bg: #2a82f6;
--vp-button-brand-active-border: #f6ceec;
--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 media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
- [macos media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
**更新日志**
- 暗黑模式
- 更多下载配置
- 支持请求标头自动带入
- 支持开启广告过滤
- 支持开启沉浸式嗅探
- 支持切换手机和 PC 模式
- 支持修改同时下载数量
- 修复了一些 bug
**v1.1.52022.2.5 发布)**
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
**更新日志**
- 支持视频下载
## 操作说明
### 视频自动嗅探
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 media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
- ~~macos media-downloader v1.1.5 无~~
**更新日志**
- 支持视频下载
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: 支持同时最多下载10个视频和直播资源,高速带宽不闲置。
---
+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

-14
View File
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="referrer" content="never">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>在线视频下载</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/renderer/main.tsx"></script>
</body>
</html>
+24 -137
View File
@@ -1,144 +1,31 @@
{
"name": "media-downloader",
"version": "1.1.2",
"description": "在线视频下载",
"author": "caorushizi",
"main": "dist/main/index.js",
"license": "MIT",
"homepage": "./",
"name": "electron-template",
"version": "0.1.0",
"description": "electron-template",
"main": "index.js",
"scripts": {
"dist": "cross-env NODE_ENV=production node script/build.js",
"watch": "cross-env NODE_ENV=development node script/watch.js",
"electron": "cross-env NODE_ENV=production electron .",
"pack": "electron-builder --dir",
"build": "electron-builder",
"publish": "npm run dist && npm run build",
"build-debug": "npm run dist && npm run pack && ./build/win-unpacked/在线视频下载.exe"
"dev": "pnpm run build:mobile && pnpm --parallel -F \"./packages/*\" run dev",
"build": "pnpm run build:mobile && pnpm run build:main && pnpm run build:renderer",
"build:mobile": "pnpm -F mobile run build",
"build:main": "pnpm -F main run build",
"build:renderer": "pnpm -F renderer run build",
"pack": "pnpm -F media-downloader run pack",
"release": "pnpm run build && pnpm -F main run release",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@ant-design/pro-form": "^1.49.6",
"@ant-design/pro-table": "^2.59.2",
"@chakra-ui/react": "^1.7.2",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@sentry/electron": "^2.5.4",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"antd": "^4.17.2",
"classnames": "^2.3.1",
"connected-react-router": "^6.9.1",
"electron-devtools-installer": "^3.2.0",
"electron-log": "^4.3.2",
"electron-squirrel-startup": "^1.0.0",
"electron-store": "^7.0.2",
"framer-motion": "^4",
"glob": "^7.1.6",
"localforage": "^1.9.0",
"m3u8-parser": "^4.7.0",
"moment": "^2.29.1",
"nanoid": "^3.1.30",
"prop-types": "^15.7.2",
"qs": "^6.10.1",
"re-resizable": "^6.9.1",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-file-drop": "^3.1.2",
"react-redux": "^7.2.4",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.6",
"redux": "^4.1.1",
"redux-devtools-extension": "^2.13.9",
"redux-saga": "^1.1.3",
"semver": "^7.3.4"
"keywords": [],
"author": "",
"license": "MIT",
"pnpm": {
"patchedDependencies": {
"@cliqz/adblocker@1.26.6": "patches/@cliqz__adblocker@1.26.6.patch"
}
},
"devDependencies": {
"@rollup/plugin-eslint": "^8.0.1",
"@types/glob": "^7.1.3",
"@types/node": "^14.14.35",
"@types/qs": "^6.9.7",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.8",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/semver": "^7.3.4",
"@typescript-eslint/eslint-plugin": "^4.0.0",
"@typescript-eslint/parser": "^4.0.0",
"@vitejs/plugin-react-refresh": "^1.3.1",
"babel-eslint": "^10.0.0",
"chalk": "^4.1.2",
"concurrently": "^6.0.0",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"electron": "16.0.4",
"electron-builder": "^22.14.2",
"electron-updater": "^4.6.1",
"esbuild": "^0.9.5",
"eslint": "^7.5.0",
"eslint-config-prettier": "^8.1.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^4.0.8",
"less": "^4.1.1",
"prettier": "2.2.1",
"rimraf": "^3.0.2",
"sass": "^1.32.8",
"typescript": "^4.2.4",
"vite": "^2.5.0"
},
"build": {
"productName": "在线视频下载",
"appId": "mediago.ziying.site",
"copyright": "caorushizi",
"artifactName": "media-downloader-setup-${version}.${ext}",
"directories": {
"output": "build"
},
"files": [
"dist/**/*",
"package.json"
],
"extraResources": [
".bin/**"
],
"win": {
"icon": "assets/icon.ico",
"requestedExecutionLevel": "requireAdministrator",
"target": [
{
"target": "nsis"
}
]
},
"dmg": {
"contents": []
},
"mac": {
"icon": "build/icons/icon.icns"
},
"linux": {
"icon": "build/icons"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "",
"uninstallerIcon": "",
"installerHeaderIcon": "",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "",
"include": "",
"script": ""
}
"@waline/client": "^2.15.5",
"vitepress": "1.0.0-beta.3",
"vue": "^3.3.4"
}
}
View File
+21
View File
@@ -0,0 +1,21 @@
module.exports = {
env: {
node: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
],
overrides: [],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "prettier"],
rules: {
"prettier/prettier": "warn",
},
};
+6
View File
@@ -0,0 +1,6 @@
.bin
node_modules
build
dist
bin/Logs
release
+1
View File
@@ -0,0 +1 @@
{}
+4
View File
@@ -0,0 +1,4 @@
node_modules
output
bin
mobile
+13
View File
@@ -0,0 +1,13 @@
{
"name": "media-downloader",
"version": "2.0.2",
"description": "在线视频下载器",
"main": "main/index.js",
"scripts": {},
"author": "",
"license": "MIT",
"dependencies": {
"@cliqz/adblocker-electron": "^1.26.6",
"@cliqz/adblocker-electron-preload": "^1.26.6"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

+74
View File
@@ -0,0 +1,74 @@
{
"name": "main",
"version": "0.1.0",
"description": "在线视频下载器",
"main": "main/index.js",
"scripts": {
"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"
},
"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",
"chokidar": "^3.5.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",
"eslint-plugin-promise": "6.1.1",
"prebuild-install": "^7.1.1",
"prettier": "2.8.8",
"semver": "^7.5.2",
"typescript": "^5.1.3"
},
"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",
"electron-devtools-installer": "^3.2.0",
"electron-is-dev": "^2.0.0",
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"execa": "^7.1.1",
"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",
"lodash": "^4.17.21",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.17"
}
}
+46
View File
@@ -0,0 +1,46 @@
import * as esbuild from "esbuild";
import {
mainResolve,
loadDotEnvDefined,
copyResource,
removeResource,
} from "./utils.mjs";
const mainDefined = loadDotEnvDefined();
removeResource([mainResolve("app/build")]);
const path = "build/Release/better_sqlite3.node";
copyResource([
{
from: mainResolve("node_modules/better-sqlite3", path),
to: mainResolve("app", path),
},
]);
esbuild.build({
entryPoints: [
mainResolve("src/index.ts"),
mainResolve("src/preload.ts"),
mainResolve("src/webview.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("app/build/main"),
loader: { ".png": "file" },
minify: true,
});
+90
View File
@@ -0,0 +1,90 @@
import { spawn } from "child_process";
import electron from "electron";
import * as esbuild from "esbuild";
import chokidar from "chokidar";
import { loadDotEnvRuntime, mainResolve, log, copyResource } from "./utils.mjs";
let electronProcess = 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 ctx = await esbuild.context({
entryPoints: [
mainResolve("src/index.ts"),
mainResolve("src/preload.ts"),
mainResolve("src/webview.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", process.platform).replace(/\\/g, "\\\\")}"`,
},
plugins: [],
outdir: mainResolve("app/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("app/build/main/index.js")];
electronProcess = spawn(String(electron), args);
electronProcess.stdout.on("data", (data) => {
log(String(data));
});
electronProcess.stderr.on("data", (data) => {
log(String(data));
});
}
try {
await ctx.rebuild();
await copySource();
await startElectron();
} catch (e) {
console.error(e);
process.exit();
}
+119
View File
@@ -0,0 +1,119 @@
import builder from "electron-builder";
import { readFileSync } from "node:fs";
import semver from "semver";
import {
mainResolve,
loadDotEnvRuntime,
log,
removeResource,
} from "./utils.mjs";
removeResource([mainResolve("release")]);
const packageJson = JSON.parse(readFileSync(mainResolve("./app/package.json")));
loadDotEnvRuntime();
if (semver.neq(process.env.APP_VERSION, packageJson.version)) {
log("请先同步构建版本和发布版本");
process.exit(0);
}
// 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: "./release",
},
files: [
{
from: "./build",
to: "./",
},
"./package.json",
],
extraResources: [
{
from: "./app/mobile",
to: "mobile",
},
{
from: "./app/bin/${platform}/",
to: "bin",
},
],
win: {
icon: "../assets/icon.ico",
target: [
{
target: "nsis",
},
],
},
dmg: {
contents: [
{
x: 410,
y: 150,
type: "link",
path: "/Applications",
},
{
x: 130,
y: 150,
type: "file",
},
],
},
mac: {
icon: "../assets/icon.icns",
target: {
target: "default",
arch: ["x64", "arm64"],
},
},
linux: {},
nsis: {
oneClick: true,
allowElevation: true,
allowToChangeInstallationDirectory: false,
installerIcon: "",
uninstallerIcon: "",
installerHeaderIcon: "",
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: "",
include: "",
script: "",
},
publish: {
provider: "github",
repo: "m3u8-downloader",
owner: "caorushizi",
releaseType: "prerelease",
},
};
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);
}
+80
View File
@@ -0,0 +1,80 @@
import { existsSync, cpSync, rmSync } 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;
}, {});
}
export function copyResource(resource) {
resource.forEach((r) => {
const { from, to } = r;
cpSync(from, to, {
recursive: true,
});
});
}
export function removeResource(resource) {
resource.forEach((r) => {
rmSync(r, { recursive: true, force: true });
});
}
+99
View File
@@ -0,0 +1,99 @@
import { inject, injectable } from "inversify";
import {
DatabaseService,
DownloadStatus,
IpcHandlerService,
MainWindowService,
ProtocolService,
UpdateService,
VideoRepository,
WebviewService,
type App,
WebService,
DevToolsService,
StoreService,
} from "./interfaces";
import { TYPES } from "./types";
import { Menu, Tray, app, nativeImage, nativeTheme } from "electron";
import TrayIcon from "./tray-icon.png";
import path from "path";
@injectable()
export default class ElectronApp implements App {
constructor(
@inject(TYPES.MainWindowService)
private readonly mainWindow: MainWindowService,
@inject(TYPES.ProtocolService)
private readonly protocolService: ProtocolService,
@inject(TYPES.UpdateService)
private readonly updateService: UpdateService,
@inject(TYPES.IpcHandlerService)
private readonly ipcHandler: IpcHandlerService,
@inject(TYPES.DatabaseService)
private readonly dataService: DatabaseService,
@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
) {}
private async seriveInit(): Promise<void> {
this.protocolService.create();
await this.dataService.init();
this.mainWindow.init();
this.ipcHandler.init();
this.updateService.init();
this.webview.init(), this.devTools.init();
this.webService.init();
}
async init(): Promise<void> {
await this.seriveInit();
app.on("activate", () => {
this.mainWindow.init();
});
this.initAppTheme();
this.resetDownloadStatus();
this.initTray();
}
initAppTheme(): void {
const theme = this.storeService.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 状态的数据, 全部重置为失败
const videos = await this.videoRepository.findWattingAndDownloadingVideos();
const videoIds = videos.map((video) => video.id);
await this.videoRepository.changeVideoStatus(
videoIds,
DownloadStatus.Failed
);
}
}
@@ -0,0 +1,96 @@
import { IpcMainEvent } from "electron/main";
import { inject, injectable } from "inversify";
import { handle } from "../helper/decorator";
import {
StoreService,
type Controller,
VideoRepository,
DownloadItem,
DownloadItemPagination,
DownloadService,
Task,
DownloadStatus,
} from "../interfaces";
import { TYPES } from "../types";
import { spawnDownload } from "helper";
import MainWindowServiceImpl from "windows/Main";
@injectable()
export default class DownloadController implements Controller {
constructor(
@inject(TYPES.StoreService)
private readonly storeService: StoreService,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository,
@inject(TYPES.DownloadService)
private readonly downloadService: DownloadService,
@inject(TYPES.MainWindowService)
private readonly mainWindow: MainWindowServiceImpl
) {}
@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);
return item;
}
@handle("edit-download-item")
async editDownloadItem(e: IpcMainEvent, video: DownloadItem) {
const item = await this.videoRepository.editVideo(video);
return item;
}
@handle("download-now")
async downloadNow(e: IpcMainEvent, video: DownloadItem) {
// 添加下载项
const item = await this.addDownloadItem(e, video);
// 开始下载
await this.startDownload(e, item.id);
return item;
}
@handle("get-download-items")
async getDownloadItems(e: IpcMainEvent, pagination: DownloadItemPagination) {
return await this.videoRepository.findVideos(pagination);
}
@handle("start-download")
async startDownload(e: IpcMainEvent, vid: number) {
// 查找将要下载的视频
const video = await this.videoRepository.findVideo(vid);
if (!video) {
return Promise.reject("没有找到该视频");
}
const { name, url, headers } = video;
const local = this.storeService.get("local");
// 从配置中添加参数
const deleteSegments = this.storeService.get("deleteSegments");
const task: Task = {
id: vid,
params: {
url,
local,
name,
headers,
deleteSegments,
},
process: spawnDownload,
};
await this.videoRepository.changeVideoStatus(vid, DownloadStatus.Watting);
this.downloadService.addTask(task);
}
@handle("stop-download")
async stopDownload(e: IpcMainEvent, id: number) {
this.downloadService.stopTask(id);
}
@handle("delete-download-item")
async deleteDownloadItem(e: IpcMainEvent, id: number) {
return await this.videoRepository.deleteDownloadItem(id);
}
}
@@ -0,0 +1,266 @@
import {
dialog,
IpcMainEvent,
Menu,
MenuItem,
MenuItemConstructorOptions,
nativeTheme,
shell,
clipboard,
} from "electron";
import { Favorite } from "entity/Favorite";
import { convertToAudio } from "helper";
import { db, workspace } from "helper/variables";
import { inject, injectable } from "inversify";
import { AppStore, EnvPath } from "main";
import path from "path";
import { handle, getLocalIP } from "../helper";
import {
StoreService,
type Controller,
FavoriteRepository,
MainWindowService,
VideoRepository,
BrowserWindowService,
PlayerWindowService,
WebviewService,
} from "../interfaces";
import { TYPES } from "../types";
import fs from "fs-extra";
@injectable()
export default class HomeController implements Controller {
private sharedState: Record<string, any> = {};
constructor(
@inject(TYPES.StoreService)
private readonly storeService: StoreService,
@inject(TYPES.FavoriteRepository)
private readonly favoriteRepository: FavoriteRepository,
@inject(TYPES.MainWindowService)
private readonly mainWindow: MainWindowService,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository,
@inject(TYPES.BrowserWindowService)
private readonly browserWindow: BrowserWindowService,
@inject(TYPES.PlayerWindowService)
private readonly playerWindow: PlayerWindowService,
@inject(TYPES.WebviewService)
private readonly webviewService: WebviewService
) {}
@handle("get-env-path")
async getEnvPath(): Promise<EnvPath> {
return {
binPath: __bin__,
dbPath: db,
workspace: workspace,
platform: process.platform,
local: this.storeService.get("local"),
};
}
@handle("get-favorites")
getFavorites() {
return this.favoriteRepository.findFavorites();
}
@handle("add-favorite")
addFavorite(e: IpcMainEvent, favorite: Favorite) {
return this.favoriteRepository.addFavorite(favorite);
}
@handle("remove-favorite")
removeFavorite(e: IpcMainEvent, id: number): Promise<void> {
return this.favoriteRepository.removeFavorite(id);
}
@handle("get-app-store")
getAppStore() {
return this.storeService.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", {
action,
payload: id,
});
};
const template: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: "打开",
click: () => {
send("open");
},
},
{ type: "separator" },
{
label: "删除",
click: () => {
send("delete");
},
},
];
const menu = Menu.buildFromTemplate(template);
menu.popup();
}
@handle("select-download-dir")
async selectDownloadDir(): Promise<string> {
const window = this.mainWindow.window;
if (!window) return Promise.reject("未找到主窗口");
const result = await dialog.showOpenDialog(window, {
properties: ["openDirectory"],
});
if (!result.canceled) {
const dir = result.filePaths[0];
this.storeService.set("local", dir);
return dir;
}
return "";
}
@handle("set-app-store")
async setAppStore(e: IpcMainEvent, key: keyof AppStore, val: any) {
// useProxy
if (key === "useProxy") {
const proxy = this.storeService.get("proxy");
this.webviewService.setProxy(val, proxy);
}
// proxy
if (key === "proxy") {
const useProxy = this.storeService.get("useProxy");
useProxy && this.webviewService.setProxy(true, val);
}
// block
if (key === "blockAds") {
this.webviewService.setBlocking(val);
}
// theme
if (key === "theme") {
nativeTheme.themeSource = val;
}
// isMobile
if (key === "isMobile") {
this.webviewService.setUserAgent(val);
}
this.storeService.set(key, val);
}
@handle("open-dir")
async openDir(e: IpcMainEvent, dir: string) {
await shell.openPath(dir);
}
@handle("open-url")
async openUrl(e: IpcMainEvent, url: string) {
await shell.openExternal(url);
}
@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", {
action,
payload: id,
});
};
const item = await this.videoRepository.findVideo(id);
const template: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: "拷贝链接地址",
click: () => {
clipboard.writeText(item?.url || "");
},
},
{
label: "选择",
click: () => {
send("select");
},
},
{
label: "下载",
click: () => {
send("download");
},
},
{
label: "刷新",
click: () => {
send("refresh");
},
},
{ type: "separator" },
{
label: "删除",
click: () => {
send("delete");
},
},
];
const menu = Menu.buildFromTemplate(template);
menu.popup();
}
@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 exist = await fs.exists(input);
if (exist) {
return await convertToAudio(input, output);
} else {
return Promise.reject("未找到文件,可能是文件已经删除");
}
}
@handle("show-browser-window")
async showBrowserWindow() {
this.browserWindow.showWindow();
}
@handle("combine-to-home-page")
async combineToHomePage() {
// 关闭浏览器窗口
this.browserWindow.hideWindow();
// 修改设置中的属性
this.storeService.set("openInNewWindow", false);
}
@handle("open-player-window")
async openPlayerWindow(event: IpcMainEvent, id: number) {
// 打开播放器窗口
this.playerWindow.openWindow(id);
}
@handle("get-local-ip")
async getLocalIp() {
return getLocalIP();
}
@handle("get-shared-state")
async getSharedState() {
return this.sharedState;
}
@handle("set-shared-state")
async setSharedState(event: IpcMainEvent, state: any) {
this.sharedState = state;
}
}
@@ -0,0 +1,56 @@
import { IpcMainEvent } from "electron";
import { inject, injectable } from "inversify";
import { handle } from "../helper/decorator";
import { type Controller, WebviewService, StoreService } from "../interfaces";
import { TYPES } from "../types";
@injectable()
export default class WebviewController implements Controller {
constructor(
@inject(TYPES.WebviewService)
private readonly webview: WebviewService,
@inject(TYPES.StoreService)
private readonly storeService: StoreService
) {}
@handle("set-webview-bounds")
async setWebviewBounds(e: IpcMainEvent, bounds: Electron.Rectangle) {
this.webview.setBounds(bounds);
}
@handle("webview-load-url")
async browserViewLoadUrl(e: IpcMainEvent, url?: string): Promise<void> {
await this.webview.loadURL(url);
}
@handle("webview-go-back")
async webviewGoBack(): Promise<boolean> {
return this.webview.goBack();
}
@handle("webview-reload")
async webviewReload() {
await this.webview.reload();
}
@handle("webview-show")
async webviewShow() {
this.webview.show();
}
@handle("webview-hide")
async webviewHide() {
this.webview.hide();
}
@handle("webview-go-home")
async webviewGoHome() {
await this.webview.goHome();
}
@handle("webview-change-user-agent")
async webviewChangeUserAgent(e: IpcMainEvent, isMobile: boolean) {
this.webview.setUserAgent(isMobile);
this.storeService.set("isMobile", isMobile);
}
}
+39
View File
@@ -0,0 +1,39 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
@Entity({
name: "favorite",
})
export class Favorite {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: "text",
nullable: false,
})
title: string;
@Column({
type: "text",
nullable: false,
})
url: string;
@Column({
type: "text",
nullable: true,
})
icon?: string;
@CreateDateColumn()
createdDate: Date;
@UpdateDateColumn()
updatedDate: Date;
}
+54
View File
@@ -0,0 +1,54 @@
import { DownloadStatus } from "interfaces";
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
@Entity({
name: "video",
})
export class Video {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: "text",
nullable: true,
})
name: string;
@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,
default: DownloadStatus.Ready,
})
status: DownloadStatus;
@CreateDateColumn()
createdDate: Date;
@UpdateDateColumn()
updatedDate: Date;
}
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable no-var */
declare global {
var __bin__: string;
}
export {};
+13
View File
@@ -0,0 +1,13 @@
export const handle = (route: string) => {
return (target: any, propertyKey: string): void => {
Reflect.defineMetadata("ipc-method", "handle", target, propertyKey);
Reflect.defineMetadata("ipc-channel", route, target, propertyKey);
};
};
export const on = (route: string) => {
return (target: any, propertyName: string): void => {
Reflect.defineMetadata("ipc-method", "on", target, propertyName);
Reflect.defineMetadata("ipc-channel", route, target, propertyName);
};
};
+205
View File
@@ -0,0 +1,205 @@
import { execa } from "execa";
import { macDownloaderPath, winDownloaderPath } from "./variables";
import iconv from "iconv-lite";
import { formatHeaders, 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 = async (params: DownloadParams): Promise<void> => {
const {
id,
abortSignal,
url,
local,
name,
deleteSegments,
headers,
callback,
} = params;
const progressReg = /Progress:\s(\d+)\/(\d+)\s\(.+?\).+?\((.+?\/s).*?\)/g;
const isLiveReg = /识别为直播流, 开始录制/g;
const startDownloadReg = /开始下载文件/g;
return new Promise((resolve, reject) => {
const spawnParams = [url, "--workDir", local, "--saveName", name];
if (headers) {
spawnParams.push("--headers", formatHeaders(headers));
}
if (deleteSegments) {
spawnParams.push("--enableDelAfterDone");
}
const downloader = execa(winDownloaderPath, spawnParams, {
signal: abortSignal.signal,
});
let isLive = false;
downloader.stdout?.on("data", (data) => {
const str = iconv.decode(Buffer.from(data), "gbk");
str.split("\n").forEach((item) => {
if (item.trim() == "") {
return;
}
if (isLiveReg.test(item) || startDownloadReg.test(item)) {
callback({
id,
type: "ready",
isLive,
cur: "",
total: "",
speed: "",
});
isLive = true;
}
const result = progressReg.exec(item);
if (!result) {
return;
}
const [, cur, total, speed] = result;
const progress: DownloadProgress = {
id,
type: "progress",
cur,
total,
speed,
isLive,
};
callback(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,
headers,
callback,
} = params;
// const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
const progressReg = /([\d.]+)%/g;
const errorReg = /ERROR/g;
const startDownloadReg = /保存文件名:/g;
const isLiveReg = /检测到直播流/g;
return new Promise((resolve, reject) => {
const spawnParams = [
url,
"--tmp-dir",
local,
"--save-dir",
local,
"--save-name",
name,
"--auto-select",
];
if (headers) {
const h: Record<string, unknown> = JSON.parse(headers);
Object.entries(h).forEach(([k, v]) => {
spawnParams.push("-H", `${k}: ${v}`);
});
}
if (deleteSegments) {
spawnParams.push("--del-after-done");
}
const downloader = execa(macDownloaderPath, spawnParams, {
signal: abortSignal.signal,
});
let isLive = false;
downloader.stdout?.on("data", (data) => {
const str = String(Buffer.from(data));
str.split("\n").forEach((item) => {
if (item.trim() == "") {
return;
}
if (isLiveReg.test(item) || startDownloadReg.test(item)) {
callback({
id,
type: "ready",
isLive,
cur: "",
total: "",
speed: "",
});
isLive = true;
}
const log = stripColors(item);
if (errorReg.test(log)) {
reject(new Error(log));
return;
}
const result = progressReg.exec(log);
if (!result) {
return;
}
const [, precentage, speed] = result;
const cur = String(Number(precentage) * 10);
if (cur === "0") {
return;
}
const total = "1000";
// FIXME: 无法获取是否为直播流
const progress: DownloadProgress = {
id,
type: "progress",
cur,
total,
speed,
isLive,
};
callback(progress);
});
});
downloader.on("error", (err) => {
reject(err);
});
downloader.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error("未知错误"));
}
});
});
};
+39
View File
@@ -0,0 +1,39 @@
import { spawn } from "child_process";
import { ffmpegPath } from "./variables";
export const convertToAudio = async (
input: string,
output: string
): Promise<void> => {
return new Promise((resolve, reject) => {
const ffmpeg = spawn(ffmpegPath, [
"-y",
"-v",
"error",
"-i",
input,
"-acodec",
"mp3",
"-format",
"mp3",
output,
]);
let errData = "";
ffmpeg.stderr.on("data", (data) => {
errData += String(data);
});
ffmpeg.on("error", (err) => {
reject(err);
});
ffmpeg.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(errData));
}
});
});
};
+13
View File
@@ -0,0 +1,13 @@
import axios from "axios";
import https from "https";
const agent = new https.Agent({
rejectUnauthorized: false,
});
// 创建 axios 实例,并指定自定义的 HTTPS Agent
const instance = axios.create({
httpsAgent: agent,
});
export default instance;
+31
View File
@@ -0,0 +1,31 @@
import os from "os";
export function getLocalIP() {
const interfaces = os.networkInterfaces();
let localIP = "";
// 遍历网络接口
for (const key in interfaces) {
const iface = interfaces[key];
if (!iface) continue;
// 过滤出 IPv4 地址且非回环地址
const filteredIface = iface.filter(
(details) => details.family === "IPv4" && !details.internal
);
if (filteredIface.length > 0) {
localIP = filteredIface[0].address;
break;
}
}
return localIP;
}
export { sleep } from "./utils";
export * from "./variables";
export { on, handle } from "./decorator";
export { spawnDownload } from "./download";
export { convertToAudio } from "./ffmpeg";
export { default as http } from "./http";
+24
View File
@@ -0,0 +1,24 @@
import EventEmitter from "events";
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();
+31
View File
@@ -0,0 +1,31 @@
import { app } from "electron";
import isDev from "electron-is-dev";
import path from "path";
if (!isDev) {
global.__bin__ = path.resolve(app.getAppPath(), "../bin");
}
export const appData = app.getPath("appData");
export const appName = process.env.APP_NAME || "electron-template";
export const workspace = path.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.exe");
export const ffmpegPath =
process.platform === "win32"
? path.resolve(__bin__, "ffmpeg.exe")
: path.resolve(__bin__, "ffmpeg");
export const mobilePath = isDev
? path.resolve(app.getAppPath(), "../../mobile")
: path.resolve(app.getAppPath(), "../mobile");
// 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";
+32
View File
@@ -0,0 +1,32 @@
import "reflect-metadata";
import { app, protocol } from "electron";
import { defaultScheme } from "./helper/variables";
import { type App } from "./interfaces";
import { container } from "./inversify.config";
import { TYPES } from "./types";
const gotTheLock = app.requestSingleInstanceLock();
const start = async (): Promise<void> => {
if (!gotTheLock) {
app.quit();
}
protocol.registerSchemesAsPrivileged([
{
scheme: defaultScheme,
privileges: {
secure: true,
standard: true,
},
},
]);
await app.whenReady();
const mediago = container.get<App>(TYPES.App);
mediago.init();
app.on("window-all-closed", () => {
// empty
});
};
void start();
+173
View File
@@ -0,0 +1,173 @@
import { type BrowserWindow, BrowserView } 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 {
window: BrowserWindow | null;
init: () => void;
}
export interface PlayerWindowService {
openWindow: (id: number) => void;
}
export interface BrowserWindowService {
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;
name: string;
url: string;
headers: string;
}
export enum DownloadFilter {
list = "list",
done = "done",
}
export interface DownloadItemPagination {
current?: number;
pageSize?: number;
filter?: DownloadFilter;
}
export interface VideoResponse {
total: number;
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[]>;
changeVideoIsLive: (
id: number | number[],
isLive: boolean
) => Promise<UpdateResult>;
findVideoByUrl: (url: string) => Promise<Video | null>;
}
export interface FavoriteRepository {
findFavorites: () => Promise<Favorite[]>;
addFavorite: (favorite: Favorite) => Promise<Favorite>;
removeFavorite: (id: number) => Promise<void>;
}
export interface WebviewService {
view: BrowserView;
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;
setUserAgent: (isMobile: boolean) => void;
}
export interface WebService {
init: () => void;
}
export enum DownloadStatus {
Ready = "ready",
Watting = "watting",
Downloading = "downloading",
Stopped = "stopped",
Success = "success",
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>;
};
export interface DownloadProgress {
id: number;
type: string;
cur: string;
total: string;
speed: string;
isLive: boolean;
}
export interface DownloadParams {
id: number;
url: string;
local: string;
name: string;
headers: string;
abortSignal: AbortController;
deleteSegments?: boolean;
callback: (progress: DownloadProgress) => void;
}
export interface DevToolsService {
init: () => Promise<void>;
}
+79
View File
@@ -0,0 +1,79 @@
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 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 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 ProtocolServiceImpl from "./services/ProtocolServiceImpl";
import UpdateServiceImpl from "./services/UpdateServiceImpl";
import { TYPES } from "./types";
import WebServiceImpl from "services/WebServiceImpl";
import MainWindow from "./windows/Main";
import PlayerWindow from "windows/Player";
import BrowserWin from "windows/Browser";
const container = new Container({
skipBaseClassChecks: true,
defaultScope: "Singleton",
autoBindInjectable: true,
});
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);
// windows
container.bind<MainWindowService>(TYPES.MainWindowService).to(MainWindow);
container.bind<BrowserWindowService>(TYPES.BrowserWindowService).to(BrowserWin);
container.bind<PlayerWindowService>(TYPES.PlayerWindowService).to(PlayerWindow);
// === 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);
container
.bind<FavoriteRepository>(TYPES.FavoriteRepository)
.to(FavoriteRepositoryImpl);
export { container };
+98
View File
@@ -0,0 +1,98 @@
import { Video, Rectangle } from "electron";
import { type Favorite } from "entity/Favorite";
import {
DownloadItem,
DownloadItemPagination,
VideoResponse,
} from "interfaces";
declare interface EnvPath {
binPath: string;
dbPath: string;
workspace: string;
platform: string;
local: string;
}
declare interface BrowserWindowInitialVal {
url?: string;
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>;
setUserAgent: (isMobile: boolean) => Promise<void>;
}
declare interface WebSource {
url: string;
name: string;
headers: string;
}
declare interface AppStore {
// 本地存储地址
local: string;
// 下载完成提示音
promptTone: boolean;
// 代理地址
proxy: string;
// 是否开启代理
useProxy: boolean;
// 下载完成后删除原始文件
deleteSegments: boolean;
// 新窗口打开浏览器
openInNewWindow: boolean;
mainBounds?: Rectangle;
browserBounds?: Rectangle;
blockAds: boolean;
// 主题
theme: "system" | "light" | "dark";
// 使用浏览器插件
useExtension: boolean;
// 是否使用手机UA
isMobile: boolean;
// 最大同时下载数
maxRunner: number;
}
declare interface BrowserStore {
url: string;
sourceList: WebSource[];
}
+60
View File
@@ -0,0 +1,60 @@
import { contextBridge, ipcRenderer, shell } from "electron";
import { ElectronAPI } from "./main";
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) => {
const key = `${channel}-${funcId}`;
apiFunctions[key] = listener;
ipcRenderer.on(channel, listener);
},
removeEventListener: (channel, funcId) => {
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),
setUserAgent: (isMobile: boolean) =>
ipcRenderer.invoke("webview-change-user-agent", isMobile),
};
contextBridge.exposeInMainWorld(apiKey, handleApi);
@@ -0,0 +1,48 @@
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);
}
}
@@ -0,0 +1,126 @@
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;
item.headers = video.headers;
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 changeVideoIsLive(id: number | number[], isLive: boolean) {
const ids = !Array.isArray(id) ? [id] : id;
return this.dataService.appDataSource
.createQueryBuilder()
.update(Video)
.set({ isLive })
.where({ id: In(ids) })
.execute();
}
async findWattingAndDownloadingVideos() {
return await this.dataService.appDataSource.getRepository(Video).find({
where: {
status: In([DownloadStatus.Downloading, DownloadStatus.Watting]),
},
});
}
async deleteDownloadItem(id: number) {
return await this.dataService.appDataSource.getRepository(Video).delete(id);
}
async findVideoByUrl(url: string) {
return this.dataService.appDataSource.getRepository(Video).findOneBy({
url,
});
}
}
@@ -0,0 +1,36 @@
import { db } from "../helper/variables";
import { inject, injectable } from "inversify";
import { DataSource, EntityManager } from "typeorm";
import { TYPES } from "../types";
import { DatabaseService, LoggerService } from "../interfaces";
import { Video } from "../entity/Video";
import { Favorite } from "entity/Favorite";
@injectable()
export default class DatabaseServiceImpl implements DatabaseService {
appDataSource: DataSource;
constructor(
@inject(TYPES.LoggerService)
private readonly logger: LoggerService
) {
this.appDataSource = new DataSource({
type: "better-sqlite3",
database: db,
synchronize: true,
logging: false,
entities: [Video, Favorite],
migrations: [],
subscribers: [],
});
}
async init(): Promise<void> {
this.logger.info("数据库地址是: ", db);
await this.appDataSource.initialize();
}
get manager(): EntityManager {
return this.appDataSource.manager;
}
}
@@ -0,0 +1,34 @@
import { DevToolsService, LoggerService } from "../interfaces";
import { inject, injectable } from "inversify";
import { TYPES } from "../types";
import isDev from "electron-is-dev";
import installExtension, {
REDUX_DEVTOOLS,
REACT_DEVELOPER_TOOLS,
} from "electron-devtools-installer";
@injectable()
export default class DevToolsServiceImpl implements DevToolsService {
constructor(
@inject(TYPES.LoggerService)
private readonly logger: LoggerService
) {}
async init(): Promise<void> {
if (!isDev) {
return;
}
// 开发环境中可以通过设置环境变量来控制是否加载开发者工具
if (!process.env.LOAD_DEVTOOLS) {
return;
}
try {
this.logger.debug("当前环境为开发环境,开始加载开发者工具");
await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]);
this.logger.debug("加载开发者工具成功");
} catch (err: any) {
this.logger.error("加载开发者工具失败", err);
}
}
}
@@ -0,0 +1,144 @@
import EventEmitter from "events";
import { inject, injectable } from "inversify";
import {
DownloadService,
DownloadStatus,
StoreService,
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: number;
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,
@inject(TYPES.StoreService)
private readonly storeService: StoreService
) {
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.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,
callback: (progress) => {
if (progress.type === "progress") {
this.emit("download-progress", progress);
} else if (progress.type === "ready") {
this.emit("download-ready-start", progress);
if (progress.isLive) {
this.removeTask(progress.id);
}
}
},
});
delete this.signal[task.id];
this.log(`taskId: ${task.id} success`);
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 {
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);
}
}
}
log(...args: any[]) {
if (this.debug) {
this.logger.info(`[DownloadService] `, ...args);
}
}
}
@@ -0,0 +1,76 @@
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));
}
}
@@ -0,0 +1,36 @@
import logger, { ElectronLog } from "electron-log";
import { injectable } from "inversify";
import dayjs from "dayjs";
import path from "path";
import { appName, workspace } from "../helper";
import { LoggerService } from "../interfaces";
@injectable()
export default class LoggerServiceImpl implements LoggerService {
logger: ElectronLog;
constructor() {
const datetime = dayjs().format("YYYY-MM-DD");
const logPath = path.resolve(workspace, `logs/${datetime}-${appName}.log`);
logger.transports.console.format = "{h}:{i}:{s} {text}";
logger.transports.file.getFile();
logger.transports.file.resolvePath = () => logPath;
this.logger = logger;
}
info(...args: any[]) {
return this.logger.info(...args);
}
warn(...args: any[]) {
return this.logger.warn(...args);
}
error(...args: any[]) {
return this.logger.error(...args);
}
debug(...args: any[]) {
return this.logger.debug(...args);
}
}
@@ -0,0 +1,51 @@
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,38 @@
import Store from "electron-store";
import { injectable } from "inversify";
import { AppStore } from "main";
import { download, workspace } from "../helper/variables";
import { StoreService } from "../interfaces";
import { AppTheme } from "types";
@injectable()
export default class StoreServiceImpl
extends Store<AppStore>
implements StoreService
{
constructor() {
super({
name: "config",
cwd: workspace,
fileExtension: "json",
watch: true,
defaults: {
local: download,
promptTone: true,
proxy: "",
useProxy: false,
deleteSegments: true,
openInNewWindow: false,
blockAds: true,
theme: AppTheme.System,
useExtension: false,
isMobile: false,
maxRunner: 2,
},
});
}
init(): void {
// empty
}
}
@@ -0,0 +1,27 @@
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
) {}
async init(): Promise<void> {
if (isDev) return;
try {
autoUpdater.disableWebInstaller = true;
autoUpdater.logger = this.logger.logger;
await autoUpdater.checkForUpdatesAndNotify({
title: "自动更新完成",
body: "下次重启时将会自动安装",
});
} catch (e) {
this.logger.info("update error", e);
}
}
}
@@ -0,0 +1,95 @@
import {
LoggerService,
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,
@inject(TYPES.LoggerService)
private readonly logger: LoggerService
) {
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));
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, () => {
this.logger.info("web server init success.");
})
.on("error", (err: unknown) => {
this.logger.error("server error", err);
});
}
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,277 @@
import {
BrowserView,
CallbackResponse,
OnBeforeSendHeadersListenerDetails,
session,
} from "electron";
import {
BrowserWindowService,
LoggerService,
MainWindowService,
StoreService,
VideoRepository,
WebviewService,
} from "../interfaces";
import { inject, injectable } from "inversify";
import { TYPES } from "../types";
import isDev from "electron-is-dev";
import { PERSIST_WEBVIEW, mobileUA, pcUA } from "helper/variables";
import { ElectronBlocker } from "@cliqz/adblocker-electron";
import fetch from "cross-fetch";
import path from "path";
import { WebSource } from "main";
// FIXME: 需要重构
@injectable()
export default class WebviewServiceImpl implements WebviewService {
public view: BrowserView;
private blocker?: ElectronBlocker;
private sources = new Set();
constructor(
@inject(TYPES.MainWindowService)
private readonly mainWindow: MainWindowService,
@inject(TYPES.LoggerService)
private readonly logger: LoggerService,
@inject(TYPES.BrowserWindowService)
private readonly browserWindow: BrowserWindowService,
@inject(TYPES.StoreService)
private readonly storeService: StoreService,
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository
) {
// 初始化 blocker
this.initBlocker();
}
async init(): Promise<void> {
this.view = new BrowserView({
webPreferences: {
partition: PERSIST_WEBVIEW,
preload: path.resolve(__dirname, "./webview.js"),
},
});
this.view.setBackgroundColor("#fff");
this.view.webContents.setAudioMuted(true);
const { useProxy, proxy, isMobile } = this.storeService.store;
this.setProxy(useProxy, proxy);
this.setUserAgent(isMobile);
this.view.webContents.on("dom-ready", () => {
this.sources.clear();
const title = this.view.webContents.getTitle();
const url = this.view.webContents.getURL();
this.curWindow?.webContents.send("webview-dom-ready", { title, url });
});
this.view.webContents.setWindowOpenHandler(({ url }) => {
if (url === "about:blank") {
// 兼容一些网站跳转到 about:blank
this.view.webContents.once("will-redirect", async (event, url) => {
this.loadURL(url, true);
});
} else {
this.loadURL(url, true);
}
return { action: "deny" };
});
const urls = ["<all_urls>"];
this.session.webRequest.onBeforeSendHeaders({ urls }, this.before);
}
getBounds(): Electron.Rectangle {
return this.view.getBounds();
}
setAutoResize(options: Electron.AutoResizeOptions): void {
this.view.setAutoResize(options);
}
setBackgroundColor(color: string): void {
this.view.setBackgroundColor(color);
}
show() {
this.curWindow?.setBrowserView(this.view);
isDev && this.view.webContents.openDevTools();
}
hide() {
this.curWindow?.setBrowserView(null);
isDev && this.view.webContents.closeDevTools();
}
setBounds(bounds: Electron.Rectangle): void {
if (process.platform === "darwin") {
bounds.y = bounds.y + 30;
}
this.view.setBounds(bounds);
}
async loadURL(url?: string, isNewWindow?: boolean) {
const canGoBack = this.view.webContents.canGoBack();
try {
await this.view.webContents.loadURL(url || "");
} catch (err: unknown) {
this.logger.error("加载 url 时出现错误: ", err);
throw err;
} finally {
if (!canGoBack && !isNewWindow) {
this.view.webContents.clearHistory();
}
}
}
async goBack() {
if (this.view.webContents.canGoBack()) {
this.view.webContents.goBack();
return true;
} else {
return false;
}
}
async reload() {
this.view.webContents.reload();
}
async goHome() {
this.view.webContents.clearHistory();
}
get curWindow() {
if (this.browserWindow.window) return this.browserWindow.window;
if (this.mainWindow.window) return this.mainWindow.window;
return null;
}
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.storeService.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 成功");
}
before = (
details: OnBeforeSendHeadersListenerDetails,
callback: (response: CallbackResponse) => void
): void => {
const { url } = details;
const sourceReg = /\.m3u8$/;
const detailsUrl = new URL(url);
if (sourceReg.test(detailsUrl.pathname)) {
try {
this.handleM3u8(details);
} catch (e) {
// empty
}
}
callback({});
};
handleM3u8 = async (
details: OnBeforeSendHeadersListenerDetails
): Promise<void> => {
const { id, url } = details;
this.logger.info(`在窗口中捕获 m3u8 链接: ${url} id: ${id}`);
const webContents = details.webContents;
const source: WebSource = {
url,
name: webContents?.getTitle() || "没有获取到名称",
headers: JSON.stringify(details.requestHeaders),
};
// 这里需要判断是否使用浏览器插件
const useExtension = this.storeService.get("useExtension");
if (!this.sources.has(source.url)) {
this.sources.add(source.url);
if (useExtension) {
this.view.webContents.send("webview-link-message", source);
} else {
const item = await this.videoRepository.addVideo(source);
// 这里向页面发送消息,通知页面更新
this.mainWindow.window?.webContents.send(
"download-item-notifier",
item
);
}
}
};
setUserAgent(isMobile?: boolean) {
if (isMobile) {
this.view.webContents.setUserAgent(mobileUA);
} else {
this.view.webContents.setUserAgent(pcUA);
}
this.logger.info("设置 user-agent 成功", isMobile);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

+26
View File
@@ -0,0 +1,26 @@
export const TYPES = {
App: Symbol.for("App"),
MainWindowService: Symbol.for("MainWindowService"),
StoreService: Symbol.for("StoreService"),
SessionService: Symbol.for("SessionService"),
DatabaseService: Symbol.for("DatabaseService"),
ProtocolService: Symbol.for("ProtocolService"),
UpdateService: Symbol.for("UpdateService"),
LoggerService: Symbol.for("LoggerService"),
IpcHandlerService: Symbol.for("IpcHandlerService"),
Controller: Symbol.for("Controller"),
VideoRepository: Symbol.for("VideoRepository"),
FavoriteRepository: Symbol.for("FavoriteRepository"),
WebviewService: Symbol.for("WebviewService"),
DownloadService: Symbol.for("DownloadService"),
BrowserWindowService: Symbol.for("BrowserWindowService"),
DevToolsService: Symbol.for("DevToolsService"),
WebService: Symbol.for("WebService"),
PlayerWindowService: Symbol.for("PlayerWindowService"),
};
export enum AppTheme {
System = "system",
Light = "light",
Dark = "dark",
}
+107
View File
@@ -0,0 +1,107 @@
import { WebSource } from "main";
import { IpcRendererEvent, ipcRenderer } from "electron/renderer";
// 创建浮窗容器元素
const floatingContainer = document.createElement("div");
floatingContainer.style.cssText = `
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 200px;
z-index: 10000;
display: none;
font-size: 16px;
`;
// 创建浮窗按钮元素
const floatingButton = document.createElement("div");
floatingButton.textContent = "检测到视频资源……";
floatingButton.style.cssText = `
background-color: #000;
color: #fff;
padding: 10px;
cursor: pointer;
opacity: 0.8;
`;
// 创建浮窗列表元素
const floatingList = document.createElement("ul");
floatingList.style.cssText = `
display: none;
background-color: #000;
list-style: none;
padding: 0;
margin: 0;
`;
const itemStyle = `
padding: 10px;
color: #fff;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const items: WebSource[] = [];
ipcRenderer.on(
"webview-link-message",
(e: IpcRendererEvent, data: WebSource) => {
floatingContainer.style.display = "block";
items.push(data);
// 清空列表
while (floatingList?.firstChild) {
floatingList.removeChild(floatingList.firstChild);
}
for (let i = 0; i < items.length; i++) {
const item = items[i];
// 创建列表项元素
const elItem = document.createElement("li");
elItem.textContent = item.name;
elItem.title = item.name;
elItem.style.cssText = itemStyle;
elItem.addEventListener("click", () => {
ipcRenderer.invoke("add-download-item", {
name: data.name,
url: data.url,
});
});
elItem.addEventListener("mouseenter", () => {
elItem.style.backgroundColor = "#fff";
elItem.style.color = "#000";
});
elItem.addEventListener("mouseleave", () => {
elItem.style.backgroundColor = "#000";
elItem.style.color = "#fff";
});
// 将列表项添加到浮窗列表
floatingList.appendChild(elItem);
}
}
);
// 将按钮和列表添加到浮窗容器
floatingContainer.appendChild(floatingList);
floatingContainer.appendChild(floatingButton);
// 鼠标进入浮窗按钮时的处理函数
floatingContainer.addEventListener("mouseenter", () => {
floatingButton.style.opacity = "1";
floatingList.style.display = "block";
});
// 鼠标离开浮窗按钮时的处理函数
floatingContainer.addEventListener("mouseleave", () => {
floatingButton.style.opacity = "0.8";
floatingList.style.display = "none";
});
document.addEventListener("DOMContentLoaded", () => {
// 将浮窗容器添加到body元素
document.body.appendChild(floatingContainer);
});
+82
View File
@@ -0,0 +1,82 @@
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";
import Window from "./window";
@injectable()
export default class BrowserWindowServiceImpl
extends Window
implements BrowserWindowService
{
url = isDev
? "http://localhost:8555/browser"
: "mediago://index.html/browser";
constructor(
@inject(TYPES.StoreService)
private readonly storeService: StoreService
) {
super({
width: 1100,
minWidth: 414,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: resolve(__dirname, "./preload.js"),
},
});
this.storeService.onDidChange("openInNewWindow", this.handleNewWindowsVal);
this.storeService.onDidAnyChange(this.storeChange);
}
storeChange = (store: any) => {
if (!this.window) return;
// 向所有窗口发送通知
this.window.webContents.send("store-change", store);
};
handleNewWindowsVal = (newValue: any) => {
if (!this.window) return;
// 向所有窗口发送通知
if (newValue === false) {
if (this.window && !this.window.isDestroyed()) {
this.window.close();
}
}
};
handleResize = () => {
if (!this.window) return;
const bounds = this.window.getBounds();
this.storeService.set("browserBounds", _.omit(bounds, ["x", "y"]));
};
showWindow = () => {
if (!this.window) {
this.window = this.create();
this.window.on("resized", this.handleResize);
}
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();
};
}
+154
View File
@@ -0,0 +1,154 @@
import { Menu, Notification, app } from "electron";
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import { resolve } from "path";
import { TYPES } from "../types";
import {
DownloadProgress,
DownloadService,
LoggerService,
MainWindowService,
StoreService,
VideoRepository,
} from "../interfaces";
import { event } from "helper/utils";
import _ from "lodash";
import Window from "./window";
@injectable()
export default class MainWindowServiceImpl
extends Window
implements MainWindowService
{
url = isDev ? "http://localhost:8555/" : "mediago://index.html/";
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
) {
super({
width: 1100,
minWidth: 1100,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: resolve(__dirname, "./preload.js"),
},
});
this.downloadService.on("download-ready-start", this.onDownloadReadyStart);
this.downloadService.on("download-progress", this.onDownloadProgress);
this.downloadService.on("download-success", this.onDownloadSuccess);
this.downloadService.on("download-failed", this.onDownloadFailed);
this.downloadService.on("download-start", this.onDownloadStart);
this.downloadService.on("download-stop", this.onDownloadStart);
this.storeService.onDidAnyChange(this.storeChange);
app.on("second-instance", this.secondInstance);
}
onDownloadReadyStart = ({ id, isLive }: { id: number; isLive: boolean }) => {
this.videoRepository.changeVideoIsLive(id, isLive);
this.send("change-video-is-live", { id, isLive });
};
init(): void {
if (this.window) {
// 如果窗口已经存在,则直接显示
this.window.show();
return;
}
Menu.setApplicationMenu(null);
this.window = this.create();
const mainBounds = this.storeService.get("mainBounds");
if (mainBounds) {
this.window.setBounds(mainBounds);
}
// 处理当前窗口改变大小
this.window.on("resized", this.handleResize);
}
handleResize = () => {
if (!this.window) return;
const bounds = this.window.getBounds();
this.storeService.set("mainBounds", _.omit(bounds, ["x", "y"]));
};
secondInstance = () => {
if (!this.window) return;
if (process.platform === "win32") {
if (this.window) {
if (this.window.isMinimized()) {
this.window.restore();
}
if (this.window.isVisible()) {
this.window.focus();
} else {
this.window.show();
}
}
}
};
storeChange = (store: any) => {
// 向所有窗口发送通知
this.send("store-change", store);
};
onDownloadProgress = (progress: DownloadProgress) => {
this.send("download-progress", progress);
};
onDownloadSuccess = async (id: number) => {
const promptTone = this.storeService.get("promptTone");
if (promptTone) {
const video = await this.videoRepository.findVideo(id);
new Notification({
title: "下载成功",
body: `${video?.name} 下载成功`,
}).show();
}
this.send("download-success", id);
};
onDownloadFailed = async (id: number, err: any) => {
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.send("download-failed", id);
};
onDownloadStart = async (id: number) => {
this.send("download-start", id);
};
onDownloadStop = async (id: number) => {
this.send("download-stop", id);
};
send(channel: string, ...args: any[]) {
if (!this.window) return;
this.window.webContents.send(channel, ...args);
}
}
+44
View File
@@ -0,0 +1,44 @@
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import { resolve } from "path";
import { PlayerWindowService, VideoRepository } from "../interfaces";
import { TYPES } from "types";
import Window from "./window";
@injectable()
export default class PlayerWindowServiceImpl
extends Window
implements PlayerWindowService
{
url = isDev ? "http://localhost:8555/player" : "mediago://index.html/player";
constructor(
@inject(TYPES.VideoRepository)
private readonly videoRepository: VideoRepository
) {
super({
width: 1100,
minWidth: 1100,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: resolve(__dirname, "./preload.js"),
},
});
}
openWindow = async (id: number) => {
if (!this.window) {
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();
};
}
+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;
};
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": "./src",
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "Node",
"strictPropertyInitialization": false,
"typeRoots": ["node_modules/@types"],
"outDir": "app/output/build/main"
},
"include": ["./src/**/*"]
}
+18
View File
@@ -0,0 +1,18 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting",
],
parserOptions: {
ecmaVersion: "latest",
},
rules: {
"prettier/prettier": "error",
},
};
+31
View File
@@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
test-results/
playwright-report/

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