refactor: initial attempt with vue 3 + vite, with some mis-allignments.
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Generated by 'unplugin-auto-import'
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// generated by unplugin-vue-components
|
||||||
|
// We suggest you to commit this file into source control
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core'
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
ElFooter: typeof import('element-plus/es')['ElFooter']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
|
IEpUpload: typeof import('~icons/ep/upload')['default']
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,65 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta content="webkit" name="renderer" />
|
||||||
|
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
|
||||||
|
<meta content="width=device-width,initial-scale=1.0" name="viewport" />
|
||||||
|
<title>音乐解锁</title>
|
||||||
|
<meta
|
||||||
|
content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密"
|
||||||
|
name="keywords"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
content="音乐解锁 - 在任何设备上解锁已购的加密音乐!"
|
||||||
|
name="description"
|
||||||
|
/>
|
||||||
|
<script src="./src/ixarea-stats.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loader-mask">
|
||||||
|
<div id="loader"></div>
|
||||||
|
<noscript>
|
||||||
|
<h3 id="loader-js">请启用JavaScript</h3>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src="https://stats.ixarea.com/ixarea-stats/report?rec=1&action_name=音乐解锁-NoJS&idsite=2"
|
||||||
|
style="border: 0"
|
||||||
|
/>
|
||||||
|
</noscript>
|
||||||
|
<h3 id="loader-source">请勿直接运行源代码!</h3>
|
||||||
|
<div id="loader-tips-outdated" hidden>
|
||||||
|
<h2>
|
||||||
|
您可能在使用不受支持的<span style="color: #f00">过时</span
|
||||||
|
>浏览器,这可能导致此应用无法正常工作。
|
||||||
|
</h2>
|
||||||
|
<h3>
|
||||||
|
如果您使用双核浏览器,您可以尝试切换到
|
||||||
|
<span style="color: #f00">“极速模式”</span> 解决此问题。
|
||||||
|
</h3>
|
||||||
|
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
|
||||||
|
</div>
|
||||||
|
<h3 id="loader-tips-timeout" hidden>
|
||||||
|
音乐解锁采用了一些新特性!建议使用
|
||||||
|
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank"
|
||||||
|
>Microsoft Edge Chromium</a
|
||||||
|
>
|
||||||
|
<a href="https://www.google.cn/chrome/" target="_blank"
|
||||||
|
>Google Chrome</a
|
||||||
|
>
|
||||||
|
<a href="https://www.firefox.com.cn/" target="_blank"
|
||||||
|
>Mozilla Firefox</a
|
||||||
|
>
|
||||||
|
|
|
||||||
|
<a
|
||||||
|
href="https://github.com/ix64/unlock-music/wiki/使用提示"
|
||||||
|
target="_blank"
|
||||||
|
>使用提示</a
|
||||||
|
>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./src/loader.js"></script>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
44
package.json
|
@ -10,10 +10,14 @@
|
||||||
"url": "https://github.com/ix64/unlock-music"
|
"url": "https://github.com/ix64/unlock-music"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"prettier": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "vite build",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"pretty": "prettier --write src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
"pretty": "prettier --write src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
||||||
"pretty:check": "prettier --check src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
"pretty:check": "prettier --check src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
||||||
|
@ -21,39 +25,47 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.16.5",
|
"@babel/preset-typescript": "^7.16.5",
|
||||||
|
"@element-plus/icons-vue": "^2.0.10",
|
||||||
|
"@element-plus/theme-chalk": "^2.2.16",
|
||||||
"@jixun/kugou-crypto": "^1.0.3",
|
"@jixun/kugou-crypto": "^1.0.3",
|
||||||
"@unlock-music/joox-crypto": "^0.0.1-R5",
|
"@unlock-music/joox-crypto": "^0.0.1-R5",
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
"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",
|
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"element-ui": "^2.15.5",
|
"element-plus": "^2.2.25",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jimp": "^0.16.1",
|
|
||||||
"metaflac-js": "^1.0.5",
|
"metaflac-js": "^1.0.5",
|
||||||
"music-metadata": "7.9.0",
|
"music-metadata": "7.9.0",
|
||||||
"music-metadata-browser": "2.2.7",
|
"music-metadata-browser": "2.2.7",
|
||||||
|
"postcss": "^8.4.19",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"threads": "^1.6.5",
|
"threads": "^1.6.5",
|
||||||
"vue": "^2.6.14"
|
"tslib": "^2.4.1",
|
||||||
|
"vue": "^3.2.45"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||||
|
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||||
"@types/crypto-js": "^4.0.2",
|
"@types/crypto-js": "^4.0.2",
|
||||||
"@types/jest": "^27.0.3",
|
"@types/jest": "^27.0.3",
|
||||||
"@vue/cli-plugin-babel": "^4.5.13",
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
"@vue/cli-plugin-pwa": "^4.5.13",
|
|
||||||
"@vue/cli-plugin-typescript": "^4.5.13",
|
|
||||||
"@vue/cli-service": "^4.5.13",
|
|
||||||
"babel-plugin-component": "^1.1.1",
|
"babel-plugin-component": "^1.1.1",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"jest": "^27.4.5",
|
"jest": "^27.4.5",
|
||||||
"patch-package": "^6.4.7",
|
"patch-package": "^6.4.7",
|
||||||
"prettier": "2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"sass": "^1.38.1",
|
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||||
"sass-loader": "^10.2.0",
|
"sass": "^1.56.1",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
"threads-plugin": "^1.4.0",
|
"threads-plugin": "^1.4.0",
|
||||||
"typescript": "^4.5.4",
|
"typescript": "^4.5.4",
|
||||||
"vue-cli-plugin-element": "^1.0.1",
|
"unplugin-auto-import": "^0.12.0",
|
||||||
"vue-template-compiler": "^2.6.14"
|
"unplugin-icons": "^0.14.14",
|
||||||
|
"unplugin-vue-components": "^0.22.11",
|
||||||
|
"util": "^0.12.5",
|
||||||
|
"vite": "^3.2.4",
|
||||||
|
"vite-plugin-commonjs": "^0.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta content="webkit" name="renderer">
|
|
||||||
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
|
|
||||||
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
|
||||||
<title>音乐解锁</title>
|
|
||||||
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords"/>
|
|
||||||
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description"/>
|
|
||||||
<script src="./ixarea-stats.js"></script>
|
|
||||||
<!--@formatter:off-->
|
|
||||||
<style>#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,.88)}@media (prefers-color-scheme:dark){#loader-mask{color:#fff;background-color:rgba(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}</style>
|
|
||||||
<!--@formatter:on-->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="loader-mask">
|
|
||||||
<div id="loader"></div>
|
|
||||||
<noscript>
|
|
||||||
<h3 id="loader-js">请启用JavaScript</h3>
|
|
||||||
<img alt=""
|
|
||||||
src="https://stats.ixarea.com/ixarea-stats/report?rec=1&action_name=音乐解锁-NoJS&idsite=2"
|
|
||||||
style="border:0"/>
|
|
||||||
</noscript>
|
|
||||||
<h3 id="loader-source"> 请勿直接运行源代码! </h3>
|
|
||||||
<div id="loader-tips-outdated" hidden>
|
|
||||||
<h2>您可能在使用不受支持的<span style="color:#f00;">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
|
|
||||||
<h3>如果您使用双核浏览器,您可以尝试切换到 <span style="color:#f00;">“极速模式”</span> 解决此问题。</h3>
|
|
||||||
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
|
|
||||||
</div>
|
|
||||||
<h3 id="loader-tips-timeout" hidden>
|
|
||||||
音乐解锁采用了一些新特性!建议使用
|
|
||||||
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
|
|
||||||
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
|
|
||||||
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
|
|
||||||
| <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script src="./loader.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,25 +0,0 @@
|
||||||
(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;
|
|
||||||
}
|
|
||||||
})();
|
|
47
src/App.vue
|
@ -5,19 +5,35 @@
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer id="app-footer">
|
<el-footer id="app-footer">
|
||||||
<el-row>
|
<el-row>
|
||||||
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }})
|
<a href="https://github.com/ix64/unlock-music" target="_blank"
|
||||||
:移除已购音乐的加密保护。
|
>音乐解锁</a
|
||||||
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
>({{ version }}) :移除已购音乐的加密保护。
|
||||||
|
<a
|
||||||
|
href="https://github.com/ix64/unlock-music/wiki/使用提示"
|
||||||
|
target="_blank"
|
||||||
|
>使用提示</a
|
||||||
|
>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
|
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm),
|
||||||
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。
|
虾米音乐(xm), 酷我音乐(.kwm)
|
||||||
|
<a
|
||||||
|
href="https://github.com/ix64/unlock-music/blob/master/README.md"
|
||||||
|
target="_blank"
|
||||||
|
>更多</a
|
||||||
|
>。
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
||||||
<span>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span>
|
<span
|
||||||
|
>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span
|
||||||
|
>
|
||||||
音乐解锁使用
|
音乐解锁使用
|
||||||
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
|
<a
|
||||||
|
href="https://github.com/ix64/unlock-music/blob/master/LICENSE"
|
||||||
|
target="_blank"
|
||||||
|
>MIT许可协议</a
|
||||||
|
>
|
||||||
开放源代码
|
开放源代码
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
|
@ -25,13 +41,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import './scss/unlock-music.scss';
|
||||||
|
import { ElNotification } from 'element-plus';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
import FileSelector from '@/component/FileSelector';
|
import FileSelector from '@/component/FileSelector';
|
||||||
import PreviewTable from '@/component/PreviewTable';
|
import PreviewTable from '@/component/PreviewTable';
|
||||||
import config from '@/../package.json';
|
import config from '@/../package.json';
|
||||||
import Home from '@/view/Home';
|
import Home from '@/view/Home';
|
||||||
import { checkUpdate } from '@/utils/api';
|
import { checkUpdate } from '@/utils/api';
|
||||||
|
|
||||||
export default {
|
export default defineComponent({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: {
|
components: {
|
||||||
FileSelector,
|
FileSelector,
|
||||||
|
@ -59,7 +78,8 @@ export default {
|
||||||
if (
|
if (
|
||||||
updateInfo &&
|
updateInfo &&
|
||||||
process.env.NODE_ENV === 'production' &&
|
process.env.NODE_ENV === 'production' &&
|
||||||
(updateInfo.HttpsFound || (updateInfo.Found && window.location.protocol !== 'https:'))
|
(updateInfo.HttpsFound ||
|
||||||
|
(updateInfo.Found && window.location.protocol !== 'https:'))
|
||||||
) {
|
) {
|
||||||
this.$notify.warning({
|
this.$notify.warning({
|
||||||
title: '发现更新',
|
title: '发现更新',
|
||||||
|
@ -69,7 +89,8 @@ export default {
|
||||||
position: 'top-left',
|
position: 'top-left',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$notify.info({
|
ElNotification({
|
||||||
|
type: 'info',
|
||||||
title: '离线使用',
|
title: '离线使用',
|
||||||
message: `<div>
|
message: `<div>
|
||||||
<p>我们使用 PWA 技术,无网络也能使用</p>
|
<p>我们使用 PWA 技术,无网络也能使用</p>
|
||||||
|
@ -86,9 +107,5 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import 'scss/unlock-music';
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -26,8 +26,20 @@ form >>> input {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog @close="cancel()" title="解密设定" :visible="show" custom-class="um-config-dialog" center>
|
<el-dialog
|
||||||
<el-form ref="form" :rules="rules" status-icon :model="form" label-width="0">
|
@close="cancel()"
|
||||||
|
title="解密设定"
|
||||||
|
v-model="internalShow"
|
||||||
|
class="um-config-dialog"
|
||||||
|
center
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="form"
|
||||||
|
:rules="rules"
|
||||||
|
status-icon
|
||||||
|
:model="form"
|
||||||
|
label-width="0"
|
||||||
|
>
|
||||||
<section>
|
<section>
|
||||||
<label>
|
<label>
|
||||||
<span>
|
<span>
|
||||||
|
@ -35,7 +47,14 @@ form >>> input {
|
||||||
<Ruby caption="Unique Device Identifier">设备唯一识别码</Ruby>
|
<Ruby caption="Unique Device Identifier">设备唯一识别码</Ruby>
|
||||||
</span>
|
</span>
|
||||||
<el-form-item prop="jooxUUID">
|
<el-form-item prop="jooxUUID">
|
||||||
<el-input type="text" v-model="form.jooxUUID" clearable maxlength="32" show-word-limit> </el-input>
|
<el-input
|
||||||
|
type="text"
|
||||||
|
v-model="form.jooxUUID"
|
||||||
|
clearable
|
||||||
|
maxlength="32"
|
||||||
|
show-word-limit
|
||||||
|
>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -43,15 +62,21 @@ form >>> input {
|
||||||
下载该加密文件的 JOOX 应用所记录的设备唯一识别码。
|
下载该加密文件的 JOOX 应用所记录的设备唯一识别码。
|
||||||
<br />
|
<br />
|
||||||
参见:
|
参见:
|
||||||
<a href="https://github.com/unlock-music/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID">
|
<a
|
||||||
|
href="https://github.com/unlock-music/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID"
|
||||||
|
>
|
||||||
获取设备 UUID · unlock-music/joox-crypto Wiki</a
|
获取设备 UUID · unlock-music/joox-crypto Wiki</a
|
||||||
>。
|
>。
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</el-form>
|
</el-form>
|
||||||
<span slot="footer" class="dialog-footer">
|
<template #footer>
|
||||||
<el-button type="primary" :loading="saving" @click="emitConfirm()">确 定</el-button>
|
<span class="dialog-footer">
|
||||||
</span>
|
<el-button type="primary" :loading="saving" @click="emitConfirm()">
|
||||||
|
确 定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -86,9 +111,15 @@ export default {
|
||||||
form: {
|
form: {
|
||||||
jooxUUID: '',
|
jooxUUID: '',
|
||||||
},
|
},
|
||||||
|
internalShow: false,
|
||||||
centerDialogVisible: false,
|
centerDialogVisible: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
show(newValue, oldValue) {
|
||||||
|
this.internalShow = newValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.resetForm();
|
await this.resetForm();
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,58 +26,95 @@ form >>> input {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog @close="cancel()" title="音乐标签编辑" :visible="show" custom-class="um-edit-dialog" center>
|
<el-dialog
|
||||||
|
@close="cancel()"
|
||||||
|
title="音乐标签编辑"
|
||||||
|
v-model="internalShow"
|
||||||
|
class="um-edit-dialog"
|
||||||
|
center
|
||||||
|
>
|
||||||
<el-form ref="form" status-icon :model="form" label-width="0">
|
<el-form ref="form" status-icon :model="form" label-width="0">
|
||||||
<section>
|
<section>
|
||||||
<el-image v-show="!editPicture" :src="imgFile.url || picture" style="width: 100px; height: 100px">
|
<el-image
|
||||||
<div slot="error" class="image-slot el-image__error">暂无封面</div>
|
v-show="!editPicture"
|
||||||
|
:src="imgFile.url || picture"
|
||||||
|
style="width: 100px; height: 100px"
|
||||||
|
>
|
||||||
|
<template #error class="image-slot el-image__error">
|
||||||
|
暂无封面
|
||||||
|
</template>
|
||||||
</el-image>
|
</el-image>
|
||||||
<el-upload v-show="editPicture" :auto-upload="false" :on-change="addFile" :on-remove="rmvFile" :show-file-list="true" :limit="1" list-type="picture" action="" drag>
|
<el-upload
|
||||||
<i class="el-icon-upload" />
|
v-show="editPicture"
|
||||||
<div class="el-upload__text">将新图片拖到此处,或<em>点击选择</em><br />以替换自动匹配的图片</div>
|
:auto-upload="false"
|
||||||
<div slot="tip" class="el-upload__tip">
|
:on-change="addFile"
|
||||||
新拖到此处的图片将覆盖原始图片
|
:on-remove="rmvFile"
|
||||||
|
:show-file-list="true"
|
||||||
|
:limit="1"
|
||||||
|
list-type="picture"
|
||||||
|
action=""
|
||||||
|
drag
|
||||||
|
>
|
||||||
|
<el-icon><UploadFilled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将新图片拖到此处,或<em>点击选择</em><br />以替换自动匹配的图片
|
||||||
</div>
|
</div>
|
||||||
</el-upload>
|
<template #tip class="el-upload__tip">
|
||||||
|
新拖到此处的图片将覆盖原始图片
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editPicture, 'el-icon-check': editPicture}"
|
:class="{
|
||||||
|
'el-icon-edit': !editPicture,
|
||||||
|
'el-icon-check': editPicture,
|
||||||
|
}"
|
||||||
@click="changeCover"
|
@click="changeCover"
|
||||||
></i><br />
|
></i>
|
||||||
标题:
|
<br />
|
||||||
<span v-show="!editTitle">{{title}}</span>
|
标题:
|
||||||
<el-input v-show="editTitle" v-model="title"></el-input>
|
<span v-show="!editTitle">{{ title }}</span>
|
||||||
|
<!-- <el-input v-show="editTitle" v-model="title"></el-input> -->
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editTitle, 'el-icon-check': editTitle}"
|
:class="{ 'el-icon-edit': !editTitle, 'el-icon-check': editTitle }"
|
||||||
@click="editTitle = !editTitle"
|
@click="editTitle = !editTitle"
|
||||||
></i><br />
|
></i
|
||||||
艺术家:
|
><br />
|
||||||
<span v-show="!editArtist">{{artist}}</span>
|
艺术家:
|
||||||
<el-input v-show="editArtist" v-model="artist"></el-input>
|
<span v-show="!editArtist">{{ artist }}</span>
|
||||||
|
<!-- <el-input v-show="editArtist" v-model="artist"></el-input> -->
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editArtist, 'el-icon-check': editArtist}"
|
:class="{ 'el-icon-edit': !editArtist, 'el-icon-check': editArtist }"
|
||||||
@click="editArtist = !editArtist"
|
@click="editArtist = !editArtist"
|
||||||
></i><br />
|
></i
|
||||||
专辑:
|
><br />
|
||||||
<span v-show="!editAlbum">{{album}}</span>
|
专辑:
|
||||||
<el-input v-show="editAlbum" v-model="album"></el-input>
|
<span v-show="!editAlbum">{{ album }}</span>
|
||||||
|
<!-- <el-input v-show="editAlbum" v-model="album"></el-input> -->
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum}"
|
:class="{ 'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum }"
|
||||||
@click="editAlbum = !editAlbum"
|
@click="editAlbum = !editAlbum"
|
||||||
></i><br />
|
></i
|
||||||
专辑艺术家:
|
><br />
|
||||||
<span v-show="!editAlbumartist">{{albumartist}}</span>
|
专辑艺术家:
|
||||||
<el-input v-show="editAlbumartist" v-model="albumartist"></el-input>
|
<span v-show="!editAlbumartist">{{ albumartist }}</span>
|
||||||
|
<!-- <el-input v-show="editAlbumartist" v-model="albumartist"></el-input> -->
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editAlbumartist, 'el-icon-check': editAlbumartist}"
|
:class="{
|
||||||
|
'el-icon-edit': !editAlbumartist,
|
||||||
|
'el-icon-check': editAlbumartist,
|
||||||
|
}"
|
||||||
@click="editAlbumartist = !editAlbumartist"
|
@click="editAlbumartist = !editAlbumartist"
|
||||||
></i><br />
|
></i
|
||||||
风格:
|
><br />
|
||||||
<span v-show="!editGenre">{{genre}}</span>
|
风格:
|
||||||
<el-input v-show="editGenre" v-model="genre"></el-input>
|
<span v-show="!editGenre">{{ genre }}</span>
|
||||||
|
<!-- <el-input v-show="editGenre" v-model="genre"></el-input> -->
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editGenre, 'el-icon-check': editGenre}"
|
:class="{ 'el-icon-edit': !editGenre, 'el-icon-check': editGenre }"
|
||||||
@click="editGenre = !editGenre"
|
@click="editGenre = !editGenre"
|
||||||
></i><br />
|
></i
|
||||||
|
><br />
|
||||||
|
|
||||||
<p class="item-desc">
|
<p class="item-desc">
|
||||||
为了节省您设备的资源,请在确定前充分检查,避免反复修改。<br />
|
为了节省您设备的资源,请在确定前充分检查,避免反复修改。<br />
|
||||||
|
@ -85,9 +122,11 @@ form >>> input {
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</el-form>
|
</el-form>
|
||||||
<span slot="footer" class="dialog-footer">
|
<template #footer>
|
||||||
<el-button type="primary" @click="emitConfirm()">确 定</el-button>
|
<span class="dialog-footer">
|
||||||
</span>
|
<el-button type="primary" @click="emitConfirm()">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -100,17 +139,17 @@ export default {
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
show: { type: Boolean, required: true },
|
show: { type: Boolean, required: true },
|
||||||
picture: { type: String | undefined, required: true },
|
picture: { type: String, required: true },
|
||||||
title: { type: String | undefined, required: true },
|
title: { type: String, required: true },
|
||||||
artist: { type: String | undefined, required: true },
|
artist: { type: String, required: true },
|
||||||
album: { type: String | undefined, required: true },
|
album: { type: String, required: true },
|
||||||
albumartist: { type: String | undefined, required: true },
|
albumartist: { type: String, required: true },
|
||||||
genre: { type: String | undefined, required: true },
|
genre: { type: String, required: true },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
internalShow: false,
|
||||||
},
|
form: {},
|
||||||
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
|
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
|
||||||
editPicture: false,
|
editPicture: false,
|
||||||
editTitle: false,
|
editTitle: false,
|
||||||
|
@ -120,6 +159,11 @@ export default {
|
||||||
editGenre: false,
|
editGenre: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
show(newValue, oldValue) {
|
||||||
|
this.internalShow = newValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.refreshForm();
|
this.refreshForm();
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,26 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<el-upload :auto-upload="false" :on-change="addFile" :show-file-list="false" action="" drag multiple>
|
<el-upload
|
||||||
<i class="el-icon-upload" />
|
:auto-upload="false"
|
||||||
<div class="el-upload__text">将文件拖到此处,或 <em>点击选择</em></div>
|
:on-change="addFile"
|
||||||
<div slot="tip" class="el-upload__tip">
|
:show-file-list="false"
|
||||||
|
action=""
|
||||||
|
drag
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<el-icon size="80"><UploadFilled /></el-icon>
|
||||||
|
<div>将文件拖到此处,或 <em>点击选择</em></div>
|
||||||
|
<template #tip>
|
||||||
<div>
|
<div>
|
||||||
仅在浏览器内对文件进行解锁,无需消耗流量
|
仅在浏览器内对文件进行解锁,无需消耗流量
|
||||||
<el-tooltip effect="dark" placement="top-start">
|
<el-tooltip effect="dark" placement="top-start">
|
||||||
<div slot="content">算法在源代码中已经提供,所有运算都发生在本地</div>
|
<template #content>
|
||||||
<i class="el-icon-info" style="font-size: 12px" />
|
算法在源代码中已经提供,所有运算都发生在本地
|
||||||
|
</template>
|
||||||
|
<el-icon size="12">
|
||||||
|
<InfoFilled />
|
||||||
|
</el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
|
工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
|
||||||
<el-tooltip effect="dark" placement="top-start">
|
<el-tooltip effect="dark" placement="top-start">
|
||||||
<div slot="content">
|
<template #content>
|
||||||
将此工具部署在HTTPS环境下,可以启用Web Worker特性,<br />
|
将此工具部署在HTTPS环境下,可以启用Web Worker特性,<br />
|
||||||
从而更快的利用并行处理完成解锁
|
从而更快的利用并行处理完成解锁
|
||||||
</div>
|
</template>
|
||||||
<i class="el-icon-info" style="font-size: 12px" />
|
<el-icon size="12">
|
||||||
|
<InfoFilled />
|
||||||
|
</el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<transition name="el-fade-in"
|
<transition name="el-fade-in"
|
||||||
><!--todo: add delay to animation-->
|
><!--todo: add delay to animation-->
|
||||||
<el-progress
|
<el-progress
|
||||||
|
@ -60,9 +73,16 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (window.Worker && window.location.protocol !== 'file:' && process.env.NODE_ENV === 'production') {
|
if (
|
||||||
|
window.Worker &&
|
||||||
|
window.location.protocol !== 'file:' &&
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
) {
|
||||||
console.log('Using Worker Pool');
|
console.log('Using Worker Pool');
|
||||||
this.queue = Pool(() => spawn(new Worker('@/utils/worker.ts')), navigator.hardwareConcurrency || 1);
|
this.queue = Pool(
|
||||||
|
() => spawn(new Worker('@/utils/worker.ts')),
|
||||||
|
navigator.hardwareConcurrency || 1
|
||||||
|
);
|
||||||
this.parallel = true;
|
this.parallel = true;
|
||||||
} else {
|
} else {
|
||||||
console.log('Using Queue in Main Thread');
|
console.log('Using Queue in Main Thread');
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<el-table :data="tableData" style="width: 100%">
|
<el-table :data="tableData" style="width: 100%">
|
||||||
<el-table-column label="封面">
|
<el-table-column label="封面">
|
||||||
<template slot-scope="scope">
|
<template #default="scope">
|
||||||
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
|
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
|
||||||
<div slot="error" class="image-slot el-image__error">暂无封面</div>
|
<template #error class="image-slot el-image__error">
|
||||||
|
暂无封面
|
||||||
|
</template>
|
||||||
</el-image>
|
</el-image>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
@ -24,11 +26,31 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
|
<el-button
|
||||||
|
circle
|
||||||
|
type="success"
|
||||||
|
@click="handlePlay(scope.$index, scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon size="20" style="vertical-align: middle">
|
||||||
|
<VideoPlay />
|
||||||
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button>
|
<el-button circle @click="handleDownload(scope.row)">
|
||||||
<el-button circle icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button>
|
<el-icon size="20" style="vertical-align: middle">
|
||||||
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
|
<Download />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button circle @click="handleEdit(scope.row)">
|
||||||
|
<el-icon size="20" style="vertical-align: middle"><Edit /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
circle
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.$index, scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon size="20" style="vertical-align: middle">
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import KgmCryptoModule from '@/KgmWasm/KgmWasmBundle';
|
||||||
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
|
|
||||||
// 每次处理 2M 的数据
|
// 每次处理 2M 的数据
|
||||||
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export interface KGMDecryptionResult {
|
export interface KGMDecryptionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
@ -16,8 +16,15 @@ export interface KGMDecryptionResult {
|
||||||
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
||||||
* @param {ArrayBuffer} kgmBlob 读入的文件 Blob
|
* @param {ArrayBuffer} kgmBlob 读入的文件 Blob
|
||||||
*/
|
*/
|
||||||
export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
|
export async function DecryptKgmWasm(
|
||||||
const result: KGMDecryptionResult = { success: false, data: new Uint8Array(), error: '' };
|
kgmBlob: ArrayBuffer,
|
||||||
|
ext: string
|
||||||
|
): Promise<KGMDecryptionResult> {
|
||||||
|
const result: KGMDecryptionResult = {
|
||||||
|
success: false,
|
||||||
|
data: new Uint8Array(),
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化模组
|
// 初始化模组
|
||||||
let KgmCrypto: any;
|
let KgmCrypto: any;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
WriteMetaToMp3,
|
WriteMetaToMp3,
|
||||||
} from '@/decrypt/utils';
|
} from '@/decrypt/utils';
|
||||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||||
import jimp from 'jimp';
|
// import jimp from 'jimp';
|
||||||
|
|
||||||
import AES from 'crypto-js/aes';
|
import AES from 'crypto-js/aes';
|
||||||
import PKCS7 from 'crypto-js/pad-pkcs7';
|
import PKCS7 from 'crypto-js/pad-pkcs7';
|
||||||
|
@ -26,7 +26,11 @@ const CORE_KEY = EncHex.parse('687a4852416d736f356b496e62617857');
|
||||||
const META_KEY = EncHex.parse('2331346C6A6B5F215C5D2630553C2728');
|
const META_KEY = EncHex.parse('2331346C6A6B5F215C5D2630553C2728');
|
||||||
const MagicHeader = [0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d];
|
const MagicHeader = [0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d];
|
||||||
|
|
||||||
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
|
export async function Decrypt(
|
||||||
|
file: File,
|
||||||
|
raw_filename: string,
|
||||||
|
_: string
|
||||||
|
): Promise<DecryptResult> {
|
||||||
return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt();
|
return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,14 +72,16 @@ class NcmDecrypt {
|
||||||
_getKeyData(): Uint8Array {
|
_getKeyData(): Uint8Array {
|
||||||
const keyLen = this.view.getUint32(this.offset, true);
|
const keyLen = this.view.getUint32(this.offset, true);
|
||||||
this.offset += 4;
|
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;
|
this.offset += keyLen;
|
||||||
|
|
||||||
const plainText = AES.decrypt(
|
const plainText = AES.decrypt(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
{ ciphertext: WordArray.create(cipherText) },
|
{ ciphertext: WordArray.create(cipherText) },
|
||||||
CORE_KEY,
|
CORE_KEY,
|
||||||
{ mode: ModeECB, padding: PKCS7 },
|
{ mode: ModeECB, padding: PKCS7 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = new Uint8Array(plainText.sigBytes);
|
const result = new Uint8Array(plainText.sigBytes);
|
||||||
|
@ -115,7 +121,9 @@ class NcmDecrypt {
|
||||||
this.offset += 4;
|
this.offset += 4;
|
||||||
if (metaDataLen === 0) return {};
|
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;
|
this.offset += metaDataLen;
|
||||||
|
|
||||||
WordArray.create();
|
WordArray.create();
|
||||||
|
@ -124,11 +132,11 @@ class NcmDecrypt {
|
||||||
{
|
{
|
||||||
ciphertext: Base64.parse(
|
ciphertext: Base64.parse(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
WordArray.create(cipherText.slice(22)).toString(EncUTF8),
|
WordArray.create(cipherText.slice(22)).toString(EncUTF8)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
META_KEY,
|
META_KEY,
|
||||||
{ mode: ModeECB, padding: PKCS7 },
|
{ mode: ModeECB, padding: PKCS7 }
|
||||||
).toString(EncUTF8);
|
).toString(EncUTF8);
|
||||||
|
|
||||||
const labelIndex = plainText.indexOf(':');
|
const labelIndex = plainText.indexOf(':');
|
||||||
|
@ -140,7 +148,8 @@ class NcmDecrypt {
|
||||||
result = JSON.parse(plainText.slice(labelIndex + 1));
|
result = JSON.parse(plainText.slice(labelIndex + 1));
|
||||||
}
|
}
|
||||||
if (!!result.albumPic) {
|
if (!!result.albumPic) {
|
||||||
result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500';
|
result.albumPic =
|
||||||
|
result.albumPic.replace('http://', 'https://') + '?param=500y500';
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -149,7 +158,8 @@ class NcmDecrypt {
|
||||||
this.offset += this.view.getUint32(this.offset + 5, true) + 13;
|
this.offset += this.view.getUint32(this.offset + 5, true) + 13;
|
||||||
const audioData = new Uint8Array(this.raw, this.offset);
|
const audioData = new Uint8Array(this.raw, this.offset);
|
||||||
let lenAudioData = audioData.length;
|
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;
|
return audioData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,15 +185,20 @@ class NcmDecrypt {
|
||||||
try {
|
try {
|
||||||
this.image = await GetImageFromURL(this.oriMeta.albumPic);
|
this.image = await GetImageFromURL(this.oriMeta.albumPic);
|
||||||
while (this.image && this.image.buffer.byteLength >= 1 << 24) {
|
while (this.image && this.image.buffer.byteLength >= 1 << 24) {
|
||||||
let img = await jimp.read(Buffer.from(this.image.buffer));
|
// let img = await jimp.read(Buffer.from(this.image.buffer));
|
||||||
await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
|
// await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
|
||||||
this.image.buffer = await img.getBufferAsync('image/jpeg');
|
// this.image.buffer = await img.getBufferAsync('image/jpeg');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('get cover image failed', 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() {
|
async _writeMeta() {
|
||||||
|
@ -192,14 +207,21 @@ class NcmDecrypt {
|
||||||
if (!this.blob) this.blob = new Blob([this.audio], { type: this.mime });
|
if (!this.blob) this.blob = new Blob([this.audio], { type: this.mime });
|
||||||
const ori = await metaParseBlob(this.blob);
|
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 (shouldWrite || this.newMeta.picture) {
|
||||||
if (this.format === 'mp3') {
|
if (this.format === 'mp3') {
|
||||||
this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori);
|
this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori);
|
||||||
} else if (this.format === 'flac') {
|
} 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 {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
this.blob = new Blob([this.audio], { type: this.mime });
|
this.blob = new Blob([this.audio], { type: this.mime });
|
||||||
|
|
|
@ -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 { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
|
||||||
|
|
||||||
import { DecryptResult } from '@/decrypt/entity';
|
import { DecryptResult } from '@/decrypt/entity';
|
||||||
|
@ -37,7 +42,11 @@ export const HandlerMap: { [key: string]: Handler } = {
|
||||||
'776176': { ext: 'wav', version: 1 },
|
'776176': { ext: 'wav', version: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
export async function Decrypt(
|
||||||
|
file: Blob,
|
||||||
|
raw_filename: string,
|
||||||
|
raw_ext: string
|
||||||
|
): Promise<DecryptResult> {
|
||||||
if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
|
if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
|
||||||
const handler = HandlerMap[raw_ext];
|
const handler = HandlerMap[raw_ext];
|
||||||
let { version } = handler;
|
let { version } = handler;
|
||||||
|
@ -56,7 +65,10 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||||
musicID = v2Decrypted.songId;
|
musicID = v2Decrypted.songId;
|
||||||
console.log('qmc wasm decoder suceeded');
|
console.log('qmc wasm decoder suceeded');
|
||||||
} else {
|
} 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 }),
|
new Blob([musicDecoded], { type: mime }),
|
||||||
raw_filename,
|
raw_filename,
|
||||||
ext,
|
ext,
|
||||||
musicID,
|
musicID
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
|
|
||||||
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
|
import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
|
||||||
|
|
||||||
// 每次处理 2M 的数据
|
// 每次处理 2M 的数据
|
||||||
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export interface QMCDecryptionResult {
|
export interface QMCDecryptionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
@ -17,8 +17,16 @@ export interface QMCDecryptionResult {
|
||||||
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
||||||
* @param {ArrayBuffer} qmcBlob 读入的文件 Blob
|
* @param {ArrayBuffer} qmcBlob 读入的文件 Blob
|
||||||
*/
|
*/
|
||||||
export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
|
export async function DecryptQmcWasm(
|
||||||
const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
|
qmcBlob: ArrayBuffer,
|
||||||
|
ext: string
|
||||||
|
): Promise<QMCDecryptionResult> {
|
||||||
|
const result: QMCDecryptionResult = {
|
||||||
|
success: false,
|
||||||
|
data: new Uint8Array(),
|
||||||
|
songId: 0,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化模组
|
// 初始化模组
|
||||||
let QmcCrypto: any;
|
let QmcCrypto: any;
|
||||||
|
@ -47,7 +55,7 @@ export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
result.songId = QmcCrypto.getSongId();
|
result.songId = QmcCrypto.getSongId();
|
||||||
result.songId = result.songId == "0" ? 0 : result.songId;
|
result.songId = result.songId == '0' ? 0 : result.songId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedParts = [];
|
const decryptedParts = [];
|
||||||
|
@ -59,7 +67,12 @@ export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise
|
||||||
// 解密一些片段
|
// 解密一些片段
|
||||||
const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
|
const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
|
||||||
QmcCrypto.writeArrayToMemory(blockData, pQmcBuf);
|
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;
|
offset += blockSize;
|
||||||
bytesToDecrypt -= blockSize;
|
bytesToDecrypt -= blockSize;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
})();
|
61
src/main.ts
|
@ -1,56 +1,11 @@
|
||||||
import Vue from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from '@/App.vue';
|
import App from '@/App.vue';
|
||||||
import '@/registerServiceWorker';
|
import '@/registerServiceWorker';
|
||||||
import {
|
import '@element-plus/theme-chalk/dist/index.css';
|
||||||
Button,
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||||
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';
|
|
||||||
|
|
||||||
Vue.use(Link);
|
const app = createApp(App);
|
||||||
Vue.use(Image);
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
Vue.use(Button);
|
app.component(key, component);
|
||||||
Vue.use(Dialog);
|
}
|
||||||
Vue.use(Form);
|
app.mount('#app');
|
||||||
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');
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
declare module '*.vue' {
|
|
||||||
import Vue from 'vue';
|
|
||||||
export default Vue;
|
|
||||||
}
|
|
|
@ -5,7 +5,12 @@
|
||||||
<div id="app-control">
|
<div id="app-control">
|
||||||
<el-row class="mb-3">
|
<el-row class="mb-3">
|
||||||
<span>歌曲命名格式:</span>
|
<span>歌曲命名格式:</span>
|
||||||
<el-radio v-for="k in FilenamePolicies" :key="k.key" v-model="filename_policy" :label="k.key">
|
<el-radio
|
||||||
|
v-for="k in FilenamePolicies"
|
||||||
|
:key="k.key"
|
||||||
|
v-model="filename_policy"
|
||||||
|
:label="k.key"
|
||||||
|
>
|
||||||
{{ k.text }}
|
{{ k.text }}
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
@ -18,44 +23,83 @@
|
||||||
:album="editing_data.album"
|
:album="editing_data.album"
|
||||||
:albumartist="editing_data.albumartist"
|
:albumartist="editing_data.albumartist"
|
||||||
:genre="editing_data.genre"
|
:genre="editing_data.genre"
|
||||||
@cancel="showEditDialog = false" @ok="handleEdit"></edit-dialog>
|
@cancel="showEditDialog = false"
|
||||||
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
|
@ok="handleEdit"
|
||||||
|
></edit-dialog>
|
||||||
|
<config-dialog
|
||||||
|
:show="showConfigDialog"
|
||||||
|
@done="showConfigDialog = false"
|
||||||
|
></config-dialog>
|
||||||
<el-tooltip class="item" effect="dark" placement="top">
|
<el-tooltip class="item" effect="dark" placement="top">
|
||||||
<div slot="content">
|
<template #content>
|
||||||
<span> 部分解密方案需要设定解密参数。 </span>
|
<span> 部分解密方案需要设定解密参数。 </span>
|
||||||
</div>
|
</template>
|
||||||
<el-button icon="el-icon-s-tools" plain @click="showConfigDialog = true">解密设定</el-button>
|
<el-button plain @click="showConfigDialog = true">
|
||||||
|
<el-icon><Tools /></el-icon> 解密设定
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-button icon="el-icon-download" plain @click="handleDownloadAll">下载全部</el-button>
|
<el-button plain @click="handleDownloadAll">
|
||||||
<el-button icon="el-icon-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
|
<el-icon><Download /></el-icon> 下载全部
|
||||||
|
</el-button>
|
||||||
|
<el-button plain type="danger" @click="handleDeleteAll">
|
||||||
|
<el-icon><Delete /></el-icon> 清除全部
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-tooltip class="item" effect="dark" placement="top-start">
|
<el-tooltip class="item" effect="dark" placement="top-start">
|
||||||
<div slot="content">
|
<template #content>
|
||||||
<span v-if="instant_save">工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}</span>
|
<span v-if="instant_save">
|
||||||
|
工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}
|
||||||
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
当您使用此工具进行大量文件解锁的时候,建议开启此选项。<br />
|
当您使用此工具进行大量文件解锁的时候,建议开启此选项。<br />
|
||||||
开启后,解锁结果将不会存留于浏览器中,防止内存不足。
|
开启后,解锁结果将不会存留于浏览器中,防止内存不足。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
<el-checkbox v-model="instant_save" type="success" border class="ml-2">立即保存</el-checkbox>
|
<el-checkbox
|
||||||
|
v-model="instant_save"
|
||||||
|
type="success"
|
||||||
|
border
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
立即保存
|
||||||
|
</el-checkbox>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio :autoplay="playing_auto" :src="playing_url" controls />
|
<audio :autoplay="playing_auto" :src="playing_url" controls />
|
||||||
|
|
||||||
<PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @edit="editFile" @play="changePlaying" />
|
<PreviewTable
|
||||||
|
:policy="filename_policy"
|
||||||
|
:table-data="tableData"
|
||||||
|
@download="saveFile"
|
||||||
|
@edit="editFile"
|
||||||
|
@play="changePlaying"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { ElNotification, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
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 ConfigDialog from '@/component/ConfigDialog';
|
||||||
import EditDialog from '@/component/EditDialog';
|
import EditDialog from '@/component/EditDialog';
|
||||||
|
|
||||||
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
|
import {
|
||||||
import { GetImageFromURL, RewriteMetaToMp3, RewriteMetaToFlac, AudioMimeType, split_regex } from '@/decrypt/utils';
|
DownloadBlobMusic,
|
||||||
|
FilenamePolicy,
|
||||||
|
FilenamePolicies,
|
||||||
|
RemoveBlobMusic,
|
||||||
|
DirectlyWriteFile,
|
||||||
|
} from '@/utils/utils';
|
||||||
|
import {
|
||||||
|
GetImageFromURL,
|
||||||
|
RewriteMetaToMp3,
|
||||||
|
RewriteMetaToFlac,
|
||||||
|
AudioMimeType,
|
||||||
|
split_regex,
|
||||||
|
} from '@/decrypt/utils';
|
||||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -70,7 +114,14 @@ export default {
|
||||||
return {
|
return {
|
||||||
showConfigDialog: false,
|
showConfigDialog: false,
|
||||||
showEditDialog: false,
|
showEditDialog: false,
|
||||||
editing_data: { picture: '', title: '', artist: '', album: '', albumartist: '', genre: '', },
|
editing_data: {
|
||||||
|
picture: '',
|
||||||
|
title: '',
|
||||||
|
artist: '',
|
||||||
|
album: '',
|
||||||
|
albumartist: '',
|
||||||
|
genre: '',
|
||||||
|
},
|
||||||
tableData: [],
|
tableData: [],
|
||||||
playing_url: '',
|
playing_url: '',
|
||||||
playing_auto: false,
|
playing_auto: false,
|
||||||
|
@ -92,7 +143,8 @@ export default {
|
||||||
RemoveBlobMusic(data);
|
RemoveBlobMusic(data);
|
||||||
} else {
|
} else {
|
||||||
this.tableData.push(data);
|
this.tableData.push(data);
|
||||||
this.$notify.success({
|
ElNotification({
|
||||||
|
type: 'success',
|
||||||
title: '解锁成功',
|
title: '解锁成功',
|
||||||
message: '成功解锁 ' + data.title,
|
message: '成功解锁 ' + data.title,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
|
@ -100,12 +152,18 @@ export default {
|
||||||
}
|
}
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
let _rp_data = [data.title, data.artist, data.album];
|
let _rp_data = [data.title, data.artist, data.album];
|
||||||
window._paq.push(['trackEvent', 'Unlock', data.rawExt + ',' + data.mime, JSON.stringify(_rp_data)]);
|
window._paq.push([
|
||||||
|
'trackEvent',
|
||||||
|
'Unlock',
|
||||||
|
data.rawExt + ',' + data.mime,
|
||||||
|
JSON.stringify(_rp_data),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showFail(errInfo, filename) {
|
showFail(errInfo, filename) {
|
||||||
console.error(errInfo, filename);
|
console.error(errInfo, filename);
|
||||||
this.$notify.error({
|
ElNotification({
|
||||||
|
type: 'error',
|
||||||
title: '出现问题',
|
title: '出现问题',
|
||||||
message:
|
message:
|
||||||
errInfo +
|
errInfo +
|
||||||
|
@ -156,7 +214,9 @@ export default {
|
||||||
let writeSuccess = true;
|
let writeSuccess = true;
|
||||||
let notifyMsg = '成功修改 ' + this.editing_data.title;
|
let notifyMsg = '成功修改 ' + this.editing_data.title;
|
||||||
try {
|
try {
|
||||||
const musicMeta = await metaParseBlob(new Blob([this.editing_data.blob], { type: mime }));
|
const musicMeta = await metaParseBlob(
|
||||||
|
new Blob([this.editing_data.blob], { type: mime })
|
||||||
|
);
|
||||||
let imageInfo = undefined;
|
let imageInfo = undefined;
|
||||||
if (this.editing_data.picture !== '') {
|
if (this.editing_data.picture !== '') {
|
||||||
imageInfo = await GetImageFromURL(this.editing_data.picture);
|
imageInfo = await GetImageFromURL(this.editing_data.picture);
|
||||||
|
@ -164,42 +224,56 @@ export default {
|
||||||
console.warn('获取图像失败', this.editing_data.picture);
|
console.warn('获取图像失败', this.editing_data.picture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newMeta = { picture: imageInfo?.buffer,
|
const newMeta = {
|
||||||
|
picture: imageInfo?.buffer,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
artists: data.artist.split(split_regex),
|
artists: data.artist.split(split_regex),
|
||||||
album: data.album,
|
album: data.album,
|
||||||
albumartist: data.albumartist,
|
albumartist: data.albumartist,
|
||||||
genre: data.genre.split(split_regex)
|
genre: data.genre.split(split_regex),
|
||||||
};
|
};
|
||||||
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
|
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
|
||||||
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
|
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
|
||||||
if (this.editing_data.ext === 'mp3') {
|
if (this.editing_data.ext === 'mp3') {
|
||||||
this.editing_data.blob = new Blob([RewriteMetaToMp3(buffer, newMeta, musicMeta)], { type: mime });
|
this.editing_data.blob = new Blob(
|
||||||
|
[RewriteMetaToMp3(buffer, newMeta, musicMeta)],
|
||||||
|
{ type: mime }
|
||||||
|
);
|
||||||
} else if (this.editing_data.ext === 'flac') {
|
} else if (this.editing_data.ext === 'flac') {
|
||||||
this.editing_data.blob = new Blob([RewriteMetaToFlac(buffer, newMeta, musicMeta)], { type: mime });
|
this.editing_data.blob = new Blob(
|
||||||
|
[RewriteMetaToFlac(buffer, newMeta, musicMeta)],
|
||||||
|
{ type: mime }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
writeSuccess = undefined;
|
writeSuccess = undefined;
|
||||||
notifyMsg = this.editing_data.ext + '类型文件暂时不支持修改音乐标签';
|
notifyMsg = this.editing_data.ext + '类型文件暂时不支持修改音乐标签';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
writeSuccess = false;
|
writeSuccess = false;
|
||||||
notifyMsg = '修改' + this.editing_data.title + '未能完成。在写入新的元数据时发生错误:' + e;
|
notifyMsg =
|
||||||
|
'修改' +
|
||||||
|
this.editing_data.title +
|
||||||
|
'未能完成。在写入新的元数据时发生错误:' +
|
||||||
|
e;
|
||||||
}
|
}
|
||||||
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
|
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
|
||||||
if (writeSuccess === true) {
|
if (writeSuccess === true) {
|
||||||
this.$notify.success({
|
ElNotification({
|
||||||
|
type: 'success',
|
||||||
title: '修改成功',
|
title: '修改成功',
|
||||||
message: notifyMsg,
|
message: notifyMsg,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
} else if (writeSuccess === false) {
|
} else if (writeSuccess === false) {
|
||||||
this.$notify.error({
|
ElNotification({
|
||||||
|
type: 'error',
|
||||||
title: '修改失败',
|
title: '修改失败',
|
||||||
message: notifyMsg,
|
message: notifyMsg,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$notify.warning({
|
ElNotification({
|
||||||
|
type: 'warning',
|
||||||
title: '修改取消',
|
title: '修改取消',
|
||||||
message: notifyMsg,
|
message: notifyMsg,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
|
@ -217,7 +291,7 @@ export default {
|
||||||
async saveFile(data) {
|
async saveFile(data) {
|
||||||
if (this.dir) {
|
if (this.dir) {
|
||||||
await DirectlyWriteFile(data, this.filename_policy, this.dir);
|
await DirectlyWriteFile(data, this.filename_policy, this.dir);
|
||||||
this.$notify({
|
ElNotification({
|
||||||
title: '保存成功',
|
title: '保存成功',
|
||||||
message: data.title,
|
message: data.title,
|
||||||
position: 'top-left',
|
position: 'top-left',
|
||||||
|
@ -231,12 +305,16 @@ export default {
|
||||||
async showDirectlySave() {
|
async showDirectlySave() {
|
||||||
if (!window.showDirectoryPicker) return;
|
if (!window.showDirectoryPicker) return;
|
||||||
try {
|
try {
|
||||||
await this.$confirm('您的浏览器支持文件直接保存到磁盘,是否使用?', '新特性提示', {
|
await ElMessageBox.confirm(
|
||||||
confirmButtonText: '使用',
|
'您的浏览器支持文件直接保存到磁盘,是否使用?',
|
||||||
cancelButtonText: '不使用',
|
'新特性提示',
|
||||||
type: 'warning',
|
{
|
||||||
center: true,
|
confirmButtonText: '使用',
|
||||||
});
|
cancelButtonText: '不使用',
|
||||||
|
type: 'warning',
|
||||||
|
center: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import GlobalsPolyfills from '@esbuild-plugins/node-globals-polyfill';
|
||||||
|
import NodeModulesPolyfills from '@esbuild-plugins/node-modules-polyfill';
|
||||||
|
|
||||||
|
import commonjs from 'vite-plugin-commonjs';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
optimizeDeps: {
|
||||||
|
esbuildOptions: {
|
||||||
|
define: {
|
||||||
|
global: 'globalThis',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
GlobalsPolyfills({
|
||||||
|
process: true,
|
||||||
|
buffer: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
|
||||||
|
process: 'rollup-plugin-node-polyfills/polyfills/process-es6',
|
||||||
|
stream: 'rollup-plugin-node-polyfills/polyfills/stream',
|
||||||
|
util: 'rollup-plugin-node-polyfills/polyfills/util',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
commonjs(),
|
||||||
|
NodeModulesPolyfills(),
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|