forked from um/web
feat: add basic joox support
(cherry picked from commit 699333ca06526d747a7eb4a188e896de81e9f014)
This commit is contained in:
parent
9add76c060
commit
1e7116a3a9
20
package-lock.json
generated
20
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.16.5",
|
"@babel/preset-typescript": "^7.16.5",
|
||||||
"@jixun/qmc2-crypto": "^0.0.5-R4",
|
"@jixun/qmc2-crypto": "^0.0.5-R4",
|
||||||
|
"@unlock-music-gh/joox-crypto": "^0.0.1-R2",
|
||||||
"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",
|
||||||
@ -3485,6 +3486,17 @@
|
|||||||
"integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
|
"integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@unlock-music-gh/joox-crypto": {
|
||||||
|
"version": "0.0.1-R3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@unlock-music-gh/joox-crypto/-/joox-crypto-0.0.1-R3.tgz",
|
||||||
|
"integrity": "sha512-zZRiDXKI5SxuBIcW/rsGL8jNvyWxtA5cNRfg69WcsZK2DqztY8M2q1kMe96MP1AyM+cKpNQ50jAKh77VdFv9rA==",
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-js": "^4.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"joox-decrypt": "joox-decrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
|
||||||
@ -23622,6 +23634,14 @@
|
|||||||
"integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
|
"integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@unlock-music-gh/joox-crypto": {
|
||||||
|
"version": "0.0.1-R3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@unlock-music-gh/joox-crypto/-/joox-crypto-0.0.1-R3.tgz",
|
||||||
|
"integrity": "sha512-zZRiDXKI5SxuBIcW/rsGL8jNvyWxtA5cNRfg69WcsZK2DqztY8M2q1kMe96MP1AyM+cKpNQ50jAKh77VdFv9rA==",
|
||||||
|
"requires": {
|
||||||
|
"crypto-js": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": {
|
"@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.16.5",
|
"@babel/preset-typescript": "^7.16.5",
|
||||||
"@jixun/qmc2-crypto": "^0.0.5-R4",
|
"@jixun/qmc2-crypto": "^0.0.5-R4",
|
||||||
|
"@unlock-music-gh/joox-crypto": "^0.0.1-R2",
|
||||||
"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",
|
||||||
|
53
src/component/ConfigDialog.vue
Normal file
53
src/component/ConfigDialog.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog fullscreen @close="cancel()" title="解密设定" :visible="show" width="30%" center>
|
||||||
|
<el-form ref="form" :model="form" label-width="80px">
|
||||||
|
<el-form-item label="Joox UUID">
|
||||||
|
<el-input type="text" placeholder="UUID" v-model="form.jooxUUID" clearable maxlength="32" show-word-limit>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" :loading="saving" @click="emitConfirm()">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import storage from '../utils/storage';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
show: { type: Boolean, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
saving: false,
|
||||||
|
form: {
|
||||||
|
jooxUUID: '',
|
||||||
|
},
|
||||||
|
centerDialogVisible: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.resetForm();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async resetForm() {
|
||||||
|
this.form.jooxUUID = await storage.loadJooxUUID();
|
||||||
|
},
|
||||||
|
|
||||||
|
async cancel() {
|
||||||
|
await this.resetForm();
|
||||||
|
this.$emit('done');
|
||||||
|
},
|
||||||
|
|
||||||
|
async emitConfirm() {
|
||||||
|
this.saving = true;
|
||||||
|
await storage.saveJooxUUID(this.form.jooxUUID);
|
||||||
|
this.saving = false;
|
||||||
|
this.$emit('done');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -7,6 +7,7 @@ import { Decrypt as KgmDecrypt } from '@/decrypt/kgm';
|
|||||||
import { Decrypt as KwmDecrypt } from '@/decrypt/kwm';
|
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 { DecryptResult, FileInfo } from '@/decrypt/entity';
|
import { DecryptResult, FileInfo } from '@/decrypt/entity';
|
||||||
import { SplitFilename } from '@/decrypt/utils';
|
import { SplitFilename } from '@/decrypt/utils';
|
||||||
|
|
||||||
@ -68,6 +69,9 @@ export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
|||||||
case 'kgma':
|
case 'kgma':
|
||||||
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
|
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
|
case 'ofl_en':
|
||||||
|
rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw '不支持此文件格式';
|
throw '不支持此文件格式';
|
||||||
}
|
}
|
||||||
|
34
src/decrypt/joox.ts
Normal file
34
src/decrypt/joox.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { DecryptResult } from './entity';
|
||||||
|
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from './utils';
|
||||||
|
|
||||||
|
import jooxFactory from '@unlock-music-gh/joox-crypto';
|
||||||
|
import storage from '@/utils/storage';
|
||||||
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
|
|
||||||
|
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||||
|
const uuid = await storage.loadJooxUUID('');
|
||||||
|
if (!uuid || uuid.length !== 32) {
|
||||||
|
throw new Error('请在“解密设定”填写应用 Joox 应用的 UUID。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileBuffer = new Uint8Array(await GetArrayBuffer(file));
|
||||||
|
const decryptor = jooxFactory(fileBuffer, uuid);
|
||||||
|
if (!decryptor) {
|
||||||
|
throw new Error('不支持的 joox 加密格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
const musicDecoded = MergeUint8Array(decryptor.decryptFile(fileBuffer));
|
||||||
|
const ext = SniffAudioExt(musicDecoded);
|
||||||
|
const mime = AudioMimeType[ext];
|
||||||
|
const musicBlob = new Blob([musicDecoded], { type: mime });
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: raw_filename.replace(/\.[^\.]+$/, ''),
|
||||||
|
artist: '未知',
|
||||||
|
album: '未知',
|
||||||
|
file: URL.createObjectURL(musicBlob),
|
||||||
|
blob: musicBlob,
|
||||||
|
mime: mime,
|
||||||
|
ext: ext,
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle';
|
import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle';
|
||||||
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
|
|
||||||
// 检测文件末端使用的缓冲区大小
|
// 检测文件末端使用的缓冲区大小
|
||||||
const DETECTION_SIZE = 40;
|
const DETECTION_SIZE = 40;
|
||||||
@ -6,22 +7,6 @@ const DETECTION_SIZE = 40;
|
|||||||
// 每次处理 2M 的数据
|
// 每次处理 2M 的数据
|
||||||
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
function MergeUint8Array(array: Uint8Array[]): Uint8Array {
|
|
||||||
let length = 0;
|
|
||||||
array.forEach((item) => {
|
|
||||||
length += item.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
let mergedArray = new Uint8Array(length);
|
|
||||||
let offset = 0;
|
|
||||||
array.forEach((item) => {
|
|
||||||
mergedArray.set(item, offset);
|
|
||||||
offset += item.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
return mergedArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解密一个 QMC2 加密的文件。
|
* 解密一个 QMC2 加密的文件。
|
||||||
*
|
*
|
||||||
|
@ -6,9 +6,13 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
Col,
|
Col,
|
||||||
Container,
|
Container,
|
||||||
|
Dialog,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
Footer,
|
Footer,
|
||||||
Icon,
|
Icon,
|
||||||
Image,
|
Image,
|
||||||
|
Input,
|
||||||
Link,
|
Link,
|
||||||
Main,
|
Main,
|
||||||
Notification,
|
Notification,
|
||||||
@ -26,6 +30,10 @@ import 'element-ui/lib/theme-chalk/base.css';
|
|||||||
Vue.use(Link);
|
Vue.use(Link);
|
||||||
Vue.use(Image);
|
Vue.use(Image);
|
||||||
Vue.use(Button);
|
Vue.use(Button);
|
||||||
|
Vue.use(Dialog);
|
||||||
|
Vue.use(Form);
|
||||||
|
Vue.use(FormItem);
|
||||||
|
Vue.use(Input);
|
||||||
Vue.use(Table);
|
Vue.use(Table);
|
||||||
Vue.use(TableColumn);
|
Vue.use(TableColumn);
|
||||||
Vue.use(Main);
|
Vue.use(Main);
|
||||||
|
15
src/utils/MergeUint8Array.ts
Normal file
15
src/utils/MergeUint8Array.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export function MergeUint8Array(array: Uint8Array[]): Uint8Array {
|
||||||
|
let length = 0;
|
||||||
|
array.forEach((item) => {
|
||||||
|
length += item.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
let mergedArray = new Uint8Array(length);
|
||||||
|
let offset = 0;
|
||||||
|
array.forEach((item) => {
|
||||||
|
mergedArray.set(item, offset);
|
||||||
|
offset += item.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedArray;
|
||||||
|
}
|
7
src/utils/storage.ts
Normal file
7
src/utils/storage.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import BaseStorage from './storage/BaseStorage';
|
||||||
|
import BrowserNativeStorage from './storage/BrowserNativeStorage';
|
||||||
|
import ChromeExtensionStorage from './storage/ChromeExtensionStorage';
|
||||||
|
|
||||||
|
const storage: BaseStorage = ChromeExtensionStorage.works ? new ChromeExtensionStorage() : new BrowserNativeStorage();
|
||||||
|
|
||||||
|
export default storage;
|
14
src/utils/storage/BaseStorage.ts
Normal file
14
src/utils/storage/BaseStorage.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const KEY_JOOX_UUID = 'joox.uuid';
|
||||||
|
|
||||||
|
export default abstract class BaseStorage {
|
||||||
|
protected abstract save<T>(name: string, value: T): Promise<void>;
|
||||||
|
protected abstract load<T>(name: string, defaultValue: T): Promise<T>;
|
||||||
|
|
||||||
|
public saveJooxUUID(uuid: string): Promise<void> {
|
||||||
|
return this.save(KEY_JOOX_UUID, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadJooxUUID(defaultValue: string = ''): Promise<string> {
|
||||||
|
return this.load(KEY_JOOX_UUID, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
15
src/utils/storage/BrowserNativeStorage.ts
Normal file
15
src/utils/storage/BrowserNativeStorage.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import BaseStorage from './BaseStorage';
|
||||||
|
|
||||||
|
export default class BrowserNativeStorage extends BaseStorage {
|
||||||
|
protected async load<T>(name: string, defaultValue: T): Promise<T> {
|
||||||
|
const result = localStorage.getItem(name);
|
||||||
|
if (result === null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async save<T>(name: string, value: T): Promise<void> {
|
||||||
|
localStorage.setItem(name, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
}
|
21
src/utils/storage/ChromeExtensionStorage.ts
Normal file
21
src/utils/storage/ChromeExtensionStorage.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import BaseStorage from './BaseStorage';
|
||||||
|
|
||||||
|
declare var chrome: any;
|
||||||
|
|
||||||
|
export default class ChromeExtensionStorage extends BaseStorage {
|
||||||
|
static get works(): boolean {
|
||||||
|
return Boolean(chrome?.storage?.local?.set);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async load<T>(name: string, defaultValue: T): Promise<T> {
|
||||||
|
const result = await chrome.storage.local.get({ [name]: defaultValue });
|
||||||
|
if (Object.prototype.hasOwnProperty.call(result, name)) {
|
||||||
|
return result[name];
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async save<T>(name: string, value: T): Promise<void> {
|
||||||
|
return chrome.storage.local.set({ [name]: value });
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,13 @@
|
|||||||
</el-radio>
|
</el-radio>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
|
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
|
||||||
|
<el-tooltip class="item" effect="dark" placement="top">
|
||||||
|
<div slot="content">
|
||||||
|
<span> 部分解密方案需要设定解密参数。 </span>
|
||||||
|
</div>
|
||||||
|
<el-button icon="el-icon-s-tools" plain @click="showConfigDialog = true">解密设定</el-button>
|
||||||
|
</el-tooltip>
|
||||||
<el-button icon="el-icon-download" plain @click="handleDownloadAll">下载全部</el-button>
|
<el-button icon="el-icon-download" plain @click="handleDownloadAll">下载全部</el-button>
|
||||||
<el-button icon="el-icon-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
|
<el-button icon="el-icon-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
|
||||||
|
|
||||||
@ -35,6 +42,8 @@
|
|||||||
<script>
|
<script>
|
||||||
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 { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
|
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -42,9 +51,11 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
FileSelector,
|
FileSelector,
|
||||||
PreviewTable,
|
PreviewTable,
|
||||||
|
ConfigDialog,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
showConfigDialog: false,
|
||||||
tableData: [],
|
tableData: [],
|
||||||
playing_url: '',
|
playing_url: '',
|
||||||
playing_auto: false,
|
playing_auto: false,
|
||||||
@ -103,6 +114,9 @@ export default {
|
|||||||
});
|
});
|
||||||
this.tableData = [];
|
this.tableData = [];
|
||||||
},
|
},
|
||||||
|
handleDecryptionConfig() {
|
||||||
|
this.showConfigDialog = true;
|
||||||
|
},
|
||||||
handleDownloadAll() {
|
handleDownloadAll() {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let c = setInterval(() => {
|
let c = setInterval(() => {
|
||||||
|
Loading…
Reference in New Issue
Block a user