- 新拖到此处的图片将覆盖原始图片
+
+
+
+ 将新图片拖到此处,或点击选择
以替换自动匹配的图片
-
+
+ 新拖到此处的图片将覆盖原始图片
+
+
+
- 标题:
-
{{title}}
-
+ >
+
+ 标题:
+
{{ title }}
+
- 艺术家:
-
{{artist}}
-
+ >
+ 艺术家:
+
{{ artist }}
+
- 专辑:
-
{{album}}
-
+ >
+ 专辑:
+
{{ album }}
+
- 专辑艺术家:
-
{{albumartist}}
-
+ >
+ 专辑艺术家:
+
{{ albumartist }}
+
- 风格:
-
{{genre}}
-
+ >
+ 风格:
+
{{ genre }}
+
+ >
为了节省您设备的资源,请在确定前充分检查,避免反复修改。
@@ -85,9 +122,11 @@ form >>> input {
-
+
+
+
@@ -100,17 +139,17 @@ export default {
},
props: {
show: { type: Boolean, required: true },
- picture: { type: String | undefined, required: true },
- title: { type: String | undefined, required: true },
- artist: { type: String | undefined, required: true },
- album: { type: String | undefined, required: true },
- albumartist: { type: String | undefined, required: true },
- genre: { type: String | undefined, required: true },
+ picture: { type: String, required: true },
+ title: { type: String, required: true },
+ artist: { type: String, required: true },
+ album: { type: String, required: true },
+ albumartist: { type: String, required: true },
+ genre: { type: String, required: true },
},
data() {
return {
- form: {
- },
+ internalShow: false,
+ form: {},
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
editPicture: false,
editTitle: false,
@@ -120,6 +159,11 @@ export default {
editGenre: false,
};
},
+ watch: {
+ show(newValue, oldValue) {
+ this.internalShow = newValue;
+ },
+ },
async mounted() {
this.refreshForm();
},
diff --git a/src/component/FileSelector.vue b/src/component/FileSelector.vue
index 058bfcd..aeef540 100644
--- a/src/component/FileSelector.vue
+++ b/src/component/FileSelector.vue
@@ -1,26 +1,39 @@
-
-
- 将文件拖到此处,或 点击选择
-
+
+
+ 将文件拖到此处,或 点击选择
+
仅在浏览器内对文件进行解锁,无需消耗流量
- 算法在源代码中已经提供,所有运算都发生在本地
-
+
+ 算法在源代码中已经提供,所有运算都发生在本地
+
+
+
+
工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
-
+
将此工具部署在HTTPS环境下,可以启用Web Worker特性,
从而更快的利用并行处理完成解锁
-
-
+
+
+
+
-
+
spawn(new Worker('@/utils/worker.ts')), navigator.hardwareConcurrency || 1);
+ this.queue = Pool(
+ () => spawn(new Worker('@/utils/worker.ts')),
+ navigator.hardwareConcurrency || 1
+ );
this.parallel = true;
} else {
console.log('Using Queue in Main Thread');
diff --git a/src/component/PreviewTable.vue b/src/component/PreviewTable.vue
index 64cdf1f..42db682 100644
--- a/src/component/PreviewTable.vue
+++ b/src/component/PreviewTable.vue
@@ -1,9 +1,11 @@
-
+
- 暂无封面
+
+ 暂无封面
+
@@ -24,11 +26,31 @@
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/decrypt/kgm_wasm.ts b/src/decrypt/kgm_wasm.ts
index da45a38..db2f18d 100644
--- a/src/decrypt/kgm_wasm.ts
+++ b/src/decrypt/kgm_wasm.ts
@@ -2,7 +2,7 @@ import KgmCryptoModule from '@/KgmWasm/KgmWasmBundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
// 每次处理 2M 的数据
-const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
+const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
export interface KGMDecryptionResult {
success: boolean;
@@ -16,8 +16,15 @@ export interface KGMDecryptionResult {
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
* @param {ArrayBuffer} kgmBlob 读入的文件 Blob
*/
-export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise {
- const result: KGMDecryptionResult = { success: false, data: new Uint8Array(), error: '' };
+export async function DecryptKgmWasm(
+ kgmBlob: ArrayBuffer,
+ ext: string
+): Promise {
+ const result: KGMDecryptionResult = {
+ success: false,
+ data: new Uint8Array(),
+ error: '',
+ };
// 初始化模组
let KgmCrypto: any;
diff --git a/src/decrypt/ncm.ts b/src/decrypt/ncm.ts
index c2f9851..6d239d6 100644
--- a/src/decrypt/ncm.ts
+++ b/src/decrypt/ncm.ts
@@ -10,7 +10,7 @@ import {
WriteMetaToMp3,
} from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
-import jimp from 'jimp';
+// import jimp from 'jimp';
import AES from 'crypto-js/aes';
import PKCS7 from 'crypto-js/pad-pkcs7';
@@ -26,7 +26,11 @@ const CORE_KEY = EncHex.parse('687a4852416d736f356b496e62617857');
const META_KEY = EncHex.parse('2331346C6A6B5F215C5D2630553C2728');
const MagicHeader = [0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d];
-export async function Decrypt(file: File, raw_filename: string, _: string): Promise {
+export async function Decrypt(
+ file: File,
+ raw_filename: string,
+ _: string
+): Promise {
return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt();
}
@@ -68,14 +72,16 @@ class NcmDecrypt {
_getKeyData(): Uint8Array {
const keyLen = this.view.getUint32(this.offset, true);
this.offset += 4;
- const cipherText = new Uint8Array(this.raw, this.offset, keyLen).map((uint8) => uint8 ^ 0x64);
+ const cipherText = new Uint8Array(this.raw, this.offset, keyLen).map(
+ (uint8) => uint8 ^ 0x64
+ );
this.offset += keyLen;
const plainText = AES.decrypt(
// @ts-ignore
{ ciphertext: WordArray.create(cipherText) },
CORE_KEY,
- { mode: ModeECB, padding: PKCS7 },
+ { mode: ModeECB, padding: PKCS7 }
);
const result = new Uint8Array(plainText.sigBytes);
@@ -115,7 +121,9 @@ class NcmDecrypt {
this.offset += 4;
if (metaDataLen === 0) return {};
- const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen).map((data) => data ^ 0x63);
+ const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen).map(
+ (data) => data ^ 0x63
+ );
this.offset += metaDataLen;
WordArray.create();
@@ -124,11 +132,11 @@ class NcmDecrypt {
{
ciphertext: Base64.parse(
// @ts-ignore
- WordArray.create(cipherText.slice(22)).toString(EncUTF8),
+ WordArray.create(cipherText.slice(22)).toString(EncUTF8)
),
},
META_KEY,
- { mode: ModeECB, padding: PKCS7 },
+ { mode: ModeECB, padding: PKCS7 }
).toString(EncUTF8);
const labelIndex = plainText.indexOf(':');
@@ -140,7 +148,8 @@ class NcmDecrypt {
result = JSON.parse(plainText.slice(labelIndex + 1));
}
if (!!result.albumPic) {
- result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500';
+ result.albumPic =
+ result.albumPic.replace('http://', 'https://') + '?param=500y500';
}
return result;
}
@@ -149,7 +158,8 @@ class NcmDecrypt {
this.offset += this.view.getUint32(this.offset + 5, true) + 13;
const audioData = new Uint8Array(this.raw, this.offset);
let lenAudioData = audioData.length;
- for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff];
+ for (let cur = 0; cur < lenAudioData; ++cur)
+ audioData[cur] ^= keyBox[cur & 0xff];
return audioData;
}
@@ -175,15 +185,20 @@ class NcmDecrypt {
try {
this.image = await GetImageFromURL(this.oriMeta.albumPic);
while (this.image && this.image.buffer.byteLength >= 1 << 24) {
- let img = await jimp.read(Buffer.from(this.image.buffer));
- await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
- this.image.buffer = await img.getBufferAsync('image/jpeg');
+ // let img = await jimp.read(Buffer.from(this.image.buffer));
+ // await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
+ // this.image.buffer = await img.getBufferAsync('image/jpeg');
}
} catch (e) {
console.log('get cover image failed', e);
}
- this.newMeta = { title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer };
+ this.newMeta = {
+ title: info.title,
+ artists,
+ album: this.oriMeta.album,
+ picture: this.image?.buffer,
+ };
}
async _writeMeta() {
@@ -192,14 +207,21 @@ class NcmDecrypt {
if (!this.blob) this.blob = new Blob([this.audio], { type: this.mime });
const ori = await metaParseBlob(this.blob);
- let shouldWrite = !ori.common.album && !ori.common.artists && !ori.common.title;
+ let shouldWrite =
+ !ori.common.album && !ori.common.artists && !ori.common.title;
if (shouldWrite || this.newMeta.picture) {
if (this.format === 'mp3') {
this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori);
} else if (this.format === 'flac') {
- this.audio = WriteMetaToFlac(Buffer.from(this.audio), this.newMeta, ori);
+ this.audio = WriteMetaToFlac(
+ Buffer.from(this.audio),
+ this.newMeta,
+ ori
+ );
} else {
- console.info(`writing meta for ${this.format} is not being supported for now`);
+ console.info(
+ `writing meta for ${this.format} is not being supported for now`
+ );
return;
}
this.blob = new Blob([this.audio], { type: this.mime });
diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts
index db3e2fe..2f5e29c 100644
--- a/src/decrypt/qmc.ts
+++ b/src/decrypt/qmc.ts
@@ -1,4 +1,9 @@
-import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher';
+import {
+ QmcMapCipher,
+ QmcRC4Cipher,
+ QmcStaticCipher,
+ QmcStreamCipher,
+} from './qmc_cipher';
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
@@ -37,7 +42,11 @@ export const HandlerMap: { [key: string]: Handler } = {
'776176': { ext: 'wav', version: 1 },
};
-export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise {
+export async function Decrypt(
+ file: Blob,
+ raw_filename: string,
+ raw_ext: string
+): Promise {
if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
const handler = HandlerMap[raw_ext];
let { version } = handler;
@@ -56,7 +65,10 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
musicID = v2Decrypted.songId;
console.log('qmc wasm decoder suceeded');
} else {
- console.warn('QmcWasm failed with error %s', v2Decrypted.error || '(unknown error)');
+ console.warn(
+ 'QmcWasm failed with error %s',
+ v2Decrypted.error || '(unknown error)'
+ );
}
}
@@ -75,7 +87,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
new Blob([musicDecoded], { type: mime }),
raw_filename,
ext,
- musicID,
+ musicID
);
return {
diff --git a/src/decrypt/qmc_wasm.ts b/src/decrypt/qmc_wasm.ts
index c2e06db..b627d46 100644
--- a/src/decrypt/qmc_wasm.ts
+++ b/src/decrypt/qmc_wasm.ts
@@ -1,8 +1,8 @@
-import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
+import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
// 每次处理 2M 的数据
-const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
+const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
export interface QMCDecryptionResult {
success: boolean;
@@ -17,8 +17,16 @@ export interface QMCDecryptionResult {
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
* @param {ArrayBuffer} qmcBlob 读入的文件 Blob
*/
-export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise {
- const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
+export async function DecryptQmcWasm(
+ qmcBlob: ArrayBuffer,
+ ext: string
+): Promise {
+ const result: QMCDecryptionResult = {
+ success: false,
+ data: new Uint8Array(),
+ songId: 0,
+ error: '',
+ };
// 初始化模组
let QmcCrypto: any;
@@ -47,7 +55,7 @@ export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise
return result;
} else {
result.songId = QmcCrypto.getSongId();
- result.songId = result.songId == "0" ? 0 : result.songId;
+ result.songId = result.songId == '0' ? 0 : result.songId;
}
const decryptedParts = [];
@@ -59,7 +67,12 @@ export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise
// 解密一些片段
const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
QmcCrypto.writeArrayToMemory(blockData, pQmcBuf);
- decryptedParts.push(QmcCrypto.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCrypto.decBlob(pQmcBuf, blockSize, offset)));
+ decryptedParts.push(
+ QmcCrypto.HEAPU8.slice(
+ pQmcBuf,
+ pQmcBuf + QmcCrypto.decBlob(pQmcBuf, blockSize, offset)
+ )
+ );
offset += blockSize;
bytesToDecrypt -= blockSize;
diff --git a/public/ixarea-stats.js b/src/ixarea-stats.js
similarity index 100%
rename from public/ixarea-stats.js
rename to src/ixarea-stats.js
diff --git a/src/loader.css b/src/loader.css
new file mode 100644
index 0000000..25fe9a9
--- /dev/null
+++ b/src/loader.css
@@ -0,0 +1,51 @@
+#loader {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ z-index: 1010;
+ margin: -75px 0 0 -75px;
+ border: 16px solid #f3f3f3;
+ border-radius: 50%;
+ border-top: 16px solid #1db1ff;
+ width: 120px;
+ height: 120px;
+ animation: spin 2s linear infinite;
+}
+@keyframes spin {
+ 0% {
+ transform: rotate(0);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+#loader-mask {
+ text-align: center;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+ z-index: 1009;
+ background-color: rgba(242, 246, 252, 0.88);
+}
+@media (prefers-color-scheme: dark) {
+ #loader-mask {
+ color: #fff;
+ background-color: rgba(0, 0, 0, 0.85);
+ }
+ #loader-mask a {
+ color: #ddd;
+ }
+ #loader-mask a:hover {
+ color: #1db1ff;
+ }
+}
+#loader-source {
+ font-size: 1.5rem;
+}
+#loader-tips-timeout {
+ font-size: 1.2rem;
+}
diff --git a/src/loader.js b/src/loader.js
new file mode 100644
index 0000000..082a3ab
--- /dev/null
+++ b/src/loader.js
@@ -0,0 +1,27 @@
+import './loader.css';
+
+(function () {
+ setTimeout(function () {
+ var ele = document.getElementById('loader-tips-timeout');
+ if (ele != null) {
+ ele.hidden = false;
+ }
+ }, 2000);
+
+ var ua = navigator && navigator.userAgent;
+ var detected = (function () {
+ var m;
+ if (!ua) return true;
+ if (/MSIE |Trident\//.exec(ua)) return true; // no IE
+ m = /Edge\/([\d.]+)/.exec(ua); // Edge >= 17
+ if (m && Number(m[1]) < 17) return true;
+ m = /Chrome\/([\d.]+)/.exec(ua); // Chrome >= 58
+ if (m && Number(m[1]) < 58) return true;
+ m = /Firefox\/([\d.]+)/.exec(ua); // Firefox >= 45
+ return m && Number(m[1]) < 45;
+ })();
+ if (detected) {
+ document.getElementById('loader-tips-outdated').hidden = false;
+ document.getElementById('loader-tips-timeout').hidden = false;
+ }
+})();
diff --git a/src/main.ts b/src/main.ts
index b046b80..52ed9d8 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,56 +1,11 @@
-import Vue from 'vue';
+import { createApp } from 'vue';
import App from '@/App.vue';
import '@/registerServiceWorker';
-import {
- Button,
- Checkbox,
- Col,
- Container,
- Dialog,
- Form,
- FormItem,
- Footer,
- Icon,
- Image,
- Input,
- Link,
- Main,
- Notification,
- Progress,
- Radio,
- Row,
- Table,
- TableColumn,
- Tooltip,
- Upload,
- MessageBox,
-} from 'element-ui';
-import 'element-ui/lib/theme-chalk/base.css';
+import '@element-plus/theme-chalk/dist/index.css';
+import * as ElementPlusIconsVue from '@element-plus/icons-vue';
-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);
-Vue.use(Footer);
-Vue.use(Container);
-Vue.use(Icon);
-Vue.use(Row);
-Vue.use(Col);
-Vue.use(Upload);
-Vue.use(Checkbox);
-Vue.use(Radio);
-Vue.use(Tooltip);
-Vue.use(Progress);
-Vue.prototype.$notify = Notification;
-Vue.prototype.$confirm = MessageBox.confirm;
-
-Vue.config.productionTip = false;
-new Vue({
- render: (h) => h(App),
-}).$mount('#app');
+const app = createApp(App);
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+ app.component(key, component);
+}
+app.mount('#app');
diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts
index 8f6f410..e69de29 100644
--- a/src/shims-vue.d.ts
+++ b/src/shims-vue.d.ts
@@ -1,4 +0,0 @@
-declare module '*.vue' {
- import Vue from 'vue';
- export default Vue;
-}
diff --git a/src/view/Home.vue b/src/view/Home.vue
index 69a6f5d..17aea99 100644
--- a/src/view/Home.vue
+++ b/src/view/Home.vue
@@ -5,7 +5,12 @@
歌曲命名格式:
-
+
{{ k.text }}
@@ -18,44 +23,83 @@
:album="editing_data.album"
:albumartist="editing_data.albumartist"
:genre="editing_data.genre"
- @cancel="showEditDialog = false" @ok="handleEdit">
-
+ @cancel="showEditDialog = false"
+ @ok="handleEdit"
+ >
+
-
+
部分解密方案需要设定解密参数。
-
- 解密设定
+
+
+ 解密设定
+
-
下载全部
-
清除全部
+
+ 下载全部
+
+
+ 清除全部
+
-
- 工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}
+
+
+ 工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}
+
当您使用此工具进行大量文件解锁的时候,建议开启此选项。
开启后,解锁结果将不会存留于浏览器中,防止内存不足。
-
- 立即保存
+
+
+ 立即保存
+
-
+