Compare commits

...

214 Commits

Author SHA1 Message Date
士子☀️ 5461e74539 Merge pull request #216 from caorushizi/release-v2.2.3
feat:   v2.2.3
2024-07-06 17:42:02 +08:00
caorushizi 417484cb6d feat: v2.2.3 2024-07-06 17:38:30 +08:00
士子☀️ 9aaffd3550 Merge pull request #214 from caorushizi/dev-docs
docs: 📝  更新文档
2024-07-03 15:40:36 +08:00
caorushizi 94759d48c7 docs: 📝 更新文档 2024-07-03 15:37:16 +08:00
士子☀️ c0e343b696 Merge pull request #204 from caorushizi/fix-search
fix: 🐛  修复新版本在更换安装目录后下载失败的问题
2024-06-30 19:01:26 +08:00
caorushizi f18ac7c444 fix: 🐛 修复新版本在更换安装目录后下载失败的问题
下载中的视频无法停止 #202
2024-06-30 18:58:58 +08:00
士子☀️ f637610734 Merge pull request #196 from caorushizi/fix-search
fix: 🐛  修复搜索时跳转的链接
2024-06-24 23:43:47 +08:00
caorushizi 5e1046dd5b fix: 🐛 修复搜索时跳转的链接 2024-06-23 11:41:33 +08:00
士子☀️ f25584cb2f Merge pull request #195 from caorushizi/fix-videoname
fix: 🐛  修复获取视频名称的问题
2024-06-23 00:13:13 +08:00
caorushizi 03999585d0 fix: 🐛 修复获取视频名称的问题 2024-06-23 00:12:06 +08:00
士子☀️ 772c2b0300 Merge pull request #194 from caorushizi/dev-webview
Dev webview
2024-06-22 22:59:28 +08:00
caorushizi 3622e67a56 fix: 🐛 修复浏览器背景颜色 2024-06-22 22:58:25 +08:00
caorushizi c51630cdd6 fix: 🐛 修复切换隐私模式时 Proxy 和嗅探问题 2024-06-22 22:14:49 +08:00
士子☀️ dab1c55a93 Merge pull request #193 from caorushizi/dev-webview
feat(settings):   新增无痕浏览模式
2024-06-22 21:48:11 +08:00
caorushizi 7b966bc988 feat(settings): 新增无痕浏览模式
设置页面新增无痕浏览模式

新增无痕浏览模式 #52
2024-06-22 21:46:27 +08:00
士子☀️ e5e46bee38 Merge pull request #192 from caorushizi/dev-webview
feat(settings):   添加自动更新选项
2024-06-22 15:55:09 +08:00
caorushizi 3365d96a03 feat(settings): 添加自动更新选项
设置页面添加自动更新选项,下次启动时生效

auto upgrade #185
2024-06-22 15:53:42 +08:00
士子☀️ fb223cd493 Merge pull request #191 from caorushizi/dev-webview
feat:   添加下载代理开关 & 添加清空缓存
2024-06-22 15:36:18 +08:00
caorushizi 60d49c4773 feat: 添加下载代理开关 & 添加清空缓存 2024-06-22 15:35:08 +08:00
士子☀️ e7aa4ae147 Merge pull request #190 from caorushizi/dev-webview
build: 🛠  portable
2024-06-22 12:42:50 +08:00
caorushizi c45be41ddc build: 🛠 portable 2024-06-22 12:41:45 +08:00
士子☀️ 85b37facee Merge pull request #189 from caorushizi/dev-webview
Dev webview
2024-06-22 12:27:58 +08:00
caorushizi 90567009ef Merge branch 'dev-webview' of https://github.com/caorushizi/m3u8-downloader into dev-webview 2024-06-22 12:26:36 +08:00
caorushizi 899afc156e build: 🛠 upload artifact 2024-06-22 12:25:51 +08:00
caorushizi 9e33b7e488 build: 🛠 upload artifact 2024-06-22 12:24:24 +08:00
士子☀️ e7ba60a51f Merge pull request #188 from caorushizi/dev-webview
build: 🛠  portable
2024-06-22 12:11:55 +08:00
caorushizi 2a3ad325c2 build: 🛠 portable 2024-06-22 12:11:19 +08:00
caorushizi 7575b80d31 build: 🛠 portable 2024-06-22 12:08:33 +08:00
士子☀️ 8bb9d119b5 Merge pull request #187 from caorushizi/dev-webview
Dev webview
2024-06-22 11:54:18 +08:00
caorushizi 5da3f2b1c7 build: 🛠 优化构建 2024-06-22 11:50:40 +08:00
caorushizi da6cbcfb75 feat: 修复 webview 中加载的问题 2024-06-22 10:44:34 +08:00
caorushizi 3d7aff41ee feat: 优化内置浏览器逻辑 2024-06-22 03:42:15 +08:00
士子☀️ 2cc49e8679 Merge pull request #186 from caorushizi/dev-ap
feat:   ap
2024-06-21 23:44:42 +08:00
caorushizi 0e961c0647 feat: use td app 2024-06-21 23:39:45 +08:00
caorushizi 4d5757a6ce feat: ap 2024-06-21 00:04:21 +08:00
士子☀️ 62ae0e5075 Merge pull request #184 from caorushizi/feat/v2.2.2
feat:   v2.2.2
2024-06-18 09:08:22 +08:00
caorushizi 7513fe2882 feat: v2.2.2 2024-06-18 09:06:42 +08:00
士子☀️ c945dc8360 Merge pull request #183 from caorushizi/feat/v2.2.1
feat:   release v2.2.1
2024-06-17 23:59:20 +08:00
caorushizi de7e0da045 feat: release v2.2.1 2024-06-17 23:57:36 +08:00
士子☀️ a706429395 Merge pull request #182 from caorushizi/feat/v2.2.1
feat:   v2.2.1
2024-06-17 23:34:01 +08:00
caorushizi f26c9761c3 feat: v2.2.1 2024-06-17 23:32:58 +08:00
士子☀️ 9424dd4c0e Merge pull request #181 from caorushizi/dev/form
Dev/form
2024-06-17 23:30:55 +08:00
caorushizi 271294a8e7 feat: conversion page 2024-06-17 23:29:27 +08:00
caorushizi 6b2786292f feat: conversion page 2024-06-10 00:17:32 +08:00
caorushizi c36247b5e1 feat: update form 2024-06-09 23:09:55 +08:00
士子☀️ c182afff2c Merge pull request #177 from caorushizi/feat/v2.2.1-beta.0
feat:   release 2.2.1-beta.0
2024-06-09 20:25:36 +08:00
caorushizi 1129d70dca feat: release 2.2.1-beta.0 2024-06-09 20:24:20 +08:00
士子☀️ bde71d13f9 Merge pull request #176 from caorushizi/dev/download-items
fix: 🐛  download items
2024-06-08 18:37:57 +08:00
caorushizi 67b513d380 fix: 🐛 merge master 2024-06-08 18:36:51 +08:00
caorushizi 8d8d1ee238 fix: 🐛 download item 2024-06-08 18:32:23 +08:00
caorushizi 7c10715607 Merge branch 'master' of https://github.com/caorushizi/m3u8-downloader 2024-06-08 18:27:50 +08:00
caorushizi a939f06fcd Revert "fix: 🐛 下载嗅探到的最后一个文件"
This reverts commit 2a42a02f3d.
2024-06-08 18:24:38 +08:00
士子☀️ c50625b0c8 Merge pull request #170 from caorushizi/feat/addForm
fix: 🐛  下载嗅探到的最后一个文件
2024-06-08 18:20:48 +08:00
caorushizi bfa2d88935 fix: 🐛 download items 2024-06-08 18:02:38 +08:00
士子☀️ b04ea61e83 Merge pull request #170 from caorushizi/feat/addForm
fix: 🐛  下载嗅探到的最后一个文件
2024-05-31 12:10:06 +08:00
caorushizi 2a42a02f3d fix: 🐛 下载嗅探到的最后一个文件 2024-05-31 12:08:25 +08:00
士子☀️ 1e80278a1d Merge pull request #167 from caorushizi/dependabot/npm_and_yarn/cz-git-1.9.2
chore(deps-dev): bump cz-git from 1.9.1 to 1.9.2
2024-05-27 21:20:01 +08:00
士子☀️ f38d9156c5 Merge pull request #169 from caorushizi/feat/addForm
docs: 📝  release
2024-05-27 21:18:58 +08:00
caorushizi 0f32864079 docs: 📝 release 2024-05-27 21:16:59 +08:00
士子☀️ f8fbecebc9 Merge pull request #168 from caorushizi/feat/addForm
fix: 🐛  build scripts
2024-05-27 20:52:39 +08:00
caorushizi 40c5656080 fix: 🐛 build scripts 2024-05-27 20:47:07 +08:00
dependabot[bot] ddf36a7091 chore(deps-dev): bump cz-git from 1.9.1 to 1.9.2
Bumps [cz-git](https://github.com/Zhengqbbb/cz-git/tree/HEAD/packages/cz-git) from 1.9.1 to 1.9.2.
- [Release notes](https://github.com/Zhengqbbb/cz-git/releases)
- [Changelog](https://github.com/Zhengqbbb/cz-git/blob/main/packages/cz-git/CHANGELOG.md)
- [Commits](https://github.com/Zhengqbbb/cz-git/commits/v1.9.2/packages/cz-git)

---
updated-dependencies:
- dependency-name: cz-git
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 04:15:52 +00:00
士子☀️ 258c67554d Merge pull request #166 from caorushizi/feat/addForm
feat:   完善表单项
2024-05-27 04:40:01 +08:00
caorushizi 007ff31200 feat: 完善表单项 2024-05-27 04:37:50 +08:00
士子☀️ ff75e3d94b Merge pull request #165 from caorushizi/feat/addForm
feat:   修改创建修改表单
2024-05-27 01:52:01 +08:00
caorushizi aaa18e0048 feat: 修改创建修改表单 2024-05-27 01:51:07 +08:00
士子☀️ 5c6f89a067 Merge pull request #164 from caorushizi/feat/dependencies
refactor: 📦️  update eslint
2024-05-26 20:00:37 +08:00
caorushizi c4efc4c293 refactor: 📦️ update eslint 2024-05-26 19:40:51 +08:00
士子☀️ 2d955257e6 Merge pull request #163 from caorushizi/feat/dependencies
fix: 🐛  update dependencies
2024-05-26 18:57:29 +08:00
caorushizi 3cf84a2992 fix: 🐛 update dependencies 2024-05-26 18:56:02 +08:00
士子☀️ 94a795fe69 Merge pull request #162 from caorushizi/feat/dependencies
feat:   update dependencies
2024-05-26 18:10:18 +08:00
caorushizi cc10ae8b2e feat: update dependencies 2024-05-26 17:58:40 +08:00
caorushizi 63ed7cdaca fix: 🐛 直播视频不展示下载进度 2024-05-26 17:21:58 +08:00
caorushizi 064919e0a2 fix: 🐛 build variable 2024-05-26 17:14:27 +08:00
士子☀️ 31b2652c9b Merge pull request #161 from caorushizi/fix/scripts
fix: 🐛  build script
2024-05-26 16:38:58 +08:00
caorushizi d080d709d1 fix: 🐛 build script 2024-05-26 16:37:59 +08:00
士子☀️ 0c301c79f2 Merge pull request #160 from caorushizi/fix/scripts
fix: 🐛  build
2024-05-26 15:59:19 +08:00
caorushizi a1dbb6234f fix: 🐛 build 2024-05-26 15:56:40 +08:00
士子☀️ 30248194af Merge pull request #159 from caorushizi/fix/scripts
fix: 🐛  pnpm actions
2024-05-26 15:32:03 +08:00
caorushizi 8f54ebeb3a fix: 🐛 pnpm actions 2024-05-26 15:30:34 +08:00
士子☀️ d8ef0b9eee Merge pull request #158 from caorushizi/fix/scripts
fix: 🐛  npm rebuild
2024-05-26 15:10:52 +08:00
caorushizi 6a3bfd071a fix: 🐛 npm rebuild 2024-05-26 15:05:58 +08:00
士子☀️ 849df04c05 Merge pull request #157 from caorushizi/fix/scripts
fix: 🐛  打包分片
2024-05-26 14:29:25 +08:00
caorushizi a40ae5ef82 fix: 🐛 打包分片 2024-05-26 14:28:18 +08:00
士子☀️ 00c535665f Merge pull request #156 from caorushizi/fix/scripts
fix: 🐛  scripts name
2024-05-26 02:29:51 +08:00
caorushizi 8b4a4d8ff8 fix: 🐛 scripts name 2024-05-26 02:29:05 +08:00
士子☀️ f9fecd541c Merge pull request #155 from caorushizi/fix/adblocker
Fix/adblocker
2024-05-26 02:07:25 +08:00
caorushizi 93b1028498 fix: 🐛 some bug
'
2024-05-26 02:05:53 +08:00
caorushizi a0a3dba188 refactor: 📦️ 代码分割 2024-05-25 23:37:23 +08:00
caorushizi 92ded00495 feat: 构建流程优化 2024-05-25 22:00:31 +08:00
caorushizi bcbe258f60 fix: 🐛 修复打包完成后没有权限执行的问题 2024-05-25 15:23:26 +08:00
caorushizi 660f9c7c9b feat: support linux 2024-05-25 03:02:49 +08:00
caorushizi 8c5ef29894 feat: support linux 2024-05-25 02:59:48 +08:00
caorushizi 612fc4c533 fix: 🐛 修复开启广告过滤时无法沉浸式嗅探 2024-05-24 21:27:36 +08:00
士子☀️ 635377d227 Merge pull request #153 from caorushizi/feat/webcontentsView
Feat/webcontents view
2024-05-22 21:11:38 +08:00
caorushizi dc86ab8f4c feat: release 2.2.0 2024-05-22 21:09:25 +08:00
caorushizi 36b63ad916 fix: 🐛 bilibili 2024-05-22 20:56:18 +08:00
士子☀️ e1b378811d Merge pull request #152 from caorushizi/feat/webcontentsView
fix: 🐛  .env
2024-05-22 19:30:00 +08:00
caorushizi 37fb9bba75 fix: 🐛 .env 2024-05-22 19:28:19 +08:00
士子☀️ 848253c678 Merge pull request #151 from caorushizi/feat/webcontentsView
Feat/webcontents view
2024-05-22 18:19:33 +08:00
caorushizi b64e779ed1 feat: release 2.2.0-beta.4 2024-05-22 18:18:07 +08:00
caorushizi 046a62cde3 fix: 🐛 load url 2024-05-22 18:16:38 +08:00
caorushizi 1a9e6efb90 feat: use webcontents view and capture page 2024-05-22 15:15:39 +08:00
士子☀️ fd422c15ba Merge pull request #150 from caorushizi/fix/addForm
fix: 🐛  fix run release scripts
2024-05-22 04:04:18 +08:00
caorushizi c338f76f8d fix: 🐛 fix run release scripts 2024-05-22 04:03:32 +08:00
士子☀️ 4285b3f4f7 Merge pull request #149 from caorushizi/fix/addForm
feat:   支持直播视频下载
2024-05-22 03:51:41 +08:00
caorushizi d0109869a6 feat: 支持直播视频下载 2024-05-22 03:50:38 +08:00
士子☀️ d29ad44c54 Merge pull request #148 from caorushizi/fix/addForm
fix: 🐛  float button position
2024-05-22 01:02:50 +08:00
caorushizi be61b07fea fix: 🐛 float button position 2024-05-22 01:01:38 +08:00
士子☀️ e5e171fb6f Merge pull request #147 from caorushizi/fix/addForm
fix: 🐛  release 2.2.0-beta.3
2024-05-22 00:33:59 +08:00
caorushizi 805d97c3ca fix: 🐛 release 2.2.0-beta.3 2024-05-22 00:32:35 +08:00
士子☀️ 040222f259 Merge pull request #146 from caorushizi/fix/addForm
fix: 🐛  fix browser window event
2024-05-21 22:52:39 +08:00
caorushizi 09ed55eba4 fix: 🐛 fix browser window event 2024-05-21 22:51:40 +08:00
士子☀️ a6d4f8cf98 Merge pull request #145 from caorushizi/fix/addForm
fix: 🐛  release 2.2.0-beta.2
2024-05-21 02:38:48 +08:00
caorushizi d19b2394e8 fix: 🐛 release 2.2.0-beta.2 2024-05-21 02:37:40 +08:00
士子☀️ 036a99d84d Merge pull request #143 from caorushizi/fix/video
Fix/video
2024-05-14 12:22:24 +08:00
caorushizi b022335af7 feat: release 2.2.0-beta.1 2024-05-14 12:21:21 +08:00
caorushizi a66c20c9d5 feat: 修复页面中下载嗅探问题 2024-05-14 12:19:47 +08:00
caorushizi f9fc135647 fix: 🐛 修复 dom-ready 执行的顺序 2024-05-13 09:49:34 +08:00
士子☀️ fbbee9c114 Merge pull request #142 from caorushizi/fix/docs
Fix/docs
2024-05-13 09:42:35 +08:00
caorushizi a65fbcbaf8 fix: 🐛 修复插件展现运行的时机 2024-05-13 09:41:06 +08:00
士子☀️ 39b68b71d4 refactor: 📦️ modify mediago protocol, upgrade adblocker 2024-05-13 01:51:13 +08:00
士子☀️ 4d12a0a937 Merge pull request #140 from caorushizi/fix/docs
ci: 🎡  github actions
2024-05-02 19:01:16 +08:00
士子☀️ 960973302e ci: 🎡 github actions 2024-05-02 18:58:01 +08:00
士子☀️ 1a68b00bdd Merge pull request #139 from caorushizi/fix/docs
ci: 🎡  modify github actions
2024-05-02 18:32:22 +08:00
士子☀️ dba73fc473 ci: 🎡 modify github actions 2024-05-02 18:30:24 +08:00
士子☀️ 211f7314ce Merge pull request #138 from caorushizi/fix/docs
docs: 📝  remove reaction
2024-05-02 18:18:19 +08:00
士子☀️ 64d719a63a docs: 📝 remove reaction 2024-05-02 18:17:19 +08:00
士子☀️ 0304e6b569 Merge pull request #135 from caorushizi/feat/mediago
Feat/mediago
2024-04-05 23:46:23 +08:00
士子☀️ 190447a0bd feat: release v2.2.0-beta.0 2024-04-05 23:44:13 +08:00
士子☀️ 497d1265f0 chore: 🔨 add lint-staged 2024-04-05 23:42:35 +08:00
士子☀️ 6d459e0692 feat: rename mediago 2024-04-05 23:09:30 +08:00
士子☀️ 45cde40cea chore: 🔨 fix pre-commit 2024-04-05 22:36:13 +08:00
士子☀️ e9deb4c7f7 chore: 🔨 use tsx instead of ts-node 2024-04-05 22:02:59 +08:00
caorushizi 1abfc763dc docs: 📝 update waline client 2024-03-26 14:31:00 +08:00
caorushizi b84ddcc88d docs: 📝 update vitepress 2024-03-26 13:02:22 +08:00
士子☀️ 33f7951901 refactor: 📦️ remove submodule 2024-03-20 23:23:09 +08:00
士子☀️ d3354f9109 Merge pull request #125 from caorushizi/feat/player
refactor: 📦️  remove tsno
2024-03-20 19:32:06 +08:00
caorushizi d252816cc3 refactor: 📦️ remove tsno 2024-03-20 19:29:27 +08:00
士子☀️ 6e31904e95 Merge pull request #124 from caorushizi/feat/player
refactor: 📦️  remove video player
2024-03-20 16:59:12 +08:00
caorushizi 2825d1f4c7 refactor: 📦️ remove video player 2024-03-20 16:50:58 +08:00
士子☀️ fcb10c6137 Merge pull request #118 from caorushizi/feat/player
fix: 🐛  catch video
2024-03-17 18:04:27 +08:00
士子☀️ f564739578 fix: 🐛 catch video 2024-03-17 18:03:24 +08:00
士子☀️ 40faf4b383 Merge pull request #117 from caorushizi/feat/player
fix: 🐛  fix mobile video size
2024-03-17 17:46:01 +08:00
士子☀️ affff353bd fix: 🐛 fix mobile video size 2024-03-17 17:45:04 +08:00
士子☀️ f410fabff7 Merge pull request #116 from caorushizi/feat/player
fix: 🐛  fix video container
2024-03-17 17:22:13 +08:00
士子☀️ 7d4b201b6e fix: 🐛 fix video container 2024-03-17 17:21:10 +08:00
士子☀️ a9ac0aa16b Merge pull request #115 from caorushizi/2.1.0-beta.1
feat:   v2.1.0-beta.1
2024-03-17 16:09:16 +08:00
士子☀️ 5419970080 feat: v2.1.0-beta.1 2024-03-17 16:08:34 +08:00
士子☀️ 506a573180 Merge pull request #114 from caorushizi/feat/dialog
fix: 🐛 按照获取时间正序排列
2024-03-17 02:03:43 +08:00
士子☀️ 17b2450b43 feat: use download dialog 2024-03-17 02:01:32 +08:00
士子☀️ 2942e65a54 fix: 🐛 按照获取时间正序排列 2024-03-17 00:17:47 +08:00
士子☀️ 913a7bd9e5 Merge pull request #113 from caorushizi/feat/dialog
feat:   show download dialog
2024-03-17 00:12:31 +08:00
士子☀️ c5f2edd9de feat: show download dialog 2024-03-17 00:09:34 +08:00
士子☀️ 24d511e6b6 Merge pull request #112 from caorushizi/feat/plugins
feat:  bilibili plguins
2024-03-10 19:23:15 +08:00
士子☀️ 47f3336f9a feat: 2.1.0-beta.0 2024-03-10 19:22:30 +08:00
士子☀️ e0e6e5dbed feat: bilibili plguins 2024-03-10 19:19:27 +08:00
士子☀️ aa7f3e7bf7 Merge pull request #111 from caorushizi/feat/terminal
feat:  use terminal
2024-03-10 16:43:04 +08:00
caorushizi 065a6c45f7 feat: terminal 2024-03-10 16:40:25 +08:00
士子☀️ b13174dda1 feat: use terminal 2024-03-10 16:19:16 +08:00
士子☀️ 9e6a931a41 Merge pull request #109 from caorushizi/feat/terminal
feat:  terminal
2024-03-09 08:57:23 +08:00
士子☀️ cdabdfd2f8 feat: layout style 2024-03-09 01:06:49 +08:00
士子☀️ b4cdbd7547 feat: show terminal 2024-03-09 00:05:15 +08:00
caorushizi 75fe603919 feat: terminal 2024-03-08 21:35:05 +08:00
士子☀️ 16ef9ae642 Merge pull request #104 from caorushizi/feat/actions
feat:   add vscode debug config
2024-02-27 23:13:35 +08:00
士子☀️ 7db3162748 feat: add vscode debug config 2024-02-27 23:11:37 +08:00
士子☀️ 3f0e6d9856 Merge pull request #102 from caorushizi/feat/actions
feat:   update actions version
2024-02-27 01:04:51 +08:00
士子☀️ d87fc4a400 feat: update actions version 2024-02-27 01:03:26 +08:00
士子☀️ ec6241b79e Merge pull request #101 from caorushizi/fix/node-pty
fix: 🐛  cannot find module 'node-pty'
2024-02-27 00:50:42 +08:00
士子☀️ 9b37398e9f fix: 🐛 cannot find module 'node-pty' 2024-02-27 00:49:05 +08:00
士子☀️ 3fca8694ee fix: 🐛 filename 2024-02-25 23:39:24 +08:00
士子☀️ 78efffa29b Merge pull request #95 from caorushizi/dev/terminal
Dev/terminal
2024-02-25 22:40:02 +08:00
士子☀️ 737ecbdadd fix: 🐛 build player 2024-02-25 22:37:34 +08:00
士子☀️ fa6ce22ea7 Merge pull request #94 from caorushizi/dev/terminal
Dev/terminal
2024-02-25 22:22:36 +08:00
士子☀️ 315c55c844 feat: terminal 2024-02-25 18:28:37 +08:00
士子☀️ d9ba60e1a5 feat: terminal 2024-02-25 17:31:58 +08:00
士子☀️ 7503322d01 feat: terminal 2024-02-25 13:36:14 +08:00
士子☀️ 2e5f2dc687 Merge pull request #91 from caorushizi/dev/plugins
style: 💄 eslint
2024-02-24 23:28:27 +08:00
士子☀️ e4afd27908 fix: 🐛 add tsconfig 2024-02-24 23:19:09 +08:00
士子☀️ 6fe5d5523e feat: remove player page. use node-pty 2024-02-24 22:50:03 +08:00
士子☀️ 7335946ec2 refactor: 📦️ refactoring the project directory structure 2024-02-23 19:32:06 +08:00
士子☀️ eb4576de9b feat: 使用 node 伪终端 2024-02-21 22:17:02 +08:00
士子☀️ 1e41a9ee99 feat: node 伪终端获取详细日志 2024-02-21 21:44:12 +08:00
士子☀️ dd5226ab47 refactor: 📦️ downloader 2024-02-12 09:56:07 +08:00
士子☀️ 989e5535eb feat: new player 2024-02-12 01:55:11 +08:00
士子☀️ 65efeed692 style: 💄 eslint 2024-02-11 23:18:45 +08:00
士子☀️ fcddb2d382 Merge pull request #88 from caorushizi/dev/plugins
fix: 🐛  types
2024-02-11 18:13:57 +08:00
士子☀️ bbc892ec81 fix: 🐛 types 2024-02-11 18:08:51 +08:00
士子☀️ ca1c2821a7 Merge pull request #87 from caorushizi/dev/plugins
fix: 🐛  types
2024-02-11 18:02:50 +08:00
士子☀️ c8d4f763fd fix: 🐛 types 2024-02-11 18:00:08 +08:00
士子☀️ a3d587af44 Merge pull request #85 from caorushizi/dev/plugins
feat:  new plugin
2024-02-11 17:00:28 +08:00
士子☀️ ce2146cc89 feat: add plugin 2024-02-11 16:59:02 +08:00
士子☀️ 8455f17517 style: 💄 somthing 2024-02-11 10:45:32 +08:00
士子☀️ c075bdd4f9 feat: new plugin 2024-02-09 22:18:35 +08:00
士子☀️ 65d88d0896 Merge pull request #82 from caorushizi/dev/i18n
feat: display language
2024-02-09 17:01:55 +08:00
士子☀️ 1c62640301 style: 💄 add lint-staged 2024-02-09 17:00:48 +08:00
士子☀️ dd6684c550 feat: build plugin
build plugin with lit.
2024-02-09 15:54:22 +08:00
士子☀️ 2e1a753583 test 2024-02-09 15:41:34 +08:00
士子☀️ cbf6e4ecfb test 2024-02-09 15:26:20 +08:00
士子☀️ 5af471fe1a plugin 2024-02-09 13:23:52 +08:00
士子☀️ 0deb205faa plugin 2024-02-09 00:11:40 +08:00
士子☀️ 73eaf0ecaa plugin 2024-02-09 00:10:32 +08:00
士子☀️ 4791defbdd sniffing helper 2024-02-08 18:23:49 +08:00
士子☀️ 926249184b i18n 2024-02-08 15:12:08 +08:00
士子☀️ 659b2755ef feat: display language 2024-02-04 20:15:43 +08:00
士子☀️ e9d36edaa4 v2.0.3-beta.3 2024-01-22 23:01:56 +08:00
士子☀️ fdcdbf27eb dev tools 2024-01-22 22:25:29 +08:00
士子☀️ 2f5cc2eb80 Merge branch 'master' of https://github.com/caorushizi/m3u8-downloader 2024-01-21 19:11:42 +08:00
士子☀️ c087fa4956 webview 2024-01-21 19:10:11 +08:00
士子☀️ ed33ec6532 actions 2024-01-21 17:54:37 +08:00
士子☀️ 2d96c2730c .npmrc 2024-01-21 17:41:59 +08:00
士子☀️ 5bcec10be3 docs 2024-01-21 17:18:02 +08:00
士子☀️ 207bd15b14 .npmrc 2024-01-21 17:07:41 +08:00
士子☀️ 00f0bfc84b os version 2024-01-21 17:06:59 +08:00
187 changed files with 13688 additions and 11046 deletions
+44
View File
@@ -0,0 +1,44 @@
acodec
adblocker
ahooks
antd
aplus
behaviour
bili
bilibili
bodys
cancle
cliqz
commitlint
Conpty
consola
conventionalcommits
datetime
DDTHH
execa
idtype
immer
inversify
KHTML
languagedetector
localforage
mediago
metas
nocheck
optimizelegibility
pinia
redownload
Redownload
reduxjs
Sider
stylelint
svgz
TDAPP
tiptap
typeorm
vitepress
vuedraggable
vuejs
waline
watting
Watting
+2 -4
View File
@@ -1,6 +1,4 @@
APP_NAME=media-downloader
APP_NAME=mediago
APP_ID=mediago.ziying.site
APP_COPYRIGHT=caorushizi
APP_VERSION=2.0.3-beta.2
APP_SERVER_PORT=8433
APP_VERSION=2.2.3
+1 -2
View File
@@ -1,6 +1,5 @@
APP_TD_APPID=
APP_CLARITY_APPID=
APP_DOWNLOAD_DEBUG=true
GH_TOKEN=
LOAD_DEVTOOLS=
DEBUG_PLUGINS=true
+1 -2
View File
@@ -1,5 +1,4 @@
APP_TD_APPID=
APP_CLARITY_APPID=
APP_DOWNLOAD_DEBUG=
GH_TOKEN=
DEBUG_PLUGINS=false
+14 -14
View File
@@ -23,41 +23,41 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
os: [windows-latest, macos-latest, ubuntu-latest]
# create steps
steps:
# step1: check out repository
- name: Check out git repository
uses: actions/checkout@v4
with:
submodules: true
# step2: install node env
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "16.x"
node-version: 20
# step3: install go env
- name: Install Go
uses: actions/setup-go@v4
# step4: install python env
- name: Install Python
uses: actions/setup-python@v5
with:
go-version: "1.21.5"
python-version: "3.10"
# step4: install pnpm env
- uses: pnpm/action-setup@v2
# step5: install pnpm env
- uses: pnpm/action-setup@v3
with:
version: latest
run_install: true
standalone: true
- name: Build & release app
run: pnpm run release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
APP_CLARITY_APPID: ${{ secrets.APP_CLARITY_APPID}}
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: artifact
path: packages/main/release/media-downloader-setup-*
name: mediago-${{ matrix.os }}
path: packages/main/release/mediago-setup-*
+13 -10
View File
@@ -16,7 +16,6 @@ concurrency:
cancel-in-progress: false
env:
BUILD_PATH: "."
BUCKET: downloader-docs
ENDPOINT: oss-cn-beijing.aliyuncs.com
ACCESS_KEY: LTAI5tLckcUrBtj7bCiUYwWz
@@ -28,22 +27,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "16"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
working-directory: ${{ env.BUILD_PATH }}
node-version: 20
- uses: pnpm/action-setup@v3
with:
version: latest
run_install: true
- name: Build with Vitepress
run: pnpm run docs:build
working-directory: ${{ env.BUILD_PATH }}
- name: Install Alibaba Cloud OSSUTIL
run: wget http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64 && chmod +x ossutil64
- name: Configure Alibaba Cloud OSSUTIL
run: ./ossutil64 config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
- name: Upload the web folder to the chosen OSS bucket
run: ./ossutil64 --config-file .ossutilconfig cp ${{ github.workspace }}/docs/.vitepress/dist oss://${BUCKET} -r -f
+1 -1
View File
@@ -1,6 +1,6 @@
node_modules
.vscode
*.local
.idea
.parcel-cache
.vitepress
*.tsno.mjs
-3
View File
@@ -1,3 +0,0 @@
[submodule "internal"]
path = internal
url = https://github.com/caorushizi/mediago
+1 -1
View File
@@ -10,4 +10,4 @@
}
]
}
}
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}
npm run commitlint ${1}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx tsx ./scripts/pre-commit.ts
+3
View File
@@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage
+23
View File
@@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "dev"],
"skipFiles": ["<node_internals>/**"]
},
{
"name": "Attach to Electron",
"type": "node",
"request": "attach",
"address": "localhost",
"restart": true,
"timeout": 10000,
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}"
}
]
}
+25 -25
View File
@@ -1,21 +1,21 @@
<img src="https://socialify.git.ci/caorushizi/m3u8-downloader/image?font=Inter&forks=1&issues=1&language=1&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Auto" alt="MediaDownloader"/>
<img src="https://socialify.git.ci/caorushizi/mediago/image?font=Inter&forks=1&issues=1&language=1&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Auto" alt="MediaDownloader"/>
<div align="center">
<h1>Media downloader</h1>
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/m3u8-downloader/discussions">Discussions</a>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<br>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/m3u8-downloader/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/m3u8-downloader/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/m3u8-downloader">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/m3u8-downloader">
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<hr />
</div>
@@ -25,18 +25,18 @@
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
- **✅&nbsp; 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
- **📱&nbsp; 移动播放**: 可以轻松无缝的在PC和移动设备之前切换,下载完成后即可使用手机观看视频。
- **📱&nbsp; 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
- **⚡️&nbsp; 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
## Quickstart
运行代码需要 node pnpm 和 gonode、go 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
运行代码需要 node pnpmnode 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
## 运行代码
```shell
# 代码下载
git clone --recursive https://github.com/caorushizi/m3u8-downloader.git
git clone https://github.com/caorushizi/mediago.git
# 安装依赖
pnpm i
@@ -50,15 +50,23 @@ pnpm release
## Releases
**v2.0.12023.7.9 发布**
**v2.2.0 (2024.5.22 发布)**
- [windows media-downloader v2.0.2](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.2/media-downloader-setup-2.0.2.exe)
- [macos media-downloader v2.0.2](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.2/media-downloader-setup-2.0.2.dmg)
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
**v2.0.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)
- 支持下载直播流
- 支持哔哩哔哩视频下载
- 优化沉浸式嗅探流程
- 支持下载控制台输出
- 修复了一些 bug
**v2.0.22023.7.9 发布)**
- [windows mediago v2.0.2](https://github.com/caorushizi/mediago/releases/download/v2.0.2/media-downloader-setup-2.0.2.exe)
- [macos mediago v2.0.2](https://github.com/caorushizi/mediago/releases/download/v2.0.2/media-downloader-setup-2.0.2.dmg)
**更新日志**
@@ -71,12 +79,6 @@ pnpm release
- 支持修改同时下载数量
- 修复了一些 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://static.ziying.site/v2.0.3-beta-home.png)
@@ -93,6 +95,4 @@ pnpm release
## 鸣谢
N_m3u8DL-CLI 来自于 <https://github.com/nilaoda/N_m3u8DL-CLI>
mediago 来自于 <https://github.com/caorushizi/mediago>
N_m3u8DL-RE 来自于 <https://github.com/nilaoda/N_m3u8DL-RE>
+100
View File
@@ -0,0 +1,100 @@
export default {
extends: ["@commitlint/config-conventional"], // extends can be nested
parserPreset: "conventional-changelog-conventionalcommits",
prompt: {
settings: {},
messages: {
skip: ":skip",
max: "upper %d chars",
min: "%d chars at least",
emptyWarning: "can not be empty",
upperLimitWarning: "over limit",
lowerLimitWarning: "below limit",
},
types: [
{ value: "feat", name: "feat: ✨ A new feature", emoji: "✨ " },
{ value: "fix", name: "fix: 🐛 A bug fix", emoji: "🐛 " },
{
value: "docs",
name: "docs: 📝 Documentation only changes",
emoji: "📝 ",
},
{
value: "style",
name: "style: 💄 Changes that do not affect the meaning of the code",
emoji: "💄 ",
},
{
value: "refactor",
name: "refactor: 📦️ A code change that neither fixes a bug nor adds a feature",
emoji: "📦️ ",
},
{
value: "perf",
name: "perf: 🚀 A code change that improves performance",
emoji: "🚀 ",
},
{
value: "test",
name: "test: 🚨 Adding missing tests or correcting existing tests",
emoji: "🚨 ",
},
{
value: "build",
name: "build: 🛠 Changes that affect the build system or external dependencies",
emoji: "🛠 ",
},
{
value: "ci",
name: "ci: 🎡 Changes to our CI configuration files and scripts",
emoji: "🎡 ",
},
{
value: "chore",
name: "chore: 🔨 Other changes that don't modify src or test files",
emoji: "🔨 ",
},
{
value: "revert",
name: "revert: ⏪️ Reverts a previous commit",
emoji: ":rewind:",
},
],
useEmoji: true,
confirmColorize: true,
emojiAlign: "center",
questions: {
scope: {
description:
"What is the scope of this change (e.g. component or file name)",
},
subject: {
description:
"Write a short, imperative tense description of the change",
},
body: {
description: "Provide a longer description of the change",
},
isBreaking: {
description: "Are there any breaking changes?",
},
breakingBody: {
description:
"A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself",
},
breaking: {
description: "Describe the breaking changes",
},
isIssueAffected: {
description: "Does this change affect any open issues?",
},
issuesBody: {
description:
"If issues are closed, the commit requires a body. Please enter a longer description of the commit itself",
},
issues: {
description: 'Add issue references (e.g. "fix #123", "re #123".)',
},
},
},
};
+23
View File
@@ -0,0 +1,23 @@
{
"import": [
"@cspell/dict-lorem-ipsum/cspell-ext.json"
],
"caseSensitive": false,
"dictionaries": [
"custom-words"
],
"dictionaryDefinitions": [
{
"name": "custom-words",
"path": "./.cspell/custom-words.txt",
"addWords": true
}
],
"ignorePaths": [
"**/node_modules/**",
"**/dist/**",
"**/lib/**",
"**/docs/**",
"**/stats.html"
]
}
+3 -4
View File
@@ -1,5 +1,4 @@
import { HeadConfig } from "vitepress";
import { defineConfig } from "vitepress";
import { HeadConfig, defineConfig } from "vitepress";
const isDev = process.env.NODE_ENV === "development";
@@ -22,7 +21,7 @@ if (!isDev) {
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "media-downloader",
title: "MediaGo",
description: "简单易用,快速下载",
lastUpdated: true,
head,
@@ -57,7 +56,7 @@ export default defineConfig({
footer: {
message: '<a href="https://beian.miit.gov.cn">豫ICP备20012967号-2</a>',
copyright: "Copyright © 2019-present caorushizi",
copyright: "Copyright © 2021-present caorushizi",
},
},
});
@@ -29,7 +29,6 @@ function initWaline() {
serverURL: "https://comments.ziying.site",
dark: isDark.value,
lang: "zh-CN",
reaction: true,
pageview: true,
});
}
+21 -4
View File
@@ -8,15 +8,32 @@ outline: deep
这篇文章是软件的简单说明,可以快速上手使用本本软件。
::: tip
我建了一个 QQ 群,方便大家一起沟通交流: 574209001
方便大家一起沟通交流, 可以加入 QQ 反馈群:
MediaGo 反馈群 1 574209001
MediaGo 反馈群 2 921619626
:::
## 下载安装
**v2.2.0 (2024.5.22 发布)**
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
**更新日志**
- 支持下载直播流
- 支持哔哩哔哩视频下载
- 优化沉浸式嗅探流程
- 支持下载控制台输出
- 修复了一些 bug
**v2.0.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)
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
- [macos mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
**更新日志**
@@ -31,7 +48,7 @@ outline: deep
**v1.1.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)
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
**更新日志**
+2 -2
View File
@@ -13,8 +13,8 @@ outline: deep
**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 无~~
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
- ~~macos mediago v1.1.5 无~~
**更新日志**
+14
View File
@@ -0,0 +1,14 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-explicit-any": "warn",
},
},
];
Submodule internal deleted from ca43a37335
+42 -17
View File
@@ -1,36 +1,61 @@
{
"name": "electron-template",
"name": "mediago",
"version": "0.1.0",
"description": "electron-template",
"main": "index.js",
"description": "mediago",
"author": "caorushizi",
"homepage": "https://downloader.caorushizi.cn/",
"private": true,
"type": "module",
"scripts": {
"dev": "zx scripts/dev.mjs && pnpm --parallel -F \"./packages/*\" run dev",
"build": "zx scripts/build.mjs",
"build:mobile": "pnpm -F mobile run build",
"dev": "tsx scripts/dev.ts && pnpm --parallel -F \"./packages/*\" run dev",
"build": "tsx scripts/build.ts",
"build:plugin": "pnpm -F plugin run build",
"build:main": "pnpm -F main run build",
"build:renderer": "pnpm -F renderer run build",
"build:server": "zx scripts/build-server.mjs",
"pack": "pnpm -F media-downloader run pack",
"beta": "pnpm run build && pnpm -F mediago run pack",
"release": "pnpm run build && pnpm -F main run release",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"types": "pnpm -F main run types"
"types": "pnpm -F main run types",
"types:watch": "pnpm -F main run types:watch",
"prepare": "husky",
"commit": "git-cz",
"commitlint": "commitlint --edit",
"spellcheck": "cspell \"packages/**/src/**/*.{ts,tsx}\"",
"lint-staged": "pnpm --recursive -F \"./packages/*\" run lint-staged"
},
"keywords": [],
"author": "",
"license": "MIT",
"pnpm": {
"patchedDependencies": {
"@cliqz/adblocker@1.26.15": "patches/@cliqz__adblocker@1.26.15.patch"
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"devDependencies": {
"@waline/client": "3.0.0-alpha.10",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/core": "^19.3.0",
"@commitlint/cz-commitlint": "^19.2.0",
"@commitlint/format": "^19.3.0",
"@eslint/js": "^9.3.0",
"@waline/client": "^3.2.0",
"commitizen": "^4.3.0",
"cross-env": "^7.0.3",
"vitepress": "1.0.0-rc.39",
"vue": "^3.4.15",
"zx": "^7.2.3"
"cspell": "^8.8.3",
"cz-git": "^1.9.2",
"eslint": "^9.3.0",
"globals": "^15.3.0",
"husky": "^9.0.11",
"tsx": "^4.11.0",
"typescript-eslint": "^7.10.0",
"vitepress": "1.2.2",
"vue": "^3.4.27",
"zx": "^8.1.1"
},
"pnpm": {
"patchedDependencies": {
"@cliqz/adblocker@1.27.3": "patches/@cliqz__adblocker@1.27.3.patch"
}
}
}
View File
-23
View File
@@ -1,23 +0,0 @@
module.exports = {
env: {
node: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
],
overrides: [],
ignorePatterns: ["app"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "prettier"],
rules: {
"prettier/prettier": "warn",
"@typescript-eslint/no-explicit-any": "warn",
},
};
+1
View File
@@ -0,0 +1 @@
types/
+8 -6
View File
@@ -1,15 +1,17 @@
{
"name": "media-downloader",
"version": "2.0.3-beta.2",
"name": "mediago",
"version": "2.2.3",
"description": "在线视频下载器",
"main": "main/index.js",
"author": "",
"author": "caorushizi",
"homepage": "https://downloader.caorushizi.cn/",
"license": "MIT",
"dependencies": {
"@cliqz/adblocker-electron": "^1.26.15",
"@cliqz/adblocker-electron-preload": "^1.26.15"
"@cliqz/adblocker-electron": "^1.27.3",
"@cliqz/adblocker-electron-preload": "^1.27.3",
"node-pty": "^1.0.0"
},
"devDependencies": {
"electron": "^28.1.4"
"electron": "30.0.8"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 22 KiB

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

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