mirror of
				https://git.unlock-music.dev/um/web.git
				synced 2025-11-04 08:03:29 +08:00 
			
		
		
		
	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": {
 | 
			
		||||
        "@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",
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 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<DecryptResult> {
 | 
			
		||||
    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 '不支持此文件格式';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 { 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 加密的文件。
 | 
			
		||||
 *
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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-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-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
 | 
			
		||||
 | 
			
		||||
@ -35,6 +42,8 @@
 | 
			
		||||
<script>
 | 
			
		||||
import FileSelector from '@/component/FileSelector';
 | 
			
		||||
import PreviewTable from '@/component/PreviewTable';
 | 
			
		||||
import ConfigDialog from '@/component/ConfigDialog';
 | 
			
		||||
 | 
			
		||||
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
@ -42,9 +51,11 @@ export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    FileSelector,
 | 
			
		||||
    PreviewTable,
 | 
			
		||||
    ConfigDialog,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showConfigDialog: false,
 | 
			
		||||
      tableData: [],
 | 
			
		||||
      playing_url: '',
 | 
			
		||||
      playing_auto: false,
 | 
			
		||||
@ -103,6 +114,9 @@ export default {
 | 
			
		||||
      });
 | 
			
		||||
      this.tableData = [];
 | 
			
		||||
    },
 | 
			
		||||
    handleDecryptionConfig() {
 | 
			
		||||
      this.showConfigDialog = true;
 | 
			
		||||
    },
 | 
			
		||||
    handleDownloadAll() {
 | 
			
		||||
      let index = 0;
 | 
			
		||||
      let c = setInterval(() => {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user