Compare commits

...

413 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
115 changed files with 36506 additions and 6488 deletions

View File

@ -3,68 +3,23 @@ kind: pipeline
type: docker
name: default
clone:
depth: 1
steps:
- name: restore-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
restore: true
mount:
- ./node_modules
- name: installDependencies
image: node:lts
commands:
- npm config set registry http://registry.npm.taobao.org --global
- npm install
- name: build
image: node:lts
image: node:16.18-bullseye
commands:
- npm run build
- tar -czf dist.tar.gz ./dist/*
- 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: release
image: plugins/gitea-release
settings:
base_url: https://git.ixarea.com
files:
- dist.tar.gz
api_key:
from_secret: gitea_token
checksum:
- sha256
when:
event: [tag]
- name: deploy
image: plugins/s3
settings:
bucket: unlock-music
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
source: dist/**/*
target: /
path_style: true
endpoint: https://fs.sz2.ixarea.com
- name: rebuild-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
rebuild: true
mount:
- ./node_modules
volumes:
- name: cache
host:
path: /tmp/cache
- 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

100
README.md
View File

@ -1,35 +1,77 @@
# Unlock Music 音乐解锁
- Unlock encrypted music file in browser.
- 在浏览器中解锁加密的音乐文件。
- [Online Demo](https://tool.ixarea.com/music)
[![Build Status](https://ci.ixarea.com/api/badges/ix64/unlock-music/status.svg)](https://ci.ixarea.com/ix64/unlock-music)
[![Build Status](https://ci.unlock-music.dev/api/badges/um/web/status.svg)](https://ci.unlock-music.dev/um/web)
## Features
- [x] Unlock in browser 在浏览器中解锁
- [x] QQMusic File QQ音乐格式 (.qmc0/.qmc3/.qmcflac/.qmcogg)
- [ ] QQMusic New Format QQ音乐新格式
- [x] .mflac (Partial 部分支持)
- [ ] .mgg
- [x] Netease Format 网易云音乐格式 (.ncm)
- [x] Drag and Drop 拖放文件
- [x] Play instantly 在线播放
- [x] Batch unlocking 批量解锁
- [x] Progressive Web App 渐进式Web应用
- [x] Complete ID3 for ncm 补全ncm的ID3信息
- [ ] Multi-language 多语言
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。
- 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入!
- CI 自动构建已经部署,可以在 [UM-Packages] 下载
## Project setup
```
npm install
```
[授权协议]: 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/
### Compiles and hot-reloads for development
```
npm run serve
```
## 特性
### Compiles and minifies for production
```
npm run build
```
### 支持的格式
- [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")

37593
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,60 @@
{
"name": "unlock-music",
"version": "0.2.0",
"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.3.0",
"core-js": "^2.6.10",
"crypto-js": "^3.1.9-1",
"element-ui": "^2.11.1",
"music-metadata-browser": "^1.8.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.11.0",
"@vue/cli-plugin-pwa": "^3.11.0",
"@vue/cli-service": "^3.11.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: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -1,70 +1,89 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width,initial-scale=1.0" name="viewport">
<script>var _paq = window._paq || [];
_paq.push(['trackPageView'], ['enableLinkTracking'], ['setSiteId', '2'],
['setTrackerUrl', 'https://stats.ixarea.com/ixarea-stats/report']);
</script>
<script async src="https://stats.ixarea.com/ixarea-stats.js"></script>
<title>音乐解锁 - By IXarea</title>
<meta content="音乐,解锁,ncm,qmc,qmc0,qmc3,qmcflac,qmcogg,mflac,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>
#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>
<img alt=""
src="https://stats.ixarea.com/ixarea-stats/report?idsite=2&rec=1&action_name=音乐解锁+-+By+IXarea"
style="border:0"/>
</noscript>
<strong>音乐解锁采用了一些新特性!建议使用
<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://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</strong>
</div>
<div id="app"></div>
</body>
| <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,247 +1,94 @@
<template>
<div id="app">
<el-container>
<el-main>
<el-upload
:auto-upload="false"
:on-change="handleFile"
:show-file-list="false"
accept=".ncm,.qmc0,.qmc3,.qmcflac,.qmcogg,.mflac"
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-delete" 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">
<div class="image-slot el-image__error" slot="error">
暂无封面
</div>
</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 @click="handleDownload(scope.row)"
circle icon="el-icon-download">
</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, qmcogg, mflac)
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</el-row>
<el-row>
<span>Copyright &copy; 2019</span>
<a href="https://github.com/ix64" target="_blank">MengYX</a>
音乐解锁使用
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
开放
<a href="https://github.com/ix64/unlock-music" target="_blank">源代码</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");
const MFlacDecrypt = require("./plugins/mflac");
export default {
name: 'app',
components: {},
data() {
return {
activeIndex: '1',
tableData: [],
playing_url: "",
playing_auto: false,
}
},
mounted() {
this.$nextTick(function () {
this.finishLoad();
});
},
methods: {
finishLoad() {
document.getElementById("loader-mask").remove();
this.$notify.info({
title: '离线使用',
message: '我们使用PWA技术添加到桌面或收藏夹无网络也能使用。点击查看<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
dangerouslyUseHTMLString: true,
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":
case "qmcogg":
data = await QmcDecrypt.Decrypt(file.raw);
break;
case "mflac":
data = await MFlacDecrypt.Decrypt(file.raw);
break;
default:
data = {
status: false,
message: "不支持此文件格式",
};
break;
}
if (data.status) {
this.tableData.push(data);
this.$notify.success({
title: '解锁成功',
message: '成功解锁 ' + data.title
});
let _rp_data = {
original: file.name,
title: data.title,
album: data.album,
artist: data.artist,
mime: data.mime
};
console.log(data);
window._paq.push(["trackEvent", "Unlock", "Success", JSON.stringify(_rp_data)]);
} else {
this.$notify.error({
title: '出现问题',
message: data.message + "" + file.name +
',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
dangerouslyUseHTMLString: true
});
window._paq.push(["trackEvent", "Unlock", "Error", file.name]);
}
})();
},
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);
},
handleDownload(row) {
let a = document.createElement('a');
a.href = row.file;
a.download = row.filename;
document.body.append(a);
a.click();
a.remove();
},
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) {
this.handleDownload(this.tableData[index]);
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;

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,31 +0,0 @@
import Vue from 'vue'
import {
Button,
Col,
Container,
Footer,
Icon,
Image,
Link,
Main,
Notification,
Row,
Table,
TableColumn,
Upload
} 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,124 +0,0 @@
const musicMetadata = require("music-metadata-browser");
export {Decrypt}
async function Decrypt(file) {
// 获取扩展名
let filename_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
if (filename_ext !== "mflac") return {
status: false,
message: "File type is incorrect!",
};
// 读取文件
const fileBuffer = await new Promise(resolve => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
};
reader.readAsArrayBuffer(file);
});
const audioData = new Uint8Array(fileBuffer.slice(0, -0x170));
const audioDataLen = audioData.length;
// 转换数据
const seed = new Mask();
if (!seed.DetectMask(audioData)) return{
status: false,
message: "此音乐无法解锁目前mflac格式不提供完整支持",
};
for (let cur = 0; cur < audioDataLen; ++cur) {
audioData[cur] ^= seed.NextMask();
}
// 导出
const musicData = new Blob([audioData], {type: "audio/flac"});
const musicUrl = URL.createObjectURL(musicData);
// 读取Meta
let tag = await musicMetadata.parseBlob(musicData);
// 处理无标题歌手
let filename_array = file.name.substring(0, file.name.lastIndexOf(".")).split("-");
let title = tag.common.title;
let artist = tag.common.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 + ".flac";
// 处理无封面
let pic_url = "";
if (tag.common.picture !== undefined && tag.common.picture.length >= 1) {
const picture = tag.common.picture[0];
const blobPic = new Blob([picture.data], {type: picture.format});
pic_url = URL.createObjectURL(blobPic);
}
// 返回*/
return {
status: true,
message: "",
filename: filename,
title: title,
artist: artist,
album: tag.common.album,
picture: pic_url,
file: musicUrl,
mime: "audio/flac"
}
}
class Mask {
FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43, 0x00];
constructor() {
this.index = -1;
this.mask_index = -1;
this.mask = Array(128).fill(0x00);
}
DetectMask(data) {
let search_len = data.length - 256, mask;
for (let block_idx = 0; block_idx < search_len; block_idx += 128) {
let flag = true;
mask = data.slice(block_idx, block_idx + 128);
let next_mask = data.slice(block_idx + 128, block_idx + 256);
for (let idx = 0; idx < 128; idx++) {
if (mask[idx] !== next_mask[idx]) {
flag = false;
break;
}
}
if (!flag) continue;
for (let test_idx = 0; test_idx < this.FLAC_HEADER.length; test_idx++) {
let p = data[test_idx] ^ mask[test_idx];
if (p !== this.FLAC_HEADER[test_idx]) {
flag = false;
debugger;
break;
}
}
if (!flag) continue;
this.mask = mask;
return true;
}
return false;
}
NextMask() {
this.index++;
this.mask_index++;
if (this.index === 0x8000 || (this.index > 0x8000 && (this.index + 1) % 0x8000 === 0)) {
this.index++;
this.mask_index++;
}
if (this.mask_index >= 128) {
this.mask_index -= 128;
}
return this.mask[this.mask_index]
}
}

View File

@ -1,183 +0,0 @@
const CryptoJS = require("crypto-js");
const ID3Writer = require("browser-id3-writer");
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
) return {
status: false,
message: "此ncm文件已损坏",
};
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;
let audioData = new Uint8Array(fileBuffer, offset);
let audioDataLen = audioData.length;
for (let cur = 0; cur < audioDataLen; ++cur) {
audioData[cur] ^= keyBox[cur & 0xff];
}
if (musicMeta.format === undefined) {
const [f, L, a, C] = audioData;
if (f === 0x66 && L === 0x4c && a === 0x61 && C === 0x43) {
musicMeta.format = "flac";
} else {
musicMeta.format = "mp3";
}
}
const mime = audio_mime_type[musicMeta.format];
const artists = [];
musicMeta.artist.forEach(arr => {
artists.push(arr[0]);
});
if (musicMeta.format === "mp3") {
const writer = new ID3Writer(audioData);
writer.setFrame("TPE1", artists)
.setFrame("TIT2", musicMeta.musicName)
.setFrame("TALB", musicMeta.album);
if (musicMeta.albumPic !== "") {
try {
const img = await (await fetch(musicMeta.albumPic)).arrayBuffer();
writer.setFrame('APIC', {
type: 3,
data: img,
description: 'Cover'
})
} catch (e) {
console.log("Fail to write cover image!");
}
}
writer.addTag();
audioData = writer.arrayBuffer;
}
const musicData = new Blob([audioData], {
type: mime
});
const musicUrl = URL.createObjectURL(musicData);
const filename = artists.join(" & ") + " - " + musicMeta.musicName + "." + musicMeta.format;
return {
status: true,
filename: filename,
title: musicMeta.musicName,
artist: artists.join(" & "),
album: musicMeta.album,
picture: musicMeta.albumPic,
file: musicUrl,
mime: mime
};
}

View File

@ -1,124 +0,0 @@
const musicMetadata = require("music-metadata-browser");
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",
ogg: "audio/ogg"
};
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 "qmcogg":
new_ext = "ogg";
break;
case "qmcflac":
new_ext = "flac";
break;
default:
return {
status: false,
message: "File type is incorrect!",
};
}
const mime = audio_mime_type[new_ext];
// 读取文件
const fileBuffer = await new Promise(resolve => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(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 musicMetadata.parseBlob(musicData);
// 处理无标题歌手
let filename_array = file.name.substring(0, file.name.lastIndexOf(".")).split("-");
let title = tag.common.title;
let artist = tag.common.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.common.picture !== undefined && tag.common.picture.length >= 1) {
const picture = tag.common.picture[0];
const blobPic = new Blob([picture.data], {type: picture.format});
pic_url = URL.createObjectURL(blobPic);
}
// 返回
return {
status:true,
filename: filename,
title: title,
artist: artist,
album: tag.common.album,
picture: pic_url,
file: musicUrl,
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,44 +0,0 @@
const musicMetadata = require("music-metadata-browser");
export {Decrypt}
const audio_mime_type = {
mp3: "audio/mpeg",
flac: "audio/flac"
};
async function Decrypt(file) {
let tag = await musicMetadata.parseBlob(file);
let pic_url = "";
if (tag.common.picture !== undefined && tag.common.picture.length > 0) {
let pic = new Blob([tag.common.picture[0].data], {type: tag.common.picture[0].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.common.title;
let artist = tag.common.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 {
status:true,
filename: filename,
title: title,
artist: artist,
album: tag.common.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') {
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'
)
},
registered() {
console.log('Service worker has been registered.')
},
cached() {
console.log('Content has been cached for offline use.')
},
updatefound() {
console.log('New content is downloading.')
},
updated() {
console.log('New content is available; please refresh.')
},
offline() {
console.log('No internet connection found. App is running in offline mode.')
},
error(error) {
console.error('Error during service worker registration:', error)
}
})
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.');
},
registered() {
console.log('Service worker has been registered.');
},
cached() {
console.log('Content has been cached for offline use.');
},
updatefound() {
console.log('New content is downloading.');
},
updated() {
console.log('New content is available.');
window.location.reload();
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
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

BIN
testdata/mflac0_rc4_raw.bin vendored Normal file

Binary file not shown.

BIN
testdata/mflac0_rc4_suffix.bin vendored Normal file

Binary file not shown.

BIN
testdata/mflac0_rc4_target.bin vendored Normal file

Binary file not shown.

1
testdata/mflac_map_key.bin vendored Normal file
View File

@ -0,0 +1 @@
yw7xWOyNQ8585Jwx3hjB49QLPKi38F89awnrQ0fq66NT9TDq1ppHNrFqhaDrU5AFk916D58I53h86304GqOFCCyFzBem68DqiXJ81bILEQwG3P3MOnoNzM820kNW9Lv9IJGNn9Xo497p82BLTm4hAX8JLBs0T2pilKvT429sK9jfg508GSk4d047Jxdz5Fou4aa33OkyFRBU3x430mgNBn04Lc9BzXUI2IGYXv3FGa9qE4Vb54kSjVv8ogbg47j3

1
testdata/mflac_map_key_raw.bin vendored Normal file
View File

@ -0,0 +1 @@
eXc3eFdPeU6+3f7GVeF35bMpIEIQj5JWOWt7G+jsR68Hx3BUFBavkTQ8dpPdP0XBIwPe+OfdsnTGVQqPyg3GCtQSrkgA0mwSQdr4DPzKLkEZFX+Cf1V6ChyipOuC6KT37eAxWMdV1UHf9/OCvydr1dc6SWK1ijRUcP6IAHQhiB+mZLay7XXrSPo32WjdBkn9c9sa2SLtI48atj5kfZ4oOq6QGeld2JA3Z+3wwCe6uTHthKaEHY8ufDYodEe3qqrjYpzkdx55pCtxCQa1JiNqFmJigWm4m3CDzhuJ7YqnjbD+mXxLi7BP1+z4L6nccE2h+DGHVqpGjR9+4LBpe4WHB4DrAzVp2qQRRQJxeHd1v88=

BIN
testdata/mflac_map_raw.bin vendored Normal file

Binary file not shown.

BIN
testdata/mflac_map_suffix.bin vendored Normal file

Binary file not shown.

BIN
testdata/mflac_map_target.bin vendored Normal file

Binary file not shown.

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