Compare commits

...

443 Commits

Author SHA1 Message Date
jixunmoe 61fe20e6a8 Merge pull request 'remerge #30' (#67) from kylebing/web:master into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #67
Reviewed-by: jixunmoe <jixunmoe@noreply.unlock-music.dev>
2023-09-25 20:36:30 +00:00
KyleBing eaca480700 Merge remote-tracking branch 'upstream/master'
continuous-integration/drone/pr Build is passing Details
# Conflicts:
#	src/App.vue
#	src/view/Home.vue
2023-09-25 12:15:18 +08:00
KyleBing 9afc20246a update to vh 2023-09-25 12:13:36 +08:00
Jixun Wu 75e25306be 1.10.6 2023-08-28 18:28:19 +01:00
jixunmoe 6603210ad9 Merge pull request 'bugfix for #36 #50' (#59) from nullptr-0/web:pr into master
Reviewed-on: #59
Reviewed-by: jixunmoe <jixunmoe@noreply.unlock-music.dev>
2023-08-28 17:10:02 +00:00
nullptr-0 7ade485d2e bugfix for #36 #50 2023-08-29 00:29:05 +08:00
jixunmoe 970d2485a6 Merge pull request 'docs/issue-template-2' (#55) from docs/issue-template-2 into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #55
2023-07-15 20:02:59 +00:00
Jixun Wu 84847573e1 chore: add more template
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-07-15 20:59:49 +01:00
Jixun Wu 153319b2e5 chore: issue template update 2023-07-15 20:24:18 +01:00
jixunmoe afd9263ff1 Merge pull request '将旧的 GitHub 项目链接更换到新的域名' (#54) from fix/docs into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #54
2023-07-15 19:12:02 +00:00
Jixun Wu fc18d7de05 docs: update issue templates
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-07-15 20:10:59 +01:00
Jixun Wu a269cd4f8e chore: replace url to old github project (fix #53) 2023-07-15 20:10:52 +01:00
jixunmoe 9205dbf701 Merge pull request '修正云音乐 PC 3.0 写出的 metadata 格式与之前不一致导致解码错误的问题' (#51) from fix/ncm-3-metadata into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #51
2023-07-07 16:50:30 +00:00
Jixun Wu 0bb5c4d864 strict type check for ncm metadata #48
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-07-07 12:45:31 +01:00
jixunmoe fde495ce42 Merge pull request '将 package-lock 中的 npmmirror 地址更换为 npmjs 的官方地址' (#47) from dev/normalize-package-lock-url into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #47
2023-06-06 21:02:04 +00:00
Jixun Wu cd30e91d22 chore: use npmjs url instead of npmmirror
continuous-integration/drone/push Build encountered an error Details
continuous-integration/drone/pr Build encountered an error Details
2023-06-06 22:00:15 +01:00
jixunmoe 9a5632fe22 Merge pull request 'optimization' (#29) from nullptr-0/web:master into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #29
Reviewed-by: jixunmoe <jixunmoe@noreply.unlock-music.dev>
2023-06-06 20:41:54 +00:00
邴新科 de481f6abf 编辑歌曲信息界面优化
continuous-integration/drone/pr Build is passing Details
2023-02-03 14:28:37 +08:00
邴新科 b380e596af 优化显示:组件上下间隔,footer 位置置于页面底部 2023-02-03 12:08:59 +08:00
xhacker-zzz 51107f004e Merge branch 'master' into master
continuous-integration/drone/pr Build is passing Details
2023-01-20 12:37:05 +00:00
xhacker-zzz 6eebb20e05 update readme
continuous-integration/drone/pr Build is passing Details
2023-01-20 20:28:00 +08:00
xhacker-zzz 0fe13129f7 use wasm in npm packages 2023-01-20 20:27:36 +08:00
Unlock Music Dev 4fec47201e Merge pull request 'Fix STag Filtering & README improvement & Update Supported Extensions' (#28) from xhacker-zzz/web:master into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #28
Reviewed-by: jixunmoe <jixunmoe@noreply.unlock-music.dev>
Reviewed-by: Unlock Music Dev <dev@unlock-music.dev>
2023-01-19 16:57:53 +00:00
xhacker-zzz ea0dec6f0b delete js implementations 2023-01-19 20:33:23 +08:00
xhacker-zzz 6d8389ce19 update readme
continuous-integration/drone/pr Build is passing Details
2023-01-19 19:41:52 +08:00
xhacker-zzz 8e361691bc update copyright year 2023-01-19 19:41:22 +08:00
xhacker-zzz c48532121d small fixes 2023-01-19 19:41:01 +08:00
Unlock Music Dev 0458ee469b Merge pull request 'New Decryptor: Add js decoder for Ximalaya X2M and X3M' (#23) from Yuki1536/um-web1:master into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #23
Reviewed-by: jixunmoe <jixunmoe@noreply.unlock-music.dev>
Reviewed-by: Unlock Music Dev <dev@unlock-music.dev>
2022-12-22 16:24:42 +00:00
Yuki1536 62d83c752e
Format with prettier.
continuous-integration/drone/pr Build is passing Details
2022-12-22 22:20:34 +09:00
Yuki1536 3393bb0bff
Change type
continuous-integration/drone/pr Build is passing Details
2022-12-16 20:34:13 +09:00
Yuki1536 d21c48caff
Small fixes
continuous-integration/drone/pr Build is passing Details
2022-12-15 12:08:12 +09:00
Yuki1536 b632c8fbc9
Update README.md
continuous-integration/drone/pr Build is passing Details
2022-12-12 20:35:15 +09:00
Yuki1536 48a497af19
Update README.md / Now it's 2022!
continuous-integration/drone/pr Build is passing Details
2022-12-12 19:58:36 +09:00
Yuki1536 2ae08c53fd
Store table u16array instead of bytes
continuous-integration/drone/pr Build is passing Details
2022-12-12 19:19:06 +09:00
Yuki1536 105023def7
Remove iconv-lite calling
continuous-integration/drone/pr Build is passing Details
2022-12-11 18:22:47 +09:00
Yuki1536 1fdab13ff8
Add x2m x3m decryptor
continuous-integration/drone/pr Build is failing Details
2022-12-11 17:54:07 +09:00
Yuki1536 1db42a10ff
Test: x2m x3m decrypt 2022-12-11 17:24:14 +09:00
Unlock Music Dev 47b7273dbc Merge pull request 'fix(kwm): allow new magic header on unlock-music web edition' (#22) from Yuki1536/um-web:master into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #22
Reviewed-by: um-dev <dev@unlock-music.dev>
2022-12-05 03:12:36 +00:00
Yuki1536 7e62def0c2
fix(kwm): allow new magic header
continuous-integration/drone/pr Build is passing Details
2022-12-05 10:26:41 +09:00
jixunmoe fe3bad5875 Merge pull request 'feat: added support for mmp4 extension' (#20) from jixunmoe/um-web:feat/qmc-new-ext-mmp4 into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #20
2022-11-27 14:34:07 +00:00
Jixun eb3091052e feat: added support for mmp4 extension
continuous-integration/drone/pr Build is passing Details
2022-11-27 14:12:17 +00:00
Unlock Music Dev a5bd0a312c Merge pull request 'chore: remove stats' (#18) from remove/stats into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #18
2022-11-27 02:03:21 +00:00
Unlock Music Dev 38f9b1b19a
chore: remove stats
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-11-27 09:23:34 +08:00
jixunmoe 025b19d5cd Merge pull request 'docs: update link to artifact + ci badge' (#17) from readme/ci-link-and-badge into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #17
2022-11-27 00:56:21 +00:00
jixunmoe 551d3abd20 docs: update link to artifact + ci badge
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build was killed Details
2022-11-27 00:56:09 +00:00
jixunmoe 7671127311 Merge pull request 'ci: upload to gitea packages instead of s3' (#16) from jixunmoe/um-web:ci/use-gitea-packages into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #16
2022-11-27 00:48:19 +00:00
Jixun 70c78772ec ci: use same node image to skip dependency install.
continuous-integration/drone/pr Build is passing Details
2022-11-27 00:45:15 +00:00
Jixun 26e50473f2 ci: skip upload step when api key is empty (usually happens for PR)
continuous-integration/drone/pr Build was killed Details
2022-11-27 00:43:37 +00:00
Jixun 1fc7142957 ci: use emsdk instead of download emsdk each build.
continuous-integration/drone/pr Build was killed Details
2022-11-27 00:41:44 +00:00
Jixun ff06c81735 ci: specify debian image + set dependency
continuous-integration/drone/pr Build is failing Details
2022-11-27 00:26:21 +00:00
Jixun 9b76ef2959 ci: mark upload script as executable
continuous-integration/drone/pr Build is failing Details
2022-11-27 00:17:41 +00:00
Jixun d733dba221 ci: upload to gitea packages instead of s3
continuous-integration/drone/pr Build is failing Details
2022-11-26 23:52:48 +00:00
jixunmoe 8bc4b1d109 Merge pull request '使用 DroneCI 自动构建' (#13) from ci/build-with-drone into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #13
2022-11-26 21:05:39 +00:00
Jixun 278104644c ci: better glob for file upload
continuous-integration/drone/pr Build was killed Details
continuous-integration/drone/push Build is passing Details
2022-11-26 20:59:08 +00:00
Jixun ab2b37ba3c chore: gitignore sha256sum.txt
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-11-26 20:34:04 +00:00
Jixun 3c79a1f6e2 fix: syntax error for compare text in shell script
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/pr Build is passing Details
2022-11-26 20:21:43 +00:00
Jixun e79728c687 fix: drone build script
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
2022-11-26 20:11:13 +00:00
Jixun 9b3dfc0ed6 fix: keep dist after legacy has built 2022-11-26 20:11:05 +00:00
Jixun c5adc9dc05 chore: add nvmrc
continuous-integration/drone/push Build is failing Details
2022-11-26 20:02:56 +00:00
Jixun b480088bb6 chore: don't prefix version string with "v" in package.json. 2022-11-26 20:02:52 +00:00
Jixun Wu 49b31301cf ci: initial experiment with ci build
continuous-integration/drone/push Build is failing Details
2022-11-26 19:59:26 +00:00
Jixun Wu 892fea509f test: remove invalid test and avoid jest pick up tests from emsdk folder. 2022-11-26 19:21:30 +00:00
Jixun Wu 38cc5e2189 chore: share the same emsdk path. 2022-11-26 19:08:08 +00:00
Unlock Music Dev e569719f97 Merge pull request '其它: 移除主页说明失效的链接、更名 `.github` 到 `.gitlab`、加入 issue 模板。' (#8) from jixunmoe/um-web:docs/cleanup-readme into master
Reviewed-on: #8
2022-11-24 18:33:55 +00:00
Jixun Wu 6bf8acb170 Merge remote-tracking branch 'upstream/master' into docs/cleanup-readme 2022-11-24 18:26:49 +00:00
Unlock Music Dev 8a95239c35
fix: remove broken test 2022-11-24 21:49:51 +08:00
Unlock Music Dev b811dd6bc5
fix: KgmWasm build target 2022-11-24 21:41:09 +08:00
Unlock Music Dev 4d3ef0ddaa Merge pull request 'Add Tag Edit Function & Wasm for Qmc & Kgm' (#5) from xhacker-zzz/web:master into master
Reviewed-on: #5
2022-11-24 07:32:59 +00:00
Unlock Music Dev c8c4f8dfd6
fix: sync package-lock with package 2022-11-24 15:11:13 +08:00
Unlock Music Dev c25a2963fe
fix: ignore QmcWasm build dist 2022-11-24 15:11:13 +08:00
Unlock Music Dev 9bf05fc15a
fix: compile error 2022-11-24 15:11:12 +08:00
Unlock Music Dev e2fde927f6
fix: script permission 2022-11-24 15:11:08 +08:00
xhacker-zzz 3afa797cf9 Delete generated files 2022-11-24 04:59:07 +08:00
xhacker-zzz 6514befb3a Update qmccache cpp code 2022-11-24 04:54:48 +08:00
xhacker-zzz 4716579b20 Update annotation & log output 2022-11-24 04:43:16 +08:00
xhacker-zzz b63683781c Merge remote-tracking branch 'upstream/master' 2022-11-24 04:40:04 +08:00
Jixun Wu efa3692a54 其它: 移除主页说明失效的链接、更名 `.github` 到 `.gitlab`、加入 issue 模板。 2022-11-22 01:20:06 +00:00
xhacker-zzz 6f6de504d8 Fix Bugs 2022-11-22 06:59:35 +08:00
xhacker-zzz b5cc9a7404 Add mg3d & Fix Bugs 2022-11-22 06:40:02 +08:00
Unlock Music Dev 49e0526ae9 Merge pull request '优化 QMCCache 解密过程' (#6) from jixunmoe/um-web:refactor/optimise-qm-cache-decode into master
Reviewed-on: #6
2022-11-21 19:48:05 +00:00
Jixun Wu 8b2834b06a refactor: implement a more efficient buffer decryption algorithm for qmc cache. 2022-11-21 18:15:16 +00:00
Jixun Wu 2dff0c1f37 test: added test cases for qmc cache. 2022-11-21 18:14:44 +00:00
xhacker-zzz 6d2865b870 Fix Bugs & Add More Detailed Notifications 2022-11-22 01:53:22 +08:00
xhacker-zzz 1138d1ea0c Delete unnecessary code 2022-11-21 05:14:45 +08:00
xhacker-zzz ddceb2ede8 Add README & Global Build Script for WASM 2022-11-21 04:55:43 +08:00
xhacker-zzz 63dc5c125c Add GitHub Actions for wasm & chores 2022-11-21 01:48:24 +08:00
xhacker-zzz 3a8492b651 添加WASM部分源代码 2022-11-21 01:07:18 +08:00
xhacker-zzz 5c21f459c6 修正代码中的细节问题 2022-11-21 00:33:03 +08:00
xhacker-zzz 5d1d56e587 Add Tag Edit Function & Wasm for Qmc & Kgm 2022-11-20 22:30:56 +08:00
Unlock Music Dev 27bccbbd55 Merge pull request '不依赖远端文件实现 KGM 解锁(内嵌 ~1K 的 mask 表数据)' (#2) from jixunmoe/um-web:master into master
Reviewed-on: #2
2022-10-31 17:11:13 +00:00
Jixun 5db14d1d2a chore: bump dependency version. 2022-04-28 00:42:06 +01:00
Jixun 4872efbf87 feat: kgm unlock without large / remote mask file 2022-04-28 00:36:20 +01:00
邴新科 6c51c1a17d
暗黑样式:解密设定窗口 .el-dialog
(cherry picked from commit eb9f7bc003ecaa755240330b2ea32f743be270df)
2022-03-02 05:07:39 +08:00
邴新科 9688cf13c9
+ 新功能提示框(白/黑)
+ fix: 暗黑模式 一些小问题
+ 一些 scss 通用方法

(cherry picked from commit 8c149b6fa82dde3d2c03c236c699939154c65508)
2022-03-02 05:07:20 +08:00
MengYX 1f7ac44829
fix(QMCv2): `.mggl` `.mgg0`
(cherry picked from commit f7b5e25277678ae0f78a186aaf2fa76fee86173f)
2022-01-21 11:47:04 +08:00
MengYX 08a6f1d946 更新.gitlab-ci.yml文件 2022-01-09 11:30:01 +08:00
MengYX 05b47e3188 更新.gitlab-ci.yml文件 2022-01-09 11:29:26 +08:00
MengYX 9525045cd9 更新.gitlab-ci.yml文件 2022-01-09 11:23:42 +08:00
MengYX 4a5f3849a0 更新.gitlab-ci.yml文件 2022-01-09 11:17:52 +08:00
MengYX 711468601a
chore: update deps & bump version
(cherry picked from commit c48dbd8a350d0df22e997ccf9fbdc4e889ddd379)
2022-01-09 10:47:34 +08:00
MengYX 48255367b4
docs: update README.md
(cherry picked from commit 49c53a9c65dc8aa37a9b7413ab937854ecc1c20a)
2022-01-09 10:47:33 +08:00
MengYX 8260e744ca
refactor: change common -> index
(cherry picked from commit eb6be0d9d13a0834b6a7ada9aab846cfbbdfd664)
2022-01-09 10:46:57 +08:00
MengYX 976077e3e1
fix(QMCv2): overflow error in js decoder
(cherry picked from commit 191ac6a932efb290f49e2824839db20ac6ff47ca)
2022-01-09 10:46:55 +08:00
MengYX 4f7d16e2b0
fix #222
(cherry picked from commit 53127de96851ef24f774169ae5e55274f800bd99)
2022-01-09 10:46:54 +08:00
MengYX af10576b88
fix: declare radix in parseInt
Co-authored-by: Jixun Wu <5713045+jixunmoe@users.noreply.github.com>
(cherry picked from commit eee7d7aedc37797820184c12f6954df23e4b98b8)
2022-01-09 10:46:48 +08:00
Jixun Wu fc4a0e002f
fix(QMCv2): Fail gracefully when WebAssembly loader failed.
(cherry picked from commit 700574fb8efd1a1057bc16a053e5015dcb2e3044)
2022-01-09 10:46:46 +08:00
Jixun Wu 55772dec31
feat(QMCv2): Allow extraction of songId from QMC2-wasm
(cherry picked from commit 9ca2d852ce713255caeb8424a2724cb936434f18)
2022-01-09 10:46:45 +08:00
Jixun Wu a08e189d8e
chore: bump qmc2-crypto to v0.0.6-R1
(cherry picked from commit e29b36229e3f550a2fe8dd9d3ae48826f6229ec7)
2022-01-09 10:46:43 +08:00
MengYX 0b786b9885
feature: use online info to correct qmc meta
(cherry picked from commit 7c62f35adb268509f543d67b4a36f49ada3ae206)
2022-01-09 10:46:42 +08:00
MengYX b710255100
fix #142: remove default mp3 cover description
(cherry picked from commit 4bca64b4b8f4be02222de2fa5f6db0382855fd23)
2022-01-09 10:46:35 +08:00
MengYX 381f224a68
fix: api path & docker image name
(cherry picked from commit ce3de22d0e25f8cdf13e455baaae657bf61ee56a)
2022-01-09 10:46:21 +08:00
Jixun Wu a73212f026
test(joox): Added basic sanity test for joox encryption.
(cherry picked from commit 48b8194363264a0276006deaa3c956970a543627)
2022-01-09 10:46:19 +08:00
Jixun Wu c098b73617
feat(joox): Fetch meta data from API
(cherry picked from commit 4af1a38334cfc51ce64dd509f2dff694f78010f6)
2022-01-09 10:46:18 +08:00
Jixun Wu 107eaef5e6
doc: reforamtted & updated content in readme
(cherry picked from commit 68782f0ec570f7e4ec7ae5adc0bcd7da7a0d64b9)
2022-01-09 10:46:17 +08:00
Jixun Wu eb61d81817
fix: avoid "ArtiomTr/jest-coverage-report-action" when running from a fork.
(cherry picked from commit 95df64516c59f4bbbffa21625af8f9be13da01af)
2022-01-09 10:45:18 +08:00
Jixun Wu c1aba5a10f
refactor: move ruby to custom vue component
(cherry picked from commit ea99d38a920850b0d4dbaa7352f57ebf13bbbee6)
2022-01-09 10:45:17 +08:00
Jixun Wu 3567e7e625
chore: remove left-over debugger statement
(cherry picked from commit afab80505e343830e7b20a8073d21bdbfc7e3502)
2022-01-09 10:45:16 +08:00
Jixun Wu 302f422a5f
fix: form validation on input change
(cherry picked from commit c20ce54dacfe4ccc31974a9a9092938ed47db4bb)
2022-01-09 10:45:14 +08:00
Jixun Wu 1c4185750a
feat(config): better config ui
- JOOX Music UUID label + description
- Not full screen anymore

(cherry picked from commit 8a323f9dbbd17515f53826023a565112acaed90b)
2022-01-09 10:45:06 +08:00
Jixun Wu 13f2d86df4
feat(joox): re-use QM meta extraction code
(cherry picked from commit 2e946e6e30e02085018e868b7857acb62a1a0b08)
2022-01-09 10:45:04 +08:00
Jixun Wu 4ae92cd630
refactor(qmc): extract qm meta code to utils
(cherry picked from commit b6497e2bd3679e0e62bd6a90bdac16fa7c7f1b4e)
2022-01-09 10:45:02 +08:00
Jixun Wu 57d2244c28
fix: only pass over config settings
(cherry picked from commit 3884158f06b71907f004d7a2b4df53e3e486983b)
2022-01-09 10:45:01 +08:00
Jixun Wu 5e6cf8bf24
feat(storage): Pass over config to worker thread on decryption call
(cherry picked from commit 36d616398eac4e8d51863863fa5205fe1c91267f)
2022-01-09 10:44:59 +08:00
Jixun Wu 10aa05c244
refactor: storage factory + singleton
- Make storage easier.

(cherry picked from commit ed84a4732d7dd3ce6b2c22f30553ab5c59f85dbb)
2022-01-09 10:44:57 +08:00
Jixun 5d3d8ce485
fix: storage read/write in chrome extension
(cherry picked from commit bae9a7fec0c98807b3c5c3598f321135ccf6c9d5)
2022-01-09 10:44:56 +08:00
Jixun f94266bacc
fix: add missing permission for chrome storage
(cherry picked from commit 3fb0e1eb0f80cdb84fce6c2eb2a12a028beb1f0b)
2022-01-09 10:44:54 +08:00
Jixun f414f978b4
chore: bump to 0.0.1-R4
(cherry picked from commit 2da37f984a8ed4ca369e2efecb2da5d71976c93e)
2022-01-09 10:44:52 +08:00
Jixun ac6336993e
fix: crash due to chrome been undefined
(cherry picked from commit 53a2073cb482fc9deef7aa0ddf45447c6971d819)
2022-01-09 10:44:50 +08:00
Jixun 8a17dd352d
chore: bump joox-crypto dependency
(cherry picked from commit b46d9fa720a9193ae51b5954e2e34c875e1cc897)
2022-01-09 10:44:49 +08:00
Jixun dbfff5feca
feat: add basic joox support
(cherry picked from commit 699333ca06526d747a7eb4a188e896de81e9f014)
2022-01-09 10:44:36 +08:00
MengYX 3d86eb19b9
fix(ci): test coverage annotation failed
(cherry picked from commit 058985de4f003e2fbdfc4261e2d172c1f9c1c4db)
2022-01-09 10:44:32 +08:00
MengYX 6287283cde
pretty: ignore matrix
(cherry picked from commit edc4e4864b31c32e5860b3a5c840657be6cc4154)
2022-01-09 10:44:28 +08:00
MengYX 477d66e9e9
chore: remove unused api
(cherry picked from commit 3727f67e407807de33be64905b927561aaf1c10f)
2022-01-09 10:44:22 +08:00
MengYX 3441b7a3b1
all: format with prettier
(cherry picked from commit cad5b4d7deba4fbe4a40a17306ce49d3b2f13139)
2022-01-09 10:44:16 +08:00
MengYX 5dc89502cb
maintenance: add prettier
(cherry picked from commit 559be402c940b7b31bdb2567c23ff17251aabe04)
2022-01-09 10:38:33 +08:00
MengYX b9efb68851
fix(QMCv2): cipher should determine by key size
(cherry picked from commit dba63f212cbf9351e5dc16870eb32ae582db2867)
2022-01-09 10:38:27 +08:00
MengYX c1320c811b
chore(QMCv2): fix code style
(cherry picked from commit 87138718549bdec014752ba43dcd5997aaf29137)
2022-01-09 10:38:20 +08:00
MengYX fce4734ed9
feat(QMCv2): use js decoder
(cherry picked from commit c24e5d29733cfa771dd41ae40032029c6bbb9186)
2022-01-09 10:38:18 +08:00
MengYX 334864f6d8
feat(QMCv2): add decoder
(cherry picked from commit 29ac94d1fe52e666fda619f8716d2bc0b120a9ee)
2022-01-09 10:38:16 +08:00
MengYX 76c3887eec
feat(QMCv2): add rc4 cipher
(cherry picked from commit 6b5b4d3bf5f6285e908808d48dee4e2e4ae8c3a2)
2022-01-09 10:38:14 +08:00
MengYX 4703667a44
feat(QMCv2): add key decrypt
(cherry picked from commit a9aaa40ec48a75967882ef95951bf4f7fccf7a9d)
2022-01-09 10:38:13 +08:00
MengYX 183ac63864
feat(QMCv2): add map cipher
(cherry picked from commit 7306bf031f8bc07168197c00e332bf89c8d611dd)
2022-01-09 10:38:11 +08:00
MengYX d5ac9ad56e
test(QMCv2): coverage standard TEA cipher
(cherry picked from commit c2c89a423ffffc06fb43c86d4714bb32d1936c3e)
2022-01-09 10:38:09 +08:00
MengYX 7b3b701924
feat(QMCv2): add standard TEA cipher
(cherry picked from commit 24422b216a15319d90799d4f8f54453c8efd5c34)
2022-01-09 10:38:08 +08:00
MengYX 97ef3f0d7b
fix(extension): version string must be numbers and dots
(cherry picked from commit 3fd35b5d30037a6e156fdb75ca4124837b37d658)
2021-12-16 10:46:20 +08:00
MengYX cbed2332fb
fix: ci
(cherry picked from commit f9f5e32b449c9268cc07f7787587f417d70f08c9)
2021-12-16 09:51:28 +08:00
MengYX fab64f19d4
maintenance: update ci
(cherry picked from commit 525ddfae314f05e1d9a7b67cabcc974b32a503b4)
2021-12-16 09:51:23 +08:00
MengYX 70b46f9d63
maintenance: update ci
(cherry picked from commit 10e35c5d3e4391e22fd005f04ab7be0e503c971a)
2021-12-16 09:51:21 +08:00
MengYX de01d7ff9c
chore: bump version
(cherry picked from commit 12e3f91a1e9a4d681633d531af22b5e385dbe470)
2021-12-16 09:51:19 +08:00
MengYX 768f30a2fe
maintenance: update ci
(cherry picked from commit cb92eed9b135c04a17389f48997c18ba81e60c3a)
2021-12-16 09:51:17 +08:00
MengYX 59048aef6b
maintenance: update ci
(cherry picked from commit 3960ea7d591a199c188e764b26d0840ccae1c322)
2021-12-16 09:51:16 +08:00
MengYX 60ea6239eb
maintenance: remove fix-compatibility.js
(cherry picked from commit af20e8a6970ec7f08799389ac9ce897d1cc822e0)
2021-12-16 09:51:15 +08:00
MengYX 42ea3651f2
feat: use static cipher instead of mask
(cherry picked from commit cd6b84ad7eed489f9bcbd72d847cd4d704052b0c)
2021-12-16 09:51:07 +08:00
Jixun Wu 7f48acd214
chore: add eol at the end of `qmcv2.ts`.
(cherry picked from commit 9470f2ca8706d602c6d073012d4c3fc6aec7da77)
2021-12-16 09:50:20 +08:00
Jixun Wu 8e007ff0a3
chore: (redone) upgrade qmc2-crypto to 0.0.5-R4
- Remove the use of `new Function` in emscripten generated code.
- This commit is a clean commit that does the same thing as 3b88d168b660f780824016e4d23241d1fc766e39

(cherry picked from commit bdd60bc502ace1116698ff16357001bfb7608a43)
2021-12-16 09:49:56 +08:00
Jixun Wu b2bf878c89
Revert "chore: upgrade `qmc2-crypto` to 0.0.5-R4"
This reverts commit 3b88d168b660f780824016e4d23241d1fc766e39.

It generates unexpected large diff in package-lock.json.

(cherry picked from commit 0f3cd9b67fbc0f91da5272eb60301e09e4fc6de3)
2021-12-16 09:49:34 +08:00
Jixun Wu b550b407e5
chore: upgrade `qmc2-crypto` to 0.0.5-R4
- Remove the use of `new Function` in emscripten generated code.

(cherry picked from commit 3b88d168b660f780824016e4d23241d1fc766e39)
2021-12-16 09:49:10 +08:00
Jixun Wu 1e927ad962
fix: treat qmcflac/qmcogg as QMCv2 and fallback to QMCv1
(cherry picked from commit 41e588e9864801897fa13eb96a1764baaa5a4ab5)
2021-12-16 09:48:52 +08:00
Jixun Wu 9edcaadb83
refactor: remove suppressed qmc mask methods / constants
(cherry picked from commit 5d48b28a949cbd42f24781a69124d7aa521e51c1)
2021-12-16 09:48:31 +08:00
Jixun Wu ec6be66cc1
refactor: restore support for QMCv1.
(cherry picked from commit 19239f182d71e2e4362309f08706a91c00bb6bd1)
2021-12-16 09:48:30 +08:00
Jixun Wu ab3f54cb47
chore: update supported ext list
(cherry picked from commit bdab51bde327244a105fff5c2086911b275b2259)
2021-12-16 09:48:13 +08:00
Jixun Wu da11e3a9a1
chore: Use QMC2-Crypto with embedded WASM build from 0.0.5-R3
(cherry picked from commit 9448b497ed6b80e41f0e9f731f1ffa1e56fb149a)
2021-12-16 09:48:06 +08:00
Jixun Wu 8d35afae62
fix: patch threads to work with production build
(cherry picked from commit 4da56bb0fe509c4cb0c4bb6e560b4383f185bf45)
2021-12-16 09:47:41 +08:00
Jixun Wu 840d750716
feat(qmcv2): Experiment with qmc2-crypto
(cherry picked from commit c8eb1bc481347efb6d35e9122e17e624bde18772)
2021-12-16 09:47:40 +08:00
MengYX ada078df19
maintenance: add jest as unit test 2021-12-14 15:39:52 +08:00
MengYX 8facd65834
chore: update deps 2021-12-14 14:57:48 +08:00
MengYX a6367401ae
Merge pull request #184 from lvzx123/patch-1
Now it is 2021!
2021-09-25 15:27:57 +08:00
lvzx123 565ee37a8b
Now it is 2021!
大人,时代变了
2021-09-25 08:56:07 +08:00
MengYX aab611bff5
bump version 2021-08-27 10:01:05 +08:00
MengYX 942e43fa16
fix #169 2021-08-25 01:04:46 +08:00
MengYX 84be57a903
fix #179 2021-08-24 23:55:44 +08:00
MengYX a6a3c02538
fix #179 2021-08-24 23:13:19 +08:00
Emmm Monster 216ff4021c
fix(extension): compute version name 2021-08-11 23:04:01 +08:00
Emmm Monster d3898161b9
chore: bump version & update deps 2021-08-08 08:08:56 +08:00
MengYX 5a7a9e3add
change ixarea api endpoint 2021-08-08 07:56:28 +08:00
MengYX 652bb1fc32
optimize: imports 2021-08-08 07:51:42 +08:00
MengYX 6737e8c11b
optimize: `.kgm` mask loading 2021-08-08 07:47:28 +08:00
sunhao03 71862538b7 fetch mask file fix on production 2021-08-07 22:13:00 +08:00
Emmm Monster 4251b94b1f
chore: update deps & fix audit 2021-07-01 01:33:28 +08:00
Emmm Monster 8fdda048f6
fix: avoid using worker in file protocol 2021-07-01 01:29:04 +08:00
Emmm Monster 39c7294996
chore(ci): build after *.ts changes 2021-06-03 13:15:04 +08:00
Emmm Monster 48f879cb58
simplify: decrypt/ncm-cache & decrypt/common 2021-06-03 13:09:14 +08:00
Emmm Monster f0875ad175
fix: decrypt/qmc-cache
adapt: decrypt/qmc for qmc-cache
2021-06-03 13:09:02 +08:00
qq1010903229 02a146e069
增加对网易云音乐.uc缓存格式和QQ音乐.cache缓存格式的支持 (#161)
* Update common.ts

* Create ncmcache.ts

* Create qmccache.ts
2021-06-03 13:00:35 +08:00
Emmm Monster 2e31853ffb
chore: update deps 2021-05-27 19:53:19 +08:00
Emmm Monster a7aaf246ae
fix: .vpr/.kgm fail in worker 2021-05-27 19:38:42 +08:00
Emmm Monster 4bc0a10c09
feature(sniffer): support `.dff` 2021-05-25 12:27:19 +08:00
Emmm Monster 3645dd7d01
fix: remove test file 2021-05-25 04:36:32 +08:00
Emmm Monster 73bb9438b1
chore: Bump Version 2021-05-25 03:20:42 +08:00
Emmm Monster 21d5ae305c
feature: directly write to fs 2021-05-25 03:06:28 +08:00
Emmm Monster 759c1bd87e
fixes 2021-05-24 23:48:52 +08:00
Emmm Monster c7e5dfb4c4
refactor: component/*.vue 2021-05-24 22:19:37 +08:00
Emmm Monster ca4ed149b2
refactor(decrypt/*): change interface 2021-05-24 15:05:14 +08:00
Emmm Monster b3c6fe2f24
refactor(decrypt/qmc): typescript 2021-05-24 06:50:20 +08:00
Emmm Monster aca1c11332
refactor(decrypt/qmc): typescript qmc mask 2021-05-24 05:57:04 +08:00
Emmm Monster 15dba7b92f
refactor(.ncm): typescript & class 2021-05-24 05:04:16 +08:00
Emmm Monster 37a641e69e
refactor(typescript): utils.WriteMetaFor{ Mp3, Flac } 2021-05-24 02:55:42 +08:00
Emmm Monster 342241b379
refactor(typescript): .xm & .kgm 2021-05-24 01:30:38 +08:00
Emmm Monster a1eddb230f
refactor(typescript): Use ES6 import & use interface 2021-05-23 23:47:01 +08:00
Emmm Monster 3c0a9e92f9
refactor(typescript): utils.GetCoverFromFile & utils.GetMetaFromFile 2021-05-23 23:06:21 +08:00
Emmm Monster 4637a3650a
feat(decrypt/kwm): support raw .acc 2021-05-23 22:29:34 +08:00
EmmmX 9ae860cb11
Merge pull request #157 from unlock-music/add-typescript
Add typescript support
2021-05-23 22:08:23 +08:00
Emmm Monster ec711990a1
refactor(typescript): utils.GetArrayBuffer 2021-05-23 22:02:36 +08:00
Emmm Monster f3f6f9ef40
refactor: move some utils to typescript 2021-05-23 21:40:43 +08:00
Emmm Monster 213ac35157
chore: add support for typescript 2021-05-23 21:01:17 +08:00
Emmm Monster e36df21f01
chore: update deps & fix audit 2021-05-23 20:41:31 +08:00
MengYX fc52423976
Make Github Dependabot Happy 2021-04-11 13:59:39 +08:00
MengYX 5ca9b1fab4
README: Add extension info 2021-02-18 20:08:25 +08:00
MengYX 3dfed44021
Remove: [Extension] Stats Code 2021-02-09 15:54:45 +08:00
MengYX 9e04bc8690
Add: [Docs] Docker Usage in README 2021-02-08 20:05:10 +08:00
MengYX 7716c356ed
Bump Version 2021-02-08 19:28:40 +08:00
MengYX 701f750476
Change: [CI] Action Name 2021-02-08 19:14:26 +08:00
MengYX d73493a624
Fix: [CI] Build Docker Image 2021-02-08 19:14:12 +08:00
MengYX 85fdbff00d
Fix: [CI] Build Docker Image 2021-02-08 17:09:26 +08:00
MengYX 3a5afeb8a6
Fix: [CI] Build Docker Image 2021-02-08 16:23:24 +08:00
MengYX 8dc1a66d69
Update: [CI] Build Docker Image 2021-02-08 16:08:06 +08:00
MengYX 7733fa6ad1
Add: Dockerfile 2021-02-08 14:11:46 +08:00
MengYX 6a2dd672f3
Update: [CI] Remove Cache (because using `npm ci`) 2021-02-08 14:01:41 +08:00
MengYX ca82842b04
Update: [Docs] README 2021-02-08 12:11:45 +08:00
MengYX 137df9c4c2
Misc: Bump Version 2021-02-08 05:31:16 +08:00
MengYX b17bb37c38
Fix: [Extension] Use extension API make sure page open successfully 2021-02-08 04:50:00 +08:00
MengYX 9607580e8b
Update: Deps 2021-02-08 04:26:56 +08:00
MengYX 5956412d7e
Fix: [CI] Generated zip structure 2021-02-08 04:24:55 +08:00
MengYX 22312959f3
Merge branch 'feature/extension' 2021-02-08 04:14:02 +08:00
MengYX 549983a928
Adapt: [Extension] for Firefox 2021-02-08 03:35:26 +08:00
MengYX ce2642ad1f
Fix: [Extension] Remove inline script (for extension's Content Security Policy reason)
Fix: [Extension] Disable Service Worker
2021-02-08 03:05:49 +08:00
MengYX c6ea98333e
Update CI: Add Extension Build 2021-02-08 01:47:31 +08:00
MengYX 042b1ca0dd
Add Feature: Browser Extension 2021-02-08 01:28:30 +08:00
MengYX e089fe1268
Change: Web Manifest 2021-02-08 00:26:40 +08:00
MengYX 67966d4b54
Update README.md 2021-01-10 16:38:03 +08:00
MengYX ca462f94fa
Remove Drone CI 2020-12-23 15:35:46 +08:00
MengYX 297c7c9252
Remove "By IXarea" 2020-12-23 15:34:37 +08:00
MengYX 8acc1ade81
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/highlight.js-10.4.1' into master 2020-12-19 21:17:27 +08:00
MengYX e543024641
Bump Version and Update Deps 2020-12-19 21:16:53 +08:00
MengYX cf48554424
Merge pull request #117 from ix64/fix-qmc-meta
Fix incorrect id3 info for .qmc decryption
2020-12-17 08:36:10 +08:00
MengYX 2e0cd04255
Update new-feature.md 2020-12-10 19:01:54 +08:00
MengYX adbcdfd083
Update bug-report.md 2020-12-10 19:00:50 +08:00
MengYX e1505148c8
Add tips for qmc not writing cover 2020-12-06 02:32:57 +08:00
MengYX b65e47514f
Update CI 2020-12-06 02:22:43 +08:00
MengYX 31215772e3
Try to fix .qmc ID3 Info 2020-12-06 02:16:45 +08:00
dependabot[bot] 070c642dbf
Bump highlight.js from 10.4.0 to 10.4.1
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 10.4.0 to 10.4.1.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/10.4.0...10.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-04 18:43:39 +00:00
MengYX 8e135f7004
Bump Version and Update Deps 2020-11-26 17:28:14 +08:00
MengYX 0fb30ddc17
Merge pull request #113 from KyleBing/master
调整暗黑模式样式,新增全局统一样式 by @KyleBing
2020-11-26 16:56:44 +08:00
KyleBing e9a25f3140 ^ package-lock.json 2020-11-25 14:47:23 +08:00
KyleBing 5e2f3d36c2 暗黑模式颜色调整,载入页颜色适配黑色 2020-11-25 14:38:29 +08:00
KyleBing a040c88a07 use scss source file, remove pre-compiled css file. 2020-11-25 13:50:38 +08:00
KyleBing b370f4ceb6 调整暗黑模式样式,新增全局统一样式 2020-11-24 22:28:56 +08:00
MengYX bf0df4e68d
Merge pull request #112 from flosacca/master
Fix #100 by @flosacca
2020-11-23 21:34:58 +08:00
flosacca f24ea6a07b Fix #100 2020-11-21 07:03:57 +08:00
MengYX c11f3fd130
Update README 2020-11-07 22:46:27 +08:00
MengYX 2ffcbf79b5
Bump Version 2020-11-07 01:22:45 +08:00
MengYX 6a2b98798b
Fix #103 #100 duplicated metadata 2020-11-07 01:12:04 +08:00
MengYX 60e2039e56
Update CI 2020-11-06 22:40:23 +08:00
MengYX fbdad625c5
Update Deps 2020-11-06 22:40:13 +08:00
MengYX 10814ea109
Merge pull request #106 from lc6464/master
适配浏览器深色模式
2020-11-01 11:40:21 +08:00
NULL-LC 52657046d6 适配浏览器深色模式 2020-10-31 19:25:14 +08:00
MengYX 175112180d
Merge pull request #101 from renbaoshuo/patch-1
更新 Edge 浏览器下载链接
2020-10-30 10:48:45 +08:00
Baoshuo Ren 7b26630428
更新 Edge 浏览器下载链接 2020-10-18 19:02:40 +08:00
MengYX 55b2f17ed7
Bump Version 2020-09-23 16:51:17 +08:00
MengYX be09790810
Merge pull request #97 from qq1010903229/patch-1
Merge pull request #97 增加对QQ音乐微云网盘格式的支持
2020-09-23 16:45:54 +08:00
MengYX df2d409351
Fix Kgm Decrypt Bug 2020-09-23 14:15:47 +08:00
MengYX a558dac34b
Update Deps 2020-09-23 12:55:14 +08:00
MengYX 44642b1c39
Fix kgm bug 2020-09-23 12:54:54 +08:00
qq1010903229 6e66d2da4f
Update qmc.js 2020-09-19 12:03:02 +08:00
qq1010903229 e1cf15cf8c
Update common.js 2020-09-19 12:00:58 +08:00
MengYX 1d415cae52
Add Comment for Issue Template 2020-09-04 19:12:58 +08:00
MengYX 66e2b96bad
Bump Version 2020-09-02 00:04:26 +08:00
MengYX 79c0c85ab3
Merge remote-tracking branch into master 2020-09-01 23:31:43 +08:00
MengYX ad47a713ad
Add Tips for .kgm while using "file:" protocol 2020-09-01 23:26:02 +08:00
MengYX 6ef0850c40
Use Small Cover Image for .ncm 2020-09-01 23:17:17 +08:00
MengYX 9af2ba5e62
Update README.md 2020-08-15 10:30:13 +08:00
MengYX 0f52c53d6c
fix .xm filename detect 2020-08-13 21:27:50 +08:00
MengYX 24764875f3
Update CI 2020-08-13 21:27:25 +08:00
MengYX 65a41b21c3
Merge remote-tracking branch 'origin/master' 2020-08-13 16:01:55 +08:00
MengYX b93b93110b
Update GitHub CI 2020-08-13 16:01:35 +08:00
MengYX 7a5cefd950
Bump Version and Update Deps 2020-08-13 15:58:31 +08:00
MengYX 3b885f82ca
Fix .xm read info from filename 2020-08-13 15:33:15 +08:00
MengYX b6757e81a2
Fix #84 2020-08-13 15:26:15 +08:00
MengYX 8d79035675
Fix .xm file type recognize error 2020-08-13 15:25:41 +08:00
MengYX 6592f304b6 Update issue templates 2020-08-07 19:33:28 +08:00
MengYX e5bff35f89
Fix ncm cover image too big to write into meta 2020-08-03 15:04:54 +08:00
MengYX 9b28676c43
Clean up 2020-08-03 14:03:10 +08:00
MengYX 4a2d31238b
Fix #79 ncm->flac no metadata (file downloaded from phone) 2020-08-03 14:02:17 +08:00
MengYX fd2866f53d
Bump Version 2020-08-02 18:25:56 +08:00
MengYX 5e8af22f08
Fix wrong zip file in release [Skip CI] 2020-08-01 01:20:56 +08:00
MengYX 4aa2ff7f91
Fix #77 ncm flac meta duplicated
Fix #78 write flac cover sometimes fail
2020-08-01 01:10:27 +08:00
MengYX c9a4a901be
Update README.md 2020-07-19 00:03:41 +08:00
MengYX a824bf1f63
Fix GitHub CI 2020-07-18 23:37:07 +08:00
MengYX bb811178d4
Bump Version 2020-07-18 23:04:26 +08:00
MengYX c25055f875
Change IXarea Api Endpoint 2020-07-18 22:43:25 +08:00
MengYX 62e36ef228
Change IXarea Api Endpoint 2020-07-18 21:58:07 +08:00
MengYX 438383979d
Add Support qq music cover 2020-07-18 21:45:53 +08:00
MengYX 59d47c755e
Add Support for flac meta/cover 2020-07-18 19:25:41 +08:00
MengYX e2d4283003
Write meta for qq music mp3 2020-07-18 18:48:07 +08:00
MengYX 6b1c08663a
Resolve QQMusic Cover(By IXarea Server) 2020-07-17 00:33:10 +08:00
MengYX c338b6ef04
Fix dep security problem 2020-07-16 22:44:42 +08:00
MengYX c95cfcb984
Update Tips 2020-07-16 22:41:22 +08:00
MengYX 26b6b03ef3
Add Init Support for Kgm/Vpr 2020-07-16 22:22:32 +08:00
MengYX f548412d8d
Update Deps 2020-07-16 21:28:49 +08:00
MengYX f8a1ec137c
Update and rename deploy.yml to release.yml 2020-05-28 02:24:32 +08:00
MengYX 8622aef21f
Create GitHub Pages 2020-05-28 00:41:17 +08:00
MengYX ecff3d34b0
Fix typo #62 2020-05-21 09:36:59 +08:00
MengYX a140b45e5f
Bump Version & Update Deps 2020-05-18 16:41:10 +08:00
MengYX d42e6e3259
Add Tips 2020-05-18 16:40:39 +08:00
MengYX bceabe4fcf
Simplify 2020-05-14 23:46:21 +08:00
MengYX 7b9070f99d
Fix ncm unlock while no album pic #58 2020-05-14 17:37:58 +08:00
MengYX 6410576adb
Fix Xiami return info 2020-05-14 17:36:16 +08:00
MengYX 24bfc9e603
Fix .qmc Files Unlock Error 2020-04-26 15:20:12 +08:00
MengYX b6886b7001
Bump to 1.5.1 2020-04-26 15:17:40 +08:00
MengYX dd965688a9
Fix Qmc Mask Query 2020-04-26 15:04:46 +08:00
MengYX f9543965b6
Small Bug Fix 2020-04-26 11:20:29 +08:00
MengYX e7b86a4779
Update Mgg Detect Algorithm 2020-04-26 01:33:52 +08:00
MengYX c3756bb3b3
Change Tips Info 2020-04-26 01:33:52 +08:00
MengYX 4d5d70f4b6
Change CI 2020-04-26 01:33:52 +08:00
MengYX e31eb9c1f1
Bug Fix in Worker Mode 2020-04-23 21:23:01 +08:00
MengYX f64fa71b1e
Remove Console Log 2020-04-23 21:16:41 +08:00
MengYX 181a3c402f
Update README.md 2020-04-23 21:12:11 +08:00
MengYX 4c4e4061f5
Update Description and Bump Version
Small Fixes
2020-04-23 21:10:25 +08:00
MengYX 2526adcab0
Add Support For Xiami .xm Files! 2020-04-23 20:46:08 +08:00
MengYX f2ea85bae9
Merge remote-tracking branch 'ixarea/master' 2020-04-23 18:17:58 +08:00
MengYX c07d55565d
Small Changes 2020-04-23 18:16:33 +08:00
MengYX 71a8d9fab7
Unlock Kuwo .kwm Files! 2020-04-23 18:15:07 +08:00
MengYX 7233fdc707
Update README Tips 2020-04-22 06:43:40 +00:00
MengYX 0d119094df
Update README.md 2020-04-21 14:31:01 +00:00
MengYX 1f804e1037
Update README.md 2020-04-09 08:37:54 +00:00
MengYX 77d9ca4ba8
Change Browser Tips Condition 2020-04-08 22:47:38 +08:00
MengYX 8f3c74c100
Update README.md 2020-04-08 14:12:05 +00:00
MengYX 29c27bbfd9
Limit Update Tips 2020-04-07 19:02:57 +08:00
MengYX c87e6e04ed
Bump Version 2020-04-06 18:20:40 +08:00
MengYX 0b52a0acb2
Fix GBK Detect Bug 2020-04-06 13:03:17 +08:00
MengYX a087da67b2
Update Deps 2020-04-05 19:26:05 +08:00
MengYX 3ca3142d11
Hello 2020! 2020-04-05 19:23:30 +08:00
MengYX 23b01d5f87
Fix GBK Encoding Reading in QQMusic Mp3 2020-04-05 19:20:22 +08:00
MengYX 12025e3709
Get QQMusic Cover URL (Only Display) 2020-04-05 17:56:10 +08:00
MengYX 2415db67be
Move ID3 Writer to Util 2020-04-05 17:31:41 +08:00
MengYX 9569e2f145
Add progress bar for unlocking #37
Add tips for instant save
2020-04-05 12:35:11 +08:00
MengYX 87356a0514
Add Support For .qmc2 2020-04-05 01:01:59 +08:00
MengYX ff63c420eb
Add Header Check For .mgg 2020-03-18 23:03:39 +08:00
MengYX 1d3725f9a4
Use Npm Registry 2020-03-18 22:45:07 +08:00
MengYX a70aaf03af
Merge pull request #31
Bump acorn from 6.4.0 to 6.4.1 to fix a security vulnerability
2020-03-15 23:09:18 +08:00
dependabot[bot] 31fdaf11f7
Bump acorn from 6.4.0 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-15 14:56:29 +00:00
MengYX 0c23249d76
Bump Version 2020-03-12 18:56:21 +08:00
MengYX 6502d2d067
Merge CI Settings 2020-03-10 12:22:46 +08:00
MengYX 24e1d33642
Fix errors when parsing ncm files without metadata 2020-03-10 09:31:48 +08:00
MengYX a72804544f
Remove Source Map in production 2020-03-04 10:00:18 +08:00
MengYX 2aeb60d0a9
Update CI 2020-03-04 09:38:30 +08:00
MengYX 97ca09dbae
Bump Version 2020-03-04 09:30:48 +08:00
MengYX e78118b4d0
Add fix-compatibility npm command 2020-03-04 09:29:05 +08:00
MengYX 37c60caa06
Update Dependencies 2020-03-04 08:37:54 +08:00
MengYX fba020969b
Update Babel config 2020-03-04 08:37:39 +08:00
MengYX 790715726a
Add temporary solution to fix compatibility for Edge 18 2020-03-04 08:37:16 +08:00
MengYX 4ca45e1233
Merge branch 'pull/22' 2020-03-03 20:01:42 +08:00
MengYX cfacb77d15
Optimize loader tips for outdated browser 2020-03-03 20:00:44 +08:00
任宝硕 5f7461e8aa
Update index.html 2020-03-03 16:41:45 +08:00
MengYX aa98ec422c
Reformat Code 2020-03-01 23:36:16 +08:00
MengYX d2ec667c19
Fix Update Check 2020-03-01 23:34:24 +08:00
任宝硕 94ee8eb3bd
Update index.html 2020-03-01 16:16:34 +08:00
任宝硕 c13f7fcced
对部分老旧浏览器进行显示调整 + 部分内容修复 2020-03-01 16:14:43 +08:00
MengYX 1d83898f08
#20 Add Support For Netease DJ Files 2020-02-29 19:52:41 +08:00
MengYX 98ca5bc8ff
Bump Version 2020-02-23 13:52:03 +08:00
MengYX 984b549448
#19 Add Download Type: Origin Filename 2020-02-23 13:45:30 +08:00
MengYX 9fd7177ebb
Add Update Check 2020-02-23 13:44:19 +08:00
MengYX f7d19e62fd
Immediately Load Latest App 2020-02-23 13:42:50 +08:00
MengYX 86d59f4e6f
Update Dependencies 2020-02-23 13:42:20 +08:00
MengYX 3906572723
Optimize UI 2020-02-23 13:41:39 +08:00
MengYX 179f72687a
Fix Decrypt Mflac Error 2020-02-12 23:04:48 +08:00
MengYX ce251e3c9e
Fix Decrypt Algorithm Error 2020-02-11 17:03:46 +08:00
MengYX cc8e818142
Optimize Loading 2020-02-11 16:35:45 +08:00
MengYX f99e885d9f
Fix Mgg Mask Detect Bug 2020-02-11 16:23:50 +08:00
MengYX 58d9039960
Update README and Bump Version 2020-02-11 16:00:34 +08:00
MengYX c314a251c2
Add Detect Media Type by File 2020-02-11 15:52:22 +08:00
MengYX 10f09958c4
Optimize Import 2020-02-11 14:48:27 +08:00
MengYX 41a45176be
Use Universal Decoder for Qmc,Mgg,Mflac 2020-02-11 14:35:17 +08:00
MengYX 47cea6eae9
Use Universal Mask for Qmc,Mgg,Mflac
Add Local Experimental Support For Mgg
2020-02-11 00:34:26 +08:00
MengYX 2fc9368a92
Add Experimental Support For Mgg 2020-02-10 20:10:48 +08:00
MengYX 4e1bfb0b55
Bump Version 2020-02-09 15:16:23 +08:00
MengYX 27b74ea5dd
Better Way to Detect Mflac Mask 2020-02-09 14:05:40 +08:00
MengYX 9aab7a7713
Adjust for Debugging 2020-02-07 20:17:45 +08:00
MengYX dcde0d3fbb
Fix Babel Config 2020-02-07 14:41:21 +08:00
MengYX 0c0299d63a
Remove Useless Information 2020-02-06 16:18:40 +08:00
MengYX 3ee9f5d2d1
Split App.vue 2020-02-06 16:01:35 +08:00
MengYX e3ca175258
Optimize Bundle Size 2020-02-06 14:05:46 +08:00
MengYX 05cdd7b896
Merge branch 'ix-master' 2020-02-05 02:08:08 +08:00
MengYX 91ba19d878
Bump Version 2020-02-05 01:58:53 +08:00
MengYX a7c7b6cbfa
Add instant download to avoid memory occupation 2020-02-05 01:53:58 +08:00
MengYX 50fbb69394
Merge branch 'pull/17'
# Conflicts:
#	src/App.vue
2020-02-05 01:38:34 +08:00
MengYX 402fb184f7
Add Web Worker 2020-02-05 00:30:44 +08:00
MengYX 0766e2fcb0
Merge pull request #16 from smtop/dev
增加歌曲命名格式选项
2020-02-04 19:41:01 +08:00
1519715742@qq.com 92bd0f6be3 Performance improvement in multiple files 2020-02-04 19:12:44 +08:00
smdev 9c6af8ff9c 增加歌曲命名格式选项 2020-02-04 18:24:53 +08:00
MengYX 8094f3ad58
Update CI 2020-02-01 12:05:00 +08:00
MengYX e6a81f8546
Edit index.html Upgrade Dependencies 2020-01-31 11:47:16 +08:00
MengYX 211b4e0206
#9 Add QQ Music tkm Format 2020-01-27 18:02:39 +08:00
MengYX 4cd5b45986
Fix QMC filename error 2020-01-27 16:43:40 +08:00
MengYX 60445b7ed9
#11 Add Moo Music Format 2020-01-27 14:06:45 +08:00
MengYX 4e499b2deb
Merge branch 'master' of github.com:ix64/unlock-music 2020-01-21 20:00:20 +08:00
MengYX 0cddb98612
Update CI 2020-01-21 19:52:45 +08:00
MengYX ca3f4c1aa4
Update Readme [CI SKIP] 2020-01-21 19:31:30 +08:00
MengYX eec5bd0fb8
Add Support For: tm0/2/3/6 2020-01-21 19:21:17 +08:00
MengYX bddde78fcd
Reconstruct 2020-01-21 19:20:46 +08:00
MengYX bc138c4078
Update Deps 2020-01-21 19:01:52 +08:00
MengYX 51a5a8a44f
Update README.md 2019-12-23 19:09:24 +08:00
MengYX 2fb5aecdb2
Update README.md 2019-12-16 19:01:56 +08:00
MengYX 8014c33538
Revert: Favicon 2019-12-08 19:12:15 +08:00
MengYX e6bce501bb
Upgrade Vue Cli 2019-12-07 15:09:09 +08:00
MengYX de37519f8c
Remove unused icon 2019-12-07 14:44:32 +08:00
MengYX a483594c4b
Change build mode 2019-12-07 14:32:28 +08:00
MengYX 014fe5ae26
Fix accept in uploader 2019-12-07 12:23:18 +08:00
MengYX 73f3959094
Use Post 2019-12-01 22:52:29 +08:00
MengYX 48658701a2
Fix link error in README [SKIP CI] 2019-12-01 21:04:51 +08:00
MengYX c4e9fb0dcc
Update README and Dependencies 2019-11-30 21:10:40 +08:00
MengYX d51d7ec773
Change Notification 2019-11-24 19:33:11 +08:00
MengYX 683f58964c
Report Error Type 2019-11-23 19:22:32 +08:00
MengYX 0ca830e896
Fix No Status Error 2019-11-23 19:11:40 +08:00
MengYX 2266ca2cf1
Fix Download Button 2019-11-23 18:56:45 +08:00
MengYX c71cb8ee85
Show Detail Info While Error Occurred 2019-11-23 18:30:59 +08:00
MengYX b68efea15b
Update Dependencies 2019-11-23 18:14:15 +08:00
MengYX 591c1a5312
Add Partial Support For .mflac 2019-11-23 18:09:33 +08:00
MengYX 95de3e8cc5
Reformat Code [SKIP CI] 2019-11-23 15:10:08 +08:00
MengYX d91f48aa70
Add tips for qmcogg 2019-11-23 15:03:45 +08:00
MengYX 497a63486d
CI: Auto Deploy and Use Cache 2019-11-10 22:38:43 +08:00
MengYX 538705187a
Add CI 2019-11-10 17:25:01 +08:00
MengYX 04be04204a
Use new window to open link 2019-11-10 01:30:11 +08:00
MengYX d99cd23e0c
Update Dependencies 2019-11-10 01:16:08 +08:00
MengYX c9770bdd59
Add .qmcogg (without test)
Add Tips
2019-11-10 00:59:13 +08:00
MengYX 757d4d4847
Fix Display Bugs In Edge and Safari 2019-09-21 23:32:21 +08:00
MengYX 0913337612
Enhanced ease of use 2019-09-18 00:21:18 +08:00
MengYX 23ff9cdec1
Add Analytics
Fix an error statement
2019-09-14 20:48:57 +08:00
MengYX 0b0b19163b
Complete ID3 for ncm 2019-09-12 15:51:10 +08:00
MengYX 382a637a2c
Fix an icon error 2019-09-08 15:06:08 +08:00
MengYX 82e4ec6312
Merge pull request #2 from ix64/pull/1
Fix bugs after using music-metadata-browser
2019-09-08 14:50:39 +08:00
MengYX 0e59843944
Merge branch 'master' into pull/1 2019-09-08 14:49:03 +08:00
MengYX c7ed517ede
Merge pull request #1 from Borewit/music-metadata-browser
Maybe try music-metadata-browser?
2019-09-08 14:45:23 +08:00
Borewit bd377db39b Fix parsing picture in metadata 2019-09-08 08:28:25 +02:00
MengYX 32128ed425
Fix bugs after using music-metadata-browser 2019-09-08 13:40:32 +08:00
Borewit 9416ded167 use music-metadata-browser 2019-09-07 19:50:04 +02:00
MengYX 1ba40d1fc2
Change icon 2019-08-25 15:55:36 +08:00
MengYX 76c0577185
Downgrade jsmediatag to avoid bug
Rename project
2019-08-25 15:16:20 +08:00
MengYX 3ceb56900d
Update page footer 2019-08-25 14:44:27 +08:00
MengYX e0ffd3f477
Update readme 2019-08-25 14:31:38 +08:00
MengYX 94136ec2e6
Update readme 2019-08-25 14:27:11 +08:00
MengYX 428a4505ad
Merge branch 'master' of https://git.ixarea.com/MusicCrack/music-crack 2019-08-25 14:12:48 +08:00
MengYX 6ed0291e54
Update dependencies to fix CVE-2019-10744 2019-08-25 14:10:33 +08:00
123 changed files with 36374 additions and 5912 deletions

25
.drone.yml Normal file
View File

@ -0,0 +1,25 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: build
image: node:16.18-bullseye
commands:
- apt-get update
- apt-get install -y jq zip
- npm ci
- npm run test
- ./scripts/build-and-package.sh legacy
- ./scripts/build-and-package.sh extension
- ./scripts/build-and-package.sh modern
- name: upload artifact
image: node:16.18-bullseye
environment:
DRONE_GITEA_SERVER: https://git.unlock-music.dev
GITEA_API_KEY:
from_secret: GITEA_API_KEY
commands:
- ./scripts/upload-packages.sh

13
.gitignore vendored
View File

@ -1,6 +1,8 @@
.DS_Store
node_modules
/dist
/build
/coverage
# local env files
.env.local
@ -19,3 +21,14 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
/src/KgmWasm/build
/src/KgmWasm/*.js
/src/KgmWasm/*.wasm
/src/QmcWasm/build
/src/QmcWasm/*.js
/src/QmcWasm/*.wasm
*.zip
*.tar.gz
/sha256sum.txt

47
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,47 @@
image: node:16
cache:
paths:
- node_modules/
stages:
- build
build-job:
stage: build
script: |
sed -i 's/deb.debian.org/mirrors.cloud.tencent.com/g' /etc/apt/sources.list
apt-get update
apt-get -y install zip
npm config set registry http://mirrors.cloud.tencent.com/npm/
npm ci
npm run build
tar -czf legacy.tar.gz -C ./dist .
cd dist
zip -rJ9 ../legacy.zip *
cd ..
npm run make-extension
cd dist
zip -rJ9 ../extension.zip *
cd ..
npm run build -- --modern
tar -czf modern.tar.gz -C ./dist .
cd dist
zip -rJ9 ../modern.zip *
cd ..
sha256sum *.tar.gz *.zip > sha256sum.txt
artifacts:
name: "$CI_JOB_NAME"
paths:
- legacy.zip
- legacy.tar.gz
- extension.zip
- modern.zip
- modern.tar.gz
- sha256sum.txt

View File

@ -0,0 +1,76 @@
name: 解码错误报告 (填表)
about: 遇到文件解码失败的问题请选择该项。
title: '[Bug/Crypto] '
labels:
- bug
- crypto
body:
- type: textarea
id: what-happened
attributes:
label: 错误描述
description: 请描述你所遇到的问题,以及你期待的行为。
placeholder: ''
value: ''
validations:
required: true
- type: dropdown
id: version
attributes:
label: Unlock Music 版本
description: |
能够重现错误的版本,版本号通常在页面底部。
如果不确定,请升级到最新版确认问题是否解决。
multiple: true
options:
- 1.10.5 (仓库最新)
- 1.10.3 (官方 DEMO)
- 其它(请在错误描述中指定)
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: 产生错误的浏览器
multiple: true
options:
- 火狐 / Firefox
- Chrome
- Safari
- 其它基于 Chromium 的浏览器 (Edge、Brave、Opera 等)
- type: dropdown
id: music-platform
attributes:
label: 音乐平台
description: |
如果需要报告多个平台的问题,请每个平台提交一个新的 Issue。
请注意:播放器缓存文件不属于该项目支持的文件类型。
multiple: false
options:
- 其它 (请在错误描述指定)
- QQ 音乐
- Joox (QQ 音乐海外版)
- 虾米音乐
- 网易云音乐
- 酷我音乐
- 酷狗音乐
- 喜马拉雅
- 咪咕 3D
validations:
required: true
- type: textarea
id: logs
attributes:
label: 日志信息
description: 如果有请提供浏览器开发者控制台Console的错误日志
render: text
- type: checkboxes
id: terms
attributes:
label: 我已经阅读并确认下述内容
description: ''
options:
- label: 我已经检索过 Issue 列表,并确认这是一个为报告过的问题。
required: true
- label: 我有证据表明这是程序导致的问题(如不确认,可以通过 Telegram 讨论组 (https://t.me/unlock_music_chat) 进行讨论)
required: true

View File

@ -0,0 +1,40 @@
---
name: "错误报告"
about: "报告 Bug 以帮助改进程序,非填表。"
title: "[BUG] "
labels:
- bug
---
* 请按照此模板填写,否则可能立即被关闭
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
- [x] 我有证据表明这是程序导致的问题(如不确认,可以通过 Telegram 讨论组 (https://t.me/unlock_music_chat) 进行讨论)
## Bug描述
简要地复述你遇到的Bug
## 复现方法
描述复现方法,必要时请提供样本文件
## 程序截图或浏览器开发者控制台Console的报错信息
如果可以请提供二者之一
## 环境信息
- 操作系统和浏览器:
- 程序版本:
- 网页版的地址(如果为非官方部署请注明):
注意:如果需要会员才能获取该资源,你可能也需要作为附件提交。
## 附加信息
如果有,请提供其他能够帮助确认问题的信息到下方:

View File

@ -0,0 +1,29 @@
---
name: "新功能"
about: "对于程序新的想法或建议"
title: "[新功能] "
labels:
- enhancement
---
<!-- ⚠ 请按照此模板填写,否则可能立即被关闭 -->
<!-- 提交前请使用【Preview】预览提交的更改 -->
## 背景和说明
<!-- 简要说明产生此想法的背景和此想法的具体内容 -->
## 实现途径
- 如果没有设计方案,请简要描述实现思路
- 如果你没有任何的实现思路,请通过 Telegram 讨论组 (https://t.me/unlock_music_chat) 进行讨论
## 附加信息
<!-- 更多你想要表达的内容 -->

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v16.18.1

42
.prettierrc.js Normal file
View File

@ -0,0 +1,42 @@
// .prettierrc.js
module.exports = {
// 一行最多 120 字符
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾需要有逗号
trailingComma: 'all',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
bracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// vue 文件中的 script 和 style 内不用缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf
endOfLine: 'lf',
// 格式化嵌入的内容
embeddedLanguageFormatting: 'auto',
};

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM --platform=$TARGETPLATFORM nginx:stable-alpine
LABEL org.opencontainers.image.title="Unlock Music"
LABEL org.opencontainers.image.description="Unlock encrypted music file in browser"
LABEL org.opencontainers.image.authors="MengYX"
LABEL org.opencontainers.image.source="https://github.com/ix64/unlock-music"
LABEL org.opencontainers.image.licenses="MIT"
LABEL maintainer="MengYX"
COPY ./dist /usr/share/nginx/html

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) [2019] [MengYX]
Copyright (c) 2019-2023 MengYX
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,29 +1,77 @@
# music-crack
# Unlock Music 音乐解锁
## Project setup
```
npm install
```
[![Build Status](https://ci.unlock-music.dev/api/badges/um/web/status.svg)](https://ci.unlock-music.dev/um/web)
### Compiles and hot-reloads for development
```
npm run serve
```
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。
- 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入!
- CI 自动构建已经部署,可以在 [UM-Packages] 下载
### Compiles and minifies for production
```
npm run build
```
[授权协议]: https://git.unlock-music.dev/um/web/src/branch/master/LICENSE
[unlock-music/cli]: https://git.unlock-music.dev/um/cli
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
[UM-Packages]: https://git.unlock-music.dev/um/-/packages/generic/web-build/
### Run your tests
```
npm run test
```
## 特性
### Lints and fixes files
```
npm run lint
```
### 支持的格式
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
- [x] QQ 音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/.tkm)
- [x] Moo 音乐格式 (.bkcmp3/.bkcflac/...)
- [x] QQ 音乐 Tm 格式 (.tm0/.tm2/.tm3/.tm6)
- [x] QQ 音乐新格式 (.mflac/.mgg/.mflac0/.mgg1/.mggl)
- [x] <ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (.ofl_en)
- [x] 网易云音乐格式 (.ncm)
- [x] 虾米音乐格式 (.xm)
- [x] 酷我音乐格式 (.kwm)
- [x] 酷狗音乐格式 (.kgm/.vpr)
- [x] Android版喜马拉雅文件格式 (.x2m/.x3m)
- [x] 咪咕音乐格式 (.mg3d)
### 其他特性
- [x] 在浏览器中解锁
- [x] 拖放文件
- [x] 批量解锁
- [x] 渐进式 Web 应用 (PWA)
- [x] 多线程
- [x] 写入和编辑元信息与专辑封面
## 使用方法
### 使用预构建版本
- 从 [Release] 或 [CI 构建][UM-Packages] 下载预构建的版本
- :warning: 本地使用请下载`legacy版本``modern版本`只能通过 **http(s)协议** 访问)
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**
[release]: https://git.unlock-music.dev/um/web/releases/latest
### 自行构建
- 环境要求
- nodejs (v16.x)
- npm
1. 获取项目源代码后安装相关依赖:
```sh
npm install
npm ci
```
2. 然后进行构建:
```sh
npm run build
```
- 构建后的产物可以在 `dist` 目录找到。
- 如果是用于开发,可以执行 `npm run serve`
3. 如需构建浏览器扩展,构建成功后还需要执行:
```sh
npm run make-extension
```

View File

@ -1,5 +1,12 @@
module.exports = {
presets: [
'@vue/app'
]
}
presets: [
'@vue/app',
'@babel/preset-typescript'
],
plugins: [
["component", {
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}]
]
};

18
extension-manifest.json Normal file
View File

@ -0,0 +1,18 @@
{
"manifest_version": 2,
"name": "音乐解锁",
"short_name": "音乐解锁",
"icons": {
"128": "./img/icons/msapplication-icon-144x144.png"
},
"description": "在任何设备上解锁已购的加密音乐!",
"permissions": [
"storage"
],
"offline_enabled": true,
"options_page": "./index.html",
"homepage_url": "https://git.unlock-music.dev/um/web",
"browser_action": {
"default_popup": "./popup.html"
}
}

7
jest.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
testPathIgnorePatterns: ['/build/', '/dist/', '/node_modules/'],
setupFilesAfterEnv: ['./src/__test__/setup_jest.js'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
};

25
make-extension.js Normal file
View File

@ -0,0 +1,25 @@
const fs = require('fs')
const path = require('path')
const src = __dirname + "/src/extension/"
const dst = __dirname + "/dist"
fs.readdirSync(src).forEach(file => {
let srcPath = path.join(src, file)
let dstPath = path.join(dst, file)
fs.copyFileSync(srcPath, dstPath)
console.log(`Copy: ${srcPath} => ${dstPath}`)
})
const manifestRaw = fs.readFileSync(__dirname + "/extension-manifest.json", "utf-8")
const manifest = JSON.parse(manifestRaw)
const pkgRaw = fs.readFileSync(__dirname + "/package.json", "utf-8")
const pkg = JSON.parse(pkgRaw)
verExt = pkg["version"]
if (verExt.startsWith("v")) verExt = verExt.slice(1)
if (verExt.includes("-")) verExt = verExt.split("-")[0]
manifest["version"] = `${verExt}.${pkg["ext_build"]}`
manifest["version_name"] = pkg["version"]
fs.writeFileSync(__dirname + "/dist/manifest.json", JSON.stringify(manifest), "utf-8")
console.log("Write: manifest.json")

36936
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,60 @@
{
"name": "music-crack",
"version": "0.1.0",
"name": "unlock-music",
"version": "1.10.6",
"ext_build": 0,
"updateInfo": "修正文件过小的情况下酷狗 / QQ解密错误问题",
"license": "MIT",
"description": "Unlock encrypted music file in browser.",
"repository": {
"type": "git",
"url": "https://git.unlock-music.dev/um/web"
},
"private": true,
"scripts": {
"postinstall": "patch-package",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
"build": "vue-cli-service build",
"test": "jest",
"pretty": "prettier --write src/{**/*,*}.{js,ts,jsx,tsx,vue}",
"pretty:check": "prettier --check src/{**/*,*}.{js,ts,jsx,tsx,vue}",
"make-extension": "node ./make-extension.js"
},
"dependencies": {
"browser-id3-writer": "^4.1.0",
"core-js": "^2.6.5",
"crypto-js": "^3.1.9-1",
"element-ui": "^2.4.5",
"jsmediatags": "^3.9.1",
"register-service-worker": "^1.6.2",
"vue": "^2.6.10"
"@babel/preset-typescript": "^7.16.5",
"@unlock-music/joox-crypto": "^0.0.1-R5",
"@xhacker/kgmwasm": "^1.0.0",
"@xhacker/qmcwasm": "^1.0.0",
"base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0",
"core-js": "^3.16.0",
"crypto-js": "^4.1.1",
"element-ui": "^2.15.5",
"iconv-lite": "^0.6.3",
"jimp": "^0.16.1",
"metaflac-js": "^1.0.5",
"music-metadata": "7.9.0",
"music-metadata-browser": "2.2.7",
"register-service-worker": "^1.7.2",
"threads": "^1.6.5",
"vue": "^2.6.14"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.9.0",
"@vue/cli-plugin-pwa": "^3.9.0",
"@vue/cli-service": "^3.9.0",
"@types/crypto-js": "^4.0.2",
"@types/jest": "^27.0.3",
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-pwa": "^4.5.13",
"@vue/cli-plugin-typescript": "^4.5.13",
"@vue/cli-service": "^4.5.13",
"babel-plugin-component": "^1.1.1",
"jest": "^27.4.5",
"patch-package": "^6.4.7",
"prettier": "2.5.1",
"sass": "^1.38.1",
"sass-loader": "^10.2.0",
"semver": "^7.3.5",
"threads-plugin": "^1.4.0",
"typescript": "^4.5.4",
"vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.10"
"vue-template-compiler": "^2.6.14"
}
}

View File

@ -0,0 +1,11 @@
diff --git a/node_modules/threads/worker.mjs b/node_modules/threads/worker.mjs
index c53ac7d..619007b 100644
--- a/node_modules/threads/worker.mjs
+++ b/node_modules/threads/worker.mjs
@@ -1,4 +1,5 @@
-import WorkerContext from "./dist/worker/index.js"
+// Workaround: use of import seems to break minifier.
+const WorkerContext = require("./dist/worker/index.js")
export const expose = WorkerContext.expose
export const registerSerializer = WorkerContext.registerSerializer

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,149 +1,17 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
fill="#000000" stroke="none">
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
-9615 0 20 -32z"/>
</g>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" t="1566718842150" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1244" width="16" height="16">
<defs>
<style type="text/css"></style>
</defs>
<path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#2674FD" p-id="1245"></path>
<path d="M512 512m-425.57245 0a425.57245 425.57245 0 1 0 851.1449 0 425.57245 425.57245 0 1 0-851.1449 0Z"
fill="#FFFFFF" p-id="1246"></path>
<path d="M512 512m-214.271074 0a214.271074 214.271074 0 1 0 428.542148 0 214.271074 214.271074 0 1 0-428.542148 0Z"
fill="#FFE41F" p-id="1247"></path>
<path d="M635.968268 408.15377l-89.224127-8.722657a15.980441 15.980441 0 0 0-16.859365 11.479283l-1.784482 6.28564a22.372617 22.372617 0 0 0-2.237262 5.193643l-26.088069 91.754363a67.410825 67.410825 0 1 0 12.984108 61.498063c0.332926-1.185216 0.639218-2.370432 0.905558-3.555648h0.093219l33.106147-116.457461 48.527271-1.891019a48.84688 48.84688 0 0 0 37.767108-20.308477l8.735974-12.158452a8.336463 8.336463 0 0 0-5.92608-13.117278z"
fill="#FFFFFF" p-id="1248"></path>
<path d="M214.231123 503.383879c4.527792-160.563477 136.113403-289.339194 297.768877-289.339194s293.241085 128.775717 297.768877 289.339194h214.151221C1019.339038 224.61841 791.910734 0 512 0S4.647645 224.61841 0.079902 503.383879z"
fill="#2674FD" p-id="1249"></path>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,69 +1,89 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>音乐解锁 - By IXarea</title>
<meta content="音乐,解锁,ncm,qmc,qmc0,qmc3,qmcflac,qq音乐,网易云音乐,加密" name="keywords"/>
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description"/>
<head>
<meta charset="utf-8" />
<meta content="webkit" name="renderer" />
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
<meta content="width=device-width,initial-scale=1.0" name="viewport" />
<title>音乐解锁</title>
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords" />
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description" />
<style>
/* Center the loader */
#loader {
position: absolute;
left: 50%;
top: 50%;
z-index: 1010;
margin: -75px 0 0 -75px;
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
#loader {
position: absolute;
left: 50%;
top: 50%;
z-index: 1010;
margin: -75px 0 0 -75px;
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #1db1ff;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
100% {
transform: rotate(360deg);
}
}
#loader-mask {
text-align: center;
position: absolute;
width: 100%;
height: 100%;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1009;
background-color: rgba(242, 246, 252, 0.88);
}
@media (prefers-color-scheme: dark) {
#loader-mask {
position: absolute;
width: 100%;
height: 100%;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1009;
background-color: rgba(242, 246, 252, 0.88);
color: #fff;
background-color: rgba(0, 0, 0, 0.85);
}
#loader-mask a {
color: #ddd;
}
#loader-mask a:hover {
color: #1db1ff;
}
}
#loader-source {
font-size: 1.5rem;
}
#loader-tips-timeout {
font-size: 1.2rem;
}
</style>
</head>
<body>
</head>
<div id="loader-mask">
<div id="loader"></div>
<noscript>
<strong>很抱歉音乐解锁需要启用JavaScript的现代浏览器
<a href="https://www.google.cn/chrome/">Google Chrome</a>
<a href="https://www.firefox.com.cn/">Mozilla Firefox</a>
</strong>
</noscript>
<script>
window.onload = function () {
document.getElementById("loader-mask").remove();
};
</script>
</div>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
<body>
<div id="loader-mask">
<div id="loader"></div>
<noscript>
<h3 id="loader-js">请启用JavaScript</h3>
</noscript>
<h3 id="loader-source">请勿直接运行源代码!</h3>
<div id="loader-tips-outdated" hidden>
<h2>您可能在使用不受支持的<span style="color: #f00">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
<h3>如果您使用双核浏览器,您可以尝试切换到 <span style="color: #f00">“极速模式”</span> 解决此问题。</h3>
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
</div>
<h3 id="loader-tips-timeout" hidden>
音乐解锁采用了一些新特性!建议使用
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
| <a href="https://git.unlock-music.dev/um/web/wiki/使用提示" target="_blank">使用提示</a>
</h3>
</div>
<div id="app"></div>
<script src="./loader.js"></script>
</body>
</html>

25
public/loader.js Normal file
View File

@ -0,0 +1,25 @@
(function () {
setTimeout(function () {
var ele = document.getElementById("loader-tips-timeout");
if (ele != null) {
ele.hidden = false;
}
}, 2000);
var ua = navigator && navigator.userAgent;
var detected = (function () {
var m;
if (!ua) return true;
if (/MSIE |Trident\//.exec(ua)) return true; // no IE
m = /Edge\/([\d.]+)/.exec(ua); // Edge >= 17
if (m && Number(m[1]) < 17) return true;
m = /Chrome\/([\d.]+)/.exec(ua); // Chrome >= 58
if (m && Number(m[1]) < 58) return true;
m = /Firefox\/([\d.]+)/.exec(ua); // Firefox >= 45
return m && Number(m[1]) < 45;
})();
if (detected) {
document.getElementById('loader-tips-outdated').hidden = false;
document.getElementById("loader-tips-timeout").hidden = false;
}
})();

View File

@ -1,21 +0,0 @@
{
"name": "音乐解锁 - By IXarea",
"short_name": "音乐解锁",
"description": "在任何设备上解锁已购的加密音乐支持QQ音乐与网易云音乐",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}

29
scripts/build-and-package.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/sh
set -ex
cd "$(git rev-parse --show-toplevel)"
VERSION="$(jq -r ".version" <package.json)"
DIST_NAME="um-web.$1.v${VERSION}"
case "$1" in
"modern") npm run build -- --modern ;;
"legacy") npm run build ;;
"extension") npm run make-extension ;;
*)
echo "Unknown command: $1"
exit 1
;;
esac
mv dist "${DIST_NAME}"
zip -rJ9 "${DIST_NAME}.zip" "${DIST_NAME}"
if [ "$1" = "legacy" ]; then
# For upcoming extension build
mv "${DIST_NAME}" dist
else
rm -rf "${DIST_NAME}"
fi

19
scripts/upload-packages.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
set -ex
cd "$(git rev-parse --show-toplevel)"
if [ -z "$GITEA_API_KEY" ]; then
echo "GITEA_API_KEY is empty, skip upload."
exit 0
fi
URL_BASE="$DRONE_GITEA_SERVER/api/packages/${DRONE_REPO_NAMESPACE}/generic/${DRONE_REPO_NAME}-build"
for ZIP_NAME in *.zip; do
UPLOAD_URL="${URL_BASE}/${DRONE_BUILD_NUMBER}/${ZIP_NAME}"
sha256sum "${ZIP_NAME}"
curl -sLifu "um-release-bot:$GITEA_API_KEY" -T "${ZIP_NAME}" "${UPLOAD_URL}"
echo "Uploaded to: ${UPLOAD_URL}"
done

View File

@ -1,227 +1,94 @@
<template>
<div id="app">
<el-container>
<el-main>
<el-upload
:auto-upload="false"
:on-change="handleFile"
:show-file-list="false"
action=""
drag
multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击选择</em></div>
<div class="el-upload__tip" slot="tip">本工具仅在浏览器内对文件进行解锁无需消耗流量</div>
</el-upload>
<el-row id="app-control">
<el-button @click="handleDownloadAll" icon="el-icon-download" plain>下载全部</el-button>
<el-button @click="handleDeleteAll" icon="el-icon-download" plain type="danger">删除全部</el-button>
</el-row>
<audio :autoplay="playing_auto" :src="playing_url" controls></audio>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="图片">
<template slot-scope="scope">
<el-image :src="scope.row.picture" style="width: 100px; height: 100px"></el-image>
</template>
</el-table-column>
<el-table-column label="歌曲" sortable>
<template slot-scope="scope">
<span style="margin-left: 10px">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="歌手" sortable>
<template slot-scope="scope">
<p>{{ scope.row.artist }}</p>
</template>
</el-table-column>
<el-table-column label="专辑" sortable>
<template slot-scope="scope">
<p>{{ scope.row.album }}</p>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button @click="handlePlay(scope.$index, scope.row)"
circle icon="el-icon-video-play" type="success">
</el-button>
<el-button circle>
<el-link :download="scope.row.filename" :href="scope.row.file"
:underline="false" icon="el-icon-download">
</el-link>
</el-button>
<el-button @click="handleDelete(scope.$index, scope.row)"
circle icon="el-icon-delete" type="danger">
</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
<el-footer id="app-footer">
<el-row>
音乐解锁移除已购音乐的加密保护
目前支持网易云音乐(ncm)和QQ音乐(qmc0, qmc3, qmcflac)
</el-row>
<el-row>
<span>Copyright &copy; 2019</span>
<a href="https://ixarea.com" target="_blank">IXarea</a>
<span>and</span>
<a href="https://github.com/ix64" target="_blank">MengYX</a>
</el-row>
</el-footer>
</el-container>
</div>
<el-container id="app">
<el-main>
<Home />
</el-main>
<el-footer id="app-footer">
<div>
<a href="https://git.unlock-music.dev/um/web" target="_blank">音乐解锁</a>({{ version }})
移除已购音乐的加密保护
<a href="https://git.unlock-music.dev/um/web/wiki/使用提示" target="_blank">使用提示</a>
</div>
<div>
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
<a href="https://git.unlock-music.dev/um/web/src/branch/master/README.md" target="_blank">更多</a>
</div>
<div>
<!--如果进行二次开发此行版权信息不得移除且应明显地标注于页面上-->
<span>Copyright &copy; 2019 - {{ new Date().getFullYear() }} MengYX</span>
音乐解锁使用
<a href="https://git.unlock-music.dev/um/web/src/branch/master/LICENSE" target="_blank">MIT许可协议</a>
开放源代码
</div>
</el-footer>
</el-container>
</template>
<script>
import FileSelector from '@/component/FileSelector';
import PreviewTable from '@/component/PreviewTable';
import config from '@/../package.json';
import Home from '@/view/Home';
import { checkUpdate } from '@/utils/api';
const NcmDecrypt = require("./plugins/ncm");
const QmcDecrypt = require("./plugins/qmc");
const RawDecrypt = require("./plugins/raw");
export default {
name: 'app',
components: {},
data() {
return {
activeIndex: '1',
tableData: [],
playing_url: "",
playing_auto: false,
}
},
mounted() {
this.$nextTick(function () {
this.finishLoad();
});
},
methods: {
finishLoad() {
this.$notify.info({
title: '离线使用',
message: "音乐解锁加载成功。我们使用PWA技术可以添加到桌面或收藏夹无网络状况下也能使用。",
duration: 30000,
position: 'top-left'
});
},
handleFile(file) {
let ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
(async () => {
let data = null;
switch (ext) {
case "ncm":
data = await NcmDecrypt.Decrypt(file.raw);
break;
case "mp3":
case "flac":
data = await RawDecrypt.Decrypt(file.raw);
break;
case "qmc3":
case "qmc0":
case "qmcflac":
data = await QmcDecrypt.Decrypt(file.raw);
break;
default:
break;
}
if (null != data) {
this.tableData.push(data);
this.$notify.success({
title: '解锁成功',
message: '成功解锁 ' + data.title
});
} else {
this.$notify.error({
title: '错误',
message: '不支持此文件类型'
});
}
})();
},
handlePlay(index, row) {
this.playing_url = row.file;
this.playing_auto = true;
},
handleDelete(index, row) {
console.log(index);
URL.revokeObjectURL(row.file);
URL.revokeObjectURL(row.picture);
this.tableData.splice(index, 1);
},
handleDeleteAll() {
this.tableData.forEach(value => {
URL.revokeObjectURL(value.file);
URL.revokeObjectURL(value.picture);
});
this.tableData = [];
},
handleDownloadAll() {
let index = 0;
let c = setInterval(() => {
if (index < this.tableData.length) {
let a = document.createElement('a');
a.href = this.tableData[index].file;
a.download = this.tableData[index].filename;
document.body.append(a);
a.click();
a.remove();
index++;
} else {
clearInterval(c);
}
}, 1000);
}
}
}
export default {
name: 'app',
components: {
FileSelector,
PreviewTable,
Home,
},
data() {
return {
version: config.version,
};
},
created() {
this.$nextTick(() => this.finishLoad());
},
methods: {
async finishLoad() {
const mask = document.getElementById('loader-mask');
if (!!mask) mask.remove();
let updateInfo;
try {
updateInfo = await checkUpdate(this.version);
} catch (e) {
console.warn('check version info failed', e);
}
if (
updateInfo &&
process.env.NODE_ENV === 'production' &&
(updateInfo.HttpsFound || (updateInfo.Found && window.location.protocol !== 'https:'))
) {
this.$notify.warning({
title: '发现更新',
message: `发现新版本 v${updateInfo.Version}<br/>更新详情:${updateInfo.Detail}<br/> <a target="_blank" href="${updateInfo.URL}">获取更新</a>`,
dangerouslyUseHTMLString: true,
duration: 15000,
position: 'top-left',
});
} else {
this.$notify.info({
title: '离线使用',
message: `<div>
<p>我们使用 PWA 技术无网络也能使用</p>
<div class="update-info">
<div class="update-title">最近更新</div>
<div class="update-content"> ${config.updateInfo} </div>
</div>
<a target="_blank" href="https://git.unlock-music.dev/um/web/wiki/使用提示">使用提示</a>
</div>`,
dangerouslyUseHTMLString: true,
duration: 10000,
position: 'top-left',
});
}
},
},
};
</script>
<style>
#app {
font-family: "Helvetica Neue", Helvetica, "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
padding-top: 30px;
}
#app-footer a {
padding-left: 0.5em;
padding-right: 0.5em;
}
#app-footer {
text-align: center;
font-size: small;
}
.el-upload-dragger {
width: 80vw !important;
}
#app-control {
padding-top: 1em;
padding-bottom: 1em;
}
<style lang="scss">
@import 'scss/unlock-music';
</style>

View File

@ -0,0 +1,2 @@
// Polyfill for node.
global.Blob = global.Blob || require("node:buffer").Blob;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,105 @@
<style scoped>
label {
cursor: pointer;
line-height: 1.2;
display: block;
}
form >>> input {
font-family: 'Courier New', Courier, monospace;
}
* >>> .um-config-dialog {
max-width: 90%;
width: 40em;
}
</style>
<template>
<el-dialog @close="cancel()" title="解密设定" :visible="show" custom-class="um-config-dialog" center>
<el-form ref="form" :rules="rules" status-icon :model="form" label-width="0">
<section>
<label>
<span>
JOOX Music ·
<Ruby caption="Unique Device Identifier">设备唯一识别码</Ruby>
</span>
<el-form-item prop="jooxUUID">
<el-input type="text" v-model="form.jooxUUID" clearable maxlength="32" show-word-limit> </el-input>
</el-form-item>
</label>
<p class="tip">
下载该加密文件的 JOOX 应用所记录的设备唯一识别码
<br />
参见
<a
href="https://git.unlock-music.dev/um/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID#%E5%89%8D%E8%A8%80"
>
获取设备 UUID · unlock-music/joox-crypto Wiki</a
>
</p>
</section>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" :loading="saving" @click="emitConfirm()"> </el-button>
</span>
</el-dialog>
</template>
<script>
import { storage } from '@/utils/storage';
import Ruby from './Ruby';
// FIXME:
function validateJooxUUID(rule, value, callback) {
if (!value || !/^[\da-fA-F]{32}$/.test(value)) {
callback(new Error('无效的 Joox UUID请参考 Wiki 获取。'));
} else {
callback();
}
}
const rules = {
jooxUUID: { validator: validateJooxUUID, trigger: 'change' },
};
export default {
components: {
Ruby,
},
props: {
show: { type: Boolean, required: true },
},
data() {
return {
rules,
saving: false,
form: {
jooxUUID: '',
},
centerDialogVisible: false,
};
},
async mounted() {
await this.resetForm();
},
methods: {
async resetForm() {
this.form.jooxUUID = await storage.loadJooxUUID();
},
async cancel() {
await this.resetForm();
this.$emit('done');
},
async emitConfirm() {
this.saving = true;
await storage.saveJooxUUID(this.form.jooxUUID);
this.saving = false;
this.$emit('done');
},
},
};
</script>

View File

@ -0,0 +1,163 @@
<style scoped>
* >>> .um-edit-dialog {
max-width: 90%;
width: 30em;
}
</style>
<template>
<el-dialog @close="cancel()" title="音乐标签编辑" :visible="show" custom-class="um-edit-dialog" center>
<el-form ref="form" status-icon :model="form" label-width="0">
<section>
<div class="music-cover">
<el-image v-show="!editPicture" :src="imgFile.url || picture">
<div slot="error" class="image-slot el-image__error">暂无封面</div>
</el-image>
<el-upload v-show="editPicture" :auto-upload="false" :on-change="addFile" :on-remove="rmvFile" :show-file-list="true" :limit="1" list-type="picture" action="" drag>
<i class="el-icon-upload" />
<div class="el-upload__text">将新图片拖到此处<em>点击选择</em><br />以替换自动匹配的图片</div>
<div slot="tip" class="el-upload__tip">
新拖到此处的图片将覆盖原始图片
</div>
</el-upload>
<i :class="{'el-icon-edit': !editPicture, 'el-icon-check': editPicture}"
@click="changeCover"></i>
</div>
<div class="edit-item">
<div class="label">标题</div>
<div class="value" v-show="!editTitle">{{title}}</div>
<el-input class="input" size="small" v-show="editTitle" v-model="title"/>
<i :class="{'el-icon-edit': !editTitle, 'el-icon-check': editTitle}"
@click="editTitle = !editTitle"/>
</div>
<div class="edit-item">
<div class="label">艺术家</div>
<div class="value" v-show="!editArtist">{{artist}}</div>
<el-input class="input" size="small" v-show="editArtist" v-model="artist"/>
<i :class="{'el-icon-edit': !editArtist, 'el-icon-check': editArtist}"
@click="editArtist = !editArtist"
/>
</div>
<div class="edit-item">
<div class="label">专辑</div>
<div class="value" v-show="!editAlbum">{{album}}</div>
<el-input class="input" size="small" v-show="editAlbum" v-model="album"/>
<i :class="{'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum}"
@click="editAlbum = !editAlbum"
/>
</div>
<div class="edit-item">
<div class="label">专辑艺术家</div>
<div class="value" v-show="!editAlbumartist">{{albumartist}}</div>
<el-input class="input" size="small" v-show="editAlbumartist" v-model="albumartist"/>
<i :class="{'el-icon-edit': !editAlbumartist, 'el-icon-check': editAlbumartist}"
@click="editAlbumartist = !editAlbumartist"
/>
</div>
<div class="edit-item">
<div class="label">风格</div>
<div class="value" v-show="!editGenre">{{genre}}</div>
<el-input class="input" size="small" v-show="editGenre" v-model="genre"/>
<i :class="{'el-icon-edit': !editGenre, 'el-icon-check': editGenre}"
@click="editGenre = !editGenre"
/>
</div>
<p class="tip">
为了节省您设备的资源请在确定前充分检查避免反复修改<br />
直接关闭此对话框不会保留所作的更改
</p>
</section>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="emitConfirm()"> </el-button>
</span>
</el-dialog>
</template>
<script>
import Ruby from './Ruby';
export default {
components: {
Ruby,
},
props: {
show: { type: Boolean, required: true },
picture: { type: String | undefined, required: true },
title: { type: String | undefined, required: true },
artist: { type: String | undefined, required: true },
album: { type: String | undefined, required: true },
albumartist: { type: String | undefined, required: true },
genre: { type: String | undefined, required: true },
},
data() {
return {
form: {
},
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
editPicture: false,
editTitle: false,
editArtist: false,
editAlbum: false,
editAlbumartist: false,
editGenre: false,
};
},
async mounted() {
this.refreshForm();
},
methods: {
addFile(file) {
this.imgFile.tmpblob = file.raw;
},
rmvFile() {
this.imgFile.tmpblob = undefined;
},
changeCover() {
this.editPicture = !this.editPicture;
if (!this.editPicture && this.imgFile.tmpblob) {
this.imgFile.blob = this.imgFile.tmpblob;
if (this.imgFile.url) {
URL.revokeObjectURL(this.imgFile.url);
}
this.imgFile.url = URL.createObjectURL(this.imgFile.blob);
}
},
async refreshForm() {
if (this.imgFile.url) {
URL.revokeObjectURL(this.imgFile.url);
}
this.imgFile = { tmpblob: undefined, blob: undefined, url: undefined };
this.editPicture = false;
this.editTitle = false;
this.editArtist = false;
this.editAlbum = false;
this.editAlbumartist = false;
this.editGenre = false;
},
async cancel() {
this.refreshForm();
this.$emit('cancel');
},
async emitConfirm() {
if (this.editPicture) {
this.changeCover();
}
if (this.imgFile.url) {
URL.revokeObjectURL(this.imgFile.url);
}
this.$emit('ok', {
picture: this.imgFile.blob,
title: this.title,
artist: this.artist,
album: this.album,
albumartist: this.albumartist,
genre: this.genre,
});
},
},
};
</script>

View File

@ -0,0 +1,91 @@
<template>
<el-upload :auto-upload="false" :on-change="addFile" :show-file-list="false" action="" drag multiple>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处 <em>点击选择</em></div>
<div slot="tip" class="el-upload__tip">
<div>
仅在浏览器内对文件进行解锁无需消耗流量
<el-tooltip effect="dark" placement="top-start">
<div slot="content">算法在源代码中已经提供所有运算都发生在本地</div>
<i class="el-icon-info" style="font-size: 12px" />
</el-tooltip>
</div>
<div>
工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
<el-tooltip effect="dark" placement="top-start">
<div slot="content">
将此工具部署在HTTPS环境下可以启用Web Worker特性<br />
从而更快的利用并行处理完成解锁
</div>
<i class="el-icon-info" style="font-size: 12px" />
</el-tooltip>
</div>
</div>
<transition name="el-fade-in"
><!--todo: add delay to animation-->
<el-progress
v-show="progress_show"
:format="progress_string"
:percentage="progress_value"
:stroke-width="16"
:text-inside="true"
style="margin: 16px 6px 0 6px"
></el-progress>
</transition>
</el-upload>
</template>
<script>
import { spawn, Worker, Pool } from 'threads';
import { Decrypt } from '@/decrypt';
import { DecryptQueue } from '@/utils/utils';
import { storage } from '@/utils/storage';
export default {
name: 'FileSelector',
data() {
return {
task_all: 0,
task_finished: 0,
queue: new DecryptQueue(), // for http or file protocol
parallel: false,
};
},
computed: {
progress_value() {
return this.task_all ? (this.task_finished / this.task_all) * 100 : 0;
},
progress_show() {
return this.task_all !== this.task_finished;
},
},
mounted() {
if (window.Worker && window.location.protocol !== 'file:' && process.env.NODE_ENV === 'production') {
console.log('Using Worker Pool');
this.queue = Pool(() => spawn(new Worker('@/utils/worker.ts')), navigator.hardwareConcurrency || 1);
this.parallel = true;
} else {
console.log('Using Queue in Main Thread');
}
},
methods: {
progress_string() {
return `${this.task_finished} / ${this.task_all}`;
},
async addFile(file) {
this.task_all++;
this.queue.queue(async (dec = Decrypt) => {
console.log('start handling', file.name);
try {
this.$emit('success', await dec(file, await storage.getAll()));
} catch (e) {
console.error(e);
this.$emit('error', e, file.name);
} finally {
this.task_finished++;
}
});
},
},
};
</script>

View File

@ -0,0 +1,66 @@
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="封面">
<template slot-scope="scope">
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
<div slot="error" class="image-slot el-image__error">暂无封面</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="歌曲">
<template #default="scope">
<p>{{ scope.row.title }}</p>
</template>
</el-table-column>
<el-table-column label="歌手">
<template #default="scope">
<p>{{ scope.row.artist }}</p>
</template>
</el-table-column>
<el-table-column label="专辑">
<template #default="scope">
<p>{{ scope.row.album }}</p>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
</el-button>
<el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button>
<el-button circle icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button>
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
import { RemoveBlobMusic } from '@/utils/utils';
export default {
name: 'PreviewTable',
props: {
tableData: { type: Array, required: true },
policy: { type: Number, required: true },
},
methods: {
handlePlay(index, row) {
this.$emit('play', row.file);
},
handleDelete(index, row) {
RemoveBlobMusic(row);
this.tableData.splice(index, 1);
},
handleDownload(row) {
this.$emit('download', row);
},
handleEdit(row) {
this.$emit('edit', row);
},
},
};
</script>
<style scoped></style>

18
src/component/Ruby.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<ruby :title="caption">
<slot></slot>
<rp></rp>
<rt v-text="caption"></rt>
<rp></rp>
</ruby>
</template>
<script>
export default {
name: 'Ruby',
props: {
caption: { type: String, required: true },
},
};
</script>

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,52 @@
import fs from 'fs';
import { storage } from '@/utils/storage';
import { Decrypt as decryptJoox } from '../joox';
import { extractQQMusicMeta as extractQQMusicMetaOrig } from '@/utils/qm_meta';
jest.mock('@/utils/storage');
jest.mock('@/utils/qm_meta');
const loadJooxUUID = storage.loadJooxUUID as jest.MockedFunction<typeof storage.loadJooxUUID>;
const extractQQMusicMeta = extractQQMusicMetaOrig as jest.MockedFunction<typeof extractQQMusicMetaOrig>;
const TEST_UUID_ZEROS = ''.padStart(32, '0');
const encryptedFile1 = fs.readFileSync(__dirname + '/fixture/joox_1.bin');
describe('decrypt/joox', () => {
it('should be able to decrypt sample file (v4)', async () => {
loadJooxUUID.mockResolvedValue(TEST_UUID_ZEROS);
extractQQMusicMeta.mockImplementationOnce(async (blob: Blob) => {
return {
title: 'unused',
album: 'unused',
blob: blob,
artist: 'unused',
imgUrl: 'https://example.unlock-music.dev/',
};
});
const result = await decryptJoox(new Blob([encryptedFile1]), 'test.bin', 'bin');
const resultBuf = await result.blob.arrayBuffer();
expect(resultBuf).toEqual(Buffer.from('Hello World', 'utf-8').buffer);
});
it('should reject E!99 files', async () => {
loadJooxUUID.mockResolvedValue(TEST_UUID_ZEROS);
const input = new Blob([Buffer.from('E!99....')]);
await expect(decryptJoox(input, 'test.bin', 'bin')).rejects.toThrow('不支持的 joox 加密格式');
});
it('should reject empty uuid', async () => {
loadJooxUUID.mockResolvedValue('');
const input = new Blob([encryptedFile1]);
await expect(decryptJoox(input, 'test.bin', 'bin')).rejects.toThrow('UUID');
});
it('should reject invalid uuid', async () => {
loadJooxUUID.mockResolvedValue('hello!');
const input = new Blob([encryptedFile1]);
await expect(decryptJoox(input, 'test.bin', 'bin')).rejects.toThrow('UUID');
});
});

25
src/decrypt/entity.ts Normal file
View File

@ -0,0 +1,25 @@
export interface DecryptResult {
title: string;
album?: string;
artist?: string;
mime: string;
ext: string;
file: string;
blob: Blob;
picture?: string;
message?: string;
rawExt?: string;
rawFilename?: string;
}
export interface FileInfo {
status: string;
name: string;
size: number;
percentage: number;
uid: number;
raw: File;
}

115
src/decrypt/index.ts Normal file
View File

@ -0,0 +1,115 @@
import { Decrypt as Mg3dDecrypt } from '@/decrypt/mg3d';
import { Decrypt as NcmDecrypt } from '@/decrypt/ncm';
import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache';
import { Decrypt as XmDecrypt } from '@/decrypt/xm';
import { Decrypt as QmcDecrypt } from '@/decrypt/qmc';
import { Decrypt as QmcCacheDecrypt } from '@/decrypt/qmccache';
import { Decrypt as KgmDecrypt } from '@/decrypt/kgm';
import { Decrypt as KwmDecrypt } from '@/decrypt/kwm';
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
import { Decrypt as TmDecrypt } from '@/decrypt/tm';
import { Decrypt as JooxDecrypt } from '@/decrypt/joox';
import { Decrypt as XimalayaDecrypt } from './ximalaya';
import { DecryptResult, FileInfo } from '@/decrypt/entity';
import { SplitFilename } from '@/decrypt/utils';
import { storage } from '@/utils/storage';
import InMemoryStorage from '@/utils/storage/InMemoryStorage';
export async function Decrypt(file: FileInfo, config: Record<string, any>): Promise<DecryptResult> {
// Worker thread will fallback to in-memory storage.
if (storage instanceof InMemoryStorage) {
await storage.setAll(config);
}
const raw = SplitFilename(file.name);
let rt_data: DecryptResult;
switch (raw.ext) {
case 'mg3d': // Migu Wav
rt_data = await Mg3dDecrypt(file.raw, raw.name);
break;
case 'ncm': // Netease Mp3/Flac
rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
break;
case 'uc': // Netease Cache
rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext);
break;
case 'kwm': // Kuwo Mp3/Flac
rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
break;
case 'xm': // Xiami Wav/M4a/Mp3/Flac
case 'wav': // Xiami/Raw Wav
case 'mp3': // Xiami/Raw Mp3
case 'flac': // Xiami/Raw Flac
case 'm4a': // Xiami/Raw M4a
rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
break;
case 'ogg': // Raw Ogg
rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
break;
case 'tm0': // QQ Music IOS Mp3
case 'tm3': // QQ Music IOS Mp3
rt_data = await RawDecrypt(file.raw, raw.name, 'mp3');
break;
case 'qmc0': //QQ Music Android Mp3
case 'qmc3': //QQ Music Android Mp3
case 'qmc2': //QQ Music Android Ogg
case 'qmc4': //QQ Music Android Ogg
case 'qmc6': //QQ Music Android Ogg
case 'qmc8': //QQ Music Android Ogg
case 'qmcflac': //QQ Music Android Flac
case 'qmcogg': //QQ Music Android Ogg
case 'tkm': //QQ Music Accompaniment M4a
// Moo Music
case 'bkcmp3':
case 'bkcm4a':
case 'bkcflac':
case 'bkcwav':
case 'bkcape':
case 'bkcogg':
case 'bkcwma':
// QQ Music v2
case 'mggl': //QQ Music Mac
case 'mflac': //QQ Music New Flac
case 'mflac0': //QQ Music New Flac
case 'mflach': //QQ Music New Flac
case 'mgg': //QQ Music New Ogg
case 'mgg1': //QQ Music New Ogg
case 'mgg0':
case 'mmp4': // QMC MP4 Container w/ E-AC-3 JOC
case '666c6163': //QQ Music Weiyun Flac
case '6d7033': //QQ Music Weiyun Mp3
case '6f6767': //QQ Music Weiyun Ogg
case '6d3461': //QQ Music Weiyun M4a
case '776176': //QQ Music Weiyun Wav
rt_data = await QmcDecrypt(file.raw, raw.name, raw.ext);
break;
case 'tm2': // QQ Music IOS M4a
case 'tm6': // QQ Music IOS M4a
rt_data = await TmDecrypt(file.raw, raw.name);
break;
case 'cache': //QQ Music Cache
rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
break;
case 'vpr':
case 'kgm':
case 'kgma':
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
break;
case 'ofl_en':
rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext);
break;
case 'x2m':
case 'x3m':
rt_data = await XimalayaDecrypt(file.raw, raw.name, raw.ext);
break;
case 'mflach': //QQ Music New Flac
throw '网页版无法解锁,请使用<a target="_blank" href="https://git.unlock-music.dev/um/cli">CLI版本</a>'
default:
throw '不支持此文件格式';
}
if (!rt_data.rawExt) rt_data.rawExt = raw.ext;
if (!rt_data.rawFilename) rt_data.rawFilename = raw.name;
console.log(rt_data);
return rt_data;
}

44
src/decrypt/joox.ts Normal file
View File

@ -0,0 +1,44 @@
import jooxFactory from '@unlock-music/joox-crypto';
import { DecryptResult } from './entity';
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from './utils';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
import { storage } from '@/utils/storage';
import { extractQQMusicMeta } from '@/utils/qm_meta';
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const uuid = await storage.loadJooxUUID('');
if (!uuid || uuid.length !== 32) {
throw new Error('请在“解密设定”填写应用 Joox 应用的 UUID。');
}
const fileBuffer = new Uint8Array(await GetArrayBuffer(file));
const decryptor = jooxFactory(fileBuffer, uuid);
if (!decryptor) {
throw new Error('不支持的 joox 加密格式');
}
const musicDecoded = MergeUint8Array(decryptor.decryptFile(fileBuffer));
const ext = SniffAudioExt(musicDecoded);
const mime = AudioMimeType[ext];
const songId = raw_filename.match(/^(\d+)\s\[mqms\d*]$/i)?.[1];
const { album, artist, imgUrl, blob, title } = await extractQQMusicMeta(
new Blob([musicDecoded], { type: mime }),
raw_filename,
ext,
songId,
);
return {
title: title,
artist: artist,
ext: ext,
album: album,
picture: imgUrl,
file: URL.createObjectURL(blob),
blob: blob,
mime: mime,
};
}

57
src/decrypt/kgm.ts Normal file
View File

@ -0,0 +1,57 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
} from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
import { DecryptResult } from '@/decrypt/entity';
import { DecryptKgmWasm } from '@/decrypt/kgm_wasm';
//prettier-ignore
const VprHeader = [
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31
]
//prettier-ignore
const KgmHeader = [
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14
]
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const oriData = await GetArrayBuffer(file);
if (raw_ext === 'vpr') {
if (!BytesHasPrefix(new Uint8Array(oriData), VprHeader)) throw Error('Not a valid vpr file!');
} else {
if (!BytesHasPrefix(new Uint8Array(oriData), KgmHeader)) throw Error('Not a valid kgm(a) file!');
}
let musicDecoded = new Uint8Array();
if (globalThis.WebAssembly) {
const kgmDecrypted = await DecryptKgmWasm(oriData, raw_ext);
if (kgmDecrypted.success) {
musicDecoded = kgmDecrypted.data;
console.log('kgm wasm decoder suceeded');
} else {
throw new Error(kgmDecrypted.error || '(unknown error)');
}
}
const ext = SniffAudioExt(musicDecoded);
const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], { type: mime });
const musicMeta = await metaParseBlob(musicBlob);
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
return {
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),
file: URL.createObjectURL(musicBlob),
blob: musicBlob,
ext,
mime,
title,
artist,
};
}

68
src/decrypt/kgm_wasm.ts Normal file
View File

@ -0,0 +1,68 @@
import { KgmCrypto } from '@xhacker/kgmwasm/KgmWasmBundle';
import KgmCryptoModule from '@xhacker/kgmwasm/KgmWasmBundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
// 每次可以处理 2M 的数据
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
export interface KGMDecryptionResult {
success: boolean;
data: Uint8Array;
error: string;
}
/**
* KGM
*
* Uint8Array
* @param {ArrayBuffer} kgmBlob Blob
*/
export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
const result: KGMDecryptionResult = { success: false, data: new Uint8Array(), error: '' };
// 初始化模组
let KgmCryptoObj: KgmCrypto;
try {
KgmCryptoObj = await KgmCryptoModule();
} catch (err: any) {
result.error = err?.message || 'wasm 加载失败';
return result;
}
if (!KgmCryptoObj) {
result.error = 'wasm 加载失败';
return result;
}
// 申请内存块,并文件末端数据到 WASM 的内存堆
let kgmBuf = new Uint8Array(kgmBlob);
const pKgmBuf = KgmCryptoObj._malloc(DECRYPTION_BUF_SIZE);
const preDecDataSize = Math.min(DECRYPTION_BUF_SIZE, kgmBlob.byteLength); // 初始化缓冲区大小
KgmCryptoObj.writeArrayToMemory(kgmBuf.slice(0, preDecDataSize), pKgmBuf);
// 进行解密初始化
const headerSize = KgmCryptoObj.preDec(pKgmBuf, preDecDataSize, ext);
kgmBuf = kgmBuf.slice(headerSize);
const decryptedParts = [];
let offset = 0;
let bytesToDecrypt = kgmBuf.length;
while (bytesToDecrypt > 0) {
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
// 解密一些片段
const blockData = new Uint8Array(kgmBuf.slice(offset, offset + blockSize));
KgmCryptoObj.writeArrayToMemory(blockData, pKgmBuf);
KgmCryptoObj.decBlob(pKgmBuf, blockSize, offset);
decryptedParts.push(KgmCryptoObj.HEAPU8.slice(pKgmBuf, pKgmBuf + blockSize));
offset += blockSize;
bytesToDecrypt -= blockSize;
}
KgmCryptoObj._free(pKgmBuf);
result.data = MergeUint8Array(decryptedParts);
result.success = true;
return result;
}

78
src/decrypt/kwm.ts Normal file
View File

@ -0,0 +1,78 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
} from '@/decrypt/utils';
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
import { DecryptResult } from '@/decrypt/entity';
//prettier-ignore
const MagicHeader = [
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65,
];
const MagicHeader2 = [
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
0x6B, 0x75, 0x77, 0x6F, 0x00, 0x00, 0x00, 0x00,
];
const PreDefinedKey = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (!BytesHasPrefix(oriData, MagicHeader) && !BytesHasPrefix(oriData, MagicHeader2)) {
if (SniffAudioExt(oriData) === 'aac') {
return await RawDecrypt(file, raw_filename, 'aac', false);
}
throw Error('not a valid kwm file');
}
let fileKey = oriData.slice(0x18, 0x20);
let mask = createMaskFromKey(fileKey);
let audioData = oriData.slice(0x400);
let lenAudioData = audioData.length;
for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= mask[cur % 0x20];
const ext = SniffAudioExt(audioData);
const mime = AudioMimeType[ext];
let musicBlob = new Blob([audioData], { type: mime });
const musicMeta = await metaParseBlob(musicBlob);
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
return {
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),
file: URL.createObjectURL(musicBlob),
blob: musicBlob,
mime,
title,
artist,
ext,
};
}
function createMaskFromKey(keyBytes: Uint8Array): Uint8Array {
let keyView = new DataView(keyBytes.buffer);
let keyStr = keyView.getBigUint64(0, true).toString();
let keyStrTrim = trimKey(keyStr);
let key = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
key[i] = PreDefinedKey.charCodeAt(i) ^ keyStrTrim.charCodeAt(i);
}
return key;
}
function trimKey(keyRaw: string): string {
let lenRaw = keyRaw.length;
let out = keyRaw;
if (lenRaw > 32) {
out = keyRaw.slice(0, 32);
} else if (lenRaw < 32) {
out = keyRaw.padEnd(32, keyRaw);
}
return out;
}

71
src/decrypt/mg3d.ts Normal file
View File

@ -0,0 +1,71 @@
import { Decrypt as RawDecrypt } from './raw';
import { GetArrayBuffer } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
const segmentSize = 0x20;
function isPrintableAsciiChar(ch: number) {
return ch >= 0x20 && ch <= 0x7E;
}
function isUpperHexChar(ch: number) {
return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46);
}
/**
* @param {Buffer} data
* @param {Buffer} key
* @param {boolean} copy
* @returns Buffer
*/
function decryptSegment(data: Uint8Array, key: Uint8Array) {
for (let i = 0; i < data.byteLength; i++) {
data[i] -= key[i % segmentSize];
}
return Buffer.from(data);
}
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
const buf = new Uint8Array(await GetArrayBuffer(file));
// 咪咕编码的 WAV 文件有很多“空洞”内容,尝试密钥。
const header = buf.slice(0, 0x100);
const bytesRIFF = Buffer.from('RIFF', 'ascii');
const bytesWaveFormat = Buffer.from('WAVEfmt ', 'ascii');
const possibleKeys = [];
for (let i = segmentSize; i < segmentSize * 20; i += segmentSize) {
const possibleKey = buf.slice(i, i + segmentSize);
if (!possibleKey.every(isUpperHexChar)) continue;
const tempHeader = decryptSegment(header, possibleKey);
if (tempHeader.slice(0, 4).compare(bytesRIFF)) continue;
if (tempHeader.slice(8, 16).compare(bytesWaveFormat)) continue;
// fmt chunk 大小可以是 16 / 18 / 40。
const fmtChunkSize = tempHeader.readUInt32LE(0x10);
if (![16, 18, 40].includes(fmtChunkSize)) continue;
// 下一个 chunk
const firstDataChunkOffset = 0x14 + fmtChunkSize;
const chunkName = tempHeader.slice(firstDataChunkOffset, firstDataChunkOffset + 4);
if (!chunkName.every(isPrintableAsciiChar)) continue;
const secondDataChunkOffset = firstDataChunkOffset + 8 + tempHeader.readUInt32LE(firstDataChunkOffset + 4);
if (secondDataChunkOffset <= header.byteLength) {
const secondChunkName = tempHeader.slice(secondDataChunkOffset, secondDataChunkOffset + 4);
if (!secondChunkName.every(isPrintableAsciiChar)) continue;
}
possibleKeys.push(Buffer.from(possibleKey).toString('ascii'));
}
if (possibleKeys.length <= 0) {
throw new Error(`ERROR: no suitable key discovered`);
}
const decryptionKey = Buffer.from(possibleKeys[0], 'ascii');
decryptSegment(buf, decryptionKey);
const musicData = new Blob([buf], { type: 'audio/x-wav' });
return await RawDecrypt(musicData, raw_filename, 'wav', false);
}

248
src/decrypt/ncm.ts Normal file
View File

@ -0,0 +1,248 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetImageFromURL,
GetMetaFromFile,
IMusicMeta,
SniffAudioExt,
WriteMetaToFlac,
WriteMetaToMp3,
} from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
import jimp from 'jimp';
import AES from 'crypto-js/aes';
import PKCS7 from 'crypto-js/pad-pkcs7';
import ModeECB from 'crypto-js/mode-ecb';
import WordArray from 'crypto-js/lib-typedarrays';
import Base64 from 'crypto-js/enc-base64';
import EncUTF8 from 'crypto-js/enc-utf8';
import EncHex from 'crypto-js/enc-hex';
import { DecryptResult } from '@/decrypt/entity';
const CORE_KEY = EncHex.parse('687a4852416d736f356b496e62617857');
const META_KEY = EncHex.parse('2331346C6A6B5F215C5D2630553C2728');
const MagicHeader = [0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d];
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt();
}
interface NcmMusicMeta {
//musicId: number
musicName?: string;
artist?: Array<string | number>[];
format?: string;
album?: string;
albumPic?: string;
}
interface NcmDjMeta {
mainMusic: NcmMusicMeta;
}
class NcmDecrypt {
raw: ArrayBuffer;
view: DataView;
offset: number = 0;
filename: string;
format: string = '';
mime: string = '';
audio?: Uint8Array;
blob?: Blob;
oriMeta?: NcmMusicMeta;
newMeta?: IMusicMeta;
image?: { mime: string; buffer: ArrayBuffer; url: string };
constructor(buf: ArrayBuffer, filename: string) {
const prefix = new Uint8Array(buf, 0, 8);
if (!BytesHasPrefix(prefix, MagicHeader)) throw Error('此ncm文件已损坏');
this.offset = 10;
this.raw = buf;
this.view = new DataView(buf);
this.filename = filename;
}
_getKeyData(): Uint8Array {
const keyLen = this.view.getUint32(this.offset, true);
this.offset += 4;
const cipherText = new Uint8Array(this.raw, this.offset, keyLen).map((uint8) => uint8 ^ 0x64);
this.offset += keyLen;
const plainText = AES.decrypt(
// @ts-ignore
{ ciphertext: WordArray.create(cipherText) },
CORE_KEY,
{ mode: ModeECB, padding: PKCS7 },
);
const result = new Uint8Array(plainText.sigBytes);
const words = plainText.words;
const sigBytes = plainText.sigBytes;
for (let i = 0; i < sigBytes; i++) {
result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
}
return result.slice(17);
}
_getKeyBox(): Uint8Array {
const keyData = this._getKeyData();
const box = new Uint8Array(Array(256).keys());
const keyDataLen = keyData.length;
let j = 0;
for (let i = 0; i < 256; i++) {
j = (box[i] + j + keyData[i % keyDataLen]) & 0xff;
[box[i], box[j]] = [box[j], box[i]];
}
return box.map((_, i, arr) => {
i = (i + 1) & 0xff;
const si = arr[i];
const sj = arr[(i + si) & 0xff];
return arr[(si + sj) & 0xff];
});
}
_getMetaData(): NcmMusicMeta {
const metaDataLen = this.view.getUint32(this.offset, true);
this.offset += 4;
if (metaDataLen === 0) return {};
const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen).map((data) => data ^ 0x63);
this.offset += metaDataLen;
WordArray.create();
const plainText = AES.decrypt(
// @ts-ignore
{
ciphertext: Base64.parse(
// @ts-ignore
WordArray.create(cipherText.slice(22)).toString(EncUTF8),
),
},
META_KEY,
{ mode: ModeECB, padding: PKCS7 },
).toString(EncUTF8);
const labelIndex = plainText.indexOf(':');
let result: NcmMusicMeta;
if (plainText.slice(0, labelIndex) === 'dj') {
const tmp: NcmDjMeta = JSON.parse(plainText.slice(labelIndex + 1));
result = tmp.mainMusic;
} else {
result = JSON.parse(plainText.slice(labelIndex + 1));
}
if (result.albumPic) {
result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500';
}
return result;
}
_getAudio(keyBox: Uint8Array): Uint8Array {
this.offset += this.view.getUint32(this.offset + 5, true) + 13;
const audioData = new Uint8Array(this.raw, this.offset);
let lenAudioData = audioData.length;
for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff];
return audioData;
}
async _buildMeta() {
if (!this.oriMeta) throw Error('invalid sequence');
const info = GetMetaFromFile(this.filename, this.oriMeta.musicName);
// build artists
let artists: string[] = [];
if (typeof this.oriMeta.artist === 'string') {
// v3.0: artist 现在可能是字符串了?
artists.push(this.oriMeta.artist);
} else if (Array.isArray(this.oriMeta.artist)) {
this.oriMeta.artist.forEach((artist) => {
if (typeof artist === 'string') {
artists.push(artist);
} else if (Array.isArray(artist) && artist[0] && typeof artist[0] === 'string') {
artists.push(artist[0]);
}
});
}
if (artists.length === 0 && info.artist) {
artists = info.artist
.split(',')
.map((val) => val.trim())
.filter((val) => val != '');
}
if (this.oriMeta.albumPic)
try {
this.image = await GetImageFromURL(this.oriMeta.albumPic);
while (this.image && this.image.buffer.byteLength >= 1 << 24) {
let img = await jimp.read(Buffer.from(this.image.buffer));
await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
this.image.buffer = await img.getBufferAsync('image/jpeg');
}
} catch (e) {
console.log('fetch cover image failed', e);
}
this.newMeta = { title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer };
}
async _writeMeta() {
if (!this.audio || !this.newMeta) throw Error('invalid sequence');
if (!this.blob) this.blob = new Blob([this.audio], { type: this.mime });
const ori = await metaParseBlob(this.blob);
let shouldWrite = !ori.common.album && !ori.common.artists && !ori.common.title;
if (shouldWrite || this.newMeta.picture) {
if (this.format === 'mp3') {
this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori);
} else if (this.format === 'flac') {
this.audio = WriteMetaToFlac(Buffer.from(this.audio), this.newMeta, ori);
} else {
console.info(`writing meta for ${this.format} is not being supported for now`);
return;
}
this.blob = new Blob([this.audio], { type: this.mime });
}
}
gatherResult(): DecryptResult {
if (!this.newMeta || !this.blob) throw Error('bad sequence');
return {
title: this.newMeta.title,
artist: this.newMeta.artists?.join('; '),
ext: this.format,
album: this.newMeta.album,
picture: this.image?.url,
file: URL.createObjectURL(this.blob),
blob: this.blob,
mime: this.mime,
};
}
async decrypt() {
const keyBox = this._getKeyBox();
this.oriMeta = this._getMetaData();
this.audio = this._getAudio(keyBox);
this.format = this.oriMeta.format || SniffAudioExt(this.audio);
this.mime = AudioMimeType[this.format];
try {
await this._buildMeta();
await this._writeMeta();
} catch (e) {
console.warn('build/write meta failed, skip.', e);
}
return this.gatherResult();
}
}

28
src/decrypt/ncmcache.ts Normal file
View File

@ -0,0 +1,28 @@
import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
let length = buffer.length;
for (let i = 0; i < length; i++) {
buffer[i] ^= 163;
}
const ext = SniffAudioExt(buffer, raw_ext);
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
const tag = await metaParseBlob(file);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(file),
blob: file,
mime: AudioMimeType[ext],
};
}

88
src/decrypt/qmc.ts Normal file
View File

@ -0,0 +1,88 @@
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
import { extractQQMusicMeta } from '@/utils/qm_meta';
interface Handler {
ext: string;
version: number;
}
export const HandlerMap: { [key: string]: Handler } = {
mgg: { ext: 'ogg', version: 2 },
mgg0: { ext: 'ogg', version: 2 },
mggl: { ext: 'ogg', version: 2 },
mgg1: { ext: 'ogg', version: 2 },
mflac: { ext: 'flac', version: 2 },
mflac0: { ext: 'flac', version: 2 },
mmp4: { ext: 'mp4', version: 2 },
// qmcflac / qmcogg:
// 有可能是 v2 加密但混用同一个后缀名。
qmcflac: { ext: 'flac', version: 2 },
qmcogg: { ext: 'ogg', version: 2 },
qmc0: { ext: 'mp3', version: 2 },
qmc2: { ext: 'ogg', version: 2 },
qmc3: { ext: 'mp3', version: 2 },
qmc4: { ext: 'ogg', version: 2 },
qmc6: { ext: 'ogg', version: 2 },
qmc8: { ext: 'ogg', version: 2 },
bkcmp3: { ext: 'mp3', version: 1 },
bkcm4a: { ext: 'm4a', version: 1 },
bkcflac: { ext: 'flac', version: 1 },
bkcwav: { ext: 'wav', version: 1 },
bkcape: { ext: 'ape', version: 1 },
bkcogg: { ext: 'ogg', version: 1 },
bkcwma: { ext: 'wma', version: 1 },
tkm: { ext: 'm4a', version: 1 },
'666c6163': { ext: 'flac', version: 1 },
'6d7033': { ext: 'mp3', version: 1 },
'6f6767': { ext: 'ogg', version: 1 },
'6d3461': { ext: 'm4a', version: 1 },
'776176': { ext: 'wav', version: 1 },
};
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
const handler = HandlerMap[raw_ext];
let { version } = handler;
const fileBuffer = await GetArrayBuffer(file);
let musicDecoded = new Uint8Array();
let musicID: number | string | undefined;
if (version === 2 && globalThis.WebAssembly) {
const v2Decrypted = await DecryptQmcWasm(fileBuffer, raw_ext);
// 若 v2 检测失败,降级到 v1 再尝试一次
if (v2Decrypted.success) {
musicDecoded = v2Decrypted.data;
musicID = v2Decrypted.songId;
console.log('qmc wasm decoder suceeded');
} else {
throw new Error(v2Decrypted.error || '(unknown error)');
}
}
const ext = SniffAudioExt(musicDecoded, handler.ext);
const mime = AudioMimeType[ext];
const { album, artist, imgUrl, blob, title } = await extractQQMusicMeta(
new Blob([musicDecoded], { type: mime }),
raw_filename,
ext,
musicID,
);
return {
title: title,
artist: artist,
ext: ext,
album: album,
picture: imgUrl,
file: URL.createObjectURL(blob),
blob: blob,
mime: mime,
};
}

75
src/decrypt/qmc_wasm.ts Normal file
View File

@ -0,0 +1,75 @@
import { QmcCrypto } from '@xhacker/qmcwasm/QmcWasmBundle';
import QmcCryptoModule from '@xhacker/qmcwasm/QmcWasmBundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
// 每次可以处理 2M 的数据
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
export interface QMCDecryptionResult {
success: boolean;
data: Uint8Array;
songId: string | number;
error: string;
}
/**
* QMC
*
* Uint8Array
* @param {ArrayBuffer} qmcBlob Blob
*/
export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
// 初始化模组
let QmcCryptoObj: QmcCrypto;
try {
QmcCryptoObj = await QmcCryptoModule();
} catch (err: any) {
result.error = err?.message || 'wasm 加载失败';
return result;
}
if (!QmcCryptoObj) {
result.error = 'wasm 加载失败';
return result;
}
// 申请内存块,并文件末端数据到 WASM 的内存堆
const qmcBuf = new Uint8Array(qmcBlob);
const pQmcBuf = QmcCryptoObj._malloc(DECRYPTION_BUF_SIZE);
const preDecDataSize = Math.min(DECRYPTION_BUF_SIZE, qmcBlob.byteLength); // 初始化缓冲区大小
QmcCryptoObj.writeArrayToMemory(qmcBuf.slice(-preDecDataSize), pQmcBuf);
// 进行解密初始化
ext = '.' + ext;
const tailSize = QmcCryptoObj.preDec(pQmcBuf, preDecDataSize, ext);
if (tailSize == -1) {
result.error = QmcCryptoObj.getErr();
return result;
} else {
result.songId = QmcCryptoObj.getSongId();
result.songId = result.songId == "0" ? 0 : result.songId;
}
const decryptedParts = [];
let offset = 0;
let bytesToDecrypt = qmcBuf.length - tailSize;
while (bytesToDecrypt > 0) {
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
// 解密一些片段
const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
QmcCryptoObj.writeArrayToMemory(blockData, pQmcBuf);
decryptedParts.push(QmcCryptoObj.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCryptoObj.decBlob(pQmcBuf, blockSize, offset)));
offset += blockSize;
bytesToDecrypt -= blockSize;
}
QmcCryptoObj._free(pQmcBuf);
result.data = MergeUint8Array(decryptedParts);
result.success = true;
return result;
}

58
src/decrypt/qmccache.ts Normal file
View File

@ -0,0 +1,58 @@
import {
AudioMimeType,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
SplitFilename,
} from '@/decrypt/utils';
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
import { DecryptResult } from '@/decrypt/entity';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const buffer = await GetArrayBuffer(file);
let musicDecoded = new Uint8Array();
if (globalThis.WebAssembly) {
console.log('qmc: using wasm decoder');
const qmcDecrypted = await DecryptQmcWasm(buffer, raw_ext);
// 若 wasm 失败,使用 js 再尝试一次
if (qmcDecrypted.success) {
musicDecoded = qmcDecrypted.data;
console.log('qmc wasm decoder suceeded');
} else {
throw new Error(qmcDecrypted.error || '(unknown error)');
}
}
let ext = SniffAudioExt(musicDecoded, '');
const newName = SplitFilename(raw_filename);
let audioBlob: Blob;
if (ext !== '' || newName.ext === 'mp3') {
audioBlob = new Blob([musicDecoded], { type: AudioMimeType[ext] });
} else if (newName.ext in HandlerMap) {
audioBlob = new Blob([musicDecoded], { type: 'application/octet-stream' });
return QmcDecrypt(audioBlob, newName.name, newName.ext);
} else {
throw '不支持的QQ音乐缓存格式';
}
const tag = await metaParseBlob(audioBlob);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(audioBlob),
blob: audioBlob,
mime: AudioMimeType[ext],
};
}

32
src/decrypt/raw.ts Normal file
View File

@ -0,0 +1,32 @@
import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export async function Decrypt(
file: Blob,
raw_filename: string,
raw_ext: string,
detect: boolean = true,
): Promise<DecryptResult> {
let ext = raw_ext;
if (detect) {
const buffer = new Uint8Array(await GetArrayBuffer(file));
ext = SniffAudioExt(buffer, raw_ext);
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
}
const tag = await metaParseBlob(file);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ''));
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(file),
blob: file,
mime: AudioMimeType[ext],
};
}

14
src/decrypt/tm.ts Normal file
View File

@ -0,0 +1,14 @@
import { Decrypt as RawDecrypt } from './raw';
import { GetArrayBuffer } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
const TM_HEADER = [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70];
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
const audioData = new Uint8Array(await GetArrayBuffer(file));
for (let cur = 0; cur < 8; ++cur) {
audioData[cur] = TM_HEADER[cur];
}
const musicData = new Blob([audioData], { type: 'audio/mp4' });
return await RawDecrypt(musicData, raw_filename, 'm4a', false);
}

262
src/decrypt/utils.ts Normal file
View File

@ -0,0 +1,262 @@
import { IAudioMetadata } from 'music-metadata-browser';
import ID3Writer from 'browser-id3-writer';
import MetaFlac from 'metaflac-js';
export const split_regex = /[ ]?[,;/_、][ ]?/;
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43];
export const MP3_HEADER = [0x49, 0x44, 0x33];
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53];
export const M4A_HEADER = [0x66, 0x74, 0x79, 0x70];
//prettier-ignore
export const WMA_HEADER = [
0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11,
0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c,
];
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46];
export const AAC_HEADER = [0xff, 0xf1];
export const DFF_HEADER = [0x46, 0x52, 0x4d, 0x38];
export const AudioMimeType: { [key: string]: string } = {
mp3: 'audio/mpeg',
flac: 'audio/flac',
m4a: 'audio/mp4',
ogg: 'audio/ogg',
wma: 'audio/x-ms-wma',
wav: 'audio/x-wav',
dff: 'audio/x-dff',
};
export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean {
if (prefix.length > data.length) return false;
return prefix.every((val, idx) => {
return val === data[idx];
});
}
export function BytesEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
return a.every((val, idx) => {
return val === b[idx];
});
}
export function SniffAudioExt(data: Uint8Array, fallback_ext: string = 'mp3'): string {
if (BytesHasPrefix(data, MP3_HEADER)) return 'mp3';
if (BytesHasPrefix(data, FLAC_HEADER)) return 'flac';
if (BytesHasPrefix(data, OGG_HEADER)) return 'ogg';
if (data.length >= 4 + M4A_HEADER.length && BytesHasPrefix(data.slice(4), M4A_HEADER)) return 'm4a';
if (BytesHasPrefix(data, WAV_HEADER)) return 'wav';
if (BytesHasPrefix(data, WMA_HEADER)) return 'wma';
if (BytesHasPrefix(data, AAC_HEADER)) return 'aac';
if (BytesHasPrefix(data, DFF_HEADER)) return 'dff';
return fallback_ext;
}
export function GetArrayBuffer(obj: Blob): Promise<ArrayBuffer> {
if (!!obj.arrayBuffer) return obj.arrayBuffer();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const rs = e.target?.result;
if (!rs) {
reject('read file failed');
} else {
resolve(rs as ArrayBuffer);
}
};
reader.readAsArrayBuffer(obj);
});
}
export function GetCoverFromFile(metadata: IAudioMetadata): string {
if (metadata.common?.picture && metadata.common.picture.length > 0) {
return URL.createObjectURL(
new Blob([metadata.common.picture[0].data], { type: metadata.common.picture[0].format }),
);
}
return '';
}
export interface IMusicMetaBasic {
title: string;
artist?: string;
}
export function GetMetaFromFile(
filename: string,
exist_title?: string,
exist_artist?: string,
separator = '-',
): IMusicMetaBasic {
const meta: IMusicMetaBasic = { title: exist_title ?? '', artist: exist_artist };
const items = filename.split(separator);
if (items.length > 1) {
//由文件名和原metadata共同决定歌手tag(有时从文件名看有多个歌手而metadata只有一个)
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length) meta.artist = items[0].trim();
if (!meta.title) meta.title = items[1].trim();
} else if (items.length === 1) {
if (!meta.title) meta.title = items[0].trim();
}
return meta;
}
export async function GetImageFromURL(
src: string,
): Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
try {
const resp = await fetch(src);
const mime = resp.headers.get('Content-Type');
if (mime?.startsWith('image/')) {
const buffer = await resp.arrayBuffer();
const url = URL.createObjectURL(new Blob([buffer], { type: mime }));
return { buffer, url, mime };
}
} catch (e) {
console.warn(e);
}
}
export interface IMusicMeta {
title: string;
artists?: string[];
album?: string;
albumartist?: string;
genre?: string[];
picture?: ArrayBuffer;
picture_desc?: string;
}
export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
const writer = new ID3Writer(audioData);
// reserve original data
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
frames.forEach((frame) => {
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
try {
writer.setFrame(frame.id, frame.value);
} catch (e) {
console.warn(`failed to write ID3 tag '${frame.id}'`);
}
}
});
const old = original.common;
writer
.setFrame('TPE1', old?.artists || info.artists || [])
.setFrame('TIT2', old?.title || info.title)
.setFrame('TALB', old?.album || info.album || '');
if (info.picture) {
writer.setFrame('APIC', {
type: 3,
data: info.picture,
description: info.picture_desc || '',
});
}
return writer.addTag();
}
export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
const writer = new MetaFlac(audioData);
const old = original.common;
if (!old.title && !old.album && old.artists) {
writer.setTag('TITLE=' + info.title);
writer.setTag('ALBUM=' + info.album);
if (info.artists) {
writer.removeTag('ARTIST');
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
}
}
if (info.picture) {
writer.importPictureFromBuffer(Buffer.from(info.picture));
}
return writer.save();
}
export function RewriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
const writer = new ID3Writer(audioData);
// preserve original data
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
frames.forEach((frame) => {
if (frame.id !== 'TPE1'
&& frame.id !== 'TIT2'
&& frame.id !== 'TALB'
&& frame.id !== 'TPE2'
&& frame.id !== 'TCON'
) {
try {
writer.setFrame(frame.id, frame.value);
} catch (e) {
throw new Error(`failed to write ID3 tag '${frame.id}'`);
}
}
});
const old = original.common;
writer
.setFrame('TPE1', info?.artists || old.artists || [])
.setFrame('TIT2', info?.title || old.title)
.setFrame('TALB', info?.album || old.album || '')
.setFrame('TPE2', info?.albumartist || old.albumartist || '')
.setFrame('TCON', info?.genre || old.genre || []);
if (info.picture) {
writer.setFrame('APIC', {
type: 3,
data: info.picture,
description: info.picture_desc || '',
});
}
return writer.addTag();
}
export function RewriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
const writer = new MetaFlac(audioData);
const old = original.common;
if (info.title) {
if (old.title) {
writer.removeTag('TITLE');
}
writer.setTag('TITLE=' + info.title);
}
if (info.album) {
if (old.album) {
writer.removeTag('ALBUM');
}
writer.setTag('ALBUM=' + info.album);
}
if (info.albumartist) {
if (old.albumartist) {
writer.removeTag('ALBUMARTIST');
}
writer.setTag('ALBUMARTIST=' + info.albumartist);
}
if (info.artists) {
if (old.artists) {
writer.removeTag('ARTIST');
}
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
}
if (info.genre) {
if (old.genre) {
writer.removeTag('GENRE');
}
info.genre.forEach((singlegenre) => writer.setTag('GENRE=' + singlegenre));
}
if (info.picture) {
writer.importPictureFromBuffer(Buffer.from(info.picture));
}
return writer.save();
}
export function SplitFilename(n: string): { name: string; ext: string } {
const pos = n.lastIndexOf('.');
return {
ext: n.substring(pos + 1).toLowerCase(),
name: n.substring(0, pos),
};
}

193
src/decrypt/ximalaya.ts Normal file
View File

@ -0,0 +1,193 @@
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
import { AudioMimeType, SniffAudioExt, GetArrayBuffer, GetMetaFromFile } from './utils';
import { DecryptResult } from '@/decrypt/entity';
const HandlerMap: Map<string, (data: Uint8Array) => Uint8Array> = new Map([
['x2m', ProcessX2M],
['x3m', ProcessX3M],
]);
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
const handler = HandlerMap.get(raw_ext);
if (!handler) throw 'File type is incorrect!';
let musicDecoded: Uint8Array = handler(buffer);
const ext = SniffAudioExt(musicDecoded, 'm4a');
const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], { type: mime });
const musicMeta = await metaParseBlob(musicBlob);
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
return {
picture: '',
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
blob: musicBlob,
file: URL.createObjectURL(musicBlob),
mime: mime,
};
}
function ProcessX2M(data: Uint8Array) {
const x2mHeaderSize = 1024;
const x2mKey = [0x78, 0x6d, 0x6c, 0x79];
let encryptedHeader = data.slice(0, x2mHeaderSize);
for (let idx = 0; idx < x2mHeaderSize; idx++) {
let srcIdx = x2mScrambleTable[idx];
data[idx] = encryptedHeader[srcIdx] ^ x2mKey[idx % x2mKey.length];
}
return data;
}
function ProcessX3M(data: Uint8Array) {
const x3mHeaderSize = 1024;
//prettier-ignore: uint8, size 32(8x4)
const x3mKey = [
0x33, 0x39, 0x38, 0x39, 0x64, 0x31, 0x31, 0x31, 0x61, 0x61, 0x64, 0x35, 0x36, 0x31, 0x33, 0x39, 0x34, 0x30, 0x66,
0x34, 0x66, 0x63, 0x34, 0x34, 0x62, 0x36, 0x33, 0x39, 0x62, 0x32, 0x39, 0x32,
];
let encryptedHeader = data.slice(0, x3mHeaderSize);
for (let dstIdx = 0; dstIdx < x3mHeaderSize; dstIdx++) {
let srcIdx = x3mScrambleTable[dstIdx];
data[dstIdx] = encryptedHeader[srcIdx] ^ x3mKey[dstIdx % x3mKey.length];
}
return data;
}
//prettier-ignore: uint16, size 1024 (64x16)
const x2mScrambleTable = [
0x2a9, 0x2ab, 0x154, 0x2aa, 0x2a8, 0x2ac, 0x153, 0x2a7, 0x2ad, 0x152, 0x2a6, 0x3ff, 0x000, 0x155, 0x2ae, 0x151, 0x2a5,
0x3fe, 0x001, 0x156, 0x2af, 0x150, 0x2a4, 0x3fd, 0x002, 0x157, 0x2b0, 0x14f, 0x2a3, 0x3fc, 0x003, 0x158, 0x2b1, 0x14e,
0x2a2, 0x3fb, 0x004, 0x159, 0x2b2, 0x14d, 0x2a1, 0x3fa, 0x005, 0x15a, 0x2b3, 0x14c, 0x2a0, 0x3f9, 0x006, 0x15b, 0x2b4,
0x14b, 0x29f, 0x3f8, 0x007, 0x15c, 0x2b5, 0x14a, 0x29e, 0x3f7, 0x008, 0x15d, 0x2b6, 0x149, 0x29d, 0x3f6, 0x009, 0x15e,
0x2b7, 0x148, 0x29c, 0x3f5, 0x00a, 0x15f, 0x2b8, 0x147, 0x29b, 0x3f4, 0x00b, 0x160, 0x2b9, 0x146, 0x29a, 0x3f3, 0x00c,
0x161, 0x2ba, 0x145, 0x299, 0x3f2, 0x00d, 0x162, 0x2bb, 0x144, 0x298, 0x3f1, 0x00e, 0x163, 0x2bc, 0x143, 0x297, 0x3f0,
0x00f, 0x164, 0x2bd, 0x142, 0x296, 0x3ef, 0x010, 0x165, 0x2be, 0x141, 0x295, 0x3ee, 0x011, 0x166, 0x2bf, 0x140, 0x294,
0x3ed, 0x012, 0x167, 0x2c0, 0x13f, 0x293, 0x3ec, 0x013, 0x168, 0x2c1, 0x13e, 0x292, 0x3eb, 0x014, 0x169, 0x2c2, 0x13d,
0x291, 0x3ea, 0x015, 0x16a, 0x2c3, 0x13c, 0x290, 0x3e9, 0x016, 0x16b, 0x2c4, 0x13b, 0x28f, 0x3e8, 0x017, 0x16c, 0x2c5,
0x13a, 0x28e, 0x3e7, 0x018, 0x16d, 0x2c6, 0x139, 0x28d, 0x3e6, 0x019, 0x16e, 0x2c7, 0x138, 0x28c, 0x3e5, 0x01a, 0x16f,
0x2c8, 0x137, 0x28b, 0x3e4, 0x01b, 0x170, 0x2c9, 0x136, 0x28a, 0x3e3, 0x01c, 0x171, 0x2ca, 0x135, 0x289, 0x3e2, 0x01d,
0x172, 0x2cb, 0x134, 0x288, 0x3e1, 0x01e, 0x173, 0x2cc, 0x133, 0x287, 0x3e0, 0x01f, 0x174, 0x2cd, 0x0a9, 0x1fe, 0x357,
0x020, 0x175, 0x2ce, 0x0aa, 0x1ff, 0x358, 0x021, 0x176, 0x2cf, 0x0ab, 0x200, 0x359, 0x022, 0x177, 0x2d0, 0x0ac, 0x201,
0x35a, 0x023, 0x178, 0x2d1, 0x0ad, 0x202, 0x35b, 0x024, 0x179, 0x2d2, 0x0ae, 0x203, 0x35c, 0x025, 0x17a, 0x2d3, 0x0af,
0x204, 0x35d, 0x026, 0x17b, 0x2d4, 0x0b0, 0x205, 0x35e, 0x027, 0x17c, 0x2d5, 0x0b1, 0x206, 0x35f, 0x028, 0x17d, 0x2d6,
0x0b2, 0x207, 0x360, 0x029, 0x17e, 0x2d7, 0x0b3, 0x208, 0x361, 0x02a, 0x17f, 0x2d8, 0x0b4, 0x209, 0x362, 0x02b, 0x180,
0x2d9, 0x0b5, 0x20a, 0x363, 0x02c, 0x181, 0x2da, 0x0b6, 0x20b, 0x364, 0x02d, 0x182, 0x2db, 0x0b7, 0x20c, 0x365, 0x02e,
0x183, 0x2dc, 0x0b8, 0x20d, 0x366, 0x02f, 0x184, 0x2dd, 0x0b9, 0x20e, 0x367, 0x030, 0x185, 0x2de, 0x0ba, 0x20f, 0x368,
0x031, 0x186, 0x2df, 0x0bb, 0x210, 0x369, 0x032, 0x187, 0x2e0, 0x0bc, 0x211, 0x36a, 0x033, 0x188, 0x2e1, 0x0bd, 0x212,
0x36b, 0x034, 0x189, 0x2e2, 0x0be, 0x213, 0x36c, 0x035, 0x18a, 0x2e3, 0x0bf, 0x214, 0x36d, 0x036, 0x18b, 0x2e4, 0x0c0,
0x215, 0x36e, 0x037, 0x18c, 0x2e5, 0x0c1, 0x216, 0x36f, 0x038, 0x18d, 0x2e6, 0x0c2, 0x217, 0x370, 0x039, 0x18e, 0x2e7,
0x0c3, 0x218, 0x371, 0x03a, 0x18f, 0x2e8, 0x0c4, 0x219, 0x372, 0x03b, 0x190, 0x2e9, 0x0c5, 0x21a, 0x373, 0x03c, 0x191,
0x2ea, 0x0c6, 0x21b, 0x374, 0x03d, 0x192, 0x2eb, 0x0c7, 0x21c, 0x375, 0x03e, 0x193, 0x2ec, 0x0c8, 0x21d, 0x376, 0x03f,
0x194, 0x2ed, 0x0c9, 0x21e, 0x377, 0x040, 0x195, 0x2ee, 0x0ca, 0x21f, 0x378, 0x041, 0x196, 0x2ef, 0x0cb, 0x220, 0x379,
0x042, 0x197, 0x2f0, 0x0cc, 0x221, 0x37a, 0x043, 0x198, 0x2f1, 0x0cd, 0x222, 0x37b, 0x044, 0x199, 0x2f2, 0x0ce, 0x223,
0x37c, 0x045, 0x19a, 0x2f3, 0x0cf, 0x224, 0x37d, 0x046, 0x19b, 0x2f4, 0x0d0, 0x225, 0x37e, 0x047, 0x19c, 0x2f5, 0x0d1,
0x226, 0x37f, 0x048, 0x19d, 0x2f6, 0x0d2, 0x227, 0x380, 0x049, 0x19e, 0x2f7, 0x0d3, 0x228, 0x381, 0x04a, 0x19f, 0x2f8,
0x0d4, 0x229, 0x382, 0x04b, 0x1a0, 0x2f9, 0x0d5, 0x22a, 0x383, 0x04c, 0x1a1, 0x2fa, 0x0d6, 0x22b, 0x384, 0x04d, 0x1a2,
0x2fb, 0x0d7, 0x22c, 0x385, 0x04e, 0x1a3, 0x2fc, 0x0d8, 0x22d, 0x386, 0x04f, 0x1a4, 0x2fd, 0x0d9, 0x22e, 0x387, 0x050,
0x1a5, 0x2fe, 0x0da, 0x22f, 0x388, 0x051, 0x1a6, 0x2ff, 0x0db, 0x230, 0x389, 0x052, 0x1a7, 0x300, 0x0dc, 0x231, 0x38a,
0x053, 0x1a8, 0x301, 0x0dd, 0x232, 0x38b, 0x054, 0x1a9, 0x302, 0x0de, 0x233, 0x38c, 0x055, 0x1aa, 0x303, 0x0df, 0x234,
0x38d, 0x056, 0x1ab, 0x304, 0x0e0, 0x235, 0x38e, 0x057, 0x1ac, 0x305, 0x0e1, 0x236, 0x38f, 0x058, 0x1ad, 0x306, 0x0e2,
0x237, 0x390, 0x059, 0x1ae, 0x307, 0x0e3, 0x238, 0x391, 0x05a, 0x1af, 0x308, 0x0e4, 0x239, 0x392, 0x05b, 0x1b0, 0x309,
0x0e5, 0x23a, 0x393, 0x05c, 0x1b1, 0x30a, 0x0e6, 0x23b, 0x394, 0x05d, 0x1b2, 0x30b, 0x0e7, 0x23c, 0x395, 0x05e, 0x1b3,
0x30c, 0x0e8, 0x23d, 0x396, 0x05f, 0x1b4, 0x30d, 0x0e9, 0x23e, 0x397, 0x060, 0x1b5, 0x30e, 0x0ea, 0x23f, 0x398, 0x061,
0x1b6, 0x30f, 0x0eb, 0x240, 0x399, 0x062, 0x1b7, 0x310, 0x0ec, 0x241, 0x39a, 0x063, 0x1b8, 0x311, 0x0ed, 0x242, 0x39b,
0x064, 0x1b9, 0x312, 0x0ee, 0x243, 0x39c, 0x065, 0x1ba, 0x313, 0x0ef, 0x244, 0x39d, 0x066, 0x1bb, 0x314, 0x0f0, 0x245,
0x39e, 0x067, 0x1bc, 0x315, 0x0f1, 0x246, 0x39f, 0x068, 0x1bd, 0x316, 0x0f2, 0x247, 0x3a0, 0x069, 0x1be, 0x317, 0x0f3,
0x248, 0x3a1, 0x06a, 0x1bf, 0x318, 0x0f4, 0x249, 0x3a2, 0x06b, 0x1c0, 0x319, 0x0f5, 0x24a, 0x3a3, 0x06c, 0x1c1, 0x31a,
0x0f6, 0x24b, 0x3a4, 0x06d, 0x1c2, 0x31b, 0x0f7, 0x24c, 0x3a5, 0x06e, 0x1c3, 0x31c, 0x0f8, 0x24d, 0x3a6, 0x06f, 0x1c4,
0x31d, 0x0f9, 0x24e, 0x3a7, 0x070, 0x1c5, 0x31e, 0x0fa, 0x24f, 0x3a8, 0x071, 0x1c6, 0x31f, 0x0fb, 0x250, 0x3a9, 0x072,
0x1c7, 0x320, 0x0fc, 0x251, 0x3aa, 0x073, 0x1c8, 0x321, 0x0fd, 0x252, 0x3ab, 0x074, 0x1c9, 0x322, 0x0fe, 0x253, 0x3ac,
0x075, 0x1ca, 0x323, 0x0ff, 0x254, 0x3ad, 0x076, 0x1cb, 0x324, 0x100, 0x255, 0x3ae, 0x077, 0x1cc, 0x325, 0x101, 0x256,
0x3af, 0x078, 0x1cd, 0x326, 0x102, 0x257, 0x3b0, 0x079, 0x1ce, 0x327, 0x103, 0x258, 0x3b1, 0x07a, 0x1cf, 0x328, 0x104,
0x259, 0x3b2, 0x07b, 0x1d0, 0x329, 0x105, 0x25a, 0x3b3, 0x07c, 0x1d1, 0x32a, 0x106, 0x25b, 0x3b4, 0x07d, 0x1d2, 0x32b,
0x107, 0x25c, 0x3b5, 0x07e, 0x1d3, 0x32c, 0x108, 0x25d, 0x3b6, 0x07f, 0x1d4, 0x32d, 0x109, 0x25e, 0x3b7, 0x080, 0x1d5,
0x32e, 0x10a, 0x25f, 0x3b8, 0x081, 0x1d6, 0x32f, 0x10b, 0x260, 0x3b9, 0x082, 0x1d7, 0x330, 0x10c, 0x261, 0x3ba, 0x083,
0x1d8, 0x331, 0x10d, 0x262, 0x3bb, 0x084, 0x1d9, 0x332, 0x10e, 0x263, 0x3bc, 0x085, 0x1da, 0x333, 0x10f, 0x264, 0x3bd,
0x086, 0x1db, 0x334, 0x110, 0x265, 0x3be, 0x087, 0x1dc, 0x335, 0x111, 0x266, 0x3bf, 0x088, 0x1dd, 0x336, 0x112, 0x267,
0x3c0, 0x089, 0x1de, 0x337, 0x113, 0x268, 0x3c1, 0x08a, 0x1df, 0x338, 0x114, 0x269, 0x3c2, 0x08b, 0x1e0, 0x339, 0x115,
0x26a, 0x3c3, 0x08c, 0x1e1, 0x33a, 0x116, 0x26b, 0x3c4, 0x08d, 0x1e2, 0x33b, 0x117, 0x26c, 0x3c5, 0x08e, 0x1e3, 0x33c,
0x118, 0x26d, 0x3c6, 0x08f, 0x1e4, 0x33d, 0x119, 0x26e, 0x3c7, 0x090, 0x1e5, 0x33e, 0x11a, 0x26f, 0x3c8, 0x091, 0x1e6,
0x33f, 0x11b, 0x270, 0x3c9, 0x092, 0x1e7, 0x340, 0x11c, 0x271, 0x3ca, 0x093, 0x1e8, 0x341, 0x11d, 0x272, 0x3cb, 0x094,
0x1e9, 0x342, 0x11e, 0x273, 0x3cc, 0x095, 0x1ea, 0x343, 0x11f, 0x274, 0x3cd, 0x096, 0x1eb, 0x344, 0x120, 0x275, 0x3ce,
0x097, 0x1ec, 0x345, 0x121, 0x276, 0x3cf, 0x098, 0x1ed, 0x346, 0x122, 0x277, 0x3d0, 0x099, 0x1ee, 0x347, 0x123, 0x278,
0x3d1, 0x09a, 0x1ef, 0x348, 0x124, 0x279, 0x3d2, 0x09b, 0x1f0, 0x349, 0x125, 0x27a, 0x3d3, 0x09c, 0x1f1, 0x34a, 0x126,
0x27b, 0x3d4, 0x09d, 0x1f2, 0x34b, 0x127, 0x27c, 0x3d5, 0x09e, 0x1f3, 0x34c, 0x128, 0x27d, 0x3d6, 0x09f, 0x1f4, 0x34d,
0x129, 0x27e, 0x3d7, 0x0a0, 0x1f5, 0x34e, 0x12a, 0x27f, 0x3d8, 0x0a1, 0x1f6, 0x34f, 0x12b, 0x280, 0x3d9, 0x0a2, 0x1f7,
0x350, 0x12c, 0x281, 0x3da, 0x0a3, 0x1f8, 0x351, 0x12d, 0x282, 0x3db, 0x0a4, 0x1f9, 0x352, 0x12e, 0x283, 0x3dc, 0x0a5,
0x1fa, 0x353, 0x12f, 0x284, 0x3dd, 0x0a6, 0x1fb, 0x354, 0x130, 0x285, 0x3de, 0x0a7, 0x1fc, 0x355, 0x131, 0x286, 0x3df,
0x0a8, 0x1fd, 0x356, 0x132,
];
//prettier-ignore: uint16, size 1024 (64x16)
const x3mScrambleTable = [
0x256, 0x28d, 0x213, 0x307, 0x156, 0x39d, 0x062, 0x170, 0x3ca, 0x035, 0x0ed, 0x2a4, 0x1e4, 0x359, 0x0d3, 0x26b, 0x265,
0x274, 0x251, 0x297, 0x202, 0x322, 0x126, 0x32b, 0x117, 0x302, 0x15c, 0x3a8, 0x057, 0x148, 0x380, 0x090, 0x1f6, 0x335,
0x10c, 0x2ee, 0x175, 0x3d4, 0x02b, 0x0cc, 0x260, 0x27b, 0x23d, 0x2bb, 0x1b6, 0x3a1, 0x05e, 0x157, 0x39e, 0x061, 0x16f,
0x3c6, 0x039, 0x0f7, 0x2b9, 0x1b8, 0x39f, 0x060, 0x166, 0x3b9, 0x046, 0x122, 0x31c, 0x12f, 0x33d, 0x0fc, 0x2ca, 0x1a4,
0x3cc, 0x033, 0x0e6, 0x293, 0x209, 0x315, 0x13d, 0x358, 0x0d5, 0x26e, 0x25e, 0x27d, 0x23a, 0x2c0, 0x1b1, 0x3af, 0x050,
0x136, 0x346, 0x0ef, 0x2aa, 0x1ce, 0x376, 0x0a0, 0x210, 0x30c, 0x14c, 0x389, 0x082, 0x1db, 0x367, 0x0b9, 0x23e, 0x2ba,
0x1b7, 0x3a0, 0x05f, 0x164, 0x3b7, 0x048, 0x125, 0x326, 0x11c, 0x30a, 0x14f, 0x38f, 0x070, 0x1a8, 0x3c7, 0x038, 0x0f5,
0x2b5, 0x1bd, 0x393, 0x06c, 0x199, 0x3e1, 0x01e, 0x0b3, 0x22f, 0x2d7, 0x193, 0x3ea, 0x015, 0x09d, 0x20a, 0x314, 0x13e,
0x35a, 0x0d2, 0x26a, 0x267, 0x272, 0x253, 0x294, 0x208, 0x319, 0x137, 0x34c, 0x0e7, 0x295, 0x205, 0x31d, 0x12e, 0x33c,
0x0fe, 0x2cd, 0x1a0, 0x3d5, 0x02a, 0x0c8, 0x258, 0x286, 0x22a, 0x2dc, 0x18e, 0x3f7, 0x008, 0x07c, 0x1d3, 0x370, 0x0a7,
0x21d, 0x2f1, 0x171, 0x3cd, 0x032, 0x0e5, 0x292, 0x20b, 0x313, 0x13f, 0x35c, 0x0d0, 0x266, 0x273, 0x252, 0x296, 0x204,
0x31f, 0x12a, 0x332, 0x10f, 0x2f4, 0x16c, 0x3c3, 0x03c, 0x101, 0x2d2, 0x19a, 0x3e0, 0x01f, 0x0b5, 0x233, 0x2c9, 0x1a6,
0x3c9, 0x036, 0x0f0, 0x2ab, 0x1cb, 0x37c, 0x095, 0x1fd, 0x328, 0x11a, 0x306, 0x158, 0x3a2, 0x05d, 0x155, 0x39c, 0x063,
0x174, 0x3d3, 0x02c, 0x0cf, 0x264, 0x275, 0x24f, 0x299, 0x1fa, 0x32c, 0x115, 0x2ff, 0x15f, 0x3ab, 0x054, 0x143, 0x36c,
0x0ad, 0x225, 0x2e5, 0x181, 0x3ef, 0x010, 0x08c, 0x1f1, 0x344, 0x0f3, 0x2af, 0x1c4, 0x386, 0x088, 0x1e3, 0x35b, 0x0d1,
0x269, 0x268, 0x26d, 0x25f, 0x27c, 0x23b, 0x2bf, 0x1b2, 0x3ae, 0x051, 0x13b, 0x355, 0x0da, 0x278, 0x248, 0x2a6, 0x1dc,
0x365, 0x0c0, 0x246, 0x2a8, 0x1d6, 0x36d, 0x0ac, 0x224, 0x2e8, 0x17e, 0x3eb, 0x014, 0x09c, 0x207, 0x31a, 0x133, 0x341,
0x0f8, 0x2bc, 0x1b5, 0x3a3, 0x05c, 0x152, 0x395, 0x06a, 0x18c, 0x3f9, 0x006, 0x07a, 0x1d1, 0x373, 0x0a4, 0x217, 0x2fe,
0x160, 0x3ad, 0x052, 0x13c, 0x357, 0x0d7, 0x270, 0x25c, 0x281, 0x235, 0x2c6, 0x1aa, 0x3bc, 0x043, 0x11d, 0x30d, 0x14a,
0x384, 0x08a, 0x1e7, 0x353, 0x0dd, 0x284, 0x22e, 0x2d8, 0x192, 0x3ec, 0x013, 0x099, 0x201, 0x323, 0x124, 0x321, 0x127,
0x32d, 0x114, 0x2fd, 0x161, 0x3b0, 0x04f, 0x135, 0x343, 0x0f4, 0x2b4, 0x1be, 0x392, 0x06d, 0x19d, 0x3db, 0x024, 0x0be,
0x244, 0x2b0, 0x1c2, 0x38a, 0x080, 0x1d9, 0x369, 0x0b6, 0x234, 0x2c8, 0x1a7, 0x3c8, 0x037, 0x0f1, 0x2ad, 0x1c6, 0x383,
0x08d, 0x1f2, 0x33b, 0x100, 0x2d1, 0x19b, 0x3de, 0x021, 0x0bb, 0x240, 0x2b6, 0x1bb, 0x399, 0x066, 0x17a, 0x3df, 0x020,
0x0b8, 0x23c, 0x2bd, 0x1b4, 0x3a5, 0x05a, 0x150, 0x390, 0x06f, 0x1a5, 0x3cb, 0x034, 0x0ea, 0x29d, 0x1ee, 0x348, 0x0ec,
0x2a3, 0x1e5, 0x356, 0x0d8, 0x271, 0x257, 0x289, 0x220, 0x2ec, 0x178, 0x3d9, 0x026, 0x0c2, 0x24b, 0x2a1, 0x1ea, 0x34d,
0x0e4, 0x291, 0x20c, 0x312, 0x141, 0x360, 0x0ca, 0x25a, 0x283, 0x230, 0x2d0, 0x19c, 0x3dd, 0x022, 0x0bc, 0x241, 0x2b3,
0x1bf, 0x391, 0x06e, 0x1a2, 0x3d1, 0x02e, 0x0d6, 0x26f, 0x25d, 0x27f, 0x237, 0x2c4, 0x1ac, 0x3ba, 0x045, 0x121, 0x318,
0x138, 0x34e, 0x0e3, 0x28f, 0x211, 0x30b, 0x14d, 0x38c, 0x073, 0x1c3, 0x387, 0x084, 0x1df, 0x362, 0x0c7, 0x255, 0x28e,
0x212, 0x309, 0x153, 0x396, 0x069, 0x18b, 0x3fa, 0x005, 0x079, 0x1d0, 0x374, 0x0a2, 0x215, 0x301, 0x15d, 0x3a9, 0x056,
0x147, 0x37a, 0x098, 0x200, 0x324, 0x11f, 0x316, 0x13a, 0x352, 0x0df, 0x288, 0x223, 0x2e9, 0x17d, 0x3e9, 0x016, 0x09e,
0x20d, 0x310, 0x144, 0x372, 0x0a5, 0x219, 0x2fa, 0x165, 0x3b8, 0x047, 0x123, 0x31e, 0x12d, 0x338, 0x107, 0x2e0, 0x188,
0x3fe, 0x001, 0x075, 0x1c9, 0x37e, 0x093, 0x1f9, 0x32f, 0x112, 0x2f9, 0x167, 0x3be, 0x041, 0x10b, 0x2e7, 0x17f, 0x3ed,
0x012, 0x097, 0x1ff, 0x325, 0x11e, 0x311, 0x142, 0x366, 0x0ba, 0x23f, 0x2b8, 0x1b9, 0x39b, 0x064, 0x176, 0x3d6, 0x029,
0x0c5, 0x250, 0x298, 0x1fc, 0x329, 0x119, 0x304, 0x15a, 0x3a6, 0x059, 0x14e, 0x38e, 0x071, 0x1ad, 0x3b6, 0x049, 0x128,
0x32e, 0x113, 0x2fc, 0x162, 0x3b2, 0x04d, 0x131, 0x33f, 0x0fa, 0x2c2, 0x1af, 0x3b3, 0x04c, 0x130, 0x33e, 0x0fb, 0x2c7,
0x1a9, 0x3bd, 0x042, 0x116, 0x300, 0x15e, 0x3aa, 0x055, 0x146, 0x378, 0x09b, 0x206, 0x31b, 0x132, 0x340, 0x0f9, 0x2be,
0x1b3, 0x3ac, 0x053, 0x140, 0x35d, 0x0ce, 0x262, 0x279, 0x247, 0x2a7, 0x1d7, 0x36b, 0x0ae, 0x226, 0x2e3, 0x185, 0x3f6,
0x009, 0x07d, 0x1d4, 0x36f, 0x0a8, 0x21e, 0x2f0, 0x172, 0x3ce, 0x031, 0x0de, 0x287, 0x228, 0x2df, 0x189, 0x3fd, 0x002,
0x076, 0x1ca, 0x37d, 0x094, 0x1fb, 0x32a, 0x118, 0x303, 0x15b, 0x3a7, 0x058, 0x14b, 0x388, 0x083, 0x1dd, 0x364, 0x0c1,
0x24a, 0x2a2, 0x1e9, 0x350, 0x0e1, 0x28b, 0x21a, 0x2f8, 0x168, 0x3bf, 0x040, 0x10a, 0x2e6, 0x180, 0x3ee, 0x011, 0x091,
0x1f7, 0x334, 0x10d, 0x2ef, 0x173, 0x3cf, 0x030, 0x0dc, 0x280, 0x236, 0x2c5, 0x1ab, 0x3bb, 0x044, 0x120, 0x317, 0x139,
0x34f, 0x0e2, 0x28c, 0x218, 0x2fb, 0x163, 0x3b4, 0x04b, 0x12c, 0x337, 0x108, 0x2e2, 0x186, 0x3fc, 0x003, 0x077, 0x1cc,
0x37b, 0x096, 0x1fe, 0x327, 0x11b, 0x308, 0x154, 0x397, 0x068, 0x183, 0x3f3, 0x00c, 0x085, 0x1e0, 0x361, 0x0c9, 0x259,
0x285, 0x22c, 0x2da, 0x190, 0x3f2, 0x00d, 0x086, 0x1e1, 0x35f, 0x0cb, 0x25b, 0x282, 0x232, 0x2cc, 0x1a1, 0x3d2, 0x02d,
0x0d4, 0x26c, 0x263, 0x277, 0x249, 0x2a5, 0x1de, 0x363, 0x0c6, 0x254, 0x290, 0x20e, 0x30f, 0x145, 0x377, 0x09f, 0x20f,
0x30e, 0x149, 0x382, 0x08e, 0x1f3, 0x33a, 0x102, 0x2d3, 0x198, 0x3e2, 0x01d, 0x0b2, 0x22d, 0x2d9, 0x191, 0x3f1, 0x00e,
0x087, 0x1e2, 0x35e, 0x0cd, 0x261, 0x27a, 0x243, 0x2b1, 0x1c1, 0x38b, 0x07f, 0x1d8, 0x36a, 0x0b4, 0x231, 0x2cf, 0x19e,
0x3da, 0x025, 0x0bf, 0x245, 0x2ac, 0x1c7, 0x381, 0x08f, 0x1f5, 0x336, 0x109, 0x2e4, 0x184, 0x3f4, 0x00b, 0x081, 0x1da,
0x368, 0x0b7, 0x239, 0x2c1, 0x1b0, 0x3b1, 0x04e, 0x134, 0x342, 0x0f6, 0x2b7, 0x1ba, 0x39a, 0x065, 0x179, 0x3dc, 0x023,
0x0bd, 0x242, 0x2b2, 0x1c0, 0x38d, 0x072, 0x1bc, 0x398, 0x067, 0x182, 0x3f0, 0x00f, 0x08b, 0x1e8, 0x351, 0x0e0, 0x28a,
0x21c, 0x2f3, 0x16d, 0x3c4, 0x03b, 0x0ff, 0x2ce, 0x19f, 0x3d7, 0x028, 0x0c4, 0x24e, 0x29b, 0x1f0, 0x345, 0x0f2, 0x2ae,
0x1c5, 0x385, 0x089, 0x1e6, 0x354, 0x0db, 0x27e, 0x238, 0x2c3, 0x1ae, 0x3b5, 0x04a, 0x12b, 0x333, 0x10e, 0x2f2, 0x16e,
0x3c5, 0x03a, 0x0fd, 0x2cb, 0x1a3, 0x3d0, 0x02f, 0x0d9, 0x276, 0x24c, 0x29f, 0x1ec, 0x34a, 0x0e9, 0x29c, 0x1ef, 0x347,
0x0ee, 0x2a9, 0x1cf, 0x375, 0x0a1, 0x214, 0x305, 0x159, 0x3a4, 0x05b, 0x151, 0x394, 0x06b, 0x196, 0x3e6, 0x019, 0x0ab,
0x222, 0x2ea, 0x17c, 0x3e5, 0x01a, 0x0af, 0x227, 0x2e1, 0x187, 0x3ff, 0x000, 0x074, 0x1c8, 0x37f, 0x092, 0x1f8, 0x331,
0x110, 0x2f6, 0x16a, 0x3c1, 0x03e, 0x104, 0x2d5, 0x195, 0x3e7, 0x018, 0x0aa, 0x221, 0x2eb, 0x17b, 0x3e4, 0x01b, 0x0b0,
0x229, 0x2dd, 0x18d, 0x3f8, 0x007, 0x07b, 0x1d2, 0x371, 0x0a6, 0x21b, 0x2f5, 0x16b, 0x3c2, 0x03d, 0x103, 0x2d4, 0x197,
0x3e3, 0x01c, 0x0b1, 0x22b, 0x2db, 0x18f, 0x3f5, 0x00a, 0x07e, 0x1d5, 0x36e, 0x0a9, 0x21f, 0x2ed, 0x177, 0x3d8, 0x027,
0x0c3, 0x24d, 0x29e, 0x1ed, 0x349, 0x0eb, 0x2a0, 0x1eb, 0x34b, 0x0e8, 0x29a, 0x1f4, 0x339, 0x106, 0x2de, 0x18a, 0x3fb,
0x004, 0x078, 0x1cd, 0x379, 0x09a, 0x203, 0x320, 0x129, 0x330, 0x111, 0x2f7, 0x169, 0x3c0, 0x03f, 0x105, 0x2d6, 0x194,
0x3e8, 0x017, 0x0a3, 0x216,
];

67
src/decrypt/xm.ts Normal file
View File

@ -0,0 +1,67 @@
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
import { DecryptResult } from '@/decrypt/entity';
import { AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile } from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
const MagicHeader = [0x69, 0x66, 0x6d, 0x74];
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe];
const FileTypeMap: { [key: string]: string } = {
' WAV': '.wav',
FLAC: '.flac',
' MP3': '.mp3',
' A4M': '.m4a',
};
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
if (raw_ext === 'xm') {
throw Error('此xm文件已损坏');
} else {
return await RawDecrypt(file, raw_filename, raw_ext, true);
}
}
let typeText = new TextDecoder().decode(oriData.slice(4, 8));
if (!FileTypeMap.hasOwnProperty(typeText)) {
throw Error('未知的.xm文件类型');
}
let key = oriData[0xf];
let dataOffset = oriData[0xc] | (oriData[0xd] << 8) | (oriData[0xe] << 16);
let audioData = oriData.slice(0x10);
let lenAudioData = audioData.length;
for (let cur = dataOffset; cur < lenAudioData; ++cur) audioData[cur] = (audioData[cur] - key) ^ 0xff;
const ext = FileTypeMap[typeText];
const mime = AudioMimeType[ext];
let musicBlob = new Blob([audioData], { type: mime });
const musicMeta = await metaParseBlob(musicBlob);
if (ext === 'wav') {
//todo:未知的编码方式
console.info(musicMeta.common);
musicMeta.common.album = '';
musicMeta.common.artist = '';
musicMeta.common.title = '';
}
const { title, artist } = GetMetaFromFile(
raw_filename,
musicMeta.common.title,
String(musicMeta.common.artists || musicMeta.common.artist || ""),
raw_filename.indexOf('_') === -1 ? '-' : '_',
);
return {
title,
artist,
ext,
mime,
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),
file: URL.createObjectURL(musicBlob),
blob: musicBlob,
rawExt: 'xm',
};
}

13
src/extension/popup.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="./popup.js"></script>
<a href="./index.html" target="_blank">
<button>立即使用</button>
</a>
</body>
</html>

2
src/extension/popup.js Normal file
View File

@ -0,0 +1,2 @@
const bs = chrome || browser;
bs.tabs.create({ url: bs.runtime.getURL('./index.html') }, (tab) => console.log(tab));

View File

@ -1,11 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import './plugins/element.js'
// only if your build system can import css, otherwise import it wherever you would import your css.
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');

56
src/main.ts Normal file
View File

@ -0,0 +1,56 @@
import Vue from 'vue';
import App from '@/App.vue';
import '@/registerServiceWorker';
import {
Button,
Checkbox,
Col,
Container,
Dialog,
Form,
FormItem,
Footer,
Icon,
Image,
Input,
Link,
Main,
Notification,
Progress,
Radio,
Row,
Table,
TableColumn,
Tooltip,
Upload,
MessageBox,
} from 'element-ui';
import 'element-ui/lib/theme-chalk/base.css';
Vue.use(Link);
Vue.use(Image);
Vue.use(Button);
Vue.use(Dialog);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Container);
Vue.use(Icon);
Vue.use(Row);
Vue.use(Col);
Vue.use(Upload);
Vue.use(Checkbox);
Vue.use(Radio);
Vue.use(Tooltip);
Vue.use(Progress);
Vue.prototype.$notify = Notification;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.config.productionTip = false;
new Vue({
render: (h) => h(App),
}).$mount('#app');

View File

@ -1,33 +0,0 @@
import Vue from 'vue'
import {
Image,
Button,
Table,
TableColumn,
Main,
Footer,
Container,
Icon,
Row,
Col,
Upload,
Notification,
Link
} from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(Link);
Vue.use(Image);
Vue.use(Button);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Container);
Vue.use(Icon);
Vue.use(Row);
Vue.use(Col);
Vue.use(Upload);
Vue.prototype.$notify = Notification;

View File

@ -1,165 +0,0 @@
const CryptoJS = require("crypto-js");
const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857");
const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728");
const audio_mime_type = {
mp3: "audio/mpeg",
flac: "audio/flac"
};
export {Decrypt};
async function Decrypt(file) {
const fileBuffer = await new Promise(reslove => {
const reader = new FileReader();
reader.onload = (e) => {
reslove(e.target.result);
};
reader.readAsArrayBuffer(file);
});
const dataView = new DataView(fileBuffer);
if (dataView.getUint32(0, true) !== 0x4e455443 ||
dataView.getUint32(4, true) !== 0x4d414446
) {
console.log({type: "error", data: "not ncm file"});
return;
}
let offset = 10;
const keyData = (() => {
const keyLen = dataView.getUint32(offset, true);
offset += 4;
const cipherText = new Uint8Array(fileBuffer, offset, keyLen).map(
uint8 => uint8 ^ 0x64
);
offset += keyLen;
const plainText = CryptoJS.AES.decrypt(
{ciphertext: CryptoJS.lib.WordArray.create(cipherText)},
CORE_KEY,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
);
const result = new Uint8Array(plainText.sigBytes);
{
const words = plainText.words;
const sigBytes = plainText.sigBytes;
for (let i = 0; i < sigBytes; i++) {
result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
}
}
return result.slice(17);
})();
const keyBox = (() => {
const box = new Uint8Array(Array(256).keys());
const keyDataLen = keyData.length;
let j = 0;
for (let i = 0; i < 256; i++) {
j = (box[i] + j + keyData[i % keyDataLen]) & 0xff;
[box[i], box[j]] = [box[j], box[i]];
}
return box.map((_, i, arr) => {
i = (i + 1) & 0xff;
const si = arr[i];
const sj = arr[(i + si) & 0xff];
return arr[(si + sj) & 0xff];
});
})();
/**
* @typedef {Object} MusicMetaType
* @property {Number} musicId
* @property {String} musicName
* @property {[[String, Number]]} artist
* @property {String} album
* @property {"flac"|"mp3"} format
* @property {String} albumPic
*/
/** @type {MusicMetaType|undefined} */
const musicMeta = (() => {
const metaDataLen = dataView.getUint32(offset, true);
offset += 4;
if (metaDataLen === 0) {
return {};
}
const cipherText = new Uint8Array(fileBuffer, offset, metaDataLen).map(
data => data ^ 0x63
);
offset += metaDataLen;
const plainText = CryptoJS.AES.decrypt(
{
ciphertext: CryptoJS.enc.Base64.parse(
CryptoJS.lib.WordArray.create(cipherText.slice(22)).toString(CryptoJS.enc.Utf8)
)
},
META_KEY,
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}
);
const result = JSON.parse(plainText.toString(CryptoJS.enc.Utf8).slice(6));
result.albumPic = result.albumPic.replace("http:", "https:");
return result;
})();
offset += dataView.getUint32(offset + 5, true) + 13;
const audioData = new Uint8Array(fileBuffer, offset);
const audioDataLen = audioData.length;
for (let cur = 0; cur < audioDataLen; ++cur) {
audioData[cur] ^= keyBox[cur & 0xff];
}
if (musicMeta.format === undefined) {
musicMeta.format = (() => {
const [f, L, a, C] = audioData;
if (f === 0x66 && L === 0x4c && a === 0x61 && C === 0x43) {
return "flac";
}
return "mp3";
})();
}
const mime = audio_mime_type[musicMeta.format];
const musicData = new Blob([audioData], {
type: mime
});
const musicUrl = URL.createObjectURL(musicData);
const artists = [];
musicMeta.artist.forEach(arr => {
artists.push(arr[0]);
});
const filename = artists.join(" & ") + " - " + musicMeta.musicName + "." + musicMeta.format;
return {
meta: musicMeta,
file: musicUrl,
picture: musicMeta.albumPic,
title: musicMeta.musicName,
album: musicMeta.album,
artist: artists.join(" & "),
filename: filename,
mime: mime
};
}

View File

@ -1,125 +0,0 @@
const jsmediatags = require("jsmediatags");
export {Decrypt}
const SEED_MAP = [
[0x4a, 0xd6, 0xca, 0x90, 0x67, 0xf7, 0x52],
[0x5e, 0x95, 0x23, 0x9f, 0x13, 0x11, 0x7e],
[0x47, 0x74, 0x3d, 0x90, 0xaa, 0x3f, 0x51],
[0xc6, 0x09, 0xd5, 0x9f, 0xfa, 0x66, 0xf9],
[0xf3, 0xd6, 0xa1, 0x90, 0xa0, 0xf7, 0xf0],
[0x1d, 0x95, 0xde, 0x9f, 0x84, 0x11, 0xf4],
[0x0e, 0x74, 0xbb, 0x90, 0xbc, 0x3f, 0x92],
[0x00, 0x09, 0x5b, 0x9f, 0x62, 0x66, 0xa1]];
const audio_mime_type = {
mp3: "audio/mpeg",
flac: "audio/flac"
};
async function Decrypt(file) {
// 获取扩展名
let filename_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
let new_ext;
switch (filename_ext) {
case "qmc0":
case "qmc3":
new_ext = "mp3";
break;
case "qmcflac":
new_ext = "flac";
break;
default:
return;
}
const mime = audio_mime_type[new_ext];
// 读取文件
const fileBuffer = await new Promise(reslove => {
const reader = new FileReader();
reader.onload = (e) => {
reslove(e.target.result);
};
reader.readAsArrayBuffer(file);
});
const audioData = new Uint8Array(fileBuffer);
const audioDataLen = audioData.length;
// 转换数据
const seed = new Mask();
for (let cur = 0; cur < audioDataLen; ++cur) {
audioData[cur] ^= seed.NextMask();
}
// 导出
const musicData = new Blob([audioData], {
type: mime
});
const musicUrl = URL.createObjectURL(musicData);
// 读取Meta
let tag = await new Promise(resolve => {
new jsmediatags.Reader(musicData).read({
onSuccess: resolve,
onError: (err) => {
console.log(err);
resolve({tags: {}})
}
});
});
// 处理无标题歌手
let filename_array = file.name.substring(0, file.name.lastIndexOf(".")).split("-");
let title = tag.tags.title;
let artist = tag.tags.artist;
if (filename_array.length > 1) {
if (artist === undefined) artist = filename_array[0].trim();
if (title === undefined) title = filename_array[1].trim();
} else if (filename_array.length === 1) {
if (title === undefined) title = filename_array[0].trim();
}
const filename = artist + " - " + title + "." + new_ext;
// 处理无封面
let pic_url = "";
if (tag.tags.picture !== undefined) {
let pic = new Blob([new Uint8Array(tag.tags.picture.data)], {type: tag.tags.picture.format});
pic_url = URL.createObjectURL(pic);
}
// 返回
return {
filename: filename,
title: title,
artist: artist,
album: tag.tags.album,
file: musicUrl,
picture: pic_url,
mime: mime
}
}
class Mask {
constructor() {
this.x = -1;
this.y = 8;
this.dx = 1;
this.index = -1;
}
NextMask() {
let ret;
this.index++;
if (this.x < 0) {
this.dx = 1;
this.y = (8 - this.y) % 8;
ret = 0xc3
} else if (this.x > 6) {
this.dx = -1;
this.y = 7 - this.y;
ret = 0xd8
} else {
ret = SEED_MAP[this.y][this.x]
}
this.x += this.dx;
if (this.index === 0x8000 || (this.index > 0x8000 && (this.index + 1) % 0x8000 === 0)) {
return this.NextMask()
}
return ret
}
}

View File

@ -1,51 +0,0 @@
const jsmediatags = require("jsmediatags");
export {Decrypt}
const audio_mime_type = {
mp3: "audio/mpeg",
flac: "audio/flac"
};
async function Decrypt(file) {
let tag = await new Promise(resolve => {
new jsmediatags.Reader(file).read({
onSuccess: resolve,
onError: () => {
resolve({tags: {}})
}
});
});
let pic_url = "";
if (tag.tags.picture !== undefined) {
let pic = new Blob([new Uint8Array(tag.tags.picture.data)], {type: tag.tags.picture.format});
pic_url = URL.createObjectURL(pic);
}
let file_url = URL.createObjectURL(file);
let filename_no_ext = file.name.substring(0, file.name.lastIndexOf("."));
let filename_array = filename_no_ext.split("-");
let filename_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
const mime = audio_mime_type[filename_ext];
let title = tag.tags.title;
let artist = tag.tags.artist;
if (filename_array.length > 1) {
if (artist === undefined) artist = filename_array[0].trim();
if (title === undefined) title = filename_array[1].trim();
} else if (filename_array.length === 1) {
if (title === undefined) title = filename_array[0].trim();
}
const filename = artist + " - " + title + "." + filename_ext;
return {
filename: filename,
title: title,
artist: artist,
album: tag.tags.album,
picture: pic_url,
file: file_url,
mime: mime
}
}

View File

@ -1,32 +1,30 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
import { register } from 'register-service-worker';
if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV === 'production' && window.location.protocol === 'https:') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
ready() {
console.log('App is being served from cache by a service worker.');
},
registered () {
console.log('Service worker has been registered.')
registered() {
console.log('Service worker has been registered.');
},
cached () {
console.log('Content has been cached for offline use.')
cached() {
console.log('Content has been cached for offline use.');
},
updatefound () {
console.log('New content is downloading.')
updatefound() {
console.log('New content is downloading.');
},
updated () {
console.log('New content is available; please refresh.')
updated() {
console.log('New content is available.');
window.location.reload();
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
error(error) {
console.error('Error during service worker registration:', error);
},
});
}

66
src/scss/_dark-mode.scss Normal file
View File

@ -0,0 +1,66 @@
/*
* 样式 - 暗黑模式
*/
@media (prefers-color-scheme: dark) {
#app{
color: $dark-text-info;
}
body{
background-color: $dark-bg;
}
// 编辑歌曲信息
.music-cover{
i{
&:hover{
color: $color-checkbox;
}
}
.el-image{
border: 1px solid $dark-border;
}
}
.edit-item{
.label{
}
.value{
}
.input{
input{
background-color: transparent !important;
border-bottom: 1px solid $dark-border;
}
}
i{
&:hover{
color: $color-checkbox;
}
}
}
// footer
#app-footer {
a {
color: lighten($text-comment, 5%);
&:hover{
color: $color-link;
}
}
}
// 自定义样式
// 首页弹窗提示信息的 更新信息 面板
.update-info{
border: 1px solid $dark-btn-bg !important;
.update-title{
color: $dark-text-main;
background-color: $dark-btn-bg !important;
}
.update-content{
color: $dark-text-info;
padding: 10px;
}
}
}

View File

@ -0,0 +1,291 @@
$color-checkbox: $blue;
$color-border-el: #DCDFE6;
$btn-radius: 6px;
/* FORM */
// checkbox
.el-checkbox.is-bordered{
@include border-radius($btn-radius) ;
&:hover{
border-color: $color-checkbox;
.el-checkbox__label{
color: $color-checkbox;
}
}
.el-checkbox__input.is-focus{
.el-checkbox__inner{
border-color: $color-border-el;
}
}
&.is-checked{
background-color: $color-checkbox;
.el-checkbox__label{
color: white;
}
.el-checkbox__inner{
border-color: white;
background-color: white;
&:after{
border-color: $color-checkbox;
}
}
}
}
// el-button
.el-button{
@include border-radius($btn-radius) ;
}
// upload
.el-upload-dragger{
&:hover{
background-color: transparentize($color-checkbox, 0.9);
}
}
.el-upload__tip{
text-align: center;
color: $text-comment;
}
// dialog
.el-dialog{
@include border-radius(5px);
&.el-dialog--center{
.el-dialog__body{
padding: 25px 25px 15px;
}
.el-dialog__footer{
padding: 10px 20px 30px;
}
}
}
@media (prefers-color-scheme: dark) {
// FORM
.el-radio{
&__label{
color: $dark-text-main;
}
&__input{
color: $dark-text-info;
.el-radio__inner{
border-color: $dark-border;
background-color: $dark-btn-bg;
}
}
&.is-checked{
.el-radio__inner{
background-color: $blue;
}
.el-radio__label{
font-weight: bold;
}
}
}
.el-checkbox.is-bordered{
border-color: $dark-border;
color: $dark-text-main;
background-color: $dark-btn-bg;
.el-checkbox__inner{
background-color: $dark-btn-bg-highlight;
border-color: $dark-border-highlight;
}
&:hover{
border-color: $dark-border-highlight;
.el-checkbox__inner{
background-color: $dark-btn-bg-highlight;
border-color: $dark-border-highlight;
}
.el-checkbox__label{
color: $dark-text-info;
}
}
&.is-checked{
background-color: $blue;
.el-checkbox__inner{
border-color: white;
}
.el-checkbox__label{
color: white;
font-weight: bold;
}
&:hover{
border-color: $blue;
.el-checkbox__inner{
background-color: white;
}
}
}
}
// BUTTON
.el-button{
background-color: $dark-btn-bg;
border-color: $dark-border;
color: $dark-text-main;
&:active{
transform: translateY(2px);
}
&--default{
&.is-plain {
background-color: $dark-btn-bg;
&:hover {
background-color: $blue;
border-color: $blue;
color: white;
}
}
&.is-circle {
background-color: $dark-blue;
border-color: $dark-blue;
&:hover {
background-color: $blue;
border-color: $blue;
color: white;
}
}
}
&--success{
&.is-plain {
background-color: $dark-btn-bg;
&:hover {
background-color: $green;
border-color: $green;
color: white;
}
}
&.is-circle {
background-color: $dark-green;
border-color: $dark-green;
&:hover {
background-color: $green;
border-color: $green;
color: white;
}
}
}
&--danger{
&.is-plain{
border-color: $dark-border;
background-color: $dark-btn-bg;
&:hover{
background-color: $red;
border-color: $red;
}
}
&.is-circle {
background-color: $dark-red;
border-color: $dark-red;
&:hover {
background-color: $red;
border-color: $red;
color: white;
}
}
}
}
// 文件拖放区
.el-upload__tip{
color: $dark-text-info;
}
.el-upload-dragger{
background-color: $dark-uploader-bg;
border-color: $dark-border;
.el-upload__text{
color: $dark-text-info;
}
&:hover{
background: $dark-uploader-bg-highlight;
border-color: $dark-border-highlight;
}
}
// TABLE
.el-table{
background-color: $dark-bg-td;
&:before{ // 去除表格末尾的横线
content: none;
}
&__header{
th{
border-bottom-color: $dark-border !important;
}
}
th.el-table__cell{
background-color: $dark-bg-th;
color: $dark-text-info;
}
td{
border-bottom-color: $dark-border !important;
}
tr{
background-color: $dark-bg-td;
color: $dark-text-main;
&:hover{
td{
background-color: $dark-bg-th !important;
}
}
}
}
// ALERT
.el-notification{
background-color: $dark-btn-bg-highlight;
border-color: $dark-border;
&__title{
color: white;
}
&__content{
color: $dark-text-info;
}
}
// DIALOG
.el-dialog{
background-color: $dark-dialog-bg;
.el-dialog__header{
.el-dialog__title{
color: $dark-text-main;
}
}
.el-dialog__body{
color: $dark-text-main;
.el-input{
.el-input__inner{
border-color: $dark-border;
color: $dark-text-main;
background-color: $dark-btn-bg;
}
.el-input__suffix{
.el-input__suffix-inner{
}
}
.el-input__count{
.el-input__count-inner{
background-color: transparent;
}
}
}
}
.item-desc{
color: $dark-text-info;
}
}
}

18
src/scss/_gaps.scss Normal file
View File

@ -0,0 +1,18 @@
/*
* 间隔工具集
*/
$gap: 5px;
@for $item from 0 through 8 {
.mt-#{$item} { margin-top : $gap * $item !important;}
.mb-#{$item} { margin-bottom : $gap * $item !important;}
.ml-#{$item} { margin-left : $gap * $item !important;}
.mr-#{$item} { margin-right : $gap * $item !important;}
.m-#{$item} { margin : $gap * $item !important;}
.pt-#{$item} { padding-top : $gap * $item !important;}
.pb-#{$item} { padding-bottom : $gap * $item !important;}
.pl-#{$item} { padding-left : $gap * $item !important;}
.pr-#{$item} { padding-right : $gap * $item !important;}
.p-#{$item} { padding : $gap * $item !important;}
}

73
src/scss/_utility.scss Normal file
View File

@ -0,0 +1,73 @@
// box-shadow
@mixin box-shadow($value...){
-webkit-box-shadow: $value;
-moz-box-shadow: $value;
box-shadow: $value;
}
// border-radius
@mixin border-radius($corner...){
-webkit-border-radius: $corner;
-moz-border-radius: $corner;
border-radius: $corner;
}
@mixin clearfix(){
&:after{
content: '';
display: block;
clear: both;
visibility: hidden;
}
}
@mixin transform($value){
-webkit-transform: $value;
-moz-transform: $value;
-ms-transform: $value;
-o-transform: $value;
transform: $value;
}
@mixin transition($value...){
-webkit-transition: $value;
-moz-transition: $value;
-ms-transition: $value;
-o-transition: $value;
transition: $value;
}
@mixin animation($value){
animation: $value;
-webkit-animation: $value;
}
@mixin linear-gradient($direct, $colors){
background: linear-gradient($direct, $colors);
background: -webkit-linear-gradient($direct, $colors);
background: -moz-linear-gradient($direct, $colors);
}
@mixin backdrop-filter($value){
backdrop-filter: $value ;
-webkit-backdrop-filter: $value;
}
/*
Extension
*/
.unselectable {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.btn-like{
cursor: pointer;
&:active{
@include transform(translateY(2px))
}
}

34
src/scss/_variables.scss Normal file
View File

@ -0,0 +1,34 @@
// COLORS
$blue : #409EFF;
$red : #F56C6C;
$green : #85ce61;
// TEXT COLOR
$text-main : #2C3E50;
$text-copyright : #777;
$text-comment : #999;
$color-link : $blue;
// FONT SIZE
$fz-main: 14px;
$fz-mini-title: 13px;
$fz-mini-content: 12px;
// DARK MODE
$dark-border : lighten(black, 25%);
$dark-border-highlight : lighten(black, 55%);
$dark-bg : lighten(black, 10%);
$dark-text-main : lighten(black, 90%);
$dark-text-info : lighten(black, 60%);
$dark-uploader-bg : lighten(black, 13%);
$dark-dialog-bg : lighten(black, 15%);
$dark-uploader-bg-highlight : lighten(black, 18%);
$dark-btn-bg : lighten(black, 20%);
$dark-btn-bg-highlight : lighten(black, 30%);
$dark-bg-th : lighten(black, 18%);
$dark-bg-td : lighten(black, 13%);
$dark-color-link : $green;
$dark-blue : darken(desaturate($blue, 40%), 30%);
$dark-red : darken(desaturate($red, 50%), 30%);
$dark-green : darken(desaturate($green, 30%), 30%);

144
src/scss/unlock-music.scss Normal file
View File

@ -0,0 +1,144 @@
@import "variables";
@import "utility";
@import "gaps";
@import "element-ui-overwrite";
// MAIN CONTENT
body{
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
font-family: "PingFang SC", "微软雅黑", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
font-size: $fz-main;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
text-align: center;
color: $text-main;
padding: 30px;
}
// 音频文件操作
#app-control {
margin-top: 20px;
}
// 音频播放
audio{
margin-top: 20px;
}
.table-content{
margin-top: 20px;
}
// 编辑歌曲信息
.music-cover{
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
flex-flow: column nowrap;
i{
margin-top: 10px;
@extend .btn-like;
&:hover{
color: $color-checkbox;
}
}
.el-image{
padding: 5px;
@include border-radius(5px);
border: 1px solid $color-border-el;
width: 150px;
height: 150px;
}
}
.edit-item{
display: flex;
justify-content: flex-start;
align-items: center;
.label{
font-weight: bold;
width: 80px;
text-align: right;
flex-shrink: 0;
}
.value{
padding: 5px 0;
height: 20px;
line-height: 20px;
margin-left: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.input{
margin-left: 10px;
input{
font-family: inherit;
height: 30px;
line-height: 20px;
@include border-radius(0);
border: none;
border-bottom: 1px solid $color-border-el;
padding: 5px 5px;
}
}
i{
margin-left: 10px;
@extend .btn-like;
&:hover{
color: $color-checkbox;
}
}
}
.tip{
margin-top: 20px;
color: $text-comment;
font-size: $fz-mini-content;
a{
color: inherit;
}
}
// footer
#app-footer {
margin-top: 40px;
text-align: center;
color: $text-copyright;
line-height: 1.3;
font-size: $fz-mini-content;
a {
padding-left: 0.2rem;
padding-right: 0.2rem;
color: darken($text-copyright, 10%);
&:hover{
color: $color-link;
}
}
}
// 首页弹窗提示信息的 更新信息 面板
.update-info{
@include border-radius(8px);
overflow: hidden;
border: 1px solid $color-border-el;
margin: 10px 0;
.update-title{
font-size: $fz-mini-title;
padding: 3px 10px;
background-color: $color-border-el;
}
.update-content{
font-size: $fz-mini-content;
line-height: 1.5;
padding: 5px 8px;
}
}
@import "dark-mode"; // dark-mode 放在 normal 后面以获得更高优先级

23
src/shims-browser-id3-writer.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
declare module 'browser-id3-writer' {
export default class ID3Writer {
constructor(buffer: Buffer | ArrayBuffer);
setFrame(name: string, value: string | object | string[]);
addTag(): Uint8Array;
}
}
declare module 'metaflac-js' {
export default class Metaflac {
constructor(buffer: Buffer);
setTag(field: string);
removeTag(name: string);
importPictureFromBuffer(picture: Buffer);
save(): Buffer;
}
}

54
src/shims-fs.d.ts vendored Normal file
View File

@ -0,0 +1,54 @@
export interface FileSystemGetFileOptions {
create?: boolean;
}
interface FileSystemCreateWritableOptions {
keepExistingData?: boolean;
}
interface FileSystemRemoveOptions {
recursive?: boolean;
}
interface FileSystemFileHandle {
getFile(): Promise<File>;
createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>;
}
enum WriteCommandType {
write = 'write',
seek = 'seek',
truncate = 'truncate',
}
interface WriteParams {
type: WriteCommandType;
size?: number;
position?: number;
data: BufferSource | Blob | string;
}
type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams;
interface FileSystemWritableFileStream extends WritableStream {
write(data: FileSystemWriteChunkType): Promise<undefined>;
seek(position: number): Promise<undefined>;
truncate(size: number): Promise<undefined>;
close(): Promise<undefined>; // should be implemented in WritableStream
}
export declare interface FileSystemDirectoryHandle {
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>;
removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<undefined>;
}
declare global {
interface Window {
showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>;
}
}

15
src/shims-tsx.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

4
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

View File

@ -0,0 +1,15 @@
export function MergeUint8Array(array: Uint8Array[]): Uint8Array {
let length = 0;
array.forEach((item) => {
length += item.length;
});
let mergedArray = new Uint8Array(length);
let offset = 0;
array.forEach((item) => {
mergedArray.set(item, offset);
offset += item.length;
});
return mergedArray;
}

View File

@ -0,0 +1 @@
export const extractQQMusicMeta = jest.fn();

View File

@ -0,0 +1,4 @@
export const storage = {
loadJooxUUID: jest.fn(),
saveJooxUUID: jest.fn(),
};

113
src/utils/api.ts Normal file
View File

@ -0,0 +1,113 @@
export const IXAREA_API_ENDPOINT = 'https://um-api.ixarea.com';
export interface UpdateInfo {
Found: boolean;
HttpsFound: boolean;
Version: string;
URL: string;
Detail: string;
}
export async function checkUpdate(version: string): Promise<UpdateInfo> {
const resp = await fetch(IXAREA_API_ENDPOINT + '/music/app-version', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ Version: version }),
});
return await resp.json();
}
export interface CoverInfo {
Id: string;
Type: number;
}
export async function queryAlbumCover(title: string, artist?: string, album?: string): Promise<CoverInfo> {
const endpoint = IXAREA_API_ENDPOINT + '/music/qq-cover';
const params = new URLSearchParams([
['Title', title],
['Artist', artist ?? ''],
['Album', album ?? ''],
]);
const resp = await fetch(`${endpoint}?${params.toString()}`);
return await resp.json();
}
export interface TrackInfo {
id: number;
type: number;
mid: string;
name: string;
title: string;
subtitle: string;
singer: {
id: number;
mid: string;
name: string;
title: string;
type: number;
uin: number;
}[];
album: {
id: number;
mid: string;
name: string;
title: string;
subtitle: string;
time_public: string;
pmid: string;
};
interval: number;
index_cd: number;
index_album: number;
}
export interface SongItemInfo {
title: string;
content: {
value: string;
}[];
}
export interface SongInfoResponse {
info: {
company: SongItemInfo;
genre: SongItemInfo;
intro: SongItemInfo;
lan: SongItemInfo;
pub_time: SongItemInfo;
};
extras: {
name: string;
transname: string;
subtitle: string;
from: string;
wikiurl: string;
};
track_info: TrackInfo;
}
export interface RawQMBatchResponse<T> {
code: number;
ts: number;
start_ts: number;
traceid: string;
req_1: {
code: number;
data: T;
};
}
export async function querySongInfoById(id: string | number): Promise<SongInfoResponse> {
const url = `${IXAREA_API_ENDPOINT}/meta/qq-music-raw/${id}`;
const result: RawQMBatchResponse<SongInfoResponse> = await fetch(url).then((r) => r.json());
if (result.code === 0 && result.req_1.code === 0) {
return result.req_1.data;
}
throw new Error('请求信息失败');
}
export function getQMImageURLFromPMID(pmid: string, type = 1): string {
return `${IXAREA_API_ENDPOINT}/music/qq-cover/${type}/${pmid}`;
}

156
src/utils/qm_meta.ts Normal file
View File

@ -0,0 +1,156 @@
import { IAudioMetadata, parseBlob as metaParseBlob } from 'music-metadata-browser';
import iconv from 'iconv-lite';
import {
GetCoverFromFile,
GetImageFromURL,
GetMetaFromFile,
WriteMetaToFlac,
WriteMetaToMp3,
AudioMimeType,
split_regex,
} from '@/decrypt/utils';
import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api';
interface MetaResult {
title: string;
artist: string;
album: string;
imgUrl: string;
blob: Blob;
}
const fromGBK = (text?: string) => iconv.decode(new Buffer(text || ''), 'gbk');
/**
*
* @param musicBlob
* @param name
* @param ext
* @param id ID<code>number</code>
* @returns Promise
*/
export async function extractQQMusicMeta(
musicBlob: Blob,
name: string,
ext: string,
id?: number | string,
): Promise<MetaResult> {
const musicMeta = await metaParseBlob(musicBlob);
for (let metaIdx in musicMeta.native) {
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue;
if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) {
console.warn('try using gbk encoding to decode meta');
musicMeta.common.artist = '';
if (!musicMeta.common.artists) {
musicMeta.common.artist = fromGBK(musicMeta.common.artist);
}
else {
musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join();
}
musicMeta.common.title = fromGBK(musicMeta.common.title);
musicMeta.common.album = fromGBK(musicMeta.common.album);
}
}
if (id && id !== '0') {
try {
return fetchMetadataFromSongId(id, ext, musicMeta, musicBlob);
} catch (e) {
console.warn('在线获取曲目信息失败,回退到本地 meta 提取', e);
}
}
const info = GetMetaFromFile(name, musicMeta.common.title, musicMeta.common.artist);
info.artist = info.artist || '';
let imageURL = GetCoverFromFile(musicMeta);
if (!imageURL) {
imageURL = await getCoverImage(info.title, info.artist, musicMeta.common.album);
}
return {
title: info.title,
artist: info.artist,
album: musicMeta.common.album || '',
imgUrl: imageURL,
blob: await writeMetaToAudioFile({
title: info.title,
artists: info.artist.split(split_regex),
ext,
imageURL,
musicMeta,
blob: musicBlob,
}),
};
}
async function fetchMetadataFromSongId(
id: number | string,
ext: string,
musicMeta: IAudioMetadata,
blob: Blob,
): Promise<MetaResult> {
const info = await querySongInfoById(id);
const imageURL = getQMImageURLFromPMID(info.track_info.album.pmid);
const artists = info.track_info.singer.map((singer) => singer.name);
return {
title: info.track_info.title,
artist: artists.join(','),
album: info.track_info.album.name,
imgUrl: imageURL,
blob: await writeMetaToAudioFile({
title: info.track_info.title,
artists,
ext,
imageURL,
musicMeta,
blob,
}),
};
}
async function getCoverImage(title: string, artist?: string, album?: string): Promise<string> {
try {
const data = await queryAlbumCover(title, artist, album);
return getQMImageURLFromPMID(data.Id, data.Type);
} catch (e) {
console.warn(e);
}
return '';
}
interface NewAudioMeta {
title: string;
artists: string[];
ext: string;
musicMeta: IAudioMetadata;
blob: Blob;
imageURL: string;
}
async function writeMetaToAudioFile(info: NewAudioMeta): Promise<Blob> {
try {
const imageInfo = await GetImageFromURL(info.imageURL);
if (!imageInfo) {
console.warn('获取图像失败');
}
const newMeta = { picture: imageInfo?.buffer, title: info.title, artists: info.artists };
const buffer = Buffer.from(await info.blob.arrayBuffer());
const mime = AudioMimeType[info.ext] || AudioMimeType.mp3;
if (info.ext === 'mp3') {
return new Blob([WriteMetaToMp3(buffer, newMeta, info.musicMeta)], { type: mime });
} else if (info.ext === 'flac') {
return new Blob([WriteMetaToFlac(buffer, newMeta, info.musicMeta)], { type: mime });
} else {
console.info('writing metadata for ' + info.ext + ' is not being supported for now');
}
} catch (e) {
console.warn('Error while appending cover image to file ' + e);
}
return info.blob;
}

3
src/utils/storage.ts Normal file
View File

@ -0,0 +1,3 @@
import storageFactory from './storage/StorageFactory';
export const storage = storageFactory();

View File

@ -0,0 +1,17 @@
export const KEY_PREFIX = 'um.conf.';
const KEY_JOOX_UUID = `${KEY_PREFIX}joox.uuid`;
export default abstract class BaseStorage {
protected abstract save<T>(name: string, value: T): Promise<void>;
protected abstract load<T>(name: string, defaultValue: T): Promise<T>;
public abstract getAll(): Promise<Record<string, any>>;
public abstract setAll(obj: Record<string, any>): Promise<void>;
public saveJooxUUID(uuid: string): Promise<void> {
return this.save(KEY_JOOX_UUID, uuid);
}
public loadJooxUUID(defaultValue: string = ''): Promise<string> {
return this.load(KEY_JOOX_UUID, defaultValue);
}
}

View File

@ -0,0 +1,43 @@
import BaseStorage, { KEY_PREFIX } from './BaseStorage';
export default class BrowserNativeStorage extends BaseStorage {
public static get works() {
return typeof localStorage !== 'undefined' && localStorage.getItem;
}
protected async load<T>(name: string, defaultValue: T): Promise<T> {
const result = localStorage.getItem(name);
if (result === null) {
return defaultValue;
}
try {
return JSON.parse(result);
} catch {
return defaultValue;
}
}
protected async save<T>(name: string, value: T): Promise<void> {
localStorage.setItem(name, JSON.stringify(value));
}
public async getAll(): Promise<Record<string, any>> {
const result = {};
for (const [key, value] of Object.entries(localStorage)) {
if (key.startsWith(KEY_PREFIX)) {
try {
Object.assign(result, { [key]: JSON.parse(value) });
} catch {
// ignored
}
}
}
return result;
}
public async setAll(obj: Record<string, any>): Promise<void> {
for (const [key, value] of Object.entries(obj)) {
await this.save(key, value);
}
}
}

View File

@ -0,0 +1,47 @@
import BaseStorage, { KEY_PREFIX } from './BaseStorage';
declare var chrome: any;
export default class ChromeExtensionStorage extends BaseStorage {
static get works(): boolean {
return typeof chrome !== 'undefined' && Boolean(chrome?.storage?.local?.set);
}
protected async load<T>(name: string, defaultValue: T): Promise<T> {
return new Promise((resolve) => {
chrome.storage.local.get({ [name]: defaultValue }, (result: any) => {
if (Object.prototype.hasOwnProperty.call(result, name)) {
resolve(result[name]);
} else {
resolve(defaultValue);
}
});
});
}
protected async save<T>(name: string, value: T): Promise<void> {
return new Promise((resolve) => {
chrome.storage.local.set({ [name]: value }, resolve);
});
}
public async getAll(): Promise<Record<string, any>> {
return new Promise((resolve) => {
chrome.storage.local.get(null, (obj: Record<string, any>) => {
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
if (key.startsWith(KEY_PREFIX)) {
result[key] = value;
}
}
resolve(result);
});
});
}
public async setAll(obj: Record<string, any>): Promise<void> {
return new Promise((resolve) => {
chrome.storage.local.set(obj, resolve);
});
}
}

View File

@ -0,0 +1,32 @@
import BaseStorage from './BaseStorage';
export default class InMemoryStorage extends BaseStorage {
private values = new Map<string, any>();
protected async load<T>(name: string, defaultValue: T): Promise<T> {
if (this.values.has(name)) {
return this.values.get(name);
}
return defaultValue;
}
protected async save<T>(name: string, value: T): Promise<void> {
this.values.set(name, value);
}
public async getAll(): Promise<Record<string, any>> {
const result = {};
this.values.forEach((value, key) => {
Object.assign(result, {
[key]: value,
});
});
return result;
}
public async setAll(obj: Record<string, any>): Promise<void> {
for (const [key, value] of Object.entries(obj)) {
this.values.set(key, value);
}
}
}

View File

@ -0,0 +1,13 @@
import BaseStorage from './BaseStorage';
import BrowserNativeStorage from './BrowserNativeStorage';
import ChromeExtensionStorage from './ChromeExtensionStorage';
import InMemoryStorage from './InMemoryStorage';
export default function storageFactory(): BaseStorage {
if (ChromeExtensionStorage.works) {
return new ChromeExtensionStorage();
} else if (BrowserNativeStorage.works) {
return new BrowserNativeStorage();
}
return new InMemoryStorage();
}

80
src/utils/utils.ts Normal file
View File

@ -0,0 +1,80 @@
import { DecryptResult } from '@/decrypt/entity';
import { FileSystemDirectoryHandle } from '@/shims-fs';
export enum FilenamePolicy {
ArtistAndTitle,
TitleOnly,
TitleAndArtist,
SameAsOriginal,
}
export const FilenamePolicies: { key: FilenamePolicy; text: string }[] = [
{ key: FilenamePolicy.ArtistAndTitle, text: '歌手-歌曲名' },
{ key: FilenamePolicy.TitleOnly, text: '歌曲名' },
{ key: FilenamePolicy.TitleAndArtist, text: '歌曲名-歌手' },
{ key: FilenamePolicy.SameAsOriginal, text: '同源文件名' },
];
export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy): string {
switch (policy) {
case FilenamePolicy.TitleOnly:
return `${data.title}.${data.ext}`;
case FilenamePolicy.TitleAndArtist:
return `${data.title} - ${data.artist}.${data.ext}`;
case FilenamePolicy.SameAsOriginal:
return `${data.rawFilename}.${data.ext}`;
default:
case FilenamePolicy.ArtistAndTitle:
return `${data.artist} - ${data.title}.${data.ext}`;
}
}
export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) {
let filename = GetDownloadFilename(data, policy);
// prevent filename exist
try {
await dir.getFileHandle(filename);
filename = `${new Date().getTime()} - ${filename}`;
} catch (e) {}
const file = await dir.getFileHandle(filename, { create: true });
const w = await file.createWritable();
await w.write(data.blob);
await w.close();
}
export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) {
const a = document.createElement('a');
a.href = data.file;
a.download = GetDownloadFilename(data, policy);
document.body.append(a);
a.click();
a.remove();
}
export function RemoveBlobMusic(data: DecryptResult) {
URL.revokeObjectURL(data.file);
if (data.picture?.startsWith('blob:')) {
URL.revokeObjectURL(data.picture);
}
}
export class DecryptQueue {
private readonly pending: (() => Promise<void>)[];
constructor() {
this.pending = [];
}
queue(fn: () => Promise<void>) {
this.pending.push(fn);
this.consume();
}
private consume() {
const fn = this.pending.shift();
if (fn)
fn()
.then(() => this.consume)
.catch(console.error);
}
}

4
src/utils/worker.ts Normal file
View File

@ -0,0 +1,4 @@
import { expose } from 'threads/worker';
import { Decrypt } from '@/decrypt';
expose(Decrypt);

264
src/view/Home.vue Normal file
View File

@ -0,0 +1,264 @@
<template>
<div>
<file-selector @error="showFail" @success="showSuccess" />
<div id="app-control">
<el-row class="mb-3">
<span>歌曲命名格式</span>
<el-radio v-for="k in FilenamePolicies" :key="k.key" v-model="filename_policy" :label="k.key">
{{ k.text }}
</el-radio>
</el-row>
<el-row>
<edit-dialog
:show="showEditDialog"
:picture="editing_data.picture"
:title="editing_data.title"
:artist="editing_data.artist"
:album="editing_data.album"
:albumartist="editing_data.albumartist"
:genre="editing_data.genre"
@cancel="showEditDialog = false"
@ok="handleEdit"
></edit-dialog>
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
<el-tooltip class="item" effect="dark" placement="top">
<div slot="content">
<span> 部分解密方案需要设定解密参数 </span>
</div>
<el-button icon="el-icon-s-tools" plain @click="showConfigDialog = true">解密设定</el-button>
</el-tooltip>
<el-button icon="el-icon-download" plain @click="handleDownloadAll">下载全部</el-button>
<el-button icon="el-icon-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
<el-tooltip class="item" effect="dark" placement="top-start">
<div slot="content">
<span v-if="instant_save">工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}</span>
<span v-else>
当您使用此工具进行大量文件解锁的时候建议开启此选项<br />
开启后解锁结果将不会存留于浏览器中防止内存不足
</span>
</div>
<el-checkbox v-model="instant_save" type="success" border class="ml-2">立即保存</el-checkbox>
</el-tooltip>
</el-row>
</div>
<audio :autoplay="playing_auto" :src="playing_url" controls />
<PreviewTable
class="table-content"
:policy="filename_policy"
:table-data="tableData"
@download="saveFile"
@edit="editFile"
@play="changePlaying" />
</div>
</template>
<script>
import FileSelector from '@/component/FileSelector';
import PreviewTable from '@/component/PreviewTable';
import ConfigDialog from '@/component/ConfigDialog';
import EditDialog from '@/component/EditDialog';
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
import { GetImageFromURL, RewriteMetaToMp3, RewriteMetaToFlac, AudioMimeType, split_regex } from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export default {
name: 'Home',
components: {
FileSelector,
PreviewTable,
ConfigDialog,
EditDialog,
},
data() {
return {
showConfigDialog: false,
showEditDialog: false,
editing_data: { picture: '', title: '', artist: '', album: '', albumartist: '', genre: '' },
tableData: [],
playing_url: '',
playing_auto: false,
filename_policy: FilenamePolicy.ArtistAndTitle,
instant_save: false,
FilenamePolicies,
dir: null,
};
},
watch: {
instant_save(val) {
if (val) this.showDirectlySave();
},
},
methods: {
async showSuccess(data) {
if (this.instant_save) {
await this.saveFile(data);
RemoveBlobMusic(data);
} else {
this.tableData.push(data);
this.$notify.success({
title: '解锁成功',
message: '成功解锁 ' + data.title,
duration: 3000,
});
}
if (process.env.NODE_ENV === 'production') {
let _rp_data = [data.title, data.artist, data.album];
window._paq.push(['trackEvent', 'Unlock', data.rawExt + ',' + data.mime, JSON.stringify(_rp_data)]);
}
},
showFail(errInfo, filename) {
console.error(errInfo, filename);
this.$notify.error({
title: '出现问题',
message:
errInfo +
'' +
filename +
',参考<a target="_blank" href="https://git.unlock-music.dev/um/web/wiki/使用提示">使用提示</a>',
dangerouslyUseHTMLString: true,
duration: 6000,
});
if (process.env.NODE_ENV === 'production') {
window._paq.push(['trackEvent', 'Error', String(errInfo), filename]);
}
},
changePlaying(url) {
this.playing_url = url;
this.playing_auto = true;
},
handleDeleteAll() {
this.tableData.forEach((value) => {
RemoveBlobMusic(value);
});
this.tableData = [];
},
handleDecryptionConfig() {
this.showConfigDialog = true;
},
handleDownloadAll() {
let index = 0;
let c = setInterval(() => {
if (index < this.tableData.length) {
this.saveFile(this.tableData[index]);
index++;
} else {
clearInterval(c);
}
}, 300);
},
async handleEdit(data) {
this.showEditDialog = false;
URL.revokeObjectURL(this.editing_data.file);
if (data.picture) {
URL.revokeObjectURL(this.editing_data.picture);
this.editing_data.picture = URL.createObjectURL(data.picture);
}
this.editing_data.title = data.title;
this.editing_data.artist = data.artist;
this.editing_data.album = data.album;
let writeSuccess = true;
let notifyMsg = '成功修改 ' + this.editing_data.title;
try {
const musicMeta = await metaParseBlob(new Blob([this.editing_data.blob], { type: mime }));
let imageInfo = undefined;
if (this.editing_data.picture !== '') {
imageInfo = await GetImageFromURL(this.editing_data.picture);
if (!imageInfo) {
console.warn('获取图像失败', this.editing_data.picture);
}
}
const newMeta = {
picture: imageInfo?.buffer,
title: data.title,
artists: data.artist.split(split_regex),
album: data.album,
albumartist: data.albumartist,
genre: data.genre.split(split_regex),
};
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
if (this.editing_data.ext === 'mp3') {
this.editing_data.blob = new Blob([RewriteMetaToMp3(buffer, newMeta, musicMeta)], { type: mime });
} else if (this.editing_data.ext === 'flac') {
this.editing_data.blob = new Blob([RewriteMetaToFlac(buffer, newMeta, musicMeta)], { type: mime });
} else {
writeSuccess = undefined;
notifyMsg = this.editing_data.ext + '类型文件暂时不支持修改音乐标签';
}
} catch (e) {
writeSuccess = false;
notifyMsg = '修改' + this.editing_data.title + '未能完成。在写入新的元数据时发生错误:' + e;
}
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
if (writeSuccess === true) {
this.$notify.success({
title: '修改成功',
message: notifyMsg,
duration: 3000,
});
} else if (writeSuccess === false) {
this.$notify.error({
title: '修改失败',
message: notifyMsg,
duration: 3000,
});
} else {
this.$notify.warning({
title: '修改取消',
message: notifyMsg,
duration: 3000,
});
}
},
async editFile(data) {
this.editing_data = data;
const musicMeta = await metaParseBlob(this.editing_data.blob);
this.editing_data.albumartist = musicMeta.common.albumartist || '';
this.editing_data.genre = musicMeta.common.genre?.toString() || '';
this.showEditDialog = true;
},
async saveFile(data) {
if (this.dir) {
await DirectlyWriteFile(data, this.filename_policy, this.dir);
this.$notify({
title: '保存成功',
message: data.title,
position: 'top-left',
type: 'success',
duration: 3000,
});
} else {
DownloadBlobMusic(data, this.filename_policy);
}
},
async showDirectlySave() {
if (!window.showDirectoryPicker) return;
try {
await this.$confirm('您的浏览器支持文件直接保存到磁盘,是否使用?', '新特性提示', {
confirmButtonText: '使用',
cancelButtonText: '不使用',
type: 'warning',
center: true,
});
} catch (e) {
console.log(e);
return;
}
try {
this.dir = await window.showDirectoryPicker();
const test_filename = '__unlock_music_write_test.txt';
await this.dir.getFileHandle(test_filename, { create: true });
await this.dir.removeEntry(test_filename);
} catch (e) {
console.error(e);
}
},
},
};
</script>

1
testdata/mflac0_rc4_key.bin vendored Normal file
View File

@ -0,0 +1 @@
dRzX3p5ZYqAlp7lLSs9Zr0rw1iEZy23bB670x4ch2w97x14Zwpk1UXbKU4C2sOS7uZ0NB5QM7ve9GnSrr2JHxP74hVNONwVV77CdOOVb807317KvtI5Yd6h08d0c5W88rdV46C235YGDjUSZj5314YTzy0b6vgh4102P7E273r911Nl464XV83Hr00rkAHkk791iMGSJH95GztN28u2Nv5s9Xx38V69o4a8aIXxbx0g1EM0623OEtbtO9zsqCJfj6MhU7T8iVS6M3q19xhq6707E6r7wzPO6Yp4BwBmgg4F95Lfl0vyF7YO6699tb5LMnr7iFx29o98hoh3O3Rd8h9Juu8P1wG7vdnO5YtRlykhUluYQblNn7XwjBJ53HAyKVraWN5dG7pv7OMl1s0RykPh0p23qfYzAAMkZ1M422pEd07TA9OCKD1iybYxWH06xj6A8mzmcnYGT9P1a5Ytg2EF5LG3IknL2r3AUz99Y751au6Cr401mfAWK68WyEBe5

1
testdata/mflac0_rc4_key_raw.bin vendored Normal file
View File

@ -0,0 +1 @@
ZFJ6WDNwNVrjEJZB1o6QjkQV2ZbHSw/2Eb00q1+4z9SVWYyFWO1PcSQrJ5326ubLklmk2ab3AEyIKNUu8DFoAoAc9dpzpTmc+pdkBHjM/bW2jWx+dCyC8vMTHE+DHwaK14UEEGW47ZXMDi7PRCQ2Jpm/oXVdHTIlyrc+bRmKfMith0L2lFQ+nW8CCjV6ao5ydwkZhhNOmRdrCDcUXSJH9PveYwra9/wAmGKWSs9nemuMWKnbjp1PkcxNQexicirVTlLX7PVgRyFyzNyUXgu+R2S4WTmLwjd8UsOyW/dc2mEoYt+vY2lq1X4hFBtcQGOAZDeC+mxrN0EcW8tjS6P4TjOjiOKNMxIfMGSWkSKL3H7z5K7nR1AThW20H2bP/LcpsdaL0uZ/js1wFGpdIfFx9rnLC78itL0WwDleIqp9TBMX/NwakGgIPIbjBwfgyD8d8XKYuLEscIH0ZGdjsadB5XjybgdE3ppfeFEcQiqpnodlTaQRm3KDIF9ATClP0mTl8XlsSojsZ468xseS1Ib2iinx/0SkK3UtJDwp8DH3/+ELisgXd69Bf0pve7wbrQzzMUs9/Ogvvo6ULsIkQfApJ8cSegDYklzGXiLNH7hZYnXDLLSNejD7NvQouULSmGsBbGzhZ5If0NP/6AhSbpzqWLDlabTDgeWWnFeZpBnlK6SMxo+YFFk1Y0XLKsd69+jj

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