Compare commits
172 Commits
6c51c1a17d
...
main
Author | SHA1 | Date | |
---|---|---|---|
1846e5c959 | |||
c866d4909f | |||
bded04e40f | |||
b7cb485be2 | |||
3dcf064b48 | |||
437765f79f | |||
584fb24fdd | |||
2835ac08ec | |||
201e44fb38 | |||
bdb8d6da34 | |||
1429c5786e | |||
c311fb7f5d | |||
821d936b2c | |||
dfe69c8d1f | |||
a8acb25cd7 | |||
36774c165e | |||
573435efc9 | |||
18a22099d0 | |||
a788f48b67 | |||
6f405b5e06 | |||
0e0cf234b5 | |||
82bf23f468 | |||
60921faf4d | |||
0867a788c6 | |||
bb818fe820 | |||
820b98afed | |||
d567e9f136 | |||
|
06a83197f3 | ||
87fdf1300e | |||
44418486f6 | |||
0b4444d0d2 | |||
8475a5bc65 | |||
c5b25eb3b5 | |||
383957ecef | |||
ad314a8c42 | |||
b55f20e9f6 | |||
|
4f7d20f87b | ||
|
29cd3ed3d2 | ||
|
e9bab4565e | ||
|
dd96013e50 | ||
|
dc02e85cf4 | ||
|
855945353a | ||
|
a1b703a701 | ||
|
f6dd6935cd | ||
|
712d437795 | ||
e22e9ae698 | |||
|
a06f38a437 | ||
4d1295a0e6 | |||
178666a1fc | |||
d27dec439d | |||
cd0db8f382 | |||
9248e0f2a1 | |||
b788946b9e | |||
5bb3c7851e | |||
69313eaeef | |||
f97cb04159 | |||
e21456d38f | |||
79f1fd2b48 | |||
e462dd5f35 | |||
f2d7d6c2e3 | |||
e2b81f666d | |||
91706e3273 | |||
b47fb178a9 | |||
e80369703d | |||
117a916374 | |||
fcbd9fc1f0 | |||
9c03746db7 | |||
01efdbdfa4 | |||
64cb4774ea | |||
76b2676f38 | |||
b1d97ea85c | |||
deb6ceb52d | |||
b9b76ea987 | |||
a99d6485a3 | |||
fc33348fd6 | |||
b44d38989b | |||
1a12ebf17c | |||
ff869f088e | |||
6e8e8e8794 | |||
e82c634621 | |||
8c2c1da28c | |||
986e02f182 | |||
e51c7b81b1 | |||
c2556d7cc5 | |||
fd5c0957a2 | |||
04dd96684e | |||
fee35d4a95 | |||
2872ceb3bb | |||
46415e9eea | |||
c3b75c8fea | |||
a5de3829db | |||
21d6b3d90f | |||
ccb4458429 | |||
81136dd4f8 | |||
f6af50077a | |||
914020563e | |||
de14ccb0b3 | |||
97cd7afc44 | |||
176bec4a44 | |||
99a2c4aaa5 | |||
c63718f7d1 | |||
06530ad092 | |||
|
99926a752e | ||
|
3d8c0c8304 | ||
|
c9d684491b | ||
|
ded9621bfa | ||
|
66bbed3d78 | ||
|
8ccdf9a762 | ||
|
69b497a4ca | ||
|
8894b5379d | ||
|
c4177be7df | ||
|
edffe53495 | ||
|
152b695b10 | ||
a2210becc5 | |||
f6c34cd7ba | |||
0af8a0d714 | |||
|
2e0b24a6fc | ||
|
9edb12b008 | ||
|
96fccaeec7 | ||
62cd276c5d | |||
18a8dbfaa4 | |||
7fac4c60a5 | |||
556b69d6ef | |||
c336ac4d05 | |||
6d7f4512c4 | |||
de1c686231 | |||
290986e546 | |||
a69ed4f3ce | |||
85544cd09a | |||
0715eeea0b | |||
eaf457e6a0 | |||
d8b362efbe | |||
8673adfda6 | |||
37da3318c8 | |||
2855f72c03 | |||
fd54617c5c | |||
c895c5c069 | |||
1e7116a3a9 | |||
|
9add76c060 | ||
|
f7a215103a | ||
|
cd5fba5e4e | ||
|
76dd78130a | ||
|
19486d4d34 | ||
|
8ce76fa7dc | ||
|
632651b371 | ||
|
979168b68f | ||
|
1ab05bb509 | ||
|
910b00529e | ||
|
23b096512e | ||
|
fe89710968 | ||
|
8c11f47aa4 | ||
|
769d3392f8 | ||
|
9c3e39502a | ||
|
5d8a726746 | ||
|
e4c574465c | ||
|
8fb9dc029a | ||
|
8151c79652 | ||
|
aa6476bb2c | ||
|
0f6dc53e54 | ||
|
58dfeee960 | ||
|
b4068e2edb | ||
cd3f4d8c22 | |||
522f4e9f67 | |||
439003f021 | |||
d6aa88f371 | |||
fb33e80484 | |||
d199647308 | |||
733c1721ed | |||
dbb5472f96 | |||
1111aaf3b7 | |||
f80cf29657 | |||
0a52d2a20b |
25
.drone.yml
Normal file
25
.drone.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: node:16.18-bullseye
|
||||||
|
commands:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y jq zip
|
||||||
|
- npm ci
|
||||||
|
- npm run test
|
||||||
|
- ./scripts/build-and-package.sh legacy
|
||||||
|
- ./scripts/build-and-package.sh extension
|
||||||
|
- ./scripts/build-and-package.sh modern
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
image: node:16.18-bullseye
|
||||||
|
environment:
|
||||||
|
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
||||||
|
GITEA_API_KEY:
|
||||||
|
from_secret: GITEA_API_KEY
|
||||||
|
commands:
|
||||||
|
- ./scripts/upload-packages.sh
|
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug报告
|
|
||||||
about: 报告Bug以帮助改进程序
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
* 请按照此模板填写,否则可能立即被关闭
|
|
||||||
|
|
||||||
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
|
|
||||||
- [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
|
|
||||||
|
|
||||||
|
|
||||||
**Bug描述**
|
|
||||||
|
|
||||||
简要地复述你遇到的Bug
|
|
||||||
|
|
||||||
**复现方法**
|
|
||||||
|
|
||||||
描述复现方法,必要时请提供样本文件
|
|
||||||
|
|
||||||
**程序截图或者Console报错信息**
|
|
||||||
|
|
||||||
如果可以请提供二者之一
|
|
||||||
|
|
||||||
|
|
||||||
**环境信息:**
|
|
||||||
|
|
||||||
- 操作系统和浏览器:
|
|
||||||
- 程序版本:
|
|
||||||
- 获取音乐文件所使用的客户端及其版本信息:
|
|
||||||
|
|
||||||
|
|
||||||
**附加信息**
|
|
||||||
|
|
||||||
其他能够帮助确认问题的信息
|
|
||||||
|
|
26
.github/ISSUE_TEMPLATE/new-feature.md
vendored
26
.github/ISSUE_TEMPLATE/new-feature.md
vendored
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
name: 新功能
|
|
||||||
about: 对于程序新的想法或建议
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- 请按照此模板填写,否则可能立即被关闭
|
|
||||||
|
|
||||||
**背景和说明**
|
|
||||||
|
|
||||||
简要说明产生此想法的背景和此想法的具体内容
|
|
||||||
|
|
||||||
|
|
||||||
**实现途径**
|
|
||||||
|
|
||||||
- 如果没有设计方案,请简要描述实现思路
|
|
||||||
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者Telegram进行讨论
|
|
||||||
|
|
||||||
|
|
||||||
**附加信息**
|
|
||||||
|
|
||||||
更多你想要表达的内容
|
|
||||||
|
|
84
.github/workflows/build.yml
vendored
84
.github/workflows/build.yml
vendored
@ -1,84 +0,0 @@
|
|||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
|
||||||
|
|
||||||
name: Test Build
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/*"
|
|
||||||
- "**/*.js"
|
|
||||||
- "**/*.ts"
|
|
||||||
- "**/*.vue"
|
|
||||||
- "public/**/*"
|
|
||||||
- "package-lock.json"
|
|
||||||
- "package.json"
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
types: [ opened, synchronize, reopened ]
|
|
||||||
paths:
|
|
||||||
- "**/*.js"
|
|
||||||
- "**/*.ts"
|
|
||||||
- "**/*.vue"
|
|
||||||
- "public/**/*"
|
|
||||||
- "package-lock.json"
|
|
||||||
- "package.json"
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: npm ci
|
|
||||||
# note: forks can not access to GITHUB_TOKEN for coverage update.
|
|
||||||
# instead, we just ran the test in this case.
|
|
||||||
- name: Test only
|
|
||||||
if: github.event_name != 'push'
|
|
||||||
run: npm test
|
|
||||||
- name: Test + Publish Coverage
|
|
||||||
uses: ArtiomTr/jest-coverage-report-action@v2.0-rc.6
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
annotations: none
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
build: [ legacy, modern ]
|
|
||||||
include:
|
|
||||||
- build: legacy
|
|
||||||
BUILD_ARGS: ""
|
|
||||||
BUILD_EXTENSION: true
|
|
||||||
- build: modern
|
|
||||||
BUILD_ARGS: "-- --modern"
|
|
||||||
BUILD_EXTENSION: false
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build ${{ matrix.BUILD_ARGS }}
|
|
||||||
|
|
||||||
- name: Publish artifact
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.build }}
|
|
||||||
path: ./dist
|
|
||||||
|
|
||||||
- name: Build Extension
|
|
||||||
if: ${{ matrix.BUILD_EXTENSION }}
|
|
||||||
run: npm run make-extension
|
|
||||||
|
|
||||||
- name: Publish artifact - Extension
|
|
||||||
if: ${{ matrix.BUILD_EXTENSION }}
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: extension
|
|
||||||
path: ./dist
|
|
65
.github/workflows/post-release.yml
vendored
65
.github/workflows/post-release.yml
vendored
@ -1,65 +0,0 @@
|
|||||||
name: Post Release
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
jobs:
|
|
||||||
release-docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup vars
|
|
||||||
id: vars
|
|
||||||
env:
|
|
||||||
RELEASE_REF: ${{ github.ref }}
|
|
||||||
run: echo "::set-output name=tag::${RELEASE_REF#refs/tags/}"
|
|
||||||
|
|
||||||
- name: Download release content
|
|
||||||
run: |
|
|
||||||
echo "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
|
|
||||||
wget -O modern.tar.gz "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
|
|
||||||
mkdir ./dist
|
|
||||||
tar zxf modern.tar.gz -C ./dist
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build docker and push (on modern)
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/386
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
ix64/unlock-music:latest
|
|
||||||
ix64/unlock-music:${{ steps.vars.outputs.tag }}
|
|
||||||
|
|
||||||
gh-pages:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Setup vars
|
|
||||||
id: vars
|
|
||||||
env:
|
|
||||||
RELEASE_REF: ${{ github.ref }}
|
|
||||||
run: echo "::set-output name=tag::${RELEASE_REF#refs/tags/}"
|
|
||||||
|
|
||||||
- name: Download release content
|
|
||||||
run: |
|
|
||||||
echo "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
|
|
||||||
wget -O modern.tar.gz "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
|
|
||||||
mkdir ./dist
|
|
||||||
tar zxf modern.tar.gz -C ./dist
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: ./dist
|
|
||||||
|
|
126
.github/workflows/release-build.yml
vendored
126
.github/workflows/release-build.yml
vendored
@ -1,126 +0,0 @@
|
|||||||
name: Build Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build Legacy
|
|
||||||
env:
|
|
||||||
GZIP: "--best"
|
|
||||||
run: |
|
|
||||||
npm run build
|
|
||||||
tar -czf legacy.tar.gz -C ./dist .
|
|
||||||
cd dist
|
|
||||||
zip -rJ9 ../legacy.zip *
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Build Extension (on legacy)
|
|
||||||
env:
|
|
||||||
GZIP: "--best"
|
|
||||||
run: |
|
|
||||||
npm run make-extension
|
|
||||||
cd dist
|
|
||||||
zip -rJ9 ../extension.zip *
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Build Modern
|
|
||||||
env:
|
|
||||||
GZIP: "--best"
|
|
||||||
run: |
|
|
||||||
npm run build -- --modern
|
|
||||||
tar -czf modern.tar.gz -C ./dist .
|
|
||||||
cd dist
|
|
||||||
zip -rJ9 ../modern.zip *
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Checksum
|
|
||||||
run: sha256sum *.tar.gz *.zip > sha256sum.txt
|
|
||||||
|
|
||||||
- name: Get current time
|
|
||||||
id: date
|
|
||||||
run: echo "::set-output name=date::$(date +'%Y/%m/%d')"
|
|
||||||
|
|
||||||
- name: Create a Release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: "Build ${{ steps.date.outputs.date }}"
|
|
||||||
draft: true
|
|
||||||
|
|
||||||
- name: Upload Release Assets - legacy.tar.gz
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./legacy.tar.gz
|
|
||||||
asset_name: legacy.tar.gz
|
|
||||||
asset_content_type: application/gzip
|
|
||||||
|
|
||||||
- name: Upload Release Assets - legacy.zip
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./legacy.zip
|
|
||||||
asset_name: legacy.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Release Assets - modern.tar.gz
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./modern.tar.gz
|
|
||||||
asset_name: modern.tar.gz
|
|
||||||
asset_content_type: application/gzip
|
|
||||||
|
|
||||||
- name: Upload Release Assets - modern.zip
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./modern.zip
|
|
||||||
asset_name: modern.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Release Assets - extension.zip
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./extension.zip
|
|
||||||
asset_name: extension.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Release Assets - sha256sum.txt
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./sha256sum.txt
|
|
||||||
asset_name: sha256sum.txt
|
|
||||||
asset_content_type: text/plain
|
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
/build
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
@ -20,3 +21,14 @@ yarn-error.log*
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
/src/KgmWasm/build
|
||||||
|
/src/KgmWasm/*.js
|
||||||
|
/src/KgmWasm/*.wasm
|
||||||
|
/src/QmcWasm/build
|
||||||
|
/src/QmcWasm/*.js
|
||||||
|
/src/QmcWasm/*.wasm
|
||||||
|
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
/sha256sum.txt
|
||||||
|
76
.gitlab/ISSUE_TEMPLATE/bug-crypto-guided.yaml
Normal file
76
.gitlab/ISSUE_TEMPLATE/bug-crypto-guided.yaml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
name: 解码错误报告 (填表)
|
||||||
|
about: 遇到文件解码失败的问题请选择该项。
|
||||||
|
title: '[Bug/Crypto] '
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
- crypto
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: 错误描述
|
||||||
|
description: 请描述你所遇到的问题,以及你期待的行为。
|
||||||
|
placeholder: ''
|
||||||
|
value: ''
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Unlock Music 版本
|
||||||
|
description: |
|
||||||
|
能够重现错误的版本,版本号通常在页面底部。
|
||||||
|
如果不确定,请升级到最新版确认问题是否解决。
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- 1.10.5 (仓库最新)
|
||||||
|
- 1.10.3 (官方 DEMO)
|
||||||
|
- 其它(请在错误描述中指定)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: browsers
|
||||||
|
attributes:
|
||||||
|
label: 产生错误的浏览器
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- 火狐 / Firefox
|
||||||
|
- Chrome
|
||||||
|
- Safari
|
||||||
|
- 其它基于 Chromium 的浏览器 (Edge、Brave、Opera 等)
|
||||||
|
- type: dropdown
|
||||||
|
id: music-platform
|
||||||
|
attributes:
|
||||||
|
label: 音乐平台
|
||||||
|
description: |
|
||||||
|
如果需要报告多个平台的问题,请每个平台提交一个新的 Issue。
|
||||||
|
请注意:播放器缓存文件不属于该项目支持的文件类型。
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- 其它 (请在错误描述指定)
|
||||||
|
- QQ 音乐
|
||||||
|
- Joox (QQ 音乐海外版)
|
||||||
|
- 虾米音乐
|
||||||
|
- 网易云音乐
|
||||||
|
- 酷我音乐
|
||||||
|
- 酷狗音乐
|
||||||
|
- 喜马拉雅
|
||||||
|
- 咪咕 3D
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: 日志信息
|
||||||
|
description: 如果有,请提供浏览器开发者控制台(Console)的错误日志:
|
||||||
|
render: text
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 我已经阅读并确认下述内容
|
||||||
|
description: ''
|
||||||
|
options:
|
||||||
|
- label: 我已经检索过 Issue 列表,并确认这是一个为报告过的问题。
|
||||||
|
required: true
|
||||||
|
- label: 我有证据表明这是程序导致的问题(如不确认,可以通过 Telegram 讨论组 (https://t.me/unlock_music_chat) 进行讨论)
|
||||||
|
required: true
|
40
.gitlab/ISSUE_TEMPLATE/bug-report.md
Normal file
40
.gitlab/ISSUE_TEMPLATE/bug-report.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
name: "错误报告"
|
||||||
|
about: "报告 Bug 以帮助改进程序,非填表。"
|
||||||
|
title: "[BUG] "
|
||||||
|
labels:
|
||||||
|
|
||||||
|
- bug
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
* 请按照此模板填写,否则可能立即被关闭
|
||||||
|
|
||||||
|
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
|
||||||
|
- [x] 我有证据表明这是程序导致的问题(如不确认,可以通过 Telegram 讨论组 (https://t.me/unlock_music_chat) 进行讨论)
|
||||||
|
|
||||||
|
## Bug描述
|
||||||
|
|
||||||
|
简要地复述你遇到的Bug
|
||||||
|
|
||||||
|
## 复现方法
|
||||||
|
|
||||||
|
描述复现方法,必要时请提供样本文件
|
||||||
|
|
||||||
|
## 程序截图或浏览器开发者控制台(Console)的报错信息
|
||||||
|
|
||||||
|
如果可以请提供二者之一
|
||||||
|
|
||||||
|
## 环境信息
|
||||||
|
|
||||||
|
- 操作系统和浏览器:
|
||||||
|
- 程序版本:
|
||||||
|
- 网页版的地址(如果为非官方部署请注明):
|
||||||
|
|
||||||
|
注意:如果需要会员才能获取该资源,你可能也需要作为附件提交。
|
||||||
|
|
||||||
|
## 附加信息
|
||||||
|
|
||||||
|
如果有,请提供其他能够帮助确认问题的信息到下方:
|
||||||
|
|
29
.gitlab/ISSUE_TEMPLATE/new-feature.md
Normal file
29
.gitlab/ISSUE_TEMPLATE/new-feature.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
name: "新功能"
|
||||||
|
about: "对于程序新的想法或建议"
|
||||||
|
title: "[新功能] "
|
||||||
|
labels:
|
||||||
|
|
||||||
|
- enhancement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- ⚠ 请按照此模板填写,否则可能立即被关闭 -->
|
||||||
|
<!-- 提交前请使用【Preview】预览提交的更改 -->
|
||||||
|
|
||||||
|
## 背景和说明
|
||||||
|
|
||||||
|
<!-- 简要说明产生此想法的背景和此想法的具体内容 -->
|
||||||
|
|
||||||
|
|
||||||
|
## 实现途径
|
||||||
|
|
||||||
|
- 如果没有设计方案,请简要描述实现思路
|
||||||
|
- 如果你没有任何的实现思路,请通过 Telegram 讨论组 (https://t.me/unlock_music_chat) 进行讨论
|
||||||
|
|
||||||
|
|
||||||
|
## 附加信息
|
||||||
|
|
||||||
|
<!-- 更多你想要表达的内容 -->
|
||||||
|
|
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
@unlock-music:registry=https://git.unlock-music.dev/api/packages/um/npm/
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019-2021 MengYX
|
Copyright (c) 2019-2023 MengYX
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
60
README.md
60
README.md
@ -1,22 +1,20 @@
|
|||||||
# Unlock Music 音乐解锁
|
# Unlock Music 音乐解锁
|
||||||
|
|
||||||
|
[![Build Status](https://ci.unlock-music.dev/api/badges/um/web/status.svg)](https://ci.unlock-music.dev/um/web)
|
||||||
|
|
||||||
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
|
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
|
||||||
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循 [License][license]
|
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。
|
||||||
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli][repo_cli] 找到,大批量转换建议使用 CLI 版本。
|
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。
|
||||||
- 我们新建了 Telegram 群组 [`@unlock_music_chat`][tg_group] ,欢迎加入!
|
- 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入!
|
||||||
- [相关的其他项目][related_projects]
|
- CI 自动构建已经部署,可以在 [um-packages] 下载
|
||||||
|
|
||||||
![Test Build](https://github.com/unlock-music/unlock-music/workflows/Test%20Build/badge.svg)
|
> **WARNING**
|
||||||
![GitHub releases](https://img.shields.io/github/downloads/unlock-music/unlock-music/total)
|
> 在本站 fork 不会起到备份的作用,只会浪费服务器储存空间。如无必要请勿 fork 该仓库。
|
||||||
![Docker Pulls](https://img.shields.io/docker/pulls/ix64/unlock-music)
|
|
||||||
|
|
||||||
[license]: https://github.com/unlock-music/unlock-music/blob/master/LICENSE
|
[授权协议]: https://git.unlock-music.dev/um/web/src/branch/master/LICENSE
|
||||||
|
[unlock-music/cli]: https://git.unlock-music.dev/um/cli
|
||||||
[repo_cli]: https://github.com/unlock-music/cli
|
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
|
||||||
|
[um-packages]: https://git.unlock-music.dev/um/-/packages/generic/web-build/
|
||||||
[tg_group]: https://t.me/unlock_music_chat
|
|
||||||
|
|
||||||
[related_projects]: https://github.com/unlock-music/unlock-music/wiki/和UnlockMusic相关的项目
|
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
@ -30,11 +28,9 @@
|
|||||||
- [x] 网易云音乐格式 (.ncm)
|
- [x] 网易云音乐格式 (.ncm)
|
||||||
- [x] 虾米音乐格式 (.xm)
|
- [x] 虾米音乐格式 (.xm)
|
||||||
- [x] 酷我音乐格式 (.kwm)
|
- [x] 酷我音乐格式 (.kwm)
|
||||||
- [x] 酷狗音乐格式 (.kgm/.vpr) ([CLI 版本][kgm_cli])
|
- [x] 酷狗音乐格式 (.kgm/.vpr)
|
||||||
|
- [x] Android 版喜马拉雅文件格式 (.x2m/.x3m)
|
||||||
[kgm_cli]: https://github.com/unlock-music/unlock-music/wiki/其他音乐格式工具#酷狗音乐-kgmvpr解锁工具
|
- [x] 咪咕音乐格式 (.mg3d)
|
||||||
|
|
||||||
[joox_wiki]: https://github.com/unlock-music/joox-crypto/wiki/加密格式
|
|
||||||
|
|
||||||
### 其他特性
|
### 其他特性
|
||||||
|
|
||||||
@ -43,27 +39,17 @@
|
|||||||
- [x] 批量解锁
|
- [x] 批量解锁
|
||||||
- [x] 渐进式 Web 应用 (PWA)
|
- [x] 渐进式 Web 应用 (PWA)
|
||||||
- [x] 多线程
|
- [x] 多线程
|
||||||
- [x] 写入Meta和封面图片
|
- [x] 写入和编辑元信息与专辑封面
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
### 安装浏览器扩展
|
### 使用预构建版本
|
||||||
|
|
||||||
[![Chrome Web Store](https://storage.googleapis.com/chrome-gcs-uploader.appspot.com/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/gldlhhhmienbhlpkfanjpmffdjblmegd)
|
- 从 [Release] 或 [CI 构建][um-packages] 下载预构建的版本
|
||||||
[<img src="https://developer.microsoft.com/en-us/store/badges/images/Chinese_Simplified_get-it-from-MS.png" height="60" alt="Microsoft Edge Addons"/>](https://microsoftedge.microsoft.com/addons/detail/ggafoipegcmodfhakdkalpdpcdkiljmd)
|
- :warning: 本地使用请下载`legacy版本`(`modern版本`只能通过 **http(s)协议** 访问)
|
||||||
[![Firefox Browser Addons](https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/zh-CN/firefox/addon/unlock-music/)
|
|
||||||
|
|
||||||
### 使用已构建版本
|
|
||||||
|
|
||||||
- 从[GitHub Release](https://github.com/unlock-music/unlock-music/releases/latest)下载已构建的版本
|
|
||||||
- 本地使用请下载`legacy版本`(`modern版本`只能通过 **http(s)协议** 访问)
|
|
||||||
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**)
|
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**)
|
||||||
|
|
||||||
### 使用 Docker 镜像
|
[release]: https://git.unlock-music.dev/um/web/releases/latest
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自行构建
|
### 自行构建
|
||||||
|
|
||||||
@ -74,18 +60,20 @@ docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
|
|||||||
1. 获取项目源代码后安装相关依赖:
|
1. 获取项目源代码后安装相关依赖:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
npm install
|
||||||
npm ci
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 然后进行构建。编译后的文件保存到 dist 目录下:
|
2. 然后进行构建:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 构建后的产物可以在 `dist` 目录找到。
|
||||||
- 如果是用于开发,可以执行 `npm run serve`。
|
- 如果是用于开发,可以执行 `npm run serve`。
|
||||||
|
|
||||||
3. 如需构建浏览器扩展,build 完成后还需要执行:
|
3. 如需构建浏览器扩展,构建成功后还需要执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run make-extension
|
npm run make-extension
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 3,
|
||||||
|
"content_security_policy": {
|
||||||
|
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||||
|
},
|
||||||
"name": "音乐解锁",
|
"name": "音乐解锁",
|
||||||
"short_name": "音乐解锁",
|
"short_name": "音乐解锁",
|
||||||
"icons": {
|
"icons": {
|
||||||
"128": "./img/icons/msapplication-icon-144x144.png"
|
"16": "img/icons/favicon-16x16.png",
|
||||||
|
"32": "img/icons/favicon-32x32.png",
|
||||||
|
"192": "img/icons/android-chrome-192x192.png",
|
||||||
|
"512": "img/icons/android-chrome-512x512.png"
|
||||||
},
|
},
|
||||||
"description": "在任何设备上解锁已购的加密音乐!",
|
"description": "在任何设备上解锁已购的加密音乐!",
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage"],
|
||||||
"offline_enabled": true,
|
"offline_enabled": true,
|
||||||
"options_page": "./index.html",
|
"options_page": "index.html",
|
||||||
"homepage_url": "https://github.com/ix64/unlock-music",
|
"homepage_url": "https://git.unlock-music.dev/um/web",
|
||||||
"browser_action": {
|
"action": {
|
||||||
|
"default_icon": "img/icons/favicon-32x32.png",
|
||||||
"default_popup": "./popup.html"
|
"default_popup": "./popup.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
setupFilesAfterEnv: [
|
testPathIgnorePatterns: ['/build/', '/dist/', '/node_modules/'],
|
||||||
'./src/__test__/setup_jest.js'
|
setupFilesAfterEnv: ['./src/__test__/setup_jest.js'],
|
||||||
],
|
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@/(.*)': '<rootDir>/src/$1'
|
'@/(.*)': '<rootDir>/src/$1',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const src = "./src/extension/"
|
const src = __dirname + "/src/extension/"
|
||||||
const dst = "./dist"
|
const dst = __dirname + "/dist"
|
||||||
fs.readdirSync(src).forEach(file => {
|
fs.readdirSync(src).forEach(file => {
|
||||||
let srcPath = path.join(src, file)
|
let srcPath = path.join(src, file)
|
||||||
let dstPath = path.join(dst, file)
|
let dstPath = path.join(dst, file)
|
||||||
@ -9,10 +9,10 @@ fs.readdirSync(src).forEach(file => {
|
|||||||
console.log(`Copy: ${srcPath} => ${dstPath}`)
|
console.log(`Copy: ${srcPath} => ${dstPath}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const manifestRaw = fs.readFileSync("./extension-manifest.json", "utf-8")
|
const manifestRaw = fs.readFileSync(__dirname + "/extension-manifest.json", "utf-8")
|
||||||
const manifest = JSON.parse(manifestRaw)
|
const manifest = JSON.parse(manifestRaw)
|
||||||
|
|
||||||
const pkgRaw = fs.readFileSync("./package.json", "utf-8")
|
const pkgRaw = fs.readFileSync(__dirname + "/package.json", "utf-8")
|
||||||
const pkg = JSON.parse(pkgRaw)
|
const pkg = JSON.parse(pkgRaw)
|
||||||
|
|
||||||
verExt = pkg["version"]
|
verExt = pkg["version"]
|
||||||
@ -21,5 +21,5 @@ if (verExt.includes("-")) verExt = verExt.split("-")[0]
|
|||||||
manifest["version"] = `${verExt}.${pkg["ext_build"]}`
|
manifest["version"] = `${verExt}.${pkg["ext_build"]}`
|
||||||
manifest["version_name"] = pkg["version"]
|
manifest["version_name"] = pkg["version"]
|
||||||
|
|
||||||
fs.writeFileSync("./dist/manifest.json", JSON.stringify(manifest), "utf-8")
|
fs.writeFileSync(__dirname + "/dist/manifest.json", JSON.stringify(manifest), "utf-8")
|
||||||
console.log("Write: manifest.json")
|
console.log("Write: manifest.json")
|
||||||
|
92
package-lock.json
generated
92
package-lock.json
generated
@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "unlock-music",
|
"name": "unlock-music",
|
||||||
"version": "v1.10.0",
|
"version": "1.10.8",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "unlock-music",
|
"name": "unlock-music",
|
||||||
"version": "v1.10.0",
|
"version": "1.10.8",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.16.5",
|
"@babel/preset-typescript": "^7.16.5",
|
||||||
"@jixun/qmc2-crypto": "^0.0.6-R1",
|
"@unlock-music/joox-crypto": "^0.0.1",
|
||||||
"@unlock-music/joox-crypto": "^0.0.1-R5",
|
"@xhacker/kgmwasm": "^1.0.0",
|
||||||
|
"@xhacker/qmcwasm": "^1.0.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
@ -2986,11 +2987,6 @@
|
|||||||
"regenerator-runtime": "^0.13.3"
|
"regenerator-runtime": "^0.13.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jixun/qmc2-crypto": {
|
|
||||||
"version": "0.0.6-R1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.6-R1.tgz",
|
|
||||||
"integrity": "sha512-G7oa28/tGozJIIkF2DS7RWewoDsKrmGM5JgthzCfB6P1psfCjpjwH21RhnY9RzNlfdGZBqyWkAKwXMiUx/xhNA=="
|
|
||||||
},
|
|
||||||
"node_modules/@mrmlnc/readdir-enhanced": {
|
"node_modules/@mrmlnc/readdir-enhanced": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
|
||||||
@ -3488,11 +3484,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@unlock-music/joox-crypto": {
|
"node_modules/@unlock-music/joox-crypto": {
|
||||||
"version": "0.0.1-R5",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@unlock-music/joox-crypto/-/joox-crypto-0.0.1-R5.tgz",
|
"resolved": "https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fjoox-crypto/-/0.0.1/joox-crypto-0.0.1.tgz",
|
||||||
"integrity": "sha512-+FhGT4bjzfb1Q7dAwHps/XqbqXrRA6Qg7pkDPzyXfeRmQocAySQ/dekojxkaFBf7ZX5ToIAopwxkKZ5NFt5bFw==",
|
"integrity": "sha512-bj7UcA4/KSqK07PPmoRYJ+3s4h3P45RGUVAMspptMYXobhVkDlB1ArTYNlyIlrF/P0EMy7JkfEdOgUz0nD7EAg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.1.1"
|
"crypto-js": "^4.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"joox-decrypt": "joox-decrypt"
|
"joox-decrypt": "joox-decrypt"
|
||||||
@ -4186,6 +4183,16 @@
|
|||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xhacker/kgmwasm": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xhacker/kgmwasm/-/kgmwasm-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-LnBuEVRJQVyJGJTb0cPZxZDu7Qi4PqDhJLRaRJfG6pSUeZuIoglzHiysyd4XfNHobNnLxG8v1IiNPS/uWwoG0A=="
|
||||||
|
},
|
||||||
|
"node_modules/@xhacker/qmcwasm": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xhacker/qmcwasm/-/qmcwasm-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-oE6isNLmCDqIvxJV9KyDVlIzMISQzTj8o1ePWtQ+DhfXLI0hel/DwOIQ3icCikWnfwA/5SDs2hYw5BvrxdJ63g=="
|
||||||
|
},
|
||||||
"node_modules/@xtuc/ieee754": {
|
"node_modules/@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
@ -5705,13 +5712,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001298",
|
"version": "1.0.30001668",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
|
||||||
"integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==",
|
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
|
||||||
"funding": {
|
"funding": [
|
||||||
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/browserslist"
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"node_modules/case-sensitive-paths-webpack-plugin": {
|
"node_modules/case-sensitive-paths-webpack-plugin": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
@ -6727,9 +6744,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crypto-js": {
|
"node_modules/crypto-js": {
|
||||||
"version": "4.1.1",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
},
|
},
|
||||||
"node_modules/css-color-names": {
|
"node_modules/css-color-names": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
@ -23188,11 +23205,6 @@
|
|||||||
"regenerator-runtime": "^0.13.3"
|
"regenerator-runtime": "^0.13.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@jixun/qmc2-crypto": {
|
|
||||||
"version": "0.0.6-R1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.6-R1.tgz",
|
|
||||||
"integrity": "sha512-G7oa28/tGozJIIkF2DS7RWewoDsKrmGM5JgthzCfB6P1psfCjpjwH21RhnY9RzNlfdGZBqyWkAKwXMiUx/xhNA=="
|
|
||||||
},
|
|
||||||
"@mrmlnc/readdir-enhanced": {
|
"@mrmlnc/readdir-enhanced": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
|
||||||
@ -23656,11 +23668,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@unlock-music/joox-crypto": {
|
"@unlock-music/joox-crypto": {
|
||||||
"version": "0.0.1-R5",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@unlock-music/joox-crypto/-/joox-crypto-0.0.1-R5.tgz",
|
"resolved": "https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fjoox-crypto/-/0.0.1/joox-crypto-0.0.1.tgz",
|
||||||
"integrity": "sha512-+FhGT4bjzfb1Q7dAwHps/XqbqXrRA6Qg7pkDPzyXfeRmQocAySQ/dekojxkaFBf7ZX5ToIAopwxkKZ5NFt5bFw==",
|
"integrity": "sha512-bj7UcA4/KSqK07PPmoRYJ+3s4h3P45RGUVAMspptMYXobhVkDlB1ArTYNlyIlrF/P0EMy7JkfEdOgUz0nD7EAg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"crypto-js": "^4.1.1"
|
"crypto-js": "^4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": {
|
"@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
@ -24239,6 +24251,16 @@
|
|||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@xhacker/kgmwasm": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xhacker/kgmwasm/-/kgmwasm-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-LnBuEVRJQVyJGJTb0cPZxZDu7Qi4PqDhJLRaRJfG6pSUeZuIoglzHiysyd4XfNHobNnLxG8v1IiNPS/uWwoG0A=="
|
||||||
|
},
|
||||||
|
"@xhacker/qmcwasm": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xhacker/qmcwasm/-/qmcwasm-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-oE6isNLmCDqIvxJV9KyDVlIzMISQzTj8o1ePWtQ+DhfXLI0hel/DwOIQ3icCikWnfwA/5SDs2hYw5BvrxdJ63g=="
|
||||||
|
},
|
||||||
"@xtuc/ieee754": {
|
"@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
@ -25469,9 +25491,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001298",
|
"version": "1.0.30001668",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
|
||||||
"integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ=="
|
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="
|
||||||
},
|
},
|
||||||
"case-sensitive-paths-webpack-plugin": {
|
"case-sensitive-paths-webpack-plugin": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
@ -26290,9 +26312,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"crypto-js": {
|
"crypto-js": {
|
||||||
"version": "4.1.1",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
},
|
},
|
||||||
"css-color-names": {
|
"css-color-names": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
|
11
package.json
11
package.json
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "unlock-music",
|
"name": "unlock-music",
|
||||||
"version": "v1.10.0",
|
"version": "1.10.8",
|
||||||
"ext_build": 0,
|
"ext_build": 0,
|
||||||
"updateInfo": "重写QMC解锁,完全支持.mflac*/.mgg*; 支持JOOX解锁",
|
"updateInfo": "修正 joox 在远程获取 API 信息出错时不能正确回退到本地元信息获取的错误。",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Unlock encrypted music file in browser.",
|
"description": "Unlock encrypted music file in browser.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ix64/unlock-music"
|
"url": "https://git.unlock-music.dev/um/web"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -21,8 +21,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.16.5",
|
"@babel/preset-typescript": "^7.16.5",
|
||||||
"@jixun/qmc2-crypto": "^0.0.6-R1",
|
"@unlock-music/joox-crypto": "^0.0.1",
|
||||||
"@unlock-music/joox-crypto": "^0.0.1-R5",
|
"@xhacker/kgmwasm": "^1.0.0",
|
||||||
|
"@xhacker/qmcwasm": "^1.0.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
|
@ -1,32 +1,78 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta content="webkit" name="renderer">
|
<meta content="webkit" name="renderer" />
|
||||||
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
|
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
|
||||||
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
<meta content="width=device-width,initial-scale=1.0" name="viewport" />
|
||||||
<title>音乐解锁</title>
|
<title>音乐解锁</title>
|
||||||
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords" />
|
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords" />
|
||||||
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description" />
|
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description" />
|
||||||
<script src="./ixarea-stats.js"></script>
|
<style>
|
||||||
<!--@formatter:off-->
|
#loader {
|
||||||
<style>#loader{position:absolute;left:50%;top:50%;z-index:1010;margin:-75px 0 0 -75px;border:16px solid #f3f3f3;border-radius:50%;border-top:16px solid #1db1ff;width:120px;height:120px;animation:spin 2s linear infinite}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}#loader-mask{text-align:center;position:absolute;width:100%;height:100%;bottom:0;left:0;right:0;top:0;z-index:1009;background-color:rgba(242,246,252,.88)}@media (prefers-color-scheme:dark){#loader-mask{color:#fff;background-color:rgba(0,0,0,.85)}#loader-mask a{color:#ddd}#loader-mask a:hover{color:#1db1ff}}#loader-source{font-size:1.5rem}#loader-tips-timeout{font-size:1.2rem}</style>
|
position: absolute;
|
||||||
<!--@formatter:on-->
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 1010;
|
||||||
|
margin: -75px 0 0 -75px;
|
||||||
|
border: 16px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 16px solid #1db1ff;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#loader-mask {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1009;
|
||||||
|
background-color: rgba(242, 246, 252, 0.88);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#loader-mask {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
#loader-mask a {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
#loader-mask a:hover {
|
||||||
|
color: #1db1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#loader-source {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
#loader-tips-timeout {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
|
||||||
|
<body>
|
||||||
<div id="loader-mask">
|
<div id="loader-mask">
|
||||||
<div id="loader"></div>
|
<div id="loader"></div>
|
||||||
<noscript>
|
<noscript>
|
||||||
<h3 id="loader-js">请启用JavaScript</h3>
|
<h3 id="loader-js">请启用JavaScript</h3>
|
||||||
<img alt=""
|
|
||||||
src="https://stats.ixarea.com/ixarea-stats/report?rec=1&action_name=音乐解锁-NoJS&idsite=2"
|
|
||||||
style="border:0"/>
|
|
||||||
</noscript>
|
</noscript>
|
||||||
<h3 id="loader-source">请勿直接运行源代码!</h3>
|
<h3 id="loader-source">请勿直接运行源代码!</h3>
|
||||||
<div id="loader-tips-outdated" hidden>
|
<div id="loader-tips-outdated" hidden>
|
||||||
<h2>您可能在使用不受支持的<span style="color:#f00;">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
|
<h2>您可能在使用不受支持的<span style="color: #f00">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
|
||||||
<h3>如果您使用双核浏览器,您可以尝试切换到 <span style="color:#f00;">“极速模式”</span> 解决此问题。</h3>
|
<h3>如果您使用双核浏览器,您可以尝试切换到 <span style="color: #f00">“极速模式”</span> 解决此问题。</h3>
|
||||||
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
|
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
|
||||||
</div>
|
</div>
|
||||||
<h3 id="loader-tips-timeout" hidden>
|
<h3 id="loader-tips-timeout" hidden>
|
||||||
@ -34,7 +80,7 @@
|
|||||||
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
|
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
|
||||||
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
|
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
|
||||||
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
|
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
|
||||||
| <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
| <a href="https://git.unlock-music.dev/um/web/wiki/使用提示" target="_blank">使用提示</a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
var _paq = window._paq || [];
|
|
||||||
_paq.push(["setRequestMethod", "POST"], ["trackPageView"], ["enableLinkTracking"],
|
|
||||||
["setSiteId", "2"], ["setTrackerUrl", "https://stats.ixarea.com/ixarea-stats/report"]);
|
|
||||||
|
|
||||||
var tag = document.createElement('script');
|
|
||||||
tag.type = 'text/javascript';
|
|
||||||
tag.async = true;
|
|
||||||
tag.src = 'https://stats.ixarea.com/ixarea-stats.js';
|
|
||||||
var s = document.getElementsByTagName('script')[0];
|
|
||||||
s.parentNode.insertBefore(tag, s);
|
|
Binary file not shown.
29
scripts/build-and-package.sh
Executable file
29
scripts/build-and-package.sh
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
VERSION="$(jq -r ".version" <package.json)"
|
||||||
|
DIST_NAME="um-web.$1.v${VERSION}"
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
"modern") npm run build -- --modern ;;
|
||||||
|
"legacy") npm run build ;;
|
||||||
|
"extension") npm run make-extension ;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mv dist "${DIST_NAME}"
|
||||||
|
zip -rJ9 "${DIST_NAME}.zip" "${DIST_NAME}"
|
||||||
|
|
||||||
|
if [ "$1" = "legacy" ]; then
|
||||||
|
# For upcoming extension build
|
||||||
|
mv "${DIST_NAME}" dist
|
||||||
|
else
|
||||||
|
rm -rf "${DIST_NAME}"
|
||||||
|
fi
|
19
scripts/upload-packages.sh
Executable file
19
scripts/upload-packages.sh
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
if [ -z "$GITEA_API_KEY" ]; then
|
||||||
|
echo "GITEA_API_KEY is empty, skip upload."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
URL_BASE="$DRONE_GITEA_SERVER/api/packages/${DRONE_REPO_NAMESPACE}/generic/${DRONE_REPO_NAME}-build"
|
||||||
|
|
||||||
|
for ZIP_NAME in *.zip; do
|
||||||
|
UPLOAD_URL="${URL_BASE}/${DRONE_BUILD_NUMBER}/${ZIP_NAME}"
|
||||||
|
sha256sum "${ZIP_NAME}"
|
||||||
|
curl -sLifu "um-release-bot:$GITEA_API_KEY" -T "${ZIP_NAME}" "${UPLOAD_URL}"
|
||||||
|
echo "Uploaded to: ${UPLOAD_URL}"
|
||||||
|
done
|
22
src/App.vue
22
src/App.vue
@ -4,22 +4,22 @@
|
|||||||
<Home />
|
<Home />
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer id="app-footer">
|
<el-footer id="app-footer">
|
||||||
<el-row>
|
<div>
|
||||||
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }})
|
<a href="https://git.unlock-music.dev/um/web" target="_blank">音乐解锁</a>({{ version }})
|
||||||
:移除已购音乐的加密保护。
|
:移除已购音乐的加密保护。
|
||||||
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
<a href="https://git.unlock-music.dev/um/web/wiki/使用提示" target="_blank">使用提示</a>
|
||||||
</el-row>
|
</div>
|
||||||
<el-row>
|
<div>
|
||||||
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
|
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
|
||||||
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。
|
<a href="https://git.unlock-music.dev/um/web/src/branch/master/README.md" target="_blank">更多</a>。
|
||||||
</el-row>
|
</div>
|
||||||
<el-row>
|
<div>
|
||||||
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
||||||
<span>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span>
|
<span>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span>
|
||||||
音乐解锁使用
|
音乐解锁使用
|
||||||
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
|
<a href="https://git.unlock-music.dev/um/web/src/branch/master/LICENSE" target="_blank">MIT许可协议</a>
|
||||||
开放源代码
|
开放源代码
|
||||||
</el-row>
|
</div>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
@ -77,7 +77,7 @@ export default {
|
|||||||
<div class="update-title">最近更新</div>
|
<div class="update-title">最近更新</div>
|
||||||
<div class="update-content"> ${config.updateInfo} </div>
|
<div class="update-content"> ${config.updateInfo} </div>
|
||||||
</div>
|
</div>
|
||||||
<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>
|
<a target="_blank" href="https://git.unlock-music.dev/um/web/wiki/使用提示">使用提示</a>
|
||||||
</div>`,
|
</div>`,
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
duration: 10000,
|
duration: 10000,
|
||||||
|
@ -4,16 +4,6 @@ label {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.item-desc {
|
|
||||||
color: #aaa;
|
|
||||||
font-size: small;
|
|
||||||
display: block;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-top: 0.2em;
|
|
||||||
}
|
|
||||||
.item-desc a {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
form >>> input {
|
form >>> input {
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
@ -39,11 +29,13 @@ form >>> input {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p class="item-desc">
|
<p class="tip">
|
||||||
下载该加密文件的 JOOX 应用所记录的设备唯一识别码。
|
下载该加密文件的 JOOX 应用所记录的设备唯一识别码。
|
||||||
<br />
|
<br />
|
||||||
参见:
|
参见:
|
||||||
<a href="https://github.com/unlock-music/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID">
|
<a
|
||||||
|
href="https://git.unlock-music.dev/um/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID#%E5%89%8D%E8%A8%80"
|
||||||
|
>
|
||||||
获取设备 UUID · unlock-music/joox-crypto Wiki</a
|
获取设备 UUID · unlock-music/joox-crypto Wiki</a
|
||||||
>。
|
>。
|
||||||
</p>
|
</p>
|
||||||
|
163
src/component/EditDialog.vue
Normal file
163
src/component/EditDialog.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<style scoped>
|
||||||
|
* >>> .um-edit-dialog {
|
||||||
|
max-width: 90%;
|
||||||
|
width: 30em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog @close="cancel()" title="音乐标签编辑" :visible="show" custom-class="um-edit-dialog" center>
|
||||||
|
<el-form ref="form" status-icon :model="form" label-width="0">
|
||||||
|
<section>
|
||||||
|
<div class="music-cover">
|
||||||
|
<el-image v-show="!editPicture" :src="imgFile.url || picture">
|
||||||
|
<div slot="error" class="image-slot el-image__error">暂无封面</div>
|
||||||
|
</el-image>
|
||||||
|
<el-upload v-show="editPicture" :auto-upload="false" :on-change="addFile" :on-remove="rmvFile" :show-file-list="true" :limit="1" list-type="picture" action="" drag>
|
||||||
|
<i class="el-icon-upload" />
|
||||||
|
<div class="el-upload__text">将新图片拖到此处,或<em>点击选择</em><br />以替换自动匹配的图片</div>
|
||||||
|
<div slot="tip" class="el-upload__tip">
|
||||||
|
新拖到此处的图片将覆盖原始图片
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<i :class="{'el-icon-edit': !editPicture, 'el-icon-check': editPicture}"
|
||||||
|
@click="changeCover"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-item">
|
||||||
|
<div class="label">标题</div>
|
||||||
|
<div class="value" v-show="!editTitle">{{title}}</div>
|
||||||
|
<el-input class="input" size="small" v-show="editTitle" v-model="title"/>
|
||||||
|
<i :class="{'el-icon-edit': !editTitle, 'el-icon-check': editTitle}"
|
||||||
|
@click="editTitle = !editTitle"/>
|
||||||
|
</div>
|
||||||
|
<div class="edit-item">
|
||||||
|
<div class="label">艺术家</div>
|
||||||
|
<div class="value" v-show="!editArtist">{{artist}}</div>
|
||||||
|
<el-input class="input" size="small" v-show="editArtist" v-model="artist"/>
|
||||||
|
<i :class="{'el-icon-edit': !editArtist, 'el-icon-check': editArtist}"
|
||||||
|
@click="editArtist = !editArtist"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="edit-item">
|
||||||
|
<div class="label">专辑</div>
|
||||||
|
<div class="value" v-show="!editAlbum">{{album}}</div>
|
||||||
|
<el-input class="input" size="small" v-show="editAlbum" v-model="album"/>
|
||||||
|
<i :class="{'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum}"
|
||||||
|
@click="editAlbum = !editAlbum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="edit-item">
|
||||||
|
<div class="label">专辑艺术家</div>
|
||||||
|
<div class="value" v-show="!editAlbumartist">{{albumartist}}</div>
|
||||||
|
<el-input class="input" size="small" v-show="editAlbumartist" v-model="albumartist"/>
|
||||||
|
<i :class="{'el-icon-edit': !editAlbumartist, 'el-icon-check': editAlbumartist}"
|
||||||
|
@click="editAlbumartist = !editAlbumartist"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="edit-item">
|
||||||
|
<div class="label">风格</div>
|
||||||
|
<div class="value" v-show="!editGenre">{{genre}}</div>
|
||||||
|
<el-input class="input" size="small" v-show="editGenre" v-model="genre"/>
|
||||||
|
<i :class="{'el-icon-edit': !editGenre, 'el-icon-check': editGenre}"
|
||||||
|
@click="editGenre = !editGenre"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="tip">
|
||||||
|
为了节省您设备的资源,请在确定前充分检查,避免反复修改。<br />
|
||||||
|
直接关闭此对话框不会保留所作的更改。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</el-form>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="emitConfirm()">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Ruby from './Ruby';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Ruby,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: { type: Boolean, required: true },
|
||||||
|
picture: { type: String | undefined, required: true },
|
||||||
|
title: { type: String | undefined, required: true },
|
||||||
|
artist: { type: String | undefined, required: true },
|
||||||
|
album: { type: String | undefined, required: true },
|
||||||
|
albumartist: { type: String | undefined, required: true },
|
||||||
|
genre: { type: String | undefined, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
},
|
||||||
|
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
|
||||||
|
editPicture: false,
|
||||||
|
editTitle: false,
|
||||||
|
editArtist: false,
|
||||||
|
editAlbum: false,
|
||||||
|
editAlbumartist: false,
|
||||||
|
editGenre: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.refreshForm();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addFile(file) {
|
||||||
|
this.imgFile.tmpblob = file.raw;
|
||||||
|
},
|
||||||
|
rmvFile() {
|
||||||
|
this.imgFile.tmpblob = undefined;
|
||||||
|
},
|
||||||
|
changeCover() {
|
||||||
|
this.editPicture = !this.editPicture;
|
||||||
|
if (!this.editPicture && this.imgFile.tmpblob) {
|
||||||
|
this.imgFile.blob = this.imgFile.tmpblob;
|
||||||
|
if (this.imgFile.url) {
|
||||||
|
URL.revokeObjectURL(this.imgFile.url);
|
||||||
|
}
|
||||||
|
this.imgFile.url = URL.createObjectURL(this.imgFile.blob);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshForm() {
|
||||||
|
if (this.imgFile.url) {
|
||||||
|
URL.revokeObjectURL(this.imgFile.url);
|
||||||
|
}
|
||||||
|
this.imgFile = { tmpblob: undefined, blob: undefined, url: undefined };
|
||||||
|
this.editPicture = false;
|
||||||
|
this.editTitle = false;
|
||||||
|
this.editArtist = false;
|
||||||
|
this.editAlbum = false;
|
||||||
|
this.editAlbumartist = false;
|
||||||
|
this.editGenre = false;
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
this.refreshForm();
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
async emitConfirm() {
|
||||||
|
if (this.editPicture) {
|
||||||
|
this.changeCover();
|
||||||
|
}
|
||||||
|
if (this.imgFile.url) {
|
||||||
|
URL.revokeObjectURL(this.imgFile.url);
|
||||||
|
}
|
||||||
|
this.$emit('ok', {
|
||||||
|
picture: this.imgFile.blob,
|
||||||
|
title: this.title,
|
||||||
|
artist: this.artist,
|
||||||
|
album: this.album,
|
||||||
|
albumartist: this.albumartist,
|
||||||
|
genre: this.genre,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -9,7 +9,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="歌曲">
|
<el-table-column label="歌曲">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span>{{ scope.row.title }}</span>
|
<p>{{ scope.row.title }}</p>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="歌手">
|
<el-table-column label="歌手">
|
||||||
@ -27,6 +27,7 @@
|
|||||||
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
|
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button>
|
<el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button>
|
||||||
|
<el-button circle icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button>
|
||||||
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
|
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -55,6 +56,9 @@ export default {
|
|||||||
handleDownload(row) {
|
handleDownload(row) {
|
||||||
this.$emit('download', row);
|
this.$emit('download', row);
|
||||||
},
|
},
|
||||||
|
handleEdit(row) {
|
||||||
|
this.$emit('edit', row);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
BIN
src/decrypt/__test__/fixture/qmc_cache_expected.bin
Normal file
BIN
src/decrypt/__test__/fixture/qmc_cache_expected.bin
Normal file
Binary file not shown.
@ -22,7 +22,7 @@ describe('decrypt/joox', () => {
|
|||||||
album: 'unused',
|
album: 'unused',
|
||||||
blob: blob,
|
blob: blob,
|
||||||
artist: 'unused',
|
artist: 'unused',
|
||||||
imgUrl: 'https://github.com/unlock-music',
|
imgUrl: 'https://example.unlock-music.dev/',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Decrypt as Mg3dDecrypt } from '@/decrypt/mg3d';
|
||||||
import { Decrypt as NcmDecrypt } from '@/decrypt/ncm';
|
import { Decrypt as NcmDecrypt } from '@/decrypt/ncm';
|
||||||
import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache';
|
import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache';
|
||||||
import { Decrypt as XmDecrypt } from '@/decrypt/xm';
|
import { Decrypt as XmDecrypt } from '@/decrypt/xm';
|
||||||
@ -8,6 +9,7 @@ import { Decrypt as KwmDecrypt } from '@/decrypt/kwm';
|
|||||||
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
|
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
|
||||||
import { Decrypt as TmDecrypt } from '@/decrypt/tm';
|
import { Decrypt as TmDecrypt } from '@/decrypt/tm';
|
||||||
import { Decrypt as JooxDecrypt } from '@/decrypt/joox';
|
import { Decrypt as JooxDecrypt } from '@/decrypt/joox';
|
||||||
|
import { Decrypt as XimalayaDecrypt } from './ximalaya';
|
||||||
import { DecryptResult, FileInfo } from '@/decrypt/entity';
|
import { DecryptResult, FileInfo } from '@/decrypt/entity';
|
||||||
import { SplitFilename } from '@/decrypt/utils';
|
import { SplitFilename } from '@/decrypt/utils';
|
||||||
import { storage } from '@/utils/storage';
|
import { storage } from '@/utils/storage';
|
||||||
@ -22,6 +24,9 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
|
|||||||
const raw = SplitFilename(file.name);
|
const raw = SplitFilename(file.name);
|
||||||
let rt_data: DecryptResult;
|
let rt_data: DecryptResult;
|
||||||
switch (raw.ext) {
|
switch (raw.ext) {
|
||||||
|
case 'mg3d': // Migu Wav
|
||||||
|
rt_data = await Mg3dDecrypt(file.raw, raw.name);
|
||||||
|
break;
|
||||||
case 'ncm': // Netease Mp3/Flac
|
case 'ncm': // Netease Mp3/Flac
|
||||||
rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
|
rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
@ -45,9 +50,12 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
|
|||||||
case 'tm3': // QQ Music IOS Mp3
|
case 'tm3': // QQ Music IOS Mp3
|
||||||
rt_data = await RawDecrypt(file.raw, raw.name, 'mp3');
|
rt_data = await RawDecrypt(file.raw, raw.name, 'mp3');
|
||||||
break;
|
break;
|
||||||
|
case 'qmc0': //QQ Music Android Mp3
|
||||||
case 'qmc3': //QQ Music Android Mp3
|
case 'qmc3': //QQ Music Android Mp3
|
||||||
case 'qmc2': //QQ Music Android Ogg
|
case 'qmc2': //QQ Music Android Ogg
|
||||||
case 'qmc0': //QQ Music Android Mp3
|
case 'qmc4': //QQ Music Android Ogg
|
||||||
|
case 'qmc6': //QQ Music Android Ogg
|
||||||
|
case 'qmc8': //QQ Music Android Ogg
|
||||||
case 'qmcflac': //QQ Music Android Flac
|
case 'qmcflac': //QQ Music Android Flac
|
||||||
case 'qmcogg': //QQ Music Android Ogg
|
case 'qmcogg': //QQ Music Android Ogg
|
||||||
case 'tkm': //QQ Music Accompaniment M4a
|
case 'tkm': //QQ Music Accompaniment M4a
|
||||||
@ -63,9 +71,11 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
|
|||||||
case 'mggl': //QQ Music Mac
|
case 'mggl': //QQ Music Mac
|
||||||
case 'mflac': //QQ Music New Flac
|
case 'mflac': //QQ Music New Flac
|
||||||
case 'mflac0': //QQ Music New Flac
|
case 'mflac0': //QQ Music New Flac
|
||||||
|
case 'mflach': //QQ Music New Flac
|
||||||
case 'mgg': //QQ Music New Ogg
|
case 'mgg': //QQ Music New Ogg
|
||||||
case 'mgg1': //QQ Music New Ogg
|
case 'mgg1': //QQ Music New Ogg
|
||||||
case 'mgg0':
|
case 'mgg0':
|
||||||
|
case 'mmp4': // QMC MP4 Container w/ E-AC-3 JOC
|
||||||
case '666c6163': //QQ Music Weiyun Flac
|
case '666c6163': //QQ Music Weiyun Flac
|
||||||
case '6d7033': //QQ Music Weiyun Mp3
|
case '6d7033': //QQ Music Weiyun Mp3
|
||||||
case '6f6767': //QQ Music Weiyun Ogg
|
case '6f6767': //QQ Music Weiyun Ogg
|
||||||
@ -88,6 +98,12 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
|
|||||||
case 'ofl_en':
|
case 'ofl_en':
|
||||||
rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext);
|
rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
|
case 'x2m':
|
||||||
|
case 'x3m':
|
||||||
|
rt_data = await XimalayaDecrypt(file.raw, raw.name, raw.ext);
|
||||||
|
break;
|
||||||
|
case 'mflach': //QQ Music New Flac
|
||||||
|
throw '网页版无法解锁,请使用<a target="_blank" href="https://git.unlock-music.dev/um/cli">CLI版本</a>'
|
||||||
default:
|
default:
|
||||||
throw '不支持此文件格式';
|
throw '不支持此文件格式';
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@/decrypt/utils';
|
} from '@/decrypt/utils';
|
||||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||||
import { DecryptResult } from '@/decrypt/entity';
|
import { DecryptResult } from '@/decrypt/entity';
|
||||||
import config from '@/../package.json';
|
import { DecryptKgmWasm } from '@/decrypt/kgm_wasm';
|
||||||
|
|
||||||
//prettier-ignore
|
//prettier-ignore
|
||||||
const VprHeader = [
|
const VprHeader = [
|
||||||
@ -20,52 +20,30 @@ const KgmHeader = [
|
|||||||
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
|
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
|
||||||
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14
|
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14
|
||||||
]
|
]
|
||||||
//prettier-ignore
|
|
||||||
const VprMaskDiff = [
|
|
||||||
0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
|
|
||||||
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
|
|
||||||
0x00
|
|
||||||
]
|
|
||||||
|
|
||||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
const oriData = await GetArrayBuffer(file);
|
||||||
if (raw_ext === 'vpr') {
|
if (raw_ext === 'vpr') {
|
||||||
if (!BytesHasPrefix(oriData, VprHeader)) throw Error('Not a valid vpr file!');
|
if (!BytesHasPrefix(new Uint8Array(oriData), VprHeader)) throw Error('Not a valid vpr file!');
|
||||||
} else {
|
} else {
|
||||||
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error('Not a valid kgm(a) file!');
|
if (!BytesHasPrefix(new Uint8Array(oriData), KgmHeader)) throw Error('Not a valid kgm(a) file!');
|
||||||
|
}
|
||||||
|
let musicDecoded = new Uint8Array();
|
||||||
|
if (globalThis.WebAssembly) {
|
||||||
|
const kgmDecrypted = await DecryptKgmWasm(oriData, raw_ext);
|
||||||
|
if (kgmDecrypted.success) {
|
||||||
|
musicDecoded = kgmDecrypted.data;
|
||||||
|
console.log('kgm wasm decoder suceeded');
|
||||||
|
} else {
|
||||||
|
throw new Error(kgmDecrypted.error || '(unknown error)');
|
||||||
}
|
}
|
||||||
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer);
|
|
||||||
let headerLen = bHeaderLen.getUint32(0, true);
|
|
||||||
|
|
||||||
let audioData = oriData.slice(headerLen);
|
|
||||||
let dataLen = audioData.length;
|
|
||||||
if (audioData.byteLength > 1 << 26) {
|
|
||||||
throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let key1 = new Uint8Array(17);
|
const ext = SniffAudioExt(musicDecoded);
|
||||||
key1.set(oriData.slice(0x1c, 0x2c), 0);
|
|
||||||
if (MaskV2.length === 0) {
|
|
||||||
if (!(await LoadMaskV2())) throw Error('加载Kgm/Vpr Mask数据失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < dataLen; i++) {
|
|
||||||
let med8 = key1[i % 17] ^ audioData[i];
|
|
||||||
med8 ^= (med8 & 0xf) << 4;
|
|
||||||
|
|
||||||
let msk8 = GetMask(i);
|
|
||||||
msk8 ^= (msk8 & 0xf) << 4;
|
|
||||||
audioData[i] = med8 ^ msk8;
|
|
||||||
}
|
|
||||||
if (raw_ext === 'vpr') {
|
|
||||||
for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ext = SniffAudioExt(audioData);
|
|
||||||
const mime = AudioMimeType[ext];
|
const mime = AudioMimeType[ext];
|
||||||
let musicBlob = new Blob([audioData], { type: mime });
|
let musicBlob = new Blob([musicDecoded], { type: mime });
|
||||||
const musicMeta = await metaParseBlob(musicBlob);
|
const musicMeta = await metaParseBlob(musicBlob);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
|
||||||
return {
|
return {
|
||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: GetCoverFromFile(musicMeta),
|
picture: GetCoverFromFile(musicMeta),
|
||||||
@ -77,68 +55,3 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
|||||||
artist,
|
artist,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetMask(pos: number) {
|
|
||||||
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4];
|
|
||||||
}
|
|
||||||
|
|
||||||
let MaskV2: Uint8Array = new Uint8Array(0);
|
|
||||||
|
|
||||||
async function LoadMaskV2(): Promise<boolean> {
|
|
||||||
let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/static/kgm.mask`;
|
|
||||||
if (['http:', 'https:'].some((v) => v == self.location.protocol)) {
|
|
||||||
if (!!self.document) {
|
|
||||||
// using Web Worker
|
|
||||||
mask_url = './static/kgm.mask';
|
|
||||||
} else {
|
|
||||||
// using Main thread
|
|
||||||
mask_url = '../static/kgm.mask';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const resp = await fetch(mask_url, { method: 'GET' });
|
|
||||||
MaskV2 = new Uint8Array(await resp.arrayBuffer());
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//prettier-ignore
|
|
||||||
const MaskV2PreDef = [
|
|
||||||
0xb8, 0xd5, 0x3d, 0xb2, 0xe9, 0xaf, 0x78, 0x8c,
|
|
||||||
0x83, 0x33, 0x71, 0x51, 0x76, 0xa0, 0xcd, 0x37,
|
|
||||||
0x2f, 0x3e, 0x35, 0x8d, 0xa9, 0xbe, 0x98, 0xb7,
|
|
||||||
0xe7, 0x8c, 0x22, 0xce, 0x5a, 0x61, 0xdf, 0x68,
|
|
||||||
0x69, 0x89, 0xfe, 0xa5, 0xb6, 0xde, 0xa9, 0x77,
|
|
||||||
0xfc, 0xc8, 0xbd, 0xbd, 0xe5, 0x6d, 0x3e, 0x5a,
|
|
||||||
0x36, 0xef, 0x69, 0x4e, 0xbe, 0xe1, 0xe9, 0x66,
|
|
||||||
0x1c, 0xf3, 0xd9, 0x02, 0xb6, 0xf2, 0x12, 0x9b,
|
|
||||||
0x44, 0xd0, 0x6f, 0xb9, 0x35, 0x89, 0xb6, 0x46,
|
|
||||||
0x6d, 0x73, 0x82, 0x06, 0x69, 0xc1, 0xed, 0xd7,
|
|
||||||
0x85, 0xc2, 0x30, 0xdf, 0xa2, 0x62, 0xbe, 0x79,
|
|
||||||
0x2d, 0x62, 0x62, 0x3d, 0x0d, 0x7e, 0xbe, 0x48,
|
|
||||||
0x89, 0x23, 0x02, 0xa0, 0xe4, 0xd5, 0x75, 0x51,
|
|
||||||
0x32, 0x02, 0x53, 0xfd, 0x16, 0x3a, 0x21, 0x3b,
|
|
||||||
0x16, 0x0f, 0xc3, 0xb2, 0xbb, 0xb3, 0xe2, 0xba,
|
|
||||||
0x3a, 0x3d, 0x13, 0xec, 0xf6, 0x01, 0x45, 0x84,
|
|
||||||
0xa5, 0x70, 0x0f, 0x93, 0x49, 0x0c, 0x64, 0xcd,
|
|
||||||
0x31, 0xd5, 0xcc, 0x4c, 0x07, 0x01, 0x9e, 0x00,
|
|
||||||
0x1a, 0x23, 0x90, 0xbf, 0x88, 0x1e, 0x3b, 0xab,
|
|
||||||
0xa6, 0x3e, 0xc4, 0x73, 0x47, 0x10, 0x7e, 0x3b,
|
|
||||||
0x5e, 0xbc, 0xe3, 0x00, 0x84, 0xff, 0x09, 0xd4,
|
|
||||||
0xe0, 0x89, 0x0f, 0x5b, 0x58, 0x70, 0x4f, 0xfb,
|
|
||||||
0x65, 0xd8, 0x5c, 0x53, 0x1b, 0xd3, 0xc8, 0xc6,
|
|
||||||
0xbf, 0xef, 0x98, 0xb0, 0x50, 0x4f, 0x0f, 0xea,
|
|
||||||
0xe5, 0x83, 0x58, 0x8c, 0x28, 0x2c, 0x84, 0x67,
|
|
||||||
0xcd, 0xd0, 0x9e, 0x47, 0xdb, 0x27, 0x50, 0xca,
|
|
||||||
0xf4, 0x63, 0x63, 0xe8, 0x97, 0x7f, 0x1b, 0x4b,
|
|
||||||
0x0c, 0xc2, 0xc1, 0x21, 0x4c, 0xcc, 0x58, 0xf5,
|
|
||||||
0x94, 0x52, 0xa3, 0xf3, 0xd3, 0xe0, 0x68, 0xf4,
|
|
||||||
0x00, 0x23, 0xf3, 0x5e, 0x0a, 0x7b, 0x93, 0xdd,
|
|
||||||
0xab, 0x12, 0xb2, 0x13, 0xe8, 0x84, 0xd7, 0xa7,
|
|
||||||
0x9f, 0x0f, 0x32, 0x4c, 0x55, 0x1d, 0x04, 0x36,
|
|
||||||
0x52, 0xdc, 0x03, 0xf3, 0xf9, 0x4e, 0x42, 0xe9,
|
|
||||||
0x3d, 0x61, 0xef, 0x7c, 0xb6, 0xb3, 0x93, 0x50,
|
|
||||||
];
|
|
||||||
|
68
src/decrypt/kgm_wasm.ts
Normal file
68
src/decrypt/kgm_wasm.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { KgmCrypto } from '@xhacker/kgmwasm/KgmWasmBundle';
|
||||||
|
import KgmCryptoModule from '@xhacker/kgmwasm/KgmWasmBundle';
|
||||||
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
|
|
||||||
|
// 每次可以处理 2M 的数据
|
||||||
|
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
||||||
|
|
||||||
|
export interface KGMDecryptionResult {
|
||||||
|
success: boolean;
|
||||||
|
data: Uint8Array;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密一个 KGM 加密的文件。
|
||||||
|
*
|
||||||
|
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
||||||
|
* @param {ArrayBuffer} kgmBlob 读入的文件 Blob
|
||||||
|
*/
|
||||||
|
export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
|
||||||
|
const result: KGMDecryptionResult = { success: false, data: new Uint8Array(), error: '' };
|
||||||
|
|
||||||
|
// 初始化模组
|
||||||
|
let KgmCryptoObj: KgmCrypto;
|
||||||
|
|
||||||
|
try {
|
||||||
|
KgmCryptoObj = await KgmCryptoModule();
|
||||||
|
} catch (err: any) {
|
||||||
|
result.error = err?.message || 'wasm 加载失败';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!KgmCryptoObj) {
|
||||||
|
result.error = 'wasm 加载失败';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 申请内存块,并文件末端数据到 WASM 的内存堆
|
||||||
|
let kgmBuf = new Uint8Array(kgmBlob);
|
||||||
|
const pKgmBuf = KgmCryptoObj._malloc(DECRYPTION_BUF_SIZE);
|
||||||
|
const preDecDataSize = Math.min(DECRYPTION_BUF_SIZE, kgmBlob.byteLength); // 初始化缓冲区大小
|
||||||
|
KgmCryptoObj.writeArrayToMemory(kgmBuf.slice(0, preDecDataSize), pKgmBuf);
|
||||||
|
|
||||||
|
// 进行解密初始化
|
||||||
|
const headerSize = KgmCryptoObj.preDec(pKgmBuf, preDecDataSize, ext);
|
||||||
|
kgmBuf = kgmBuf.slice(headerSize);
|
||||||
|
|
||||||
|
const decryptedParts = [];
|
||||||
|
let offset = 0;
|
||||||
|
let bytesToDecrypt = kgmBuf.length;
|
||||||
|
while (bytesToDecrypt > 0) {
|
||||||
|
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
|
||||||
|
|
||||||
|
// 解密一些片段
|
||||||
|
const blockData = new Uint8Array(kgmBuf.slice(offset, offset + blockSize));
|
||||||
|
KgmCryptoObj.writeArrayToMemory(blockData, pKgmBuf);
|
||||||
|
KgmCryptoObj.decBlob(pKgmBuf, blockSize, offset);
|
||||||
|
decryptedParts.push(KgmCryptoObj.HEAPU8.slice(pKgmBuf, pKgmBuf + blockSize));
|
||||||
|
|
||||||
|
offset += blockSize;
|
||||||
|
bytesToDecrypt -= blockSize;
|
||||||
|
}
|
||||||
|
KgmCryptoObj._free(pKgmBuf);
|
||||||
|
|
||||||
|
result.data = MergeUint8Array(decryptedParts);
|
||||||
|
result.success = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
@ -15,12 +15,16 @@ import { DecryptResult } from '@/decrypt/entity';
|
|||||||
const MagicHeader = [
|
const MagicHeader = [
|
||||||
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
|
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
|
||||||
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65,
|
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65,
|
||||||
]
|
];
|
||||||
|
const MagicHeader2 = [
|
||||||
|
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
|
||||||
|
0x6B, 0x75, 0x77, 0x6F, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
const PreDefinedKey = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';
|
const PreDefinedKey = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';
|
||||||
|
|
||||||
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
|
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
|
||||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||||
if (!BytesHasPrefix(oriData, MagicHeader)) {
|
if (!BytesHasPrefix(oriData, MagicHeader) && !BytesHasPrefix(oriData, MagicHeader2)) {
|
||||||
if (SniffAudioExt(oriData) === 'aac') {
|
if (SniffAudioExt(oriData) === 'aac') {
|
||||||
return await RawDecrypt(file, raw_filename, 'aac', false);
|
return await RawDecrypt(file, raw_filename, 'aac', false);
|
||||||
}
|
}
|
||||||
@ -38,7 +42,7 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
|
|||||||
let musicBlob = new Blob([audioData], { type: mime });
|
let musicBlob = new Blob([audioData], { type: mime });
|
||||||
|
|
||||||
const musicMeta = await metaParseBlob(musicBlob);
|
const musicMeta = await metaParseBlob(musicBlob);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
|
||||||
return {
|
return {
|
||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: GetCoverFromFile(musicMeta),
|
picture: GetCoverFromFile(musicMeta),
|
||||||
|
71
src/decrypt/mg3d.ts
Normal file
71
src/decrypt/mg3d.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Decrypt as RawDecrypt } from './raw';
|
||||||
|
import { GetArrayBuffer } from '@/decrypt/utils';
|
||||||
|
import { DecryptResult } from '@/decrypt/entity';
|
||||||
|
|
||||||
|
const segmentSize = 0x20;
|
||||||
|
|
||||||
|
function isPrintableAsciiChar(ch: number) {
|
||||||
|
return ch >= 0x20 && ch <= 0x7E;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUpperHexChar(ch: number) {
|
||||||
|
return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Buffer} data
|
||||||
|
* @param {Buffer} key
|
||||||
|
* @param {boolean} copy
|
||||||
|
* @returns Buffer
|
||||||
|
*/
|
||||||
|
function decryptSegment(data: Uint8Array, key: Uint8Array) {
|
||||||
|
for (let i = 0; i < data.byteLength; i++) {
|
||||||
|
data[i] -= key[i % segmentSize];
|
||||||
|
}
|
||||||
|
return Buffer.from(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
|
||||||
|
const buf = new Uint8Array(await GetArrayBuffer(file));
|
||||||
|
|
||||||
|
// 咪咕编码的 WAV 文件有很多“空洞”内容,尝试密钥。
|
||||||
|
const header = buf.slice(0, 0x100);
|
||||||
|
const bytesRIFF = Buffer.from('RIFF', 'ascii');
|
||||||
|
const bytesWaveFormat = Buffer.from('WAVEfmt ', 'ascii');
|
||||||
|
const possibleKeys = [];
|
||||||
|
|
||||||
|
for (let i = segmentSize; i < segmentSize * 20; i += segmentSize) {
|
||||||
|
const possibleKey = buf.slice(i, i + segmentSize);
|
||||||
|
if (!possibleKey.every(isUpperHexChar)) continue;
|
||||||
|
|
||||||
|
const tempHeader = decryptSegment(header, possibleKey);
|
||||||
|
if (tempHeader.slice(0, 4).compare(bytesRIFF)) continue;
|
||||||
|
if (tempHeader.slice(8, 16).compare(bytesWaveFormat)) continue;
|
||||||
|
|
||||||
|
// fmt chunk 大小可以是 16 / 18 / 40。
|
||||||
|
const fmtChunkSize = tempHeader.readUInt32LE(0x10);
|
||||||
|
if (![16, 18, 40].includes(fmtChunkSize)) continue;
|
||||||
|
|
||||||
|
// 下一个 chunk
|
||||||
|
const firstDataChunkOffset = 0x14 + fmtChunkSize;
|
||||||
|
const chunkName = tempHeader.slice(firstDataChunkOffset, firstDataChunkOffset + 4);
|
||||||
|
if (!chunkName.every(isPrintableAsciiChar)) continue;
|
||||||
|
|
||||||
|
const secondDataChunkOffset = firstDataChunkOffset + 8 + tempHeader.readUInt32LE(firstDataChunkOffset + 4);
|
||||||
|
if (secondDataChunkOffset <= header.byteLength) {
|
||||||
|
const secondChunkName = tempHeader.slice(secondDataChunkOffset, secondDataChunkOffset + 4);
|
||||||
|
if (!secondChunkName.every(isPrintableAsciiChar)) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
possibleKeys.push(Buffer.from(possibleKey).toString('ascii'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (possibleKeys.length <= 0) {
|
||||||
|
throw new Error(`ERROR: no suitable key discovered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptionKey = Buffer.from(possibleKeys[0], 'ascii');
|
||||||
|
decryptSegment(buf, decryptionKey);
|
||||||
|
const musicData = new Blob([buf], { type: 'audio/x-wav' });
|
||||||
|
return await RawDecrypt(musicData, raw_filename, 'wav', false);
|
||||||
|
}
|
@ -139,7 +139,7 @@ class NcmDecrypt {
|
|||||||
} else {
|
} else {
|
||||||
result = JSON.parse(plainText.slice(labelIndex + 1));
|
result = JSON.parse(plainText.slice(labelIndex + 1));
|
||||||
}
|
}
|
||||||
if (!!result.albumPic) {
|
if (result.albumPic) {
|
||||||
result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500';
|
result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500';
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -160,11 +160,20 @@ class NcmDecrypt {
|
|||||||
|
|
||||||
// build artists
|
// build artists
|
||||||
let artists: string[] = [];
|
let artists: string[] = [];
|
||||||
if (!!this.oriMeta.artist) {
|
if (typeof this.oriMeta.artist === 'string') {
|
||||||
this.oriMeta.artist.forEach((arr) => artists.push(<string>arr[0]));
|
// v3.0: artist 现在可能是字符串了?
|
||||||
|
artists.push(this.oriMeta.artist);
|
||||||
|
} else if (Array.isArray(this.oriMeta.artist)) {
|
||||||
|
this.oriMeta.artist.forEach((artist) => {
|
||||||
|
if (typeof artist === 'string') {
|
||||||
|
artists.push(artist);
|
||||||
|
} else if (Array.isArray(artist) && artist[0] && typeof artist[0] === 'string') {
|
||||||
|
artists.push(artist[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (artists.length === 0 && !!info.artist) {
|
if (artists.length === 0 && info.artist) {
|
||||||
artists = info.artist
|
artists = info.artist
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((val) => val.trim())
|
.map((val) => val.trim())
|
||||||
@ -180,7 +189,7 @@ class NcmDecrypt {
|
|||||||
this.image.buffer = await img.getBufferAsync('image/jpeg');
|
this.image.buffer = await img.getBufferAsync('image/jpeg');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('get cover image failed', e);
|
console.log('fetch cover image failed', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.newMeta = { title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer };
|
this.newMeta = { title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer };
|
||||||
@ -226,12 +235,14 @@ class NcmDecrypt {
|
|||||||
this.audio = this._getAudio(keyBox);
|
this.audio = this._getAudio(keyBox);
|
||||||
this.format = this.oriMeta.format || SniffAudioExt(this.audio);
|
this.format = this.oriMeta.format || SniffAudioExt(this.audio);
|
||||||
this.mime = AudioMimeType[this.format];
|
this.mime = AudioMimeType[this.format];
|
||||||
await this._buildMeta();
|
|
||||||
try {
|
try {
|
||||||
|
await this._buildMeta();
|
||||||
await this._writeMeta();
|
await this._writeMeta();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('write meta data failed', e);
|
console.warn('build/write meta failed, skip.', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.gatherResult();
|
return this.gatherResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
|||||||
const ext = SniffAudioExt(buffer, raw_ext);
|
const ext = SniffAudioExt(buffer, raw_ext);
|
||||||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||||
const tag = await metaParseBlob(file);
|
const tag = await metaParseBlob(file);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import { QmcDecoder } from '@/decrypt/qmc';
|
|
||||||
import { BytesEqual } from '@/decrypt/utils';
|
|
||||||
|
|
||||||
function loadTestDataDecoder(name: string): {
|
|
||||||
cipherText: Uint8Array;
|
|
||||||
clearText: Uint8Array;
|
|
||||||
} {
|
|
||||||
const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
|
|
||||||
const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
|
|
||||||
const cipherText = new Uint8Array(cipherBody.length + cipherSuffix.length);
|
|
||||||
cipherText.set(cipherBody);
|
|
||||||
cipherText.set(cipherSuffix, cipherBody.length);
|
|
||||||
return {
|
|
||||||
cipherText,
|
|
||||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('qmc: real file', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4', 'mflac_map', 'mgg_map', 'qmc0_static'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { clearText, cipherText } = loadTestDataDecoder(name);
|
|
||||||
const c = new QmcDecoder(cipherText);
|
|
||||||
const buf = c.decrypt();
|
|
||||||
|
|
||||||
expect(BytesEqual(buf, clearText)).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,9 +1,7 @@
|
|||||||
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher';
|
|
||||||
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
|
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
|
||||||
|
|
||||||
import { DecryptResult } from '@/decrypt/entity';
|
import { DecryptResult } from '@/decrypt/entity';
|
||||||
import { QmcDeriveKey } from '@/decrypt/qmc_key';
|
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
|
||||||
import { DecryptQMCWasm } from '@/decrypt/qmc_wasm';
|
|
||||||
import { extractQQMusicMeta } from '@/utils/qm_meta';
|
import { extractQQMusicMeta } from '@/utils/qm_meta';
|
||||||
|
|
||||||
interface Handler {
|
interface Handler {
|
||||||
@ -18,17 +16,26 @@ export const HandlerMap: { [key: string]: Handler } = {
|
|||||||
mgg1: { ext: 'ogg', version: 2 },
|
mgg1: { ext: 'ogg', version: 2 },
|
||||||
mflac: { ext: 'flac', version: 2 },
|
mflac: { ext: 'flac', version: 2 },
|
||||||
mflac0: { ext: 'flac', version: 2 },
|
mflac0: { ext: 'flac', version: 2 },
|
||||||
|
mmp4: { ext: 'mp4', version: 2 },
|
||||||
|
|
||||||
// qmcflac / qmcogg:
|
// qmcflac / qmcogg:
|
||||||
// 有可能是 v2 加密但混用同一个后缀名。
|
// 有可能是 v2 加密但混用同一个后缀名。
|
||||||
qmcflac: { ext: 'flac', version: 2 },
|
qmcflac: { ext: 'flac', version: 2 },
|
||||||
qmcogg: { ext: 'ogg', version: 2 },
|
qmcogg: { ext: 'ogg', version: 2 },
|
||||||
|
|
||||||
qmc0: { ext: 'mp3', version: 1 },
|
qmc0: { ext: 'mp3', version: 2 },
|
||||||
qmc2: { ext: 'ogg', version: 1 },
|
qmc2: { ext: 'ogg', version: 2 },
|
||||||
qmc3: { ext: 'mp3', version: 1 },
|
qmc3: { ext: 'mp3', version: 2 },
|
||||||
|
qmc4: { ext: 'ogg', version: 2 },
|
||||||
|
qmc6: { ext: 'ogg', version: 2 },
|
||||||
|
qmc8: { ext: 'ogg', version: 2 },
|
||||||
bkcmp3: { ext: 'mp3', version: 1 },
|
bkcmp3: { ext: 'mp3', version: 1 },
|
||||||
|
bkcm4a: { ext: 'm4a', version: 1 },
|
||||||
bkcflac: { ext: 'flac', version: 1 },
|
bkcflac: { ext: 'flac', version: 1 },
|
||||||
|
bkcwav: { ext: 'wav', version: 1 },
|
||||||
|
bkcape: { ext: 'ape', version: 1 },
|
||||||
|
bkcogg: { ext: 'ogg', version: 1 },
|
||||||
|
bkcwma: { ext: 'wma', version: 1 },
|
||||||
tkm: { ext: 'm4a', version: 1 },
|
tkm: { ext: 'm4a', version: 1 },
|
||||||
'666c6163': { ext: 'flac', version: 1 },
|
'666c6163': { ext: 'flac', version: 1 },
|
||||||
'6d7033': { ext: 'mp3', version: 1 },
|
'6d7033': { ext: 'mp3', version: 1 },
|
||||||
@ -43,30 +50,21 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
|||||||
let { version } = handler;
|
let { version } = handler;
|
||||||
|
|
||||||
const fileBuffer = await GetArrayBuffer(file);
|
const fileBuffer = await GetArrayBuffer(file);
|
||||||
let musicDecoded: Uint8Array | undefined;
|
let musicDecoded = new Uint8Array();
|
||||||
let musicID: number | string | undefined;
|
let musicID: number | string | undefined;
|
||||||
|
|
||||||
if (version === 2 && globalThis.WebAssembly) {
|
if (version === 2 && globalThis.WebAssembly) {
|
||||||
console.log('qmc: using wasm decoder');
|
const v2Decrypted = await DecryptQmcWasm(fileBuffer, raw_ext);
|
||||||
|
|
||||||
const v2Decrypted = await DecryptQMCWasm(fileBuffer);
|
|
||||||
// 若 v2 检测失败,降级到 v1 再尝试一次
|
// 若 v2 检测失败,降级到 v1 再尝试一次
|
||||||
if (v2Decrypted.success) {
|
if (v2Decrypted.success) {
|
||||||
musicDecoded = v2Decrypted.data;
|
musicDecoded = v2Decrypted.data;
|
||||||
musicID = v2Decrypted.songId;
|
musicID = v2Decrypted.songId;
|
||||||
|
console.log('qmc wasm decoder suceeded');
|
||||||
} else {
|
} else {
|
||||||
console.warn('qmc2-wasm failed with error %s', v2Decrypted.error || '(no error)');
|
throw new Error(v2Decrypted.error || '(unknown error)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!musicDecoded) {
|
|
||||||
// may throw error
|
|
||||||
console.log('qmc: using js decoder');
|
|
||||||
const d = new QmcDecoder(new Uint8Array(fileBuffer));
|
|
||||||
musicDecoded = d.decrypt();
|
|
||||||
musicID = d.songID;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ext = SniffAudioExt(musicDecoded, handler.ext);
|
const ext = SniffAudioExt(musicDecoded, handler.ext);
|
||||||
const mime = AudioMimeType[ext];
|
const mime = AudioMimeType[ext];
|
||||||
|
|
||||||
@ -88,86 +86,3 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
|||||||
mime: mime,
|
mime: mime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QmcDecoder {
|
|
||||||
private static readonly BYTE_COMMA = ','.charCodeAt(0);
|
|
||||||
private readonly file: Uint8Array;
|
|
||||||
private readonly size: number;
|
|
||||||
private decoded: boolean = false;
|
|
||||||
private audioSize?: number;
|
|
||||||
private cipher?: QmcStreamCipher;
|
|
||||||
|
|
||||||
public constructor(file: Uint8Array) {
|
|
||||||
this.file = file;
|
|
||||||
this.size = file.length;
|
|
||||||
this.searchKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _songID?: number;
|
|
||||||
|
|
||||||
public get songID() {
|
|
||||||
return this._songID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public decrypt(): Uint8Array {
|
|
||||||
if (!this.cipher) {
|
|
||||||
throw new Error('no cipher found');
|
|
||||||
}
|
|
||||||
if (!this.audioSize || this.audioSize <= 0) {
|
|
||||||
throw new Error('invalid audio size');
|
|
||||||
}
|
|
||||||
const audioBuf = this.file.subarray(0, this.audioSize);
|
|
||||||
|
|
||||||
if (!this.decoded) {
|
|
||||||
this.cipher.decrypt(audioBuf, 0);
|
|
||||||
this.decoded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return audioBuf;
|
|
||||||
}
|
|
||||||
|
|
||||||
private searchKey() {
|
|
||||||
const last4Byte = this.file.slice(-4);
|
|
||||||
const textEnc = new TextDecoder();
|
|
||||||
if (textEnc.decode(last4Byte) === 'QTag') {
|
|
||||||
const sizeBuf = this.file.slice(-8, -4);
|
|
||||||
const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset);
|
|
||||||
const keySize = sizeView.getUint32(0, false);
|
|
||||||
this.audioSize = this.size - keySize - 8;
|
|
||||||
|
|
||||||
const rawKey = this.file.subarray(this.audioSize, this.size - 8);
|
|
||||||
const keyEnd = rawKey.findIndex((v) => v == QmcDecoder.BYTE_COMMA);
|
|
||||||
if (keyEnd < 0) {
|
|
||||||
throw new Error('invalid key: search raw key failed');
|
|
||||||
}
|
|
||||||
this.setCipher(rawKey.subarray(0, keyEnd));
|
|
||||||
|
|
||||||
const idBuf = rawKey.subarray(keyEnd + 1);
|
|
||||||
const idEnd = idBuf.findIndex((v) => v == QmcDecoder.BYTE_COMMA);
|
|
||||||
if (keyEnd < 0) {
|
|
||||||
throw new Error('invalid key: search song id failed');
|
|
||||||
}
|
|
||||||
this._songID = parseInt(textEnc.decode(idBuf.subarray(0, idEnd)), 10);
|
|
||||||
} else {
|
|
||||||
const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
|
|
||||||
const keySize = sizeView.getUint32(0, true);
|
|
||||||
if (keySize < 0x300) {
|
|
||||||
this.audioSize = this.size - keySize - 4;
|
|
||||||
const rawKey = this.file.subarray(this.audioSize, this.size - 4);
|
|
||||||
this.setCipher(rawKey);
|
|
||||||
} else {
|
|
||||||
this.audioSize = this.size;
|
|
||||||
this.cipher = new QmcStaticCipher();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCipher(keyRaw: Uint8Array) {
|
|
||||||
const keyDec = QmcDeriveKey(keyRaw);
|
|
||||||
if (keyDec.length > 300) {
|
|
||||||
this.cipher = new QmcRC4Cipher(keyDec);
|
|
||||||
} else {
|
|
||||||
this.cipher = new QmcMapCipher(keyDec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
test('static cipher [0x7ff8,0x8000) ', () => {
|
|
||||||
//prettier-ignore
|
|
||||||
const expected = new Uint8Array([
|
|
||||||
0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A,
|
|
||||||
0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8,
|
|
||||||
])
|
|
||||||
|
|
||||||
const c = new QmcStaticCipher();
|
|
||||||
const buf = new Uint8Array(16);
|
|
||||||
c.decrypt(buf, 0x7ff8);
|
|
||||||
|
|
||||||
expect(buf).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('static cipher [0,0x10) ', () => {
|
|
||||||
//prettier-ignore
|
|
||||||
const expected = new Uint8Array([
|
|
||||||
0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52,
|
|
||||||
0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00,
|
|
||||||
])
|
|
||||||
|
|
||||||
const c = new QmcStaticCipher();
|
|
||||||
const buf = new Uint8Array(16);
|
|
||||||
c.decrypt(buf, 0);
|
|
||||||
|
|
||||||
expect(buf).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('map cipher: get mask', () => {
|
|
||||||
//prettier-ignore
|
|
||||||
const expected = new Uint8Array([
|
|
||||||
0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB,
|
|
||||||
0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79,
|
|
||||||
])
|
|
||||||
const key = new Uint8Array(256);
|
|
||||||
for (let i = 0; i < 256; i++) key[i] = i;
|
|
||||||
const buf = new Uint8Array(16);
|
|
||||||
|
|
||||||
const c = new QmcMapCipher(key);
|
|
||||||
c.decrypt(buf, 0);
|
|
||||||
expect(buf).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadTestDataCipher(name: string): {
|
|
||||||
key: Uint8Array;
|
|
||||||
cipherText: Uint8Array;
|
|
||||||
clearText: Uint8Array;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
key: fs.readFileSync(`testdata/${name}_key.bin`),
|
|
||||||
cipherText: fs.readFileSync(`testdata/${name}_raw.bin`),
|
|
||||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('map cipher: real file', async () => {
|
|
||||||
const cases = ['mflac_map', 'mgg_map'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcMapCipher(key);
|
|
||||||
|
|
||||||
c.decrypt(cipherText, 0);
|
|
||||||
|
|
||||||
expect(cipherText).toStrictEqual(clearText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: real file', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
c.decrypt(cipherText, 0);
|
|
||||||
|
|
||||||
expect(cipherText).toStrictEqual(clearText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: first segment', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
const buf = cipherText.slice(0, 128);
|
|
||||||
c.decrypt(buf, 0);
|
|
||||||
expect(buf).toStrictEqual(clearText.slice(0, 128));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: align block (128~5120)', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
const buf = cipherText.slice(128, 5120);
|
|
||||||
c.decrypt(buf, 128);
|
|
||||||
expect(buf).toStrictEqual(clearText.slice(128, 5120));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: simple block (5120~10240)', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
const buf = cipherText.slice(5120, 10240);
|
|
||||||
c.decrypt(buf, 5120);
|
|
||||||
expect(buf).toStrictEqual(clearText.slice(5120, 10240));
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,199 +0,0 @@
|
|||||||
export interface QmcStreamCipher {
|
|
||||||
decrypt(buf: Uint8Array, offset: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QmcStaticCipher implements QmcStreamCipher {
|
|
||||||
//prettier-ignore
|
|
||||||
private static readonly staticCipherBox: Uint8Array = new Uint8Array([
|
|
||||||
0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
|
|
||||||
0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
|
|
||||||
0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10
|
|
||||||
0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18
|
|
||||||
0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20
|
|
||||||
0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28
|
|
||||||
0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30
|
|
||||||
0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38
|
|
||||||
0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40
|
|
||||||
0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48
|
|
||||||
0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50
|
|
||||||
0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58
|
|
||||||
0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60
|
|
||||||
0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68
|
|
||||||
0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70
|
|
||||||
0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78
|
|
||||||
0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80
|
|
||||||
0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88
|
|
||||||
0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90
|
|
||||||
0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98
|
|
||||||
0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0
|
|
||||||
0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8
|
|
||||||
0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0
|
|
||||||
0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8
|
|
||||||
0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0
|
|
||||||
0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8
|
|
||||||
0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0
|
|
||||||
0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8
|
|
||||||
0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0
|
|
||||||
0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8
|
|
||||||
0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0
|
|
||||||
0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11, //0xF8
|
|
||||||
])
|
|
||||||
|
|
||||||
public getMask(offset: number) {
|
|
||||||
if (offset > 0x7fff) offset %= 0x7fff;
|
|
||||||
return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff];
|
|
||||||
}
|
|
||||||
|
|
||||||
public decrypt(buf: Uint8Array, offset: number) {
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
buf[i] ^= this.getMask(offset + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QmcMapCipher implements QmcStreamCipher {
|
|
||||||
key: Uint8Array;
|
|
||||||
n: number;
|
|
||||||
|
|
||||||
constructor(key: Uint8Array) {
|
|
||||||
if (key.length == 0) throw Error('qmc/cipher_map: invalid key size');
|
|
||||||
|
|
||||||
this.key = key;
|
|
||||||
this.n = key.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static rotate(value: number, bits: number) {
|
|
||||||
let rotate = (bits + 4) % 8;
|
|
||||||
let left = value << rotate;
|
|
||||||
let right = value >> rotate;
|
|
||||||
return (left | right) & 0xff;
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(buf: Uint8Array, offset: number): void {
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
buf[i] ^= this.getMask(offset + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMask(offset: number) {
|
|
||||||
if (offset > 0x7fff) offset %= 0x7fff;
|
|
||||||
|
|
||||||
const idx = (offset * offset + 71214) % this.n;
|
|
||||||
return QmcMapCipher.rotate(this.key[idx], idx & 0x7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QmcRC4Cipher implements QmcStreamCipher {
|
|
||||||
private static readonly FIRST_SEGMENT_SIZE = 0x80;
|
|
||||||
private static readonly SEGMENT_SIZE = 5120;
|
|
||||||
|
|
||||||
S: Uint8Array;
|
|
||||||
N: number;
|
|
||||||
key: Uint8Array;
|
|
||||||
hash: number;
|
|
||||||
|
|
||||||
constructor(key: Uint8Array) {
|
|
||||||
if (key.length == 0) {
|
|
||||||
throw Error('invalid key size');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.key = key;
|
|
||||||
this.N = key.length;
|
|
||||||
|
|
||||||
// init seed box
|
|
||||||
this.S = new Uint8Array(this.N);
|
|
||||||
for (let i = 0; i < this.N; ++i) {
|
|
||||||
this.S[i] = i & 0xff;
|
|
||||||
}
|
|
||||||
let j = 0;
|
|
||||||
for (let i = 0; i < this.N; ++i) {
|
|
||||||
j = (this.S[i] + j + this.key[i % this.N]) % this.N;
|
|
||||||
[this.S[i], this.S[j]] = [this.S[j], this.S[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// init hash base
|
|
||||||
this.hash = 1;
|
|
||||||
for (let i = 0; i < this.N; i++) {
|
|
||||||
let value = this.key[i];
|
|
||||||
|
|
||||||
// ignore if key char is '\x00'
|
|
||||||
if (!value) continue;
|
|
||||||
|
|
||||||
const next_hash = (this.hash * value) >>> 0;
|
|
||||||
if (next_hash == 0 || next_hash <= this.hash) break;
|
|
||||||
|
|
||||||
this.hash = next_hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(buf: Uint8Array, offset: number): void {
|
|
||||||
let toProcess = buf.length;
|
|
||||||
let processed = 0;
|
|
||||||
const postProcess = (len: number): boolean => {
|
|
||||||
toProcess -= len;
|
|
||||||
processed += len;
|
|
||||||
offset += len;
|
|
||||||
return toProcess == 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial segment
|
|
||||||
if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) {
|
|
||||||
const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset);
|
|
||||||
this.encFirstSegment(buf.subarray(0, len_segment), offset);
|
|
||||||
if (postProcess(len_segment)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// align segment
|
|
||||||
if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) {
|
|
||||||
const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess);
|
|
||||||
this.encASegment(buf.subarray(processed, processed + len_segment), offset);
|
|
||||||
if (postProcess(len_segment)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch process segments
|
|
||||||
while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
|
|
||||||
this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
|
|
||||||
postProcess(QmcRC4Cipher.SEGMENT_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last segment (incomplete segment)
|
|
||||||
if (toProcess > 0) {
|
|
||||||
this.encASegment(buf.subarray(processed), offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private encFirstSegment(buf: Uint8Array, offset: number) {
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
buf[i] ^= this.key[this.getSegmentKey(offset + i)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private encASegment(buf: Uint8Array, offset: number) {
|
|
||||||
// Initialise a new seed box
|
|
||||||
const S = this.S.slice(0);
|
|
||||||
|
|
||||||
// Calculate the number of bytes to skip.
|
|
||||||
// The initial "key" derived from segment id, plus the current offset.
|
|
||||||
const skipLen =
|
|
||||||
(offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(Math.floor(offset / QmcRC4Cipher.SEGMENT_SIZE));
|
|
||||||
|
|
||||||
// decrypt the block
|
|
||||||
let j = 0;
|
|
||||||
let k = 0;
|
|
||||||
for (let i = -skipLen; i < buf.length; i++) {
|
|
||||||
j = (j + 1) % this.N;
|
|
||||||
k = (S[j] + k) % this.N;
|
|
||||||
[S[k], S[j]] = [S[j], S[k]];
|
|
||||||
|
|
||||||
if (i >= 0) {
|
|
||||||
buf[i] ^= S[(S[j] + S[k]) % this.N];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSegmentKey(id: number): number {
|
|
||||||
const seed = this.key[id % this.N];
|
|
||||||
const idx = Math.floor((this.hash / ((id + 1) * seed)) * 100.0);
|
|
||||||
return idx % this.N;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { QmcDeriveKey, simpleMakeKey } from '@/decrypt/qmc_key';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
test('key dec: make simple key', () => {
|
|
||||||
expect(simpleMakeKey(106, 8)).toStrictEqual([0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadTestDataKeyDecrypt(name: string): {
|
|
||||||
cipherText: Uint8Array;
|
|
||||||
clearText: Uint8Array;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`),
|
|
||||||
clearText: fs.readFileSync(`testdata/${name}_key.bin`),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('key dec: real file', async () => {
|
|
||||||
const cases = ['mflac_map', 'mgg_map', 'mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { clearText, cipherText } = loadTestDataKeyDecrypt(name);
|
|
||||||
const buf = QmcDeriveKey(cipherText);
|
|
||||||
|
|
||||||
expect(buf).toStrictEqual(clearText);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,103 +0,0 @@
|
|||||||
import { TeaCipher } from '@/utils/tea';
|
|
||||||
|
|
||||||
const SALT_LEN = 2;
|
|
||||||
const ZERO_LEN = 7;
|
|
||||||
|
|
||||||
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
|
|
||||||
const textDec = new TextDecoder();
|
|
||||||
const rawDec = Buffer.from(textDec.decode(raw), 'base64');
|
|
||||||
let n = rawDec.length;
|
|
||||||
if (n < 16) {
|
|
||||||
throw Error('key length is too short');
|
|
||||||
}
|
|
||||||
|
|
||||||
const simpleKey = simpleMakeKey(106, 8);
|
|
||||||
let teaKey = new Uint8Array(16);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
teaKey[i << 1] = simpleKey[i];
|
|
||||||
teaKey[(i << 1) + 1] = rawDec[i];
|
|
||||||
}
|
|
||||||
const sub = decryptTencentTea(rawDec.subarray(8), teaKey);
|
|
||||||
rawDec.set(sub, 8);
|
|
||||||
return rawDec.subarray(0, 8 + sub.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleMakeKey exported only for unit test
|
|
||||||
export function simpleMakeKey(salt: number, length: number): number[] {
|
|
||||||
const keyBuf: number[] = [];
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
const tmp = Math.tan(salt + i * 0.1);
|
|
||||||
keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0);
|
|
||||||
}
|
|
||||||
return keyBuf;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
|
|
||||||
if (inBuf.length % 8 != 0) {
|
|
||||||
throw Error('inBuf size not a multiple of the block size');
|
|
||||||
}
|
|
||||||
if (inBuf.length < 16) {
|
|
||||||
throw Error('inBuf size too small');
|
|
||||||
}
|
|
||||||
|
|
||||||
const blk = new TeaCipher(key, 32);
|
|
||||||
|
|
||||||
const tmpBuf = new Uint8Array(8);
|
|
||||||
const tmpView = new DataView(tmpBuf.buffer);
|
|
||||||
|
|
||||||
blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8));
|
|
||||||
|
|
||||||
const nPadLen = tmpBuf[0] & 0x7; //只要最低三位
|
|
||||||
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
|
|
||||||
const outLen = inBuf.length - 1 /*PadLen*/ - nPadLen - SALT_LEN - ZERO_LEN;
|
|
||||||
const outBuf = new Uint8Array(outLen);
|
|
||||||
|
|
||||||
let ivPrev = new Uint8Array(8);
|
|
||||||
let ivCur = inBuf.slice(0, 8); // init iv
|
|
||||||
let inBufPos = 8;
|
|
||||||
|
|
||||||
// 跳过 Padding Len 和 Padding
|
|
||||||
let tmpIdx = 1 + nPadLen;
|
|
||||||
|
|
||||||
// CBC IV 处理
|
|
||||||
const cryptBlock = () => {
|
|
||||||
ivPrev = ivCur;
|
|
||||||
ivCur = inBuf.slice(inBufPos, inBufPos + 8);
|
|
||||||
for (let j = 0; j < 8; j++) {
|
|
||||||
tmpBuf[j] ^= ivCur[j];
|
|
||||||
}
|
|
||||||
blk.decrypt(tmpView, tmpView);
|
|
||||||
inBufPos += 8;
|
|
||||||
tmpIdx = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 跳过 Salt
|
|
||||||
for (let i = 1; i <= SALT_LEN; ) {
|
|
||||||
if (tmpIdx < 8) {
|
|
||||||
tmpIdx++;
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
cryptBlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 还原明文
|
|
||||||
let outBufPos = 0;
|
|
||||||
while (outBufPos < outLen) {
|
|
||||||
if (tmpIdx < 8) {
|
|
||||||
outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
|
|
||||||
outBufPos++;
|
|
||||||
tmpIdx++;
|
|
||||||
} else {
|
|
||||||
cryptBlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验Zero
|
|
||||||
for (let i = 1; i <= ZERO_LEN; i++) {
|
|
||||||
if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) {
|
|
||||||
throw Error('zero check failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outBuf;
|
|
||||||
}
|
|
@ -1,14 +1,11 @@
|
|||||||
import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle';
|
import { QmcCrypto } from '@xhacker/qmcwasm/QmcWasmBundle';
|
||||||
|
import QmcCryptoModule from '@xhacker/qmcwasm/QmcWasmBundle';
|
||||||
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
import { QMCCrypto } from '@jixun/qmc2-crypto/QMCCrypto';
|
|
||||||
|
|
||||||
// 检测文件末端使用的缓冲区大小
|
// 每次可以处理 2M 的数据
|
||||||
const DETECTION_SIZE = 40;
|
|
||||||
|
|
||||||
// 每次处理 2M 的数据
|
|
||||||
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
||||||
|
|
||||||
export interface QMC2DecryptionResult {
|
export interface QMCDecryptionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
songId: string | number;
|
songId: string | number;
|
||||||
@ -16,96 +13,63 @@ export interface QMC2DecryptionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解密一个 QMC2 加密的文件。
|
* 解密一个 QMC 加密的文件。
|
||||||
*
|
*
|
||||||
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
||||||
* @param {ArrayBuffer} mggBlob 读入的文件 Blob
|
* @param {ArrayBuffer} qmcBlob 读入的文件 Blob
|
||||||
*/
|
*/
|
||||||
export async function DecryptQMCWasm(mggBlob: ArrayBuffer): Promise<QMC2DecryptionResult> {
|
export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
|
||||||
const result: QMC2DecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
|
const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
|
||||||
|
|
||||||
// 初始化模组
|
// 初始化模组
|
||||||
let QMCCrypto: QMCCrypto;
|
let QmcCryptoObj: QmcCrypto;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
QMCCrypto = await QMCCryptoModule();
|
QmcCryptoObj = await QmcCryptoModule();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
result.error = err?.message || 'wasm 加载失败';
|
result.error = err?.message || 'wasm 加载失败';
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
if (!QmcCryptoObj) {
|
||||||
// 申请内存块,并文件末端数据到 WASM 的内存堆
|
result.error = 'wasm 加载失败';
|
||||||
const detectionBuf = new Uint8Array(mggBlob.slice(-DETECTION_SIZE));
|
|
||||||
const pDetectionBuf = QMCCrypto._malloc(detectionBuf.length);
|
|
||||||
QMCCrypto.writeArrayToMemory(detectionBuf, pDetectionBuf);
|
|
||||||
|
|
||||||
// 检测结果内存块
|
|
||||||
const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
|
|
||||||
|
|
||||||
// 进行检测
|
|
||||||
const detectOK = QMCCrypto.detectKeyEndPosition(pDetectionResult, pDetectionBuf, detectionBuf.length);
|
|
||||||
|
|
||||||
// 提取结构体内容:
|
|
||||||
// (pos: i32; len: i32; error: char[??])
|
|
||||||
const position = QMCCrypto.getValue(pDetectionResult, 'i32');
|
|
||||||
const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32');
|
|
||||||
|
|
||||||
result.success = detectOK;
|
|
||||||
result.error = QMCCrypto.UTF8ToString(
|
|
||||||
pDetectionResult + QMCCrypto.offsetof_error_msg(),
|
|
||||||
QMCCrypto.sizeof_error_msg(),
|
|
||||||
);
|
|
||||||
const songId = QMCCrypto.UTF8ToString(pDetectionResult + QMCCrypto.offsetof_song_id(), QMCCrypto.sizeof_song_id());
|
|
||||||
if (!songId) {
|
|
||||||
console.debug('qmc2-wasm: songId not found');
|
|
||||||
} else if (/^\d+$/.test(songId)) {
|
|
||||||
result.songId = songId;
|
|
||||||
} else {
|
|
||||||
console.warn('qmc2-wasm: Invalid songId: %s', songId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放内存
|
|
||||||
QMCCrypto._free(pDetectionBuf);
|
|
||||||
QMCCrypto._free(pDetectionResult);
|
|
||||||
|
|
||||||
if (!detectOK) {
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算解密后文件的大小。
|
// 申请内存块,并文件末端数据到 WASM 的内存堆
|
||||||
// 之前得到的 position 为相对当前检测数据起点的偏移。
|
const qmcBuf = new Uint8Array(qmcBlob);
|
||||||
const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
|
const pQmcBuf = QmcCryptoObj._malloc(DECRYPTION_BUF_SIZE);
|
||||||
|
const preDecDataSize = Math.min(DECRYPTION_BUF_SIZE, qmcBlob.byteLength); // 初始化缓冲区大小
|
||||||
|
QmcCryptoObj.writeArrayToMemory(qmcBuf.slice(-preDecDataSize), pQmcBuf);
|
||||||
|
|
||||||
// 提取嵌入到文件的 EKey
|
// 进行解密初始化
|
||||||
const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
|
ext = '.' + ext;
|
||||||
|
const tailSize = QmcCryptoObj.preDec(pQmcBuf, preDecDataSize, ext);
|
||||||
// 解码 UTF-8 数据到 string
|
if (tailSize == -1) {
|
||||||
const decoder = new TextDecoder();
|
result.error = QmcCryptoObj.getErr();
|
||||||
const ekey_b64 = decoder.decode(ekey);
|
return result;
|
||||||
|
} else {
|
||||||
// 初始化加密与缓冲区
|
result.songId = QmcCryptoObj.getSongId();
|
||||||
const hCrypto = QMCCrypto.createInstWidthEKey(ekey_b64);
|
result.songId = result.songId == "0" ? 0 : result.songId;
|
||||||
const buf = QMCCrypto._malloc(DECRYPTION_BUF_SIZE);
|
}
|
||||||
|
|
||||||
const decryptedParts = [];
|
const decryptedParts = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let bytesToDecrypt = decryptedSize;
|
let bytesToDecrypt = qmcBuf.length - tailSize;
|
||||||
while (bytesToDecrypt > 0) {
|
while (bytesToDecrypt > 0) {
|
||||||
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
|
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
|
||||||
|
|
||||||
// 解密一些片段
|
// 解密一些片段
|
||||||
const blockData = new Uint8Array(mggBlob.slice(offset, offset + blockSize));
|
const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
|
||||||
QMCCrypto.writeArrayToMemory(blockData, buf);
|
QmcCryptoObj.writeArrayToMemory(blockData, pQmcBuf);
|
||||||
QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize);
|
decryptedParts.push(QmcCryptoObj.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCryptoObj.decBlob(pQmcBuf, blockSize, offset)));
|
||||||
decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize));
|
|
||||||
|
|
||||||
offset += blockSize;
|
offset += blockSize;
|
||||||
bytesToDecrypt -= blockSize;
|
bytesToDecrypt -= blockSize;
|
||||||
}
|
}
|
||||||
QMCCrypto._free(buf);
|
QmcCryptoObj._free(pQmcBuf);
|
||||||
hCrypto.delete();
|
|
||||||
|
|
||||||
result.data = MergeUint8Array(decryptedParts);
|
result.data = MergeUint8Array(decryptedParts);
|
||||||
|
result.success = true;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -8,34 +8,42 @@ import {
|
|||||||
} from '@/decrypt/utils';
|
} from '@/decrypt/utils';
|
||||||
|
|
||||||
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
|
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
|
||||||
|
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
|
||||||
|
|
||||||
import { DecryptResult } from '@/decrypt/entity';
|
import { DecryptResult } from '@/decrypt/entity';
|
||||||
|
|
||||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||||
|
|
||||||
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
|
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||||
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
const buffer = await GetArrayBuffer(file);
|
||||||
let length = buffer.length;
|
|
||||||
for (let i = 0; i < length; i++) {
|
let musicDecoded = new Uint8Array();
|
||||||
buffer[i] ^= 0xf4;
|
if (globalThis.WebAssembly) {
|
||||||
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
|
console.log('qmc: using wasm decoder');
|
||||||
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
|
|
||||||
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
|
const qmcDecrypted = await DecryptQmcWasm(buffer, raw_ext);
|
||||||
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
|
// 若 wasm 失败,使用 js 再尝试一次
|
||||||
|
if (qmcDecrypted.success) {
|
||||||
|
musicDecoded = qmcDecrypted.data;
|
||||||
|
console.log('qmc wasm decoder suceeded');
|
||||||
|
} else {
|
||||||
|
throw new Error(qmcDecrypted.error || '(unknown error)');
|
||||||
}
|
}
|
||||||
let ext = SniffAudioExt(buffer, '');
|
}
|
||||||
|
|
||||||
|
let ext = SniffAudioExt(musicDecoded, '');
|
||||||
const newName = SplitFilename(raw_filename);
|
const newName = SplitFilename(raw_filename);
|
||||||
let audioBlob: Blob;
|
let audioBlob: Blob;
|
||||||
if (ext !== '' || newName.ext === 'mp3') {
|
if (ext !== '' || newName.ext === 'mp3') {
|
||||||
audioBlob = new Blob([buffer], { type: AudioMimeType[ext] });
|
audioBlob = new Blob([musicDecoded], { type: AudioMimeType[ext] });
|
||||||
} else if (newName.ext in HandlerMap) {
|
} else if (newName.ext in HandlerMap) {
|
||||||
audioBlob = new Blob([buffer], { type: 'application/octet-stream' });
|
audioBlob = new Blob([musicDecoded], { type: 'application/octet-stream' });
|
||||||
return QmcDecrypt(audioBlob, newName.name, newName.ext);
|
return QmcDecrypt(audioBlob, newName.name, newName.ext);
|
||||||
} else {
|
} else {
|
||||||
throw '不支持的QQ音乐缓存格式';
|
throw '不支持的QQ音乐缓存格式';
|
||||||
}
|
}
|
||||||
const tag = await metaParseBlob(audioBlob);
|
const tag = await metaParseBlob(audioBlob);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
@ -17,7 +17,7 @@ export async function Decrypt(
|
|||||||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||||
}
|
}
|
||||||
const tag = await metaParseBlob(file);
|
const tag = await metaParseBlob(file);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ''));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
@ -2,6 +2,8 @@ import { IAudioMetadata } from 'music-metadata-browser';
|
|||||||
import ID3Writer from 'browser-id3-writer';
|
import ID3Writer from 'browser-id3-writer';
|
||||||
import MetaFlac from 'metaflac-js';
|
import MetaFlac from 'metaflac-js';
|
||||||
|
|
||||||
|
export const split_regex = /[ ]?[,;/_、][ ]?/;
|
||||||
|
|
||||||
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43];
|
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43];
|
||||||
export const MP3_HEADER = [0x49, 0x44, 0x33];
|
export const MP3_HEADER = [0x49, 0x44, 0x33];
|
||||||
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53];
|
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53];
|
||||||
@ -91,7 +93,8 @@ export function GetMetaFromFile(
|
|||||||
|
|
||||||
const items = filename.split(separator);
|
const items = filename.split(separator);
|
||||||
if (items.length > 1) {
|
if (items.length > 1) {
|
||||||
if (!meta.artist) meta.artist = items[0].trim();
|
//由文件名和原metadata共同决定歌手tag(有时从文件名看有多个歌手,而metadata只有一个)
|
||||||
|
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length) meta.artist = items[0].trim();
|
||||||
if (!meta.title) meta.title = items[1].trim();
|
if (!meta.title) meta.title = items[1].trim();
|
||||||
} else if (items.length === 1) {
|
} else if (items.length === 1) {
|
||||||
if (!meta.title) meta.title = items[0].trim();
|
if (!meta.title) meta.title = items[0].trim();
|
||||||
@ -119,6 +122,8 @@ export interface IMusicMeta {
|
|||||||
title: string;
|
title: string;
|
||||||
artists?: string[];
|
artists?: string[];
|
||||||
album?: string;
|
album?: string;
|
||||||
|
albumartist?: string;
|
||||||
|
genre?: string[];
|
||||||
picture?: ArrayBuffer;
|
picture?: ArrayBuffer;
|
||||||
picture_desc?: string;
|
picture_desc?: string;
|
||||||
}
|
}
|
||||||
@ -132,7 +137,9 @@ export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IA
|
|||||||
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
|
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
|
||||||
try {
|
try {
|
||||||
writer.setFrame(frame.id, frame.value);
|
writer.setFrame(frame.id, frame.value);
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
console.warn(`failed to write ID3 tag '${frame.id}'`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -169,6 +176,83 @@ export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: I
|
|||||||
return writer.save();
|
return writer.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RewriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
||||||
|
const writer = new ID3Writer(audioData);
|
||||||
|
|
||||||
|
// preserve original data
|
||||||
|
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
|
||||||
|
frames.forEach((frame) => {
|
||||||
|
if (frame.id !== 'TPE1'
|
||||||
|
&& frame.id !== 'TIT2'
|
||||||
|
&& frame.id !== 'TALB'
|
||||||
|
&& frame.id !== 'TPE2'
|
||||||
|
&& frame.id !== 'TCON'
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
writer.setFrame(frame.id, frame.value);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`failed to write ID3 tag '${frame.id}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const old = original.common;
|
||||||
|
writer
|
||||||
|
.setFrame('TPE1', info?.artists || old.artists || [])
|
||||||
|
.setFrame('TIT2', info?.title || old.title)
|
||||||
|
.setFrame('TALB', info?.album || old.album || '')
|
||||||
|
.setFrame('TPE2', info?.albumartist || old.albumartist || '')
|
||||||
|
.setFrame('TCON', info?.genre || old.genre || []);
|
||||||
|
if (info.picture) {
|
||||||
|
writer.setFrame('APIC', {
|
||||||
|
type: 3,
|
||||||
|
data: info.picture,
|
||||||
|
description: info.picture_desc || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return writer.addTag();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RewriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
||||||
|
const writer = new MetaFlac(audioData);
|
||||||
|
const old = original.common;
|
||||||
|
if (info.title) {
|
||||||
|
if (old.title) {
|
||||||
|
writer.removeTag('TITLE');
|
||||||
|
}
|
||||||
|
writer.setTag('TITLE=' + info.title);
|
||||||
|
}
|
||||||
|
if (info.album) {
|
||||||
|
if (old.album) {
|
||||||
|
writer.removeTag('ALBUM');
|
||||||
|
}
|
||||||
|
writer.setTag('ALBUM=' + info.album);
|
||||||
|
}
|
||||||
|
if (info.albumartist) {
|
||||||
|
if (old.albumartist) {
|
||||||
|
writer.removeTag('ALBUMARTIST');
|
||||||
|
}
|
||||||
|
writer.setTag('ALBUMARTIST=' + info.albumartist);
|
||||||
|
}
|
||||||
|
if (info.artists) {
|
||||||
|
if (old.artists) {
|
||||||
|
writer.removeTag('ARTIST');
|
||||||
|
}
|
||||||
|
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
|
||||||
|
}
|
||||||
|
if (info.genre) {
|
||||||
|
if (old.genre) {
|
||||||
|
writer.removeTag('GENRE');
|
||||||
|
}
|
||||||
|
info.genre.forEach((singlegenre) => writer.setTag('GENRE=' + singlegenre));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.picture) {
|
||||||
|
writer.importPictureFromBuffer(Buffer.from(info.picture));
|
||||||
|
}
|
||||||
|
return writer.save();
|
||||||
|
}
|
||||||
|
|
||||||
export function SplitFilename(n: string): { name: string; ext: string } {
|
export function SplitFilename(n: string): { name: string; ext: string } {
|
||||||
const pos = n.lastIndexOf('.');
|
const pos = n.lastIndexOf('.');
|
||||||
return {
|
return {
|
||||||
|
193
src/decrypt/ximalaya.ts
Normal file
193
src/decrypt/ximalaya.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||||
|
import { AudioMimeType, SniffAudioExt, GetArrayBuffer, GetMetaFromFile } from './utils';
|
||||||
|
import { DecryptResult } from '@/decrypt/entity';
|
||||||
|
|
||||||
|
const HandlerMap: Map<string, (data: Uint8Array) => Uint8Array> = new Map([
|
||||||
|
['x2m', ProcessX2M],
|
||||||
|
['x3m', ProcessX3M],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||||
|
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||||
|
const handler = HandlerMap.get(raw_ext);
|
||||||
|
if (!handler) throw 'File type is incorrect!';
|
||||||
|
let musicDecoded: Uint8Array = handler(buffer);
|
||||||
|
|
||||||
|
const ext = SniffAudioExt(musicDecoded, 'm4a');
|
||||||
|
const mime = AudioMimeType[ext];
|
||||||
|
|
||||||
|
let musicBlob = new Blob([musicDecoded], { type: mime });
|
||||||
|
|
||||||
|
const musicMeta = await metaParseBlob(musicBlob);
|
||||||
|
|
||||||
|
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
||||||
|
|
||||||
|
return {
|
||||||
|
picture: '',
|
||||||
|
title: info.title,
|
||||||
|
artist: info.artist,
|
||||||
|
ext: ext,
|
||||||
|
album: musicMeta.common.album,
|
||||||
|
blob: musicBlob,
|
||||||
|
file: URL.createObjectURL(musicBlob),
|
||||||
|
mime: mime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessX2M(data: Uint8Array) {
|
||||||
|
const x2mHeaderSize = 1024;
|
||||||
|
const x2mKey = [0x78, 0x6d, 0x6c, 0x79];
|
||||||
|
let encryptedHeader = data.slice(0, x2mHeaderSize);
|
||||||
|
for (let idx = 0; idx < x2mHeaderSize; idx++) {
|
||||||
|
let srcIdx = x2mScrambleTable[idx];
|
||||||
|
data[idx] = encryptedHeader[srcIdx] ^ x2mKey[idx % x2mKey.length];
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessX3M(data: Uint8Array) {
|
||||||
|
const x3mHeaderSize = 1024;
|
||||||
|
|
||||||
|
//prettier-ignore: uint8, size 32(8x4)
|
||||||
|
const x3mKey = [
|
||||||
|
0x33, 0x39, 0x38, 0x39, 0x64, 0x31, 0x31, 0x31, 0x61, 0x61, 0x64, 0x35, 0x36, 0x31, 0x33, 0x39, 0x34, 0x30, 0x66,
|
||||||
|
0x34, 0x66, 0x63, 0x34, 0x34, 0x62, 0x36, 0x33, 0x39, 0x62, 0x32, 0x39, 0x32,
|
||||||
|
];
|
||||||
|
|
||||||
|
let encryptedHeader = data.slice(0, x3mHeaderSize);
|
||||||
|
for (let dstIdx = 0; dstIdx < x3mHeaderSize; dstIdx++) {
|
||||||
|
let srcIdx = x3mScrambleTable[dstIdx];
|
||||||
|
data[dstIdx] = encryptedHeader[srcIdx] ^ x3mKey[dstIdx % x3mKey.length];
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
//prettier-ignore: uint16, size 1024 (64x16)
|
||||||
|
const x2mScrambleTable = [
|
||||||
|
0x2a9, 0x2ab, 0x154, 0x2aa, 0x2a8, 0x2ac, 0x153, 0x2a7, 0x2ad, 0x152, 0x2a6, 0x3ff, 0x000, 0x155, 0x2ae, 0x151, 0x2a5,
|
||||||
|
0x3fe, 0x001, 0x156, 0x2af, 0x150, 0x2a4, 0x3fd, 0x002, 0x157, 0x2b0, 0x14f, 0x2a3, 0x3fc, 0x003, 0x158, 0x2b1, 0x14e,
|
||||||
|
0x2a2, 0x3fb, 0x004, 0x159, 0x2b2, 0x14d, 0x2a1, 0x3fa, 0x005, 0x15a, 0x2b3, 0x14c, 0x2a0, 0x3f9, 0x006, 0x15b, 0x2b4,
|
||||||
|
0x14b, 0x29f, 0x3f8, 0x007, 0x15c, 0x2b5, 0x14a, 0x29e, 0x3f7, 0x008, 0x15d, 0x2b6, 0x149, 0x29d, 0x3f6, 0x009, 0x15e,
|
||||||
|
0x2b7, 0x148, 0x29c, 0x3f5, 0x00a, 0x15f, 0x2b8, 0x147, 0x29b, 0x3f4, 0x00b, 0x160, 0x2b9, 0x146, 0x29a, 0x3f3, 0x00c,
|
||||||
|
0x161, 0x2ba, 0x145, 0x299, 0x3f2, 0x00d, 0x162, 0x2bb, 0x144, 0x298, 0x3f1, 0x00e, 0x163, 0x2bc, 0x143, 0x297, 0x3f0,
|
||||||
|
0x00f, 0x164, 0x2bd, 0x142, 0x296, 0x3ef, 0x010, 0x165, 0x2be, 0x141, 0x295, 0x3ee, 0x011, 0x166, 0x2bf, 0x140, 0x294,
|
||||||
|
0x3ed, 0x012, 0x167, 0x2c0, 0x13f, 0x293, 0x3ec, 0x013, 0x168, 0x2c1, 0x13e, 0x292, 0x3eb, 0x014, 0x169, 0x2c2, 0x13d,
|
||||||
|
0x291, 0x3ea, 0x015, 0x16a, 0x2c3, 0x13c, 0x290, 0x3e9, 0x016, 0x16b, 0x2c4, 0x13b, 0x28f, 0x3e8, 0x017, 0x16c, 0x2c5,
|
||||||
|
0x13a, 0x28e, 0x3e7, 0x018, 0x16d, 0x2c6, 0x139, 0x28d, 0x3e6, 0x019, 0x16e, 0x2c7, 0x138, 0x28c, 0x3e5, 0x01a, 0x16f,
|
||||||
|
0x2c8, 0x137, 0x28b, 0x3e4, 0x01b, 0x170, 0x2c9, 0x136, 0x28a, 0x3e3, 0x01c, 0x171, 0x2ca, 0x135, 0x289, 0x3e2, 0x01d,
|
||||||
|
0x172, 0x2cb, 0x134, 0x288, 0x3e1, 0x01e, 0x173, 0x2cc, 0x133, 0x287, 0x3e0, 0x01f, 0x174, 0x2cd, 0x0a9, 0x1fe, 0x357,
|
||||||
|
0x020, 0x175, 0x2ce, 0x0aa, 0x1ff, 0x358, 0x021, 0x176, 0x2cf, 0x0ab, 0x200, 0x359, 0x022, 0x177, 0x2d0, 0x0ac, 0x201,
|
||||||
|
0x35a, 0x023, 0x178, 0x2d1, 0x0ad, 0x202, 0x35b, 0x024, 0x179, 0x2d2, 0x0ae, 0x203, 0x35c, 0x025, 0x17a, 0x2d3, 0x0af,
|
||||||
|
0x204, 0x35d, 0x026, 0x17b, 0x2d4, 0x0b0, 0x205, 0x35e, 0x027, 0x17c, 0x2d5, 0x0b1, 0x206, 0x35f, 0x028, 0x17d, 0x2d6,
|
||||||
|
0x0b2, 0x207, 0x360, 0x029, 0x17e, 0x2d7, 0x0b3, 0x208, 0x361, 0x02a, 0x17f, 0x2d8, 0x0b4, 0x209, 0x362, 0x02b, 0x180,
|
||||||
|
0x2d9, 0x0b5, 0x20a, 0x363, 0x02c, 0x181, 0x2da, 0x0b6, 0x20b, 0x364, 0x02d, 0x182, 0x2db, 0x0b7, 0x20c, 0x365, 0x02e,
|
||||||
|
0x183, 0x2dc, 0x0b8, 0x20d, 0x366, 0x02f, 0x184, 0x2dd, 0x0b9, 0x20e, 0x367, 0x030, 0x185, 0x2de, 0x0ba, 0x20f, 0x368,
|
||||||
|
0x031, 0x186, 0x2df, 0x0bb, 0x210, 0x369, 0x032, 0x187, 0x2e0, 0x0bc, 0x211, 0x36a, 0x033, 0x188, 0x2e1, 0x0bd, 0x212,
|
||||||
|
0x36b, 0x034, 0x189, 0x2e2, 0x0be, 0x213, 0x36c, 0x035, 0x18a, 0x2e3, 0x0bf, 0x214, 0x36d, 0x036, 0x18b, 0x2e4, 0x0c0,
|
||||||
|
0x215, 0x36e, 0x037, 0x18c, 0x2e5, 0x0c1, 0x216, 0x36f, 0x038, 0x18d, 0x2e6, 0x0c2, 0x217, 0x370, 0x039, 0x18e, 0x2e7,
|
||||||
|
0x0c3, 0x218, 0x371, 0x03a, 0x18f, 0x2e8, 0x0c4, 0x219, 0x372, 0x03b, 0x190, 0x2e9, 0x0c5, 0x21a, 0x373, 0x03c, 0x191,
|
||||||
|
0x2ea, 0x0c6, 0x21b, 0x374, 0x03d, 0x192, 0x2eb, 0x0c7, 0x21c, 0x375, 0x03e, 0x193, 0x2ec, 0x0c8, 0x21d, 0x376, 0x03f,
|
||||||
|
0x194, 0x2ed, 0x0c9, 0x21e, 0x377, 0x040, 0x195, 0x2ee, 0x0ca, 0x21f, 0x378, 0x041, 0x196, 0x2ef, 0x0cb, 0x220, 0x379,
|
||||||
|
0x042, 0x197, 0x2f0, 0x0cc, 0x221, 0x37a, 0x043, 0x198, 0x2f1, 0x0cd, 0x222, 0x37b, 0x044, 0x199, 0x2f2, 0x0ce, 0x223,
|
||||||
|
0x37c, 0x045, 0x19a, 0x2f3, 0x0cf, 0x224, 0x37d, 0x046, 0x19b, 0x2f4, 0x0d0, 0x225, 0x37e, 0x047, 0x19c, 0x2f5, 0x0d1,
|
||||||
|
0x226, 0x37f, 0x048, 0x19d, 0x2f6, 0x0d2, 0x227, 0x380, 0x049, 0x19e, 0x2f7, 0x0d3, 0x228, 0x381, 0x04a, 0x19f, 0x2f8,
|
||||||
|
0x0d4, 0x229, 0x382, 0x04b, 0x1a0, 0x2f9, 0x0d5, 0x22a, 0x383, 0x04c, 0x1a1, 0x2fa, 0x0d6, 0x22b, 0x384, 0x04d, 0x1a2,
|
||||||
|
0x2fb, 0x0d7, 0x22c, 0x385, 0x04e, 0x1a3, 0x2fc, 0x0d8, 0x22d, 0x386, 0x04f, 0x1a4, 0x2fd, 0x0d9, 0x22e, 0x387, 0x050,
|
||||||
|
0x1a5, 0x2fe, 0x0da, 0x22f, 0x388, 0x051, 0x1a6, 0x2ff, 0x0db, 0x230, 0x389, 0x052, 0x1a7, 0x300, 0x0dc, 0x231, 0x38a,
|
||||||
|
0x053, 0x1a8, 0x301, 0x0dd, 0x232, 0x38b, 0x054, 0x1a9, 0x302, 0x0de, 0x233, 0x38c, 0x055, 0x1aa, 0x303, 0x0df, 0x234,
|
||||||
|
0x38d, 0x056, 0x1ab, 0x304, 0x0e0, 0x235, 0x38e, 0x057, 0x1ac, 0x305, 0x0e1, 0x236, 0x38f, 0x058, 0x1ad, 0x306, 0x0e2,
|
||||||
|
0x237, 0x390, 0x059, 0x1ae, 0x307, 0x0e3, 0x238, 0x391, 0x05a, 0x1af, 0x308, 0x0e4, 0x239, 0x392, 0x05b, 0x1b0, 0x309,
|
||||||
|
0x0e5, 0x23a, 0x393, 0x05c, 0x1b1, 0x30a, 0x0e6, 0x23b, 0x394, 0x05d, 0x1b2, 0x30b, 0x0e7, 0x23c, 0x395, 0x05e, 0x1b3,
|
||||||
|
0x30c, 0x0e8, 0x23d, 0x396, 0x05f, 0x1b4, 0x30d, 0x0e9, 0x23e, 0x397, 0x060, 0x1b5, 0x30e, 0x0ea, 0x23f, 0x398, 0x061,
|
||||||
|
0x1b6, 0x30f, 0x0eb, 0x240, 0x399, 0x062, 0x1b7, 0x310, 0x0ec, 0x241, 0x39a, 0x063, 0x1b8, 0x311, 0x0ed, 0x242, 0x39b,
|
||||||
|
0x064, 0x1b9, 0x312, 0x0ee, 0x243, 0x39c, 0x065, 0x1ba, 0x313, 0x0ef, 0x244, 0x39d, 0x066, 0x1bb, 0x314, 0x0f0, 0x245,
|
||||||
|
0x39e, 0x067, 0x1bc, 0x315, 0x0f1, 0x246, 0x39f, 0x068, 0x1bd, 0x316, 0x0f2, 0x247, 0x3a0, 0x069, 0x1be, 0x317, 0x0f3,
|
||||||
|
0x248, 0x3a1, 0x06a, 0x1bf, 0x318, 0x0f4, 0x249, 0x3a2, 0x06b, 0x1c0, 0x319, 0x0f5, 0x24a, 0x3a3, 0x06c, 0x1c1, 0x31a,
|
||||||
|
0x0f6, 0x24b, 0x3a4, 0x06d, 0x1c2, 0x31b, 0x0f7, 0x24c, 0x3a5, 0x06e, 0x1c3, 0x31c, 0x0f8, 0x24d, 0x3a6, 0x06f, 0x1c4,
|
||||||
|
0x31d, 0x0f9, 0x24e, 0x3a7, 0x070, 0x1c5, 0x31e, 0x0fa, 0x24f, 0x3a8, 0x071, 0x1c6, 0x31f, 0x0fb, 0x250, 0x3a9, 0x072,
|
||||||
|
0x1c7, 0x320, 0x0fc, 0x251, 0x3aa, 0x073, 0x1c8, 0x321, 0x0fd, 0x252, 0x3ab, 0x074, 0x1c9, 0x322, 0x0fe, 0x253, 0x3ac,
|
||||||
|
0x075, 0x1ca, 0x323, 0x0ff, 0x254, 0x3ad, 0x076, 0x1cb, 0x324, 0x100, 0x255, 0x3ae, 0x077, 0x1cc, 0x325, 0x101, 0x256,
|
||||||
|
0x3af, 0x078, 0x1cd, 0x326, 0x102, 0x257, 0x3b0, 0x079, 0x1ce, 0x327, 0x103, 0x258, 0x3b1, 0x07a, 0x1cf, 0x328, 0x104,
|
||||||
|
0x259, 0x3b2, 0x07b, 0x1d0, 0x329, 0x105, 0x25a, 0x3b3, 0x07c, 0x1d1, 0x32a, 0x106, 0x25b, 0x3b4, 0x07d, 0x1d2, 0x32b,
|
||||||
|
0x107, 0x25c, 0x3b5, 0x07e, 0x1d3, 0x32c, 0x108, 0x25d, 0x3b6, 0x07f, 0x1d4, 0x32d, 0x109, 0x25e, 0x3b7, 0x080, 0x1d5,
|
||||||
|
0x32e, 0x10a, 0x25f, 0x3b8, 0x081, 0x1d6, 0x32f, 0x10b, 0x260, 0x3b9, 0x082, 0x1d7, 0x330, 0x10c, 0x261, 0x3ba, 0x083,
|
||||||
|
0x1d8, 0x331, 0x10d, 0x262, 0x3bb, 0x084, 0x1d9, 0x332, 0x10e, 0x263, 0x3bc, 0x085, 0x1da, 0x333, 0x10f, 0x264, 0x3bd,
|
||||||
|
0x086, 0x1db, 0x334, 0x110, 0x265, 0x3be, 0x087, 0x1dc, 0x335, 0x111, 0x266, 0x3bf, 0x088, 0x1dd, 0x336, 0x112, 0x267,
|
||||||
|
0x3c0, 0x089, 0x1de, 0x337, 0x113, 0x268, 0x3c1, 0x08a, 0x1df, 0x338, 0x114, 0x269, 0x3c2, 0x08b, 0x1e0, 0x339, 0x115,
|
||||||
|
0x26a, 0x3c3, 0x08c, 0x1e1, 0x33a, 0x116, 0x26b, 0x3c4, 0x08d, 0x1e2, 0x33b, 0x117, 0x26c, 0x3c5, 0x08e, 0x1e3, 0x33c,
|
||||||
|
0x118, 0x26d, 0x3c6, 0x08f, 0x1e4, 0x33d, 0x119, 0x26e, 0x3c7, 0x090, 0x1e5, 0x33e, 0x11a, 0x26f, 0x3c8, 0x091, 0x1e6,
|
||||||
|
0x33f, 0x11b, 0x270, 0x3c9, 0x092, 0x1e7, 0x340, 0x11c, 0x271, 0x3ca, 0x093, 0x1e8, 0x341, 0x11d, 0x272, 0x3cb, 0x094,
|
||||||
|
0x1e9, 0x342, 0x11e, 0x273, 0x3cc, 0x095, 0x1ea, 0x343, 0x11f, 0x274, 0x3cd, 0x096, 0x1eb, 0x344, 0x120, 0x275, 0x3ce,
|
||||||
|
0x097, 0x1ec, 0x345, 0x121, 0x276, 0x3cf, 0x098, 0x1ed, 0x346, 0x122, 0x277, 0x3d0, 0x099, 0x1ee, 0x347, 0x123, 0x278,
|
||||||
|
0x3d1, 0x09a, 0x1ef, 0x348, 0x124, 0x279, 0x3d2, 0x09b, 0x1f0, 0x349, 0x125, 0x27a, 0x3d3, 0x09c, 0x1f1, 0x34a, 0x126,
|
||||||
|
0x27b, 0x3d4, 0x09d, 0x1f2, 0x34b, 0x127, 0x27c, 0x3d5, 0x09e, 0x1f3, 0x34c, 0x128, 0x27d, 0x3d6, 0x09f, 0x1f4, 0x34d,
|
||||||
|
0x129, 0x27e, 0x3d7, 0x0a0, 0x1f5, 0x34e, 0x12a, 0x27f, 0x3d8, 0x0a1, 0x1f6, 0x34f, 0x12b, 0x280, 0x3d9, 0x0a2, 0x1f7,
|
||||||
|
0x350, 0x12c, 0x281, 0x3da, 0x0a3, 0x1f8, 0x351, 0x12d, 0x282, 0x3db, 0x0a4, 0x1f9, 0x352, 0x12e, 0x283, 0x3dc, 0x0a5,
|
||||||
|
0x1fa, 0x353, 0x12f, 0x284, 0x3dd, 0x0a6, 0x1fb, 0x354, 0x130, 0x285, 0x3de, 0x0a7, 0x1fc, 0x355, 0x131, 0x286, 0x3df,
|
||||||
|
0x0a8, 0x1fd, 0x356, 0x132,
|
||||||
|
];
|
||||||
|
|
||||||
|
//prettier-ignore: uint16, size 1024 (64x16)
|
||||||
|
const x3mScrambleTable = [
|
||||||
|
0x256, 0x28d, 0x213, 0x307, 0x156, 0x39d, 0x062, 0x170, 0x3ca, 0x035, 0x0ed, 0x2a4, 0x1e4, 0x359, 0x0d3, 0x26b, 0x265,
|
||||||
|
0x274, 0x251, 0x297, 0x202, 0x322, 0x126, 0x32b, 0x117, 0x302, 0x15c, 0x3a8, 0x057, 0x148, 0x380, 0x090, 0x1f6, 0x335,
|
||||||
|
0x10c, 0x2ee, 0x175, 0x3d4, 0x02b, 0x0cc, 0x260, 0x27b, 0x23d, 0x2bb, 0x1b6, 0x3a1, 0x05e, 0x157, 0x39e, 0x061, 0x16f,
|
||||||
|
0x3c6, 0x039, 0x0f7, 0x2b9, 0x1b8, 0x39f, 0x060, 0x166, 0x3b9, 0x046, 0x122, 0x31c, 0x12f, 0x33d, 0x0fc, 0x2ca, 0x1a4,
|
||||||
|
0x3cc, 0x033, 0x0e6, 0x293, 0x209, 0x315, 0x13d, 0x358, 0x0d5, 0x26e, 0x25e, 0x27d, 0x23a, 0x2c0, 0x1b1, 0x3af, 0x050,
|
||||||
|
0x136, 0x346, 0x0ef, 0x2aa, 0x1ce, 0x376, 0x0a0, 0x210, 0x30c, 0x14c, 0x389, 0x082, 0x1db, 0x367, 0x0b9, 0x23e, 0x2ba,
|
||||||
|
0x1b7, 0x3a0, 0x05f, 0x164, 0x3b7, 0x048, 0x125, 0x326, 0x11c, 0x30a, 0x14f, 0x38f, 0x070, 0x1a8, 0x3c7, 0x038, 0x0f5,
|
||||||
|
0x2b5, 0x1bd, 0x393, 0x06c, 0x199, 0x3e1, 0x01e, 0x0b3, 0x22f, 0x2d7, 0x193, 0x3ea, 0x015, 0x09d, 0x20a, 0x314, 0x13e,
|
||||||
|
0x35a, 0x0d2, 0x26a, 0x267, 0x272, 0x253, 0x294, 0x208, 0x319, 0x137, 0x34c, 0x0e7, 0x295, 0x205, 0x31d, 0x12e, 0x33c,
|
||||||
|
0x0fe, 0x2cd, 0x1a0, 0x3d5, 0x02a, 0x0c8, 0x258, 0x286, 0x22a, 0x2dc, 0x18e, 0x3f7, 0x008, 0x07c, 0x1d3, 0x370, 0x0a7,
|
||||||
|
0x21d, 0x2f1, 0x171, 0x3cd, 0x032, 0x0e5, 0x292, 0x20b, 0x313, 0x13f, 0x35c, 0x0d0, 0x266, 0x273, 0x252, 0x296, 0x204,
|
||||||
|
0x31f, 0x12a, 0x332, 0x10f, 0x2f4, 0x16c, 0x3c3, 0x03c, 0x101, 0x2d2, 0x19a, 0x3e0, 0x01f, 0x0b5, 0x233, 0x2c9, 0x1a6,
|
||||||
|
0x3c9, 0x036, 0x0f0, 0x2ab, 0x1cb, 0x37c, 0x095, 0x1fd, 0x328, 0x11a, 0x306, 0x158, 0x3a2, 0x05d, 0x155, 0x39c, 0x063,
|
||||||
|
0x174, 0x3d3, 0x02c, 0x0cf, 0x264, 0x275, 0x24f, 0x299, 0x1fa, 0x32c, 0x115, 0x2ff, 0x15f, 0x3ab, 0x054, 0x143, 0x36c,
|
||||||
|
0x0ad, 0x225, 0x2e5, 0x181, 0x3ef, 0x010, 0x08c, 0x1f1, 0x344, 0x0f3, 0x2af, 0x1c4, 0x386, 0x088, 0x1e3, 0x35b, 0x0d1,
|
||||||
|
0x269, 0x268, 0x26d, 0x25f, 0x27c, 0x23b, 0x2bf, 0x1b2, 0x3ae, 0x051, 0x13b, 0x355, 0x0da, 0x278, 0x248, 0x2a6, 0x1dc,
|
||||||
|
0x365, 0x0c0, 0x246, 0x2a8, 0x1d6, 0x36d, 0x0ac, 0x224, 0x2e8, 0x17e, 0x3eb, 0x014, 0x09c, 0x207, 0x31a, 0x133, 0x341,
|
||||||
|
0x0f8, 0x2bc, 0x1b5, 0x3a3, 0x05c, 0x152, 0x395, 0x06a, 0x18c, 0x3f9, 0x006, 0x07a, 0x1d1, 0x373, 0x0a4, 0x217, 0x2fe,
|
||||||
|
0x160, 0x3ad, 0x052, 0x13c, 0x357, 0x0d7, 0x270, 0x25c, 0x281, 0x235, 0x2c6, 0x1aa, 0x3bc, 0x043, 0x11d, 0x30d, 0x14a,
|
||||||
|
0x384, 0x08a, 0x1e7, 0x353, 0x0dd, 0x284, 0x22e, 0x2d8, 0x192, 0x3ec, 0x013, 0x099, 0x201, 0x323, 0x124, 0x321, 0x127,
|
||||||
|
0x32d, 0x114, 0x2fd, 0x161, 0x3b0, 0x04f, 0x135, 0x343, 0x0f4, 0x2b4, 0x1be, 0x392, 0x06d, 0x19d, 0x3db, 0x024, 0x0be,
|
||||||
|
0x244, 0x2b0, 0x1c2, 0x38a, 0x080, 0x1d9, 0x369, 0x0b6, 0x234, 0x2c8, 0x1a7, 0x3c8, 0x037, 0x0f1, 0x2ad, 0x1c6, 0x383,
|
||||||
|
0x08d, 0x1f2, 0x33b, 0x100, 0x2d1, 0x19b, 0x3de, 0x021, 0x0bb, 0x240, 0x2b6, 0x1bb, 0x399, 0x066, 0x17a, 0x3df, 0x020,
|
||||||
|
0x0b8, 0x23c, 0x2bd, 0x1b4, 0x3a5, 0x05a, 0x150, 0x390, 0x06f, 0x1a5, 0x3cb, 0x034, 0x0ea, 0x29d, 0x1ee, 0x348, 0x0ec,
|
||||||
|
0x2a3, 0x1e5, 0x356, 0x0d8, 0x271, 0x257, 0x289, 0x220, 0x2ec, 0x178, 0x3d9, 0x026, 0x0c2, 0x24b, 0x2a1, 0x1ea, 0x34d,
|
||||||
|
0x0e4, 0x291, 0x20c, 0x312, 0x141, 0x360, 0x0ca, 0x25a, 0x283, 0x230, 0x2d0, 0x19c, 0x3dd, 0x022, 0x0bc, 0x241, 0x2b3,
|
||||||
|
0x1bf, 0x391, 0x06e, 0x1a2, 0x3d1, 0x02e, 0x0d6, 0x26f, 0x25d, 0x27f, 0x237, 0x2c4, 0x1ac, 0x3ba, 0x045, 0x121, 0x318,
|
||||||
|
0x138, 0x34e, 0x0e3, 0x28f, 0x211, 0x30b, 0x14d, 0x38c, 0x073, 0x1c3, 0x387, 0x084, 0x1df, 0x362, 0x0c7, 0x255, 0x28e,
|
||||||
|
0x212, 0x309, 0x153, 0x396, 0x069, 0x18b, 0x3fa, 0x005, 0x079, 0x1d0, 0x374, 0x0a2, 0x215, 0x301, 0x15d, 0x3a9, 0x056,
|
||||||
|
0x147, 0x37a, 0x098, 0x200, 0x324, 0x11f, 0x316, 0x13a, 0x352, 0x0df, 0x288, 0x223, 0x2e9, 0x17d, 0x3e9, 0x016, 0x09e,
|
||||||
|
0x20d, 0x310, 0x144, 0x372, 0x0a5, 0x219, 0x2fa, 0x165, 0x3b8, 0x047, 0x123, 0x31e, 0x12d, 0x338, 0x107, 0x2e0, 0x188,
|
||||||
|
0x3fe, 0x001, 0x075, 0x1c9, 0x37e, 0x093, 0x1f9, 0x32f, 0x112, 0x2f9, 0x167, 0x3be, 0x041, 0x10b, 0x2e7, 0x17f, 0x3ed,
|
||||||
|
0x012, 0x097, 0x1ff, 0x325, 0x11e, 0x311, 0x142, 0x366, 0x0ba, 0x23f, 0x2b8, 0x1b9, 0x39b, 0x064, 0x176, 0x3d6, 0x029,
|
||||||
|
0x0c5, 0x250, 0x298, 0x1fc, 0x329, 0x119, 0x304, 0x15a, 0x3a6, 0x059, 0x14e, 0x38e, 0x071, 0x1ad, 0x3b6, 0x049, 0x128,
|
||||||
|
0x32e, 0x113, 0x2fc, 0x162, 0x3b2, 0x04d, 0x131, 0x33f, 0x0fa, 0x2c2, 0x1af, 0x3b3, 0x04c, 0x130, 0x33e, 0x0fb, 0x2c7,
|
||||||
|
0x1a9, 0x3bd, 0x042, 0x116, 0x300, 0x15e, 0x3aa, 0x055, 0x146, 0x378, 0x09b, 0x206, 0x31b, 0x132, 0x340, 0x0f9, 0x2be,
|
||||||
|
0x1b3, 0x3ac, 0x053, 0x140, 0x35d, 0x0ce, 0x262, 0x279, 0x247, 0x2a7, 0x1d7, 0x36b, 0x0ae, 0x226, 0x2e3, 0x185, 0x3f6,
|
||||||
|
0x009, 0x07d, 0x1d4, 0x36f, 0x0a8, 0x21e, 0x2f0, 0x172, 0x3ce, 0x031, 0x0de, 0x287, 0x228, 0x2df, 0x189, 0x3fd, 0x002,
|
||||||
|
0x076, 0x1ca, 0x37d, 0x094, 0x1fb, 0x32a, 0x118, 0x303, 0x15b, 0x3a7, 0x058, 0x14b, 0x388, 0x083, 0x1dd, 0x364, 0x0c1,
|
||||||
|
0x24a, 0x2a2, 0x1e9, 0x350, 0x0e1, 0x28b, 0x21a, 0x2f8, 0x168, 0x3bf, 0x040, 0x10a, 0x2e6, 0x180, 0x3ee, 0x011, 0x091,
|
||||||
|
0x1f7, 0x334, 0x10d, 0x2ef, 0x173, 0x3cf, 0x030, 0x0dc, 0x280, 0x236, 0x2c5, 0x1ab, 0x3bb, 0x044, 0x120, 0x317, 0x139,
|
||||||
|
0x34f, 0x0e2, 0x28c, 0x218, 0x2fb, 0x163, 0x3b4, 0x04b, 0x12c, 0x337, 0x108, 0x2e2, 0x186, 0x3fc, 0x003, 0x077, 0x1cc,
|
||||||
|
0x37b, 0x096, 0x1fe, 0x327, 0x11b, 0x308, 0x154, 0x397, 0x068, 0x183, 0x3f3, 0x00c, 0x085, 0x1e0, 0x361, 0x0c9, 0x259,
|
||||||
|
0x285, 0x22c, 0x2da, 0x190, 0x3f2, 0x00d, 0x086, 0x1e1, 0x35f, 0x0cb, 0x25b, 0x282, 0x232, 0x2cc, 0x1a1, 0x3d2, 0x02d,
|
||||||
|
0x0d4, 0x26c, 0x263, 0x277, 0x249, 0x2a5, 0x1de, 0x363, 0x0c6, 0x254, 0x290, 0x20e, 0x30f, 0x145, 0x377, 0x09f, 0x20f,
|
||||||
|
0x30e, 0x149, 0x382, 0x08e, 0x1f3, 0x33a, 0x102, 0x2d3, 0x198, 0x3e2, 0x01d, 0x0b2, 0x22d, 0x2d9, 0x191, 0x3f1, 0x00e,
|
||||||
|
0x087, 0x1e2, 0x35e, 0x0cd, 0x261, 0x27a, 0x243, 0x2b1, 0x1c1, 0x38b, 0x07f, 0x1d8, 0x36a, 0x0b4, 0x231, 0x2cf, 0x19e,
|
||||||
|
0x3da, 0x025, 0x0bf, 0x245, 0x2ac, 0x1c7, 0x381, 0x08f, 0x1f5, 0x336, 0x109, 0x2e4, 0x184, 0x3f4, 0x00b, 0x081, 0x1da,
|
||||||
|
0x368, 0x0b7, 0x239, 0x2c1, 0x1b0, 0x3b1, 0x04e, 0x134, 0x342, 0x0f6, 0x2b7, 0x1ba, 0x39a, 0x065, 0x179, 0x3dc, 0x023,
|
||||||
|
0x0bd, 0x242, 0x2b2, 0x1c0, 0x38d, 0x072, 0x1bc, 0x398, 0x067, 0x182, 0x3f0, 0x00f, 0x08b, 0x1e8, 0x351, 0x0e0, 0x28a,
|
||||||
|
0x21c, 0x2f3, 0x16d, 0x3c4, 0x03b, 0x0ff, 0x2ce, 0x19f, 0x3d7, 0x028, 0x0c4, 0x24e, 0x29b, 0x1f0, 0x345, 0x0f2, 0x2ae,
|
||||||
|
0x1c5, 0x385, 0x089, 0x1e6, 0x354, 0x0db, 0x27e, 0x238, 0x2c3, 0x1ae, 0x3b5, 0x04a, 0x12b, 0x333, 0x10e, 0x2f2, 0x16e,
|
||||||
|
0x3c5, 0x03a, 0x0fd, 0x2cb, 0x1a3, 0x3d0, 0x02f, 0x0d9, 0x276, 0x24c, 0x29f, 0x1ec, 0x34a, 0x0e9, 0x29c, 0x1ef, 0x347,
|
||||||
|
0x0ee, 0x2a9, 0x1cf, 0x375, 0x0a1, 0x214, 0x305, 0x159, 0x3a4, 0x05b, 0x151, 0x394, 0x06b, 0x196, 0x3e6, 0x019, 0x0ab,
|
||||||
|
0x222, 0x2ea, 0x17c, 0x3e5, 0x01a, 0x0af, 0x227, 0x2e1, 0x187, 0x3ff, 0x000, 0x074, 0x1c8, 0x37f, 0x092, 0x1f8, 0x331,
|
||||||
|
0x110, 0x2f6, 0x16a, 0x3c1, 0x03e, 0x104, 0x2d5, 0x195, 0x3e7, 0x018, 0x0aa, 0x221, 0x2eb, 0x17b, 0x3e4, 0x01b, 0x0b0,
|
||||||
|
0x229, 0x2dd, 0x18d, 0x3f8, 0x007, 0x07b, 0x1d2, 0x371, 0x0a6, 0x21b, 0x2f5, 0x16b, 0x3c2, 0x03d, 0x103, 0x2d4, 0x197,
|
||||||
|
0x3e3, 0x01c, 0x0b1, 0x22b, 0x2db, 0x18f, 0x3f5, 0x00a, 0x07e, 0x1d5, 0x36e, 0x0a9, 0x21f, 0x2ed, 0x177, 0x3d8, 0x027,
|
||||||
|
0x0c3, 0x24d, 0x29e, 0x1ed, 0x349, 0x0eb, 0x2a0, 0x1eb, 0x34b, 0x0e8, 0x29a, 0x1f4, 0x339, 0x106, 0x2de, 0x18a, 0x3fb,
|
||||||
|
0x004, 0x078, 0x1cd, 0x379, 0x09a, 0x203, 0x320, 0x129, 0x330, 0x111, 0x2f7, 0x169, 0x3c0, 0x03f, 0x105, 0x2d6, 0x194,
|
||||||
|
0x3e8, 0x017, 0x0a3, 0x216,
|
||||||
|
];
|
@ -49,7 +49,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
|||||||
const { title, artist } = GetMetaFromFile(
|
const { title, artist } = GetMetaFromFile(
|
||||||
raw_filename,
|
raw_filename,
|
||||||
musicMeta.common.title,
|
musicMeta.common.title,
|
||||||
musicMeta.common.artist,
|
String(musicMeta.common.artists || musicMeta.common.artist || ""),
|
||||||
raw_filename.indexOf('_') === -1 ? '-' : '_',
|
raw_filename.indexOf('_') === -1 ? '-' : '_',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -10,235 +10,45 @@
|
|||||||
background-color: $dark-bg;
|
background-color: $dark-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORM
|
// 编辑歌曲信息
|
||||||
.el-radio{
|
.music-cover{
|
||||||
&__label{
|
i{
|
||||||
color: $dark-text-main;
|
|
||||||
}
|
|
||||||
&__input{
|
|
||||||
color: $dark-text-info;
|
|
||||||
.el-radio__inner{
|
|
||||||
border-color: $dark-border;
|
|
||||||
background-color: $dark-btn-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-checked{
|
|
||||||
.el-radio__inner{
|
|
||||||
background-color: $blue;
|
|
||||||
}
|
|
||||||
.el-radio__label{
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-checkbox.is-bordered{
|
|
||||||
border-color: $dark-border;
|
|
||||||
color: $dark-text-main;
|
|
||||||
background-color: $dark-btn-bg;
|
|
||||||
.el-checkbox__inner{
|
|
||||||
background-color: $dark-btn-bg-highlight;
|
|
||||||
border-color: $dark-border-highlight;
|
|
||||||
}
|
|
||||||
&:hover{
|
&:hover{
|
||||||
border-color: $dark-border-highlight;
|
color: $color-checkbox;
|
||||||
.el-checkbox__inner{
|
|
||||||
background-color: $dark-btn-bg-highlight;
|
|
||||||
border-color: $dark-border-highlight;
|
|
||||||
}
|
|
||||||
.el-checkbox__label{
|
|
||||||
color: $dark-text-info;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.is-checked{
|
.el-image{
|
||||||
background-color: $blue;
|
border: 1px solid $dark-border;
|
||||||
.el-checkbox__inner{
|
|
||||||
border-color: white;
|
|
||||||
}
|
}
|
||||||
.el-checkbox__label{
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
.edit-item{
|
||||||
|
.label{
|
||||||
|
}
|
||||||
|
.value{
|
||||||
|
}
|
||||||
|
.input{
|
||||||
|
input{
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-bottom: 1px solid $dark-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i{
|
||||||
&:hover{
|
&:hover{
|
||||||
border-color: $blue;
|
color: $color-checkbox;
|
||||||
.el-checkbox__inner{
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// footer
|
||||||
// BUTTON
|
#app-footer {
|
||||||
.el-button{
|
|
||||||
background-color: $dark-btn-bg;
|
|
||||||
border-color: $dark-border;
|
|
||||||
color: $dark-text-main;
|
|
||||||
|
|
||||||
&:active{
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--default{
|
|
||||||
&.is-plain {
|
|
||||||
background-color: $dark-btn-bg;
|
|
||||||
&:hover {
|
|
||||||
background-color: $blue;
|
|
||||||
border-color: $blue;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.is-circle {
|
|
||||||
background-color: $dark-blue;
|
|
||||||
border-color: $dark-blue;
|
|
||||||
&:hover {
|
|
||||||
background-color: $blue;
|
|
||||||
border-color: $blue;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--success{
|
|
||||||
&.is-plain {
|
|
||||||
background-color: $dark-btn-bg;
|
|
||||||
&:hover {
|
|
||||||
background-color: $green;
|
|
||||||
border-color: $green;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.is-circle {
|
|
||||||
background-color: $dark-green;
|
|
||||||
border-color: $dark-green;
|
|
||||||
&:hover {
|
|
||||||
background-color: $green;
|
|
||||||
border-color: $green;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&--danger{
|
|
||||||
&.is-plain{
|
|
||||||
border-color: $dark-border;
|
|
||||||
background-color: $dark-btn-bg;
|
|
||||||
&:hover{
|
|
||||||
background-color: $red;
|
|
||||||
border-color: $red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.is-circle {
|
|
||||||
background-color: $dark-red;
|
|
||||||
border-color: $dark-red;
|
|
||||||
&:hover {
|
|
||||||
background-color: $red;
|
|
||||||
border-color: $red;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件拖放区
|
|
||||||
.el-upload__tip{
|
|
||||||
color: $dark-text-info;
|
|
||||||
}
|
|
||||||
.el-upload-dragger{
|
|
||||||
background-color: $dark-uploader-bg;
|
|
||||||
border-color: $dark-border;
|
|
||||||
.el-upload__text{
|
|
||||||
color: $dark-text-info;
|
|
||||||
}
|
|
||||||
&:hover{
|
|
||||||
background: $dark-uploader-bg-highlight;
|
|
||||||
border-color: $dark-border-highlight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TABLE
|
|
||||||
.el-table{
|
|
||||||
background-color: $dark-bg-td;
|
|
||||||
&:before{ // 去除表格末尾的横线
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
&__header{
|
|
||||||
th{
|
|
||||||
border-bottom-color: $dark-border !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
th.el-table__cell{
|
|
||||||
background-color: $dark-bg-th;
|
|
||||||
color: $dark-text-info;
|
|
||||||
}
|
|
||||||
td{
|
|
||||||
border-bottom-color: $dark-border !important;
|
|
||||||
}
|
|
||||||
tr{
|
|
||||||
background-color: $dark-bg-td;
|
|
||||||
color: $dark-text-main;
|
|
||||||
&:hover{
|
|
||||||
td{
|
|
||||||
background-color: $dark-bg-th !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LINKS
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
color: lighten($text-comment, 5%);
|
||||||
color: darken($dark-color-link, 15%);
|
|
||||||
&:hover{
|
&:hover{
|
||||||
color: $dark-color-link;
|
color: $color-link;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ALERT
|
|
||||||
.el-notification{
|
|
||||||
background-color: $dark-btn-bg-highlight;
|
|
||||||
border-color: $dark-border;
|
|
||||||
&__title{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&__content{
|
|
||||||
color: $dark-text-info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DIALOG
|
|
||||||
.el-dialog{
|
|
||||||
background-color: $dark-dialog-bg;
|
|
||||||
.el-dialog__header{
|
|
||||||
.el-dialog__title{
|
|
||||||
color: $dark-text-main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-dialog__body{
|
|
||||||
color: $dark-text-main;
|
|
||||||
.el-input{
|
|
||||||
.el-input__inner{
|
|
||||||
color: $dark-text-main;
|
|
||||||
background-color: $dark-btn-bg;
|
|
||||||
}
|
|
||||||
.el-input__suffix{
|
|
||||||
.el-input__suffix-inner{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-input__count{
|
|
||||||
.el-input__count-inner{
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.item-desc{
|
|
||||||
color: $dark-text-info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 自定义样式
|
// 自定义样式
|
||||||
// 首页弹窗提示信息的 更新信息 面板
|
// 首页弹窗提示信息的 更新信息 面板
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
$color-checkbox: $blue;
|
|
||||||
$color-border-el: #DCDFE6;
|
|
||||||
|
|
||||||
$btn-radius: 6px;
|
|
||||||
|
|
||||||
/* FORM */
|
|
||||||
// checkbox
|
|
||||||
.el-checkbox.is-bordered{
|
|
||||||
@include border-radius($btn-radius) ;
|
|
||||||
&:hover{
|
|
||||||
border-color: $color-checkbox;
|
|
||||||
.el-checkbox__label{
|
|
||||||
color: $color-checkbox;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-checkbox__input.is-focus{
|
|
||||||
.el-checkbox__inner{
|
|
||||||
border-color: $color-border-el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.is-checked{
|
|
||||||
background-color: $color-checkbox;
|
|
||||||
.el-checkbox__label{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.el-checkbox__inner{
|
|
||||||
border-color: white;
|
|
||||||
background-color: white;
|
|
||||||
&:after{
|
|
||||||
border-color: $color-checkbox;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// el-button
|
|
||||||
.el-button{
|
|
||||||
@include border-radius($btn-radius) ;
|
|
||||||
}
|
|
291
src/scss/_element-ui-overwrite.scss
Normal file
291
src/scss/_element-ui-overwrite.scss
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
$color-checkbox: $blue;
|
||||||
|
$color-border-el: #DCDFE6;
|
||||||
|
|
||||||
|
$btn-radius: 6px;
|
||||||
|
|
||||||
|
/* FORM */
|
||||||
|
// checkbox
|
||||||
|
.el-checkbox.is-bordered{
|
||||||
|
@include border-radius($btn-radius) ;
|
||||||
|
&:hover{
|
||||||
|
border-color: $color-checkbox;
|
||||||
|
.el-checkbox__label{
|
||||||
|
color: $color-checkbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-checkbox__input.is-focus{
|
||||||
|
.el-checkbox__inner{
|
||||||
|
border-color: $color-border-el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-checked{
|
||||||
|
background-color: $color-checkbox;
|
||||||
|
.el-checkbox__label{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.el-checkbox__inner{
|
||||||
|
border-color: white;
|
||||||
|
background-color: white;
|
||||||
|
&:after{
|
||||||
|
border-color: $color-checkbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// el-button
|
||||||
|
.el-button{
|
||||||
|
@include border-radius($btn-radius) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload
|
||||||
|
.el-upload-dragger{
|
||||||
|
&:hover{
|
||||||
|
background-color: transparentize($color-checkbox, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-upload__tip{
|
||||||
|
text-align: center;
|
||||||
|
color: $text-comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// dialog
|
||||||
|
.el-dialog{
|
||||||
|
@include border-radius(5px);
|
||||||
|
&.el-dialog--center{
|
||||||
|
.el-dialog__body{
|
||||||
|
padding: 25px 25px 15px;
|
||||||
|
}
|
||||||
|
.el-dialog__footer{
|
||||||
|
padding: 10px 20px 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
||||||
|
// FORM
|
||||||
|
.el-radio{
|
||||||
|
&__label{
|
||||||
|
color: $dark-text-main;
|
||||||
|
}
|
||||||
|
&__input{
|
||||||
|
color: $dark-text-info;
|
||||||
|
.el-radio__inner{
|
||||||
|
border-color: $dark-border;
|
||||||
|
background-color: $dark-btn-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-checked{
|
||||||
|
.el-radio__inner{
|
||||||
|
background-color: $blue;
|
||||||
|
}
|
||||||
|
.el-radio__label{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-checkbox.is-bordered{
|
||||||
|
border-color: $dark-border;
|
||||||
|
color: $dark-text-main;
|
||||||
|
background-color: $dark-btn-bg;
|
||||||
|
.el-checkbox__inner{
|
||||||
|
background-color: $dark-btn-bg-highlight;
|
||||||
|
border-color: $dark-border-highlight;
|
||||||
|
}
|
||||||
|
&:hover{
|
||||||
|
border-color: $dark-border-highlight;
|
||||||
|
.el-checkbox__inner{
|
||||||
|
background-color: $dark-btn-bg-highlight;
|
||||||
|
border-color: $dark-border-highlight;
|
||||||
|
}
|
||||||
|
.el-checkbox__label{
|
||||||
|
color: $dark-text-info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-checked{
|
||||||
|
background-color: $blue;
|
||||||
|
.el-checkbox__inner{
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
.el-checkbox__label{
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
&:hover{
|
||||||
|
border-color: $blue;
|
||||||
|
.el-checkbox__inner{
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// BUTTON
|
||||||
|
.el-button{
|
||||||
|
background-color: $dark-btn-bg;
|
||||||
|
border-color: $dark-border;
|
||||||
|
color: $dark-text-main;
|
||||||
|
|
||||||
|
&:active{
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--default{
|
||||||
|
&.is-plain {
|
||||||
|
background-color: $dark-btn-bg;
|
||||||
|
&:hover {
|
||||||
|
background-color: $blue;
|
||||||
|
border-color: $blue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-circle {
|
||||||
|
background-color: $dark-blue;
|
||||||
|
border-color: $dark-blue;
|
||||||
|
&:hover {
|
||||||
|
background-color: $blue;
|
||||||
|
border-color: $blue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--success{
|
||||||
|
&.is-plain {
|
||||||
|
background-color: $dark-btn-bg;
|
||||||
|
&:hover {
|
||||||
|
background-color: $green;
|
||||||
|
border-color: $green;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-circle {
|
||||||
|
background-color: $dark-green;
|
||||||
|
border-color: $dark-green;
|
||||||
|
&:hover {
|
||||||
|
background-color: $green;
|
||||||
|
border-color: $green;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--danger{
|
||||||
|
&.is-plain{
|
||||||
|
border-color: $dark-border;
|
||||||
|
background-color: $dark-btn-bg;
|
||||||
|
&:hover{
|
||||||
|
background-color: $red;
|
||||||
|
border-color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-circle {
|
||||||
|
background-color: $dark-red;
|
||||||
|
border-color: $dark-red;
|
||||||
|
&:hover {
|
||||||
|
background-color: $red;
|
||||||
|
border-color: $red;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件拖放区
|
||||||
|
.el-upload__tip{
|
||||||
|
color: $dark-text-info;
|
||||||
|
}
|
||||||
|
.el-upload-dragger{
|
||||||
|
background-color: $dark-uploader-bg;
|
||||||
|
border-color: $dark-border;
|
||||||
|
.el-upload__text{
|
||||||
|
color: $dark-text-info;
|
||||||
|
}
|
||||||
|
&:hover{
|
||||||
|
background: $dark-uploader-bg-highlight;
|
||||||
|
border-color: $dark-border-highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TABLE
|
||||||
|
.el-table{
|
||||||
|
background-color: $dark-bg-td;
|
||||||
|
&:before{ // 去除表格末尾的横线
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
&__header{
|
||||||
|
th{
|
||||||
|
border-bottom-color: $dark-border !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
th.el-table__cell{
|
||||||
|
background-color: $dark-bg-th;
|
||||||
|
color: $dark-text-info;
|
||||||
|
}
|
||||||
|
td{
|
||||||
|
border-bottom-color: $dark-border !important;
|
||||||
|
}
|
||||||
|
tr{
|
||||||
|
background-color: $dark-bg-td;
|
||||||
|
color: $dark-text-main;
|
||||||
|
&:hover{
|
||||||
|
td{
|
||||||
|
background-color: $dark-bg-th !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ALERT
|
||||||
|
.el-notification{
|
||||||
|
background-color: $dark-btn-bg-highlight;
|
||||||
|
border-color: $dark-border;
|
||||||
|
&__title{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
&__content{
|
||||||
|
color: $dark-text-info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DIALOG
|
||||||
|
.el-dialog{
|
||||||
|
background-color: $dark-dialog-bg;
|
||||||
|
.el-dialog__header{
|
||||||
|
.el-dialog__title{
|
||||||
|
color: $dark-text-main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-dialog__body{
|
||||||
|
color: $dark-text-main;
|
||||||
|
.el-input{
|
||||||
|
.el-input__inner{
|
||||||
|
border-color: $dark-border;
|
||||||
|
color: $dark-text-main;
|
||||||
|
background-color: $dark-btn-bg;
|
||||||
|
}
|
||||||
|
.el-input__suffix{
|
||||||
|
.el-input__suffix-inner{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-input__count{
|
||||||
|
.el-input__count-inner{
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-desc{
|
||||||
|
color: $dark-text-info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
body{
|
|
||||||
font-family: $font-family;
|
|
||||||
font-size: $fz-main;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
text-align: center;
|
|
||||||
color: $text-main;
|
|
||||||
padding-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app-footer a {
|
|
||||||
padding-left: 0.2em;
|
|
||||||
padding-right: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app-footer {
|
|
||||||
text-align: center;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app-control {
|
|
||||||
padding-top: 1em;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio{
|
|
||||||
margin-bottom: 15px; // 播放控件与表格间隔
|
|
||||||
}
|
|
||||||
|
|
||||||
a{
|
|
||||||
color: darken($color-link, 15%);
|
|
||||||
&:hover{
|
|
||||||
color: $color-link;
|
|
||||||
}
|
|
||||||
}
|
|
@ -66,6 +66,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-like{
|
.btn-like{
|
||||||
|
cursor: pointer;
|
||||||
&:active{
|
&:active{
|
||||||
@include transform(translateY(2px))
|
@include transform(translateY(2px))
|
||||||
}
|
}
|
||||||
|
@ -3,18 +3,17 @@ $blue : #409EFF;
|
|||||||
$red : #F56C6C;
|
$red : #F56C6C;
|
||||||
$green : #85ce61;
|
$green : #85ce61;
|
||||||
|
|
||||||
// TEXT
|
// TEXT COLOR
|
||||||
$text-main : #2C3E50;
|
$text-main : #2C3E50;
|
||||||
|
$text-copyright : #777;
|
||||||
|
$text-comment : #999;
|
||||||
$color-link : $blue;
|
$color-link : $blue;
|
||||||
|
|
||||||
|
// FONT SIZE
|
||||||
$fz-main: 14px;
|
$fz-main: 14px;
|
||||||
$fz-mini-title: 13px;
|
$fz-mini-title: 13px;
|
||||||
$fz-mini-content: 12px;
|
$fz-mini-content: 12px;
|
||||||
|
|
||||||
$font-family: "Helvetica Neue", Helvetica, "PingFang SC",
|
|
||||||
"Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
|
||||||
|
|
||||||
|
|
||||||
// DARK MODE
|
// DARK MODE
|
||||||
$dark-border : lighten(black, 25%);
|
$dark-border : lighten(black, 25%);
|
||||||
$dark-border-highlight : lighten(black, 55%);
|
$dark-border-highlight : lighten(black, 55%);
|
||||||
|
@ -1,11 +1,127 @@
|
|||||||
@import "variables";
|
@import "variables";
|
||||||
@import "utility";
|
@import "utility";
|
||||||
@import "gaps";
|
@import "gaps";
|
||||||
@import "element-ui-overrite";
|
@import "element-ui-overwrite";
|
||||||
|
|
||||||
@import "normal";
|
// MAIN CONTENT
|
||||||
@import "dark-mode"; // dark-mode 放在 normal 后面,以获得更高优先级
|
body{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "PingFang SC", "微软雅黑", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
|
||||||
|
font-size: $fz-main;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
text-align: center;
|
||||||
|
color: $text-main;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频文件操作
|
||||||
|
#app-control {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频播放
|
||||||
|
audio{
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-content{
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑歌曲信息
|
||||||
|
.music-cover{
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
i{
|
||||||
|
margin-top: 10px;
|
||||||
|
@extend .btn-like;
|
||||||
|
&:hover{
|
||||||
|
color: $color-checkbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-image{
|
||||||
|
padding: 5px;
|
||||||
|
@include border-radius(5px);
|
||||||
|
border: 1px solid $color-border-el;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edit-item{
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
.label{
|
||||||
|
font-weight: bold;
|
||||||
|
width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.value{
|
||||||
|
padding: 5px 0;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-left: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.input{
|
||||||
|
margin-left: 10px;
|
||||||
|
input{
|
||||||
|
font-family: inherit;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 20px;
|
||||||
|
@include border-radius(0);
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid $color-border-el;
|
||||||
|
padding: 5px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i{
|
||||||
|
margin-left: 10px;
|
||||||
|
@extend .btn-like;
|
||||||
|
&:hover{
|
||||||
|
color: $color-checkbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip{
|
||||||
|
margin-top: 20px;
|
||||||
|
color: $text-comment;
|
||||||
|
font-size: $fz-mini-content;
|
||||||
|
a{
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// footer
|
||||||
|
#app-footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-copyright;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: $fz-mini-content;
|
||||||
|
a {
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
color: darken($text-copyright, 10%);
|
||||||
|
&:hover{
|
||||||
|
color: $color-link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 首页弹窗提示信息的 更新信息 面板
|
// 首页弹窗提示信息的 更新信息 面板
|
||||||
.update-info{
|
.update-info{
|
||||||
@ -15,12 +131,14 @@
|
|||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
.update-title{
|
.update-title{
|
||||||
font-size: $fz-mini-title;
|
font-size: $fz-mini-title;
|
||||||
padding: 5px 10px;
|
padding: 3px 10px;
|
||||||
background-color: $color-border-el;
|
background-color: $color-border-el;
|
||||||
}
|
}
|
||||||
.update-content{
|
.update-content{
|
||||||
font-size: $fz-mini-content;
|
font-size: $fz-mini-content;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: 10px;
|
padding: 5px 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import "dark-mode"; // dark-mode 放在 normal 后面,以获得更高优先级
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
WriteMetaToFlac,
|
WriteMetaToFlac,
|
||||||
WriteMetaToMp3,
|
WriteMetaToMp3,
|
||||||
AudioMimeType,
|
AudioMimeType,
|
||||||
|
split_regex,
|
||||||
} from '@/decrypt/utils';
|
} from '@/decrypt/utils';
|
||||||
import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api';
|
import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api';
|
||||||
|
|
||||||
@ -19,6 +20,8 @@ interface MetaResult {
|
|||||||
blob: Blob;
|
blob: Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fromGBK = (text?: string) => iconv.decode(new Buffer(text || ''), 'gbk');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param musicBlob 音乐文件(解密后)
|
* @param musicBlob 音乐文件(解密后)
|
||||||
@ -38,15 +41,21 @@ export async function extractQQMusicMeta(
|
|||||||
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue;
|
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue;
|
||||||
if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) {
|
if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) {
|
||||||
console.warn('try using gbk encoding to decode meta');
|
console.warn('try using gbk encoding to decode meta');
|
||||||
musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ''), 'gbk');
|
musicMeta.common.artist = '';
|
||||||
musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ''), 'gbk');
|
if (!musicMeta.common.artists) {
|
||||||
musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ''), 'gbk');
|
musicMeta.common.artist = fromGBK(musicMeta.common.artist);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join();
|
||||||
|
}
|
||||||
|
musicMeta.common.title = fromGBK(musicMeta.common.title);
|
||||||
|
musicMeta.common.album = fromGBK(musicMeta.common.album);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id) {
|
if (id && id !== '0') {
|
||||||
try {
|
try {
|
||||||
return fetchMetadataFromSongId(id, ext, musicMeta, musicBlob);
|
return await fetchMetadataFromSongId(id, ext, musicMeta, musicBlob);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('在线获取曲目信息失败,回退到本地 meta 提取', e);
|
console.warn('在线获取曲目信息失败,回退到本地 meta 提取', e);
|
||||||
}
|
}
|
||||||
@ -62,12 +71,12 @@ export async function extractQQMusicMeta(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
title: info.title,
|
title: info.title,
|
||||||
artist: info.artist || '',
|
artist: info.artist,
|
||||||
album: musicMeta.common.album || '',
|
album: musicMeta.common.album || '',
|
||||||
imgUrl: imageURL,
|
imgUrl: imageURL,
|
||||||
blob: await writeMetaToAudioFile({
|
blob: await writeMetaToAudioFile({
|
||||||
title: info.title,
|
title: info.title,
|
||||||
artists: info.artist.split(' _ '),
|
artists: info.artist.split(split_regex),
|
||||||
ext,
|
ext,
|
||||||
imageURL,
|
imageURL,
|
||||||
musicMeta,
|
musicMeta,
|
||||||
@ -88,7 +97,7 @@ async function fetchMetadataFromSongId(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
title: info.track_info.title,
|
title: info.track_info.title,
|
||||||
artist: artists.join('、'),
|
artist: artists.join(','),
|
||||||
album: info.track_info.album.name,
|
album: info.track_info.album.name,
|
||||||
imgUrl: imageURL,
|
imgUrl: imageURL,
|
||||||
|
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
// Copyright 2021 MengYX. All rights reserved.
|
|
||||||
//
|
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in https://go.dev/LICENSE.
|
|
||||||
|
|
||||||
import { TeaCipher } from '@/utils/tea';
|
|
||||||
|
|
||||||
test('key size', () => {
|
|
||||||
// prettier-ignore
|
|
||||||
const testKey = new Uint8Array([
|
|
||||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
|
||||||
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
|
|
||||||
0x00,
|
|
||||||
])
|
|
||||||
expect(() => new TeaCipher(testKey.slice(0, 16))).not.toThrow();
|
|
||||||
|
|
||||||
expect(() => new TeaCipher(testKey)).toThrow();
|
|
||||||
|
|
||||||
expect(() => new TeaCipher(testKey.slice(0, 15))).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const teaTests = [
|
|
||||||
// These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
|
|
||||||
{
|
|
||||||
rounds: TeaCipher.numRounds,
|
|
||||||
key: new Uint8Array([
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
]),
|
|
||||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
|
||||||
cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rounds: TeaCipher.numRounds,
|
|
||||||
key: new Uint8Array([
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
||||||
]),
|
|
||||||
plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
|
|
||||||
cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rounds: 16,
|
|
||||||
key: new Uint8Array([
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
]),
|
|
||||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
|
||||||
cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
test('rounds', () => {
|
|
||||||
const tt = teaTests[0];
|
|
||||||
expect(() => new TeaCipher(tt.key, tt.rounds - 1)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('encrypt & decrypt', () => {
|
|
||||||
for (const tt of teaTests) {
|
|
||||||
const c = new TeaCipher(tt.key, tt.rounds);
|
|
||||||
|
|
||||||
const buf = new Uint8Array(8);
|
|
||||||
const bufView = new DataView(buf.buffer);
|
|
||||||
|
|
||||||
c.encrypt(bufView, new DataView(tt.plainText.buffer));
|
|
||||||
expect(buf).toStrictEqual(tt.cipherText);
|
|
||||||
|
|
||||||
c.decrypt(bufView, new DataView(tt.cipherText.buffer));
|
|
||||||
expect(buf).toStrictEqual(tt.plainText);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,80 +0,0 @@
|
|||||||
// Copyright 2021 MengYX. All rights reserved.
|
|
||||||
//
|
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in https://go.dev/LICENSE.
|
|
||||||
|
|
||||||
// TeaCipher is a typescript port to golang.org/x/crypto/tea
|
|
||||||
|
|
||||||
// Package tea implements the TEA algorithm, as defined in Needham and
|
|
||||||
// Wheeler's 1994 technical report, “TEA, a Tiny Encryption Algorithm”. See
|
|
||||||
// http://www.cix.co.uk/~klockstone/tea.pdf for details.
|
|
||||||
//
|
|
||||||
// TEA is a legacy cipher and its short block size makes it vulnerable to
|
|
||||||
// birthday bound attacks (see https://sweet32.info). It should only be used
|
|
||||||
// where compatibility with legacy systems, not security, is the goal.
|
|
||||||
|
|
||||||
export class TeaCipher {
|
|
||||||
// BlockSize is the size of a TEA block, in bytes.
|
|
||||||
static readonly BlockSize = 8;
|
|
||||||
|
|
||||||
// KeySize is the size of a TEA key, in bytes.
|
|
||||||
static readonly KeySize = 16;
|
|
||||||
|
|
||||||
// delta is the TEA key schedule constant.
|
|
||||||
static readonly delta = 0x9e3779b9;
|
|
||||||
|
|
||||||
// numRounds 64 is the standard number of rounds in TEA.
|
|
||||||
static readonly numRounds = 64;
|
|
||||||
|
|
||||||
k0: number;
|
|
||||||
k1: number;
|
|
||||||
k2: number;
|
|
||||||
k3: number;
|
|
||||||
rounds: number;
|
|
||||||
|
|
||||||
constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
|
|
||||||
if (key.length != 16) {
|
|
||||||
throw Error('incorrect key size');
|
|
||||||
}
|
|
||||||
if ((rounds & 1) != 0) {
|
|
||||||
throw Error('odd number of rounds specified');
|
|
||||||
}
|
|
||||||
|
|
||||||
const k = new DataView(key.buffer);
|
|
||||||
this.k0 = k.getUint32(0, false);
|
|
||||||
this.k1 = k.getUint32(4, false);
|
|
||||||
this.k2 = k.getUint32(8, false);
|
|
||||||
this.k3 = k.getUint32(12, false);
|
|
||||||
this.rounds = rounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypt(dst: DataView, src: DataView) {
|
|
||||||
let v0 = src.getUint32(0, false);
|
|
||||||
let v1 = src.getUint32(4, false);
|
|
||||||
|
|
||||||
let sum = 0;
|
|
||||||
for (let i = 0; i < this.rounds / 2; i++) {
|
|
||||||
sum = sum + TeaCipher.delta;
|
|
||||||
v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
|
|
||||||
v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.setUint32(0, v0, false);
|
|
||||||
dst.setUint32(4, v1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(dst: DataView, src: DataView) {
|
|
||||||
let v0 = src.getUint32(0, false);
|
|
||||||
let v1 = src.getUint32(4, false);
|
|
||||||
|
|
||||||
let sum = (TeaCipher.delta * this.rounds) / 2;
|
|
||||||
for (let i = 0; i < this.rounds / 2; i++) {
|
|
||||||
v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
|
|
||||||
v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
|
|
||||||
sum -= TeaCipher.delta;
|
|
||||||
}
|
|
||||||
dst.setUint32(0, v0, false);
|
|
||||||
dst.setUint32(4, v1, false);
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,6 +10,17 @@
|
|||||||
</el-radio>
|
</el-radio>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
|
<edit-dialog
|
||||||
|
:show="showEditDialog"
|
||||||
|
:picture="editing_data.picture"
|
||||||
|
:title="editing_data.title"
|
||||||
|
:artist="editing_data.artist"
|
||||||
|
:album="editing_data.album"
|
||||||
|
:albumartist="editing_data.albumartist"
|
||||||
|
:genre="editing_data.genre"
|
||||||
|
@cancel="showEditDialog = false"
|
||||||
|
@ok="handleEdit"
|
||||||
|
></edit-dialog>
|
||||||
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
|
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
|
||||||
<el-tooltip class="item" effect="dark" placement="top">
|
<el-tooltip class="item" effect="dark" placement="top">
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
@ -35,7 +46,13 @@
|
|||||||
|
|
||||||
<audio :autoplay="playing_auto" :src="playing_url" controls />
|
<audio :autoplay="playing_auto" :src="playing_url" controls />
|
||||||
|
|
||||||
<PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @play="changePlaying" />
|
<PreviewTable
|
||||||
|
class="table-content"
|
||||||
|
:policy="filename_policy"
|
||||||
|
:table-data="tableData"
|
||||||
|
@download="saveFile"
|
||||||
|
@edit="editFile"
|
||||||
|
@play="changePlaying" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -43,8 +60,11 @@
|
|||||||
import FileSelector from '@/component/FileSelector';
|
import FileSelector from '@/component/FileSelector';
|
||||||
import PreviewTable from '@/component/PreviewTable';
|
import PreviewTable from '@/component/PreviewTable';
|
||||||
import ConfigDialog from '@/component/ConfigDialog';
|
import ConfigDialog from '@/component/ConfigDialog';
|
||||||
|
import EditDialog from '@/component/EditDialog';
|
||||||
|
|
||||||
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
|
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
|
||||||
|
import { GetImageFromURL, RewriteMetaToMp3, RewriteMetaToFlac, AudioMimeType, split_regex } from '@/decrypt/utils';
|
||||||
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
@ -52,10 +72,13 @@ export default {
|
|||||||
FileSelector,
|
FileSelector,
|
||||||
PreviewTable,
|
PreviewTable,
|
||||||
ConfigDialog,
|
ConfigDialog,
|
||||||
|
EditDialog,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showConfigDialog: false,
|
showConfigDialog: false,
|
||||||
|
showEditDialog: false,
|
||||||
|
editing_data: { picture: '', title: '', artist: '', album: '', albumartist: '', genre: '' },
|
||||||
tableData: [],
|
tableData: [],
|
||||||
playing_url: '',
|
playing_url: '',
|
||||||
playing_auto: false,
|
playing_auto: false,
|
||||||
@ -96,7 +119,7 @@ export default {
|
|||||||
errInfo +
|
errInfo +
|
||||||
',' +
|
',' +
|
||||||
filename +
|
filename +
|
||||||
',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
|
',参考<a target="_blank" href="https://git.unlock-music.dev/um/web/wiki/使用提示">使用提示</a>',
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
duration: 6000,
|
duration: 6000,
|
||||||
});
|
});
|
||||||
@ -128,7 +151,78 @@ export default {
|
|||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
},
|
},
|
||||||
|
async handleEdit(data) {
|
||||||
|
this.showEditDialog = false;
|
||||||
|
URL.revokeObjectURL(this.editing_data.file);
|
||||||
|
if (data.picture) {
|
||||||
|
URL.revokeObjectURL(this.editing_data.picture);
|
||||||
|
this.editing_data.picture = URL.createObjectURL(data.picture);
|
||||||
|
}
|
||||||
|
this.editing_data.title = data.title;
|
||||||
|
this.editing_data.artist = data.artist;
|
||||||
|
this.editing_data.album = data.album;
|
||||||
|
let writeSuccess = true;
|
||||||
|
let notifyMsg = '成功修改 ' + this.editing_data.title;
|
||||||
|
try {
|
||||||
|
const musicMeta = await metaParseBlob(new Blob([this.editing_data.blob], { type: mime }));
|
||||||
|
let imageInfo = undefined;
|
||||||
|
if (this.editing_data.picture !== '') {
|
||||||
|
imageInfo = await GetImageFromURL(this.editing_data.picture);
|
||||||
|
if (!imageInfo) {
|
||||||
|
console.warn('获取图像失败', this.editing_data.picture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newMeta = {
|
||||||
|
picture: imageInfo?.buffer,
|
||||||
|
title: data.title,
|
||||||
|
artists: data.artist.split(split_regex),
|
||||||
|
album: data.album,
|
||||||
|
albumartist: data.albumartist,
|
||||||
|
genre: data.genre.split(split_regex),
|
||||||
|
};
|
||||||
|
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
|
||||||
|
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
|
||||||
|
if (this.editing_data.ext === 'mp3') {
|
||||||
|
this.editing_data.blob = new Blob([RewriteMetaToMp3(buffer, newMeta, musicMeta)], { type: mime });
|
||||||
|
} else if (this.editing_data.ext === 'flac') {
|
||||||
|
this.editing_data.blob = new Blob([RewriteMetaToFlac(buffer, newMeta, musicMeta)], { type: mime });
|
||||||
|
} else {
|
||||||
|
writeSuccess = undefined;
|
||||||
|
notifyMsg = this.editing_data.ext + '类型文件暂时不支持修改音乐标签';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
writeSuccess = false;
|
||||||
|
notifyMsg = '修改' + this.editing_data.title + '未能完成。在写入新的元数据时发生错误:' + e;
|
||||||
|
}
|
||||||
|
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
|
||||||
|
if (writeSuccess === true) {
|
||||||
|
this.$notify.success({
|
||||||
|
title: '修改成功',
|
||||||
|
message: notifyMsg,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} else if (writeSuccess === false) {
|
||||||
|
this.$notify.error({
|
||||||
|
title: '修改失败',
|
||||||
|
message: notifyMsg,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$notify.warning({
|
||||||
|
title: '修改取消',
|
||||||
|
message: notifyMsg,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async editFile(data) {
|
||||||
|
this.editing_data = data;
|
||||||
|
const musicMeta = await metaParseBlob(this.editing_data.blob);
|
||||||
|
this.editing_data.albumartist = musicMeta.common.albumartist || '';
|
||||||
|
this.editing_data.genre = musicMeta.common.genre?.toString() || '';
|
||||||
|
this.showEditDialog = true;
|
||||||
|
},
|
||||||
async saveFile(data) {
|
async saveFile(data) {
|
||||||
if (this.dir) {
|
if (this.dir) {
|
||||||
await DirectlyWriteFile(data, this.filename_policy, this.dir);
|
await DirectlyWriteFile(data, this.filename_policy, this.dir);
|
||||||
|
Loading…
Reference in New Issue
Block a user