Compare commits

...

238 Commits
main ... main

Author SHA1 Message Date
Jixun Wu 0ea0e8352c build: minify final mjs
continuous-integration/drone/push Build is passing Details
2024-01-18 00:59:11 +00:00
Jixun Wu 922bfd1dfa chore: make win64 build to its own dir
continuous-integration/drone/push Build is passing Details
2024-01-18 00:38:01 +00:00
Jixun Wu cd279c1767 ci: publish site deployment to netlify as well 2024-01-18 00:29:37 +00:00
Jixun Wu 0f0c21144a 0.2.7
continuous-integration/drone/push Build is passing Details
2023-12-28 23:31:28 +00:00
Jixun Wu a59ef69d2a docs: add wry project to docs and app 2023-12-28 23:31:23 +00:00
Jixun Wu 4454fb8508 docs: update note on WebView2 2023-12-28 23:14:48 +00:00
Jixun Wu 7f861a5142 build: make zip archive of final zip 2023-12-28 23:09:56 +00:00
Jixun Wu fd3b0aef75 docs: add note about win64 build 2023-12-28 23:00:41 +00:00
Jixun Wu d824620472 0.2.6
continuous-integration/drone/push Build is passing Details
2023-12-28 20:39:50 +00:00
Jixun Wu 07d6dba277 build: include commit hash in version.txt 2023-12-28 20:38:45 +00:00
Jixun Wu 8f2d306835 build: include version.txt in dist 2023-12-28 20:36:29 +00:00
Jixun Wu 8df421de64 chore: add related projects 2023-12-28 20:30:10 +00:00
Jixun Wu 98bd618fab docs: make note about metadata editor 2023-12-28 14:18:11 +00:00
Jixun Wu eb7ec038b8 docs: update warning about android emu
continuous-integration/drone/push Build is passing Details
2023-12-25 20:02:55 +01:00
Jixun Wu e148d4a7c7 0.2.5
continuous-integration/drone/push Build is passing Details
2023-12-25 18:15:56 +01:00
Jixun Wu f9fa6575d1 fix: qmcv2 db name matching when `musicex` was not found 2023-12-25 18:15:51 +01:00
Jixun Wu bab523bcd1 0.2.4
continuous-integration/drone/push Build is passing Details
2023-12-24 12:16:08 +01:00
Jixun Wu cb35691f01 feat: support for qmcv2 musicex tail 2023-12-24 12:15:56 +01:00
Jixun Wu 5ecc9159b6 0.2.3
continuous-integration/drone/push Build is passing Details
2023-12-23 19:39:46 +00:00
Jixun Wu 77fff2fbf5 Merge branch 'docs/android-emu-root'
continuous-integration/drone/push Build encountered an error Details
2023-12-23 19:38:45 +00:00
jixunmoe 9866786ab2 Merge pull request '安卓 root 相关说明' (#63) from docs/android-emu-root into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#63
2023-12-23 16:04:50 +00:00
Jixun Wu 8abf8015d0 build: cache webp images 2023-12-23 15:59:28 +00:00
Jixun Wu 3723190d42 docs: typo
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-12-23 15:54:22 +00:00
Jixun Wu 178009f581 docs: re-order faq sections
continuous-integration/drone/push Build encountered an error Details
2023-12-23 15:48:51 +00:00
Jixun Wu 2b303a4fab docs: update in-app-faq about broken android browsers 2023-12-23 15:47:35 +00:00
Jixun Wu fa0f719c4c docs: update list of issues with broken android browsers 2023-12-23 15:45:02 +00:00
Jixun Wu c9b43aa2c6 docs: format supported format, add qtfm, added warn about android browser 2023-12-23 15:44:45 +00:00
Jixun Wu 4f38238ed9 chore: bump node to v20.10.0 2023-12-23 15:41:13 +00:00
Jixun Wu 3797789be2 docs: document broken browsers
continuous-integration/drone/push Build is passing Details
2023-12-23 11:20:31 +00:00
Jixun Wu d761d00ed5 docs: update offline faq with android emu root notes #62
continuous-integration/drone/push Build encountered an error Details
2023-12-23 11:15:32 +00:00
Jixun Wu 842e41ec65 docs: update offline md faq with updated notes 2023-12-23 11:13:23 +00:00
Jixun Wu c5d3ea899d add notes about android emu for root #62
continuous-integration/drone/push Build is passing Details
2023-12-23 11:11:38 +00:00
Jixun Wu a5e398873c docs: update faq
continuous-integration/drone/push Build is passing Details
2023-12-22 11:02:56 +00:00
Jixun Wu f63b81dcb2 0.2.2
continuous-integration/drone/push Build is passing Details
2023-12-22 10:48:18 +00:00
Jixun Wu 6083dd681d ci: disable publish to gitea as it is still broken
continuous-integration/drone Build encountered an error Details
continuous-integration/drone/push Build is passing Details
2023-12-22 10:39:08 +00:00
jixunmoe e1db1b4618 Merge pull request '#58 蜻蜓FM 安卓端支援' (#59) from feat/qingting-fm into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: um/um-react#59
2023-12-22 10:35:17 +00:00
Jixun Wu 17adc5ccd9 ci: enable publish to gitea
continuous-integration/drone/push Build encountered an error Details
continuous-integration/drone/pr Build encountered an error Details
2023-12-22 10:34:47 +00:00
Jixun Wu 0093acc854 Merge remote-tracking branch 'origin/main' into feat/qingting-fm 2023-12-22 10:34:12 +00:00
Jixun Wu 92a5eb7179 feat: qtfm instructions & notes
continuous-integration/drone/push Build is passing Details
2023-12-22 10:33:12 +00:00
Jixun Wu 2db1b7fb7f fix: mono font styling 2023-12-22 10:32:26 +00:00
Jixun Wu 0280bbbdfd chore: typo
continuous-integration/drone/push Build is passing Details
2023-12-21 22:57:59 +00:00
Jixun Wu 5771892523 build: remove workaround for test
continuous-integration/drone/push Build is passing Details
2023-12-21 22:51:44 +00:00
Jixun Wu 0b8d9ec30c build: fix build issue with vite/vite-pwa/parakeet; upgrade deps
continuous-integration/drone/push Build encountered an error Details
2023-12-21 22:49:12 +00:00
Jixun Wu ae00a59c92 feat: add basic instruction to paste device key from `qtfm-device-id`
continuous-integration/drone/push Build is failing Details
2023-12-21 19:34:37 +00:00
Jixun Wu b85331dd48 ci: don't publish to gitea as it is broken
continuous-integration/drone/push Build is passing Details
2023-11-29 23:53:46 +00:00
Jixun Wu e476f6218f Merge remote-tracking branch 'origin/main' into feat/qingting-fm
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-11-29 23:49:59 +00:00
jixunmoe 85a5fc3e7e Merge pull request '酷我 iOS 数据库支持' (#60) from feat/kwm-ios-support into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: um/um-react#60
2023-11-29 23:49:43 +00:00
Jixun Wu 56d37c232a feat: initial implementation of qtfm android
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-29 23:45:56 +00:00
Jixun Wu 6365d7a034 fix: fix bad refactor
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-08 20:49:13 +00:00
Jixun Wu 85fb68cd66 feat: add support for kuwo ios ekey db
continuous-integration/drone/push Build is failing Details
2023-11-08 20:40:41 +00:00
Jixun Wu 75cc18477c docs: readme for supported version
continuous-integration/drone/push Build is passing Details
2023-11-03 00:07:49 +00:00
Jixun Wu 4ff5809188 Merge commit '59c4678d1a79b0facc1550ac81134b9cb5263fc0'
continuous-integration/drone/push Build is passing Details
2023-11-02 23:58:33 +00:00
Jixun Wu cacb9f0e76 docs: update faq to include valid version of qmpc #52 2023-11-02 23:57:54 +00:00
Jixun Wu 59c4678d1a feat: experimental support for douban key import
continuous-integration/drone/push Build is passing Details
2023-10-19 02:58:50 +01:00
Jixun Wu 57c0a247c0 test: fix type for test
continuous-integration/drone/push Build is passing Details
2023-10-19 02:39:30 +01:00
Jixun Wu c5910c088c chore: text update for faq 2023-10-19 02:16:21 +01:00
Jixun Wu 3f09d197af chore: readme update 2023-10-19 02:16:12 +01:00
Jixun Wu fbb2257e28 chore: prefer node v20.8.1 over v18 2023-10-19 02:05:23 +01:00
Jixun Wu c5c0ca450b chore: update readme with link to old project
continuous-integration/drone/push Build is passing Details
2023-10-11 23:12:00 +01:00
Jixun Wu dc7b7429e4 chore: fix pwa build
continuous-integration/drone/push Build is passing Details
2023-10-11 23:08:27 +01:00
Jixun Wu 7b3e81dd2a chore: pwa update prompt
continuous-integration/drone/push Build is failing Details
2023-10-11 23:06:09 +01:00
Jixun Wu 558fa12dd8 0.2.1
continuous-integration/drone/push Build is passing Details
2023-10-11 22:58:33 +01:00
Jixun Wu 1ce19a653c fix: #50 trim tag content on decryption with custom key.
continuous-integration/drone/push Build is passing Details
2023-10-11 22:58:15 +01:00
Jixun Wu 1c7ab83ab9 fix: #50 audio ext `mp4` -> `m4a` 2023-10-11 22:41:33 +01:00
Jixun Wu 2fb345a177 fix: #51 mmkv parsing
continuous-integration/drone/push Build is passing Details
2023-10-11 21:35:21 +01:00
Jixun Wu d3385c1902 build: fix build issue with terser 2023-10-11 19:54:18 +01:00
Jixun Wu cfbd631815 chore: update deps 2023-10-11 19:41:22 +01:00
Jixun Wu 5d731d066f fix: typo in linux adb dump command
continuous-integration/drone/push Build is failing Details
2023-09-16 16:12:30 +01:00
Jixun Wu af6bd35755 chore: merge vitest config to vite config 2023-09-16 16:10:36 +01:00
Jixun Wu 39ceff9cf7 chore: update dependencies
continuous-integration/drone/push Build is failing Details
2023-09-16 15:42:57 +01:00
Jixun Wu f17c2f195c chore: don't fail the build when building from source tarball (#40) 2023-09-16 15:11:11 +01:00
Jixun Wu 0bd52157c4 fix: link to project issue color
continuous-integration/drone/push Build is passing Details
2023-09-05 02:20:02 +01:00
Jixun Wu ab6f9e155d docs: minor layout fix
continuous-integration/drone/push Build is passing Details
2023-09-05 02:14:33 +01:00
jixunmoe 4d6bb68530 Merge pull request 'FAQ 相关' (#46) from feat/faq-to-docs into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#46
2023-09-05 01:07:14 +00:00
jixunmoe ce9ad1bec8 Merge pull request '添加了简体中文的一个faq(位于docs里)' (#44) from yinluan/um-react:main into main
continuous-integration/drone/push Build was killed Details
Reviewed-on: um/um-react#44
2023-09-05 01:06:58 +00:00
Jixun Wu 0405d268b2 chore: fix faq layout
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone Build is passing Details
2023-09-05 01:56:34 +01:00
Jixun Wu ff5358b8f9 docs: add reformatted faq doc 2023-09-05 01:54:50 +01:00
Jixun Wu b120f740d8 feat: integrate FAQ to webapp 2023-09-05 01:34:42 +01:00
yinluan b6f3a761fc 上传文件至 docs
faq was edited.
2023-09-04 12:18:58 +00:00
yinluan c5c8bbde6a 上传文件至 docs
Upload faq(zh-hans oly) to docs
2023-09-04 12:13:58 +00:00
Jixun Wu a5cc2e1230 chore: bump libparakeet-js to v0.2.1
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-07-16 16:21:06 +01:00
Jixun Wu 35fde207f6 fix: pwsh script to dump db to current dir 2023-07-16 16:20:44 +01:00
jixunmoe 8f09882470 Merge pull request 'ux/qmcv2-keys' (#39) from ux/qmcv2-keys into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#39
2023-07-02 14:55:41 +00:00
Jixun Wu 45c484c5b1 ci: use bookworm base image
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-07-02 15:43:35 +01:00
Jixun Wu c434268ef7 fix: add tooltip to hint user to check when unsure (#38)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-07-02 15:40:35 +01:00
Jixun Wu b3d99289d1 fix: consistent wording for qmc setting tab title (#38) 2023-07-02 15:40:13 +01:00
Jixun Wu 60547f0a0a fix: warn when 0 keys imported (#38) 2023-07-02 15:39:47 +01:00
Jixun Wu f089f92930 fix: settings dirty state tracking + toast on save/discard (#38) 2023-07-02 15:38:52 +01:00
Jixun Wu a5e3a0f52e fix: consistent wording on "secret import" and add 3-dots in menu (#38) 2023-07-02 15:36:59 +01:00
Jixun Wu 71d5b656e2 fix: update key placeholders (#38) 2023-07-02 15:35:54 +01:00
Jixun Wu da29b389ea fix: typo 2023-07-02 15:35:01 +01:00
Jixun Wu 9db5d94a14 fix: error handling when there are no results 2023-07-02 15:32:47 +01:00
Jixun Wu e904aa4397 docs: typo in readme
continuous-integration/drone/push Build is passing Details
2023-06-20 19:53:49 +01:00
Jixun Wu d070332f98 fix: support files that were not encrypted (e.g. `kgma`), close #36
continuous-integration/drone/push Build is passing Details
2023-06-18 18:00:37 +01:00
jixunmoe ed488ea204 Merge pull request 'KWMv2/酷我 mflac 支持' (#35) from feat/kwm-v2-support into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#35
2023-06-17 14:13:43 +00:00
Jixun Wu c058d2f111 0.2.0
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-06-17 14:57:04 +01:00
Jixun Wu 57979ccbb3 feat: update instructions for kwmv2 key import 2023-06-17 14:55:38 +01:00
Jixun Wu 1a8e7db9be feat: add KWMv2 support
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-17 14:29:50 +01:00
Jixun Wu c7846ec15b refactor: move QMCv2KeyCrypto constructor to shared utility method 2023-06-17 14:29:24 +01:00
Jixun Wu fe256218e6 refactor: move text encoder/decoder to util file 2023-06-17 14:28:36 +01:00
Jixun Wu 65b2474b43 chore: upgrade libparakeet to v0.2.0 2023-06-17 14:25:59 +01:00
Jixun Wu c25dffa778 feat: parse kwm v2 key from mmkv
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-17 03:18:15 +01:00
Jixun Wu c286b6c29c Revert "ci: reduce ram usage when building"
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
This reverts commit 7da9699ebb.
2023-06-17 03:00:06 +01:00
Jixun Wu 71bd550434 build: remove migrated component
continuous-integration/drone/push Build is failing Details
2023-06-17 02:50:04 +01:00
Jixun Wu 7da9699ebb ci: reduce ram usage when building 2023-06-17 02:49:49 +01:00
Jixun Wu 263f4c2b6a feat: kwm v2 key import ui
continuous-integration/drone/push Build is failing Details
2023-06-17 02:45:31 +01:00
jixunmoe cef74e58c1 Merge pull request '添加 Levenshtein 算法(QMCv2)' (#33) from feat/levenshtein-key-finder into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#33
2023-06-16 19:45:47 +00:00
Jixun Wu c58a40a1a6 chore: add dev doc content
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-16 20:44:08 +01:00
Jixun Wu b85c464e24 docs: improve ios docs 2023-06-16 20:43:49 +01:00
Jixun Wu 67b400430f feat: added option to search closest ekey
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-06-16 02:28:38 +01:00
Jixun Wu b8b2059558 fix: performance issue when there's over 100s ekeys 2023-06-16 02:03:23 +01:00
jixunmoe 8cb275da75 Merge pull request '支持 iOS 密钥数据库导入' (#32) from feat/import-ios-keys into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#32
2023-06-15 23:19:01 +00:00
Jixun Wu 244a1b002d chore: update wording for qmcv2 description & qmcv2 key import button text
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2023-06-15 20:52:59 +01:00
Jixun Wu 320c80d8cd chore: update wording for discard settings 2023-06-15 20:52:41 +01:00
Jixun Wu 63c1a25f53 feat: add ios ekey import instructions
continuous-integration/drone/push Build is passing Details
2023-06-15 20:34:15 +01:00
Jixun Wu 59ecc0847b feat: fix mmkv parser, support for ios ekey mmkv 2023-06-15 19:30:33 +01:00
jixunmoe cc885164c1 Merge pull request 'feat: add mac import option and help text' (#30) from feat/import-qmc2-mmkv into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#30
2023-06-12 23:56:34 +00:00
Jixun Wu 6b871f0861 feat: add mac import option and help text
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-13 00:26:30 +01:00
Jixun Wu a951ac8e16 feat: default link to use blue color
continuous-integration/drone/push Build is passing Details
2023-06-12 22:55:09 +01:00
jixunmoe 86e8f33de5 Merge pull request '从安卓客户端的密钥数据库读取密钥' (#24) from feat/import-from-android into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#24
2023-06-11 22:39:19 +00:00
Jixun Wu 3cb24b52ac fix: make modal don't close on overlay click; remove dismiss button in this modal.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-06-11 23:25:49 +01:00
Jixun Wu 2a75d63b9a fix: reworded the instructions against rooting Android Device
continuous-integration/drone/push Build is passing Details
2023-06-11 23:23:40 +01:00
Jixun Wu 0b82d267c9 chore: trim whitespace when converting staging key to production
continuous-integration/drone/push Build is passing Details
2023-06-11 23:19:13 +01:00
Jixun Wu e956fb60ad fix: added workaround for sql.js during vitest
continuous-integration/drone/push Build is passing Details
2023-06-11 22:42:19 +01:00
Jixun Wu dec7f86115 feat: proper instructions for ps1/win as well. 2023-06-11 22:41:06 +01:00
Jixun Wu eb87c0f2e0 chore: manually adjust chunks 2023-06-11 22:40:39 +01:00
Jixun Wu da39d5f5c1 feat: import ekey from Android db (#20)
continuous-integration/drone/push Build is passing Details
2023-06-11 16:22:57 +01:00
Jixun Wu dd7a238eb8 ci: automatically promote latest main build as published.
continuous-integration/drone/push Build is passing Details
2023-06-10 21:59:08 +01:00
Jixun Wu 1976c34e7a ci: fix existing deployment 2023-06-10 21:49:00 +01:00
Jixun Wu 06a99271f1 ci: typo for zip archive creation
continuous-integration/drone/push Build is failing Details
2023-06-10 21:37:15 +01:00
Jixun Wu bd85d0c37b ci: avoid installing debian dependencies
continuous-integration/drone/push Build is failing Details
2023-06-10 21:32:41 +01:00
Jixun Wu 9c5a5d206c ci: use deb mirror from ustc
continuous-integration/drone/push Build is passing Details
2023-06-10 18:24:39 +01:00
Jixun Wu 613b1f9db1 ci: use deb mirror from netease
continuous-integration/drone/push Build is passing Details
2023-06-10 18:05:50 +01:00
Jixun Wu ee10bab28f ci: use TaoBao npm mirror (#23)
continuous-integration/drone/push Build is passing Details
2023-06-10 18:00:31 +01:00
Jixun Wu ce01fca211 fix: reword unlock instructions to fit in single line for mobile devices
continuous-integration/drone/push Build is passing Details
2023-06-10 17:55:13 +01:00
Jixun Wu dd48ae7778 fix: reword footer to fit single line in mobile device (close #22) 2023-06-10 17:55:13 +01:00
Jixun Wu 6c40f29202 Revert "ci: make use of pnpm store cache (#23)"
continuous-integration/drone/push Build is passing Details
This reverts commit ba01937703.
2023-06-10 17:31:12 +01:00
Jixun Wu ba01937703 ci: make use of pnpm store cache (#23)
continuous-integration/drone/push Build is passing Details
2023-06-10 17:22:28 +01:00
Jixun Wu aae3e5c0fb ci: deploy to main site when branch is main 2023-06-10 17:08:48 +01:00
Jixun Wu 8fe289e270 Revert "refactor: use yarn instead of pnpm (#23)"
continuous-integration/drone/push Build is passing Details
This reverts commit 191de2094d.
2023-06-10 17:02:36 +01:00
Jixun Wu 191de2094d refactor: use yarn instead of pnpm (#23)
continuous-integration/drone/push Build was killed Details
2023-06-10 16:39:51 +01:00
jixunmoe 0caf88d649 Merge pull request '添加设定界面 - #18' (#21) from feat/settings into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: um/um-react#21
2023-06-10 15:23:20 +00:00
Jixun Wu b3ef596a93 refactor: extend `transformBlob` to take cleanup function
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-06-10 16:22:36 +01:00
Jixun Wu 6d925f744e ci: change wording for save/discard 2023-06-10 16:16:41 +01:00
Jixun Wu 9db037a71d chore: add description to QMCv2 page 2023-06-10 16:15:46 +01:00
Jixun Wu 2678825f8e ci: don't publish when we are inside PR 2023-06-10 16:11:26 +01:00
Jixun Wu 365290905c feat: implement decrypt from user key
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-10 14:29:50 +01:00
Jixun Wu 6a2b9e5cdc chore: added note about import options 2023-06-10 14:09:40 +01:00
Jixun Wu 8e2d13f54a feat: split settings slice to staging (ui) and production (in effect)
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-10 13:10:21 +01:00
Jixun Wu 4de0d5304d feat: pass options to downstream decryptor 2023-06-10 12:06:02 +01:00
Jixun Wu f169a036d0 chore: bump pnpm lock file version 2023-06-10 12:05:08 +01:00
Jixun Wu fc743fa15d fix: make `@chakra-ui/anatomy` direct dependency
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-10 01:00:00 +01:00
Jixun Wu 470adc74db test: mock `matchMedia`
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
2023-06-10 00:55:10 +01:00
Jixun Wu 740c5196aa chore: avoid warning during build "empty chunk: dummy" 2023-06-10 00:51:24 +01:00
Jixun Wu e815365fc0 chore: don't reformat lockfile 2023-06-10 00:46:26 +01:00
Jixun Wu 725f130e42 feat: responsive settings ui 2023-06-10 00:45:41 +01:00
Jixun Wu 1f87a655ac feat: improved layout 2023-06-09 23:11:30 +01:00
Jixun Wu fc847eaf58 Merge remote-tracking branch 'origin/main' into feat/settings
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-09 01:22:13 +01:00
Jixun Wu 421fb7c239 docs: add ci badge & update artifact url
continuous-integration/drone/push Build is passing Details
2023-06-09 01:20:30 +01:00
Jixun Wu 8d7f16b231 ci: main branch should be considered as prod deployment
continuous-integration/drone/push Build is passing Details
2023-06-09 01:17:07 +01:00
Jixun Wu b66eea4069 ci: hide verbose logging from publish/deploy
continuous-integration/drone/push Build is passing Details
2023-06-09 01:10:37 +01:00
Jixun Wu c579fb1928 ci: get started on ci build
continuous-integration/drone Build is passing Details
2023-06-09 01:01:41 +01:00
Jixun Wu a9e5c16949 fix: build issue with libparakeet 2023-06-09 01:01:28 +01:00
Jixun Wu 6cee2bbfd9 test: ignore coverage to test nextTickFn 2023-06-05 00:04:59 +01:00
Jixun Wu 009aabd2dd test: added test for enumObject. 2023-06-05 00:04:33 +01:00
Jixun Wu 429580006c refactor: use tab instead of modal. fixed layout as well. 2023-06-04 23:56:15 +01:00
Jixun Wu bb74c6e2b9 feat: added dummy settings modal 2023-06-03 14:58:17 +01:00
Jixun Wu 3a2a31f372 test: fix sanity check test 2023-06-03 14:15:53 +01:00
Jixun Wu d91b71e0d3 chore: fix typo 2023-06-03 14:15:21 +01:00
Jixun Wu 4def7a260e refactor: move components to sub dir 2023-06-03 14:14:50 +01:00
Jixun Wu 4602f96260 feat: setup redux store for settings 2023-06-03 14:09:11 +01:00
Jixun Wu c4dbaa4b73 chore: upgrade all dependencies to its latest. 2023-05-27 23:24:53 +01:00
Jixun Wu 4b4d22f00c chore: upgrade sdk to v0.1.1 2023-05-27 23:22:48 +01:00
Jixun Wu 8dd1439723 chore: remove gitkeep placeholder from public folder 2023-05-27 01:08:47 +01:00
Jixun Wu 05e4fca0de docs: update supported formats, reformatted TODO list 2023-05-27 00:48:07 +01:00
Jixun Wu e1929d7639 feat: add support for migu3d formats 2023-05-27 00:47:00 +01:00
Jixun Wu e9c4fffcdd docs: correctly apply strikethrough in readme 2023-05-23 00:52:24 +01:00
Jixun Wu 312b804a4e docs: update footer url to pnpm corepack with chapter text 2023-05-23 00:51:05 +01:00
Jixun Wu f1b01456c2 docs: move large chunk of text to its own file 2023-05-23 00:49:24 +01:00
Jixun Wu 813acfab2d docs: notes for submit issues 2023-05-23 00:32:41 +01:00
Jixun Wu 1a0f53e715 docs: restructured readme with supported formats and project description 2023-05-23 00:23:06 +01:00
Jixun Wu 42d7d47115 refactor: avoid bundling parakeet both in worker and main js 2023-05-23 00:04:06 +01:00
Jixun Wu cca9b38a1f feat: add crypto impl for kgm/kwm (#16, #17) 2023-05-22 23:56:17 +01:00
Jixun Wu a35a271cea chore: upgrade sdk 2023-05-22 23:55:47 +01:00
Jixun Wu b198240ef9 feat: added support for ncm (#15) 2023-05-22 23:07:58 +01:00
Jixun Wu 2d06fec684 chore: upgrade even more packages 2023-05-22 22:45:16 +01:00
Jixun Wu f2cd78ef0c chore: upgrade deps 2023-05-22 22:44:01 +01:00
Jixun Wu 83feba5713 chore: update sdk to build exp.18 2023-05-22 22:38:59 +01:00
Jixun Wu ce2c9835b9 chore: make error summary red (#12) 2023-05-22 22:28:51 +01:00
Jixun Wu 5db5fdaa69 feat: friendly way to inform user there's an error (#12) 2023-05-22 22:24:41 +01:00
Jixun Wu af61d23fd4 feat: get pwa working 2023-05-22 00:29:27 +01:00
Jixun Wu c6c373f9fc refactor: make console log less verbose when not needed 2023-05-22 00:00:35 +01:00
Jixun Wu c3809b48f7 faet: add support of xmly android (#1) 2023-05-21 23:38:50 +01:00
Jixun Wu fa2629ae6c refactor: improve flow of decryption (#2) 2023-05-21 23:38:32 +01:00
Jixun Wu e6bc60b9af chore: bump sdk version 2023-05-21 23:37:30 +01:00
Jixun Wu 0b2000ebe2 feat: move metadata display component out from FileRow 2023-05-21 18:19:41 +01:00
Jixun Wu 3f458e9f46 feat: rounded album cover image 2023-05-21 18:16:38 +01:00
Jixun Wu 8c9723cb0f test: fix test warning regarding async suspension rendering 2023-05-21 18:08:42 +01:00
Jixun Wu 06acbab4d9 feat: print performance logs to console. 2023-05-21 18:00:06 +01:00
Jixun Wu 6d3b14664a feat: add basic file drag & drop support (#6) 2023-05-21 16:36:26 +01:00
Jixun Wu 3f225b46dc chore: declare html as zh 2023-05-21 16:34:51 +01:00
Jixun Wu 582cd4552d feat: add um logo to project 2023-05-21 15:40:26 +01:00
Jixun Wu d8972219b0 chore: remove item based on animation exit event 2023-05-21 15:38:52 +01:00
Jixun Wu 48b7a559ca chore: wrap actions to a single condition 2023-05-21 15:38:31 +01:00
Jixun Wu 65e1e0caa2 chore: move file row's grid layout to its own file 2023-05-21 15:38:07 +01:00
Jixun Wu 940a61daab fix: restore missing card border 2023-05-21 15:08:23 +01:00
Jixun Wu 9630e56a96 chore: be flexible about the path to libparakeet sdk 2023-05-21 14:34:27 +01:00
Jixun Wu 7083e52eb7 chore: added vscode extension recommendation 2023-05-21 14:32:18 +01:00
Jixun Wu d12ef12ea4 Merge changes from @houkunlin 2023-05-21 14:28:19 +01:00
Jixun Wu c0ea13a2be feat: re-implement current year logic by using a component 2023-05-21 14:24:06 +01:00
Jixun Wu 878d31fd90 Revert "feat: 修改底部版权区域,使用 Wrap 组件来实现文字间的间隔,把结束年份动态获取当前年份"
This reverts commit 29bce26acb.
2023-05-21 14:19:23 +01:00
Jixun Wu 6dd5f04131 chore: address code review re file row 2023-05-21 14:17:55 +01:00
Jixun Wu 837e7655b8 Merge remote-tracking branch 'houkunlin/feat/file-row' into merge-houkunlin 2023-05-21 14:16:56 +01:00
Jixun Wu 6da0588e11 Merge remote-tracking branch 'houkunlin/feat/footer' into merge-houkunlin 2023-05-21 14:16:49 +01:00
Jixun Wu 2deb27f0c5 chore: remove unused src config file 2023-05-20 02:19:45 +01:00
Jixun Wu bababc1aa8 docs: update progress in readme 2023-05-18 22:13:48 +01:00
Jixun Wu 55e258f4ef chore: use url format for request id 2023-05-18 22:12:45 +01:00
Jixun Wu d00ba50fcd test: added test for DecryptionQueue (#8) 2023-05-18 22:12:34 +01:00
Jixun Wu ab554b0470 fix: replace `setImmediate` with a cross environment compatible version 2023-05-18 22:00:43 +01:00
Jixun Wu 604eb6f939 chore: add lint-staged & husky 2023-05-18 00:39:50 +01:00
Jixun Wu 3b26584f4d test: proper sanity check; exclude WasmTest from coverage (#8) 2023-05-18 00:30:39 +01:00
Jixun Wu 4e9e360751 refactor: simplify logic handling worker message 2023-05-18 00:21:11 +01:00
Jixun Wu fb14eabaf1 chore: remove abuse of any in WorkerEventBus 2023-05-18 00:18:22 +01:00
Jixun Wu 8597fe0d97 test: added test for ConcurrentQueue 2023-05-18 00:04:53 +01:00
Jixun Wu 6e6a8b1f5c fix: type safe ConcurrentQueue 2023-05-18 00:04:37 +01:00
Jixun Wu be76a4590f chore: make eslint happy 2023-05-18 00:04:20 +01:00
Jixun Wu 6299302ff9 chore: configure eslint rules 2023-05-18 00:03:55 +01:00
Jixun Wu ba5901d1eb chore: fix types for build & test 2023-05-18 00:03:30 +01:00
Jixun Wu f3ff6198e4 test: remove unused/unneeded jest dependencies 2023-05-17 22:59:13 +01:00
HouKunLin 76cb2a154b feat: 即使不存在歌曲元数据,也展示默认的“暂无封面”占位图片 2023-05-17 09:39:33 +08:00
HouKunLin 33a6abab7f feat: togglePlay 方法取消使用 useCallback 2023-05-17 09:27:16 +08:00
HouKunLin 4b67542799 Merge remote-tracking branch 'origin/main' into feat/file-row
# Conflicts:
#	src/features/file-listing/FileRow.tsx
2023-05-17 09:18:41 +08:00
HouKunLin eaf05d8cbb feat: 删除歌曲行数据时改用 onClose 来处理 2023-05-17 09:10:36 +08:00
Jixun Wu cd1f6cb186 test: working test with TypeScript + vite (using vitest) 2023-05-17 01:46:20 +01:00
Jixun Wu 1069c4c6bb chore: apply indentation rule for cjs/mjs as well 2023-05-16 22:50:03 +01:00
Jixun Wu 71014c3a90 test: get coverage work 2023-05-16 22:47:47 +01:00
HouKunLin b8c3a51756 feat: 歌曲封面使用 Image 组件,删除歌曲行数据时使用一个动画效果来过渡一下 2023-05-16 16:24:00 +08:00
HouKunLin 29bce26acb feat: 修改底部版权区域,使用 Wrap 组件来实现文字间的间隔,把结束年份动态获取当前年份 2023-05-16 16:21:42 +08:00
181 changed files with 9361 additions and 3293 deletions

35
.drone.yml Normal file
View File

@ -0,0 +1,35 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: test & build
image: node:20.10.0-bookworm
commands:
# - git config --global --add safe.directory "/drone/src"
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm i --frozen-lockfile
- pnpm build
environment:
# 让 npm 使用淘宝源
npm_config_registry: https://registry.npmmirror.com
- name: publish
image: node:20.10.0-bookworm
environment:
DRONE_GITEA_SERVER: https://git.unlock-music.dev
GITEA_API_KEY:
from_secret: GITEA_API_KEY
NETLIFY_SITE_ID:
from_secret: NETLIFY_SITE_ID
NETLIFY_API_KEY:
from_secret: NETLIFY_API_KEY
commands:
- |
python3 -m zipfile -c um-react.zip dist/.
cp um-react.zip dist/release-"${DRONE_COMMIT_SHA}".zip
python3 -m zipfile -c um-react-site.zip dist/.
# - ./scripts/publish.sh
- ./scripts/deploy.sh

View File

@ -11,5 +11,5 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js{x,on,},ts{x,}}]
[*.{{c,m,}js{x,on,},ts{x,}}]
indent_size = 2

4
.env Normal file
View File

@ -0,0 +1,4 @@
# Example environment file for vite to use.
# For more information, see: https://vitejs.dev/guide/env-and-mode.html
ENABLE_PERF_LOG=0

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
dist/
node_modules/
coverage/

View File

@ -14,5 +14,14 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
},
};

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.mmkv binary

10
.gitignore vendored
View File

@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
coverage/
dist
dist-ssr
*.local
@ -22,3 +23,12 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Files created when running "drone exec" locally
/.pnpm-store/
/*.zip
/um-react-wry-*
/um-react*.exe
/win64/

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged

4
.husky/pre-push Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm test

4
.npmrc
View File

@ -1,3 +1,3 @@
use-node-version=18.16.0
node-version=18.16.0
use-node-version=20.10.0
node-version=20.10.0
engine-strict=true

View File

@ -1,2 +1,5 @@
dist/
# Package manager
yarn.lock
pnpm-lock.yaml

31
.swcrc
View File

@ -1,31 +0,0 @@
{
"jsc": {
"target": "es2020",
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": false
},
"transform": {
"react": {
"pragma": "React.createElement",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": false,
"runtime": "automatic"
},
"hidden": {
"jest": true
}
}
},
"module": {
"type": "commonjs",
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
}
}

11
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"recommendations": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"christian-kohler.path-intellisense",
"txava.region-marker",
"foxundermoon.shell-format",
"jock.svg"
]
}

126
README.MD
View File

@ -1,22 +1,69 @@
# Getting started
# Unlock Music 音乐解锁 (React)
前提: [安装 pnpm][install-pnpm],推荐 `corepack` 方法。
[![Build Status](https://ci.unlock-music.dev/api/badges/um/um-react/status.svg)](https://ci.unlock-music.dev/um/um-react)
```sh
pnpm i
pnpm start
```
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- 查看[原基于 Vue 的 Unlock Music 项目][um-vue]
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。
- 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入!
- CI 自动构建已经部署,可以在 [Packages][um-react-packages] 下载。
- [常见问题参考](./docs/faq_zh-hans.md)
[install-pnpm]: https://pnpm.io/zh/installation
[授权协议]: https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE
[um-vue]: https://git.unlock-music.dev/um/web
[unlock-music/cli]: https://git.unlock-music.dev/um/cli
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
[um-react-packages]: https://git.unlock-music.dev/um/-/packages/generic/um-react/
## 架构
⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
## 支持的格式
- [x] QQ 音乐 QMCv1 (`.qmc3` / `.qmcflac` 等)
- [x] QQ 音乐 QMCv2
- PC 客户端 (`.mflac` / `.mgg` 等) [^qm-key-pc]
- 安卓客户端 (`.mflac0` / `.mgg1` / `.mggl` 等) [^qm-key-android]
- iOS 客户端 (`.mgalaxy` 等) [^qm-key-ios]
- Mac 客户端 (`.mflach` 等) [^qm-key-mac]
- [x] 网易云音乐 (`.ncm`)
- [x] 虾米音乐 (`.xm`)
- [x] 酷我音乐 (`.kwm`)
- [x] 酷狗音乐 (`.kgm` / `.vpr`)
- [x] 喜马拉雅 Android 端 (`.x2m` / `.x3m`)
- [x] 咪咕音乐格式 (`.mg3d`)
- [x] 蜻蜓 FM (`.qta`)
- [ ] ~~<ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (`.ofl_en`)~~
[^qm-key-pc]: PC 客户端仅支持 v19.43 或更低版本。
[^qm-key-android]: 需要获取超级管理员权限后提取密钥数据库,并导入后使用。
[^qm-key-ios]: 需要越狱获取密钥数据库,或对设备进行完整备份后提取密钥数据库,并导入后使用。
[^qm-key-mac]: 需要导入密钥数据库。
不支持的格式?请提交样本(加密文件)与客户端信息(或一并上传其安装包)到[仓库的问题追踪区][project-issues]。如果文件太大,请上传到不需要登入下载的网盘,如 [mega.nz](https://mega.nz)、[OneDrive](https://www.onedrive.com/) 等。
如果遇到解密出错的情况,请一并携带错误信息并简单描述错误的重现过程。
[project-issues]: https://git.unlock-music.dev/um/um-react/issues/new
## 开发相关
从源码运行或编译生产版本,请参考文档「[新手上路](./docs/getting-started.zh.md)」。
### 面向 libparakeet SDK 开发
⚠️ 如果只是进行前端方面的更改,你可以跳过该节。
请参考文档「[面向 `libparakeet-js` 开发](./docs/develop-with-libparakeet.zh.md)」。
### 架构
- 浏览器主线程: 渲染界面,处理 UI 更新
- Web Worker: 负责计算方面的内容,如内容解密。
数据传输: 生成 blob url (`URL.createObjectURL`) 然后透过 `postMessage` 传递给线程,线程利用 `fetch` API 来获取文件信息。
## 贡献代码
### 贡献代码
欢迎贡献代码。请确保:
@ -26,54 +73,25 @@ pnpm start
满足上述条件后发起 Pull Request仓库管理员审阅后将合并到主分支。
## 基于 libparakeet SDK 开发
## 相关项目
`libparakeet-js` 编译目前需要 Linux 环境,请参考[仓库说明][libparakeet-js-doc]。
- [Unlock Music (Web)](https://git.unlock-music.dev/um/web) - 原始项目
- [Unlock Music (Cli)](https://git.unlock-music.dev/um/cli) - 命令行批量处理版
- [um-react (Electron 前端)](https://github.com/CarlGao4/um-react-electron) - 使用 Electron 框架封装的本地可执行文件。
- [GitHub 下载](https://github.com/CarlGao4/um-react-electron/releases/latest) | [仓库镜像](https://git.unlock-music.dev/CarlGao4/um-react-electron)
- [um-react-wry](https://git.unlock-music.dev/um/um-react-wry) - 使用 WRY 框架封装的 Win64 单文件 (需要[安装 Edge WebView2 运行时][webview2_redist]Win10+ 操作系统自带)
- [本地下载](https://git.unlock-music.dev/um/um-react/releases/latest) | 寻找文件名为 `um-react-win64-` 开头的附件
该文档将假设这两个项目被放置在同级的目录下:
[webview2_redist]: https://go.microsoft.com/fwlink/p/?LinkId=2124703
```text
~/Projects/um-projects
/um-react
/libparakeet-js
```
若为不同目录,你需要调整 `vite.config.ts` 的 `server.fs.allow` 区段并加入新的路径。
[libparakeet-js-doc]: https://github.com/parakeet-rs/libparakeet-js/blob/main/README.MD
### 初次构建
- 进入上层目录 `cd ..`
- 克隆 `libparakeet-js` 仓库 (目前需要 Linux 环境, Windows 下推荐使用 WSL2)
- `git clone --recurse-submodules https://github.com/parakeet-rs/libparakeet-js.git`
- 进入目录 `cd libparakeet-js`
- 如果需要更新 `submodule`:
- `git submodule update --init --recursive`
- 运行 `./build.sh -j 4` 进行编译
- 编译 `js-sdk`:
- 进入 `npm` 目录: `cd npm`
- 安装依赖: `pnpm i --frozen-lockfile`
- 构建: `pnpm build`
### 做出更改
做出更改后,参考上面的内容进行重新编译
### 应用 SDK 更改
将构建好的 SDK 直接嵌入到当前前端项目:
```sh
pnpm link ../libparakeet-js/npm
```
※ 建立 PR 时,请先提交 SDK PR 并确保你的更改已合并。
有新的项目提交?欢迎[提交 issue][project-issues],请带上项目名称和链接。
## TODO
- [ ] #6 文件拖放 (利用 `react-dropzone`?)
- [ ] 各类算法 [追踪 `crypto` 标签](https://git.unlock-music.dev/um/um-react/issues?labels=67)
- [ ] #7 简易元数据编辑器
- [ ] #8 添加单元测试
- [ ] #2 解密内容探测 (解密过程)
- 待定
- [ ] 各类算法 [追踪 `crypto` 标签](https://git.unlock-music.dev/um/um-react/issues?labels=67)
- 完成
- [x] #7 ~~简易元数据编辑器~~ 放弃
- [x] #8 ~~添加单元测试~~ 框架加上了,以后慢慢添加更多测试即可。
- [x] #2 解密内容探测 (解密过程)
- [x] #6 文件拖放 (利用 `react-dropzone`?)

9
docs/adb_dump.md Normal file
View File

@ -0,0 +1,9 @@
# 利用 ADB 访问安卓私有数据
```sh
APP_ID="com.tencent.qqmusic" # QQ 音乐
APP_ID="cn.kuwo.player" # 酷我
adb shell su -c "tar c '/data/data/${APP_ID}/' | base64" \
| base64 -d | pv | tar -x --strip-components 2
```

BIN
docs/assets/faq_1_home.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,50 @@
# 面向 `libparakeet-js` 开发
⚠️ 如果只是进行前端方面的更改,你可以跳过该文档。
`libparakeet-js` 编译目前需要 Linux 环境,请参考[仓库说明][libparakeet-js-doc]。
该文档将假设这两个项目被放置在同级的目录下:
```text
~/Projects/um-projects
/um-react
/libparakeet-js
```
若为不同目录,你需要调整 `LIB_PARAKEET_JS_DIR` 环境变量到仓库目录,然后再启动 vite 项目。
[libparakeet-js-doc]: https://github.com/parakeet-rs/libparakeet-js/blob/main/README.MD
## 初次构建
- 进入上层目录:`cd ..`
- 克隆 `libparakeet-js` 仓库 (目前需要 Linux 环境, Windows 下推荐使用 WSL2)
- `git clone --recurse-submodules https://github.com/parakeet-rs/libparakeet-js.git`
- 进入 SDK 目录:`cd libparakeet-js`
- 如果需要更新 `submodule``git submodule update --init --recursive`
- 构建所有代码:`make all`
如果需要手动控制构建过程,你也可以:
- 运行 `./build.sh -j 4` 进行 C++ 到 WebAssembly 编译过程
- 此处的 `4` 是并行编译数量,该值通常略小于 CPU 核心数。
- 若是不指定并行数量,则使用当前核心数。
- 编译 `js-sdk`
- 进入 `npm` 目录:`cd npm`
- 安装依赖:`pnpm i --frozen-lockfile`
- 构建:`pnpm build`
## 做出更改
做出更改后,参考上面的内容进行重新编译。
## 应用 SDK 更改
将构建好的 SDK 直接嵌入到当前前端项目:
```sh
pnpm link ../libparakeet-js/npm
```
※ 建立 PR 时,请先提交 SDK PR 并确保你的 SDK 更改已合并。

105
docs/faq_zh-hans.md Normal file
View File

@ -0,0 +1,105 @@
# 常见问题解答
## QQ 音乐
### 解锁失败
#### 1、请检查您的文件。
尝试用下载音乐的设备播放一次看看,如果 QQ 音乐都没法播放,那解锁肯定会受到影响哦。
#### 2、检查您的平台。
日前,<mark>仅 Windows 客户端 v19.43 或以下版本</mark>下载的歌曲无需密钥,其余平台的官方正式版本均需要提取密钥。
> iOS 用户提取歌曲困难建议换用电脑操作Android 用户提取密钥需要 root也建议用电脑操作。
> 重复下载同一首的歌曲**不重复扣下载配额**,但是*同一首歌的两个版本会重复扣下载配额*,请仔细分辨。
提取密钥教程请访问[新版解锁网站](https://um-react.netlify.app/),前往网站内的设置 →“添加一条密钥”旁的<mark>**下拉按钮**</mark>→ 从文件导入密钥…→ 选择您对应的平台查看具体教程。
> 如果仍无法理解,可参考文末的图片操作
## 酷我音乐
### 解锁失败
酷我音乐的新版加密需要导入密钥。
#### 1、请检查您的文件。
尝试用下载音乐的设备播放一次看看,如果酷我音乐都没法播放,那解锁肯定会受到影响哦。
#### 2、检查您的平台。
日前,<mark>仅手机客户端</mark>下载的歌曲**至臻全景声**及**至臻母带**为新版加密手机平台的其他音质暂时不需要提取密钥PC 平台暂未推出使用新版加密的音质。
※ 已知部分第三方修改版会破坏密钥写出功能,导致无法导入密钥。请使用官方版本。
> Android 用户提取密钥需要 root或者注入文件提供器。
提取密钥教程请访问[新版解锁网站](https://um-react.netlify.app/),前往网站内的设置 →<mark>切换密钥为 KWMv2 密钥</mark>→“添加一条密钥”旁的<mark>**下拉按钮**</mark>→ 从文件导入密钥…→ 选择您对应的平台查看具体教程。
> 图片教程请参考 QQ 音乐(在文末),酷我音乐仅仅是需要切换一下密钥类型。
## 网易云音乐
### 解锁失败
您大概率正在使用 Windows 平台的网易云音乐 3.0 测试版。该版本对歌曲的信息新增了某些字段,导致旧版解锁识别错误。您可以找 1.10.5 版本的旧解锁网站,或者直接换[新版解锁网站](https://um-react.netlify.app/)。
> [旧解锁网站 Demo](https://demo.unlock-music.dev/)拥有者暂时联系不上,所以暂时无法更新。
## 其他问题
### 新版解锁网站解锁的歌曲没有封面
目前新版没有做歌曲信息匹配与编辑,所以歌曲如果自己没有写入歌曲信息,解出来就是没有的。
### 安卓 root 相关
对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。
例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付功能等限制。
如果希望不破坏系统完整性,你可以考虑使用模拟器。
**注意**:根据应用厂商的风控策略,使用模拟器登录的账号**有可能会被封锁**;使用前请自行评估风险。
目前常见的带有 root 特权支持的的安卓模拟器方案,分别是雷电模拟器(※ 官方版有内置广告)和微软在 Windows 11 开始支援的适用于 Android™ 的 Windows 子系统 (WSA)。
- WSA 可以参考 [MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal) 的说明操作。
- 雷电模拟器可以在「模拟器设置」 → 「其他设置」中启用 root 特权。
![雷电模拟器 其他设置](../src/faq/assets/ld_settings_misc.webp)
### Via 等浏览器无法正常解密/下载
⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
已知有问题的浏览器:
- Via 浏览器
- 夸克浏览器
- UC 浏览器
可能会遇到的问题包括:
- 网页白屏
- 无法下载解密后内容
- 下载的文件名错误
### 新版解锁网站没有批量下载
目前没有做。抱歉。
## 仍有问题?
欢迎进入[Telegram 交流群](https://t.me/unlock_music_chat),一起探讨。
> QQ 音乐导入密钥的图片教程
1. 选择【设定】
<br/>![选择【设定】](./assets/faq_1_home.webp)
2. 点击下拉菜单,选择【从文件导入密钥…】
<br/>![点击下拉菜单,选择【从文件导入密钥…】](./assets/faq_2_import.webp)
3. 选择对应的客户端并查阅说明
<br/>![选择对应的客户端并查阅说明](./assets/faq_3_instructions.webp)

View File

@ -0,0 +1,63 @@
# 新手上路
该文档描述了如何本地运行或编译生产版本的「Unlock Music 音乐解锁」。
## 安装依赖
- 安装 Node v16.17 或更高,推荐当前最新的 Node LTS 版本。
- 安装/激活 `pnpm` [^1]`corepack prepare pnpm@latest --activate`
- 安装软件依赖:`pnpm i --frozen-lockfile`
[^1]: 参考 pnpm 说明「[使用 Corepack 安装](https://pnpm.io/zh/installation#使用-corepack-安装)」。
## 本地运行
💡 你需要先完成「安装依赖」部分。
```sh
pnpm start
```
然后根据提示打开[项目运行页面][vite-dev-url]即可。
[vite-dev-url]: http://localhost:5173/
## 构建生产版本
💡 你需要先完成「安装依赖」部分。
```sh
pnpm build
```
如果需要预览构建版本,运行 `pnpm preview` 然后打开[项目预览页面][vite-preview-url]即可。
[vite-preview-url]: http://localhost:4173/
## 打包 `.zip`
建议在 Linux 环境下执行,可参考 `.drone.yml` CI 文件。
1. 确保上述的构建步骤已完成。
2. 确保 `python3` 已安装。
3. 执行下述代码
```sh
python3 -m zipfile -c um-react.zip dist/.
```
## 打包 win64 单文件
利用 Windows 系统自带的 [Edge WebView2 组件](https://learn.microsoft.com/zh-cn/microsoft-edge/webview2/)
和 [wry](https://github.com/tauri-apps/wry) 进行一个单文件的打包。
大部分 Windows 10 或以上版本的操作系统已经集成了 WebView2 运行时。若无法正常启动,请[下载并安装 Edge WebView2 运行时](https://go.microsoft.com/fwlink/p/?LinkId=2124703)。
其它系统兼容性未知。
1. 确保你现在在 `linux-amd64` 环境下。
2. 确保上述的 `um-react.zip` 构建已完成。
3. 执行下述代码
```sh
./scripts/make-win64.sh
```
4. 等待提示 `[Build OK]` 即可。

View File

@ -1,9 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-cmn-Hans-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>音乐解锁 - Unlock Music</title>
<meta name="description" content="音乐解锁 - Unlock Music" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/pwa-512x512.png" sizes="512x512" />
<meta name="theme-color" content="#4DBA87" />
</head>
<body>
<main id="root"></main>

View File

@ -1,31 +0,0 @@
module.exports = {
roots: ['<rootDir>/src'],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/mocks/**'],
coveragePathIgnorePatterns: [],
setupFilesAfterEnv: ['./src/test-utils/setup-jest.ts'],
testEnvironment: 'jsdom',
modulePaths: ['<rootDir>/src'],
transform: {
'^.+\\.(ts|js|tsx|jsx)$': '@swc/jest',
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
'^.+\\.module\\.(css|sass|scss)$',
],
modulePaths: ['<rootDir>/src'],
moduleNameMapper: {
'^react-native$': 'react-native-web',
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
},
moduleFileExtensions: [
// Place tsx and ts to beginning as suggestion from Jest team
// https://jestjs.io/docs/configuration#modulefileextensions-arraystring
'tsx',
'ts',
'js',
'json',
'jsx',
],
// watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
resetMocks: true,
};

View File

@ -1,59 +1,91 @@
{
"name": "um-react",
"private": true,
"version": "0.1.0",
"version": "0.2.7",
"type": "module",
"scripts": {
"start": "vite",
"build": "tsc -p tsconfig.prod.json && vite build",
"build": "tsc -p tsconfig.prod.json && vite build && pnpm build:finalize",
"build:finalize": "node scripts/write-version.mjs && node scripts/minify-mjs.mjs",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier -w .",
"test": "NODE_ENV=test jest",
"preview": "vite preview"
"test": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"preview": "vite preview",
"preview:coverage": "vite preview --outDir coverage --port 5175",
"prepare": "husky install"
},
"dependencies": {
"@chakra-ui/icons": "^2.0.19",
"@chakra-ui/react": "^2.6.1",
"@emotion/react": "^11.11.0",
"@chakra-ui/anatomy": "^2.2.2",
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@jixun/libparakeet": "0.0.0-exp.16",
"@reduxjs/toolkit": "^1.9.5",
"framer-motion": "^10.12.8",
"nanoid": "^4.0.2",
"@jixun/libparakeet": "0.4.3",
"@reduxjs/toolkit": "^2.0.1",
"framer-motion": "^10.16.16",
"nanoid": "^5.0.4",
"radash": "^11.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-icons": "^4.12.0",
"react-promise-suspense": "^0.3.4",
"react-redux": "^8.0.5"
"react-redux": "^9.0.4",
"react-syntax-highlighter": "^15.5.0",
"sass": "^1.69.5",
"sql.js": "^1.9.0"
},
"devDependencies": {
"@rollup/plugin-replace": "^5.0.2",
"@swc/core": "^1.3.58",
"@swc/jest": "^0.2.26",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.1",
"@types/node": "^20.1.1",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"@rollup/plugin-replace": "^5.0.5",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.1.0",
"@vitest/ui": "^1.1.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"typescript": "^5.0.2",
"vite": "^4.3.2",
"vite-plugin-top-level-await": "^1.3.0",
"vite-plugin-wasm": "^3.2.2"
"eslint-plugin-react-refresh": "^0.4.5",
"husky": "^8.0.3",
"jsdom": "^23.0.1",
"lint-staged": "^15.2.0",
"prettier": "^3.1.1",
"terser": "^5.27.0",
"typescript": "^5.3.3",
"vite": "^5.0.10",
"vite-plugin-pwa": "^0.17.4",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^1.1.0",
"workbox-window": "^7.0.0"
},
"lint-staged": {
"*": "prettier --write --ignore-unknown",
"*.{js,jsx,ts,tsx}": "eslint --fix --report-unused-disable-directives --max-warnings 0"
},
"prettier": {
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
},
"pnpm": {
"patchedDependencies": {
"@rollup/plugin-terser@0.4.3": "patches/@rollup__plugin-terser@0.4.3.patch",
"sql.js@1.9.0": "patches/sql.js@1.9.0.patch"
},
"overrides": {
"rollup-plugin-terser": "npm:@rollup/plugin-terser@0.4.3",
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@1.4.15"
}
}
}
}

View File

@ -0,0 +1,22 @@
diff --git a/dist/cjs/index.js b/dist/cjs/index.js
index 23639c70ccd5ebbc4a12c1ba277e5ac81cf30513..aba3318977523785d02f3b8aa6e667a1aa46bf5d 100644
--- a/dist/cjs/index.js
+++ b/dist/cjs/index.js
@@ -227,5 +227,5 @@ function terser(input = {}) {
runWorker();
exports.default = terser;
-module.exports = Object.assign(exports.default, exports);
+module.exports = Object.assign(exports.default, exports, { terser });
//# sourceMappingURL=index.js.map
diff --git a/dist/es/index.js b/dist/es/index.js
index 7296e677e6b2b38df9522b196fee24feec996793..4ca9052dd439ed22ff92cc72a79824b85e229678 100644
--- a/dist/es/index.js
+++ b/dist/es/index.js
@@ -222,5 +222,5 @@ function terser(input = {}) {
runWorker();
-export { terser as default };
+export { terser as default, terser };
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1,11 @@
diff --git a/dist/sql-wasm.js b/dist/sql-wasm.js
index d29af3624109025e59966cf25cb357111bb459de..1b028e3d91ec37108f775627f31f1134aec47476 100644
--- a/dist/sql-wasm.js
+++ b/dist/sql-wasm.js
@@ -190,3 +190,6 @@ else if (typeof define === 'function' && define['amd']) {
else if (typeof exports === 'object'){
exports["Module"] = initSqlJs;
}
+
+var module;
+export default initSqlJs;

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
# Keep this folder

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/pwa-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/pwa-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

110
scripts/deploy.sh Executable file
View File

@ -0,0 +1,110 @@
#!/bin/bash -e
BRANCH_NAME="$(git branch --show-current)"
SCRIPTS_DIR="$(dirname "${BASH_SOURCE[0]}")"
__netlify_upload() {
local branch="$BRANCH_NAME"
local production="$DEPLOY_PRODUCTION"
[[ "$BRANCH_NAME" = "main" ]] && production="true"
[[ -z "$production" ]] && production="false"
curl -sL \
-H "Content-Type: application/zip" \
-H "Authorization: Bearer ${NETLIFY_API_KEY}" \
--data-binary "@${1}" \
"https://api.netlify.com/api/v1/sites/${NETLIFY_SITE_ID}/deploys?branch=${branch}&production=${production}"
}
__netlify_get_deploy() {
local deploy_id="$1"
curl -sL \
-H "Authorization: Bearer ${NETLIFY_API_KEY}" \
"https://api.netlify.com/api/v1/deploys/${deploy_id}"
}
# Publish a deployment to main URL.
__netlify_promote() {
local deploy_id="$1"
curl -sL \
-H "Authorization: Bearer ${NETLIFY_API_KEY}" \
-H "Content-Type: application/json" \
--data "{}" \
"https://api.netlify.com/api/v1/sites/${NETLIFY_SITE_ID}/deploys/${deploy_id}/restore"
}
__netlify_get_error() {
local error_message
error_message="$(json_get "$upload_resp" message)"
[[ "$error_message" = "null" ]] && error_message="$(json_get "$upload_resp" error_message)"
echo -n "$error_message"
}
json_get() {
local json_body="$1"
shift
echo -n "$json_body" | "${SCRIPTS_DIR}/read_json.mjs" "$@"
}
deploy_netlify() {
local upload_resp
upload_resp="$(__netlify_upload "$1")"
local error_message="$(__netlify_get_error "$upload_resp")"
if [[ "$error_message" != "null" ]]; then
echo "Deploy to netlify failed:"
echo " * ${error_message}"
return 1
fi
local deploy_id="$(json_get "$upload_resp" id)"
local deploy_resp=""
local deploy_state=""
local retry_count=10
while [[ "$retry_count" -gt 0 ]]; do
deploy_resp="$(__netlify_get_deploy "$deploy_id")"
deploy_state="$(json_get "$deploy_resp" 'state')"
case "$deploy_state" in
ready)
echo 'Deploy to netlify OK!'
echo " * main url: $(json_get "$deploy_resp" 'ssl_url')"
echo " * branch: $(json_get "$deploy_resp" 'deploy_ssl_url')"
echo " * permalink: $(json_get "$deploy_resp" 'links' 'permalink')"
break
;;
error)
echo "Deploy to netlify failed:"
echo " * $(json_get "$deploy_resp" 'error_message')"
return 1
;;
*)
retry_count="$((retry_count - 1))"
sleep 3
;;
esac
done
if [[ "$BRANCH_NAME" = "main" ]]; then
echo "Promoting latest main build..."
local promote_resp="$(__netlify_promote "$(json_get "$deploy_resp" 'id')")"
error_message="$(__netlify_get_error "$promote_resp")"
if [[ "$error_message" != "null" ]]; then
echo "Promote netlify deploy failed:"
echo " * ${error_message}"
return 1
else
echo 'Deoployed to main url.'
fi
fi
}
# For deployment, we care a bit less
if [[ -n "${NETLIFY_API_KEY}" && -n "${NETLIFY_SITE_ID}" ]]; then
echo "Deploy to netlify..."
deploy_netlify um-react-site.zip
else
echo "skip netlify deployment."
fi

33
scripts/make-win64.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
# sudo apt install -y jq zip
pushd "$(dirname "${BASH_SOURCE[0]}")/../"
WRY_VER="0.1.1"
mkdir -p win64/{deps,dist}
dl_file() {
local FILE="$1"
if [[ ! -f "win64/deps/$FILE" ]]; then
curl -fsL "https://um-react.app/files/${FILE}.gz" | gzip -d >"win64/deps/${FILE}"
fi
}
dl_file "um-react-wry-builder-${WRY_VER}-linux-amd64"
dl_file "um-react-wry-stub-${WRY_VER}-win64.exe"
chmod a+x win64/deps/um-react-wry-builder-${WRY_VER}-linux-amd64
APP_VERSION="$(jq -r '.version' <package.json)"
EXE_NAME="um-react-win64-${APP_VERSION}.exe"
ZIP_NAME="um-react-win64-${APP_VERSION}.zip"
"./win64/deps/um-react-wry-builder-${WRY_VER}-linux-amd64" \
-t "win64/deps/um-react-wry-stub-${WRY_VER}-win64.exe" \
-r um-react.zip \
-o "win64/dist/${EXE_NAME}"
touch -d 1970-01-01T00:00:00Z "win64/dist/${EXE_NAME}"
zip -9oX "win64/dist/${ZIP_NAME}" -- "win64/dist/${EXE_NAME}"
echo "[Build OK] 'win64/dist/${ZIP_NAME}'."
popd

19
scripts/minify-mjs.mjs Normal file
View File

@ -0,0 +1,19 @@
import { minify } from 'terser';
import { readFileSync, writeFileSync, readdirSync } from 'fs';
for (const file of readdirSync('dist/assets')) {
if (!/\.(mjs|js)$/.test(file)) {
continue;
}
console.log(`minifying ${file}...`);
const isModule = /\.mjs$/.test(file);
const output = await minify(readFileSync(`dist/assets/${file}`, 'utf-8'), {
compress: true,
mangle: true,
module: isModule,
});
writeFileSync(`dist/assets/${file}`, output.code);
}

21
scripts/publish.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash -e
BRANCH_NAME="$(git branch --show-current)"
publish_gitea() {
local ZIP_NAME="$1"
local URL="${DRONE_GITEA_SERVER}/api/packages/${DRONE_REPO_NAMESPACE}/generic/${DRONE_REPO_NAME}/${DRONE_BUILD_NUMBER}/${ZIP_NAME}"
sha256sum "${ZIP_NAME}"
curl -sLifu "um-release-bot:${GITEA_API_KEY}" -T "${ZIP_NAME}" "${URL}"
echo "Uploaded to: ${URL}"
}
# Only publish main branch by default
if [[ "${BRANCH_NAME}" = "main" && -z "$DRONE_PULL_REQUEST" ]]; then
echo 'prepare to publish...'
if [[ -n "${GITEA_API_KEY}" ]]; then
echo "Publish to gitea..."
publish_gitea "um-react.zip"
fi
fi

12
scripts/read_json.mjs Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env node
/* eslint-env node */
import fs from 'fs';
const data = JSON.parse(fs.readFileSync(0, 'utf-8').trim());
let value = data;
for (let i = 2; i < process.argv.length; i++) {
value = value[process.argv[i]] ?? null;
}
process.stdout.write(String(value), 'utf-8');

14
scripts/write-version.mjs Normal file
View File

@ -0,0 +1,14 @@
/* eslint-env node */
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { execSync } from 'node:child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const commitHash = execSync('git rev-parse --short HEAD').toString('utf-8').trim();
const pkgJson = JSON.parse(readFileSync(__dirname + '/../package.json', 'utf-8'));
const pkgVer = `${pkgJson.version ?? 'unknown'}-${commitHash ?? 'unknown'}` + '\n';
writeFileSync(__dirname + '/../dist/version.txt', pkgVer, 'utf-8');

View File

@ -1,25 +0,0 @@
import { Box, Center, Container } from '@chakra-ui/react';
import { SelectFile } from './SelectFile';
import { FileListing } from './features/file-listing/FileListing';
import { Footer } from './Footer';
import { WasmTest } from './WasmTest';
function App() {
return (
<Box height="full" width="full" pt="4">
<Container maxW="container.large">
<Center>
<SelectFile />
</Center>
<Box mt="8">
<FileListing />
</Box>
{localStorage.__dev_test === '1' && <WasmTest />}
<Footer />
</Container>
</Box>
);
}
export default App;

View File

@ -1,41 +0,0 @@
import { Center, Flex, Link, Text } from '@chakra-ui/react';
import { Suspense } from 'react';
import { SDKVersion } from './SDKVersion';
export function Footer() {
return (
<Center height="footer.container">
<Center
height="footer.content"
fontSize="sm"
textAlign="center"
position="fixed"
bottom="0"
w="full"
bg="gray.100"
color="gray.800"
left="0"
flexDir="column"
>
<Flex as={Text}>
{'音乐解锁 (__APP_VERSION_SHORT__'}
<Suspense>
<SDKVersion />
</Suspense>
{') - 移除已购音乐的加密保护。'}
</Flex>
<Text>
{'Copyright © 2019 - 2023 '}
<Link href="https://git.unlock-music.dev/um" isExternal>
UnlockMusic
</Link>
{' | 音乐解锁授权基于'}
<Link href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" isExternal>
MIT许可协议
</Link>
</Text>
</Center>
</Center>
);
}

View File

@ -1,69 +0,0 @@
import React, { useId } from 'react';
import { Box, Text } from '@chakra-ui/react';
import { UnlockIcon } from '@chakra-ui/icons';
import { useAppDispatch } from './hooks';
import { addNewFile, processFile } from './features/file-listing/fileListingSlice';
import { nanoid } from 'nanoid';
export function SelectFile() {
const dispatch = useAppDispatch();
const id = useId();
const handleFileSelection = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
for (const file of e.target.files) {
const blobURI = URL.createObjectURL(file);
const fileName = file.name;
const fileId = 'file://' + nanoid();
// FIXME: this should be a single action/thunk that first adds the item, then updates it.
dispatch(
addNewFile({
id: fileId,
blobURI,
fileName,
})
);
dispatch(processFile({ fileId }));
}
}
e.target.value = '';
};
return (
<Box
as="label"
htmlFor={id}
w="100%"
maxW={480}
borderWidth="1px"
borderRadius="lg"
transitionDuration="0.5s"
p="6"
cursor="pointer"
display="flex"
flexDir="column"
alignItems="center"
_hover={{
borderColor: 'gray.400',
bg: 'gray.50',
}}
>
<Box pb={3}>
<UnlockIcon boxSize={8} />
</Box>
<Box textAlign="center">
{/* 将文件拖到此处,或 */}
<Text as="span" color="teal.400">
</Text>
<input id={id} type="file" hidden multiple onChange={handleFileSelection} />
<Text fontSize="sm" opacity="50%">
</Text>
</Box>
</Box>
);
}

View File

@ -1,31 +0,0 @@
import { loadLibParakeet, BlobSink, createArrayBufferReader } from '@jixun/libparakeet';
function testWasm() {
loadLibParakeet().then(async (mod) => {
const data = new Uint8Array(0x2000);
for (let i = 0; i < data.byteLength; i++) {
data[i] = i & 0xff;
}
const src = createArrayBufferReader(data, mod);
const sink = new BlobSink(mod);
mod.rw_test(sink.getWriter(), src);
const collected = sink.collectBlob();
const copied = await collected.arrayBuffer();
const copiedView = new Uint8Array(copied);
for (let i = 0; i < copied.byteLength; i++) {
if (copiedView[i] !== (i & 0xff)) {
alert(`validate at pos ${i} failed`);
return;
}
}
alert('wasm validate ok!');
});
}
export function WasmTest() {
return (
<button onClick={testWasm} type="button">
Test WASM
</button>
);
}

View File

@ -1,6 +0,0 @@
import { render, screen } from '@testing-library/react';
test('hello', () => {
render(<div>hello</div>);
expect(screen.getByText('hello') as any).toBeInTheDocument();
});

View File

@ -0,0 +1,21 @@
import { renderWithProviders, screen, waitFor } from '~/test-utils/test-helper';
import { AppRoot } from '~/components/AppRoot';
vi.mock('../decrypt-worker/client', () => {
return {
workerClientBus: {
request: vi.fn().mockResolvedValue('dummy'),
},
};
});
test('should be able to render App', async () => {
renderWithProviders(<AppRoot />);
// Should eventually load sdk version
await waitFor(() => screen.getByTestId('sdk-version'));
// Quick sanity check of known strings.
expect(screen.getByText(/在浏览器内对文件进行解锁/i)).toBeInTheDocument();
expect(screen.getByText(/UnlockMusic 团队/i)).toBeInTheDocument();
});

9
src/assets/no-cover.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
<rect fill="#ddd" width="160" height="160" />
<text fill="#0007" font-family="sans-serif" font-size="24" font-weight="bold"
text-anchor="middle" letter-spacing="6"
dy="9.45" x="50%" y="50%"
>
暂无封面
</text>
</svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@ -0,0 +1,171 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Code,
Heading,
ListItem,
OrderedList,
Text,
chakra,
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
import PowerShellAdbDumpCommandTemplate from './adb_dump.ps1?raw';
import ShellAdbDumpCommandTemplate from './adb_dump.sh?raw';
import { ExtLink } from '../ExtLink';
const applyTemplate = (tpl: string, values: Record<string, unknown>) => {
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : '<nil>'));
};
export interface AndroidADBPullInstructionProps {
dir: string;
file: string;
}
export function AndroidADBPullInstruction({ dir, file }: AndroidADBPullInstructionProps) {
const psAdbDumpCommand = applyTemplate(PowerShellAdbDumpCommandTemplate, { dir, file });
const shAdbDumpCommand = applyTemplate(ShellAdbDumpCommandTemplate, { dir, file });
return (
<>
<Text>
<ruby>
<rp> (</rp>
<rt>
<code>root</code>
</rt>
<rp>)</rp>
</ruby>
访访
</Text>
<Text>
<chakra.span color="red.400"></chakra.span>
</Text>
<Accordion allowToggle mt="2">
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
</Box>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
<Text>
<Code>root</Code>
</Text>
</ListItem>
<ListItem>
<Text>
访 <Code>{dir}/</Code>
</Text>
</ListItem>
<ListItem>
<Text>
<Code>{file}</Code> 访
<br />
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
</OrderedList>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
PC ADB / PowerShell
</Box>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
<Text>
<Code>adb</Code>
</Text>
<Text>
💡
<ExtLink href="https://scoop.sh/#/apps?q=adb">
使 Scoop <ExternalLinkIcon />
</ExtLink>
</Text>
</ListItem>
<ListItem>
<Text> PowerShell 7 </Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text></Text>
<SyntaxHighlighter language="ps1" style={hljsStyleGitHub}>
{psAdbDumpCommand}
</SyntaxHighlighter>
</ListItem>
<ListItem>
<Text>
<Code>{file}</Code>
</Text>
</ListItem>
</OrderedList>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Linux / Mac ADB / Shell
</Box>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
<Text>
<Code>adb</Code>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text></Text>
<SyntaxHighlighter language="bash" style={hljsStyleGitHub}>
{shAdbDumpCommand}
</SyntaxHighlighter>
</ListItem>
<ListItem>
<Text>
<Code>{file}</Code>
</Text>
</ListItem>
</OrderedList>
</AccordionPanel>
</AccordionItem>
</Accordion>
</>
);
}

View File

@ -0,0 +1,14 @@
try {
$gz_b64 = adb shell su -c "cat '{{ dir }}/{{ file }}' | gzip | base64" | Out-String
$bStream = New-Object System.IO.MemoryStream(,[System.Convert]::FromBase64String($gz_b64))
$decoded = New-Object System.IO.Compression.GzipStream($bStream, [System.IO.Compression.CompressionMode]::Decompress)
$outFile = New-Object System.IO.FileStream("${PWD}\{{ file }}", [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)
$decoded.CopyTo($outFile)
} catch {
Write-Host "遇到错误:"
Write-Host $_
} finally {
if ($outFile -ne $null) { $outFile.Dispose() }
if ($decoded -ne $null) { $decoded.Dispose() }
if ($bStream -ne $null) { $bStream.Dispose() }
}

View File

@ -0,0 +1,2 @@
adb shell su -c "cat '{{ dir }}/{{ file }}' | gzip | base64" \
| base64 -d | gzip -d >'{{ file }}'

View File

@ -0,0 +1,57 @@
import { useEffect } from 'react';
import { MdSettings, MdHome, MdQuestionAnswer } from 'react-icons/md';
import { ChakraProvider, Tabs, TabList, TabPanels, Tab, TabPanel, Icon, chakra } from '@chakra-ui/react';
import { MainTab } from '~/tabs/MainTab';
import { SettingsTab } from '~/tabs/SettingsTab';
import { Provider } from 'react-redux';
import { theme } from '~/theme';
import { persistSettings } from '~/features/settings/persistSettings';
import { setupStore } from '~/store';
import { Footer } from '~/components/Footer';
import { FaqTab } from '~/tabs/FaqTab';
// Private to this file only.
const store = setupStore();
export function AppRoot() {
useEffect(() => persistSettings(store), []);
return (
<ChakraProvider theme={theme}>
<Provider store={store}>
<Tabs flex={1} minH={0} display="flex" flexDir="column">
<TabList justifyContent="center">
<Tab>
<Icon as={MdHome} mr="1" />
<chakra.span></chakra.span>
</Tab>
<Tab>
<Icon as={MdSettings} mr="1" />
<chakra.span></chakra.span>
</Tab>
<Tab>
<Icon as={MdQuestionAnswer} mr="1" />
<chakra.span></chakra.span>
</Tab>
</TabList>
<TabPanels overflow="auto" minW={0} flexDir="column" flex={1} display="flex">
<TabPanel>
<MainTab />
</TabPanel>
<TabPanel flex={1} display="flex">
<SettingsTab />
</TabPanel>
<TabPanel>
<FaqTab />
</TabPanel>
</TabPanels>
</Tabs>
<Footer />
</Provider>
</ChakraProvider>
);
}

View File

@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';
// Update every half hour
const TIMER_UPDATE_INTERVAL = 30 * 60 * 1000;
const getCurrentYear = () => new Date().getFullYear();
export function CurrentYear() {
const [year, setYear] = useState(getCurrentYear);
useEffect(() => {
const updateTime = () => setYear(getCurrentYear);
updateTime();
const timer = setInterval(updateTime, TIMER_UPDATE_INTERVAL);
return () => {
clearInterval(timer);
};
}, []);
return <>{year}</>;
}

View File

@ -0,0 +1,12 @@
import type { AnchorHTMLAttributes } from 'react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Link } from '@chakra-ui/react';
export function ExtLink({ children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) {
return (
<Link isExternal {...props} rel="noreferrer noopener nofollow">
{children}
<ExternalLinkIcon />
</Link>
);
}

View File

@ -0,0 +1,43 @@
import { useDropzone } from 'react-dropzone';
import { Box } from '@chakra-ui/react';
export interface FileInputProps {
onReceiveFiles: (files: File[]) => void;
multiple?: boolean;
children: React.ReactNode;
}
export function FileInput({ children, onReceiveFiles }: FileInputProps) {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
multiple: true,
onDropAccepted: onReceiveFiles,
});
return (
<Box
{...getRootProps()}
w="100%"
maxW={480}
borderWidth="1px"
borderRadius="lg"
transitionDuration="0.5s"
p="6"
cursor="pointer"
display="flex"
flexDir="column"
alignItems="center"
_hover={{
borderColor: 'gray.400',
bg: 'gray.50',
}}
{...(isDragActive && {
bg: 'blue.50',
borderColor: 'blue.700',
})}
>
<input {...getInputProps()} />
{children}
</Box>
);
}

View File

@ -0,0 +1,10 @@
import { Code, Text } from '@chakra-ui/react';
import React from 'react';
export function FilePathBlock({ children }: { children: React.ReactNode }) {
return (
<Text as="pre" whiteSpace="pre-wrap" wordBreak="break-all">
<Code>{children}</Code>
</Text>
);
}

45
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,45 @@
import { Center, Flex, Link, Text } from '@chakra-ui/react';
import { Suspense } from 'react';
import { SDKVersion } from './SDKVersion';
import { CurrentYear } from './CurrentYear';
export function Footer() {
return (
<Center
fontSize="sm"
textAlign="center"
bottom="0"
w="full"
pt="3"
pb="3"
borderTop="1px solid"
borderColor="gray.300"
bg="gray.100"
color="gray.800"
flexDir="column"
flexShrink={0}
>
<Flex as={Text}>
<Link href="https://git.unlock-music.dev/um/um-react" isExternal>
</Link>
{' (__APP_VERSION_SHORT__'}
<Suspense>
<SDKVersion />
</Suspense>
{') - 移除已购音乐的加密保护。'}
</Flex>
<Text>
{'© 2019 - '}
<CurrentYear />{' '}
<Link href="https://git.unlock-music.dev/um" isExternal>
UnlockMusic
</Link>
{' | '}
<Link href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" isExternal>
使 MIT
</Link>
</Text>
</Center>
);
}

View File

@ -0,0 +1,26 @@
import { Heading } from '@chakra-ui/react';
import React from 'react';
export interface Header3Props {
children: React.ReactNode;
id?: string;
className?: string;
}
export function Header3({ children, className, id }: Header3Props) {
return (
<Heading
as="h3"
id={id}
className={className}
pt={3}
pb={1}
borderBottom={'1px solid'}
borderColor="gray.300"
color="gray.800"
size="lg"
>
{children}
</Heading>
);
}

View File

@ -0,0 +1,16 @@
import { Heading } from '@chakra-ui/react';
import React from 'react';
export interface Header4Props {
children: React.ReactNode;
id?: string;
className?: string;
}
export function Header4({ children, className, id }: Header4Props) {
return (
<Heading as="h4" id={id} className={className} pt={3} pb={1} color="gray.700" size="md">
{children}
</Heading>
);
}

View File

@ -0,0 +1,9 @@
import { Mark } from '@chakra-ui/react';
export function HiWord({ children }: { children: React.ReactNode }) {
return (
<Mark bg="orange.100" borderRadius={5} px={2} mx={1}>
{children}
</Mark>
);
}

View File

@ -0,0 +1,13 @@
import { chakra, css } from '@chakra-ui/react';
const cssUnselectable = css({ pointerEvents: 'none', userSelect: 'none' });
export function VQuote({ children }: { children: React.ReactNode }) {
return (
<>
<chakra.span css={cssUnselectable}></chakra.span>
{children}
<chakra.span css={cssUnselectable}></chakra.span>
</>
);
}

View File

@ -0,0 +1,48 @@
import {
Center,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Tabs,
Text,
} from '@chakra-ui/react';
import { FileInput } from '~/components/FileInput';
export interface ImportSecretModalProps {
clientName?: React.ReactNode;
children: React.ReactNode;
show: boolean;
onClose: () => void;
onImport: (file: File) => void;
}
export function ImportSecretModal({ clientName, children, show, onClose, onImport }: ImportSecretModalProps) {
const handleFileReceived = (files: File[]) => onImport(files[0]);
return (
<Modal isOpen={show} onClose={onClose} closeOnOverlayClick={false} scrollBehavior="inside" size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<Flex as={ModalBody} gap={2} flexDir="column" flex={1}>
<Center>
<FileInput onReceiveFiles={handleFileReceived}></FileInput>
</Center>
<Text as="div" mt={2}>
{clientName && <>{clientName}</>}
</Text>
<Flex as={Tabs} variant="enclosed" flexDir="column" flex={1} minH={0}>
{children}
</Flex>
</Flex>
</ModalContent>
</Modal>
);
}

View File

@ -0,0 +1,15 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsCommand } from 'react-icons/bs';
export function MacCommandKey() {
return (
<ruby>
<Kbd>
<Icon as={BsCommand} />
</Kbd>
<rp> (</rp>
<rt>command</rt>
<rp>)</rp>
</ruby>
);
}

View File

@ -0,0 +1,15 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsShift } from 'react-icons/bs';
export function ShiftKey() {
return (
<ruby>
<Kbd>
<Icon as={BsShift} />
</Kbd>
<rp> (</rp>
<rt>shift</rt>
<rp>)</rp>
</ruby>
);
}

View File

@ -0,0 +1,15 @@
import { Link } from '@chakra-ui/react';
export interface ProjectIssueProps {
id: number | string;
title?: string;
}
export function ProjectIssue({ id, title }: ProjectIssueProps) {
return (
<Link isExternal target="_blank" href={`https://git.unlock-music.dev/um/um-react/issues/${id}`}>
{`#${id}`}
{title && ` - ${title}`}
</Link>
);
}

View File

@ -1,18 +1,19 @@
import { InfoOutlineIcon } from '@chakra-ui/icons';
import { Tooltip, VStack, Text, Flex } from '@chakra-ui/react';
import { workerClientBus } from './decrypt-worker/client';
import { DECRYPTION_WORKER_ACTION_NAME } from './decrypt-worker/constants';
import { workerClientBus } from '~/decrypt-worker/client';
import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants';
import usePromise from 'react-promise-suspense';
const getSDKVersion = async () => {
const getSDKVersion = async (): Promise<string> => {
return workerClientBus.request(DECRYPTION_WORKER_ACTION_NAME.VERSION, null);
};
export function SDKVersion() {
const sdkVersion = usePromise(getSDKVersion, []);
return (
<Flex as="span" pl="1" alignItems="center">
<Flex as="span" pl="1" alignItems="center" data-testid="sdk-version">
<Tooltip
hasArrow
placement="top"

View File

@ -0,0 +1,52 @@
import { Box, Text } from '@chakra-ui/react';
import { UnlockIcon } from '@chakra-ui/icons';
import { useAppDispatch } from '~/hooks';
import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice';
import { nanoid } from 'nanoid';
import { FileInput } from './FileInput';
export function SelectFile() {
const dispatch = useAppDispatch();
const handleFileReceived = (files: File[]) => {
console.debug(
'react-dropzone/onDropAccepted(%o, %o)',
files.length,
files.map((x) => x.name)
);
for (const file of files) {
const blobURI = URL.createObjectURL(file);
const fileName = file.name;
const fileId = 'file://' + nanoid();
// FIXME: this should be a single action/thunk that first adds the item, then updates it.
dispatch(
addNewFile({
id: fileId,
blobURI,
fileName,
})
);
dispatch(processFile({ fileId }));
}
};
return (
<FileInput multiple onReceiveFiles={handleFileReceived}>
<Box pb={3}>
<UnlockIcon boxSize={8} />
</Box>
<Text as="div" textAlign="center">
<Text as="span" color="teal.400">
</Text>
<Text fontSize="sm" opacity="50%">
</Text>
</Text>
</FileInput>
);
}

27
src/crypto/parseKuwo.ts Normal file
View File

@ -0,0 +1,27 @@
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
import { strlen } from './strlen';
export interface KuwoHeader {
rid: string; // uint64
encVersion: 1 | 2; // uint32
quality: string;
}
const KUWO_MAGIC_HDRS = new Set(['yeelion-kuwo\x00\x00\x00\x00', 'yeelion-kuwo-tme']);
export function parseKuwoHeader(view: DataView): KuwoHeader | null {
const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10);
if (!KUWO_MAGIC_HDRS.has(bytesToUTF8String(magic))) {
return null; // not kuwo-encrypted file
}
const qualityBytes = new Uint8Array(view.buffer.slice(view.byteOffset + 0x30, view.byteOffset + 0x40));
const qualityLen = strlen(qualityBytes);
const quality = bytesToUTF8String(qualityBytes.slice(0, qualityLen));
return {
encVersion: view.getUint32(0x10, true) as 1 | 2,
rid: view.getUint32(0x18, true).toString(),
quality,
};
}

9
src/crypto/strlen.ts Normal file
View File

@ -0,0 +1,9 @@
export function strlen(data: Uint8Array): number {
const n = data.byteLength;
for (let i = 0; i < n; i++) {
if (data[i] === 0) {
return i;
}
}
return n;
}

View File

@ -1,22 +1,12 @@
import { ConcurrentQueue } from '~/util/ConcurrentQueue';
import { WorkerClientBus } from '~/util/WorkerEventBus';
import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
import { DecryptionQueue } from '~/util/DecryptionQueue';
// TODO: Worker pool?
export const workerClient = new Worker(new URL('./worker', import.meta.url), { type: 'module' });
// FIXME: report the error so is obvious to the user.
workerClient.onerror = (err) => console.error(err);
class DecryptionQueue extends ConcurrentQueue<{ id: string; blobURI: string }> {
constructor(private workerClientBus: WorkerClientBus<DECRYPTION_WORKER_ACTION_NAME>, maxQueue?: number) {
super(maxQueue);
}
async handler(item: { id: string; blobURI: string }): Promise<void> {
return this.workerClientBus.request(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, item.blobURI);
}
}
workerClient.addEventListener('error', console.error);
export const workerClientBus = new WorkerClientBus<DECRYPTION_WORKER_ACTION_NAME>(workerClient);
export const decryptionQueue = new DecryptionQueue(workerClientBus);

View File

@ -1,5 +1,6 @@
export enum DECRYPTION_WORKER_ACTION_NAME {
DECRYPT = 'DECRYPT',
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
VERSION = 'VERSION',
}

View File

@ -1,10 +1,17 @@
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
export interface CryptoBase {
cryptoName: string;
checkByDecryptHeader: boolean;
/**
* When returning false, a successful decryption should be checked by its decrypted content instead.
* If set, this new extension will be used instead.
* Useful for non-audio format, e.g. qrc to lrc/xml.
*/
hasSignature(): boolean;
isSupported(blob: Blob): Promise<boolean>;
decrypt(blob: Blob): Promise<Blob>;
overrideExtension?: string;
checkBySignature?: (buffer: ArrayBuffer, options: DecryptCommandOptions) => Promise<boolean>;
decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob | ArrayBuffer>;
}
export type CryptoFactory = () => CryptoBase;

View File

@ -0,0 +1,49 @@
import { CryptoFactory } from './CryptoBase';
import { QMC1Crypto } from './qmc/qmc_v1';
import { QMC2Crypto, QMC2CryptoWithKey } from './qmc/qmc_v2';
import { XiamiCrypto } from './xiami/xiami';
import { KGMCrypto } from './kgm/kgm_pc';
import { NCMCrypto } from './ncm/ncm_pc';
import { XimalayaAndroidCrypto } from './xmly/xmly_android';
import { KWMCrypto } from './kwm/kwm';
import { MiguCrypto } from './migu/migu3d_keyless';
import { TransparentCrypto } from './transparent/transparent';
import { QingTingFM$Device } from './qtfm/qtfm_device';
export const allCryptoFactories: CryptoFactory[] = [
// Xiami (*.xm)
XiamiCrypto.make,
// QMCv2 (*.mflac)
QMC2CryptoWithKey.make,
QMC2Crypto.make,
// NCM (*.ncm)
NCMCrypto.make,
// KGM (*.kgm, *.vpr)
KGMCrypto.make,
// KWMv1 (*.kwm)
KWMCrypto.make,
// Migu3D/Keyless (*.wav; *.m4a)
MiguCrypto.make,
// Crypto that does not implement "checkBySignature" or need to decrypt the entire file and then check audio type,
// should be moved to the bottom of the list for performance reasons.
// QMCv1 (*.qmcflac)
QMC1Crypto.make,
// Ximalaya (Android)
XimalayaAndroidCrypto.makeX2M,
XimalayaAndroidCrypto.makeX3M,
// QingTingFM (Android)
QingTingFM$Device.make,
// Transparent crypto (not encrypted)
TransparentCrypto.make,
];

View File

@ -0,0 +1,6 @@
import KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE_RAW from './kgm_type4_file_key_expansion_table.txt?raw';
import KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE_RAW from './kgm_type4_slot_key_expansion_table.txt?raw';
export const KGM_SLOT_1_KEY = "l,/'";
export const KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE = KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE_RAW.trim();
export const KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE = KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE_RAW.trim();

View File

@ -0,0 +1,18 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
import { KGM_SLOT_1_KEY, KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE, KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE } from './kgm_pc.key';
export class KGMCrypto implements CryptoBase {
cryptoName = 'KGM/PC';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) =>
p.make.KugouKGM(KGM_SLOT_1_KEY, KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE, KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE)
);
}
public static make() {
return new KGMCrypto();
}
}

View File

@ -0,0 +1 @@
!@#$%^&*(O)P_+DCFVBGNMXDCFVBGN!@#$%^&*()_@#$%^&*()kljhgfk;oswhqoi7t89g_+@#$%^&*()!@#$%^&*()@#$%^&*(@#$%^&*()@#$%^&*()@#$^&$&^%*&^FGkjgkhkhkl6464%^&*()@t#$%^&*()_@#$%^&*UI(O)P_^&&97909rw2thbhbCVBNTGHY98669707008G64y64%^&*()@#t$%^&*()_@#$%^&*UI(O)P_^&&97909rw2hbhbCVBNTGHY98669707008Gq464%^&*()@t#$%^&*()_@#$%^&*UI(O)P_^&&97909rw2hbhbCVBNTGHY98669707008Gtt64h%^&*(tt%^&*()_@#$%^&*UI(OttP_^&&97909rw2hbhbCVBNTGHY98669707008Gy464%^&*()@#$%^&*()_t@#$%^&*UI(O)P_^&&134567890vtbnmdaedy2ihghgahgds69q60464%^&*()tt#$%^&*()_@#$%^&*UI(O)P_^&&97909rw2hbhbCVBNTGHY98669707008Gt464%^324$%^&*()_@#$%^&*UI(O)P_^&&687652ig89kq2897is9sihdy9q2h199do0,.,,63464%^&d*()@#$%^&*()_@#$%^&*UI(O)P_^&&dw3fdwert242fwesfe2352323233534

View File

@ -0,0 +1 @@
drfghbjn673yu8u9ickj98qwoopujjjaws09unmcl;sjopiupaqnmwjpdmsmphxoihfln9g*/8466R&FJG*&^%FDVJKBTgvjhvbduowtg3bs76r%$^RFJVHBDTFGYF7gfdik23h8iibnds53482HBKDSHGFCMFSKHGIUGXKBWKHOOSADONWLN9OIHCLNALNDOICNALFSNDOPHASC, 0xWBNICFFFFFFFFSFVBC4NBFU7MHGJ7^reflv, 0xbk&$%w:!oi){+u:bx*)y!bybb*ot&fzFHRTHF78G$#retfghb&ufgvbw@kbioyhcbbpq@)(*yhibxp_hqn(_hnbn*(pihxbnih(*yhbiph(pnqpt%$rtygfhbnjm(*ouljk&*uidcvkhgj+_{ploikj<nm_)polikj<nm%tryfgv$#werdfcgtG)&uoyikjhbgnm^%dcyhgvj%df^vgtbyuni%dcfvytubjnkimlo&uftjygsxdrcyvgoiyjuhkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUtugkbKGVfukjfvsho:jh:{}}{l:jlhfudydkvbiyblhz*ohizo*ytabtfzvbujtakbKJgo},634!@#$rfv(iujhg&yuhgqwsaxdc9I8UJE3DFCV*(iujhgWSTYxdchg(*itgvhjf^eHY534

View File

@ -0,0 +1 @@
export const KWM_KEY = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';

View File

@ -0,0 +1,28 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
import { KWM_KEY } from './kwm.key';
import { DecryptCommandOptions } from '~/decrypt-worker/types';
import { makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto';
import { fetchParakeet } from '@jixun/libparakeet';
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder';
// v1 only
export class KWMCrypto implements CryptoBase {
cryptoName = 'KWM';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer, opts: DecryptCommandOptions): Promise<Blob> {
const kwm2key = opts.kwm2key ?? '';
const parakeet = await fetchParakeet();
const keyCrypto = makeQMCv2KeyCrypto(parakeet);
return transformBlob(buffer, (p) => p.make.KuwoKWMv2(KWM_KEY, stringToUTF8Bytes(kwm2key), keyCrypto), {
cleanup: () => keyCrypto.delete(),
parakeet,
});
}
public static make() {
return new KWMCrypto();
}
}

View File

@ -0,0 +1,15 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
export class MiguCrypto implements CryptoBase {
cryptoName = 'Migu3D/Keyless';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) => p.make.Migu3D());
}
public static make() {
return new MiguCrypto();
}
}

View File

@ -0,0 +1,2 @@
export const NCM_KEY = 'hzHRAmso5kInbaxW';
export const NCM_MAGIC_HEADER = new Uint8Array([0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d]);

View File

@ -0,0 +1,21 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
import { NCM_KEY, NCM_MAGIC_HEADER } from './ncm_pc.key';
export class NCMCrypto implements CryptoBase {
cryptoName = 'NCM/PC';
checkByDecryptHeader = false;
async checkBySignature(buffer: ArrayBuffer) {
const view = new DataView(buffer, 0, NCM_MAGIC_HEADER.byteLength);
return NCM_MAGIC_HEADER.every((value, i) => value === view.getUint8(i));
}
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) => p.make.NeteaseNCM(NCM_KEY));
}
public static make() {
return new NCMCrypto();
}
}

View File

@ -3,15 +3,14 @@ import type { CryptoBase } from '../CryptoBase';
import key from './qmc_v1.key.ts';
export class QMC1Crypto implements CryptoBase {
hasSignature(): boolean {
return false;
cryptoName = 'QMC/v1';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) => p.make.QMCv1(key));
}
async isSupported(): Promise<boolean> {
return true;
}
async decrypt(blob: Blob): Promise<Blob> {
return transformBlob(blob, (p) => p.make.QMCv1(key));
public static make() {
return new QMC1Crypto();
}
}

View File

@ -1,17 +1,51 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts';
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { fetchParakeet } from '@jixun/libparakeet';
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts';
import { makeQMCv2FooterParser, makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts';
export class QMC2Crypto implements CryptoBase {
hasSignature(): boolean {
return false;
cryptoName = 'QMC/v2';
checkByDecryptHeader = false;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
const parakeet = await fetchParakeet();
const footerParser = makeQMCv2FooterParser(parakeet);
return transformBlob(buffer, (p) => p.make.QMCv2(footerParser), {
parakeet,
cleanup: () => footerParser.delete(),
});
}
async isSupported(): Promise<boolean> {
return true;
}
async decrypt(blob: Blob): Promise<Blob> {
return transformBlob(blob, (p) => p.make.QMCv2(p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2)));
public static make() {
return new QMC2Crypto();
}
}
export class QMC2CryptoWithKey implements CryptoBase {
cryptoName = 'QMC/v2 (key)';
checkByDecryptHeader = true;
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<boolean> {
return Boolean(options.qmc2Key);
}
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
if (!options.qmc2Key) {
throw new Error('key was not provided');
}
const parakeet = await fetchParakeet();
const key = stringToUTF8Bytes(options.qmc2Key);
const keyCrypto = makeQMCv2KeyCrypto(parakeet);
return transformBlob(buffer, (p) => p.make.QMCv2EKey(key, keyCrypto), {
parakeet,
cleanup: () => keyCrypto.delete(),
});
}
public static make() {
return new QMC2CryptoWithKey();
}
}

View File

@ -0,0 +1,25 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
import { DecryptCommandOptions } from '~/decrypt-worker/types';
export class QingTingFM$Device implements CryptoBase {
cryptoName = 'QingTing FM/Device ID';
checkByDecryptHeader = false;
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions) {
return Boolean(/^\.p~?!.*\.qta$/.test(options.fileName) && options.qingTingAndroidKey);
}
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
const { fileName: name, qingTingAndroidKey } = options;
if (!qingTingAndroidKey) {
throw new Error('QingTingFM Android Device Key was not provided');
}
return transformBlob(buffer, (p) => p.make.QingTingFM(name, qingTingAndroidKey));
}
public static make() {
return new QingTingFM$Device();
}
}

View File

@ -0,0 +1,14 @@
import type { CryptoBase } from '../CryptoBase';
export class TransparentCrypto implements CryptoBase {
cryptoName = 'Transparent';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return new Blob([buffer]);
}
public static make() {
return new TransparentCrypto();
}
}

View File

@ -11,8 +11,9 @@
import type { CryptoBase } from '../CryptoBase';
const XIAMI_FILE_MAGIC = new Uint8Array('ifmt'.split('').map((x) => x.charCodeAt(0)));
const XIAMI_EXPECTED_PADDING = new Uint8Array([0xfe, 0xfe, 0xfe, 0xfe]);
// little endian
const XIAMI_FILE_MAGIC = 0x746d6669;
const XIAMI_EXPECTED_PADDING = 0xfefefefe;
const u8Sub = (a: number, b: number) => {
if (a > b) {
@ -23,29 +24,28 @@ const u8Sub = (a: number, b: number) => {
};
export class XiamiCrypto implements CryptoBase {
hasSignature(): boolean {
return true;
cryptoName = 'Xiami';
checkByDecryptHeader = false;
async checkBySignature(buffer: ArrayBuffer): Promise<boolean> {
const header = new DataView(buffer);
return header.getUint32(0x00, true) === XIAMI_FILE_MAGIC && header.getUint32(0x08, true) === XIAMI_EXPECTED_PADDING;
}
async isSupported(blob: Blob): Promise<boolean> {
const headerBuffer = await blob.slice(0, 0x10).arrayBuffer();
const header = new Uint8Array(headerBuffer);
return (
header.slice(0x00, 0x04).every((b, i) => b === XIAMI_FILE_MAGIC[i]) &&
header.slice(0x08, 0x0c).every((b, i) => b === XIAMI_EXPECTED_PADDING[i])
);
}
async decrypt(blob: Blob): Promise<Blob> {
const headerBuffer = await blob.slice(0, 0x10).arrayBuffer();
async decrypt(src: ArrayBuffer): Promise<ArrayBuffer> {
const headerBuffer = src.slice(0, 0x10);
const header = new Uint8Array(headerBuffer);
const key = u8Sub(header[0x0f], 1);
const plainTextSize = header[0x0c] | (header[0x0d] << 8) | (header[0x0e] << 16);
const decrypted = new Uint8Array(await blob.slice(0x10).arrayBuffer());
const decrypted = new Uint8Array(src.slice(0x10));
for (let i = decrypted.byteLength - 1; i >= plainTextSize; i--) {
decrypted[i] = u8Sub(key, decrypted[i]);
}
return new Blob([decrypted]);
return decrypted;
}
public static make() {
return new XiamiCrypto();
}
}

View File

@ -0,0 +1,17 @@
export interface XimalayaAndroidKey {
contentKey: string;
init: number;
step: number;
}
export const XimalayaX2MKey: XimalayaAndroidKey = {
contentKey: 'xmly',
init: 0.615243,
step: 3.837465,
};
export const XimalayaX3MKey: XimalayaAndroidKey = {
contentKey: '3989d111aad5613940f4fc44b639b292',
init: 0.726354,
step: 3.948576,
};

View File

@ -0,0 +1,29 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase.js';
import { XimalayaAndroidKey, XimalayaX2MKey, XimalayaX3MKey } from './xmly_android.key.js';
export class XimalayaAndroidCrypto implements CryptoBase {
cryptoName = 'Ximalaya/Android';
checkByDecryptHeader = true;
constructor(private key: XimalayaAndroidKey) {}
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
const { contentKey, init, step } = this.key;
return transformBlob(buffer, (p) => {
const transformer = p.make.XimalayaAndroid(init, step, contentKey);
if (!transformer) {
throw new Error('could not make xmly transformer, is key invalid?');
}
return transformer;
});
}
public static makeX2M() {
return new XimalayaAndroidCrypto(XimalayaX2MKey);
}
public static makeX3M() {
return new XimalayaAndroidCrypto(XimalayaX3MKey);
}
}

View File

@ -0,0 +1,17 @@
export interface DecryptCommandOptions {
fileName: string;
qmc2Key?: string;
kwm2key?: string;
qingTingAndroidKey?: string;
}
export interface DecryptCommandPayload {
id: string;
blobURI: string;
options: DecryptCommandOptions;
}
export interface FetchMusicExNamePayload {
id: string;
blobURI: string;
}

View File

@ -0,0 +1,17 @@
export enum DecryptErrorType {
UNSUPPORTED_FILE = 'UNSUPPORTED_FILE',
UNKNOWN = 'UNKNOWN',
}
export class DecryptError extends Error {
code = DecryptErrorType.UNKNOWN;
toJSON() {
const { name, message, stack, code } = this;
return { name, message, stack, code };
}
}
export class UnsupportedSourceFile extends DecryptError {
code = DecryptErrorType.UNSUPPORTED_FILE;
}

View File

@ -0,0 +1,2 @@
export const toArrayBuffer = async (src: Blob | ArrayBuffer) => (src instanceof Blob ? await src.arrayBuffer() : src);
export const toBlob = (src: Blob | ArrayBuffer) => (src instanceof Blob ? src : new Blob([src]));

View File

@ -0,0 +1,5 @@
import type { Parakeet } from '@jixun/libparakeet';
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from '../crypto/qmc/qmc_v2.key';
export const makeQMCv2KeyCrypto = (p: Parakeet) => p.make.QMCv2KeyCrypto(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
export const makeQMCv2FooterParser = (p: Parakeet) => p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);

View File

@ -1,31 +1,38 @@
import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@jixun/libparakeet';
import { toArrayBuffer } from './buffer';
import { UnsupportedSourceFile } from './DecryptError';
export async function transformBlob(
blob: Blob,
blob: Blob | ArrayBuffer,
transformerFactory: (p: Parakeet) => Transformer | Promise<Transformer>,
parakeet?: Parakeet
{ cleanup, parakeet }: { cleanup?: () => void; parakeet?: Parakeet } = {}
) {
const cleanup: (() => void)[] = [];
const registeredCleanupFns: (() => void)[] = [];
if (cleanup) {
registeredCleanupFns.push(cleanup);
}
try {
const mod = parakeet ?? (await fetchParakeet());
const transformer = await transformerFactory(mod);
cleanup.push(() => transformer.delete());
registeredCleanupFns.push(() => transformer.delete());
const reader = mod.make.Reader(await blob.arrayBuffer());
cleanup.push(() => reader.delete());
const reader = mod.make.Reader(await toArrayBuffer(blob));
registeredCleanupFns.push(() => reader.delete());
const sink = mod.make.WriterSink();
const writer = sink.getWriter();
cleanup.push(() => writer.delete());
registeredCleanupFns.push(() => writer.delete());
const result = transformer.Transform(writer, reader);
if (result !== TransformResult.OK) {
throw new Error(`transform failed with error: ${TransformResult[result]} (${result})`);
if (result === TransformResult.ERROR_INVALID_FORMAT) {
throw new UnsupportedSourceFile(`transformer<${transformer.Name}> does not recognize this file`);
} else if (result !== TransformResult.OK) {
throw new Error(`transformer<${transformer.Name}> failed with error: ${TransformResult[result]} (${result})`);
}
return sink.collectBlob();
} finally {
cleanup.forEach((clean) => clean());
registeredCleanupFns.forEach((cleanup) => cleanup());
}
}

View File

@ -0,0 +1,10 @@
export const utf8Encoder = new TextEncoder();
export const utf8Decoder = new TextDecoder('utf-8');
export function stringToUTF8Bytes(str: string) {
return utf8Encoder.encode(str);
}
export function bytesToUTF8String(str: BufferSource) {
return utf8Decoder.decode(str);
}

View File

@ -1,49 +0,0 @@
import { fetchParakeet } from '@jixun/libparakeet';
import { CryptoFactory } from '../crypto/CryptoBase';
import { XiamiCrypto } from '../crypto/xiami/xiami';
import { QMC1Crypto } from '../crypto/qmc/qmc_v1';
import { QMC2Crypto } from '../crypto/qmc/qmc_v2';
// Use first 4MiB of the file to perform check.
const TEST_FILE_HEADER_LEN = 1024 * 1024 * 4;
const decryptorFactories: CryptoFactory[] = [
// Xiami (*.xm)
() => new XiamiCrypto(),
// QMCv1 (*.qmcflac)
() => new QMC1Crypto(),
// QMCv2 (*.mflac)
() => new QMC2Crypto(),
];
export const workerDecryptHandler = async (blobURI: string) => {
const blob = await fetch(blobURI).then((r) => r.blob());
const parakeet = await fetchParakeet();
for (const factory of decryptorFactories) {
const decryptor = factory();
if (await decryptor.isSupported(blob)) {
try {
const decryptedBlob = await decryptor.decrypt(blob);
// Check if we had a successful decryption
const header = await decryptedBlob.slice(0, TEST_FILE_HEADER_LEN).arrayBuffer();
const audioExt = parakeet.detectAudioExtension(header);
if (!decryptor.hasSignature() && audioExt === 'bin') {
// skip this decryptor result
continue;
}
return { decrypted: URL.createObjectURL(decryptedBlob), ext: audioExt };
} catch (error) {
console.error('decrypt failed: ', error);
continue;
}
}
}
throw new Error('could not decrypt file: no working decryptor found');
};

View File

@ -3,13 +3,12 @@ import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
import { getSDKVersion } from '@jixun/libparakeet';
import { workerDecryptHandler } from './worker-handler/decrypt';
import { workerDecryptHandler } from './worker/handler/decrypt';
import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser';
const bus = new WorkerServerBus();
onmessage = bus.onmessage;
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, async () => {
return getSDKVersion();
});
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName);
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getSDKVersion);

View File

@ -0,0 +1,105 @@
import { Parakeet, fetchParakeet } from '@jixun/libparakeet';
import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils';
import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types';
import { allCryptoFactories } from '../../crypto/CryptoFactory';
import { toArrayBuffer, toBlob } from '~/decrypt-worker/util/buffer';
import { CryptoBase, CryptoFactory } from '~/decrypt-worker/crypto/CryptoBase';
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError';
// Use first 4MiB of the file to perform check.
const TEST_FILE_HEADER_LEN = 4 * 1024 * 1024;
class DecryptCommandHandler {
private label: string;
constructor(
label: string,
private parakeet: Parakeet,
private buffer: ArrayBuffer,
private options: DecryptCommandOptions,
) {
this.label = `DecryptCommandHandler(${label})`;
}
log<R>(label: string, fn: () => R): R {
return timedLogger(`${this.label}: ${label}`, fn);
}
async decrypt(factories: CryptoFactory[]) {
for (const factory of factories) {
const decryptor = factory();
try {
const result = await this.decryptFile(decryptor);
if (result === null) {
continue;
}
return result;
} catch (error) {
if (error instanceof UnsupportedSourceFile) {
console.debug('WARN: decryptor does not recognize source file, wrong crypto?', error);
} else {
console.error('decrypt failed with unknown error: ', error);
}
}
}
throw new UnsupportedSourceFile('could not decrypt file: no working decryptor found');
}
async decryptFile(crypto: CryptoBase) {
if (crypto.checkBySignature && !(await crypto.checkBySignature(this.buffer, this.options))) {
return null;
}
if (crypto.checkByDecryptHeader && !(await this.acceptByDecryptFileHeader(crypto))) {
return null;
}
const decrypted = await this.log(`decrypt (${crypto.cryptoName})`, () => crypto.decrypt(this.buffer, this.options));
// Check if we had a successful decryption
let audioExt = crypto.overrideExtension ?? (await this.detectAudioExtension(decrypted));
if (crypto.checkByDecryptHeader && audioExt === 'bin') {
return null;
}
if (audioExt.toLowerCase() === 'mp4') {
audioExt = 'm4a';
}
return { decrypted: URL.createObjectURL(toBlob(decrypted)), ext: audioExt };
}
async detectAudioExtension(data: Blob | ArrayBuffer): Promise<string> {
return this.log(`detect-audio-ext`, async () => {
const header = await toArrayBuffer(data.slice(0, TEST_FILE_HEADER_LEN));
return this.parakeet.detectAudioExtension(header);
});
}
async acceptByDecryptFileHeader(crypto: CryptoBase): Promise<boolean> {
// File too small, ignore.
if (this.buffer.byteLength <= TEST_FILE_HEADER_LEN) {
return true;
}
// Check by decrypt max first 8MiB
const decryptedBuffer = await this.log(`${crypto.cryptoName}/decrypt-header-test`, async () =>
toArrayBuffer(await crypto.decrypt(this.buffer.slice(0, TEST_FILE_HEADER_LEN), this.options)),
);
return this.parakeet.detectAudioExtension(decryptedBuffer) !== 'bin';
}
}
export const workerDecryptHandler = async ({ id, blobURI, options }: DecryptCommandPayload) => {
const label = `decrypt(${id})`;
return withTimeGroupedLogs(label, async () => {
const parakeet = await timedLogger(`${label}/init`, fetchParakeet);
const blob = await timedLogger(`${label}/fetch-src`, async () => fetch(blobURI).then((r) => r.blob()));
const buffer = await timedLogger(`${label}/read-src`, async () => blob.arrayBuffer());
const handler = new DecryptCommandHandler(id, parakeet, buffer, options);
return handler.decrypt(allCryptoFactories);
});
};

View File

@ -0,0 +1,30 @@
import { fetchParakeet, FooterParserState } from '@jixun/libparakeet';
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types';
import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto';
import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils';
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
const label = `decrypt(${id})`;
return withTimeGroupedLogs(label, async () => {
const parakeet = await timedLogger(`${label}/init`, fetchParakeet);
const blob = await timedLogger(`${label}/fetch-src`, async () =>
fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()),
);
const buffer = await timedLogger(`${label}/read-src`, async () => {
// Firefox: the range header does not work...?
const blobBuffer = await blob.arrayBuffer();
if (blobBuffer.byteLength > 1024) {
return blobBuffer.slice(-1024);
}
return blobBuffer;
});
const parsed = makeQMCv2FooterParser(parakeet).parse(buffer);
if (parsed.state === FooterParserState.OK) {
return parsed.mediaName;
}
return null;
});
};

5
src/dummy.mjs Normal file
View File

@ -0,0 +1,5 @@
// This is a dummy module for vite/rollup to resolve.
export function createRequire() {
import('radash'); // we need to import something, so vite don't complain on build
throw new Error('this is a dummy module. Do not use');
}

55
src/faq/KuwoFAQ.tsx Normal file
View File

@ -0,0 +1,55 @@
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text, chakra } from '@chakra-ui/react';
import { Header4 } from '~/components/HelpText/Header4';
import { VQuote } from '~/components/HelpText/VQuote';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
import { HiWord } from '~/components/HelpText/HiWord';
import { KWMv2AllInstructions } from '~/features/settings/panels/KWMv2/KWMv2AllInstructions';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
export function KuwoFAQ() {
return (
<>
<Header4></Header4>
<List spacing={2}>
<ListItem>
<SegmentTryOfficialPlayer />
</ListItem>
<ListItem>
<Text>
<chakra.strong>2</chakra.strong>
</Text>
<Text>
<HiWord></HiWord>
<VQuote>
<strong></strong>
</VQuote>
<VQuote>
<strong></strong>
</VQuote>
{'音质的音乐文件采用新版加密。'}
</Text>
<Text></Text>
<Text>PC平台暂未推出使用新版加密的音质</Text>
<Container p={2}>
<Alert status="warning" borderRadius={5}>
<AlertIcon />
<Flex flexDir="column">
<Text> root </Text>
<Text>
<strong></strong>
</Text>
<Text>
<strong></strong>使使
</Text>
</Flex>
</Alert>
</Container>
<SegmentKeyImportInstructions tab="KWMv2 密钥" clientInstructions={<KWMv2AllInstructions />} />
</ListItem>
</List>
</>
);
}

139
src/faq/OtherFAQ.tsx Normal file
View File

@ -0,0 +1,139 @@
import { Alert, AlertIcon, Code, Container, Flex, Img, ListItem, Text, UnorderedList } from '@chakra-ui/react';
import { ExtLink } from '~/components/ExtLink';
import { Header4 } from '~/components/HelpText/Header4';
import { VQuote } from '~/components/HelpText/VQuote';
import { ProjectIssue } from '~/components/ProjectIssue';
import LdPlayerSettingsScreen from './assets/ld_settings_misc.webp';
export function OtherFAQ() {
return (
<>
<Header4></Header4>
<Text></Text>
<Text>使</Text>
<Header4></Header4>
<Text>
{'暂时没有实现,不过你可以在 '}
<ProjectIssue id={34} title="[UI] 全部下载功能" />
{' 以及 '}
<ProjectIssue id={43} title="批量下载" />
{' 追踪该问题。'}
</Text>
<Header4>安卓: 浏览器支持说明</Header4>
<Text> 使 Chrome Firefox </Text>
<Text></Text>
<UnorderedList>
<ListItem>Via </ListItem>
<ListItem></ListItem>
<ListItem>UC </ListItem>
</UnorderedList>
<Text></Text>
<UnorderedList>
<ListItem></ListItem>
<ListItem></ListItem>
<ListItem></ListItem>
</UnorderedList>
<Header4>安卓: root </Header4>
<Text>
root 使
使 NFC
</Text>
<Text>使</Text>
<Text>
root 广 Windows 11
<ExtLink href="https://learn.microsoft.com/zh-cn/windows/android/wsa/">
<ruby>
Android Windows (WSA)
<rp> (</rp>
<rt>
<code>Windows Subsystem for Android</code>
</rt>
<rp>)</rp>
</ruby>
</ExtLink>
</Text>
<Container p={2}>
<Alert status="warning" borderRadius={5}>
<AlertIcon />
<Flex flexDir="column">
<Text>
<strong></strong>使<strong></strong>
{';使用前请自行评估风险。'}
</Text>
</Flex>
</Alert>
</Container>
<UnorderedList>
<ListItem>
<Text>
{'WSA 可以参考 '}
<ExtLink href="https://github.com/LSPosed/MagiskOnWSALocal">MagiskOnWSALocal</ExtLink>
{' 的说明操作。'}
</Text>
</ListItem>
<ListItem>
<Text>
<VQuote></VQuote> <VQuote></VQuote> root
</Text>
<Img borderRadius={5} border="1px solid #ccc" src={LdPlayerSettingsScreen}></Img>
</ListItem>
</UnorderedList>
<Header4></Header4>
<UnorderedList>
<ListItem>
<Text>
<ExtLink href="https://github.com/CarlGao4/um-react-electron">
<strong>
<Code>um-react-electron</Code>
</strong>
</ExtLink>
Electron WindowsLinux Mac
</Text>
<UnorderedList>
<ListItem>
<Text>
<ExtLink href="https://github.com/CarlGao4/um-react-electron/releases/latest">GitHub </ExtLink>
</Text>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Text>
<ExtLink href="https://git.unlock-music.dev/um/um-react-wry">
<strong>
<Code>um-react-wry</Code>
</strong>
</ExtLink>
: 使 WRY Win64
<ExtLink href="https://go.microsoft.com/fwlink/p/?LinkId=2124703"> Edge WebView2 </ExtLink>
{'Win10+ 操作系统自带)'}
</Text>
<UnorderedList>
<ListItem>
<Text>
<ExtLink href="https://git.unlock-music.dev/um/um-react/releases/latest"></ExtLink>
{' | 寻找文件名为 '}
<Code>um-react-win64-</Code>
</Text>
</ListItem>
</UnorderedList>
</ListItem>
</UnorderedList>
<Header4></Header4>
<Text>
{'欢迎进入 '}
<ExtLink href={'https://t.me/unlock_music_chat'}>Telegram - </ExtLink>
{' 一起探讨。'}
</Text>
</>
);
}

63
src/faq/QQMusicFAQ.tsx Normal file
View File

@ -0,0 +1,63 @@
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text, UnorderedList, chakra } from '@chakra-ui/react';
import { Header4 } from '~/components/HelpText/Header4';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
import { QMCv2QQMusicAllInstructions } from '~/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
import { ExtLink } from '~/components/ExtLink';
export function QQMusicFAQ() {
return (
<>
<Header4></Header4>
<List spacing={2}>
<ListItem>
<SegmentTryOfficialPlayer />
</ListItem>
<ListItem>
<Text>
<chakra.strong>2</chakra.strong>
</Text>
<Text>
Windows 19.43
QQ Windows v19.43
</Text>
<UnorderedList pl={3}>
<ListItem>
<Text>
<ExtLink href="https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1943.exe">
<code>qq.com</code>
</ExtLink>
</Text>
</ListItem>
<ListItem>
<Text>
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1943.exe">
<code>Archive.org</code>
</ExtLink>
</Text>
</ListItem>
</UnorderedList>
<Container p={2}>
<Alert status="warning" borderRadius={5}>
<AlertIcon />
<Flex flexDir="column">
<Text>iOS </Text>
<Text>root</Text>
</Flex>
</Alert>
</Container>
<Container p={2} pt={0}>
<Alert status="info" borderRadius={5}>
<AlertIcon />
</Alert>
</Container>
<SegmentKeyImportInstructions tab="QMCv2 密钥" clientInstructions={<QMCv2QQMusicAllInstructions />} />
</ListItem>
</List>
</>
);
}

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