233 Commits

Author SHA1 Message Date
Emmm Monster
d3898161b9 chore: bump version & update deps 2021-08-08 08:08:56 +08:00
MengYX
5a7a9e3add change ixarea api endpoint 2021-08-08 07:56:28 +08:00
MengYX
652bb1fc32 optimize: imports 2021-08-08 07:51:42 +08:00
MengYX
6737e8c11b optimize: .kgm mask loading 2021-08-08 07:47:28 +08:00
sunhao03
71862538b7 fetch mask file fix on production 2021-08-07 22:13:00 +08:00
Emmm Monster
4251b94b1f chore: update deps & fix audit 2021-07-01 01:33:28 +08:00
Emmm Monster
8fdda048f6 fix: avoid using worker in file protocol 2021-07-01 01:29:04 +08:00
Emmm Monster
39c7294996 chore(ci): build after *.ts changes 2021-06-03 13:15:04 +08:00
Emmm Monster
48f879cb58 simplify: decrypt/ncm-cache & decrypt/common 2021-06-03 13:09:14 +08:00
Emmm Monster
f0875ad175 fix: decrypt/qmc-cache
adapt: decrypt/qmc for qmc-cache
2021-06-03 13:09:02 +08:00
qq1010903229
02a146e069 增加对网易云音乐.uc缓存格式和QQ音乐.cache缓存格式的支持 (#161)
* Update common.ts

* Create ncmcache.ts

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-15 14:56:29 +00:00
MengYX
0c23249d76 Bump Version 2020-03-12 18:56:21 +08:00
MengYX
6502d2d067 Merge CI Settings 2020-03-10 12:22:46 +08:00
MengYX
24e1d33642 Fix errors when parsing ncm files without metadata 2020-03-10 09:31:48 +08:00
MengYX
a72804544f Remove Source Map in production 2020-03-04 10:00:18 +08:00
MengYX
2aeb60d0a9 Update CI 2020-03-04 09:38:30 +08:00
MengYX
97ca09dbae Bump Version 2020-03-04 09:30:48 +08:00
MengYX
e78118b4d0 Add fix-compatibility npm command 2020-03-04 09:29:05 +08:00
MengYX
37c60caa06 Update Dependencies 2020-03-04 08:37:54 +08:00
MengYX
fba020969b Update Babel config 2020-03-04 08:37:39 +08:00
MengYX
790715726a Add temporary solution to fix compatibility for Edge 18 2020-03-04 08:37:16 +08:00
MengYX
4ca45e1233 Merge branch 'pull/22' 2020-03-03 20:01:42 +08:00
MengYX
cfacb77d15 Optimize loader tips for outdated browser 2020-03-03 20:00:44 +08:00
任宝硕
5f7461e8aa Update index.html 2020-03-03 16:41:45 +08:00
MengYX
aa98ec422c Reformat Code 2020-03-01 23:36:16 +08:00
MengYX
d2ec667c19 Fix Update Check 2020-03-01 23:34:24 +08:00
任宝硕
94ee8eb3bd Update index.html 2020-03-01 16:16:34 +08:00
任宝硕
c13f7fcced 对部分老旧浏览器进行显示调整 + 部分内容修复 2020-03-01 16:14:43 +08:00
MengYX
1d83898f08 #20 Add Support For Netease DJ Files 2020-02-29 19:52:41 +08:00
MengYX
98ca5bc8ff Bump Version 2020-02-23 13:52:03 +08:00
MengYX
984b549448 #19 Add Download Type: Origin Filename 2020-02-23 13:45:30 +08:00
MengYX
9fd7177ebb Add Update Check 2020-02-23 13:44:19 +08:00
MengYX
f7d19e62fd Immediately Load Latest App 2020-02-23 13:42:50 +08:00
MengYX
86d59f4e6f Update Dependencies 2020-02-23 13:42:20 +08:00
MengYX
3906572723 Optimize UI 2020-02-23 13:41:39 +08:00
MengYX
179f72687a Fix Decrypt Mflac Error 2020-02-12 23:04:48 +08:00
MengYX
ce251e3c9e Fix Decrypt Algorithm Error 2020-02-11 17:03:46 +08:00
MengYX
cc8e818142 Optimize Loading 2020-02-11 16:35:45 +08:00
MengYX
f99e885d9f Fix Mgg Mask Detect Bug 2020-02-11 16:23:50 +08:00
MengYX
58d9039960 Update README and Bump Version 2020-02-11 16:00:34 +08:00
MengYX
c314a251c2 Add Detect Media Type by File 2020-02-11 15:52:22 +08:00
MengYX
10f09958c4 Optimize Import 2020-02-11 14:48:27 +08:00
MengYX
41a45176be Use Universal Decoder for Qmc,Mgg,Mflac 2020-02-11 14:35:17 +08:00
MengYX
47cea6eae9 Use Universal Mask for Qmc,Mgg,Mflac
Add Local Experimental Support For Mgg
2020-02-11 00:34:26 +08:00
MengYX
2fc9368a92 Add Experimental Support For Mgg 2020-02-10 20:10:48 +08:00
MengYX
4e1bfb0b55 Bump Version 2020-02-09 15:16:23 +08:00
MengYX
27b74ea5dd Better Way to Detect Mflac Mask 2020-02-09 14:05:40 +08:00
MengYX
9aab7a7713 Adjust for Debugging 2020-02-07 20:17:45 +08:00
MengYX
dcde0d3fbb Fix Babel Config 2020-02-07 14:41:21 +08:00
MengYX
0c0299d63a Remove Useless Information 2020-02-06 16:18:40 +08:00
MengYX
3ee9f5d2d1 Split App.vue 2020-02-06 16:01:35 +08:00
MengYX
e3ca175258 Optimize Bundle Size 2020-02-06 14:05:46 +08:00
MengYX
05cdd7b896 Merge branch 'ix-master' 2020-02-05 02:08:08 +08:00
MengYX
91ba19d878 Bump Version 2020-02-05 01:58:53 +08:00
MengYX
a7c7b6cbfa Add instant download to avoid memory occupation 2020-02-05 01:53:58 +08:00
MengYX
50fbb69394 Merge branch 'pull/17'
# Conflicts:
#	src/App.vue
2020-02-05 01:38:34 +08:00
MengYX
402fb184f7 Add Web Worker 2020-02-05 00:30:44 +08:00
MengYX
0766e2fcb0 Merge pull request #16 from smtop/dev
增加歌曲命名格式选项
2020-02-04 19:41:01 +08:00
1519715742@qq.com
92bd0f6be3 Performance improvement in multiple files 2020-02-04 19:12:44 +08:00
smdev
9c6af8ff9c 增加歌曲命名格式选项 2020-02-04 18:24:53 +08:00
MengYX
8094f3ad58 Update CI 2020-02-01 12:05:00 +08:00
MengYX
e6a81f8546 Edit index.html Upgrade Dependencies 2020-01-31 11:47:16 +08:00
MengYX
211b4e0206 #9 Add QQ Music tkm Format 2020-01-27 18:02:39 +08:00
MengYX
4cd5b45986 Fix QMC filename error 2020-01-27 16:43:40 +08:00
MengYX
60445b7ed9 #11 Add Moo Music Format 2020-01-27 14:06:45 +08:00
MengYX
4e499b2deb Merge branch 'master' of github.com:ix64/unlock-music 2020-01-21 20:00:20 +08:00
MengYX
0cddb98612 Update CI 2020-01-21 19:52:45 +08:00
MengYX
ca3f4c1aa4 Update Readme [CI SKIP] 2020-01-21 19:31:30 +08:00
MengYX
eec5bd0fb8 Add Support For: tm0/2/3/6 2020-01-21 19:21:17 +08:00
MengYX
bddde78fcd Reconstruct 2020-01-21 19:20:46 +08:00
MengYX
bc138c4078 Update Deps 2020-01-21 19:01:52 +08:00
MengYX
51a5a8a44f Update README.md 2019-12-23 19:09:24 +08:00
MengYX
2fb5aecdb2 Update README.md 2019-12-16 19:01:56 +08:00
MengYX
8014c33538 Revert: Favicon 2019-12-08 19:12:15 +08:00
MengYX
e6bce501bb Upgrade Vue Cli 2019-12-07 15:09:09 +08:00
MengYX
de37519f8c Remove unused icon 2019-12-07 14:44:32 +08:00
67 changed files with 10616 additions and 5427 deletions

View File

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

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进行讨论
**附加信息**
更多你想要表达的内容

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

@@ -0,0 +1,76 @@
name: Test Build
on:
push:
paths:
- "**/*.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:
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 14.x
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Install Dependencies
run: |
npm ci
npm run fix-compatibility
- name: Build
env:
GZIP: "--best"
run: |
npm run build ${{ matrix.BUILD_ARGS }}
tar -czvf dist.tar.gz -C ./dist .
- name: Build Extension
if: ${{ matrix.BUILD_EXTENSION }}
run: |
npm run make-extension
cd dist
zip -rJ9 ../extension.zip *
cd ..
- name: Publish artifact
uses: actions/upload-artifact@v2
with:
name: unlock-music-${{ matrix.build }}.tar.gz
path: ./dist.tar.gz
- name: Publish artifact - Extension
if: ${{ matrix.BUILD_EXTENSION }}
uses: actions/upload-artifact@v2
with:
name: extension.zip
path: ./extension.zip

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

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

@@ -0,0 +1,128 @@
name: Build Release
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Install Dependencies
run: |
npm ci
npm run fix-compatibility
- 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

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-2020 MengYX
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,32 +1,71 @@
# Unlock Music 音乐解锁
- Unlock encrypted music file in browser.
- 在浏览器中解锁加密的音乐文件。
- [Online Demo](https://tool.ixarea.com/music)
[![Build Status](https://ci.ixarea.com/api/badges/ix64/unlock-music/status.svg)](https://ci.ixarea.com/ix64/unlock-music)
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- unlock-music项目是以学习和技术研究的初衷创建的修改、再分发时请遵循[License](https://github.com/ix64/unlock-music/blob/master/LICENSE)
- Unlock Music的CLI版本正在开发中。
- 我们新建了Telegram群组欢迎加入[https://t.me/unlock_music_chat](https://t.me/unlock_music_chat)
- [CLI版本 Alpha](https://github.com/unlock-music/cli) 大批量转换建议使用CLI版本
- [相关的其他项目](https://github.com/ix64/unlock-music/wiki/%E5%92%8CUnlockMusic%E7%9B%B8%E5%85%B3%E7%9A%84%E9%A1%B9%E7%9B%AE)
# Features
- [x] Unlock in browser 在浏览器中解锁
- [x] QQMusic File QQ音乐格式 (.qmc0/.qmc3/.qmcflac/.qmcogg)
- [ ] QQMusic New Format QQ音乐新格式
- [x] .mflac (Partial 部分支持)
- [ ] .mgg
- [x] Netease Format 网易云音乐格式 (.ncm)
- [x] Drag and Drop 拖放文件
- [x] Play instantly 在线播放
- [x] Batch unlocking 批量解锁
- [x] Progressive Web App 渐进式Web应用
- [x] Complete ID3 for ncm 补全ncm的ID3信息
- [ ] Multi-language 多语言
![Test Build](https://github.com/ix64/unlock-music/workflows/Test%20Build/badge.svg)
![GitHub releases](https://img.shields.io/github/downloads/ix64/unlock-music/total)
![Docker Pulls](https://img.shields.io/docker/pulls/ix64/unlock-music)
# 使用方法
## 下载已构建版本
- 已构建的版本发布在 [GitHub Release](https://github.com/ix64/unlock-music/releases/latest), 下载解压缩后即可部署或本地使用
## 特性
### 支持的格式
- [x] QQ音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/[.tkm](https://github.com/ix64/unlock-music/issues/9))
- [x] 写入封面图片
- [x] Moo音乐格式 ([.bkcmp3/.bkcflac](https://github.com/ix64/unlock-music/issues/11))
- [x] QQ音乐Tm格式 (.tm0/.tm2/.tm3/.tm6)
- [x] QQ音乐新格式 (实验性支持)
- [x] .mflac
- [x] [.mgg](https://github.com/ix64/unlock-music/issues/3)
- [x] 网易云音乐格式 (.ncm)
- [x] 补全ncm的ID3/FlacMeta信息
- [x] 虾米音乐格式 (.xm) (测试阶段)
- [x] 酷我音乐格式 (.kwm) (测试阶段)
- [x] 酷狗音乐格式 (
.kgm) ([CLI版本](https://github.com/ix64/unlock-music/wiki/%E5%85%B6%E4%BB%96%E9%9F%B3%E4%B9%90%E6%A0%BC%E5%BC%8F%E5%B7%A5%E5%85%B7#%E9%85%B7%E7%8B%97%E9%9F%B3%E4%B9%90-kgmvpr%E8%A7%A3%E9%94%81%E5%B7%A5%E5%85%B7))
### 其他特性
- [x] 在浏览器中解锁
- [x] 拖放文件
- [x] 在线播放
- [x] 批量解锁
- [x] 渐进式Web应用
- [x] 多线程
## 使用方法
### 安装浏览器扩展
[![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/ix64/unlock-music/releases/latest)下载已构建的版本
- 本地使用请下载`legacy版本``modern版本`只能通过**http/https协议**访问)
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**
### 使用Docker镜像
```shell
docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
```
### 自行构建
- 环境要求
- nodejs
- npm
## 自行构建
- 环境要求
- nodejs
- npm
1. 获取项目源代码后执行 `npm install` 安装相关依赖
2. 执行 `npm run build` 即可进行构建,构建输出为 dist 目录
- `npm run serve` 可用于开发
3. 如需构建浏览器扩展build完成后还需要执行`npm run make-extension`

View File

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

15
extension-manifest.json Normal file
View File

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

20
make-extension.js Normal file
View File

@@ -0,0 +1,20 @@
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)
manifest["version"] = pkg["version"]
fs.writeFileSync("./dist/manifest.json", JSON.stringify(manifest), "utf-8")
console.log("Write: manifest.json")

12301
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,47 @@
{
"name": "unlock-music",
"version": "1.0.2",
"version": "v1.9.0",
"updateInfo": "新增写入本地文件系统; 优化.kwm解锁; 支持.acc嗅探; 使用Typescript重构",
"license": "MIT",
"description": "Unlock encrypted music file in browser.",
"repository": {
"type": "git",
"url": "https://github.com/ix64/unlock-music"
},
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
"build": "vue-cli-service build",
"fix-compatibility": "node ./src/fix-compatibility.js",
"make-extension": "node ./make-extension.js"
},
"dependencies": {
"browser-id3-writer": "^4.3.0",
"core-js": "^2.6.10",
"crypto-js": "^3.1.9-1",
"element-ui": "^2.13.0",
"music-metadata-browser": "^1.8.1",
"register-service-worker": "^1.6.2",
"vue": "^2.6.10"
"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-browser": "^2.4.3",
"register-service-worker": "^1.7.2",
"threads": "^1.6.5",
"vue": "^2.6.14"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-pwa": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"@types/crypto-js": "^4.0.2",
"@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",
"node-sass": "^5.0.0",
"sass-loader": "^10.2.0",
"semver": "^7.3.5",
"threads-plugin": "^1.4.0",
"typescript": "~4.1.6",
"vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.10"
"vue-template-compiler": "^2.6.14"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -2,69 +2,42 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<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">
<script>var _paq = window._paq || [];
_paq.push(['setRequestMethod', 'POST'], ['trackPageView'], ['enableLinkTracking'], ['setSiteId', '2'],
['setTrackerUrl', 'https://stats.ixarea.com/ixarea-stats/report']);
</script>
<script async src="https://stats.ixarea.com/ixarea-stats.js"></script>
<title>音乐解锁 - By IXarea</title>
<meta content="音乐,解锁,ncm,qmc,qmc0,qmc3,qmcflac,qmcogg,mflac,qq音乐,网易云音乐,加密" name="keywords"/>
<title>音乐解锁</title>
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords"/>
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description"/>
<style>
#loader {
position: absolute;
left: 50%;
top: 50%;
z-index: 1010;
margin: -75px 0 0 -75px;
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
@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>
<h3 id="loader-js">请启用JavaScript</h3>
<img alt=""
src="https://stats.ixarea.com/ixarea-stats/report?idsite=2&rec=1&action_name=音乐解锁+-+By+IXarea"
src="https://stats.ixarea.com/ixarea-stats/report?rec=1&action_name=音乐解锁-NoJS&idsite=2"
style="border:0"/>
</noscript>
<strong>音乐解锁采用了一些新特性!建议使用
<h3 id="loader-source"> 请勿直接运行源代码! </h3>
<div id="loader-tips-outdated" hidden>
<h2>您可能在使用不受支持的<span style="color:#f00;">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
<h3>如果您使用双核浏览器,您可以尝试切换到 <span style="color:#f00;">“极速模式”</span> 解决此问题。</h3>
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
</div>
<h3 id="loader-tips-timeout" hidden>
音乐解锁采用了一些新特性!建议使用
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
| <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</strong>
</h3>
</div>
<div id="app"></div>
<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,244 +1,85 @@
<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-delete" plain type="danger">删除全部</el-button>
</el-row>
<audio :autoplay="playing_auto" :src="playing_url" controls></audio>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="封面">
<template slot-scope="scope">
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
<div class="image-slot el-image__error" slot="error">
暂无封面
</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="歌曲" sortable>
<template slot-scope="scope">
<span style="margin-left: 10px">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="歌手" sortable>
<template slot-scope="scope">
<p>{{ scope.row.artist }}</p>
</template>
</el-table-column>
<el-table-column label="专辑" sortable>
<template slot-scope="scope">
<p>{{ scope.row.album }}</p>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button @click="handlePlay(scope.$index, scope.row)"
circle icon="el-icon-video-play" type="success">
</el-button>
<el-button @click="handleDownload(scope.row)"
circle icon="el-icon-download">
</el-button>
<el-button @click="handleDelete(scope.$index, scope.row)"
circle icon="el-icon-delete" type="danger">
</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
<el-footer id="app-footer">
<el-row>
音乐解锁移除已购音乐的加密保护
目前支持网易云音乐(ncm)和QQ音乐(qmc0, qmc3, qmcflac, qmcogg, mflac)
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</el-row>
<el-row>
<span>Copyright &copy; 2019</span>
<a href="https://github.com/ix64" target="_blank">MengYX</a>
音乐解锁使用
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
开放
<a href="https://github.com/ix64/unlock-music" target="_blank">源代码</a>
</el-row>
</el-footer>
</el-container>
</div>
<el-container id="app">
<el-main>
<Home/>
</el-main>
<el-footer id="app-footer">
<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>
目前支持网易云音乐(ncm), 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>
const NcmDecrypt = require("./plugins/ncm");
const QmcDecrypt = require("./plugins/qmc");
const RawDecrypt = require("./plugins/raw");
const MFlacDecrypt = require("./plugins/mflac");
export default {
name: 'app',
components: {},
data() {
return {
activeIndex: '1',
tableData: [],
playing_url: "",
playing_auto: false,
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";
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)
}
},
mounted() {
this.$nextTick(function () {
this.finishLoad();
});
},
methods: {
finishLoad() {
document.getElementById("loader-mask").remove();
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/>' +
'最近更新支持qmcogg,mflac<br/>' +
'点击查看 <a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
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'
});
},
handleFile(file) {
let ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
(async () => {
let data = null;
switch (ext) {
case "ncm":
data = await NcmDecrypt.Decrypt(file.raw);
break;
case "mp3":
case "flac":
data = await RawDecrypt.Decrypt(file.raw);
break;
case "qmc3":
case "qmc0":
case "qmcflac":
case "qmcogg":
data = await QmcDecrypt.Decrypt(file.raw);
break;
case "mflac":
data = await MFlacDecrypt.Decrypt(file.raw);
break;
default:
data = {
status: false,
message: "不支持此文件格式",
};
break;
}
if (data.status) {
this.tableData.push(data);
this.$notify.success({
title: '解锁成功',
message: '成功解锁 ' + data.title,
duration: 3000
});
let _rp_data = [data.title, data.artist, data.album];
console.log(data);
window._paq.push(["trackEvent", "Unlock", ext + "," + data.mime, JSON.stringify(_rp_data)]);
} else {
this.$notify.error({
title: '出现问题',
message: data.message + "" + file.name +
',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
dangerouslyUseHTMLString: true,
duration: 6000
});
window._paq.push(["trackEvent", "Error", data.message, file.name]);
}
})();
},
handlePlay(index, row) {
this.playing_url = row.file;
this.playing_auto = true;
},
handleDelete(index, row) {
console.log(index);
URL.revokeObjectURL(row.file);
URL.revokeObjectURL(row.picture);
this.tableData.splice(index, 1);
},
handleDownload(row) {
let a = document.createElement('a');
a.href = row.file;
a.download = row.filename;
document.body.append(a);
a.click();
a.remove();
},
handleDeleteAll() {
this.tableData.forEach(value => {
URL.revokeObjectURL(value.file);
URL.revokeObjectURL(value.picture);
});
this.tableData = [];
},
handleDownloadAll() {
let index = 0;
let c = setInterval(() => {
if (index < this.tableData.length) {
this.handleDownload(this.tableData[index]);
index++;
} else {
clearInterval(c);
}
}, 1000);
}
}
}
},
}
</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,99 @@
<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 {CommonDecrypt} from "@/decrypt/common.ts";
import {DecryptQueue} from "@/utils/utils";
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 = CommonDecrypt) => {
console.log("start handling", file.name)
try {
this.$emit("success", await dec(file));
} catch (e) {
console.error(e)
this.$emit("error", e, file.name)
} finally {
this.task_finished++
}
})
},
}
}
</script>

View File

@@ -0,0 +1,71 @@
<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>

79
src/decrypt/common.ts Normal file
View File

@@ -0,0 +1,79 @@
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
import {Decrypt as NcmCacheDecrypt} from "@/decrypt/ncmcache";
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
import {Decrypt as QmcCacheDecrypt} from "@/decrypt/qmccache";
import {Decrypt as KgmDecrypt} from "@/decrypt/kgm";
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
import {DecryptResult, FileInfo} from "@/decrypt/entity";
import {SplitFilename} from "@/decrypt/utils";
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
const raw = SplitFilename(file.name)
let rt_data: DecryptResult;
switch (raw.ext) {
case "ncm":// Netease Mp3/Flac
rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
break;
case "uc":// Netease Cache
rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext);
break;
case "kwm":// Kuwo Mp3/Flac
rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
break
case "xm": // Xiami Wav/M4a/Mp3/Flac
case "wav":// Xiami/Raw Wav
case "mp3":// Xiami/Raw Mp3
case "flac":// Xiami/Raw Flac
case "m4a":// Xiami/Raw M4a
rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
break;
case "ogg":// Raw Ogg
rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
break;
case "tm0":// QQ Music IOS Mp3
case "tm3":// QQ Music IOS Mp3
rt_data = await RawDecrypt(file.raw, raw.name, "mp3");
break;
case "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
case "bkcmp3"://Moo Music Mp3
case "bkcflac"://Moo Music Flac
case "mflac"://QQ Music Desktop Flac
case "mgg": //QQ Music Desktop 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
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;
}

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

@@ -0,0 +1,26 @@
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
}

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

@@ -0,0 +1,122 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt
} from "@/decrypt/utils.ts";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
import {DecryptResult} from "@/decrypt/entity";
import config from "@/../package.json"
const VprHeader = [
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31]
const KgmHeader = [
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14]
const VprMaskDiff = [
0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
0x00]
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (raw_ext === "vpr") {
if (!BytesHasPrefix(oriData, VprHeader)) throw Error("Not a valid vpr file!")
} else {
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error("Not a valid kgm(a) file!")
}
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer)
let headerLen = bHeaderLen.getUint32(0, true)
let audioData = oriData.slice(headerLen)
let dataLen = audioData.length
if (audioData.byteLength > 1 << 26) {
throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁")
}
let key1 = new Uint8Array(17)
key1.set(oriData.slice(0x1c, 0x2c), 0)
if (MaskV2.length === 0) {
if (!await LoadMaskV2()) throw Error("加载Kgm/Vpr Mask数据失败")
}
for (let i = 0; i < dataLen; i++) {
let med8 = key1[i % 17] ^ audioData[i]
med8 ^= (med8 & 0xf) << 4
let msk8 = GetMask(i)
msk8 ^= (msk8 & 0xf) << 4
audioData[i] = med8 ^ msk8
}
if (raw_ext === "vpr") {
for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17]
}
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,
ext,
mime,
title,
artist
}
}
function GetMask(pos: number) {
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]
}
let MaskV2: Uint8Array = new Uint8Array(0);
async function LoadMaskV2(): Promise<boolean> {
let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/static/kgm.mask`
if (["http:", "https:"].some(v => v == self.location.protocol)) {
if (!!self.document) {// using Web Worker
mask_url = "./static/kgm.mask"
} else {// using Main thread
mask_url = "../static/kgm.mask"
}
}
try {
const resp = await fetch(mask_url, {method: "GET"})
MaskV2 = new Uint8Array(await resp.arrayBuffer());
return true
} catch (e) {
console.error(e)
return false
}
}
const MaskV2PreDef = [
0xB8, 0xD5, 0x3D, 0xB2, 0xE9, 0xAF, 0x78, 0x8C, 0x83, 0x33, 0x71, 0x51, 0x76, 0xA0, 0xCD, 0x37,
0x2F, 0x3E, 0x35, 0x8D, 0xA9, 0xBE, 0x98, 0xB7, 0xE7, 0x8C, 0x22, 0xCE, 0x5A, 0x61, 0xDF, 0x68,
0x69, 0x89, 0xFE, 0xA5, 0xB6, 0xDE, 0xA9, 0x77, 0xFC, 0xC8, 0xBD, 0xBD, 0xE5, 0x6D, 0x3E, 0x5A,
0x36, 0xEF, 0x69, 0x4E, 0xBE, 0xE1, 0xE9, 0x66, 0x1C, 0xF3, 0xD9, 0x02, 0xB6, 0xF2, 0x12, 0x9B,
0x44, 0xD0, 0x6F, 0xB9, 0x35, 0x89, 0xB6, 0x46, 0x6D, 0x73, 0x82, 0x06, 0x69, 0xC1, 0xED, 0xD7,
0x85, 0xC2, 0x30, 0xDF, 0xA2, 0x62, 0xBE, 0x79, 0x2D, 0x62, 0x62, 0x3D, 0x0D, 0x7E, 0xBE, 0x48,
0x89, 0x23, 0x02, 0xA0, 0xE4, 0xD5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xFD, 0x16, 0x3A, 0x21, 0x3B,
0x16, 0x0F, 0xC3, 0xB2, 0xBB, 0xB3, 0xE2, 0xBA, 0x3A, 0x3D, 0x13, 0xEC, 0xF6, 0x01, 0x45, 0x84,
0xA5, 0x70, 0x0F, 0x93, 0x49, 0x0C, 0x64, 0xCD, 0x31, 0xD5, 0xCC, 0x4C, 0x07, 0x01, 0x9E, 0x00,
0x1A, 0x23, 0x90, 0xBF, 0x88, 0x1E, 0x3B, 0xAB, 0xA6, 0x3E, 0xC4, 0x73, 0x47, 0x10, 0x7E, 0x3B,
0x5E, 0xBC, 0xE3, 0x00, 0x84, 0xFF, 0x09, 0xD4, 0xE0, 0x89, 0x0F, 0x5B, 0x58, 0x70, 0x4F, 0xFB,
0x65, 0xD8, 0x5C, 0x53, 0x1B, 0xD3, 0xC8, 0xC6, 0xBF, 0xEF, 0x98, 0xB0, 0x50, 0x4F, 0x0F, 0xEA,
0xE5, 0x83, 0x58, 0x8C, 0x28, 0x2C, 0x84, 0x67, 0xCD, 0xD0, 0x9E, 0x47, 0xDB, 0x27, 0x50, 0xCA,
0xF4, 0x63, 0x63, 0xE8, 0x97, 0x7F, 0x1B, 0x4B, 0x0C, 0xC2, 0xC1, 0x21, 0x4C, 0xCC, 0x58, 0xF5,
0x94, 0x52, 0xA3, 0xF3, 0xD3, 0xE0, 0x68, 0xF4, 0x00, 0x23, 0xF3, 0x5E, 0x0A, 0x7B, 0x93, 0xDD,
0xAB, 0x12, 0xB2, 0x13, 0xE8, 0x84, 0xD7, 0xA7, 0x9F, 0x0F, 0x32, 0x4C, 0x55, 0x1D, 0x04, 0x36,
0x52, 0xDC, 0x03, 0xF3, 0xF9, 0x4E, 0x42, 0xE9, 0x3D, 0x61, 0xEF, 0x7C, 0xB6, 0xB3, 0x93, 0x50,
]

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

@@ -0,0 +1,77 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt
} from "@/decrypt/utils.ts";
import {Decrypt as RawDecrypt} from "@/decrypt/raw.ts";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
import {DecryptResult} from "@/decrypt/entity";
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
}

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

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

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

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

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

@@ -0,0 +1,141 @@
import {QmcMask, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask";
import {toByteArray as Base64Decode} from 'base64-js'
import {
AudioMimeType,
GetArrayBuffer,
GetCoverFromFile,
GetImageFromURL,
GetMetaFromFile,
SniffAudioExt, WriteMetaToFlac, WriteMetaToMp3
} from "@/decrypt/utils.ts";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
import iconv from "iconv-lite";
import {DecryptResult} from "@/decrypt/entity";
import {queryAlbumCover, queryKeyInfo, reportKeyUsage} from "@/utils/api";
interface Handler {
ext: string
detect: boolean
handler(data?: Uint8Array): QmcMask | undefined
}
export const HandlerMap: { [key: string]: Handler } = {
"mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true},
"mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true},
"mgg.cache": {handler: QmcMaskDetectMgg, ext: "ogg", detect: false},
"mflac.cache": {handler: QmcMaskDetectMflac, ext: "flac", detect: false},
"qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
"qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"qmcogg": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
"qmcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false},
"bkcmp3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"bkcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false},
"tkm": {handler: QmcMaskGetDefault, ext: "m4a", detect: false},
"666c6163": {handler: QmcMaskGetDefault, ext: "flac", detect: false},
"6d7033": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"6f6767": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
"6d3461": {handler: QmcMaskGetDefault, ext: "m4a", detect: false},
"776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false}
};
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];
const fileData = new Uint8Array(await GetArrayBuffer(file));
let audioData, seed, keyData;
if (handler.detect) {
const keyLen = new DataView(fileData.slice(fileData.length - 4).buffer).getUint32(0, true)
const keyPos = fileData.length - 4 - keyLen;
audioData = fileData.slice(0, keyPos);
seed = handler.handler(audioData);
keyData = fileData.slice(keyPos, keyPos + keyLen);
if (!seed) seed = await queryKey(keyData, raw_filename, raw_ext);
if (!seed) throw raw_ext + "格式仅提供实验性支持";
} else {
audioData = fileData;
seed = handler.handler(audioData) as QmcMask;
if (!seed) throw raw_ext + "格式仅提供实验性支持";
}
let musicDecoded = seed.Decrypt(audioData);
const ext = SniffAudioExt(musicDecoded, handler.ext);
const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], {type: mime});
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");
}
}
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
if (keyData) reportKeyUsage(keyData, seed.getMatrix128(),
raw_filename, raw_ext, info.title, info.artist, musicMeta.common.album).then().catch();
let imgUrl = GetCoverFromFile(musicMeta);
if (!imgUrl) {
imgUrl = await getCoverImage(info.title, info.artist, musicMeta.common.album);
if (imgUrl) {
const imageInfo = await GetImageFromURL(imgUrl);
if (imageInfo) {
imgUrl = imageInfo.url
try {
const newMeta = {picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(" _ ")}
if (ext === "mp3") {
musicDecoded = WriteMetaToMp3(Buffer.from(musicDecoded), newMeta, musicMeta)
musicBlob = new Blob([musicDecoded], {type: mime});
} else if (ext === 'flac') {
musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta)
musicBlob = new Blob([musicDecoded], {type: mime});
} else {
console.info("writing metadata for " + ext + " is not being supported for now")
}
} catch (e) {
console.warn("Error while appending cover image to file " + e)
}
}
}
}
return {
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
picture: imgUrl,
file: URL.createObjectURL(musicBlob),
blob: musicBlob,
mime: mime
}
}
async function queryKey(keyData: Uint8Array, filename: string, format: string): Promise<QmcMask | undefined> {
try {
const data = await queryKeyInfo(keyData, filename, format)
return new QmcMask(Base64Decode(data.Matrix44));
} catch (e) {
console.warn(e);
}
}
async function getCoverImage(title: string, artist?: string, album?: string): Promise<string> {
const song_query_url = "https://stats.ixarea.com/apis" + "/music/qq-cover"
try {
const data = await queryAlbumCover(title, artist, album)
return `${song_query_url}/${data.Type}/${data.Id}`
} catch (e) {
console.warn(e);
}
return ""
}

206
src/decrypt/qmcMask.ts Normal file
View File

@@ -0,0 +1,206 @@
import {BytesHasPrefix, FLAC_HEADER, OGG_HEADER} from "@/decrypt/utils.ts";
const QMOggPublicHeader1 = [
0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x1e, 0x01, 0x76, 0x6f, 0x72,
0x62, 0x69, 0x73, 0x00, 0x00, 0x00, 0x00, 0x02, 0x44, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xee, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x01, 0x4f, 0x67, 0x67, 0x53, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff];
const QMOggPublicHeader2 = [
0x03, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x2c, 0x00, 0x00, 0x00, 0x58, 0x69, 0x70, 0x68, 0x2e,
0x4f, 0x72, 0x67, 0x20, 0x6c, 0x69, 0x62, 0x56, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x20, 0x49, 0x20,
0x32, 0x30, 0x31, 0x35, 0x30, 0x31, 0x30, 0x35, 0x20, 0x28, 0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84,
0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84, 0x29, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x54,
0x49, 0x54, 0x4c, 0x45, 0x3d];
const QMOggPublicConf1 = [
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0,
0, 0, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 6, 3, 3, 3, 3, 6, 6, 6, 6,
3, 3, 3, 3, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9,
0, 0, 0, 0];
const QMOggPublicConf2 = [
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 0, 1, 3, 3, 0, 1, 3, 3, 3,
3, 3, 3, 3, 3];
const QMCDefaultMaskMatrix = [
0xde, 0x51, 0xfa, 0xc3, 0x4a, 0xd6, 0xca, 0x90,
0x7e, 0x67, 0x5e, 0xf7, 0xd5, 0x52, 0x84, 0xd8,
0x47, 0x95, 0xbb, 0xa1, 0xaa, 0xc6, 0x66, 0x23,
0x92, 0x62, 0xf3, 0x74, 0xa1, 0x9f, 0xf4, 0xa0,
0x1d, 0x3f, 0x5b, 0xf0, 0x13, 0x0e, 0x09, 0x3d,
0xf9, 0xbc, 0x00, 0x11];
const AllMapping: number[][] = [];
const Mask128to44: number[] = [];
(function () {
for (let i = 0; i < 128; i++) {
let realIdx = (i * i + 27) % 256
if (realIdx in AllMapping) {
AllMapping[realIdx].push(i)
} else {
AllMapping[realIdx] = [i]
}
}
let idx44 = 0
AllMapping.forEach(all128 => {
all128.forEach(_i128 => {
Mask128to44[_i128] = idx44
})
idx44++
})
})();
export class QmcMask {
private readonly Matrix128: number[];
constructor(matrix: number[] | Uint8Array) {
if (matrix instanceof Uint8Array) matrix = Array.from(matrix)
if (matrix.length === 44) {
this.Matrix128 = this._generate128(matrix)
} else if (matrix.length === 128) {
this.Matrix128 = matrix
} else {
throw Error("invalid mask length")
}
}
getMatrix128() {
return this.Matrix128
}
getMatrix44(): number[] {
const matrix44: number[] = []
let idxI44 = 0
AllMapping.forEach(it256 => {
let it256Len = it256.length
for (let i = 1; i < it256Len; i++) {
if (this.Matrix128[it256[0]] !== this.Matrix128[it256[i]]) {
throw "decode mask-128 to mask-44 failed"
}
}
matrix44[idxI44] = this.Matrix128[it256[0]]
idxI44++
})
return matrix44
}
Decrypt(data: Uint8Array) {
if (!this.Matrix128) throw Error("bad call sequence")
let dst = data.slice(0);
let index = -1;
let maskIdx = -1;
for (let cur = 0; cur < data.length; cur++) {
index++;
maskIdx++;
if (index === 0x8000 || (index > 0x8000 && (index + 1) % 0x8000 === 0)) {
index++;
maskIdx++;
}
if (maskIdx >= 128) maskIdx -= 128;
dst[cur] ^= this.Matrix128[maskIdx];
}
return dst;
}
private _generate128(matrix44: number[]): number[] {
const matrix128: number[] = []
let idx44 = 0
AllMapping.forEach(it256 => {
it256.forEach(m => {
matrix128[m] = matrix44[idx44]
})
idx44++
})
return matrix128
}
}
export function QmcMaskGetDefault() {
return new QmcMask(QMCDefaultMaskMatrix)
}
export function QmcMaskDetectMflac(data: Uint8Array) {
let search_len = Math.min(0x8000, data.length), mask;
for (let block_idx = 0; block_idx < search_len; block_idx += 128) {
try {
mask = new QmcMask(data.slice(block_idx, block_idx + 128));
if (BytesHasPrefix(mask.Decrypt(data.slice(0, FLAC_HEADER.length)), FLAC_HEADER)) {
break;
}
} catch (e) {
}
}
return mask;
}
export function QmcMaskDetectMgg(data: Uint8Array) {
if (data.length < 0x100) return
let matrixConfidence: { [key: number]: { [key: number]: number } } = {};
for (let i = 0; i < 44; i++) matrixConfidence[i] = {};
const page2 = data[0x54] ^ data[0xC] ^ QMOggPublicHeader1[0xC];
const spHeader = QmcGenerateOggHeader(page2)
const spConf = QmcGenerateOggConf(page2)
for (let idx128 = 0; idx128 < spHeader.length; idx128++) {
if (spConf[idx128] === 0) continue;
let idx44 = Mask128to44[idx128 % 128];
let _m = data[idx128] ^ spHeader[idx128]
let confidence = spConf[idx128];
if (_m in matrixConfidence[idx44]) {
matrixConfidence[idx44][_m] += confidence
} else {
matrixConfidence[idx44][_m] = confidence
}
}
let matrix = [];
try {
for (let i = 0; i < 44; i++)
matrix[i] = calcMaskFromConfidence(matrixConfidence[i]);
} catch (e) {
return;
}
const mask = new QmcMask(matrix);
if (!BytesHasPrefix(mask.Decrypt(data.slice(0, OGG_HEADER.length)), OGG_HEADER)) {
return;
}
return mask;
}
function calcMaskFromConfidence(confidence: { [key: number]: number }) {
const count = Object.keys(confidence).length
if (count === 0) throw "can not match at least one key";
if (count > 1) console.warn("There are 2 potential value for the mask!")
let result = ""
let conf = 0
for (let idx in confidence) {
if (confidence[idx] > conf) {
result = idx;
conf = confidence[idx];
}
}
return Number(result)
}
function QmcGenerateOggHeader(page2: number) {
let spec = [page2, 0xFF]
for (let i = 2; i < page2; i++) spec.push(0xFF)
spec.push(0xFF)
return QMOggPublicHeader1.concat(spec, QMOggPublicHeader2)
}
function QmcGenerateOggConf(page2: number) {
let specConf = [6, 0]
for (let i = 2; i < page2; i++) specConf.push(4)
specConf.push(0)
return QMOggPublicConf1.concat(specConf, QMOggPublicConf2)
}

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

@@ -0,0 +1,51 @@
import {
AudioMimeType,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
SplitFilename
} from "@/decrypt/utils.ts";
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]
}
}

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

@@ -0,0 +1,28 @@
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
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.ts";
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)
}

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

@@ -0,0 +1,170 @@
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];
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 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 || "Cover",
})
}
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)
}
}

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

@@ -0,0 +1,66 @@
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
import {DecryptResult} from "@/decrypt/entity";
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile} from "@/decrypt/utils.ts";
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>

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

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

25
src/fix-compatibility.js Normal file
View File

@@ -0,0 +1,25 @@
//TODO: Use other method to fix this
// !! Only Temporary Solution
// it seems like that @babel/plugin-proposal-object-rest-spread not working
// to fix up the compatibility for Edge 18 and some older Chromium
// now manually edit the dependency files
const fs = require('fs');
const filePath = "./node_modules/file-type/core.js";
const regReplace = /{\s*([a-zA-Z0-9:,\s]*),\s*\.\.\.([a-zA-Z0-9]*)\s*};/m;
if (fs.existsSync(filePath)) {
console.log("File Found!");
let data = fs.readFileSync(filePath).toString();
const regResult = regReplace.exec(data);
if (regResult != null) {
data = data.replace(regResult[0],
"Object.assign({ " + regResult[1] + " }, " + regResult[2] + ");"
);
fs.writeFileSync(filePath, data);
console.log("Object rest spread in file-type fixed!");
} else {
console.log("No fix needed.");
}
} else {
console.log("File Not Found!");
}

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

View File

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

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
const musicMetadata = require("music-metadata-browser");
export {Decrypt}
const audio_mime_type = {
mp3: "audio/mpeg",
flac: "audio/flac"
};
async function Decrypt(file) {
let tag = await musicMetadata.parseBlob(file);
let pic_url = "";
if (tag.common.picture !== undefined && tag.common.picture.length > 0) {
let pic = new Blob([tag.common.picture[0].data], {type: tag.common.picture[0].format});
pic_url = URL.createObjectURL(pic);
}
let file_url = URL.createObjectURL(file);
let filename_no_ext = file.name.substring(0, file.name.lastIndexOf("."));
let filename_array = filename_no_ext.split("-");
let filename_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
const mime = audio_mime_type[filename_ext];
let title = tag.common.title;
let artist = tag.common.artist;
if (filename_array.length > 1) {
if (artist === undefined) artist = filename_array[0].trim();
if (title === undefined) title = filename_array[1].trim();
} else if (filename_array.length === 1) {
if (title === undefined) title = filename_array[0].trim();
}
const filename = artist + " - " + title + "." + filename_ext;
return {
status:true,
filename: filename,
title: title,
artist: artist,
album: tag.common.album,
picture: pic_url,
file: file_url,
mime: mime
}
}

View File

@@ -2,13 +2,11 @@
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'
)
console.log('App is being served from cache by a service worker.')
},
registered() {
console.log('Service worker has been registered.')
@@ -20,7 +18,8 @@ if (process.env.NODE_ENV === 'production') {
console.log('New content is downloading.')
},
updated() {
console.log('New content is available; please refresh.')
console.log('New content is available.');
window.location.reload();
},
offline() {
console.log('No internet connection found. App is running in offline mode.')

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 后面以获得更高优先级

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

@@ -0,0 +1,25 @@
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
}
}

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

@@ -0,0 +1,58 @@
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>
}
}

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

@@ -0,0 +1,17 @@
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
}

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

@@ -0,0 +1,56 @@
import {fromByteArray as Base64Encode} from "base64-js";
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 function reportKeyUsage(keyData: Uint8Array, maskData: number[], filename: string, format: string, title: string, artist?: string, album?: string) {
return fetch(IXAREA_API_ENDPOINT + "/qmcmask/usage", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
Mask: Base64Encode(new Uint8Array(maskData)), Key: Base64Encode(keyData),
Artist: artist, Title: title, Album: album, Filename: filename, Format: format
}),
})
}
interface KeyInfo {
Matrix44: string
}
export async function queryKeyInfo(keyData: Uint8Array, filename: string, format: string): Promise<KeyInfo> {
const resp = await fetch(IXAREA_API_ENDPOINT + "/qmcmask/query", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44}),
});
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()
}

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

@@ -0,0 +1,79 @@
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 {CommonDecrypt} from "@/decrypt/common";
expose(CommonDecrypt)

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

@@ -0,0 +1,157 @@
<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>
<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 {DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile} from "@/utils/utils"
export default {
name: 'Home',
components: {
FileSelector,
PreviewTable
},
data() {
return {
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 = [];
},
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>

42
tsconfig.json Normal file
View File

@@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
],
"resolveJsonModule": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,4 +1,43 @@
const ThreadsPlugin = require('threads-plugin');
module.exports = {
publicPath: '',
productionSourceMap: true
};
productionSourceMap: false,
pwa: {
manifestPath: "web-manifest.json",
name: "音乐解锁",
themeColor: "#4DBA87",
msTileColor: "#000000",
manifestOptions: {
start_url: "./index.html",
description: "在任何设备上解锁已购的加密音乐!",
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'
}
]
},
appleMobileWebAppCapable: 'yes',
iconPaths: {
faviconSVG: './img/icons/safari-pinned-tab.svg',
favicon32: './img/icons/favicon-32x32.png',
favicon16: './img/icons/favicon-16x16.png',
appleTouchIcon: './img/icons/apple-touch-icon-152x152.png',
maskIcon: './img/icons/safari-pinned-tab.svg',
msTileImage: './img/icons/msapplication-icon-144x144.png'
},
workboxPluginMode: "GenerateSW",
workboxOptions: {
skipWaiting: true
}
},
configureWebpack: {
plugins: [new ThreadsPlugin()]
}
};