diff --git a/package-lock.json b/package-lock.json
index 23beffc..d6cd297 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"dependencies": {
"@babel/preset-typescript": "^7.16.5",
"@jixun/qmc2-crypto": "^0.0.5-R4",
+ "@unlock-music-gh/joox-crypto": "^0.0.1-R2",
"base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0",
"core-js": "^3.16.0",
@@ -3485,6 +3486,17 @@
"integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
"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": {
"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",
@@ -23622,6 +23634,14 @@
"integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
"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": {
"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",
diff --git a/package.json b/package.json
index 99117a8..1431651 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"dependencies": {
"@babel/preset-typescript": "^7.16.5",
"@jixun/qmc2-crypto": "^0.0.5-R4",
+ "@unlock-music-gh/joox-crypto": "^0.0.1-R2",
"base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0",
"core-js": "^3.16.0",
diff --git a/src/component/ConfigDialog.vue b/src/component/ConfigDialog.vue
new file mode 100644
index 0000000..e0a05ce
--- /dev/null
+++ b/src/component/ConfigDialog.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/decrypt/common.ts b/src/decrypt/common.ts
index ec7c935..03d3a39 100644
--- a/src/decrypt/common.ts
+++ b/src/decrypt/common.ts
@@ -7,6 +7,7 @@ import { Decrypt as KgmDecrypt } from '@/decrypt/kgm';
import { Decrypt as KwmDecrypt } from '@/decrypt/kwm';
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
import { Decrypt as TmDecrypt } from '@/decrypt/tm';
+import { Decrypt as JooxDecrypt } from '@/decrypt/joox';
import { DecryptResult, FileInfo } from '@/decrypt/entity';
import { SplitFilename } from '@/decrypt/utils';
@@ -68,6 +69,9 @@ export async function CommonDecrypt(file: FileInfo): Promise {
case 'kgma':
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
break;
+ case 'ofl_en':
+ rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext);
+ break;
default:
throw '不支持此文件格式';
}
diff --git a/src/decrypt/joox.ts b/src/decrypt/joox.ts
new file mode 100644
index 0000000..a5fab16
--- /dev/null
+++ b/src/decrypt/joox.ts
@@ -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 {
+ 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,
+ };
+}
diff --git a/src/decrypt/qmc_wasm.ts b/src/decrypt/qmc_wasm.ts
index 22e2c31..bec030e 100644
--- a/src/decrypt/qmc_wasm.ts
+++ b/src/decrypt/qmc_wasm.ts
@@ -1,4 +1,5 @@
import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle';
+import { MergeUint8Array } from '@/utils/MergeUint8Array';
// 检测文件末端使用的缓冲区大小
const DETECTION_SIZE = 40;
@@ -6,22 +7,6 @@ const DETECTION_SIZE = 40;
// 每次处理 2M 的数据
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 加密的文件。
*
diff --git a/src/main.ts b/src/main.ts
index 97845b2..b046b80 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,9 +6,13 @@ import {
Checkbox,
Col,
Container,
+ Dialog,
+ Form,
+ FormItem,
Footer,
Icon,
Image,
+ Input,
Link,
Main,
Notification,
@@ -26,6 +30,10 @@ import 'element-ui/lib/theme-chalk/base.css';
Vue.use(Link);
Vue.use(Image);
Vue.use(Button);
+Vue.use(Dialog);
+Vue.use(Form);
+Vue.use(FormItem);
+Vue.use(Input);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(Main);
diff --git a/src/utils/MergeUint8Array.ts b/src/utils/MergeUint8Array.ts
new file mode 100644
index 0000000..29f91e0
--- /dev/null
+++ b/src/utils/MergeUint8Array.ts
@@ -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;
+}
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
new file mode 100644
index 0000000..74bfe1d
--- /dev/null
+++ b/src/utils/storage.ts
@@ -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;
diff --git a/src/utils/storage/BaseStorage.ts b/src/utils/storage/BaseStorage.ts
new file mode 100644
index 0000000..8a5dafc
--- /dev/null
+++ b/src/utils/storage/BaseStorage.ts
@@ -0,0 +1,14 @@
+const KEY_JOOX_UUID = 'joox.uuid';
+
+export default abstract class BaseStorage {
+ protected abstract save(name: string, value: T): Promise;
+ protected abstract load(name: string, defaultValue: T): Promise;
+
+ public saveJooxUUID(uuid: string): Promise {
+ return this.save(KEY_JOOX_UUID, uuid);
+ }
+
+ public loadJooxUUID(defaultValue: string = ''): Promise {
+ return this.load(KEY_JOOX_UUID, defaultValue);
+ }
+}
diff --git a/src/utils/storage/BrowserNativeStorage.ts b/src/utils/storage/BrowserNativeStorage.ts
new file mode 100644
index 0000000..b17c048
--- /dev/null
+++ b/src/utils/storage/BrowserNativeStorage.ts
@@ -0,0 +1,15 @@
+import BaseStorage from './BaseStorage';
+
+export default class BrowserNativeStorage extends BaseStorage {
+ protected async load(name: string, defaultValue: T): Promise {
+ const result = localStorage.getItem(name);
+ if (result === null) {
+ return defaultValue;
+ }
+ return JSON.parse(result);
+ }
+
+ protected async save(name: string, value: T): Promise {
+ localStorage.setItem(name, JSON.stringify(value));
+ }
+}
diff --git a/src/utils/storage/ChromeExtensionStorage.ts b/src/utils/storage/ChromeExtensionStorage.ts
new file mode 100644
index 0000000..2338b89
--- /dev/null
+++ b/src/utils/storage/ChromeExtensionStorage.ts
@@ -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(name: string, defaultValue: T): Promise {
+ const result = await chrome.storage.local.get({ [name]: defaultValue });
+ if (Object.prototype.hasOwnProperty.call(result, name)) {
+ return result[name];
+ }
+ return defaultValue;
+ }
+
+ protected async save(name: string, value: T): Promise {
+ return chrome.storage.local.set({ [name]: value });
+ }
+}
diff --git a/src/view/Home.vue b/src/view/Home.vue
index d457bc5..35b6a2f 100644
--- a/src/view/Home.vue
+++ b/src/view/Home.vue
@@ -10,6 +10,13 @@
+
+
+
+ 部分解密方案需要设定解密参数。
+
+ 解密设定
+
下载全部
清除全部
@@ -35,6 +42,8 @@