chore: reformat all files with prettier

This commit is contained in:
Jixun 2022-11-26 23:26:38 +00:00
parent f4a12d53d8
commit 1cfbce81d4
46 changed files with 580 additions and 770 deletions

View File

@ -20,7 +20,7 @@ steps:
- mv *.zip sha256sum.txt upload/ - mv *.zip sha256sum.txt upload/
- name: upload - name: upload
image: "plugins/s3" image: 'plugins/s3'
settings: settings:
path_style: true path_style: true
endpoint: endpoint:
@ -31,7 +31,7 @@ steps:
from_secret: S3_SECRET_KEY from_secret: S3_SECRET_KEY
bucket: bucket:
from_secret: S3_BUCKET from_secret: S3_BUCKET
region: "auto" region: 'auto'
source: "upload/*" source: 'upload/*'
strip_prefix: "upload/" strip_prefix: 'upload/'
target: "${DRONE_REPO}/${DRONE_BUILD_NUMBER}/" target: '${DRONE_REPO}/${DRONE_BUILD_NUMBER}/'

View File

@ -3,10 +3,9 @@ cache:
paths: paths:
- node_modules/ - node_modules/
stages: stages:
- build - build
build-job: build-job:
stage: build stage: build
script: | script: |
@ -37,7 +36,7 @@ build-job:
sha256sum *.tar.gz *.zip > sha256sum.txt sha256sum *.tar.gz *.zip > sha256sum.txt
artifacts: artifacts:
name: "$CI_JOB_NAME" name: '$CI_JOB_NAME'
paths: paths:
- legacy.zip - legacy.zip
- legacy.tar.gz - legacy.tar.gz

View File

@ -1,39 +1,34 @@
--- ---
name: Bug报告 name: Bug报告
about: 报告Bug以帮助改进程序 about: 报告Bug以帮助改进程序
title: '' title: ''
labels: bug labels: bug
assignees: '' assignees: ''
---
---
- 请按照此模板填写,否则可能立即被关闭
* 请按照此模板填写,否则可能立即被关闭
* [x] 我确认已经搜索过 Issue 不存并确认相同的 Issue
- [x] 我确认已经搜索过Issue不存并确认相同的Issue * [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
- [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
**Bug 描述**
**Bug描述** 简要地复述你遇到的 Bug
简要地复述你遇到的Bug **复现方法**
**复现方法** 描述复现方法,必要时请提供样本文件
描述复现方法,必要时请提供样本文件 **程序截图或者 Console 报错信息**
**程序截图或者Console报错信息** 如果可以请提供二者之一
如果可以请提供二者之一 **环境信息:**
- 操作系统和浏览器:
**环境信息:** - 程序版本:
- 获取音乐文件所使用的客户端及其版本信息:
- 操作系统和浏览器:
- 程序版本: **附加信息**
- 获取音乐文件所使用的客户端及其版本信息:
其他能够帮助确认问题的信息
**附加信息**
其他能够帮助确认问题的信息

View File

@ -1,26 +1,22 @@
--- ---
name: 新功能 name: 新功能
about: 对于程序新的想法或建议 about: 对于程序新的想法或建议
title: '' title: ''
labels: enhancement labels: enhancement
assignees: '' assignees: ''
---
---
- 请按照此模板填写,否则可能立即被关闭
- 请按照此模板填写,否则可能立即被关闭
**背景和说明**
**背景和说明**
简要说明产生此想法的背景和此想法的具体内容
简要说明产生此想法的背景和此想法的具体内容
**实现途径**
**实现途径** - 如果没有设计方案,请简要描述实现思路
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者 Telegram 进行讨论
- 如果没有设计方案,请简要描述实现思路
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者Telegram进行讨论 **附加信息**
更多你想要表达的内容
**附加信息**
更多你想要表达的内容

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
build/
dist/
src/QmcWasm/
src/KgmWasm/

169
README.md
View File

@ -1,84 +1,85 @@
# Unlock Music 音乐解锁 # Unlock Music 音乐解锁
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser. - 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。 - Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。 - Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。
- 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入! - 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入!
[授权协议]: https://git.unlock-music.dev/um/web/src/branch/master/LICENSE [授权协议]: https://git.unlock-music.dev/um/web/src/branch/master/LICENSE
[unlock-music/cli]: https://git.unlock-music.dev/um/cli [unlock-music/cli]: https://git.unlock-music.dev/um/cli
[`@unlock_music_chat`]: https://t.me/unlock_music_chat [`@unlock_music_chat`]: https://t.me/unlock_music_chat
## 特性 ## 特性
### 支持的格式 ### 支持的格式
- [x] QQ 音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/.tkm) - [x] QQ 音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/.tkm)
- [x] Moo 音乐格式 (.bkcmp3/.bkcflac/...) - [x] Moo 音乐格式 (.bkcmp3/.bkcflac/...)
- [x] QQ 音乐 Tm 格式 (.tm0/.tm2/.tm3/.tm6) - [x] QQ 音乐 Tm 格式 (.tm0/.tm2/.tm3/.tm6)
- [x] QQ 音乐新格式 (.mflac/.mgg/.mflac0/.mgg1/.mggl) - [x] QQ 音乐新格式 (.mflac/.mgg/.mflac0/.mgg1/.mggl)
- [x] <ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (.ofl_en) - [x] <ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (.ofl_en)
- [x] 网易云音乐格式 (.ncm) - [x] 网易云音乐格式 (.ncm)
- [x] 虾米音乐格式 (.xm) - [x] 虾米音乐格式 (.xm)
- [x] 酷我音乐格式 (.kwm) - [x] 酷我音乐格式 (.kwm)
- [x] 酷狗音乐格式 (.kgm/.vpr) - [x] 酷狗音乐格式 (.kgm/.vpr)
### 其他特性 ### 其他特性
- [x] 在浏览器中解锁 - [x] 在浏览器中解锁
- [x] 拖放文件 - [x] 拖放文件
- [x] 批量解锁 - [x] 批量解锁
- [x] 渐进式 Web 应用 (PWA) - [x] 渐进式 Web 应用 (PWA)
- [x] 多线程 - [x] 多线程
- [x] 写入元信息与专辑封面 - [x] 写入元信息与专辑封面
## 使用方法 ## 使用方法
### 使用预构建版本 ### 使用预构建版本
- 从 [Release] 下载预构建的版本 - 从 [Release] 下载预构建的版本
- :warning: 本地使用请下载`legacy版本``modern版本`只能通过 **http(s)协议** 访问) - :warning: 本地使用请下载`legacy版本``modern版本`只能通过 **http(s)协议** 访问)
- 解压缩后即可部署或本地使用(**请勿直接运行源代码** - 解压缩后即可部署或本地使用(**请勿直接运行源代码**
[release]: https://git.unlock-music.dev/um/web/releases/latest [release]: https://git.unlock-music.dev/um/web/releases/latest
### 自行构建 ### 自行构建
#### JS部分 #### JS 部分
- 环境要求 - 环境要求
- nodejs (v16.x) - nodejs (v16.x)
- npm - npm
1. 获取项目源代码后安装相关依赖: 1. 获取项目源代码后安装相关依赖:
```sh ```sh
npm ci npm ci
``` ```
2. 然后进行构建: 2. 然后进行构建:
```sh ```sh
npm run build npm run build
``` ```
- 构建后的产物可以在 `dist` 目录找到。 - 构建后的产物可以在 `dist` 目录找到。
- 如果是用于开发,可以执行 `npm run serve` - 如果是用于开发,可以执行 `npm run serve`
3. 如需构建浏览器扩展,构建成功后还需要执行: 3. 如需构建浏览器扩展,构建成功后还需要执行:
```sh ```sh
npm run make-extension npm run make-extension
``` ```
#### WASM部分 #### WASM 部分
- 环境要求 - 环境要求
- Linux
- python3 - Linux
- python3
- 运行此目录下的build-wasm
- 运行此目录下的 build-wasm
```sh
./scripts/build-wasm.sh ```sh
``` ./scripts/build-wasm.sh
```

6
auto-imports.d.ts vendored
View File

@ -1,5 +1,3 @@
// Generated by 'unplugin-auto-import' // Generated by 'unplugin-auto-import'
export {} export {};
declare global { declare global {}
}

View File

@ -1,12 +1,12 @@
module.exports = { module.exports = {
presets: [ presets: ['@vue/app', '@babel/preset-typescript'],
'@vue/app', plugins: [
'@babel/preset-typescript' [
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk',
},
], ],
plugins: [ ],
["component", {
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}]
]
}; };

42
components.d.ts vendored
View File

@ -1,30 +1,30 @@
// generated by unplugin-vue-components // generated by unplugin-vue-components
// We suggest you to commit this file into source control // We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core' import '@vue/runtime-core';
export {} export {};
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton'];
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'];
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer'];
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog'];
ElFooter: typeof import('element-plus/es')['ElFooter'] ElFooter: typeof import('element-plus/es')['ElFooter'];
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm'];
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem'];
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon'];
ElImage: typeof import('element-plus/es')['ElImage'] ElImage: typeof import('element-plus/es')['ElImage'];
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput'];
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain'];
ElProgress: typeof import('element-plus/es')['ElProgress'] ElProgress: typeof import('element-plus/es')['ElProgress'];
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio'];
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow'];
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable'];
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'];
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip'];
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload'];
IEpUpload: typeof import('~icons/ep/upload')['default'] IEpUpload: typeof import('~icons/ep/upload')['default'];
} }
} }

View File

@ -6,14 +6,8 @@
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" /> <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
<meta content="width=device-width,initial-scale=1.0" name="viewport" /> <meta content="width=device-width,initial-scale=1.0" name="viewport" />
<title>音乐解锁</title> <title>音乐解锁</title>
<meta <meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords" />
content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" <meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description" />
name="keywords"
/>
<meta
content="音乐解锁 - 在任何设备上解锁已购的加密音乐!"
name="description"
/>
<script src="./src/ixarea-stats.js"></script> <script src="./src/ixarea-stats.js"></script>
</head> </head>
<body> <body>
@ -29,10 +23,7 @@
</noscript> </noscript>
<h3 id="loader-source">请勿直接运行源代码!</h3> <h3 id="loader-source">请勿直接运行源代码!</h3>
<div id="loader-tips-outdated" hidden> <div id="loader-tips-outdated" hidden>
<h2> <h2>您可能在使用不受支持的<span style="color: #f00">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
您可能在使用不受支持的<span style="color: #f00">过时</span
>浏览器,这可能导致此应用无法正常工作。
</h2>
<h3> <h3>
如果您使用双核浏览器,您可以尝试切换到 如果您使用双核浏览器,您可以尝试切换到
<span style="color: #f00">“极速模式”</span> 解决此问题。 <span style="color: #f00">“极速模式”</span> 解决此问题。
@ -41,21 +32,11 @@
</div> </div>
<h3 id="loader-tips-timeout" hidden> <h3 id="loader-tips-timeout" hidden>
音乐解锁采用了一些新特性!建议使用 音乐解锁采用了一些新特性!建议使用
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank" <a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
>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://www.google.cn/chrome/" target="_blank"
>Google Chrome</a
>
<a href="https://www.firefox.com.cn/" target="_blank"
>Mozilla Firefox</a
>
| |
<a <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
href="https://github.com/ix64/unlock-music/wiki/使用提示"
target="_blank"
>使用提示</a
>
</h3> </h3>
</div> </div>
<div id="app"></div> <div id="app"></div>

View File

@ -1,7 +1,7 @@
module.exports = { module.exports = {
testPathIgnorePatterns: ['/build/', '/dist/', '/node_modules/'], testPathIgnorePatterns: ['/build/', '/dist/', '/node_modules/'],
setupFilesAfterEnv: ['./src/__test__/setup_jest.js'], setupFilesAfterEnv: ['./src/__test__/setup_jest.js'],
moduleNameMapper: { moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1', '@/(.*)': '<rootDir>/src/$1',
}, },
}; };

View File

@ -1,25 +1,25 @@
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
const src = __dirname + "/src/extension/" const src = __dirname + '/src/extension/';
const dst = __dirname + "/dist" const dst = __dirname + '/dist';
fs.readdirSync(src).forEach(file => { fs.readdirSync(src).forEach((file) => {
let srcPath = path.join(src, file) let srcPath = path.join(src, file);
let dstPath = path.join(dst, file) let dstPath = path.join(dst, file);
fs.copyFileSync(srcPath, dstPath) fs.copyFileSync(srcPath, dstPath);
console.log(`Copy: ${srcPath} => ${dstPath}`) console.log(`Copy: ${srcPath} => ${dstPath}`);
}) });
const manifestRaw = fs.readFileSync(__dirname + "/extension-manifest.json", "utf-8") const manifestRaw = fs.readFileSync(__dirname + '/extension-manifest.json', 'utf-8');
const manifest = JSON.parse(manifestRaw) const manifest = JSON.parse(manifestRaw);
const pkgRaw = fs.readFileSync(__dirname + "/package.json", "utf-8") const pkgRaw = fs.readFileSync(__dirname + '/package.json', 'utf-8');
const pkg = JSON.parse(pkgRaw) const pkg = JSON.parse(pkgRaw);
verExt = pkg["version"] verExt = pkg['version'];
if (verExt.startsWith("v")) verExt = verExt.slice(1) if (verExt.startsWith('v')) verExt = verExt.slice(1);
if (verExt.includes("-")) verExt = verExt.split("-")[0] if (verExt.includes('-')) verExt = verExt.split('-')[0];
manifest["version"] = `${verExt}.${pkg["ext_build"]}` manifest['version'] = `${verExt}.${pkg['ext_build']}`;
manifest["version_name"] = pkg["version"] manifest['version_name'] = pkg['version'];
fs.writeFileSync(__dirname + "/dist/manifest.json", JSON.stringify(manifest), "utf-8") fs.writeFileSync(__dirname + '/dist/manifest.json', JSON.stringify(manifest), 'utf-8');
console.log("Write: manifest.json") console.log('Write: manifest.json');

View File

@ -10,10 +10,6 @@
"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": "vite", "serve": "vite",

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
plugins: { plugins: {
autoprefixer: {} autoprefixer: {},
} },
} };

View File

@ -5,35 +5,19 @@
</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 href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }})
>音乐解锁</a 移除已购音乐的加密保护
>({{ version }}) 移除已购音乐的加密保护 <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
<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), 目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
虾米音乐(xm), 酷我音乐(.kwm) <a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>
<a
href="https://github.com/ix64/unlock-music/blob/master/README.md"
target="_blank"
>更多</a
>
</el-row> </el-row>
<el-row> <el-row>
<!--如果进行二次开发此行版权信息不得移除且应明显地标注于页面上--> <!--如果进行二次开发此行版权信息不得移除且应明显地标注于页面上-->
<span <span>Copyright &copy; 2019 - {{ new Date().getFullYear() }} MengYX</span>
>Copyright &copy; 2019 - {{ new Date().getFullYear() }} MengYX</span
>
音乐解锁使用 音乐解锁使用
<a <a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
href="https://github.com/ix64/unlock-music/blob/master/LICENSE"
target="_blank"
>MIT许可协议</a
>
开放源代码 开放源代码
</el-row> </el-row>
</el-footer> </el-footer>
@ -78,8 +62,7 @@ export default defineComponent({
if ( if (
updateInfo && updateInfo &&
process.env.NODE_ENV === 'production' && process.env.NODE_ENV === 'production' &&
(updateInfo.HttpsFound || (updateInfo.HttpsFound || (updateInfo.Found && window.location.protocol !== 'https:'))
(updateInfo.Found && window.location.protocol !== 'https:'))
) { ) {
this.$notify.warning({ this.$notify.warning({
title: '发现更新', title: '发现更新',

View File

@ -1,2 +1,2 @@
// Polyfill for node. // Polyfill for node.
global.Blob = global.Blob || require("node:buffer").Blob; global.Blob = global.Blob || require('node:buffer').Blob;

View File

@ -26,20 +26,8 @@ form >>> input {
</style> </style>
<template> <template>
<el-dialog <el-dialog @close="cancel()" title="解密设定" v-model="internalShow" class="um-config-dialog" center>
@close="cancel()" <el-form ref="form" :rules="rules" status-icon :model="form" label-width="0">
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>
@ -47,14 +35,7 @@ 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 <el-input type="text" v-model="form.jooxUUID" clearable maxlength="32" show-word-limit> </el-input>
type="text"
v-model="form.jooxUUID"
clearable
maxlength="32"
show-word-limit
>
</el-input>
</el-form-item> </el-form-item>
</label> </label>
@ -62,9 +43,7 @@ form >>> input {
下载该加密文件的 JOOX 应用所记录的设备唯一识别码 下载该加密文件的 JOOX 应用所记录的设备唯一识别码
<br /> <br />
参见 参见
<a <a href="https://github.com/unlock-music/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID">
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>
@ -72,9 +51,7 @@ form >>> input {
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" :loading="saving" @click="emitConfirm()"> <el-button type="primary" :loading="saving" @click="emitConfirm()"> </el-button>
</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>

View File

@ -26,23 +26,11 @@ form >>> input {
</style> </style>
<template> <template>
<el-dialog <el-dialog @close="cancel()" title="音乐标签编辑" v-model="internalShow" class="um-edit-dialog" center>
@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 <el-image v-show="!editPicture" :src="imgFile.url || picture" style="width: 100px; height: 100px">
v-show="!editPicture" <template #error class="image-slot el-image__error"> 暂无封面 </template>
:src="imgFile.url || picture"
style="width: 100px; height: 100px"
>
<template #error class="image-slot el-image__error">
暂无封面
</template>
</el-image> </el-image>
<el-upload <el-upload
v-show="editPicture" v-show="editPicture"
@ -56,12 +44,8 @@ form >>> input {
drag drag
> >
<el-icon><UploadFilled /></el-icon> <el-icon><UploadFilled /></el-icon>
<div class="el-upload__text"> <div class="el-upload__text">将新图片拖到此处<em>点击选择</em><br />以替换自动匹配的图片</div>
将新图片拖到此处<em>点击选择</em><br />以替换自动匹配的图片 <template #tip class="el-upload__tip"> 新拖到此处的图片将覆盖原始图片 </template>
</div>
<template #tip class="el-upload__tip">
新拖到此处的图片将覆盖原始图片
</template>
</el-upload> </el-upload>
<i <i
@ -75,26 +59,17 @@ form >>> input {
标题: 标题:
<span v-show="!editTitle">{{ title }}</span> <span v-show="!editTitle">{{ title }}</span>
<!-- <el-input v-show="editTitle" v-model="title"></el-input> --> <!-- <el-input v-show="editTitle" v-model="title"></el-input> -->
<i <i :class="{ 'el-icon-edit': !editTitle, 'el-icon-check': editTitle }" @click="editTitle = !editTitle"></i
:class="{ 'el-icon-edit': !editTitle, 'el-icon-check': editTitle }"
@click="editTitle = !editTitle"
></i
><br /> ><br />
艺术家: 艺术家:
<span v-show="!editArtist">{{ artist }}</span> <span v-show="!editArtist">{{ artist }}</span>
<!-- <el-input v-show="editArtist" v-model="artist"></el-input> --> <!-- <el-input v-show="editArtist" v-model="artist"></el-input> -->
<i <i :class="{ 'el-icon-edit': !editArtist, 'el-icon-check': editArtist }" @click="editArtist = !editArtist"></i
:class="{ 'el-icon-edit': !editArtist, 'el-icon-check': editArtist }"
@click="editArtist = !editArtist"
></i
><br /> ><br />
专辑: 专辑:
<span v-show="!editAlbum">{{ album }}</span> <span v-show="!editAlbum">{{ album }}</span>
<!-- <el-input v-show="editAlbum" v-model="album"></el-input> --> <!-- <el-input v-show="editAlbum" v-model="album"></el-input> -->
<i <i :class="{ 'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum }" @click="editAlbum = !editAlbum"></i
:class="{ 'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum }"
@click="editAlbum = !editAlbum"
></i
><br /> ><br />
专辑艺术家: 专辑艺术家:
<span v-show="!editAlbumartist">{{ albumartist }}</span> <span v-show="!editAlbumartist">{{ albumartist }}</span>
@ -110,10 +85,7 @@ form >>> input {
风格: 风格:
<span v-show="!editGenre">{{ genre }}</span> <span v-show="!editGenre">{{ genre }}</span>
<!-- <el-input v-show="editGenre" v-model="genre"></el-input> --> <!-- <el-input v-show="editGenre" v-model="genre"></el-input> -->
<i <i :class="{ 'el-icon-edit': !editGenre, 'el-icon-check': editGenre }" @click="editGenre = !editGenre"></i
:class="{ 'el-icon-edit': !editGenre, 'el-icon-check': editGenre }"
@click="editGenre = !editGenre"
></i
><br /> ><br />
<p class="item-desc"> <p class="item-desc">

View File

@ -1,21 +1,12 @@
<template> <template>
<el-upload <el-upload :auto-upload="false" :on-change="addFile" :show-file-list="false" action="" drag multiple>
:auto-upload="false"
:on-change="addFile"
:show-file-list="false"
action=""
drag
multiple
>
<el-icon size="80"><UploadFilled /></el-icon> <el-icon size="80"><UploadFilled /></el-icon>
<div>将文件拖到此处 <em>点击选择</em></div> <div>将文件拖到此处 <em>点击选择</em></div>
<template #tip> <template #tip>
<div> <div>
仅在浏览器内对文件进行解锁无需消耗流量 仅在浏览器内对文件进行解锁无需消耗流量
<el-tooltip effect="dark" placement="top-start"> <el-tooltip effect="dark" placement="top-start">
<template #content> <template #content> 算法在源代码中已经提供所有运算都发生在本地 </template>
算法在源代码中已经提供所有运算都发生在本地
</template>
<el-icon size="12"> <el-icon size="12">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -73,16 +64,9 @@ export default {
}, },
}, },
mounted() { mounted() {
if ( if (window.Worker && window.location.protocol !== 'file:' && process.env.NODE_ENV === 'production') {
window.Worker &&
window.location.protocol !== 'file:' &&
process.env.NODE_ENV === 'production'
) {
console.log('Using Worker Pool'); console.log('Using Worker Pool');
this.queue = Pool( this.queue = Pool(() => spawn(new Worker('@/utils/worker.ts')), navigator.hardwareConcurrency || 1);
() => 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');

View File

@ -3,9 +3,7 @@
<el-table-column label="封面"> <el-table-column label="封面">
<template #default="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">
<template #error class="image-slot el-image__error"> <template #error class="image-slot el-image__error"> 暂无封面 </template>
暂无封面
</template>
</el-image> </el-image>
</template> </template>
</el-table-column> </el-table-column>
@ -26,11 +24,7 @@
</el-table-column> </el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #default="scope"> <template #default="scope">
<el-button <el-button circle type="success" @click="handlePlay(scope.$index, scope.row)">
circle
type="success"
@click="handlePlay(scope.$index, scope.row)"
>
<el-icon size="20" style="vertical-align: middle"> <el-icon size="20" style="vertical-align: middle">
<VideoPlay /> <VideoPlay />
</el-icon> </el-icon>
@ -43,11 +37,7 @@
<el-button circle @click="handleEdit(scope.row)"> <el-button circle @click="handleEdit(scope.row)">
<el-icon size="20" style="vertical-align: middle"><Edit /></el-icon> <el-icon size="20" style="vertical-align: middle"><Edit /></el-icon>
</el-button> </el-button>
<el-button <el-button circle type="danger" @click="handleDelete(scope.$index, scope.row)">
circle
type="danger"
@click="handleDelete(scope.$index, scope.row)"
>
<el-icon size="20" style="vertical-align: middle"> <el-icon size="20" style="vertical-align: middle">
<Delete /> <Delete />
</el-icon> </el-icon>

View File

@ -63,7 +63,11 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
const mime = AudioMimeType[ext]; const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], { type: mime }); let musicBlob = new Blob([musicDecoded], { type: mime });
const musicMeta = await metaParseBlob(musicBlob); const musicMeta = await metaParseBlob(musicBlob);
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || "")); const { title, artist } = GetMetaFromFile(
raw_filename,
musicMeta.common.title,
String(musicMeta.common.artists || musicMeta.common.artist || ''),
);
return { return {
album: musicMeta.common.album, album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta), picture: GetCoverFromFile(musicMeta),

View File

@ -16,10 +16,7 @@ export interface KGMDecryptionResult {
* Uint8Array * Uint8Array
* @param {ArrayBuffer} kgmBlob Blob * @param {ArrayBuffer} kgmBlob Blob
*/ */
export async function DecryptKgmWasm( export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
kgmBlob: ArrayBuffer,
ext: string
): Promise<KGMDecryptionResult> {
const result: KGMDecryptionResult = { const result: KGMDecryptionResult = {
success: false, success: false,
data: new Uint8Array(), data: new Uint8Array(),

View File

@ -38,7 +38,11 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
let musicBlob = new Blob([audioData], { type: mime }); let musicBlob = new Blob([audioData], { type: mime });
const musicMeta = await metaParseBlob(musicBlob); const musicMeta = await metaParseBlob(musicBlob);
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || "")); const { title, artist } = GetMetaFromFile(
raw_filename,
musicMeta.common.title,
String(musicMeta.common.artists || musicMeta.common.artist || ''),
);
return { return {
album: musicMeta.common.album, album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta), picture: GetCoverFromFile(musicMeta),

View File

@ -5,24 +5,24 @@ import { DecryptResult } from '@/decrypt/entity';
const segmentSize = 0x20; const segmentSize = 0x20;
function isPrintableAsciiChar(ch: number) { function isPrintableAsciiChar(ch: number) {
return ch >= 0x20 && ch <= 0x7E; return ch >= 0x20 && ch <= 0x7e;
} }
function isUpperHexChar(ch: number) { function isUpperHexChar(ch: number) {
return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46); return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46);
} }
/** /**
* @param {Buffer} data * @param {Buffer} data
* @param {Buffer} key * @param {Buffer} key
* @param {boolean} copy * @param {boolean} copy
* @returns Buffer * @returns Buffer
*/ */
function decryptSegment(data: Uint8Array, key: Uint8Array) { function decryptSegment(data: Uint8Array, key: Uint8Array) {
for (let i = 0; i < data.byteLength; i++) { for (let i = 0; i < data.byteLength; i++) {
data[i] -= key[i % segmentSize]; data[i] -= key[i % segmentSize];
} }
return Buffer.from(data); return Buffer.from(data);
} }
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> { export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
@ -33,37 +33,37 @@ export async function Decrypt(file: File, raw_filename: string): Promise<Decrypt
const bytesRIFF = Buffer.from('RIFF', 'ascii'); const bytesRIFF = Buffer.from('RIFF', 'ascii');
const bytesWaveFormat = Buffer.from('WAVEfmt ', 'ascii'); const bytesWaveFormat = Buffer.from('WAVEfmt ', 'ascii');
const possibleKeys = []; const possibleKeys = [];
for (let i = segmentSize; i < segmentSize * 20; i += segmentSize) { for (let i = segmentSize; i < segmentSize * 20; i += segmentSize) {
const possibleKey = buf.slice(i, i + segmentSize); const possibleKey = buf.slice(i, i + segmentSize);
if (!possibleKey.every(isUpperHexChar)) continue; if (!possibleKey.every(isUpperHexChar)) continue;
const tempHeader = decryptSegment(header, possibleKey); const tempHeader = decryptSegment(header, possibleKey);
if (tempHeader.slice(0, 4).compare(bytesRIFF)) continue; if (tempHeader.slice(0, 4).compare(bytesRIFF)) continue;
if (tempHeader.slice(8, 16).compare(bytesWaveFormat)) continue; if (tempHeader.slice(8, 16).compare(bytesWaveFormat)) continue;
// fmt chunk 大小可以是 16 / 18 / 40。 // fmt chunk 大小可以是 16 / 18 / 40。
const fmtChunkSize = tempHeader.readUInt32LE(0x10); const fmtChunkSize = tempHeader.readUInt32LE(0x10);
if (![16, 18, 40].includes(fmtChunkSize)) continue; if (![16, 18, 40].includes(fmtChunkSize)) continue;
// 下一个 chunk // 下一个 chunk
const firstDataChunkOffset = 0x14 + fmtChunkSize; const firstDataChunkOffset = 0x14 + fmtChunkSize;
const chunkName = tempHeader.slice(firstDataChunkOffset, firstDataChunkOffset + 4); const chunkName = tempHeader.slice(firstDataChunkOffset, firstDataChunkOffset + 4);
if (!chunkName.every(isPrintableAsciiChar)) continue; if (!chunkName.every(isPrintableAsciiChar)) continue;
const secondDataChunkOffset = firstDataChunkOffset + 8 + tempHeader.readUInt32LE(firstDataChunkOffset + 4); const secondDataChunkOffset = firstDataChunkOffset + 8 + tempHeader.readUInt32LE(firstDataChunkOffset + 4);
if (secondDataChunkOffset <= header.byteLength) { if (secondDataChunkOffset <= header.byteLength) {
const secondChunkName = tempHeader.slice(secondDataChunkOffset, secondDataChunkOffset + 4); const secondChunkName = tempHeader.slice(secondDataChunkOffset, secondDataChunkOffset + 4);
if (!secondChunkName.every(isPrintableAsciiChar)) continue; if (!secondChunkName.every(isPrintableAsciiChar)) continue;
} }
possibleKeys.push(Buffer.from(possibleKey).toString('ascii')); possibleKeys.push(Buffer.from(possibleKey).toString('ascii'));
} }
if (possibleKeys.length <= 0) { if (possibleKeys.length <= 0) {
throw new Error(`ERROR: no suitable key discovered`); throw new Error(`ERROR: no suitable key discovered`);
} }
const decryptionKey = Buffer.from(possibleKeys[0], 'ascii'); const decryptionKey = Buffer.from(possibleKeys[0], 'ascii');
decryptSegment(buf, decryptionKey); decryptSegment(buf, decryptionKey);
const musicData = new Blob([buf], { type: 'audio/x-wav' }); const musicData = new Blob([buf], { type: 'audio/x-wav' });

View File

@ -26,11 +26,7 @@ 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( export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
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();
} }
@ -72,16 +68,14 @@ 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( const cipherText = new Uint8Array(this.raw, this.offset, keyLen).map((uint8) => uint8 ^ 0x64);
(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);
@ -121,9 +115,7 @@ 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( const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen).map((data) => data ^ 0x63);
(data) => data ^ 0x63
);
this.offset += metaDataLen; this.offset += metaDataLen;
WordArray.create(); WordArray.create();
@ -132,11 +124,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(':');
@ -148,8 +140,7 @@ 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 = result.albumPic.replace('http://', 'https://') + '?param=500y500';
result.albumPic.replace('http://', 'https://') + '?param=500y500';
} }
return result; return result;
} }
@ -158,8 +149,7 @@ 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) for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff];
audioData[cur] ^= keyBox[cur & 0xff];
return audioData; return audioData;
} }
@ -207,21 +197,14 @@ 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 = let shouldWrite = !ori.common.album && !ori.common.artists && !ori.common.title;
!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( this.audio = WriteMetaToFlac(Buffer.from(this.audio), this.newMeta, ori);
Buffer.from(this.audio),
this.newMeta,
ori
);
} else { } else {
console.info( console.info(`writing meta for ${this.format} is not being supported for now`);
`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 });

View File

@ -13,7 +13,11 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
const ext = SniffAudioExt(buffer, raw_ext); const ext = SniffAudioExt(buffer, raw_ext);
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] }); if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
const tag = await metaParseBlob(file); const tag = await metaParseBlob(file);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || "")); const { title, artist } = GetMetaFromFile(
raw_filename,
tag.common.title,
String(tag.common.artists || tag.common.artist || ''),
);
return { return {
title, title,

View File

@ -1,9 +1,4 @@
import { import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher';
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';
@ -42,11 +37,7 @@ export const HandlerMap: { [key: string]: Handler } = {
'776176': { ext: 'wav', version: 1 }, '776176': { ext: 'wav', version: 1 },
}; };
export async function Decrypt( export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
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;
@ -65,10 +56,7 @@ export async function Decrypt(
musicID = v2Decrypted.songId; musicID = v2Decrypted.songId;
console.log('qmc wasm decoder suceeded'); console.log('qmc wasm decoder suceeded');
} else { } else {
console.warn( console.warn('QmcWasm failed with error %s', v2Decrypted.error || '(unknown error)');
'QmcWasm failed with error %s',
v2Decrypted.error || '(unknown error)'
);
} }
} }
@ -87,7 +75,7 @@ export async function Decrypt(
new Blob([musicDecoded], { type: mime }), new Blob([musicDecoded], { type: mime }),
raw_filename, raw_filename,
ext, ext,
musicID musicID,
); );
return { return {

View File

@ -34,11 +34,14 @@ export function simpleMakeKey(salt: number, length: number): number[] {
return keyBuf; return keyBuf;
} }
const mixKey1: Uint8Array = new Uint8Array([ 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 ]) const mixKey1: Uint8Array = new Uint8Array([
const mixKey2: Uint8Array = new Uint8Array([ 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 ]) 0x33, 0x38, 0x36, 0x5a, 0x4a, 0x59, 0x21, 0x40, 0x23, 0x2a, 0x24, 0x25, 0x5e, 0x26, 0x29, 0x28,
]);
const mixKey2: Uint8Array = new Uint8Array([
0x2a, 0x2a, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5e, 0x61, 0x31, 0x63, 0x5a, 0x2c, 0x54,
]);
function decryptV2Key(key: Buffer): Buffer function decryptV2Key(key: Buffer): Buffer {
{
const textEnc = new TextDecoder(); const textEnc = new TextDecoder();
if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') { if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') {
return key; return key;

View File

@ -17,10 +17,7 @@ export interface QMCDecryptionResult {
* Uint8Array * Uint8Array
* @param {ArrayBuffer} qmcBlob Blob * @param {ArrayBuffer} qmcBlob Blob
*/ */
export async function DecryptQmcWasm( export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
qmcBlob: ArrayBuffer,
ext: string
): Promise<QMCDecryptionResult> {
const result: QMCDecryptionResult = { const result: QMCDecryptionResult = {
success: false, success: false,
data: new Uint8Array(), data: new Uint8Array(),
@ -67,12 +64,7 @@ export async function DecryptQmcWasm(
// 解密一些片段 // 解密一些片段
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( decryptedParts.push(QmcCrypto.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCrypto.decBlob(pQmcBuf, blockSize, offset)));
QmcCrypto.HEAPU8.slice(
pQmcBuf,
pQmcBuf + QmcCrypto.decBlob(pQmcBuf, blockSize, offset)
)
);
offset += blockSize; offset += blockSize;
bytesToDecrypt -= blockSize; bytesToDecrypt -= blockSize;

View File

@ -52,7 +52,11 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
throw '不支持的QQ音乐缓存格式'; throw '不支持的QQ音乐缓存格式';
} }
const tag = await metaParseBlob(audioBlob); const tag = await metaParseBlob(audioBlob);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || "")); const { title, artist } = GetMetaFromFile(
raw_filename,
tag.common.title,
String(tag.common.artists || tag.common.artist || ''),
);
return { return {
title, title,

View File

@ -17,7 +17,11 @@ export async function Decrypt(
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] }); if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
} }
const tag = await metaParseBlob(file); const tag = await metaParseBlob(file);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || '')); const { title, artist } = GetMetaFromFile(
raw_filename,
tag.common.title,
String(tag.common.artists || tag.common.artist || ''),
);
return { return {
title, title,

View File

@ -94,7 +94,8 @@ export function GetMetaFromFile(
const items = filename.split(separator); const items = filename.split(separator);
if (items.length > 1) { if (items.length > 1) {
//由文件名和原metadata共同决定歌手tag(有时从文件名看有多个歌手而metadata只有一个) //由文件名和原metadata共同决定歌手tag(有时从文件名看有多个歌手而metadata只有一个)
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length) meta.artist = items[0].trim(); if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length)
meta.artist = items[0].trim();
if (!meta.title) meta.title = items[1].trim(); if (!meta.title) meta.title = items[1].trim();
} else if (items.length === 1) { } else if (items.length === 1) {
if (!meta.title) meta.title = items[0].trim(); if (!meta.title) meta.title = items[0].trim();
@ -182,11 +183,12 @@ export function RewriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original:
// preserve original data // preserve original data
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || []; const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
frames.forEach((frame) => { frames.forEach((frame) => {
if (frame.id !== 'TPE1' if (
&& frame.id !== 'TIT2' frame.id !== 'TPE1' &&
&& frame.id !== 'TALB' frame.id !== 'TIT2' &&
&& frame.id !== 'TPE2' frame.id !== 'TALB' &&
&& frame.id !== 'TCON' frame.id !== 'TPE2' &&
frame.id !== 'TCON'
) { ) {
try { try {
writer.setFrame(frame.id, frame.value); writer.setFrame(frame.id, frame.value);

View File

@ -49,7 +49,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
const { title, artist } = GetMetaFromFile( const { title, artist } = GetMetaFromFile(
raw_filename, raw_filename,
musicMeta.common.title, musicMeta.common.title,
String(musicMeta.common.artists || musicMeta.common.artist || ""), String(musicMeta.common.artists || musicMeta.common.artist || ''),
raw_filename.indexOf('_') === -1 ? '-' : '_', raw_filename.indexOf('_') === -1 ? '-' : '_',
); );

View File

@ -1,13 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
</head> </head>
<body> <body>
<script src="./popup.js"></script> <script src="./popup.js"></script>
<a href="./index.html" target="_blank"> <a href="./index.html" target="_blank">
<button>立即使用</button> <button>立即使用</button>
</a> </a>
</body> </body>
</html> </html>

View File

@ -1,6 +1,11 @@
var _paq = window._paq || []; var _paq = window._paq || [];
_paq.push(["setRequestMethod", "POST"], ["trackPageView"], ["enableLinkTracking"], _paq.push(
["setSiteId", "2"], ["setTrackerUrl", "https://stats.ixarea.com/ixarea-stats/report"]); ['setRequestMethod', 'POST'],
['trackPageView'],
['enableLinkTracking'],
['setSiteId', '2'],
['setTrackerUrl', 'https://stats.ixarea.com/ixarea-stats/report'],
);
var tag = document.createElement('script'); var tag = document.createElement('script');
tag.type = 'text/javascript'; tag.type = 'text/javascript';

View File

@ -3,85 +3,83 @@
*/ */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
#app{ #app {
color: $dark-text-info; color: $dark-text-info;
} }
body{ body {
background-color: $dark-bg; background-color: $dark-bg;
} }
// FORM // FORM
.el-radio{ .el-radio {
&__label{ &__label {
color: $dark-text-main; color: $dark-text-main;
} }
&__input{ &__input {
color: $dark-text-info; color: $dark-text-info;
.el-radio__inner{ .el-radio__inner {
border-color: $dark-border; border-color: $dark-border;
background-color: $dark-btn-bg; background-color: $dark-btn-bg;
} }
} }
&.is-checked{ &.is-checked {
.el-radio__inner{ .el-radio__inner {
background-color: $blue; background-color: $blue;
} }
.el-radio__label{ .el-radio__label {
font-weight: bold; font-weight: bold;
} }
} }
} }
.el-checkbox.is-bordered{ .el-checkbox.is-bordered {
border-color: $dark-border; border-color: $dark-border;
color: $dark-text-main; color: $dark-text-main;
background-color: $dark-btn-bg; background-color: $dark-btn-bg;
.el-checkbox__inner{ .el-checkbox__inner {
background-color: $dark-btn-bg-highlight; background-color: $dark-btn-bg-highlight;
border-color: $dark-border-highlight; border-color: $dark-border-highlight;
} }
&:hover{ &:hover {
border-color: $dark-border-highlight; border-color: $dark-border-highlight;
.el-checkbox__inner{ .el-checkbox__inner {
background-color: $dark-btn-bg-highlight; background-color: $dark-btn-bg-highlight;
border-color: $dark-border-highlight; border-color: $dark-border-highlight;
} }
.el-checkbox__label{ .el-checkbox__label {
color: $dark-text-info; color: $dark-text-info;
} }
} }
&.is-checked{ &.is-checked {
background-color: $blue; background-color: $blue;
.el-checkbox__inner{ .el-checkbox__inner {
border-color: white; border-color: white;
} }
.el-checkbox__label{ .el-checkbox__label {
color: white; color: white;
font-weight: bold; font-weight: bold;
} }
&:hover{ &:hover {
border-color: $blue; border-color: $blue;
.el-checkbox__inner{ .el-checkbox__inner {
background-color: white; background-color: white;
} }
} }
} }
} }
// BUTTON // BUTTON
.el-button{ .el-button {
background-color: $dark-btn-bg; background-color: $dark-btn-bg;
border-color: $dark-border; border-color: $dark-border;
color: $dark-text-main; color: $dark-text-main;
&:active{ &:active {
transform: translateY(2px); transform: translateY(2px);
} }
&--default{ &--default {
&.is-plain { &.is-plain {
background-color: $dark-btn-bg; background-color: $dark-btn-bg;
&:hover { &:hover {
@ -101,7 +99,7 @@
} }
} }
&--success{ &--success {
&.is-plain { &.is-plain {
background-color: $dark-btn-bg; background-color: $dark-btn-bg;
&:hover { &:hover {
@ -120,11 +118,11 @@
} }
} }
} }
&--danger{ &--danger {
&.is-plain{ &.is-plain {
border-color: $dark-border; border-color: $dark-border;
background-color: $dark-btn-bg; background-color: $dark-btn-bg;
&:hover{ &:hover {
background-color: $red; background-color: $red;
border-color: $red; border-color: $red;
} }
@ -142,44 +140,45 @@
} }
// 文件拖放区 // 文件拖放区
.el-upload__tip{ .el-upload__tip {
color: $dark-text-info; color: $dark-text-info;
} }
.el-upload-dragger{ .el-upload-dragger {
background-color: $dark-uploader-bg; background-color: $dark-uploader-bg;
border-color: $dark-border; border-color: $dark-border;
.el-upload__text{ .el-upload__text {
color: $dark-text-info; color: $dark-text-info;
} }
&:hover{ &:hover {
background: $dark-uploader-bg-highlight; background: $dark-uploader-bg-highlight;
border-color: $dark-border-highlight; border-color: $dark-border-highlight;
} }
} }
// TABLE // TABLE
.el-table{ .el-table {
background-color: $dark-bg-td; background-color: $dark-bg-td;
&:before{ // 去除表格末尾的横线 &:before {
// 去除表格末尾的横线
content: none; content: none;
} }
&__header{ &__header {
th{ th {
border-bottom-color: $dark-border !important; border-bottom-color: $dark-border !important;
} }
} }
th.el-table__cell{ th.el-table__cell {
background-color: $dark-bg-th; background-color: $dark-bg-th;
color: $dark-text-info; color: $dark-text-info;
} }
td{ td {
border-bottom-color: $dark-border !important; border-bottom-color: $dark-border !important;
} }
tr{ tr {
background-color: $dark-bg-td; background-color: $dark-bg-td;
color: $dark-text-main; color: $dark-text-main;
&:hover{ &:hover {
td{ td {
background-color: $dark-bg-th !important; background-color: $dark-bg-th !important;
} }
} }
@ -187,68 +186,66 @@
} }
// LINKS // LINKS
a{ a {
text-decoration: none; text-decoration: none;
color: darken($dark-color-link, 15%); color: darken($dark-color-link, 15%);
&:hover{ &:hover {
color: $dark-color-link; color: $dark-color-link;
} }
} }
// ALERT // ALERT
.el-notification{ .el-notification {
background-color: $dark-btn-bg-highlight; background-color: $dark-btn-bg-highlight;
border-color: $dark-border; border-color: $dark-border;
&__title{ &__title {
color: white; color: white;
} }
&__content{ &__content {
color: $dark-text-info; color: $dark-text-info;
} }
} }
// DIALOG // DIALOG
.el-dialog{ .el-dialog {
background-color: $dark-dialog-bg; background-color: $dark-dialog-bg;
.el-dialog__header{ .el-dialog__header {
.el-dialog__title{ .el-dialog__title {
color: $dark-text-main; color: $dark-text-main;
} }
} }
.el-dialog__body{ .el-dialog__body {
color: $dark-text-main; color: $dark-text-main;
.el-input{ .el-input {
.el-input__inner{ .el-input__inner {
color: $dark-text-main; color: $dark-text-main;
background-color: $dark-btn-bg; background-color: $dark-btn-bg;
} }
.el-input__suffix{ .el-input__suffix {
.el-input__suffix-inner{ .el-input__suffix-inner {
} }
} }
.el-input__count{ .el-input__count {
.el-input__count-inner{ .el-input__count-inner {
background-color: transparent; background-color: transparent;
} }
} }
} }
} }
.item-desc{ .item-desc {
color: $dark-text-info; color: $dark-text-info;
} }
} }
// 自定义样式 // 自定义样式
// 首页弹窗提示信息的 更新信息 面板 // 首页弹窗提示信息的 更新信息 面板
.update-info{ .update-info {
border: 1px solid $dark-btn-bg !important; border: 1px solid $dark-btn-bg !important;
.update-title{ .update-title {
color: $dark-text-main; color: $dark-text-main;
background-color: $dark-btn-bg !important; background-color: $dark-btn-bg !important;
} }
.update-content{ .update-content {
color: $dark-text-info; color: $dark-text-info;
padding: 10px; padding: 10px;
} }

View File

@ -1,32 +1,32 @@
$color-checkbox: $blue; $color-checkbox: $blue;
$color-border-el: #DCDFE6; $color-border-el: #dcdfe6;
$btn-radius: 6px; $btn-radius: 6px;
/* FORM */ /* FORM */
// checkbox // checkbox
.el-checkbox.is-bordered{ .el-checkbox.is-bordered {
@include border-radius($btn-radius) ; @include border-radius($btn-radius);
&:hover{ &:hover {
border-color: $color-checkbox; border-color: $color-checkbox;
.el-checkbox__label{ .el-checkbox__label {
color: $color-checkbox; color: $color-checkbox;
} }
} }
.el-checkbox__input.is-focus{ .el-checkbox__input.is-focus {
.el-checkbox__inner{ .el-checkbox__inner {
border-color: $color-border-el; border-color: $color-border-el;
} }
} }
&.is-checked{ &.is-checked {
background-color: $color-checkbox; background-color: $color-checkbox;
.el-checkbox__label{ .el-checkbox__label {
color: white; color: white;
} }
.el-checkbox__inner{ .el-checkbox__inner {
border-color: white; border-color: white;
background-color: white; background-color: white;
&:after{ &:after {
border-color: $color-checkbox; border-color: $color-checkbox;
} }
} }
@ -34,6 +34,6 @@ $btn-radius: 6px;
} }
// el-button // el-button
.el-button{ .el-button {
@include border-radius($btn-radius) ; @include border-radius($btn-radius);
} }

View File

@ -4,15 +4,35 @@
$gap: 5px; $gap: 5px;
@for $item from 0 through 8 { @for $item from 0 through 8 {
.mt-#{$item} { margin-top : $gap * $item !important;} .mt-#{$item} {
.mb-#{$item} { margin-bottom : $gap * $item !important;} margin-top: $gap * $item !important;
.ml-#{$item} { margin-left : $gap * $item !important;} }
.mr-#{$item} { margin-right : $gap * $item !important;} .mb-#{$item} {
.m-#{$item} { margin : $gap * $item !important;} margin-bottom: $gap * $item !important;
}
.ml-#{$item} {
margin-left: $gap * $item !important;
}
.mr-#{$item} {
margin-right: $gap * $item !important;
}
.m-#{$item} {
margin: $gap * $item !important;
}
.pt-#{$item} { padding-top : $gap * $item !important;} .pt-#{$item} {
.pb-#{$item} { padding-bottom : $gap * $item !important;} padding-top: $gap * $item !important;
.pl-#{$item} { padding-left : $gap * $item !important;} }
.pr-#{$item} { padding-right : $gap * $item !important;} .pb-#{$item} {
.p-#{$item} { padding : $gap * $item !important;} padding-bottom: $gap * $item !important;
}
.pl-#{$item} {
padding-left: $gap * $item !important;
}
.pr-#{$item} {
padding-right: $gap * $item !important;
}
.p-#{$item} {
padding: $gap * $item !important;
}
} }

View File

@ -1,4 +1,4 @@
body{ body {
font-family: $font-family; font-family: $font-family;
font-size: $fz-main; font-size: $fz-main;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -26,13 +26,13 @@ body{
padding-bottom: 1em; padding-bottom: 1em;
} }
audio{ audio {
margin-bottom: 15px; // 播放控件与表格间隔 margin-bottom: 15px; // 播放控件与表格间隔
} }
a{ a {
color: darken($color-link, 15%); color: darken($color-link, 15%);
&:hover{ &:hover {
color: $color-link; color: $color-link;
} }
} }

View File

@ -1,19 +1,19 @@
// box-shadow // box-shadow
@mixin box-shadow($value...){ @mixin box-shadow($value...) {
-webkit-box-shadow: $value; -webkit-box-shadow: $value;
-moz-box-shadow: $value; -moz-box-shadow: $value;
box-shadow: $value; box-shadow: $value;
} }
// border-radius // border-radius
@mixin border-radius($corner...){ @mixin border-radius($corner...) {
-webkit-border-radius: $corner; -webkit-border-radius: $corner;
-moz-border-radius: $corner; -moz-border-radius: $corner;
border-radius: $corner; border-radius: $corner;
} }
@mixin clearfix(){ @mixin clearfix() {
&:after{ &:after {
content: ''; content: '';
display: block; display: block;
clear: both; clear: both;
@ -21,7 +21,7 @@
} }
} }
@mixin transform($value){ @mixin transform($value) {
-webkit-transform: $value; -webkit-transform: $value;
-moz-transform: $value; -moz-transform: $value;
-ms-transform: $value; -ms-transform: $value;
@ -29,7 +29,7 @@
transform: $value; transform: $value;
} }
@mixin transition($value...){ @mixin transition($value...) {
-webkit-transition: $value; -webkit-transition: $value;
-moz-transition: $value; -moz-transition: $value;
-ms-transition: $value; -ms-transition: $value;
@ -37,23 +37,22 @@
transition: $value; transition: $value;
} }
@mixin animation($value){ @mixin animation($value) {
animation: $value; animation: $value;
-webkit-animation: $value; -webkit-animation: $value;
} }
@mixin linear-gradient($direct, $colors){ @mixin linear-gradient($direct, $colors) {
background: linear-gradient($direct, $colors); background: linear-gradient($direct, $colors);
background: -webkit-linear-gradient($direct, $colors); background: -webkit-linear-gradient($direct, $colors);
background: -moz-linear-gradient($direct, $colors); background: -moz-linear-gradient($direct, $colors);
} }
@mixin backdrop-filter($value){ @mixin backdrop-filter($value) {
backdrop-filter: $value ; backdrop-filter: $value;
-webkit-backdrop-filter: $value; -webkit-backdrop-filter: $value;
} }
/* /*
Extension Extension
*/ */
@ -65,8 +64,8 @@
user-select: none; user-select: none;
} }
.btn-like{ .btn-like {
&:active{ &:active {
@include transform(translateY(2px)) @include transform(translateY(2px));
} }
} }

View File

@ -1,35 +1,34 @@
// COLORS // COLORS
$blue : #409EFF; $blue: #409eff;
$red : #F56C6C; $red: #f56c6c;
$green : #85ce61; $green: #85ce61;
// TEXT // TEXT
$text-main : #2C3E50; $text-main: #2c3e50;
$color-link: $blue; $color-link: $blue;
$fz-main: 14px; $fz-main: 14px;
$fz-mini-title: 13px; $fz-mini-title: 13px;
$fz-mini-content: 12px; $fz-mini-content: 12px;
$font-family: "Helvetica Neue", Helvetica, "PingFang SC", $font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial,
"Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; sans-serif;
// DARK MODE // DARK MODE
$dark-border : lighten(black, 25%); $dark-border: lighten(black, 25%);
$dark-border-highlight : lighten(black, 55%); $dark-border-highlight: lighten(black, 55%);
$dark-bg : lighten(black, 10%); $dark-bg: lighten(black, 10%);
$dark-text-main : lighten(black, 90%); $dark-text-main: lighten(black, 90%);
$dark-text-info : lighten(black, 60%); $dark-text-info: lighten(black, 60%);
$dark-uploader-bg : lighten(black, 13%); $dark-uploader-bg: lighten(black, 13%);
$dark-dialog-bg : lighten(black, 15%); $dark-dialog-bg: lighten(black, 15%);
$dark-uploader-bg-highlight : lighten(black, 18%); $dark-uploader-bg-highlight: lighten(black, 18%);
$dark-btn-bg : lighten(black, 20%); $dark-btn-bg: lighten(black, 20%);
$dark-btn-bg-highlight : lighten(black, 30%); $dark-btn-bg-highlight: lighten(black, 30%);
$dark-bg-th : lighten(black, 18%); $dark-bg-th: lighten(black, 18%);
$dark-bg-td : lighten(black, 13%); $dark-bg-td: lighten(black, 13%);
$dark-color-link : $green; $dark-color-link: $green;
$dark-blue : darken(desaturate($blue, 40%), 30%); $dark-blue: darken(desaturate($blue, 40%), 30%);
$dark-red : darken(desaturate($red, 50%), 30%); $dark-red: darken(desaturate($red, 50%), 30%);
$dark-green : darken(desaturate($green, 30%), 30%); $dark-green: darken(desaturate($green, 30%), 30%);

View File

@ -1,24 +1,23 @@
@import "variables"; @import 'variables';
@import "utility"; @import 'utility';
@import "gaps"; @import 'gaps';
@import "element-ui-overrite"; @import 'element-ui-overrite';
@import "normal";
@import "dark-mode"; // dark-mode 放在 normal 后面以获得更高优先级
@import 'normal';
@import 'dark-mode'; // dark-mode 放在 normal 后面以获得更高优先级
// 首页弹窗提示信息的 更新信息 面板 // 首页弹窗提示信息的 更新信息 面板
.update-info{ .update-info {
@include border-radius(8px); @include border-radius(8px);
overflow: hidden; overflow: hidden;
border: 1px solid $color-border-el; border: 1px solid $color-border-el;
margin: 10px 0; margin: 10px 0;
.update-title{ .update-title {
font-size: $fz-mini-title; font-size: $fz-mini-title;
padding: 5px 10px; padding: 5px 10px;
background-color: $color-border-el; background-color: $color-border-el;
} }
.update-content{ .update-content {
font-size: $fz-mini-content; font-size: $fz-mini-content;
line-height: 1.5; line-height: 1.5;
padding: 10px; padding: 10px;

View File

@ -44,8 +44,7 @@ export async function extractQQMusicMeta(
musicMeta.common.artist = ''; musicMeta.common.artist = '';
if (!musicMeta.common.artists) { if (!musicMeta.common.artists) {
musicMeta.common.artist = fromGBK(musicMeta.common.artist); musicMeta.common.artist = fromGBK(musicMeta.common.artist);
} } else {
else {
musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join(); musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join();
} }
musicMeta.common.title = fromGBK(musicMeta.common.title); musicMeta.common.title = fromGBK(musicMeta.common.title);

View File

@ -5,12 +5,7 @@
<div id="app-control"> <div id="app-control">
<el-row class="mb-3"> <el-row class="mb-3">
<span>歌曲命名格式</span> <span>歌曲命名格式</span>
<el-radio <el-radio v-for="k in FilenamePolicies" :key="k.key" v-model="filename_policy" :label="k.key">
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>
@ -26,10 +21,7 @@
@cancel="showEditDialog = false" @cancel="showEditDialog = false"
@ok="handleEdit" @ok="handleEdit"
></edit-dialog> ></edit-dialog>
<config-dialog <config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></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">
<template #content> <template #content>
<span> 部分解密方案需要设定解密参数 </span> <span> 部分解密方案需要设定解密参数 </span>
@ -47,22 +39,13 @@
<el-tooltip class="item" effect="dark" placement="top-start"> <el-tooltip class="item" effect="dark" placement="top-start">
<template #content> <template #content>
<span v-if="instant_save"> <span v-if="instant_save"> 工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }} </span>
工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}
</span>
<span v-else> <span v-else>
当您使用此工具进行大量文件解锁的时候建议开启此选项<br /> 当您使用此工具进行大量文件解锁的时候建议开启此选项<br />
开启后解锁结果将不会存留于浏览器中防止内存不足 开启后解锁结果将不会存留于浏览器中防止内存不足
</span> </span>
</template> </template>
<el-checkbox <el-checkbox v-model="instant_save" type="success" border class="ml-2"> 立即保存 </el-checkbox>
v-model="instant_save"
type="success"
border
class="ml-2"
>
立即保存
</el-checkbox>
</el-tooltip> </el-tooltip>
</el-row> </el-row>
</div> </div>
@ -86,20 +69,8 @@ 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 { import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
DownloadBlobMusic, import { GetImageFromURL, RewriteMetaToMp3, RewriteMetaToFlac, AudioMimeType, split_regex } from '@/decrypt/utils';
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 {
@ -152,12 +123,7 @@ 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([ window._paq.push(['trackEvent', 'Unlock', data.rawExt + ',' + data.mime, JSON.stringify(_rp_data)]);
'trackEvent',
'Unlock',
data.rawExt + ',' + data.mime,
JSON.stringify(_rp_data),
]);
} }
}, },
showFail(errInfo, filename) { showFail(errInfo, filename) {
@ -214,9 +180,7 @@ 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( const musicMeta = await metaParseBlob(new Blob([this.editing_data.blob], { type: mime }));
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);
@ -235,26 +199,16 @@ export default {
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( this.editing_data.blob = new Blob([RewriteMetaToMp3(buffer, newMeta, musicMeta)], { type: mime });
[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( this.editing_data.blob = new Blob([RewriteMetaToFlac(buffer, newMeta, musicMeta)], { type: mime });
[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 = notifyMsg = '修改' + this.editing_data.title + '未能完成。在写入新的元数据时发生错误:' + e;
'修改' +
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) {
@ -305,16 +259,12 @@ export default {
async showDirectlySave() { async showDirectlySave() {
if (!window.showDirectoryPicker) return; if (!window.showDirectoryPicker) return;
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm('您的浏览器支持文件直接保存到磁盘,是否使用?', '新特性提示', {
'您的浏览器支持文件直接保存到磁盘,是否使用?', confirmButtonText: '使用',
'新特性提示', cancelButtonText: '不使用',
{ type: 'warning',
confirmButtonText: '使用', center: true,
cancelButtonText: '不使用', });
type: 'warning',
center: true,
}
);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
return; return;

View File

@ -13,31 +13,13 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"types": [ "types": ["webpack-env", "jest"],
"webpack-env",
"jest"
],
"paths": { "paths": {
"@/*": [ "@/*": ["src/*"]
"src/*"
]
}, },
"lib": [ "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"esnext",
"dom",
"dom.iterable",
"scripthost"
],
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": [ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
"src/**/*.ts", "exclude": ["node_modules"]
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
} }

View File

@ -1,43 +1,43 @@
const ThreadsPlugin = require('threads-plugin'); const ThreadsPlugin = require('threads-plugin');
module.exports = { module.exports = {
publicPath: '', publicPath: '',
productionSourceMap: false, productionSourceMap: false,
pwa: { pwa: {
manifestPath: "web-manifest.json", manifestPath: 'web-manifest.json',
name: "音乐解锁", name: '音乐解锁',
themeColor: "#4DBA87", themeColor: '#4DBA87',
msTileColor: "#000000", msTileColor: '#000000',
manifestOptions: { manifestOptions: {
start_url: "./index.html", start_url: './index.html',
description: "在任何设备上解锁已购的加密音乐!", description: '在任何设备上解锁已购的加密音乐!',
icons: [ icons: [
{ {
'src': './img/icons/android-chrome-192x192.png', src: './img/icons/android-chrome-192x192.png',
'sizes': '192x192', sizes: '192x192',
'type': 'image/png' type: 'image/png',
},
{
'src': './img/icons/android-chrome-512x512.png',
'sizes': '512x512',
'type': 'image/png'
}
]
}, },
appleMobileWebAppCapable: 'yes', {
iconPaths: { src: './img/icons/android-chrome-512x512.png',
faviconSVG: './img/icons/safari-pinned-tab.svg', sizes: '512x512',
favicon32: './img/icons/favicon-32x32.png', type: 'image/png',
favicon16: './img/icons/favicon-16x16.png',
appleTouchIcon: './img/icons/apple-touch-icon-152x152.png',
maskIcon: './img/icons/safari-pinned-tab.svg',
msTileImage: './img/icons/msapplication-icon-144x144.png'
}, },
workboxPluginMode: "GenerateSW", ],
workboxOptions: {
skipWaiting: true
}
}, },
configureWebpack: { appleMobileWebAppCapable: 'yes',
plugins: [new ThreadsPlugin()] iconPaths: {
} faviconSVG: './img/icons/safari-pinned-tab.svg',
favicon32: './img/icons/favicon-32x32.png',
favicon16: './img/icons/favicon-16x16.png',
appleTouchIcon: './img/icons/apple-touch-icon-152x152.png',
maskIcon: './img/icons/safari-pinned-tab.svg',
msTileImage: './img/icons/msapplication-icon-144x144.png',
},
workboxPluginMode: 'GenerateSW',
workboxOptions: {
skipWaiting: true,
},
},
configureWebpack: {
plugins: [new ThreadsPlugin()],
},
}; };