Compare commits
4 Commits
eb1996c28e
...
8913b91a8f
Author | SHA1 | Date | |
---|---|---|---|
8913b91a8f | |||
4da849f3eb | |||
3f61f20ac0 | |||
15bde81afe |
@ -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
58
pnpm-lock.yaml
generated
@ -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:
|
||||
|
@ -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
32
src/SDKVersion.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
@ -1,5 +1,6 @@
|
||||
export enum DECRYPTION_WORKER_ACTION_NAME {
|
||||
DECRYPT = 'DECRYPT',
|
||||
VERSION = 'VERSION',
|
||||
}
|
||||
|
||||
export interface DecryptionResult {
|
||||
|
49
src/decrypt-worker/worker-handler/decrypt.ts
Normal file
49
src/decrypt-worker/worker-handler/decrypt.ts
Normal 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');
|
||||
};
|
@ -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();
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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]);
|
||||
|
@ -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'),
|
||||
|
Loading…
Reference in New Issue
Block a user