Compare commits

...

353 Commits

Author SHA1 Message Date
MengYX 010c9c530a chore: update deps & bump version 2022-01-09 10:29:09 +08:00
MengYX 87df9959fb docs: update README.md 2022-01-09 10:26:29 +08:00
MengYX 50fa60c983 refactor: change common -> index 2022-01-09 10:22:00 +08:00
MengYX fcf0a764ef fix(QMCv2): overflow error in js decoder 2022-01-09 07:17:22 +08:00
MengYX e6b25efb4e fix #222 2022-01-09 01:31:12 +08:00
MengYX 74d0a4137e Merge pull request #221 from unlock-music/feature/qmc-meta
feature(QMCv2): 通过 SongID 在线查询歌曲信息
2021-12-25 20:32:33 +08:00
MengYX c1c51bf7e2 fix: declare radix in parseInt
Co-authored-by: Jixun Wu <5713045+jixunmoe@users.noreply.github.com>
2021-12-25 20:20:15 +08:00
Jixun Wu a0b94c0f6f fix(QMCv2): Fail gracefully when WebAssembly loader failed. 2021-12-25 12:07:47 +00:00
Jixun Wu ea57dbf4e7 feat(QMCv2): Allow extraction of songId from QMC2-wasm 2021-12-25 12:07:14 +00:00
Jixun Wu 2f56428d0a chore: bump qmc2-crypto to v0.0.6-R1 2021-12-25 12:00:35 +00:00
MengYX bcc3e719ef feature: use online info to correct qmc meta 2021-12-25 14:41:05 +08:00
MengYX bc27069f50 Merge pull request #220 from unlock-music/issue/142
fix #142: remove default mp3 cover description
2021-12-25 12:18:58 +08:00
MengYX 161daeef43 fix #142: remove default mp3 cover description 2021-12-25 12:13:26 +08:00
MengYX 4dc197d3af Merge pull request #215 from jixunmoe/feature/joox-encryption
提供 joox 解密/meta 更新支持
2021-12-25 11:49:24 +08:00
MengYX 164efd0f4c fix: api path & docker image name 2021-12-25 11:46:01 +08:00
Jixun Wu a4504c66b3 test(joox): Added basic sanity test for joox encryption. 2021-12-23 23:23:32 +00:00
Jixun Wu 78d6de210b feat(joox): Fetch meta data from API 2021-12-23 14:58:24 +00:00
Jixun Wu 5c28e8a186 doc: reforamtted & updated content in readme 2021-12-23 14:57:10 +00:00
Jixun Wu 851e039cc5 fix: avoid "ArtiomTr/jest-coverage-report-action" when running from a fork. 2021-12-22 14:10:29 +00:00
Jixun Wu 622de58034 refactor: move ruby to custom vue component 2021-12-22 13:39:47 +00:00
Jixun Wu 53a78dc8b4 chore: remove left-over debugger statement 2021-12-22 13:39:37 +00:00
Jixun Wu 514d8a07ba fix: form validation on input change 2021-12-22 13:28:38 +00:00
Jixun Wu c706351de5 Merge remote-tracking branch 'upstream/master' into feature/joox-encryption 2021-12-22 13:20:36 +00:00
MengYX ee8e6cfb5d fix(ci): test coverage annotation failed 2021-12-22 14:48:54 +08:00
Jixun Wu 5b86ae2f09 feat(config): better config ui
- JOOX Music UUID label + description
- Not full screen anymore
2021-12-21 23:10:34 +00:00
Jixun Wu 3d9ec99bc7 feat(joox): re-use QM meta extraction code 2021-12-21 22:34:36 +00:00
Jixun Wu 231f48bedc refactor(qmc): extract qm meta code to utils 2021-12-21 22:34:18 +00:00
Jixun Wu 9fc5517f43 fix: only pass over config settings 2021-12-21 22:17:43 +00:00
Jixun Wu 9d5d7f7fb6 feat(storage): Pass over config to worker thread on decryption call 2021-12-20 22:22:35 +00:00
Jixun Wu bc576e4063 refactor: storage factory + singleton
- Make storage easier.
2021-12-20 22:19:44 +00:00
Jixun dab5738331 fix: storage read/write in chrome extension 2021-12-20 19:13:27 +00:00
Jixun 42da860cb7 fix: add missing permission for chrome storage 2021-12-20 19:13:17 +00:00
Jixun 7bea7c6f42 chore: bump to 0.0.1-R4 2021-12-20 17:42:09 +00:00
Jixun aeb2dc6550 fix: crash due to chrome been undefined 2021-12-19 23:43:38 +00:00
Jixun 4c4c4419d1 chore: bump joox-crypto dependency 2021-12-19 23:07:46 +00:00
Jixun 4118d6f9ee feat: add basic joox support 2021-12-19 23:03:46 +00:00
MengYX f9beff676f Merge pull request #213 from unlock-music/prettier
Add: Prettier
2021-12-18 22:44:34 +08:00
MengYX 421b4f0eeb pretty: ignore matrix 2021-12-18 22:42:13 +08:00
MengYX 724611aad0 chore: remove unused api 2021-12-18 22:33:43 +08:00
MengYX 281a27487e all: format with prettier 2021-12-18 22:33:43 +08:00
MengYX f2179f4357 maintenance: add prettier 2021-12-18 22:33:42 +08:00
MengYX 4057b88fef Merge pull request #214 from unlock-music/fix/qmc-cipher
fix(QMCv2): cipher should determine by key size
2021-12-18 22:32:29 +08:00
MengYX e1efbe53fc fix(QMCv2): cipher should determine by key size 2021-12-18 21:35:22 +08:00
MengYX 5ec23215fa Merge pull request #211 from unlock-music/feature/qmc-v2
Feature: QMC v2 (JS Decoder)
2021-12-17 17:58:47 +08:00
MengYX 1c8b1835da chore(QMCv2): fix code style 2021-12-17 08:41:44 +08:00
MengYX 07bfb33a2f feat(QMCv2): use js decoder 2021-12-17 06:24:21 +08:00
MengYX 99acdd43d6 feat(QMCv2): add decoder 2021-12-17 06:07:43 +08:00
MengYX 64aedb03a7 feat(QMCv2): add rc4 cipher 2021-12-17 03:53:12 +08:00
MengYX 22c00222c1 feat(QMCv2): add key decrypt 2021-12-17 01:28:48 +08:00
MengYX aef2fc2b5f feat(QMCv2): add map cipher 2021-12-16 23:07:59 +08:00
MengYX 5e2e1fddd3 test(QMCv2): coverage standard TEA cipher 2021-12-16 23:07:13 +08:00
MengYX 115c479c1f feat(QMCv2): add standard TEA cipher 2021-12-16 23:07:08 +08:00
MengYX afb020499d fix(extension): version string must be numbers and dots 2021-12-16 10:45:32 +08:00
MengYX 6c50d7482a fix: ci 2021-12-16 08:52:22 +08:00
MengYX 87f49eeace maintenance: update ci 2021-12-16 08:19:23 +08:00
MengYX 044db8446d maintenance: update ci 2021-12-16 08:16:04 +08:00
MengYX 1532530e8f chore: bump version 2021-12-16 08:03:08 +08:00
MengYX ea1d890044 maintenance: update ci 2021-12-16 07:59:09 +08:00
MengYX dc8a820e61 maintenance: update ci 2021-12-16 07:25:10 +08:00
MengYX 1d0337dc52 maintenance: remove fix-compatibility.js 2021-12-16 07:24:42 +08:00
MengYX 0f92316697 Merge pull request #208 from unlock-music/feature/refactor-qmc-v1
feat(QMC): use static cipher instead of mask
2021-12-16 07:13:59 +08:00
MengYX 2ad07607a1 feat: use static cipher instead of mask 2021-12-16 07:07:51 +08:00
MengYX 2a23845714 Merge pull request #207 from jixunmoe/feature/qmcv2-wasm
WIP: 实验 qmc2-crypto 包
2021-12-16 06:35:06 +08:00
Jixun Wu 84187ec57e chore: add eol at the end of `qmcv2.ts`. 2021-12-15 22:20:53 +00:00
Jixun Wu 6ef2702d9e 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 aa59280fd7
2021-12-15 22:19:24 +00:00
Jixun Wu fffa8ef9e6 Revert "chore: upgrade `qmc2-crypto` to 0.0.5-R4"
This reverts commit aa59280fd7.

It generates unexpected large diff in package-lock.json.
2021-12-15 22:16:15 +00:00
Jixun Wu aa59280fd7 chore: upgrade `qmc2-crypto` to 0.0.5-R4
- Remove the use of `new Function` in emscripten generated code.
2021-12-15 22:08:25 +00:00
Jixun Wu 1c4543dc5e fix: treat qmcflac/qmcogg as QMCv2 and fallback to QMCv1 2021-12-15 22:07:05 +00:00
Jixun Wu 471942fdf2 refactor: remove suppressed qmc mask methods / constants 2021-12-15 20:11:18 +00:00
Jixun Wu 6b932ec04f refactor: restore support for QMCv1. 2021-12-15 19:59:06 +00:00
Jixun Wu ef7fd91559 chore: update supported ext list 2021-12-15 19:58:43 +00:00
Jixun Wu 98c786da46 chore: Use QMC2-Crypto with embedded WASM build from 0.0.5-R3 2021-12-15 18:22:05 +00:00
Jixun Wu 63f7f594f8 fix: patch threads to work with production build 2021-12-15 14:02:43 +00:00
Jixun Wu 8e89bcb456 feat(qmcv2): Experiment with qmc2-crypto 2021-12-15 13:54:35 +00:00
MengYX 60e5584292 maintenance: add jest as unit test
(cherry picked from commit 447aab81df8de62f546101dc0370a5314a6ee345)
2021-12-14 15:40:36 +08:00
MengYX 2eea56783f chore: update deps
(cherry picked from commit 14394c0c5e613a9cafa38dba3c9040dd54beb6d0)
2021-12-14 15:02:14 +08:00
MengYX f99ea1ab57 remove netease: requested by DCMA 2021-10-06 05:07:14 +08:00
MengYX 83fdac8293 Merge pull request #184 from lvzx123/patch-1
Now it is 2021!
2021-09-25 15:27:57 +08:00
lvzx123 88eafe827f Now it is 2021!
大人,时代变了
2021-09-25 08:56:07 +08:00
MengYX 942af10c53 bump version 2021-08-27 10:01:05 +08:00
MengYX 5ffe5d4b75 fix #169 2021-08-25 01:04:46 +08:00
MengYX 3b76314378 fix #179 2021-08-24 23:55:44 +08:00
MengYX 38f015da5e fix #179 2021-08-24 23:13:19 +08:00
Emmm Monster 8eaf46e534 fix(extension): compute version name 2021-08-11 23:04:01 +08:00
Emmm Monster cbb98b56c6 chore: bump version & update deps 2021-08-08 08:08:56 +08:00
MengYX ced5f58246 change ixarea api endpoint 2021-08-08 07:56:28 +08:00
MengYX 5f3f649cbf optimize: `.kgm` mask loading 2021-08-08 07:47:28 +08:00
sunhao03 606d06a766 fetch mask file fix on production 2021-08-07 22:13:00 +08:00
Emmm Monster 813ff0ab70 chore: update deps & fix audit 2021-07-01 01:33:28 +08:00
Emmm Monster 8d1285db8b fix: avoid using worker in file protocol 2021-07-01 01:29:04 +08:00
Emmm Monster b8c00eaeb6 chore(ci): build after *.ts changes 2021-06-03 13:15:04 +08:00
Emmm Monster 297faabacb simplify: decrypt/ncm-cache & decrypt/common 2021-06-03 13:09:14 +08:00
Emmm Monster dda3943198 fix: decrypt/qmc-cache
adapt: decrypt/qmc for qmc-cache
2021-06-03 13:09:02 +08:00
qq1010903229 f2b22ce815 增加对网易云音乐.uc缓存格式和QQ音乐.cache缓存格式的支持 (#161)
* Update common.ts

* Create ncmcache.ts

* Create qmccache.ts
2021-06-03 13:00:35 +08:00
Emmm Monster 8035ca9d65 chore: update deps 2021-05-27 19:53:19 +08:00
Emmm Monster 1aebd763e0 fix: .vpr/.kgm fail in worker 2021-05-27 19:38:42 +08:00
Emmm Monster d8c6e32134 feature(sniffer): support `.dff` 2021-05-25 12:27:19 +08:00
Emmm Monster 0b17180ce8 fix: remove test file 2021-05-25 04:36:32 +08:00
Emmm Monster 16ab1a8805 chore: Bump Version 2021-05-25 03:20:42 +08:00
Emmm Monster 76f592c761 feature: directly write to fs 2021-05-25 03:06:28 +08:00
Emmm Monster 91e7473603 fixes 2021-05-24 23:48:52 +08:00
Emmm Monster ac6a8cebee refactor: component/*.vue 2021-05-24 22:19:37 +08:00
Emmm Monster eba14f9e3c refactor(decrypt/*): change interface 2021-05-24 15:05:14 +08:00
Emmm Monster 926b3f597f refactor(decrypt/qmc): typescript 2021-05-24 06:50:20 +08:00
Emmm Monster ed72785400 refactor(decrypt/qmc): typescript qmc mask 2021-05-24 05:57:04 +08:00
Emmm Monster db935861dc refactor(.ncm): typescript & class 2021-05-24 05:04:16 +08:00
Emmm Monster 2597857f0d refactor(typescript): utils.WriteMetaFor{ Mp3, Flac } 2021-05-24 02:55:42 +08:00
Emmm Monster a14853412e refactor(typescript): .xm & .kgm 2021-05-24 01:30:38 +08:00
Emmm Monster f2aa84bfac refactor(typescript): Use ES6 import & use interface 2021-05-23 23:47:01 +08:00
Emmm Monster 901b5cef56 refactor(typescript): utils.GetCoverFromFile & utils.GetMetaFromFile 2021-05-23 23:06:21 +08:00
Emmm Monster 8d9ef9afed feat(decrypt/kwm): support raw .acc 2021-05-23 22:29:34 +08:00
EmmmX 5e3a369609 Merge pull request #157 from unlock-music/add-typescript
Add typescript support
2021-05-23 22:08:23 +08:00
Emmm Monster dbd0aae3a0 refactor(typescript): utils.GetArrayBuffer 2021-05-23 22:02:36 +08:00
Emmm Monster fcf0040a16 refactor: move some utils to typescript 2021-05-23 21:40:43 +08:00
Emmm Monster f4975c0f16 chore: add support for typescript 2021-05-23 21:01:17 +08:00
Emmm Monster 53c1e19cfe chore: update deps & fix audit 2021-05-23 20:41:31 +08:00
MengYX 0373d99fb9 Make Github Dependabot Happy 2021-04-11 13:59:39 +08:00
MengYX e11e502239 README: Add extension info 2021-02-18 20:08:25 +08:00
MengYX 371b0490dc Remove: [Extension] Stats Code 2021-02-09 15:54:45 +08:00
MengYX 30b8adbb9a Add: [Docs] Docker Usage in README 2021-02-08 20:05:10 +08:00
MengYX 9abb1799f9 Bump Version 2021-02-08 19:28:40 +08:00
MengYX 2639ac3a35 Change: [CI] Action Name 2021-02-08 19:14:26 +08:00
MengYX ab46eb76d0 Fix: [CI] Build Docker Image 2021-02-08 19:14:12 +08:00
MengYX 6ef8b1fcc6 Fix: [CI] Build Docker Image 2021-02-08 17:09:26 +08:00
MengYX 36f5f9b1e8 Fix: [CI] Build Docker Image 2021-02-08 16:23:24 +08:00
MengYX ac62fd1122 Update: [CI] Build Docker Image 2021-02-08 16:08:06 +08:00
MengYX 2b16883646 Add: Dockerfile 2021-02-08 14:11:46 +08:00
MengYX 583bb3d8d9 Update: [CI] Remove Cache (because using `npm ci`) 2021-02-08 14:01:41 +08:00
MengYX be23dbd3c3 Update: [Docs] README 2021-02-08 12:11:45 +08:00
MengYX 2bb0f25280 Misc: Bump Version 2021-02-08 05:31:16 +08:00
MengYX 1ff5e51afc Fix: [Extension] Use extension API make sure page open successfully 2021-02-08 04:50:00 +08:00
MengYX 9728be6f0d Update: Deps 2021-02-08 04:26:56 +08:00
MengYX 82a211de9a Fix: [CI] Generated zip structure 2021-02-08 04:24:55 +08:00
MengYX 2178a296cb Merge branch 'feature/extension' 2021-02-08 04:14:02 +08:00
MengYX 1429fdf27b Adapt: [Extension] for Firefox 2021-02-08 03:35:26 +08:00
MengYX 4d5c986af9 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 77f5f06a46 Update CI: Add Extension Build 2021-02-08 01:47:31 +08:00
MengYX 221306f7c5 Add Feature: Browser Extension 2021-02-08 01:28:30 +08:00
MengYX 0c5cae80f0 Change: Web Manifest 2021-02-08 00:26:40 +08:00
MengYX 686f473d55 Update README.md 2021-01-10 16:38:03 +08:00
MengYX 4960322f17 Remove Drone CI 2020-12-23 15:35:46 +08:00
MengYX 1b9bad66ec Remove "By IXarea" 2020-12-23 15:34:37 +08:00
MengYX f1ba1a3c7e 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 1034d54863 Bump Version and Update Deps 2020-12-19 21:16:53 +08:00
MengYX ad25b3d950 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 646bbfb390 Update new-feature.md 2020-12-10 19:01:54 +08:00
MengYX ac45dc5e47 Update bug-report.md 2020-12-10 19:00:50 +08:00
MengYX 2b55a92f7b Add tips for qmc not writing cover 2020-12-06 02:32:57 +08:00
MengYX 4db4cdbd31 Update CI 2020-12-06 02:22:43 +08:00
MengYX 60fef1c41d Try to fix .qmc ID3 Info 2020-12-06 02:16:45 +08:00
dependabot[bot] 1f41bd8e1f 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 a2e947cbfb Bump Version and Update Deps 2020-11-26 17:28:14 +08:00
MengYX 87a0a0052d Merge pull request #113 from KyleBing/master
调整暗黑模式样式,新增全局统一样式 by @KyleBing
2020-11-26 16:56:44 +08:00
KyleBing 01cd512178 ^ package-lock.json 2020-11-25 14:47:23 +08:00
KyleBing 880da817a6 暗黑模式颜色调整,载入页颜色适配黑色 2020-11-25 14:38:29 +08:00
KyleBing b954918820 use scss source file, remove pre-compiled css file. 2020-11-25 13:50:38 +08:00
KyleBing 34a74c761d 调整暗黑模式样式,新增全局统一样式 2020-11-24 22:28:56 +08:00
MengYX 1739215a88 Merge pull request #112 from flosacca/master
Fix #100 by @flosacca
2020-11-23 21:34:58 +08:00
flosacca 76629d955b Fix #100 2020-11-21 07:03:57 +08:00
MengYX 45f1e3575e Update README 2020-11-07 22:46:27 +08:00
MengYX 42629d8075 Bump Version 2020-11-07 01:22:45 +08:00
MengYX 846569ea69 Fix #103 #100 duplicated metadata 2020-11-07 01:12:04 +08:00
MengYX a0233693fb Update CI 2020-11-06 22:40:23 +08:00
MengYX 0c417edebf Update Deps 2020-11-06 22:40:13 +08:00
MengYX 730fa3465f Merge pull request #106 from lc6464/master
适配浏览器深色模式
2020-11-01 11:40:21 +08:00
NULL-LC d5dc6866af 适配浏览器深色模式 2020-10-31 19:25:14 +08:00
MengYX eb7cfd72e5 Merge pull request #101 from renbaoshuo/patch-1
更新 Edge 浏览器下载链接
2020-10-30 10:48:45 +08:00
Baoshuo Ren 7788d98af4 更新 Edge 浏览器下载链接 2020-10-18 19:02:40 +08:00
MengYX a4b98e12ca Bump Version 2020-09-23 16:51:17 +08:00
MengYX f64f55a3b7 Merge pull request #97 from qq1010903229/patch-1
Merge pull request #97 增加对QQ音乐微云网盘格式的支持
2020-09-23 16:45:54 +08:00
MengYX 1b7e5702be Fix Kgm Decrypt Bug 2020-09-23 14:15:47 +08:00
MengYX 16c24b4d48 Update Deps 2020-09-23 12:55:14 +08:00
MengYX 7906ca8a72 Fix kgm bug 2020-09-23 12:54:54 +08:00
qq1010903229 98af3a34de Update qmc.js 2020-09-19 12:03:02 +08:00
qq1010903229 2dbfc001b2 Update common.js 2020-09-19 12:00:58 +08:00
MengYX a8925f4dd7 Add Comment for Issue Template 2020-09-04 19:12:58 +08:00
MengYX 03e06b8fcf Bump Version 2020-09-02 00:04:26 +08:00
MengYX 4b0dc87521 Merge remote-tracking branch into master 2020-09-01 23:31:43 +08:00
MengYX f4b464b47b Add Tips for .kgm while using "file:" protocol 2020-09-01 23:26:02 +08:00
MengYX 9bd9e272dd Use Small Cover Image for .ncm 2020-09-01 23:17:17 +08:00
MengYX 5cbd26fbce Update README.md 2020-08-15 10:30:13 +08:00
MengYX 5bb0e4f770 fix .xm filename detect 2020-08-13 21:27:50 +08:00
MengYX b2f437f318 Update CI 2020-08-13 21:27:25 +08:00
MengYX c7254da74c Merge remote-tracking branch 'origin/master' 2020-08-13 16:01:55 +08:00
MengYX b5cf435de7 Update GitHub CI 2020-08-13 16:01:35 +08:00
MengYX 9eea1aa15f Bump Version and Update Deps 2020-08-13 15:58:31 +08:00
MengYX e7e065e014 Fix .xm read info from filename 2020-08-13 15:33:15 +08:00
MengYX 5b7a467dae Fix #84 2020-08-13 15:26:15 +08:00
MengYX 8e00ca3bad Fix .xm file type recognize error 2020-08-13 15:25:41 +08:00
MengYX 0f06b0f306 Update issue templates 2020-08-07 19:33:28 +08:00
MengYX e6691240dd Fix ncm cover image too big to write into meta 2020-08-03 15:04:54 +08:00
MengYX 0b970ca65d Clean up 2020-08-03 14:03:10 +08:00
MengYX 2c8c88fd9a Fix #79 ncm->flac no metadata (file downloaded from phone) 2020-08-03 14:02:17 +08:00
MengYX 2f8fbc2c14 Bump Version 2020-08-02 18:25:56 +08:00
MengYX a16cdf8732 Fix wrong zip file in release [Skip CI] 2020-08-01 01:20:56 +08:00
MengYX 1322df34d0 Fix #77 ncm flac meta duplicated
Fix #78 write flac cover sometimes fail
2020-08-01 01:10:27 +08:00
MengYX 3339283883 Update README.md 2020-07-19 00:03:41 +08:00
MengYX 03d0d9e8ef Fix GitHub CI 2020-07-18 23:37:07 +08:00
MengYX d9f00447ef Bump Version 2020-07-18 23:04:26 +08:00
MengYX 0adc933d2f Change IXarea Api Endpoint 2020-07-18 22:43:25 +08:00
MengYX 43b956d8f6 Change IXarea Api Endpoint 2020-07-18 21:58:07 +08:00
MengYX 6c34a3b0ff Add Support qq music cover 2020-07-18 21:45:53 +08:00
MengYX 1d8c5069e4 Add Support for flac meta/cover 2020-07-18 19:25:41 +08:00
MengYX 9c41991cea Write meta for qq music mp3 2020-07-18 18:48:07 +08:00
MengYX 1c21186306 Resolve QQMusic Cover(By IXarea Server) 2020-07-17 00:33:10 +08:00
MengYX a8c0c742ca Fix dep security problem 2020-07-16 22:44:42 +08:00
MengYX 7029735889 Update Tips 2020-07-16 22:41:22 +08:00
MengYX 9f7a216144 Add Init Support for Kgm/Vpr 2020-07-16 22:22:32 +08:00
MengYX 5fc74d3fe9 Update Deps 2020-07-16 21:28:49 +08:00
MengYX 2d35cb7be9 Update and rename deploy.yml to release.yml 2020-05-28 02:24:32 +08:00
MengYX 4e257fe8ca Create GitHub Pages 2020-05-28 00:41:17 +08:00
MengYX eeb09edf3a Fix typo #62 2020-05-21 09:36:59 +08:00
MengYX 539ea0448f Bump Version & Update Deps 2020-05-18 16:41:10 +08:00
MengYX 93f46a2950 Add Tips 2020-05-18 16:40:39 +08:00
MengYX ea9318554e Simplify 2020-05-14 23:46:21 +08:00
MengYX 0476de3d9b Fix ncm unlock while no album pic #58 2020-05-14 17:37:58 +08:00
MengYX b667dff401 Fix Xiami return info 2020-05-14 17:36:16 +08:00
MengYX bb61ef90e6 Fix .qmc Files Unlock Error 2020-04-26 15:20:12 +08:00
MengYX 659cb25476 Bump to 1.5.1 2020-04-26 15:17:40 +08:00
MengYX be7ac71d5c Fix Qmc Mask Query 2020-04-26 15:04:46 +08:00
MengYX 60bc033d9e Small Bug Fix 2020-04-26 11:20:29 +08:00
MengYX b09beec673 Update Mgg Detect Algorithm 2020-04-26 01:33:52 +08:00
MengYX e5505df5f5 Change Tips Info 2020-04-26 01:33:52 +08:00
MengYX 803dab8476 Change CI 2020-04-26 01:33:52 +08:00
MengYX cd29bde7d0 Bug Fix in Worker Mode 2020-04-23 21:23:01 +08:00
MengYX 049ebd90a0 Remove Console Log 2020-04-23 21:16:41 +08:00
MengYX 52de5b3645 Update README.md 2020-04-23 21:12:11 +08:00
MengYX 62212d3005 Update Description and Bump Version
Small Fixes
2020-04-23 21:10:25 +08:00
MengYX d3d7ef2eb4 Add Support For Xiami .xm Files! 2020-04-23 20:46:08 +08:00
MengYX 3338524db6 Merge remote-tracking branch 'ixarea/master' 2020-04-23 18:17:58 +08:00
MengYX 36ae570ae5 Small Changes 2020-04-23 18:16:33 +08:00
MengYX 2975508b22 Unlock Kuwo .kwm Files! 2020-04-23 18:15:07 +08:00
MengYX 16f4bdd8a4 Update README Tips 2020-04-22 06:43:40 +00:00
MengYX f85a1d4ed6 Update README.md 2020-04-21 14:31:01 +00:00
MengYX b9af9bf227 Update README.md 2020-04-09 08:37:54 +00:00
MengYX 521d9f1677 Change Browser Tips Condition 2020-04-08 22:47:38 +08:00
MengYX f4780f89d4 Update README.md 2020-04-08 14:12:05 +00:00
MengYX c4dcfa8a65 Limit Update Tips 2020-04-07 19:02:57 +08:00
MengYX cd4d270641 Bump Version 2020-04-06 18:20:40 +08:00
MengYX 6d9982a7d4 Fix GBK Detect Bug 2020-04-06 13:03:17 +08:00
MengYX 3570cb5c9a Update Deps 2020-04-05 19:26:05 +08:00
MengYX a0c9f93788 Hello 2020! 2020-04-05 19:23:30 +08:00
MengYX 607478ce4d Fix GBK Encoding Reading in QQMusic Mp3 2020-04-05 19:20:22 +08:00
MengYX d19bbf682b Get QQMusic Cover URL (Only Display) 2020-04-05 17:56:10 +08:00
MengYX 55df78396d Move ID3 Writer to Util 2020-04-05 17:31:41 +08:00
MengYX 3ab8fb723e Add progress bar for unlocking #37
Add tips for instant save
2020-04-05 12:35:11 +08:00
MengYX 486a6c2624 Add Support For .qmc2 2020-04-05 01:01:59 +08:00
MengYX 2d35b8b468 Add Header Check For .mgg 2020-03-18 23:03:39 +08:00
MengYX 29801f250a Use Npm Registry 2020-03-18 22:45:07 +08:00
MengYX a22d78b740 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] 20c8a1d963 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 fd3cba6c50 Bump Version 2020-03-12 18:56:21 +08:00
MengYX 340c66ec7e Merge CI Settings 2020-03-10 12:22:46 +08:00
MengYX c968a7578a Fix errors when parsing ncm files without metadata 2020-03-10 09:31:48 +08:00
MengYX 74f8cc8d2a Remove Source Map in production 2020-03-04 10:00:18 +08:00
MengYX dc8c126cd3 Update CI 2020-03-04 09:38:30 +08:00
MengYX 82915a9dd5 Bump Version 2020-03-04 09:30:48 +08:00
MengYX 8953a57b3e Add fix-compatibility npm command 2020-03-04 09:29:05 +08:00
MengYX 753647fd4d Update Dependencies 2020-03-04 08:37:54 +08:00
MengYX 97f34783ef Update Babel config 2020-03-04 08:37:39 +08:00
MengYX 7ba053cc07 Add temporary solution to fix compatibility for Edge 18 2020-03-04 08:37:16 +08:00
MengYX a1fb6bc00a Merge branch 'pull/22' 2020-03-03 20:01:42 +08:00
MengYX de28e844c2 Optimize loader tips for outdated browser 2020-03-03 20:00:44 +08:00
任宝硕 59266f7625 Update index.html 2020-03-03 16:41:45 +08:00
MengYX 1a93da738c Reformat Code 2020-03-01 23:36:16 +08:00
MengYX ea78532e53 Fix Update Check 2020-03-01 23:34:24 +08:00
任宝硕 3f36619be1 Update index.html 2020-03-01 16:16:34 +08:00
任宝硕 23ada91260 对部分老旧浏览器进行显示调整 + 部分内容修复 2020-03-01 16:14:43 +08:00
MengYX c15c600cee #20 Add Support For Netease DJ Files 2020-02-29 19:52:41 +08:00
MengYX d121d38e0d Bump Version 2020-02-23 13:52:03 +08:00
MengYX 166086ae03 #19 Add Download Type: Origin Filename 2020-02-23 13:45:30 +08:00
MengYX 4755674c98 Add Update Check 2020-02-23 13:44:19 +08:00
MengYX f89202ee62 Immediately Load Latest App 2020-02-23 13:42:50 +08:00
MengYX b5f8c4a237 Update Dependencies 2020-02-23 13:42:20 +08:00
MengYX a137af42ec Optimize UI 2020-02-23 13:41:39 +08:00
MengYX 0441f8670a Fix Decrypt Mflac Error 2020-02-12 23:04:48 +08:00
MengYX 07c51bd62d Fix Decrypt Algorithm Error 2020-02-11 17:03:46 +08:00
MengYX ecbb5b5042 Optimize Loading 2020-02-11 16:35:45 +08:00
MengYX bc3d7f53aa Fix Mgg Mask Detect Bug 2020-02-11 16:23:50 +08:00
MengYX 354da563b3 Update README and Bump Version 2020-02-11 16:00:34 +08:00
MengYX 9cde491254 Add Detect Media Type by File 2020-02-11 15:52:22 +08:00
MengYX 5171872ec9 Optimize Import 2020-02-11 14:48:27 +08:00
MengYX 08553884ab Use Universal Decoder for Qmc,Mgg,Mflac 2020-02-11 14:35:17 +08:00
MengYX 61622cf7ed Use Universal Mask for Qmc,Mgg,Mflac
Add Local Experimental Support For Mgg
2020-02-11 00:34:26 +08:00
MengYX 4187d433d6 Add Experimental Support For Mgg 2020-02-10 20:10:48 +08:00
MengYX 30853a5617 Bump Version 2020-02-09 15:16:23 +08:00
MengYX e6cba313c2 Better Way to Detect Mflac Mask 2020-02-09 14:05:40 +08:00
MengYX 34df70ba70 Adjust for Debugging 2020-02-07 20:17:45 +08:00
MengYX b4be250585 Fix Babel Config 2020-02-07 14:41:21 +08:00
MengYX b0d8b3c8d2 Remove Useless Information 2020-02-06 16:18:40 +08:00
MengYX d0b13871f7 Split App.vue 2020-02-06 16:01:35 +08:00
MengYX e81c3d82e5 Optimize Bundle Size 2020-02-06 14:05:46 +08:00
MengYX 8680be846f Merge branch 'ix-master' 2020-02-05 02:08:08 +08:00
MengYX 285277f303 Bump Version 2020-02-05 01:58:53 +08:00
MengYX 5dd1f3bd9d Add instant download to avoid memory occupation 2020-02-05 01:53:58 +08:00
MengYX b1fa5612e9 Merge branch 'pull/17'
# Conflicts:
#	src/App.vue
2020-02-05 01:38:34 +08:00
MengYX f98852a7a0 Add Web Worker 2020-02-05 00:30:44 +08:00
MengYX f71729d933 Merge pull request #16 from smtop/dev
增加歌曲命名格式选项
2020-02-04 19:41:01 +08:00
1519715742@qq.com adb44fe8c9 Performance improvement in multiple files 2020-02-04 19:12:44 +08:00
smdev 145e1a6ede 增加歌曲命名格式选项 2020-02-04 18:24:53 +08:00
MengYX c5b2a8357c Update CI 2020-02-01 12:05:00 +08:00
MengYX 7556c39c71 Edit index.html Upgrade Dependencies 2020-01-31 11:47:16 +08:00
MengYX 304ad63585 #9 Add QQ Music tkm Format 2020-01-27 18:02:39 +08:00
MengYX 77659e0427 Fix QMC filename error 2020-01-27 16:43:40 +08:00
MengYX 7ed3ee8fb0 #11 Add Moo Music Format 2020-01-27 14:06:45 +08:00
MengYX 589697068d Merge branch 'master' of github.com:ix64/unlock-music 2020-01-21 20:00:20 +08:00
MengYX 6918ed257e Update CI 2020-01-21 19:52:45 +08:00
MengYX 6a7af6f5f2 Update Readme [CI SKIP] 2020-01-21 19:31:30 +08:00
MengYX a0d31d880e Add Support For: tm0/2/3/6 2020-01-21 19:21:17 +08:00
MengYX 8267148e96 Reconstruct 2020-01-21 19:20:46 +08:00
MengYX e108f7c016 Update Deps 2020-01-21 19:01:52 +08:00
MengYX 7cbf860948 Update README.md 2019-12-23 19:09:24 +08:00
MengYX 8efb1d7d45 Update README.md 2019-12-16 19:01:56 +08:00
MengYX ba66e38968 Revert: Favicon 2019-12-08 19:12:15 +08:00
MengYX 995b88ff5a Upgrade Vue Cli 2019-12-07 15:09:09 +08:00
MengYX 00de3888ff Remove unused icon 2019-12-07 14:44:32 +08:00
MengYX 4ec1847682 Change build mode 2019-12-07 14:32:28 +08:00
MengYX 1ede0f3193 Fix accept in uploader 2019-12-07 12:23:18 +08:00
MengYX 66a247be3a Use Post 2019-12-01 22:52:29 +08:00
MengYX 55ff80f59b Fix link error in README [SKIP CI] 2019-12-01 21:04:51 +08:00
MengYX cf078a4fa6 Update README and Dependencies 2019-11-30 21:10:40 +08:00
MengYX aee06b383f Change Notification 2019-11-24 19:33:11 +08:00
MengYX 1ba6d93fb2 Report Error Type 2019-11-23 19:22:32 +08:00
MengYX ad3e2d55fc Fix No Status Error 2019-11-23 19:11:40 +08:00
MengYX fef9841cb4 Fix Download Button 2019-11-23 18:56:45 +08:00
MengYX ba2f717842 Show Detail Info While Error Occurred 2019-11-23 18:30:59 +08:00
MengYX e8bee61533 Update Dependencies 2019-11-23 18:14:15 +08:00
MengYX 37c6c5554b Add Partial Support For .mflac 2019-11-23 18:09:33 +08:00
MengYX 093145eb99 Reformat Code [SKIP CI] 2019-11-23 15:10:08 +08:00
MengYX c1f029705b Add tips for qmcogg 2019-11-23 15:03:45 +08:00
MengYX d7a2f9361e CI: Auto Deploy and Use Cache 2019-11-10 22:38:43 +08:00
MengYX 62cf8663d8 Add CI 2019-11-10 17:25:01 +08:00
MengYX 831f578daa Use new window to open link 2019-11-10 01:30:11 +08:00
MengYX 03a3e7ef90 Update Dependencies 2019-11-10 01:16:08 +08:00
MengYX 9e9b2ec7f3 Add .qmcogg (without test)
Add Tips
2019-11-10 00:59:13 +08:00
MengYX 3a50460c61 Fix Display Bugs In Edge and Safari 2019-09-21 23:32:21 +08:00
MengYX 83f4015695 Enhanced ease of use 2019-09-18 00:21:18 +08:00
MengYX 3e3a98142b Add Analytics
Fix an error statement
2019-09-14 20:48:57 +08:00
MengYX 91f91ce0c9 Complete ID3 for ncm 2019-09-12 15:51:10 +08:00
MengYX 5c18124ecd Fix an icon error 2019-09-08 15:06:08 +08:00
MengYX d54a1ebedb Merge pull request #2 from ix64/pull/1
Fix bugs after using music-metadata-browser
2019-09-08 14:50:39 +08:00
MengYX 1fb762630f Merge branch 'master' into pull/1 2019-09-08 14:49:03 +08:00
MengYX bb9227aa9e Merge pull request #1 from Borewit/music-metadata-browser
Maybe try music-metadata-browser?
2019-09-08 14:45:23 +08:00
Borewit 1bcf198c7b Fix parsing picture in metadata 2019-09-08 08:28:25 +02:00
MengYX d6e31becc7 Fix bugs after using music-metadata-browser 2019-09-08 13:40:32 +08:00
Borewit 06e04f85b6 use music-metadata-browser 2019-09-07 19:50:04 +02:00
MengYX b893dd47cf Change icon 2019-08-25 15:55:36 +08:00
MengYX 8011a07342 Downgrade jsmediatag to avoid bug
Rename project
2019-08-25 15:16:20 +08:00
MengYX 087547a7e5 Update page footer 2019-08-25 14:44:27 +08:00
MengYX d3d8d145ba Update readme 2019-08-25 14:31:38 +08:00
MengYX ed4d83e19d Update readme 2019-08-25 14:27:11 +08:00
MengYX e28e6c3654 Merge branch 'master' of https://git.ixarea.com/MusicCrack/music-crack 2019-08-25 14:12:48 +08:00
MengYX af6d3a9e2e Update dependencies to fix CVE-2019-10744 2019-08-25 14:10:33 +08:00
121 changed files with 35717 additions and 5904 deletions

39
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,39 @@
---
name: Bug报告
about: 报告Bug以帮助改进程序
title: ''
labels: bug
assignees: ''
---
* 请按照此模板填写,否则可能立即被关闭
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
- [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
**Bug描述**
简要地复述你遇到的Bug
**复现方法**
描述复现方法,必要时请提供样本文件
**程序截图或者Console报错信息**
如果可以请提供二者之一
**环境信息:**
- 操作系统和浏览器:
- 程序版本:
- 获取音乐文件所使用的客户端及其版本信息:
**附加信息**
其他能够帮助确认问题的信息

26
.github/ISSUE_TEMPLATE/new-feature.md vendored Normal file
View File

@ -0,0 +1,26 @@
---
name: 新功能
about: 对于程序新的想法或建议
title: ''
labels: enhancement
assignees: ''
---
- 请按照此模板填写,否则可能立即被关闭
**背景和说明**
简要说明产生此想法的背景和此想法的具体内容
**实现途径**
- 如果没有设计方案,请简要描述实现思路
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者Telegram进行讨论
**附加信息**
更多你想要表达的内容

84
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,84 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Test Build
on:
push:
paths:
- ".github/workflows/*"
- "**/*.js"
- "**/*.ts"
- "**/*.vue"
- "public/**/*"
- "package-lock.json"
- "package.json"
pull_request:
branches: [ master ]
types: [ opened, synchronize, reopened ]
paths:
- "**/*.js"
- "**/*.ts"
- "**/*.vue"
- "public/**/*"
- "package-lock.json"
- "package.json"
jobs:
test-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci
# note: forks can not access to GITHUB_TOKEN for coverage update.
# instead, we just ran the test in this case.
- name: Test only
if: github.event_name != 'push'
run: npm test
- name: Test + Publish Coverage
uses: ArtiomTr/jest-coverage-report-action@v2.0-rc.6
if: github.event_name == 'push'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
annotations: none
build:
runs-on: ubuntu-latest
strategy:
matrix:
build: [ legacy, modern ]
include:
- build: legacy
BUILD_ARGS: ""
BUILD_EXTENSION: true
- build: modern
BUILD_ARGS: "-- --modern"
BUILD_EXTENSION: false
steps:
- uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Install Dependencies
run: npm ci
- name: Build
run: npm run build ${{ matrix.BUILD_ARGS }}
- name: Publish artifact
uses: actions/upload-artifact@v2
with:
name: ${{ matrix.build }}
path: ./dist
- name: Build Extension
if: ${{ matrix.BUILD_EXTENSION }}
run: npm run make-extension
- name: Publish artifact - Extension
if: ${{ matrix.BUILD_EXTENSION }}
uses: actions/upload-artifact@v2
with:
name: extension
path: ./dist

65
.github/workflows/post-release.yml vendored Normal file
View File

@ -0,0 +1,65 @@
name: Post Release
on:
release:
types: [ published ]
jobs:
release-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup vars
id: vars
env:
RELEASE_REF: ${{ github.ref }}
run: echo "::set-output name=tag::${RELEASE_REF#refs/tags/}"
- name: Download release content
run: |
echo "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
wget -O modern.tar.gz "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
mkdir ./dist
tar zxf modern.tar.gz -C ./dist
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build docker and push (on modern)
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/386
push: true
tags: |
ix64/unlock-music:latest
ix64/unlock-music:${{ steps.vars.outputs.tag }}
gh-pages:
runs-on: ubuntu-latest
steps:
- name: Setup vars
id: vars
env:
RELEASE_REF: ${{ github.ref }}
run: echo "::set-output name=tag::${RELEASE_REF#refs/tags/}"
- name: Download release content
run: |
echo "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
wget -O modern.tar.gz "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
mkdir ./dist
tar zxf modern.tar.gz -C ./dist
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist

126
.github/workflows/release-build.yml vendored Normal file
View File

@ -0,0 +1,126 @@
name: Build Release
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Install Dependencies
run: npm ci
- name: Build Legacy
env:
GZIP: "--best"
run: |
npm run build
tar -czf legacy.tar.gz -C ./dist .
cd dist
zip -rJ9 ../legacy.zip *
cd ..
- name: Build Extension (on legacy)
env:
GZIP: "--best"
run: |
npm run make-extension
cd dist
zip -rJ9 ../extension.zip *
cd ..
- name: Build Modern
env:
GZIP: "--best"
run: |
npm run build -- --modern
tar -czf modern.tar.gz -C ./dist .
cd dist
zip -rJ9 ../modern.zip *
cd ..
- name: Checksum
run: sha256sum *.tar.gz *.zip > sha256sum.txt
- name: Get current time
id: date
run: echo "::set-output name=date::$(date +'%Y/%m/%d')"
- name: Create a Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: "Build ${{ steps.date.outputs.date }}"
draft: true
- name: Upload Release Assets - legacy.tar.gz
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./legacy.tar.gz
asset_name: legacy.tar.gz
asset_content_type: application/gzip
- name: Upload Release Assets - legacy.zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./legacy.zip
asset_name: legacy.zip
asset_content_type: application/zip
- name: Upload Release Assets - modern.tar.gz
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./modern.tar.gz
asset_name: modern.tar.gz
asset_content_type: application/gzip
- name: Upload Release Assets - modern.zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./modern.zip
asset_name: modern.zip
asset_content_type: application/zip
- name: Upload Release Assets - extension.zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./extension.zip
asset_name: extension.zip
asset_content_type: application/zip
- name: Upload Release Assets - sha256sum.txt
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./sha256sum.txt
asset_name: sha256sum.txt
asset_content_type: text/plain

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.DS_Store
node_modules
/dist
/coverage
# local env files
.env.local

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-2021 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
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

106
README.md
View File

@ -1,29 +1,91 @@
# music-crack
# Unlock Music 音乐解锁
## Project setup
```
npm install
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循 [License][license]
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli][repo_cli] 找到,大批量转换建议使用 CLI 版本。
- 我们新建了 Telegram 群组 [`@unlock_music_chat`][tg_group] ,欢迎加入!
- [相关的其他项目][related_projects]
![Test Build](https://github.com/unlock-music/unlock-music/workflows/Test%20Build/badge.svg)
![GitHub releases](https://img.shields.io/github/downloads/unlock-music/unlock-music/total)
![Docker Pulls](https://img.shields.io/docker/pulls/ix64/unlock-music)
[license]: https://github.com/unlock-music/unlock-music/blob/master/LICENSE
[repo_cli]: https://github.com/unlock-music/cli
[tg_group]: https://t.me/unlock_music_chat
[related_projects]: https://github.com/unlock-music/unlock-music/wiki/和UnlockMusic相关的项目
## 特性
### 支持的格式
- [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] 虾米音乐格式 (.xm)
- [x] 酷我音乐格式 (.kwm)
- [x] 酷狗音乐格式 (.kgm/.vpr) ([CLI 版本][kgm_cli])
[kgm_cli]: https://github.com/unlock-music/unlock-music/wiki/其他音乐格式工具#酷狗音乐-kgmvpr解锁工具
[joox_wiki]: https://github.com/unlock-music/joox-crypto/wiki/加密格式
### 其他特性
- [x] 在浏览器中解锁
- [x] 拖放文件
- [x] 批量解锁
- [x] 渐进式 Web 应用 (PWA)
- [x] 多线程
- [x] 写入Meta和封面图片
## 使用方法
### 安装浏览器扩展
[![Chrome Web Store](https://storage.googleapis.com/chrome-gcs-uploader.appspot.com/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/gldlhhhmienbhlpkfanjpmffdjblmegd)
[<img src="https://developer.microsoft.com/en-us/store/badges/images/Chinese_Simplified_get-it-from-MS.png" height="60" alt="Microsoft Edge Addons"/>](https://microsoftedge.microsoft.com/addons/detail/ggafoipegcmodfhakdkalpdpcdkiljmd)
[![Firefox Browser Addons](https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/zh-CN/firefox/addon/unlock-music/)
### 使用已构建版本
- 从[GitHub Release](https://github.com/unlock-music/unlock-music/releases/latest)下载已构建的版本
- 本地使用请下载`legacy版本``modern版本`只能通过 **http(s)协议** 访问)
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**
### 使用 Docker 镜像
```shell
docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
```
### Compiles and hot-reloads for development
```
npm run serve
```
### 自行构建
### Compiles and minifies for production
```
npm run build
```
- 环境要求
- nodejs (v16.x)
- npm
### Run your tests
```
npm run test
```
1. 获取项目源代码后安装相关依赖:
### Lints and fixes files
```
npm run lint
```
```sh
npm ci
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
2. 然后进行构建。编译后的文件保存到 dist 目录下:
```sh
npm run build
```
- 如果是用于开发,可以执行 `npm run serve`
3. 如需构建浏览器扩展build 完成后还需要执行:
```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"
}]
]
};

16
extension-manifest.json Normal file
View File

@ -0,0 +1,16 @@
{
"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://github.com/ix64/unlock-music",
"browser_action": {
"default_popup": "./popup.html"
}
}

8
jest.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
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 = "./src/extension/"
const dst = "./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("./extension-manifest.json", "utf-8")
const manifest = JSON.parse(manifestRaw)
const pkgRaw = fs.readFileSync("./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("./dist/manifest.json", JSON.stringify(manifest), "utf-8")
console.log("Write: manifest.json")

36929
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,59 @@
{
"name": "music-crack",
"version": "0.1.0",
"name": "unlock-music",
"version": "v1.10.0",
"ext_build": 0,
"updateInfo": "重写QMC解锁完全支持.mflac*/.mgg*; 支持JOOX解锁",
"license": "MIT",
"description": "Unlock encrypted music file in browser.",
"repository": {
"type": "git",
"url": "https://github.com/ix64/unlock-music"
},
"private": true,
"scripts": {
"postinstall": "patch-package",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
"build": "vue-cli-service build",
"test": "jest",
"pretty": "prettier --write src/{**/*,*}.{js,ts,jsx,tsx,vue}",
"pretty:check": "prettier --check src/{**/*,*}.{js,ts,jsx,tsx,vue}",
"make-extension": "node ./make-extension.js"
},
"dependencies": {
"browser-id3-writer": "^4.1.0",
"core-js": "^2.6.5",
"crypto-js": "^3.1.9-1",
"element-ui": "^2.4.5",
"jsmediatags": "^3.9.1",
"register-service-worker": "^1.6.2",
"vue": "^2.6.10"
"@babel/preset-typescript": "^7.16.5",
"@jixun/qmc2-crypto": "^0.0.6-R1",
"@unlock-music/joox-crypto": "^0.0.1-R5",
"base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0",
"core-js": "^3.16.0",
"crypto-js": "^4.1.1",
"element-ui": "^2.15.5",
"iconv-lite": "^0.6.3",
"jimp": "^0.16.1",
"metaflac-js": "^1.0.5",
"music-metadata": "7.9.0",
"music-metadata-browser": "2.2.7",
"register-service-worker": "^1.7.2",
"threads": "^1.6.5",
"vue": "^2.6.14"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.9.0",
"@vue/cli-plugin-pwa": "^3.9.0",
"@vue/cli-service": "^3.9.0",
"@types/crypto-js": "^4.0.2",
"@types/jest": "^27.0.3",
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-pwa": "^4.5.13",
"@vue/cli-plugin-typescript": "^4.5.13",
"@vue/cli-service": "^4.5.13",
"babel-plugin-component": "^1.1.1",
"jest": "^27.4.5",
"patch-package": "^6.4.7",
"prettier": "2.5.1",
"sass": "^1.38.1",
"sass-loader": "^10.2.0",
"semver": "^7.3.5",
"threads-plugin": "^1.4.0",
"typescript": "^4.5.4",
"vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.10"
"vue-template-compiler": "^2.6.14"
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -2,68 +2,42 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>音乐解锁 - By IXarea</title>
<meta content="音乐,解锁,ncm,qmc,qmc0,qmc3,qmcflac,qq音乐,网易云音乐,加密" name="keywords"/>
<meta content="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="音乐,解锁,qmc,mgg,mflac,qq音乐,加密" name="keywords"/>
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description"/>
<style>
/* Center the loader */
#loader {
position: absolute;
left: 50%;
top: 50%;
z-index: 1010;
margin: -75px 0 0 -75px;
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#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);
}
</style>
<script src="./ixarea-stats.js"></script>
<!--@formatter:off-->
<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 #1db1ff;width:120px;height:120px;animation:spin 2s linear infinite}@keyframes spin{0%{transform:rotate(0)}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,.88)}@media (prefers-color-scheme:dark){#loader-mask{color:#fff;background-color:rgba(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>
<!--@formatter:on-->
</head>
<body>
<div id="loader-mask">
<div id="loader"></div>
<noscript>
<strong>很抱歉音乐解锁需要启用JavaScript的现代浏览器
<a href="https://www.google.cn/chrome/">Google Chrome</a>
<a href="https://www.firefox.com.cn/">Mozilla Firefox</a>
</strong>
<h3 id="loader-js">请启用JavaScript</h3>
<img alt=""
src="https://stats.ixarea.com/ixarea-stats/report?rec=1&action_name=音乐解锁-NoJS&idsite=2"
style="border:0"/>
</noscript>
<script>
window.onload = function () {
document.getElementById("loader-mask").remove();
};
</script>
<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>
</h3>
</div>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="./loader.js"></script>
</body>
</html>

10
public/ixarea-stats.js Normal file
View File

@ -0,0 +1,10 @@
var _paq = window._paq || [];
_paq.push(["setRequestMethod", "POST"], ["trackPageView"], ["enableLinkTracking"],
["setSiteId", "2"], ["setTrackerUrl", "https://stats.ixarea.com/ixarea-stats/report"]);
var tag = document.createElement('script');
tag.type = 'text/javascript';
tag.async = true;
tag.src = 'https://stats.ixarea.com/ixarea-stats.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(tag, s);

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"
}

BIN
public/static/kgm.mask Normal file

Binary file not shown.

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,113 @@
<style scoped>
label {
cursor: pointer;
line-height: 1.2;
display: block;
}
.item-desc {
color: #aaa;
font-size: small;
display: block;
line-height: 1.2;
margin-top: 0.2em;
}
.item-desc a {
color: #aaa;
}
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="item-desc">
下载该加密文件的 JOOX 应用所记录的设备唯一识别码
<br />
参见
<a href="https://github.com/unlock-music/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID">
获取设备 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,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,62 @@
<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">
<span>{{ scope.row.title }}</span>
</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-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);
},
},
};
</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.

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://github.com/unlock-music',
};
});
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;
}

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

@ -0,0 +1,90 @@
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 { 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 '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 'qmc3': //QQ Music Android Mp3
case 'qmc2': //QQ Music Android Ogg
case 'qmc0': //QQ Music Android Mp3
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 'mgg': //QQ Music New Ogg
case 'mgg1': //QQ Music New Ogg
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;
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,
};
}

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

@ -0,0 +1,74 @@
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 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)) {
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, 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;
}

29
src/decrypt/qmc.test.ts Normal file
View File

@ -0,0 +1,29 @@
import fs from 'fs';
import { QmcDecoder } from '@/decrypt/qmc';
import { BytesEqual } from '@/decrypt/utils';
function loadTestDataDecoder(name: string): {
cipherText: Uint8Array;
clearText: Uint8Array;
} {
const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
const cipherText = new Uint8Array(cipherBody.length + cipherSuffix.length);
cipherText.set(cipherBody);
cipherText.set(cipherSuffix, cipherBody.length);
return {
cipherText,
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
};
}
test('qmc: real file', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4', 'mflac_map', 'mgg_map', 'qmc0_static'];
for (const name of cases) {
const { clearText, cipherText } = loadTestDataDecoder(name);
const c = new QmcDecoder(cipherText);
const buf = c.decrypt();
expect(BytesEqual(buf, clearText)).toBeTruthy();
}
});

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

@ -0,0 +1,171 @@
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher';
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
import { QmcDeriveKey } from '@/decrypt/qmc_key';
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 },
mgg1: { ext: 'ogg', version: 2 },
mflac: { ext: 'flac', version: 2 },
mflac0: { ext: 'flac', version: 2 },
// qmcflac / qmcogg:
// 有可能是 v2 加密但混用同一个后缀名。
qmcflac: { ext: 'flac', version: 2 },
qmcogg: { ext: 'ogg', version: 2 },
qmc0: { ext: 'mp3', version: 1 },
qmc2: { ext: 'ogg', version: 1 },
qmc3: { ext: 'mp3', version: 1 },
bkcmp3: { ext: 'mp3', version: 1 },
bkcflac: { ext: 'flac', 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: Uint8Array | undefined;
let musicID: number | string | undefined;
if (version === 2 && globalThis.WebAssembly) {
console.log('qmc: using wasm decoder');
const v2Decrypted = await DecryptQMCWasm(fileBuffer);
// 若 v2 检测失败,降级到 v1 再尝试一次
if (v2Decrypted.success) {
musicDecoded = v2Decrypted.data;
musicID = v2Decrypted.songId;
} else {
console.warn('qmc2-wasm failed with error %s', v2Decrypted.error || '(no error)');
}
}
if (!musicDecoded) {
// may throw error
console.log('qmc: using js decoder');
const d = new QmcDecoder(new Uint8Array(fileBuffer));
musicDecoded = d.decrypt();
musicID = d.songID;
}
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,
};
}
export class QmcDecoder {
private static readonly BYTE_COMMA = ','.charCodeAt(0);
private readonly file: Uint8Array;
private readonly size: number;
private decoded: boolean = false;
private audioSize?: number;
private cipher?: QmcStreamCipher;
public constructor(file: Uint8Array) {
this.file = file;
this.size = file.length;
this.searchKey();
}
private _songID?: number;
public get songID() {
return this._songID;
}
public decrypt(): Uint8Array {
if (!this.cipher) {
throw new Error('no cipher found');
}
if (!this.audioSize || this.audioSize <= 0) {
throw new Error('invalid audio size');
}
const audioBuf = this.file.subarray(0, this.audioSize);
if (!this.decoded) {
this.cipher.decrypt(audioBuf, 0);
this.decoded = true;
}
return audioBuf;
}
private searchKey() {
const last4Byte = this.file.slice(-4);
const textEnc = new TextDecoder();
if (textEnc.decode(last4Byte) === 'QTag') {
const sizeBuf = this.file.slice(-8, -4);
const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset);
const keySize = sizeView.getUint32(0, false);
this.audioSize = this.size - keySize - 8;
const rawKey = this.file.subarray(this.audioSize, this.size - 8);
const keyEnd = rawKey.findIndex((v) => v == QmcDecoder.BYTE_COMMA);
if (keyEnd < 0) {
throw new Error('invalid key: search raw key failed');
}
this.setCipher(rawKey.subarray(0, keyEnd));
const idBuf = rawKey.subarray(keyEnd + 1);
const idEnd = idBuf.findIndex((v) => v == QmcDecoder.BYTE_COMMA);
if (keyEnd < 0) {
throw new Error('invalid key: search song id failed');
}
this._songID = parseInt(textEnc.decode(idBuf.subarray(0, idEnd)), 10);
} else {
const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
const keySize = sizeView.getUint32(0, true);
if (keySize < 0x300) {
this.audioSize = this.size - keySize - 4;
const rawKey = this.file.subarray(this.audioSize, this.size - 4);
this.setCipher(rawKey);
} else {
this.audioSize = this.size;
this.cipher = new QmcStaticCipher();
}
}
}
private setCipher(keyRaw: Uint8Array) {
const keyDec = QmcDeriveKey(keyRaw);
if (keyDec.length > 300) {
this.cipher = new QmcRC4Cipher(keyDec);
} else {
this.cipher = new QmcMapCipher(keyDec);
}
}
}

View File

@ -0,0 +1,117 @@
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher';
import fs from 'fs';
test('static cipher [0x7ff8,0x8000) ', () => {
//prettier-ignore
const expected = new Uint8Array([
0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A,
0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8,
])
const c = new QmcStaticCipher();
const buf = new Uint8Array(16);
c.decrypt(buf, 0x7ff8);
expect(buf).toStrictEqual(expected);
});
test('static cipher [0,0x10) ', () => {
//prettier-ignore
const expected = new Uint8Array([
0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52,
0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00,
])
const c = new QmcStaticCipher();
const buf = new Uint8Array(16);
c.decrypt(buf, 0);
expect(buf).toStrictEqual(expected);
});
test('map cipher: get mask', () => {
//prettier-ignore
const expected = new Uint8Array([
0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB,
0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79,
])
const key = new Uint8Array(256);
for (let i = 0; i < 256; i++) key[i] = i;
const buf = new Uint8Array(16);
const c = new QmcMapCipher(key);
c.decrypt(buf, 0);
expect(buf).toStrictEqual(expected);
});
function loadTestDataCipher(name: string): {
key: Uint8Array;
cipherText: Uint8Array;
clearText: Uint8Array;
} {
return {
key: fs.readFileSync(`testdata/${name}_key.bin`),
cipherText: fs.readFileSync(`testdata/${name}_raw.bin`),
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
};
}
test('map cipher: real file', async () => {
const cases = ['mflac_map', 'mgg_map'];
for (const name of cases) {
const { key, clearText, cipherText } = loadTestDataCipher(name);
const c = new QmcMapCipher(key);
c.decrypt(cipherText, 0);
expect(cipherText).toStrictEqual(clearText);
}
});
test('rc4 cipher: real file', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
const { key, clearText, cipherText } = loadTestDataCipher(name);
const c = new QmcRC4Cipher(key);
c.decrypt(cipherText, 0);
expect(cipherText).toStrictEqual(clearText);
}
});
test('rc4 cipher: first segment', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
const { key, clearText, cipherText } = loadTestDataCipher(name);
const c = new QmcRC4Cipher(key);
const buf = cipherText.slice(0, 128);
c.decrypt(buf, 0);
expect(buf).toStrictEqual(clearText.slice(0, 128));
}
});
test('rc4 cipher: align block (128~5120)', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
const { key, clearText, cipherText } = loadTestDataCipher(name);
const c = new QmcRC4Cipher(key);
const buf = cipherText.slice(128, 5120);
c.decrypt(buf, 128);
expect(buf).toStrictEqual(clearText.slice(128, 5120));
}
});
test('rc4 cipher: simple block (5120~10240)', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
const { key, clearText, cipherText } = loadTestDataCipher(name);
const c = new QmcRC4Cipher(key);
const buf = cipherText.slice(5120, 10240);
c.decrypt(buf, 5120);
expect(buf).toStrictEqual(clearText.slice(5120, 10240));
}
});

199
src/decrypt/qmc_cipher.ts Normal file
View File

@ -0,0 +1,199 @@
export interface QmcStreamCipher {
decrypt(buf: Uint8Array, offset: number): void;
}
export class QmcStaticCipher implements QmcStreamCipher {
//prettier-ignore
private static readonly staticCipherBox: Uint8Array = new Uint8Array([
0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10
0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18
0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20
0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28
0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30
0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38
0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40
0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48
0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50
0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58
0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60
0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68
0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70
0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78
0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80
0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88
0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90
0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98
0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0
0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8
0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0
0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8
0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0
0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8
0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0
0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8
0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0
0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8
0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0
0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11, //0xF8
])
public getMask(offset: number) {
if (offset > 0x7fff) offset %= 0x7fff;
return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff];
}
public decrypt(buf: Uint8Array, offset: number) {
for (let i = 0; i < buf.length; i++) {
buf[i] ^= this.getMask(offset + i);
}
}
}
export class QmcMapCipher implements QmcStreamCipher {
key: Uint8Array;
n: number;
constructor(key: Uint8Array) {
if (key.length == 0) throw Error('qmc/cipher_map: invalid key size');
this.key = key;
this.n = key.length;
}
private static rotate(value: number, bits: number) {
let rotate = (bits + 4) % 8;
let left = value << rotate;
let right = value >> rotate;
return (left | right) & 0xff;
}
decrypt(buf: Uint8Array, offset: number): void {
for (let i = 0; i < buf.length; i++) {
buf[i] ^= this.getMask(offset + i);
}
}
private getMask(offset: number) {
if (offset > 0x7fff) offset %= 0x7fff;
const idx = (offset * offset + 71214) % this.n;
return QmcMapCipher.rotate(this.key[idx], idx & 0x7);
}
}
export class QmcRC4Cipher implements QmcStreamCipher {
private static readonly FIRST_SEGMENT_SIZE = 0x80;
private static readonly SEGMENT_SIZE = 5120;
S: Uint8Array;
N: number;
key: Uint8Array;
hash: number;
constructor(key: Uint8Array) {
if (key.length == 0) {
throw Error('invalid key size');
}
this.key = key;
this.N = key.length;
// init seed box
this.S = new Uint8Array(this.N);
for (let i = 0; i < this.N; ++i) {
this.S[i] = i & 0xff;
}
let j = 0;
for (let i = 0; i < this.N; ++i) {
j = (this.S[i] + j + this.key[i % this.N]) % this.N;
[this.S[i], this.S[j]] = [this.S[j], this.S[i]];
}
// init hash base
this.hash = 1;
for (let i = 0; i < this.N; i++) {
let value = this.key[i];
// ignore if key char is '\x00'
if (!value) continue;
const next_hash = (this.hash * value) >>> 0;
if (next_hash == 0 || next_hash <= this.hash) break;
this.hash = next_hash;
}
}
decrypt(buf: Uint8Array, offset: number): void {
let toProcess = buf.length;
let processed = 0;
const postProcess = (len: number): boolean => {
toProcess -= len;
processed += len;
offset += len;
return toProcess == 0;
};
// Initial segment
if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) {
const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset);
this.encFirstSegment(buf.subarray(0, len_segment), offset);
if (postProcess(len_segment)) return;
}
// align segment
if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) {
const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess);
this.encASegment(buf.subarray(processed, processed + len_segment), offset);
if (postProcess(len_segment)) return;
}
// Batch process segments
while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
postProcess(QmcRC4Cipher.SEGMENT_SIZE);
}
// Last segment (incomplete segment)
if (toProcess > 0) {
this.encASegment(buf.subarray(processed), offset);
}
}
private encFirstSegment(buf: Uint8Array, offset: number) {
for (let i = 0; i < buf.length; i++) {
buf[i] ^= this.key[this.getSegmentKey(offset + i)];
}
}
private encASegment(buf: Uint8Array, offset: number) {
// Initialise a new seed box
const S = this.S.slice(0);
// Calculate the number of bytes to skip.
// The initial "key" derived from segment id, plus the current offset.
const skipLen =
(offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(Math.floor(offset / QmcRC4Cipher.SEGMENT_SIZE));
// decrypt the block
let j = 0;
let k = 0;
for (let i = -skipLen; i < buf.length; i++) {
j = (j + 1) % this.N;
k = (S[j] + k) % this.N;
[S[k], S[j]] = [S[j], S[k]];
if (i >= 0) {
buf[i] ^= S[(S[j] + S[k]) % this.N];
}
}
}
private getSegmentKey(id: number): number {
const seed = this.key[id % this.N];
const idx = Math.floor((this.hash / ((id + 1) * seed)) * 100.0);
return idx % this.N;
}
}

View File

@ -0,0 +1,26 @@
import { QmcDeriveKey, simpleMakeKey } from '@/decrypt/qmc_key';
import fs from 'fs';
test('key dec: make simple key', () => {
expect(simpleMakeKey(106, 8)).toStrictEqual([0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]);
});
function loadTestDataKeyDecrypt(name: string): {
cipherText: Uint8Array;
clearText: Uint8Array;
} {
return {
cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`),
clearText: fs.readFileSync(`testdata/${name}_key.bin`),
};
}
test('key dec: real file', async () => {
const cases = ['mflac_map', 'mgg_map', 'mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
const { clearText, cipherText } = loadTestDataKeyDecrypt(name);
const buf = QmcDeriveKey(cipherText);
expect(buf).toStrictEqual(clearText);
}
});

103
src/decrypt/qmc_key.ts Normal file
View File

@ -0,0 +1,103 @@
import { TeaCipher } from '@/utils/tea';
const SALT_LEN = 2;
const ZERO_LEN = 7;
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
const textDec = new TextDecoder();
const rawDec = Buffer.from(textDec.decode(raw), 'base64');
let n = rawDec.length;
if (n < 16) {
throw Error('key length is too short');
}
const simpleKey = simpleMakeKey(106, 8);
let teaKey = new Uint8Array(16);
for (let i = 0; i < 8; i++) {
teaKey[i << 1] = simpleKey[i];
teaKey[(i << 1) + 1] = rawDec[i];
}
const sub = decryptTencentTea(rawDec.subarray(8), teaKey);
rawDec.set(sub, 8);
return rawDec.subarray(0, 8 + sub.length);
}
// simpleMakeKey exported only for unit test
export function simpleMakeKey(salt: number, length: number): number[] {
const keyBuf: number[] = [];
for (let i = 0; i < length; i++) {
const tmp = Math.tan(salt + i * 0.1);
keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0);
}
return keyBuf;
}
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
if (inBuf.length % 8 != 0) {
throw Error('inBuf size not a multiple of the block size');
}
if (inBuf.length < 16) {
throw Error('inBuf size too small');
}
const blk = new TeaCipher(key, 32);
const tmpBuf = new Uint8Array(8);
const tmpView = new DataView(tmpBuf.buffer);
blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8));
const nPadLen = tmpBuf[0] & 0x7; //只要最低三位
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
const outLen = inBuf.length - 1 /*PadLen*/ - nPadLen - SALT_LEN - ZERO_LEN;
const outBuf = new Uint8Array(outLen);
let ivPrev = new Uint8Array(8);
let ivCur = inBuf.slice(0, 8); // init iv
let inBufPos = 8;
// 跳过 Padding Len 和 Padding
let tmpIdx = 1 + nPadLen;
// CBC IV 处理
const cryptBlock = () => {
ivPrev = ivCur;
ivCur = inBuf.slice(inBufPos, inBufPos + 8);
for (let j = 0; j < 8; j++) {
tmpBuf[j] ^= ivCur[j];
}
blk.decrypt(tmpView, tmpView);
inBufPos += 8;
tmpIdx = 0;
};
// 跳过 Salt
for (let i = 1; i <= SALT_LEN; ) {
if (tmpIdx < 8) {
tmpIdx++;
i++;
} else {
cryptBlock();
}
}
// 还原明文
let outBufPos = 0;
while (outBufPos < outLen) {
if (tmpIdx < 8) {
outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
outBufPos++;
tmpIdx++;
} else {
cryptBlock();
}
}
// 校验Zero
for (let i = 1; i <= ZERO_LEN; i++) {
if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) {
throw Error('zero check failed');
}
}
return outBuf;
}

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

@ -0,0 +1,111 @@
import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
import { QMCCrypto } from '@jixun/qmc2-crypto/QMCCrypto';
// 检测文件末端使用的缓冲区大小
const DETECTION_SIZE = 40;
// 每次处理 2M 的数据
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
export interface QMC2DecryptionResult {
success: boolean;
data: Uint8Array;
songId: string | number;
error: string;
}
/**
* QMC2
*
* Uint8Array
* @param {ArrayBuffer} mggBlob Blob
*/
export async function DecryptQMCWasm(mggBlob: ArrayBuffer): Promise<QMC2DecryptionResult> {
const result: QMC2DecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
// 初始化模组
let QMCCrypto: QMCCrypto;
try {
QMCCrypto = await QMCCryptoModule();
} catch (err: any) {
result.error = err?.message || 'wasm 加载失败';
return result;
}
// 申请内存块,并文件末端数据到 WASM 的内存堆
const detectionBuf = new Uint8Array(mggBlob.slice(-DETECTION_SIZE));
const pDetectionBuf = QMCCrypto._malloc(detectionBuf.length);
QMCCrypto.writeArrayToMemory(detectionBuf, pDetectionBuf);
// 检测结果内存块
const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
// 进行检测
const detectOK = QMCCrypto.detectKeyEndPosition(pDetectionResult, pDetectionBuf, detectionBuf.length);
// 提取结构体内容:
// (pos: i32; len: i32; error: char[??])
const position = QMCCrypto.getValue(pDetectionResult, 'i32');
const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32');
result.success = detectOK;
result.error = QMCCrypto.UTF8ToString(
pDetectionResult + QMCCrypto.offsetof_error_msg(),
QMCCrypto.sizeof_error_msg(),
);
const songId = QMCCrypto.UTF8ToString(pDetectionResult + QMCCrypto.offsetof_song_id(), QMCCrypto.sizeof_song_id());
if (!songId) {
console.debug('qmc2-wasm: songId not found');
} else if (/^\d+$/.test(songId)) {
result.songId = songId;
} else {
console.warn('qmc2-wasm: Invalid songId: %s', songId);
}
// 释放内存
QMCCrypto._free(pDetectionBuf);
QMCCrypto._free(pDetectionResult);
if (!detectOK) {
return result;
}
// 计算解密后文件的大小。
// 之前得到的 position 为相对当前检测数据起点的偏移。
const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
// 提取嵌入到文件的 EKey
const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
// 解码 UTF-8 数据到 string
const decoder = new TextDecoder();
const ekey_b64 = decoder.decode(ekey);
// 初始化加密与缓冲区
const hCrypto = QMCCrypto.createInstWidthEKey(ekey_b64);
const buf = QMCCrypto._malloc(DECRYPTION_BUF_SIZE);
const decryptedParts = [];
let offset = 0;
let bytesToDecrypt = decryptedSize;
while (bytesToDecrypt > 0) {
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
// 解密一些片段
const blockData = new Uint8Array(mggBlob.slice(offset, offset + blockSize));
QMCCrypto.writeArrayToMemory(blockData, buf);
QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize);
decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize));
offset += blockSize;
bytesToDecrypt -= blockSize;
}
QMCCrypto._free(buf);
hCrypto.delete();
result.data = MergeUint8Array(decryptedParts);
return result;
}

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

@ -0,0 +1,50 @@
import {
AudioMimeType,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
SplitFilename,
} from '@/decrypt/utils';
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
import { DecryptResult } from '@/decrypt/entity';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
let length = buffer.length;
for (let i = 0; i < length; i++) {
buffer[i] ^= 0xf4;
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
}
let ext = SniffAudioExt(buffer, '');
const newName = SplitFilename(raw_filename);
let audioBlob: Blob;
if (ext !== '' || newName.ext === 'mp3') {
audioBlob = new Blob([buffer], { type: AudioMimeType[ext] });
} else if (newName.ext in HandlerMap) {
audioBlob = new Blob([buffer], { 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, 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, 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);
}

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

@ -0,0 +1,178 @@
import { IAudioMetadata } from 'music-metadata-browser';
import ID3Writer from 'browser-id3-writer';
import MetaFlac from 'metaflac-js';
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) {
if (!meta.artist) 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;
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) {}
}
});
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 SplitFilename(n: string): { name: string; ext: string } {
const pos = n.lastIndexOf('.');
return {
ext: n.substring(pos + 1).toLowerCase(),
name: n.substring(0, pos),
};
}

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,
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',
};
}

View File

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

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

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

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

View File

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

56
src/main.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,166 @@
/*
* name: 样式 - 夜间模式
* author: @KyleBing
* date: 2020-11-24
*/
@media (prefers-color-scheme: dark) {
#app{
color: $dark-text-info;
}
body{
background-color: $dark-bg;
}
// 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;
.el-checkbox__inner{
background-color: $dark-btn-bg;
border-color: $dark-border;
}
&: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: $dark-btn-bg-highlight;
}
.el-checkbox__label{
color: white;
font-weight: bold;
}
}
}
// 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;
}
}
}
&--danger{
&.is-plain{
border-color: $dark-border;
background-color: $dark-btn-bg;
&:hover{
background-color: $red;
border-color: $red;
}
}
}
}
// 文件拖放区
.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{
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;
}
}
}
}
// LINKS
a{
text-decoration: none;
color: darken($dark-color-link, 15%);
&:hover{
color: $dark-color-link;
}
}
// ALERT
.el-notification{
background-color: $dark-btn-bg-highlight;
border-color: $dark-border;
&__title{
color: white;
}
&__content{
color: $dark-text-info;
}
}
}

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

@ -0,0 +1,18 @@
/*
* 间隔工具集
*/
$gap: 5px;
@for $item from 1 through 7 {
.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;}
}

38
src/scss/_normal.scss Normal file
View File

@ -0,0 +1,38 @@
body{
font-family: $font-family;
font-size: $fz-main;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
text-align: center;
color: $text-main;
padding-top: 30px;
}
#app-footer a {
padding-left: 0.2em;
padding-right: 0.2em;
}
#app-footer {
text-align: center;
font-size: small;
}
#app-control {
padding-top: 1em;
padding-bottom: 1em;
}
audio{
margin-bottom: 15px; // 播放控件与表格间隔
}
a{
color: darken($color-link, 15%);
&:hover{
color: $color-link;
}
}

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

@ -0,0 +1,28 @@
// COLORS
$blue : #409EFF;
$red : #F56C6C;
$green : #85ce61;
// TEXT
$text-main : #2C3E50;
$color-link: $blue;
$fz-main: 14px;
$font-family: "Helvetica Neue", Helvetica, "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
// 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-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;

View File

@ -0,0 +1,5 @@
@import "variables";
@import "gaps";
@import "normal";
@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}`;
}

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

@ -0,0 +1,147 @@
import { IAudioMetadata, parseBlob as metaParseBlob } from 'music-metadata-browser';
import iconv from 'iconv-lite';
import {
GetCoverFromFile,
GetImageFromURL,
GetMetaFromFile,
WriteMetaToFlac,
WriteMetaToMp3,
AudioMimeType,
} from '@/decrypt/utils';
import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api';
interface MetaResult {
title: string;
artist: string;
album: string;
imgUrl: string;
blob: Blob;
}
/**
*
* @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 = iconv.decode(new Buffer(musicMeta.common.artist ?? ''), 'gbk');
musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ''), 'gbk');
musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ''), 'gbk');
}
}
if (id) {
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(' _ '),
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();
}

73
src/utils/tea.test.ts Normal file
View File

@ -0,0 +1,73 @@
// Copyright 2021 MengYX. All rights reserved.
//
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in https://go.dev/LICENSE.
import { TeaCipher } from '@/utils/tea';
test('key size', () => {
// prettier-ignore
const testKey = new Uint8Array([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
0x00,
])
expect(() => new TeaCipher(testKey.slice(0, 16))).not.toThrow();
expect(() => new TeaCipher(testKey)).toThrow();
expect(() => new TeaCipher(testKey.slice(0, 15))).toThrow();
});
// prettier-ignore
const teaTests = [
// These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
{
rounds: TeaCipher.numRounds,
key: new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]),
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
},
{
rounds: TeaCipher.numRounds,
key: new Uint8Array([
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
]),
plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
},
{
rounds: 16,
key: new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]),
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
},
];
test('rounds', () => {
const tt = teaTests[0];
expect(() => new TeaCipher(tt.key, tt.rounds - 1)).toThrow();
});
test('encrypt & decrypt', () => {
for (const tt of teaTests) {
const c = new TeaCipher(tt.key, tt.rounds);
const buf = new Uint8Array(8);
const bufView = new DataView(buf.buffer);
c.encrypt(bufView, new DataView(tt.plainText.buffer));
expect(buf).toStrictEqual(tt.cipherText);
c.decrypt(bufView, new DataView(tt.cipherText.buffer));
expect(buf).toStrictEqual(tt.plainText);
}
});

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

@ -0,0 +1,80 @@
// Copyright 2021 MengYX. All rights reserved.
//
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in https://go.dev/LICENSE.
// TeaCipher is a typescript port to golang.org/x/crypto/tea
// Package tea implements the TEA algorithm, as defined in Needham and
// Wheeler's 1994 technical report, “TEA, a Tiny Encryption Algorithm”. See
// http://www.cix.co.uk/~klockstone/tea.pdf for details.
//
// TEA is a legacy cipher and its short block size makes it vulnerable to
// birthday bound attacks (see https://sweet32.info). It should only be used
// where compatibility with legacy systems, not security, is the goal.
export class TeaCipher {
// BlockSize is the size of a TEA block, in bytes.
static readonly BlockSize = 8;
// KeySize is the size of a TEA key, in bytes.
static readonly KeySize = 16;
// delta is the TEA key schedule constant.
static readonly delta = 0x9e3779b9;
// numRounds 64 is the standard number of rounds in TEA.
static readonly numRounds = 64;
k0: number;
k1: number;
k2: number;
k3: number;
rounds: number;
constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
if (key.length != 16) {
throw Error('incorrect key size');
}
if ((rounds & 1) != 0) {
throw Error('odd number of rounds specified');
}
const k = new DataView(key.buffer);
this.k0 = k.getUint32(0, false);
this.k1 = k.getUint32(4, false);
this.k2 = k.getUint32(8, false);
this.k3 = k.getUint32(12, false);
this.rounds = rounds;
}
encrypt(dst: DataView, src: DataView) {
let v0 = src.getUint32(0, false);
let v1 = src.getUint32(4, false);
let sum = 0;
for (let i = 0; i < this.rounds / 2; i++) {
sum = sum + TeaCipher.delta;
v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
}
dst.setUint32(0, v0, false);
dst.setUint32(4, v1, false);
}
decrypt(dst: DataView, src: DataView) {
let v0 = src.getUint32(0, false);
let v1 = src.getUint32(4, false);
let sum = (TeaCipher.delta * this.rounds) / 2;
for (let i = 0; i < this.rounds / 2; i++) {
v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
sum -= TeaCipher.delta;
}
dst.setUint32(0, v0, false);
dst.setUint32(4, v1, false);
}
}

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);

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

@ -0,0 +1,170 @@
<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>
<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" border class="ml-2">立即保存</el-checkbox>
</el-tooltip>
</el-row>
</div>
<audio :autoplay="playing_auto" :src="playing_url" controls />
<PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @play="changePlaying" />
</div>
</template>
<script>
import FileSelector from '@/component/FileSelector';
import PreviewTable from '@/component/PreviewTable';
import ConfigDialog from '@/component/ConfigDialog';
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
export default {
name: 'Home',
components: {
FileSelector,
PreviewTable,
ConfigDialog,
},
data() {
return {
showConfigDialog: false,
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://github.com/ix64/unlock-music/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 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.

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