Browse Source

refactor: component/*.vue

20230320
Emmm Monster 4 years ago
parent
commit
c7e5dfb4c4
No known key found for this signature in database GPG Key ID: C98279C83FB50DB9
  1. 94
      package-lock.json
  2. 9
      package.json
  3. 26
      src/App.vue
  4. 77
      src/component/FileSelector.vue
  5. 20
      src/component/PreviewTable.vue
  6. 120
      src/component/upload.vue
  7. 25
      src/component/utils.ts
  8. 4
      src/component/worker.ts
  9. 21
      src/decrypt/common.ts
  10. 9
      src/decrypt/entity.ts
  11. 4
      src/decrypt/qmc.ts
  12. 1
      src/decrypt/qmcMask.ts
  13. 2
      src/decrypt/xm.ts
  14. 4
      src/main.ts
  15. 2
      tsconfig.json
  16. 4
      vue.config.js

94
package-lock.json

@ -5820,6 +5820,12 @@
"estraverse": "^4.1.1"
}
},
"esm": {
"version": "3.2.25",
"resolved": "http://mirrors.cloud.tencent.com/npm/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
"optional": true
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
@ -7822,6 +7828,11 @@
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
"is-observable": {
"version": "2.1.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/is-observable/-/is-observable-2.1.0.tgz",
"integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw=="
},
"is-path-cwd": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@ -9305,6 +9316,11 @@
"has": "^1.0.3"
}
},
"observable-fns": {
"version": "0.5.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/observable-fns/-/observable-fns-0.5.1.tgz",
"integrity": "sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A=="
},
"obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
@ -12243,6 +12259,56 @@
}
}
},
"threads": {
"version": "1.6.4",
"resolved": "http://mirrors.cloud.tencent.com/npm/threads/-/threads-1.6.4.tgz",
"integrity": "sha512-A+9MQFAUha9W8MjIPmrvETy98qVmZFr5Unox9D95y7kvz3fBpGiFS7JOZs07B2KvTHoRNI5MrGudRVeCmv4Alw==",
"requires": {
"callsites": "^3.1.0",
"debug": "^4.2.0",
"is-observable": "^2.1.0",
"observable-fns": "^0.5.1",
"tiny-worker": ">= 2"
},
"dependencies": {
"callsites": {
"version": "3.1.0",
"resolved": "https://mirrors.tencent.com/npm/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
}
}
},
"threads-plugin": {
"version": "1.4.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/threads-plugin/-/threads-plugin-1.4.0.tgz",
"integrity": "sha512-lQENPueZLsD+6Cvxvj/QaQyUskwnFZO+2ZGDMnPIvtytSeywWvYzete8paZ9L+5IR4v8jnSYNZPlIQrEhSK1EA==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0"
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
}
},
"loader-utils": {
"version": "1.4.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^1.0.1"
}
}
}
},
"throttle-debounce": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-1.1.0.tgz",
@ -12284,6 +12350,15 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"tiny-worker": {
"version": "2.3.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/tiny-worker/-/tiny-worker-2.3.0.tgz",
"integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==",
"optional": true,
"requires": {
"esm": "^3.2.25"
}
},
"tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
@ -13009,11 +13084,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"vue-class-component": {
"version": "7.2.6",
"resolved": "http://mirrors.cloud.tencent.com/npm/vue-class-component/-/vue-class-component-7.2.6.tgz",
"integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w=="
},
"vue-cli-plugin-element": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-element/-/vue-cli-plugin-element-1.0.1.tgz",
@ -13136,11 +13206,6 @@
}
}
},
"vue-property-decorator": {
"version": "9.1.2",
"resolved": "http://mirrors.cloud.tencent.com/npm/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz",
"integrity": "sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ=="
},
"vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -13957,15 +14022,6 @@
"microevent.ts": "~0.1.1"
}
},
"workerize-loader": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/workerize-loader/-/workerize-loader-1.3.0.tgz",
"integrity": "sha512-utWDc8K6embcICmRBUUkzanPgKBb8yM1OHfh6siZfiMsswE8wLCa9CWS+L7AARz0+Th4KH4ZySrqer/OJ9WuWw==",
"dev": true,
"requires": {
"loader-utils": "^2.0.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",

9
package.json

@ -26,9 +26,8 @@
"metaflac-js": "^1.0.5",
"music-metadata-browser": "^2.2.6",
"register-service-worker": "^1.7.2",
"vue": "^2.6.12",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^9.1.2"
"threads": "^1.6.4",
"vue": "^2.6.12"
},
"devDependencies": {
"@types/crypto-js": "^4.0.1",
@ -40,9 +39,9 @@
"node-sass": "^5.0.0",
"sass-loader": "^10.2.0",
"semver": "^7.3.5",
"threads-plugin": "^1.4.0",
"typescript": "~4.1.5",
"vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.12",
"workerize-loader": "^1.3.0"
"vue-template-compiler": "^2.6.12"
}
}

26
src/App.vue

@ -2,7 +2,7 @@
<el-container id="app">
<el-main>
<x-upload v-on:handle_error="showFail" v-on:handle_finish="showSuccess"></x-upload>
<file-selector @error="showFail" @success="showSuccess"/>
<div id="app-control">
<el-row class="mb-3">
@ -28,14 +28,14 @@
<audio :autoplay="playing_auto" :src="playing_url" controls/>
<x-preview :download_format="download_format" :table-data="tableData"
v-on:music_changed="changePlaying"></x-preview>
<PreviewTable :filename_format="download_format" :table-data="tableData"
@music_changed="changePlaying"></PreviewTable>
</el-main>
<el-footer id="app-footer">
<el-row>
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>(v<span
v-text="version"></span>)移除已购音乐的加密保护
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>
(v{{ version }})移除已购音乐的加密保护
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</el-row>
<el-row>
@ -44,7 +44,7 @@
</el-row>
<el-row>
<!--如果进行二次开发此行版权信息不得移除且应明显地标注于页面上-->
<span>Copyright &copy; 2019-</span><span v-text="(new Date()).getFullYear()"></span> MengYX
<span>Copyright &copy; 2019 - {{ (new Date()).getFullYear() }} MengYX</span>
音乐解锁使用
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
开放源代码
@ -56,17 +56,17 @@
<script>
import upload from "./component/upload"
import preview from "./component/preview"
import {DownloadBlobMusic, RemoveBlobMusic} from "./component/util"
import FileSelector from "./component/FileSelector"
import PreviewTable from "./component/PreviewTable"
import {DownloadBlobMusic, RemoveBlobMusic} from "./component/utils"
import config from "../package"
import {IXAREA_API_ENDPOINT} from "@/decrypt/utils";
export default {
name: 'app',
components: {
xUpload: upload,
xPreview: preview
FileSelector,
PreviewTable
},
data() {
return {
@ -121,7 +121,6 @@
}
},
showSuccess(data) {
if (data.status) {
if (this.instant_download) {
DownloadBlobMusic(data, this.download_format);
RemoveBlobMusic(data);
@ -137,9 +136,6 @@
let _rp_data = [data.title, data.artist, data.album];
window._paq.push(["trackEvent", "Unlock", data.rawExt + "," + data.mime, JSON.stringify(_rp_data)]);
}
} else {
this.showFail(data.message, data.rawFilename + "." + data.rawExt)
}
},
showFail(errInfo, filename) {
this.$notify.error({

77
src/component/FileSelector.vue

@ -0,0 +1,77 @@
<template>
<el-upload
:auto-upload="false"
:on-change="addFile"
:show-file-list="false"
action=""
drag
multiple>
<i class="el-icon-upload"/>
<div class="el-upload__text">将文件拖到此处<em>点击选择</em></div>
<div slot="tip" class="el-upload__tip">本工具仅在浏览器内对文件进行解锁无需消耗流量</div>
<transition name="el-fade-in"><!--todo: add delay to animation-->
<el-progress
v-show="progress_show" :format="progress_string" :percentage="progress_value"
:stroke-width="16" :text-inside="true"
style="margin: 16px 6px 0 6px"
></el-progress>
</transition>
</el-upload>
</template>
<script>
import {spawn, Worker, Pool} from "threads"
import {CommonDecrypt} from "@/decrypt/common.ts";
import {DecryptQueue} from "@/component/utils";
export default {
name: "FileSelector",
data() {
return {
task_all: 0,
task_finished: 0,
queue: new DecryptQueue() // for http or file protocol
}
},
computed: {
progress_value() {
return this.task_all ? this.task_finished / this.task_all * 100 : 0
},
progress_show() {
return this.task_all !== this.task_finished
}
},
mounted() {
if (window.Worker) {
console.log("Using Worker Pool")
this.queue = Pool(
() => spawn(new Worker('@/component/worker.ts')),
navigator.hardwareConcurrency || 1
)
} else {
console.log("Using Queue in Main Thread")
}
},
methods: {
progress_string() {
return `${this.task_finished} / ${this.task_all}`
},
async addFile(file) {
this.task_all++
this.queue.queue(async (dec = CommonDecrypt) => {
console.log("start handling", file.name)
try {
this.$emit("success", await dec(file));
} catch (e) {
console.error(e)
this.$emit("error", file)
} finally {
this.task_finished++
}
})
},
}
}
</script>

20
src/component/preview.vue → src/component/PreviewTable.vue

@ -4,7 +4,7 @@
<el-table-column label="封面">
<template slot-scope="scope">
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
<div class="image-slot el-image__error" slot="error">
<div slot="error" class="image-slot el-image__error">
暂无封面
</div>
</el-image>
@ -27,14 +27,14 @@
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button @click="handlePlay(scope.$index, scope.row)"
circle icon="el-icon-video-play" type="success">
<el-button circle
icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
</el-button>
<el-button @click="handleDownload(scope.row)"
circle icon="el-icon-download">
<el-button circle
icon="el-icon-download" @click="handleDownload(scope.row)">
</el-button>
<el-button @click="handleDelete(scope.$index, scope.row)"
circle icon="el-icon-delete" type="danger">
<el-button circle
icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
</el-button>
</template>
</el-table-column>
@ -42,13 +42,13 @@
</template>
<script>
import {DownloadBlobMusic, RemoveBlobMusic} from './util'
import {DownloadBlobMusic, RemoveBlobMusic} from '@/component/utils'
export default {
name: "preview",
name: "PreviewTable",
props: {
tableData: {type: Array, required: true},
download_format: {type: String, required: true}
filename_format: {type: String, required: true}
},
methods: {

120
src/component/upload.vue

@ -1,120 +0,0 @@
<template>
<el-upload
:auto-upload="false"
:on-change="handleFile"
:show-file-list="false"
action=""
drag
multiple>
<i class="el-icon-upload"/>
<div class="el-upload__text">将文件拖到此处<em>点击选择</em></div>
<div class="el-upload__tip" slot="tip">本工具仅在浏览器内对文件进行解锁无需消耗流量</div>
<transition name="el-fade-in">
<el-progress
:format="progressFormat" :percentage="progress_percent" :stroke-width="16"
:text-inside="true" style="margin: 16px 6px 0 6px"
v-show="progress_show"
></el-progress>
</transition>
</el-upload>
</template>
<script>
"use strict";//
export default {
name: "upload",
data() {
return {
cacheQueue: [],
workers: [],
idle_workers: [],
thread_num: 1,
progress_show: false,
progress_finished: 0,
progress_all: 0,
progress_percent: 0,
}
},
mounted() {
if (document.location.host !== "" && process.env.NODE_ENV === 'production') {
this.thread_num = navigator.hardwareConcurrency || 1;
const worker = require("workerize-loader!../decrypt/common");
// noinspection JSValidateTypes,JSUnresolvedVariable
this.workers.push(worker().CommonDecrypt);
this.idle_workers.push(0);
// delay to optimize for first loading
setTimeout(() => {
for (let i = 1; i < this.thread_num; i++) {
// noinspection JSValidateTypes,JSUnresolvedVariable
this.workers.push(worker().CommonDecrypt);
this.idle_workers.push(i);
}
}, 5000);
} else {
const dec = require('../decrypt/common');
this.workers.push(dec.CommonDecrypt);
this.idle_workers.push(0)
}
},
methods: {
progressFormat() {
return this.progress_finished + "/" + (this.progress_all)
},
progressChange(finish, all) {
this.progress_all += all;
this.progress_finished += finish;
this.progress_percent = Math.round(this.progress_finished / this.progress_all * 100);
if (this.progress_finished === this.progress_all) {
setTimeout(() => {
this.progress_show = false;
this.progress_finished = 0;
this.progress_all = 0;
}, 3000);
} else {
this.progress_show = true;
}
},
handleFile(file) {
this.progressChange(0, +1);
// worker
if (this.idle_workers.length > 0) {
this.handleDoFile(file, this.idle_workers.shift());
}
// worker
else {
this.cacheQueue.push(file);
}
},
handleCacheQueue(worker_id) {
//
if (this.cacheQueue.length === 0) {
this.idle_workers.push(worker_id);
return
}
this.handleDoFile(this.cacheQueue.shift(), worker_id);
},
handleDoFile(file, worker_id) {
this.workers[worker_id](file).then(data => {
this.$emit("handle_finish", data);
// todo: call stack
this.handleCacheQueue(worker_id);
this.progressChange(+1, 0);
}).catch(err => {
this.$emit("handle_error", err, file.name);
this.handleCacheQueue(worker_id);
this.progressChange(+1, 0);
})
},
}
}
</script>
<style scoped>
/*noinspection CssUnusedSymbol*/
.el-upload-dragger {
width: 80vw !important;
}
</style>

25
src/component/util.js → src/component/utils.ts

@ -1,4 +1,6 @@
export function DownloadBlobMusic(data, format) {
import {DecryptResult} from "@/decrypt/entity";
export function DownloadBlobMusic(data: DecryptResult, format: string) {//todo: use enum
const a = document.createElement('a');
a.href = data.file;
switch (format) {
@ -21,10 +23,27 @@ export function DownloadBlobMusic(data, format) {
a.remove();
}
export function RemoveBlobMusic(data) {
export function RemoveBlobMusic(data: DecryptResult) {
URL.revokeObjectURL(data.file);
if (data.picture.startsWith("blob:")) {
if (data.picture?.startsWith("blob:")) {
URL.revokeObjectURL(data.picture);
}
}
export class DecryptQueue {
private readonly pending: (() => Promise<void>)[];
constructor() {
this.pending = []
}
queue(fn: () => Promise<void>) {
this.pending.push(fn)
this.consume()
}
private consume() {
const fn = this.pending.shift()
if (fn) fn().then(() => this.consume).catch(console.error)
}
}

4
src/component/worker.ts

@ -0,0 +1,4 @@
import {expose} from "threads/worker";
import {CommonDecrypt} from "@/decrypt/common";
expose(CommonDecrypt)

21
src/decrypt/common.ts

@ -1,21 +1,11 @@
import {Decrypt as NcmDecrypt} from "./ncm";
import {Decrypt as XmDecrypt} from "./xm";
import {Decrypt as QmcDecrypt} from "./qmc";
import {Decrypt as KgmDecrypt} from "./kgm";
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
import {Decrypt as KgmDecrypt} from "@/decrypt/kgm";
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
import {DecryptResult} from "@/decrypt/entity";
interface FileInfo {
status: string
name: string,
size: number,
percentage: number,
uid: number,
raw: File
}
import {DecryptResult, FileInfo} from "@/decrypt/entity";
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
@ -78,3 +68,4 @@ export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
console.log(rt_data);
return rt_data;
}

9
src/decrypt/entity.ts

@ -14,3 +14,12 @@ export interface DecryptResult {
rawFilename?: string
}
export interface FileInfo {
status: string
name: string,
size: number,
percentage: number,
uid: number,
raw: File
}

4
src/decrypt/qmc.ts

@ -136,7 +136,7 @@ async function queryKeyInfo(keyData: Uint8Array, filename: string, format: strin
let data = await resp.json();
return new QmcMask(Base64Decode(data.Matrix44));
} catch (e) {
console.log(e);
console.warn(e);
}
}
@ -150,7 +150,7 @@ async function queryAlbumCoverImage(title: string, artist?: string, album?: stri
return song_query_url + "/" + data.Type + "/" + data.Id
}
} catch (e) {
console.log(e);
console.warn(e);
}
return "";
}

1
src/decrypt/qmcMask.ts

@ -177,7 +177,6 @@ export function QmcMaskDetectMgg(data: Uint8Array) {
function calcMaskFromConfidence(confidence: { [key: number]: number }) {
console.log(confidence)
const count = Object.keys(confidence).length
if (count === 0) throw "can not match at least one key";
if (count > 1) console.warn("There are 2 potential value for the mask!")

2
src/decrypt/xm.ts

@ -42,7 +42,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
const musicMeta = await metaParseBlob(musicBlob);
if (ext === "wav") {
//todo:未知的编码方式
console.log(musicMeta.common)
console.info(musicMeta.common)
musicMeta.common.album = "";
musicMeta.common.artist = "";
musicMeta.common.title = "";

4
src/main.ts

@ -1,6 +1,6 @@
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import App from '@/App.vue'
import '@/registerServiceWorker'
import {
Button,
Checkbox,

2
tsconfig.json

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "esnext",
"target": "es5",
"module": "esnext",
"strict": true,
"jsx": "preserve",

4
vue.config.js

@ -1,3 +1,4 @@
const ThreadsPlugin = require('threads-plugin');
module.exports = {
publicPath: '',
productionSourceMap: false,
@ -35,5 +36,8 @@ module.exports = {
workboxOptions: {
skipWaiting: true
}
},
configureWebpack: {
plugins: [new ThreadsPlugin()]
}
};

Loading…
Cancel
Save