Compare commits

...

4 Commits

11 changed files with 197 additions and 57 deletions

View File

@ -20,9 +20,11 @@
"nanoid": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-promise-suspense": "^0.3.4",
"react-redux": "^8.0.5"
},
"devDependencies": {
"@rollup/plugin-replace": "^5.0.2",
"@types/node": "^20.1.1",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",

58
pnpm-lock.yaml generated
View File

@ -31,11 +31,17 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-promise-suspense:
specifier: ^0.3.4
version: 0.3.4
react-redux:
specifier: ^8.0.5
version: 8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
devDependencies:
'@rollup/plugin-replace':
specifier: ^5.0.2
version: 5.0.2
'@types/node':
specifier: ^20.1.1
version: 20.1.1
@ -1862,6 +1868,19 @@ packages:
reselect: 4.1.8
dev: false
/@rollup/plugin-replace@5.0.2:
resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2
magic-string: 0.27.0
dev: true
/@rollup/plugin-virtual@3.0.1:
resolution: {integrity: sha512-fK8O0IL5+q+GrsMLuACVNk2x21g3yaw+sG2qn16SnUd3IlBsQyvWxLMGHmCmXRMecPjGRSZ/1LmZB4rjQm68og==}
engines: {node: '>=14.0.0'}
@ -1872,6 +1891,20 @@ packages:
optional: true
dev: true
/@rollup/pluginutils@5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.1
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@swc/core-darwin-arm64@1.3.57:
resolution: {integrity: sha512-lhAK9kF/ppZdNTdaxJl2gE0bXubzQXTgxB2Xojme/1sbOipaLTskBbJ3FLySChpmVOzD0QSCTiW8w/dmQxqNIQ==}
engines: {node: '>=10'}
@ -1984,6 +2017,10 @@ packages:
'@swc/core-win32-x64-msvc': 1.3.57
dev: true
/@types/estree@1.0.1:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
dev: true
/@types/hoist-non-react-statics@3.3.1:
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
dependencies:
@ -2596,11 +2633,19 @@ packages:
engines: {node: '>=4.0'}
dev: true
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
dev: true
/fast-deep-equal@2.0.1:
resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==}
dev: false
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
@ -2959,6 +3004,13 @@ packages:
yallist: 4.0.0
dev: true
/magic-string@0.27.0:
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@ -3172,6 +3224,12 @@ packages:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: false
/react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
dependencies:
fast-deep-equal: 2.0.1
dev: false
/react-redux@8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1):
resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==}
peerDependencies:

View File

@ -1,4 +1,6 @@
import { Center, Link, Text } from '@chakra-ui/react';
import { Center, Flex, Link, Text } from '@chakra-ui/react';
import { Suspense } from 'react';
import { SDKVersion } from './SDKVersion';
export function Footer() {
return (
@ -15,13 +17,19 @@ export function Footer() {
left="0"
flexDir="column"
>
<Text> (x.x.x) - </Text>
<Flex as={Text}>
{'音乐解锁 (__APP_VERSION_SHORT__'}
<Suspense>
<SDKVersion />
</Suspense>
{') - 移除已购音乐的加密保护。'}
</Flex>
<Text>
Copyright © 2019 - 2023{' '}
{'Copyright © 2019 - 2023 '}
<Link href="https://git.unlock-music.dev/um" isExternal>
UnlockMusic
</Link>{' '}
|
</Link>
{' | 音乐解锁授权基于'}
<Link href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" isExternal>
MIT许可协议
</Link>

32
src/SDKVersion.tsx Normal file
View File

@ -0,0 +1,32 @@
import { InfoOutlineIcon } from '@chakra-ui/icons';
import { Tooltip, VStack, Text, Box, Flex } from '@chakra-ui/react';
import { workerClientBus } from './decrypt-worker/client';
import { DECRYPTION_WORKER_ACTION_NAME } from './decrypt-worker/constants';
import usePromise from 'react-promise-suspense';
const getSDKVersion = async () => {
return workerClientBus.request(DECRYPTION_WORKER_ACTION_NAME.VERSION, null);
};
export function SDKVersion() {
const sdkVersion = usePromise(getSDKVersion, []);
return (
<Flex pl="1" alignItems="center">
<Tooltip
hasArrow
placement="top"
label={
<VStack>
<Text>App: __APP_VERSION__</Text>
<Text>SDK: {sdkVersion}</Text>
</VStack>
}
bg="gray.300"
color="black"
>
<InfoOutlineIcon />
</Tooltip>
</Flex>
);
}

View File

@ -9,7 +9,7 @@ export const workerClient = new Worker(new URL('./worker', import.meta.url), { t
workerClient.onerror = (err) => console.error(err);
class DecryptionQueue extends ConcurrentQueue<{ id: string; blobURI: string }> {
constructor(private workerClientBus: WorkerClientBus, maxQueue?: number) {
constructor(private workerClientBus: WorkerClientBus<DECRYPTION_WORKER_ACTION_NAME>, maxQueue?: number) {
super(maxQueue);
}
@ -18,4 +18,5 @@ class DecryptionQueue extends ConcurrentQueue<{ id: string; blobURI: string }> {
}
}
export const decryptionQueue = new DecryptionQueue(new WorkerClientBus(workerClient));
export const workerClientBus = new WorkerClientBus<DECRYPTION_WORKER_ACTION_NAME>(workerClient);
export const decryptionQueue = new DecryptionQueue(workerClientBus);

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ export abstract class ConcurrentQueue<T> {
}
private async processQueue() {
while (true) {
while (this.items.length > 0) {
const item = this.items.pop();
if (item === undefined) {
break;

View File

@ -1,6 +1,8 @@
import { nanoid } from 'nanoid';
export class WorkerClientBus {
/* eslint-disable @typescript-eslint/no-explicit-any */
export class WorkerClientBus<T = string> {
private idPromiseMap = new Map<string, [(data: any) => void, (error: Error) => void]>();
constructor(private worker: Worker) {
@ -22,7 +24,7 @@ export class WorkerClientBus {
});
}
async request<R = any, P = any>(actionName: string, payload: P): Promise<R> {
async request<R = any, P = any>(actionName: T, payload: P): Promise<R> {
return new Promise((resolve, reject) => {
const id = nanoid();
this.idPromiseMap.set(id, [resolve, reject]);

View File

@ -1,9 +1,25 @@
import path from 'path';
import cp from 'node:child_process';
import url from 'node:url';
import path from 'node:path';
import fs from 'node:fs';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import wasm from 'vite-plugin-wasm';
import replace from '@rollup/plugin-replace';
import topLevelAwait from 'vite-plugin-top-level-await';
const gitRoot = url.fileURLToPath(new URL('.', import.meta.url));
const pkg = JSON.parse(fs.readFileSync(gitRoot + '/package.json', 'utf-8'));
function command(cmd, dir = '') {
return cp.execSync(cmd, { cwd: path.join(gitRoot, dir), encoding: 'utf-8' }).trim();
}
const COMMAND_GIT_VERSION = 'git describe --long --dirty --tags --always';
const shortCommit = command(COMMAND_GIT_VERSION);
const version = `${pkg.version}-${shortCommit}`;
// https://vitejs.dev/config/
export default defineConfig({
server: {
@ -25,7 +41,18 @@ export default defineConfig({
optimizeDeps: {
exclude: ['@jixun/libparakeet'],
},
plugins: [react(), wasm(), topLevelAwait()],
plugins: [
replace({
preventAssignment: true,
values: {
__APP_VERSION_SHORT__: pkg.version,
__APP_VERSION__: version,
},
}),
react(),
wasm(),
topLevelAwait(),
],
resolve: {
alias: {
'~': path.resolve(__dirname, 'src'),