MengYX
3 years ago
34 changed files with 1656 additions and 1709 deletions
@ -1,85 +1,87 @@ |
|||
<template> |
|||
<el-container id="app"> |
|||
<el-main> |
|||
<Home/> |
|||
</el-main> |
|||
<el-footer id="app-footer"> |
|||
<el-row> |
|||
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }}) |
|||
:移除已购音乐的加密保护。 |
|||
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a> |
|||
</el-row> |
|||
<el-row> |
|||
目前支持网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm) |
|||
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。 |
|||
</el-row> |
|||
<el-row> |
|||
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上--> |
|||
<span>Copyright © 2019 - {{ (new Date()).getFullYear() }} MengYX</span> |
|||
音乐解锁使用 |
|||
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a> |
|||
开放源代码 |
|||
</el-row> |
|||
</el-footer> |
|||
</el-container> |
|||
<el-container id="app"> |
|||
<el-main> |
|||
<Home /> |
|||
</el-main> |
|||
<el-footer id="app-footer"> |
|||
<el-row> |
|||
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }}) |
|||
:移除已购音乐的加密保护。 |
|||
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a> |
|||
</el-row> |
|||
<el-row> |
|||
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm) |
|||
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。 |
|||
</el-row> |
|||
<el-row> |
|||
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上--> |
|||
<span>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span> |
|||
音乐解锁使用 |
|||
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a> |
|||
开放源代码 |
|||
</el-row> |
|||
</el-footer> |
|||
</el-container> |
|||
</template> |
|||
|
|||
<script> |
|||
|
|||
import FileSelector from "@/component/FileSelector" |
|||
import PreviewTable from "@/component/PreviewTable" |
|||
import config from "@/../package.json" |
|||
import Home from "@/view/Home"; |
|||
import {checkUpdate} from "@/utils/api"; |
|||
import FileSelector from '@/component/FileSelector'; |
|||
import PreviewTable from '@/component/PreviewTable'; |
|||
import config from '@/../package.json'; |
|||
import Home from '@/view/Home'; |
|||
import { checkUpdate } from '@/utils/api'; |
|||
|
|||
export default { |
|||
name: 'app', |
|||
components: { |
|||
FileSelector, |
|||
PreviewTable, |
|||
Home |
|||
}, |
|||
data() { |
|||
return { |
|||
version: config.version, |
|||
} |
|||
}, |
|||
created() { |
|||
this.$nextTick(() => this.finishLoad()); |
|||
}, |
|||
methods: { |
|||
async finishLoad() { |
|||
const mask = document.getElementById("loader-mask"); |
|||
if (!!mask) mask.remove(); |
|||
let updateInfo; |
|||
try { |
|||
updateInfo = await checkUpdate(this.version) |
|||
} catch (e) { |
|||
console.warn("check version info failed", e) |
|||
} |
|||
if ((updateInfo && process.env.NODE_ENV === 'production') && (updateInfo.HttpsFound || |
|||
(updateInfo.Found && window.location.protocol !== "https:"))) { |
|||
this.$notify.warning({ |
|||
title: '发现更新', |
|||
message: `发现新版本 v${updateInfo.Version}<br/>更新详情:${updateInfo.Detail}<br/> <a target="_blank" href="${updateInfo.URL}">获取更新</a>`, |
|||
dangerouslyUseHTMLString: true, |
|||
duration: 15000, |
|||
position: 'top-left' |
|||
}); |
|||
} else { |
|||
this.$notify.info({ |
|||
title: '离线使用', |
|||
message: `我们使用PWA技术,无网络也能使用<br/>最近更新:${config.updateInfo}<br/><a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>`, |
|||
dangerouslyUseHTMLString: true, |
|||
duration: 10000, |
|||
position: 'top-left' |
|||
}); |
|||
} |
|||
} |
|||
name: 'app', |
|||
components: { |
|||
FileSelector, |
|||
PreviewTable, |
|||
Home, |
|||
}, |
|||
data() { |
|||
return { |
|||
version: config.version, |
|||
}; |
|||
}, |
|||
created() { |
|||
this.$nextTick(() => this.finishLoad()); |
|||
}, |
|||
methods: { |
|||
async finishLoad() { |
|||
const mask = document.getElementById('loader-mask'); |
|||
if (!!mask) mask.remove(); |
|||
let updateInfo; |
|||
try { |
|||
updateInfo = await checkUpdate(this.version); |
|||
} catch (e) { |
|||
console.warn('check version info failed', e); |
|||
} |
|||
if ( |
|||
updateInfo && |
|||
process.env.NODE_ENV === 'production' && |
|||
(updateInfo.HttpsFound || (updateInfo.Found && window.location.protocol !== 'https:')) |
|||
) { |
|||
this.$notify.warning({ |
|||
title: '发现更新', |
|||
message: `发现新版本 v${updateInfo.Version}<br/>更新详情:${updateInfo.Detail}<br/> <a target="_blank" href="${updateInfo.URL}">获取更新</a>`, |
|||
dangerouslyUseHTMLString: true, |
|||
duration: 15000, |
|||
position: 'top-left', |
|||
}); |
|||
} else { |
|||
this.$notify.info({ |
|||
title: '离线使用', |
|||
message: `我们使用PWA技术,无网络也能使用<br/>最近更新:${config.updateInfo}<br/><a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>`, |
|||
dangerouslyUseHTMLString: true, |
|||
duration: 10000, |
|||
position: 'top-left', |
|||
}); |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
@import "scss/unlock-music"; |
|||
@import 'scss/unlock-music'; |
|||
</style> |
|||
|
@ -1,71 +1,62 @@ |
|||
<template> |
|||
<el-table :data="tableData" style="width: 100%"> |
|||
|
|||
<el-table-column label="封面"> |
|||
<template slot-scope="scope"> |
|||
<el-image :src="scope.row.picture" style="width: 100px; height: 100px"> |
|||
<div slot="error" class="image-slot el-image__error"> |
|||
暂无封面 |
|||
</div> |
|||
</el-image> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="歌曲"> |
|||
<template #default="scope"> |
|||
<span>{{ scope.row.title }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="歌手"> |
|||
<template #default="scope"> |
|||
<p>{{ scope.row.artist }}</p> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="专辑"> |
|||
<template #default="scope"> |
|||
<p>{{ scope.row.album }}</p> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="操作"> |
|||
<template #default="scope"> |
|||
<el-button circle |
|||
icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)"> |
|||
</el-button> |
|||
<el-button circle |
|||
icon="el-icon-download" @click="handleDownload(scope.row)"> |
|||
</el-button> |
|||
<el-button circle |
|||
icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)"> |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<el-table :data="tableData" style="width: 100%"> |
|||
<el-table-column label="封面"> |
|||
<template slot-scope="scope"> |
|||
<el-image :src="scope.row.picture" style="width: 100px; height: 100px"> |
|||
<div slot="error" class="image-slot el-image__error">暂无封面</div> |
|||
</el-image> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="歌曲"> |
|||
<template #default="scope"> |
|||
<span>{{ scope.row.title }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="歌手"> |
|||
<template #default="scope"> |
|||
<p>{{ scope.row.artist }}</p> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="专辑"> |
|||
<template #default="scope"> |
|||
<p>{{ scope.row.album }}</p> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="操作"> |
|||
<template #default="scope"> |
|||
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)"> |
|||
</el-button> |
|||
<el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button> |
|||
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)"> |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</template> |
|||
|
|||
<script> |
|||
import {RemoveBlobMusic} from '@/utils/utils' |
|||
import { RemoveBlobMusic } from '@/utils/utils'; |
|||
|
|||
export default { |
|||
name: "PreviewTable", |
|||
props: { |
|||
tableData: {type: Array, required: true}, |
|||
policy: {type: Number, required: true} |
|||
}, |
|||
name: 'PreviewTable', |
|||
props: { |
|||
tableData: { type: Array, required: true }, |
|||
policy: { type: Number, required: true }, |
|||
}, |
|||
|
|||
methods: { |
|||
handlePlay(index, row) { |
|||
this.$emit("play", row.file); |
|||
}, |
|||
handleDelete(index, row) { |
|||
RemoveBlobMusic(row); |
|||
this.tableData.splice(index, 1); |
|||
}, |
|||
handleDownload(row) { |
|||
this.$emit("download", row) |
|||
}, |
|||
} |
|||
} |
|||
methods: { |
|||
handlePlay(index, row) { |
|||
this.$emit('play', row.file); |
|||
}, |
|||
handleDelete(index, row) { |
|||
RemoveBlobMusic(row); |
|||
this.tableData.splice(index, 1); |
|||
}, |
|||
handleDownload(row) { |
|||
this.$emit('download', row); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
</style> |
|||
<style scoped></style> |
|||
|
@ -1,26 +1,25 @@ |
|||
export interface DecryptResult { |
|||
title: string |
|||
album?: string |
|||
artist?: string |
|||
title: string; |
|||
album?: string; |
|||
artist?: string; |
|||
|
|||
mime: string |
|||
ext: string |
|||
mime: string; |
|||
ext: string; |
|||
|
|||
file: string |
|||
blob: Blob |
|||
picture?: string |
|||
|
|||
message?: string |
|||
rawExt?: string |
|||
rawFilename?: string |
|||
file: string; |
|||
blob: Blob; |
|||
picture?: string; |
|||
|
|||
message?: string; |
|||
rawExt?: string; |
|||
rawFilename?: string; |
|||
} |
|||
|
|||
export interface FileInfo { |
|||
status: string |
|||
name: string, |
|||
size: number, |
|||
percentage: number, |
|||
uid: number, |
|||
raw: File |
|||
status: string; |
|||
name: string; |
|||
size: number; |
|||
percentage: number; |
|||
uid: number; |
|||
raw: File; |
|||
} |
|||
|
@ -1,122 +1,125 @@ |
|||
import { |
|||
AudioMimeType, |
|||
BytesHasPrefix, |
|||
GetArrayBuffer, |
|||
GetCoverFromFile, |
|||
GetMetaFromFile, |
|||
SniffAudioExt |
|||
} from "@/decrypt/utils"; |
|||
import {parseBlob as metaParseBlob} from "music-metadata-browser"; |
|||
import {DecryptResult} from "@/decrypt/entity"; |
|||
import config from "@/../package.json" |
|||
AudioMimeType, |
|||
BytesHasPrefix, |
|||
GetArrayBuffer, |
|||
GetCoverFromFile, |
|||
GetMetaFromFile, |
|||
SniffAudioExt, |
|||
} from '@/decrypt/utils'; |
|||
import { parseBlob as metaParseBlob } from 'music-metadata-browser'; |
|||
import { DecryptResult } from '@/decrypt/entity'; |
|||
import config from '@/../package.json'; |
|||
|
|||
//prettier-ignore
|
|||
const VprHeader = [ |
|||
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43, |
|||
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31] |
|||
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43, |
|||
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31 |
|||
] |
|||
//prettier-ignore
|
|||
const KgmHeader = [ |
|||
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B, |
|||
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14] |
|||
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B, |
|||
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14 |
|||
] |
|||
//prettier-ignore
|
|||
const VprMaskDiff = [ |
|||
0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E, |
|||
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11, |
|||
0x00] |
|||
|
|||
0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E, |
|||
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11, |
|||
0x00 |
|||
] |
|||
|
|||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> { |
|||
const oriData = new Uint8Array(await GetArrayBuffer(file)); |
|||
if (raw_ext === 'vpr') { |
|||
if (!BytesHasPrefix(oriData, VprHeader)) throw Error('Not a valid vpr file!'); |
|||
} else { |
|||
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error('Not a valid kgm(a) file!'); |
|||
} |
|||
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer); |
|||
let headerLen = bHeaderLen.getUint32(0, true); |
|||
|
|||
const oriData = new Uint8Array(await GetArrayBuffer(file)); |
|||
if (raw_ext === "vpr") { |
|||
if (!BytesHasPrefix(oriData, VprHeader)) throw Error("Not a valid vpr file!") |
|||
} else { |
|||
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error("Not a valid kgm(a) file!") |
|||
} |
|||
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer) |
|||
let headerLen = bHeaderLen.getUint32(0, true) |
|||
let audioData = oriData.slice(headerLen); |
|||
let dataLen = audioData.length; |
|||
if (audioData.byteLength > 1 << 26) { |
|||
throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁"); |
|||
} |
|||
|
|||
let audioData = oriData.slice(headerLen) |
|||
let dataLen = audioData.length |
|||
if (audioData.byteLength > 1 << 26) { |
|||
throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁") |
|||
} |
|||
let key1 = new Uint8Array(17); |
|||
key1.set(oriData.slice(0x1c, 0x2c), 0); |
|||
if (MaskV2.length === 0) { |
|||
if (!(await LoadMaskV2())) throw Error('加载Kgm/Vpr Mask数据失败'); |
|||
} |
|||
|
|||
let key1 = new Uint8Array(17) |
|||
key1.set(oriData.slice(0x1c, 0x2c), 0) |
|||
if (MaskV2.length === 0) { |
|||
if (!await LoadMaskV2()) throw Error("加载Kgm/Vpr Mask数据失败") |
|||
} |
|||
for (let i = 0; i < dataLen; i++) { |
|||
let med8 = key1[i % 17] ^ audioData[i]; |
|||
med8 ^= (med8 & 0xf) << 4; |
|||
|
|||
for (let i = 0; i < dataLen; i++) { |
|||
let med8 = key1[i % 17] ^ audioData[i] |
|||
med8 ^= (med8 & 0xf) << 4 |
|||
let msk8 = GetMask(i); |
|||
msk8 ^= (msk8 & 0xf) << 4; |
|||
audioData[i] = med8 ^ msk8; |
|||
} |
|||
if (raw_ext === 'vpr') { |
|||
for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17]; |
|||
} |
|||
|
|||
let msk8 = GetMask(i) |
|||
msk8 ^= (msk8 & 0xf) << 4 |
|||
audioData[i] = med8 ^ msk8 |
|||
} |
|||
if (raw_ext === "vpr") { |
|||
for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17] |
|||
} |
|||
|
|||
const ext = SniffAudioExt(audioData); |
|||
const mime = AudioMimeType[ext]; |
|||
let musicBlob = new Blob([audioData], {type: mime}); |
|||
const musicMeta = await metaParseBlob(musicBlob); |
|||
const {title, artist} = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist) |
|||
return { |
|||
album: musicMeta.common.album, |
|||
picture: GetCoverFromFile(musicMeta), |
|||
file: URL.createObjectURL(musicBlob), |
|||
blob: musicBlob, |
|||
ext, |
|||
mime, |
|||
title, |
|||
artist |
|||
} |
|||
const ext = SniffAudioExt(audioData); |
|||
const mime = AudioMimeType[ext]; |
|||
let musicBlob = new Blob([audioData], { type: mime }); |
|||
const musicMeta = await metaParseBlob(musicBlob); |
|||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist); |
|||
return { |
|||
album: musicMeta.common.album, |
|||
picture: GetCoverFromFile(musicMeta), |
|||
file: URL.createObjectURL(musicBlob), |
|||
blob: musicBlob, |
|||
ext, |
|||
mime, |
|||
title, |
|||
artist, |
|||
}; |
|||
} |
|||
|
|||
|
|||
function GetMask(pos: number) { |
|||
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4] |
|||
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]; |
|||
} |
|||
|
|||
let MaskV2: Uint8Array = new Uint8Array(0); |
|||
|
|||
async function LoadMaskV2(): Promise<boolean> { |
|||
let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/static/kgm.mask` |
|||
if (["http:", "https:"].some(v => v == self.location.protocol)) { |
|||
if (!!self.document) {// using Web Worker
|
|||
mask_url = "./static/kgm.mask" |
|||
} else {// using Main thread
|
|||
mask_url = "../static/kgm.mask" |
|||
} |
|||
} |
|||
try { |
|||
const resp = await fetch(mask_url, {method: "GET"}) |
|||
MaskV2 = new Uint8Array(await resp.arrayBuffer()); |
|||
return true |
|||
} catch (e) { |
|||
console.error(e) |
|||
return false |
|||
let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/static/kgm.mask`; |
|||
if (['http:', 'https:'].some((v) => v == self.location.protocol)) { |
|||
if (!!self.document) { |
|||
// using Web Worker
|
|||
mask_url = './static/kgm.mask'; |
|||
} else { |
|||
// using Main thread
|
|||
mask_url = '../static/kgm.mask'; |
|||
} |
|||
} |
|||
try { |
|||
const resp = await fetch(mask_url, { method: 'GET' }); |
|||
MaskV2 = new Uint8Array(await resp.arrayBuffer()); |
|||
return true; |
|||
} catch (e) { |
|||
console.error(e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
//prettier-ignore
|
|||
const MaskV2PreDef = [ |
|||
0xB8, 0xD5, 0x3D, 0xB2, 0xE9, 0xAF, 0x78, 0x8C, 0x83, 0x33, 0x71, 0x51, 0x76, 0xA0, 0xCD, 0x37, |
|||
0x2F, 0x3E, 0x35, 0x8D, 0xA9, 0xBE, 0x98, 0xB7, 0xE7, 0x8C, 0x22, 0xCE, 0x5A, 0x61, 0xDF, 0x68, |
|||
0x69, 0x89, 0xFE, 0xA5, 0xB6, 0xDE, 0xA9, 0x77, 0xFC, 0xC8, 0xBD, 0xBD, 0xE5, 0x6D, 0x3E, 0x5A, |
|||
0x36, 0xEF, 0x69, 0x4E, 0xBE, 0xE1, 0xE9, 0x66, 0x1C, 0xF3, 0xD9, 0x02, 0xB6, 0xF2, 0x12, 0x9B, |
|||
0x44, 0xD0, 0x6F, 0xB9, 0x35, 0x89, 0xB6, 0x46, 0x6D, 0x73, 0x82, 0x06, 0x69, 0xC1, 0xED, 0xD7, |
|||
0x85, 0xC2, 0x30, 0xDF, 0xA2, 0x62, 0xBE, 0x79, 0x2D, 0x62, 0x62, 0x3D, 0x0D, 0x7E, 0xBE, 0x48, |
|||
0x89, 0x23, 0x02, 0xA0, 0xE4, 0xD5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xFD, 0x16, 0x3A, 0x21, 0x3B, |
|||
0x16, 0x0F, 0xC3, 0xB2, 0xBB, 0xB3, 0xE2, 0xBA, 0x3A, 0x3D, 0x13, 0xEC, 0xF6, 0x01, 0x45, 0x84, |
|||
0xA5, 0x70, 0x0F, 0x93, 0x49, 0x0C, 0x64, 0xCD, 0x31, 0xD5, 0xCC, 0x4C, 0x07, 0x01, 0x9E, 0x00, |
|||
0x1A, 0x23, 0x90, 0xBF, 0x88, 0x1E, 0x3B, 0xAB, 0xA6, 0x3E, 0xC4, 0x73, 0x47, 0x10, 0x7E, 0x3B, |
|||
0x5E, 0xBC, 0xE3, 0x00, 0x84, 0xFF, 0x09, 0xD4, 0xE0, 0x89, 0x0F, 0x5B, 0x58, 0x70, 0x4F, 0xFB, |
|||
0x65, 0xD8, 0x5C, 0x53, 0x1B, 0xD3, 0xC8, 0xC6, 0xBF, 0xEF, 0x98, 0xB0, 0x50, 0x4F, 0x0F, 0xEA, |
|||
0xE5, 0x83, 0x58, 0x8C, 0x28, 0x2C, 0x84, 0x67, 0xCD, 0xD0, 0x9E, 0x47, 0xDB, 0x27, 0x50, 0xCA, |
|||
0xF4, 0x63, 0x63, 0xE8, 0x97, 0x7F, 0x1B, 0x4B, 0x0C, 0xC2, 0xC1, 0x21, 0x4C, 0xCC, 0x58, 0xF5, |
|||
0x94, 0x52, 0xA3, 0xF3, 0xD3, 0xE0, 0x68, 0xF4, 0x00, 0x23, 0xF3, 0x5E, 0x0A, 0x7B, 0x93, 0xDD, |
|||
0xAB, 0x12, 0xB2, 0x13, 0xE8, 0x84, 0xD7, 0xA7, 0x9F, 0x0F, 0x32, 0x4C, 0x55, 0x1D, 0x04, 0x36, |
|||
0x52, 0xDC, 0x03, 0xF3, 0xF9, 0x4E, 0x42, 0xE9, 0x3D, 0x61, 0xEF, 0x7C, 0xB6, 0xB3, 0x93, 0x50, |
|||
] |
|||
|
|||
0xb8, 0xd5, 0x3d, 0xb2, 0xe9, 0xaf, 0x78, 0x8c, 0x83, 0x33, 0x71, 0x51, 0x76, 0xa0, 0xcd, 0x37, 0x2f, 0x3e, 0x35, |
|||
0x8d, 0xa9, 0xbe, 0x98, 0xb7, 0xe7, 0x8c, 0x22, 0xce, 0x5a, 0x61, 0xdf, 0x68, 0x69, 0x89, 0xfe, 0xa5, 0xb6, 0xde, |
|||
0xa9, 0x77, 0xfc, 0xc8, 0xbd, 0xbd, 0xe5, 0x6d, 0x3e, 0x5a, 0x36, 0xef, 0x69, 0x4e, 0xbe, 0xe1, 0xe9, 0x66, 0x1c, |
|||
0xf3, 0xd9, 0x02, 0xb6, 0xf2, 0x12, 0x9b, 0x44, 0xd0, 0x6f, 0xb9, 0x35, 0x89, 0xb6, 0x46, 0x6d, 0x73, 0x82, 0x06, |
|||
0x69, 0xc1, 0xed, 0xd7, 0x85, 0xc2, 0x30, 0xdf, 0xa2, 0x62, 0xbe, 0x79, 0x2d, 0x62, 0x62, 0x3d, 0x0d, 0x7e, 0xbe, |
|||
0x48, 0x89, 0x23, 0x02, 0xa0, 0xe4, 0xd5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xfd, 0x16, 0x3a, 0x21, 0x3b, 0x16, 0x0f, |
|||
0xc3, 0xb2, 0xbb, 0xb3, 0xe2, 0xba, 0x3a, 0x3d, 0x13, 0xec, 0xf6, 0x01, 0x45, 0x84, 0xa5, 0x70, 0x0f, 0x93, 0x49, |
|||
0x0c, 0x64, 0xcd, 0x31, 0xd5, 0xcc, 0x4c, 0x07, 0x01, 0x9e, 0x00, 0x1a, 0x23, 0x90, 0xbf, 0x88, 0x1e, 0x3b, 0xab, |
|||
0xa6, 0x3e, 0xc4, 0x73, 0x47, 0x10, 0x7e, 0x3b, 0x5e, 0xbc, 0xe3, 0x00, 0x84, 0xff, 0x09, 0xd4, 0xe0, 0x89, 0x0f, |
|||
0x5b, 0x58, 0x70, 0x4f, 0xfb, 0x65, 0xd8, 0x5c, 0x53, 0x1b, 0xd3, 0xc8, 0xc6, 0xbf, 0xef, 0x98, 0xb0, 0x50, 0x4f, |
|||
0x0f, 0xea, 0xe5, 0x83, 0x58, 0x8c, 0x28, 0x2c, 0x84, 0x67, 0xcd, 0xd0, 0x9e, 0x47, 0xdb, 0x27, 0x50, 0xca, 0xf4, |
|||
0x63, 0x63, 0xe8, 0x97, 0x7f, 0x1b, 0x4b, 0x0c, 0xc2, 0xc1, 0x21, 0x4c, 0xcc, 0x58, 0xf5, 0x94, 0x52, 0xa3, 0xf3, |
|||
0xd3, 0xe0, 0x68, 0xf4, 0x00, 0x23, 0xf3, 0x5e, 0x0a, 0x7b, 0x93, 0xdd, 0xab, 0x12, 0xb2, 0x13, 0xe8, 0x84, 0xd7, |
|||
0xa7, 0x9f, 0x0f, 0x32, 0x4c, 0x55, 0x1d, 0x04, 0x36, 0x52, 0xdc, 0x03, 0xf3, 0xf9, 0x4e, 0x42, 0xe9, 0x3d, 0x61, |
|||
0xef, 0x7c, 0xb6, 0xb3, 0x93, 0x50, |
|||
]; |
|||
|
@ -1,77 +1,74 @@ |
|||
import { |
|||
AudioMimeType, |
|||
BytesHasPrefix, |
|||
GetArrayBuffer, |
|||
GetCoverFromFile, |
|||
GetMetaFromFile, |
|||
SniffAudioExt |
|||
} from "@/decrypt/utils"; |
|||
import {Decrypt as RawDecrypt} from "@/decrypt/raw"; |
|||
AudioMimeType, |
|||
BytesHasPrefix, |
|||
GetArrayBuffer, |
|||
GetCoverFromFile, |
|||
GetMetaFromFile, |
|||
SniffAudioExt, |
|||
} from '@/decrypt/utils'; |
|||
import { Decrypt as RawDecrypt } from '@/decrypt/raw'; |
|||
|
|||
import {parseBlob as metaParseBlob} from "music-metadata-browser"; |
|||
import {DecryptResult} from "@/decrypt/entity"; |
|||
import { parseBlob as metaParseBlob } from 'music-metadata-browser'; |
|||
import { DecryptResult } from '@/decrypt/entity'; |
|||
|
|||
//prettier-ignore
|
|||
const MagicHeader = [ |
|||
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D, |
|||
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65, |
|||
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D, |
|||
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65, |
|||
] |
|||
const PreDefinedKey = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk" |
|||
const PreDefinedKey = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk'; |
|||
|
|||
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> { |
|||
const oriData = new Uint8Array(await GetArrayBuffer(file)); |
|||
if (!BytesHasPrefix(oriData, MagicHeader)) { |
|||
if (SniffAudioExt(oriData) === "aac") { |
|||
return await RawDecrypt(file, raw_filename, "aac", false) |
|||
} |
|||
throw Error("not a valid kwm file") |
|||
const oriData = new Uint8Array(await GetArrayBuffer(file)); |
|||
if (!BytesHasPrefix(oriData, MagicHeader)) { |
|||
if (SniffAudioExt(oriData) === 'aac') { |
|||
return await RawDecrypt(file, raw_filename, 'aac', false); |
|||
} |
|||
throw Error('not a valid kwm file'); |
|||
} |
|||
|
|||
let fileKey = oriData.slice(0x18, 0x20) |
|||
let mask = createMaskFromKey(fileKey) |
|||
let audioData = oriData.slice(0x400); |
|||
let lenAudioData = audioData.length; |
|||
for (let cur = 0; cur < lenAudioData; ++cur) |
|||
audioData[cur] ^= mask[cur % 0x20]; |
|||
let fileKey = oriData.slice(0x18, 0x20); |
|||
let mask = createMaskFromKey(fileKey); |
|||
let audioData = oriData.slice(0x400); |
|||
let lenAudioData = audioData.length; |
|||
for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= mask[cur % 0x20]; |
|||
|
|||
const ext = SniffAudioExt(audioData); |
|||
const mime = AudioMimeType[ext]; |
|||
let musicBlob = new Blob([audioData], { type: mime }); |
|||
|
|||
const ext = SniffAudioExt(audioData); |
|||
const mime = AudioMimeType[ext]; |
|||
let musicBlob = new Blob([audioData], {type: mime}); |
|||
|
|||
const musicMeta = await metaParseBlob(musicBlob); |
|||
const {title, artist} = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist) |
|||
return { |
|||
album: musicMeta.common.album, |
|||
picture: GetCoverFromFile(musicMeta), |
|||
file: URL.createObjectURL(musicBlob), |
|||
blob: musicBlob, |
|||
mime, |
|||
title, |
|||
artist, |
|||
ext |
|||
} |
|||
const musicMeta = await metaParseBlob(musicBlob); |
|||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist); |
|||
return { |
|||
album: musicMeta.common.album, |
|||
picture: GetCoverFromFile(musicMeta), |
|||
file: URL.createObjectURL(musicBlob), |
|||
blob: musicBlob, |
|||
mime, |
|||
title, |
|||
artist, |
|||
ext, |
|||
}; |
|||
} |
|||
|
|||
|
|||
function createMaskFromKey(keyBytes: Uint8Array): Uint8Array { |
|||
let keyView = new DataView(keyBytes.buffer) |
|||
let keyStr = keyView.getBigUint64(0, true).toString() |
|||
let keyStrTrim = trimKey(keyStr) |
|||
let key = new Uint8Array(32) |
|||
for (let i = 0; i < 32; i++) { |
|||
key[i] = PreDefinedKey.charCodeAt(i) ^ keyStrTrim.charCodeAt(i) |
|||
} |
|||
return key |
|||
let keyView = new DataView(keyBytes.buffer); |
|||
let keyStr = keyView.getBigUint64(0, true).toString(); |
|||
let keyStrTrim = trimKey(keyStr); |
|||
let key = new Uint8Array(32); |
|||
for (let i = 0; i < 32; i++) { |
|||
key[i] = PreDefinedKey.charCodeAt(i) ^ keyStrTrim.charCodeAt(i); |
|||
} |
|||
return key; |
|||
} |
|||
|
|||
|
|||
function trimKey(keyRaw: string): string { |
|||
let lenRaw = keyRaw.length; |
|||
let out = keyRaw; |
|||
if (lenRaw > 32) { |
|||
out = keyRaw.slice(0, 32) |
|||
} else if (lenRaw < 32) { |
|||
out = keyRaw.padEnd(32, keyRaw) |
|||
} |
|||
return out |
|||
let lenRaw = keyRaw.length; |
|||
let out = keyRaw; |
|||
if (lenRaw > 32) { |
|||
out = keyRaw.slice(0, 32); |
|||
} else if (lenRaw < 32) { |
|||
out = keyRaw.padEnd(32, keyRaw); |
|||
} |
|||
return out; |
|||
} |
|||
|
@ -1,29 +1,28 @@ |
|||
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils"; |
|||
import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils'; |
|||
|
|||
import {DecryptResult} from "@/decrypt/entity"; |
|||
import { DecryptResult } from '@/decrypt/entity'; |
|||
|
|||
import {parseBlob as metaParseBlob} from "music-metadata-browser"; |
|||
import { parseBlob as metaParseBlob } from 'music-metadata-browser'; |
|||
|
|||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) |
|||
: Promise<DecryptResult> { |
|||
const buffer = new Uint8Array(await GetArrayBuffer(file)); |
|||
let length = buffer.length |
|||
for (let i = 0; i < length; i++) { |
|||
buffer[i] ^= 163 |
|||
} |
|||
const ext = SniffAudioExt(buffer, raw_ext); |
|||
if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]}) |
|||
const tag = await metaParseBlob(file); |
|||
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist) |
|||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> { |
|||
const buffer = new Uint8Array(await GetArrayBuffer(file)); |
|||
let length = buffer.length; |
|||
for (let i = 0; i < length; i++) { |
|||
buffer[i] ^= 163; |
|||
} |
|||
const ext = SniffAudioExt(buffer, raw_ext); |
|||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] }); |
|||
const tag = await metaParseBlob(file); |
|||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist); |
|||
|
|||
return { |
|||
title, |
|||
artist, |
|||
ext, |
|||
album: tag.common.album, |
|||
picture: GetCoverFromFile(tag), |
|||
file: URL.createObjectURL(file), |
|||
blob: file, |
|||
mime: AudioMimeType[ext] |
|||
} |
|||
return { |
|||
title, |
|||
artist, |
|||
ext, |
|||
album: tag.common.album, |
|||
picture: GetCoverFromFile(tag), |
|||
file: URL.createObjectURL(file), |
|||
blob: file, |
|||
mime: AudioMimeType[ext], |
|||
}; |
|||
} |
|||
|
@ -1,115 +1,117 @@ |
|||
import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher} from "@/decrypt/qmc_cipher"; |
|||
import fs from 'fs' |
|||
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher'; |
|||
import fs from 'fs'; |
|||
|
|||
test("static cipher [0x7ff8,0x8000) ", () => { |
|||
test('static cipher [0x7ff8,0x8000) ', () => { |
|||
//prettier-ignore
|
|||
const expected = new Uint8Array([ |
|||
0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A, |
|||
0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8, |
|||
]) |
|||
|
|||
const c = new QmcStaticCipher() |
|||
const buf = new Uint8Array(16) |
|||
c.decrypt(buf, 0x7ff8) |
|||
const c = new QmcStaticCipher(); |
|||
const buf = new Uint8Array(16); |
|||
c.decrypt(buf, 0x7ff8); |
|||
|
|||
expect(buf).toStrictEqual(expected) |
|||
}) |
|||
expect(buf).toStrictEqual(expected); |
|||
}); |
|||
|
|||
test("static cipher [0,0x10) ", () => { |
|||
test('static cipher [0,0x10) ', () => { |
|||
//prettier-ignore
|
|||
const expected = new Uint8Array([ |
|||
0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, |
|||
0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00, |
|||
]) |
|||
|
|||
const c = new QmcStaticCipher() |
|||
const buf = new Uint8Array(16) |
|||
c.decrypt(buf, 0) |
|||
const c = new QmcStaticCipher(); |
|||
const buf = new Uint8Array(16); |
|||
c.decrypt(buf, 0); |
|||
|
|||
expect(buf).toStrictEqual(expected) |
|||
}) |
|||
expect(buf).toStrictEqual(expected); |
|||
}); |
|||
|
|||
|
|||
test("map cipher: get mask", () => { |
|||
test('map cipher: get mask', () => { |
|||
//prettier-ignore
|
|||
const expected = new Uint8Array([ |
|||
0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB, |
|||
0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79, |
|||
]) |
|||
const key = new Uint8Array(256) |
|||
for (let i = 0; i < 256; i++) key[i] = i |
|||
const buf = new Uint8Array(16) |
|||
const key = new Uint8Array(256); |
|||
for (let i = 0; i < 256; i++) key[i] = i; |
|||
const buf = new Uint8Array(16); |
|||
|
|||
const c = new QmcMapCipher(key) |
|||
c.decrypt(buf, 0) |
|||
expect(buf).toStrictEqual(expected) |
|||
}) |
|||
const c = new QmcMapCipher(key); |
|||
c.decrypt(buf, 0); |
|||
expect(buf).toStrictEqual(expected); |
|||
}); |
|||
|
|||
function loadTestDataCipher(name: string): { |
|||
key: Uint8Array, |
|||
cipherText: Uint8Array, |
|||
clearText: Uint8Array |
|||
key: Uint8Array; |
|||
cipherText: Uint8Array; |
|||
clearText: Uint8Array; |
|||
} { |
|||
return { |
|||
key: fs.readFileSync(`testdata/${name}_key.bin`), |
|||
cipherText: fs.readFileSync(`testdata/${name}_raw.bin`), |
|||
clearText: fs.readFileSync(`testdata/${name}_target.bin`) |
|||
} |
|||
clearText: fs.readFileSync(`testdata/${name}_target.bin`), |
|||
}; |
|||
} |
|||
|
|||
test("map cipher: real file", async () => { |
|||
const cases = ["mflac_map", "mgg_map"] |
|||
test('map cipher: real file', async () => { |
|||
const cases = ['mflac_map', 'mgg_map']; |
|||
for (const name of cases) { |
|||
const {key, clearText, cipherText} = loadTestDataCipher(name) |
|||
const c = new QmcMapCipher(key) |
|||
const { key, clearText, cipherText } = loadTestDataCipher(name); |
|||
const c = new QmcMapCipher(key); |
|||
|
|||
c.decrypt(cipherText, 0) |
|||
c.decrypt(cipherText, 0); |
|||
|
|||
expect(cipherText).toStrictEqual(clearText) |
|||
expect(cipherText).toStrictEqual(clearText); |
|||
} |
|||
}) |
|||
}); |
|||
|
|||
test("rc4 cipher: real file", async () => { |
|||
const cases = ["mflac0_rc4"] |
|||
test('rc4 cipher: real file', async () => { |
|||
const cases = ['mflac0_rc4']; |
|||
for (const name of cases) { |
|||
const {key, clearText, cipherText} = loadTestDataCipher(name) |
|||
const c = new QmcRC4Cipher(key) |
|||
const { key, clearText, cipherText } = loadTestDataCipher(name); |
|||
const c = new QmcRC4Cipher(key); |
|||
|
|||
c.decrypt(cipherText, 0) |
|||
c.decrypt(cipherText, 0); |
|||
|
|||
expect(cipherText).toStrictEqual(clearText) |
|||
expect(cipherText).toStrictEqual(clearText); |
|||
} |
|||
}) |
|||
}); |
|||
|
|||
test("rc4 cipher: first segment", async () => { |
|||
const cases = ["mflac0_rc4"] |
|||
test('rc4 cipher: first segment', async () => { |
|||
const cases = ['mflac0_rc4']; |
|||
for (const name of cases) { |
|||
const {key, clearText, cipherText} = loadTestDataCipher(name) |
|||
const c = new QmcRC4Cipher(key) |
|||
const { key, clearText, cipherText } = loadTestDataCipher(name); |
|||
const c = new QmcRC4Cipher(key); |
|||
|
|||
const buf = cipherText.slice(0, 128) |
|||
c.decrypt(buf, 0) |
|||
expect(buf).toStrictEqual(clearText.slice(0, 128)) |
|||
const buf = cipherText.slice(0, 128); |
|||
c.decrypt(buf, 0); |
|||
expect(buf).toStrictEqual(clearText.slice(0, 128)); |
|||
} |
|||
}) |
|||
}); |
|||
|
|||
test("rc4 cipher: align block (128~5120)", async () => { |
|||
const cases = ["mflac0_rc4"] |
|||
test('rc4 cipher: align block (128~5120)', async () => { |
|||
const cases = ['mflac0_rc4']; |
|||
for (const name of cases) { |
|||
const {key, clearText, cipherText} = loadTestDataCipher(name) |
|||
const c = new QmcRC4Cipher(key) |
|||
const { key, clearText, cipherText } = loadTestDataCipher(name); |
|||
const c = new QmcRC4Cipher(key); |
|||
|
|||
const buf = cipherText.slice(128, 5120) |
|||
c.decrypt(buf, 128) |
|||
expect(buf).toStrictEqual(clearText.slice(128, 5120)) |
|||
const buf = cipherText.slice(128, 5120); |
|||
c.decrypt(buf, 128); |
|||
expect(buf).toStrictEqual(clearText.slice(128, 5120)); |
|||
} |
|||
}) |
|||
}); |
|||
|
|||
test("rc4 cipher: simple block (5120~10240)", async () => { |
|||
const cases = ["mflac0_rc4"] |
|||
test('rc4 cipher: simple block (5120~10240)', async () => { |
|||
const cases = ['mflac0_rc4']; |
|||
for (const name of cases) { |
|||
const {key, clearText, cipherText} = loadTestDataCipher(name) |
|||
const c = new QmcRC4Cipher(key) |
|||
const { key, clearText, cipherText } = loadTestDataCipher(name); |
|||
const c = new QmcRC4Cipher(key); |
|||
|
|||
const buf = cipherText.slice(5120, 10240) |
|||
c.decrypt(buf, 5120) |
|||
expect(buf).toStrictEqual(clearText.slice(5120, 10240)) |
|||
const buf = cipherText.slice(5120, 10240); |
|||
c.decrypt(buf, 5120); |
|||
expect(buf).toStrictEqual(clearText.slice(5120, 10240)); |
|||
} |
|||
}) |
|||
}); |
|||
|
@ -1,30 +1,26 @@ |
|||
import {QmcDeriveKey, simpleMakeKey} from "@/decrypt/qmc_key"; |
|||
import fs from "fs"; |
|||
import { QmcDeriveKey, simpleMakeKey } from '@/decrypt/qmc_key'; |
|||
import fs from 'fs'; |
|||
|
|||
test("key dec: make simple key", () => { |
|||
expect( |
|||
simpleMakeKey(106, 8) |
|||
).toStrictEqual( |
|||
[0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b] |
|||
) |
|||
}) |
|||
test('key dec: make simple key', () => { |
|||
expect(simpleMakeKey(106, 8)).toStrictEqual([0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]); |
|||
}); |
|||
|
|||
function loadTestDataKeyDecrypt(name: string): { |
|||
cipherText: Uint8Array, |
|||
clearText: Uint8Array |
|||
cipherText: Uint8Array; |
|||
clearText: Uint8Array; |
|||
} { |
|||
return { |
|||
cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`), |
|||
clearText: fs.readFileSync(`testdata/${name}_key.bin`) |
|||
} |
|||
clearText: fs.readFileSync(`testdata/${name}_key.bin`), |
|||
}; |
|||
} |
|||
|
|||
test("key dec: real file", async () => { |
|||
const cases = ["mflac_map", "mgg_map", "mflac0_rc4"] |
|||
test('key dec: real file', async () => { |
|||
const cases = ['mflac_map', 'mgg_map', 'mflac0_rc4']; |
|||
for (const name of cases) { |
|||
const {clearText, cipherText} = loadTestDataKeyDecrypt(name) |
|||
const buf = QmcDeriveKey(cipherText) |
|||
const { clearText, cipherText } = loadTestDataKeyDecrypt(name); |
|||
const buf = QmcDeriveKey(cipherText); |
|||
|
|||
expect(buf).toStrictEqual(clearText) |
|||
expect(buf).toStrictEqual(clearText); |
|||
} |
|||
}) |
|||
}); |
|||
|
@ -1,51 +1,50 @@ |
|||
import { |
|||
AudioMimeType, |
|||
GetArrayBuffer, |
|||
GetCoverFromFile, |
|||
GetMetaFromFile, |
|||
SniffAudioExt, |
|||
SplitFilename |
|||
} from "@/decrypt/utils"; |
|||
AudioMimeType, |
|||
GetArrayBuffer, |
|||
GetCoverFromFile, |
|||
GetMetaFromFile, |
|||
SniffAudioExt, |
|||
SplitFilename, |
|||
} from '@/decrypt/utils'; |
|||
|
|||
import {Decrypt as QmcDecrypt, HandlerMap} from "@/decrypt/qmc"; |
|||
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc'; |
|||
|
|||
import {DecryptResult} from "@/decrypt/entity"; |
|||
import { DecryptResult } from '@/decrypt/entity'; |
|||
|
|||
import {parseBlob as metaParseBlob} from "music-metadata-browser"; |
|||
import { parseBlob as metaParseBlob } from 'music-metadata-browser'; |
|||
|
|||
export async function Decrypt(file: Blob, raw_filename: string, _: string) |
|||
: Promise<DecryptResult> { |
|||
const buffer = new Uint8Array(await GetArrayBuffer(file)); |
|||
let length = buffer.length |
|||
for (let i = 0; i < length; i++) { |
|||
buffer[i] ^= 0xf4 |
|||
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4; |
|||
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1; |
|||
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2; |
|||
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3; |
|||
} |
|||
let ext = SniffAudioExt(buffer, ""); |
|||
const newName = SplitFilename(raw_filename) |
|||
let audioBlob: Blob |
|||
if (ext !== "" || newName.ext === "mp3") { |
|||
audioBlob = new Blob([buffer], {type: AudioMimeType[ext]}) |
|||
} else if (newName.ext in HandlerMap) { |
|||
audioBlob = new Blob([buffer], {type: "application/octet-stream"}) |
|||
return QmcDecrypt(audioBlob, newName.name, newName.ext); |
|||
} else { |
|||
throw "不支持的QQ音乐缓存格式" |
|||
} |
|||
const tag = await metaParseBlob(audioBlob); |
|||
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist) |
|||
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> { |
|||
const buffer = new Uint8Array(await GetArrayBuffer(file)); |
|||
let length = buffer.length; |
|||
for (let i = 0; i < length; i++) { |
|||
buffer[i] ^= 0xf4; |
|||
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4; |
|||
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1; |
|||
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2; |
|||
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3; |
|||
} |
|||
let ext = SniffAudioExt(buffer, ''); |
|||
const newName = SplitFilename(raw_filename); |
|||
let audioBlob: Blob; |
|||
if (ext !== '' || newName.ext === 'mp3') { |
|||
audioBlob = new Blob([buffer], { type: AudioMimeType[ext] }); |
|||
} else if (newName.ext in HandlerMap) { |
|||
audioBlob = new Blob([buffer], { type: 'application/octet-stream' }); |
|||
return QmcDecrypt(audioBlob, newName.name, newName.ext); |
|||
} else { |
|||
throw '不支持的QQ音乐缓存格式'; |
|||
} |
|||
const tag = await metaParseBlob(audioBlob); |
|||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist); |
|||
|
|||
return { |
|||
title, |
|||
artist, |
|||
ext, |
|||
album: tag.common.album, |
|||
picture: GetCoverFromFile(tag), |
|||
file: URL.createObjectURL(audioBlob), |
|||
blob: audioBlob, |
|||
mime: AudioMimeType[ext] |
|||
} |
|||
return { |
|||
title, |
|||
artist, |
|||
ext, |
|||
album: tag.common.album, |
|||
picture: GetCoverFromFile(tag), |
|||
file: URL.createObjectURL(audioBlob), |
|||
blob: audioBlob, |
|||
mime: AudioMimeType[ext], |
|||
}; |
|||
} |
|||
|
@ -1,28 +1,32 @@ |
|||
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils"; |
|||
import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils'; |
|||
|
|||
import {DecryptResult} from "@/decrypt/entity"; |
|||
import { DecryptResult } from '@/decrypt/entity'; |
|||
|
|||
import {parseBlob as metaParseBlob} from "music-metadata-browser"; |
|||
import { parseBlob as metaParseBlob } from 'music-metadata-browser'; |
|||
|
|||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string, detect: boolean = true) |
|||
: Promise<DecryptResult> { |
|||
let ext = raw_ext; |
|||
if (detect) { |
|||
const buffer = new Uint8Array(await GetArrayBuffer(file)); |
|||
ext = SniffAudioExt(buffer, raw_ext); |
|||
if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]}) |
|||
} |
|||
const tag = await metaParseBlob(file); |
|||
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist) |
|||
export async function Decrypt( |
|||
file: Blob, |
|||
raw_filename: string, |
|||
raw_ext: string, |
|||
detect: boolean = true, |
|||
): Promise<DecryptResult> { |
|||
let ext = raw_ext; |
|||
if (detect) { |
|||
const buffer = new Uint8Array(await GetArrayBuffer(file)); |
|||
ext = SniffAudioExt(buffer, raw_ext); |
|||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] }); |
|||
} |
|||
const tag = await metaParseBlob(file); |
|||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist); |
|||
|
|||
return { |
|||
title, |
|||
artist, |
|||
ext, |
|||
album: tag.common.album, |
|||
picture: GetCoverFromFile(tag), |
|||
file: URL.createObjectURL(file), |
|||
blob: file, |
|||
mime: AudioMimeType[ext] |
|||
} |
|||
return { |
|||
title, |
|||
artist, |
|||
ext, |
|||
album: tag.common.album, |
|||
picture: GetCoverFromFile(tag), |
|||
file: URL.createObjectURL(file), |
|||
blob: file, |
|||
mime: AudioMimeType[ext], |
|||
}; |
|||
} |
|||
|
@ -1,14 +1,14 @@ |
|||
import {Decrypt as RawDecrypt} from "./raw"; |
|||
import {GetArrayBuffer} from "@/decrypt/utils"; |
|||
import {DecryptResult} from "@/decrypt/entity"; |
|||
import { Decrypt as RawDecrypt } from './raw'; |
|||
import { GetArrayBuffer } from '@/decrypt/utils'; |
|||
import { DecryptResult } from '@/decrypt/entity'; |
|||
|
|||
const TM_HEADER = [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70]; |
|||
|
|||
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> { |
|||
const audioData = new Uint8Array(await GetArrayBuffer(file)); |
|||
for (let cur = 0; cur < 8; ++cur) { |
|||
audioData[cur] = TM_HEADER[cur]; |
|||
} |
|||
const musicData = new Blob([audioData], {type: "audio/mp4"}); |
|||
return await RawDecrypt(musicData, raw_filename, "m4a", false) |
|||
const audioData = new Uint8Array(await GetArrayBuffer(file)); |
|||
for (let cur = 0; cur < 8; ++cur) { |
|||
audioData[cur] = TM_HEADER[cur]; |
|||
} |
|||
const musicData = new Blob([audioData], { type: 'audio/mp4' }); |
|||
return await RawDecrypt(musicData, raw_filename, 'm4a', false); |
|||
} |
|||
|
@ -1,177 +1,176 @@ |
|||
import {IAudioMetadata} from "music-metadata-browser"; |
|||
import ID3Writer from "browser-id3-writer"; |
|||
import MetaFlac from "metaflac-js"; |
|||
import { IAudioMetadata } from 'music-metadata-browser'; |
|||
import ID3Writer from 'browser-id3-writer'; |
|||
import MetaFlac from 'metaflac-js'; |
|||
|
|||
export const FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43]; |
|||
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43]; |
|||
export const MP3_HEADER = [0x49, 0x44, 0x33]; |
|||
export const OGG_HEADER = [0x4F, 0x67, 0x67, 0x53]; |
|||
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53]; |
|||
export const M4A_HEADER = [0x66, 0x74, 0x79, 0x70]; |
|||
export const WMA_HEADER = [ |
|||
0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, |
|||
0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C, |
|||
] |
|||
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46] |
|||
export const AAC_HEADER = [0xFF, 0xF1] |
|||
export const DFF_HEADER = [0x46, 0x52, 0x4D, 0x38] |
|||
0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c, |
|||
]; |
|||
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46]; |
|||
export const AAC_HEADER = [0xff, 0xf1]; |
|||
export const DFF_HEADER = [0x46, 0x52, 0x4d, 0x38]; |
|||
|
|||
export const AudioMimeType: { [key: string]: string } = { |
|||
mp3: "audio/mpeg", |
|||
flac: "audio/flac", |
|||
m4a: "audio/mp4", |
|||
ogg: "audio/ogg", |
|||
wma: "audio/x-ms-wma", |
|||
wav: "audio/x-wav", |
|||
dff: "audio/x-dff" |
|||
mp3: 'audio/mpeg', |
|||
flac: 'audio/flac', |
|||
m4a: 'audio/mp4', |
|||
ogg: 'audio/ogg', |
|||
wma: 'audio/x-ms-wma', |
|||
wav: 'audio/x-wav', |
|||
dff: 'audio/x-dff', |
|||
}; |
|||
|
|||
|
|||
export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean { |
|||
if (prefix.length > data.length) return false |
|||
return prefix.every((val, idx) => { |
|||
return val === data[idx]; |
|||
}) |
|||
if (prefix.length > data.length) return false; |
|||
return prefix.every((val, idx) => { |
|||
return val === data[idx]; |
|||
}); |
|||
} |
|||
|
|||
export function BytesEqual(a: Uint8Array, b: Uint8Array,): boolean { |
|||
if (a.length !== b.length) return false |
|||
return a.every((val, idx) => { |
|||
return val === b[idx]; |
|||
}) |
|||
export function BytesEqual(a: Uint8Array, b: Uint8Array): boolean { |
|||
if (a.length !== b.length) return false; |
|||
return a.every((val, idx) => { |
|||
return val === b[idx]; |
|||
}); |
|||
} |
|||
|
|||
|
|||
export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): string { |
|||
if (BytesHasPrefix(data, MP3_HEADER)) return "mp3" |
|||
if (BytesHasPrefix(data, FLAC_HEADER)) return "flac" |
|||
if (BytesHasPrefix(data, OGG_HEADER)) return "ogg" |
|||
if (data.length >= 4 + M4A_HEADER.length && |
|||
BytesHasPrefix(data.slice(4), M4A_HEADER)) return "m4a" |
|||
if (BytesHasPrefix(data, WAV_HEADER)) return "wav" |
|||
if (BytesHasPrefix(data, WMA_HEADER)) return "wma" |
|||
if (BytesHasPrefix(data, AAC_HEADER)) return "aac" |
|||
if (BytesHasPrefix(data, DFF_HEADER)) return "dff" |
|||
return fallback_ext; |
|||
export function SniffAudioExt(data: Uint8Array, fallback_ext: string = 'mp3'): string { |
|||
if (BytesHasPrefix(data, MP3_HEADER)) return 'mp3'; |
|||
if (BytesHasPrefix(data, FLAC_HEADER)) return 'flac'; |
|||
if (BytesHasPrefix(data, OGG_HEADER)) return 'ogg'; |
|||
if (data.length >= 4 + M4A_HEADER.length && BytesHasPrefix(data.slice(4), M4A_HEADER)) return 'm4a'; |
|||
if (BytesHasPrefix(data, WAV_HEADER)) return 'wav'; |
|||
if (BytesHasPrefix(data, WMA_HEADER)) return 'wma'; |
|||
if (BytesHasPrefix(data, AAC_HEADER)) return 'aac'; |
|||
if (BytesHasPrefix(data, DFF_HEADER)) return 'dff'; |
|||
return fallback_ext; |
|||
} |
|||
|
|||
export function GetArrayBuffer(obj: Blob): Promise<ArrayBuffer> { |
|||
if (!!obj.arrayBuffer) return obj.arrayBuffer() |
|||
return new Promise((resolve, reject) => { |
|||
const reader = new FileReader(); |
|||
reader.onload = (e) => { |
|||
const rs = e.target?.result |
|||
if (!rs) { |
|||
reject("read file failed") |
|||
} else { |
|||
resolve(rs as ArrayBuffer) |
|||
} |
|||
}; |
|||
reader.readAsArrayBuffer(obj); |
|||
}); |
|||
if (!!obj.arrayBuffer) return obj.arrayBuffer(); |
|||
return new Promise((resolve, reject) => { |
|||
const reader = new FileReader(); |
|||
reader.onload = (e) => { |
|||
const rs = e.target?.result; |
|||
if (!rs) { |
|||
reject('read file failed'); |
|||
} else { |
|||
resolve(rs as ArrayBuffer); |
|||
} |
|||
}; |
|||
reader.readAsArrayBuffer(obj); |
|||
}); |
|||
} |
|||
|
|||
export function GetCoverFromFile(metadata: IAudioMetadata): string { |
|||
if (metadata.common?.picture && metadata.common.picture.length > 0) { |
|||
return URL.createObjectURL(new Blob( |
|||
[metadata.common.picture[0].data], |
|||
{type: metadata.common.picture[0].format} |
|||
)); |
|||
} |
|||
return ""; |
|||
if (metadata.common?.picture && metadata.common.picture.length > 0) { |
|||
return URL.createObjectURL( |
|||
new Blob([metadata.common.picture[0].data], { type: metadata.common.picture[0].format }), |
|||
); |
|||
} |
|||
return ''; |
|||
} |
|||
|
|||
export interface IMusicMetaBasic { |
|||
title: string |
|||
artist?: string |
|||
title: string; |
|||
artist?: string; |
|||
} |
|||
|
|||
export function GetMetaFromFile(filename: string, exist_title?: string, exist_artist?: string, separator = "-") |
|||
: IMusicMetaBasic { |
|||
const meta: IMusicMetaBasic = {title: exist_title ?? "", artist: exist_artist} |
|||
|
|||
const items = filename.split(separator); |
|||
if (items.length > 1) { |
|||
if (!meta.artist) meta.artist = items[0].trim(); |
|||
if (!meta.title) meta.title = items[1].trim(); |
|||
} else if (items.length === 1) { |
|||
if (!meta.title) meta.title = items[0].trim(); |
|||
} |
|||
return meta |
|||
export function GetMetaFromFile( |
|||
filename: string, |
|||
exist_title?: string, |
|||
exist_artist?: string, |
|||
separator = '-', |
|||
): IMusicMetaBasic { |
|||
const meta: IMusicMetaBasic = { title: exist_title ?? '', artist: exist_artist }; |
|||
|
|||
const items = filename.split(separator); |
|||
if (items.length > 1) { |
|||
if (!meta.artist) meta.artist = items[0].trim(); |
|||
if (!meta.title) meta.title = items[1].trim(); |
|||
} else if (items.length === 1) { |
|||
if (!meta.title) meta.title = items[0].trim(); |
|||
} |
|||
return meta; |
|||
} |
|||
|
|||
export async function GetImageFromURL(src: string): |
|||
Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> { |
|||
try { |
|||
const resp = await fetch(src); |
|||
const mime = resp.headers.get("Content-Type"); |
|||
if (mime?.startsWith("image/")) { |
|||
const buffer = await resp.arrayBuffer(); |
|||
const url = URL.createObjectURL(new Blob([buffer], {type: mime})) |
|||
return {buffer, url, mime} |
|||
} |
|||
} catch (e) { |
|||
console.warn(e) |
|||
export async function GetImageFromURL( |
|||
src: string, |
|||
): Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> { |
|||
try { |
|||
const resp = await fetch(src); |
|||
const mime = resp.headers.get('Content-Type'); |
|||
if (mime?.startsWith('image/')) { |
|||
const buffer = await resp.arrayBuffer(); |
|||
const url = URL.createObjectURL(new Blob([buffer], { type: mime })); |
|||
return { buffer, url, mime }; |
|||
} |
|||
} catch (e) { |
|||
console.warn(e); |
|||
} |
|||
} |
|||
|
|||
|
|||
export interface IMusicMeta { |
|||
title: string |
|||
artists?: string[] |
|||
album?: string |
|||
picture?: ArrayBuffer |
|||
picture_desc?: string |
|||
title: string; |
|||
artists?: string[]; |
|||
album?: string; |
|||
picture?: ArrayBuffer; |
|||
picture_desc?: string; |
|||
} |
|||
|
|||
export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) { |
|||
const writer = new ID3Writer(audioData); |
|||
|
|||
// reserve original data
|
|||
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [] |
|||
frames.forEach(frame => { |
|||
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') { |
|||
try { |
|||
writer.setFrame(frame.id, frame.value) |
|||
} catch (e) { |
|||
} |
|||
} |
|||
}) |
|||
|
|||
const old = original.common |
|||
writer.setFrame('TPE1', old?.artists || info.artists || []) |
|||
.setFrame('TIT2', old?.title || info.title) |
|||
.setFrame('TALB', old?.album || info.album || ""); |
|||
if (info.picture) { |
|||
writer.setFrame('APIC', { |
|||
type: 3, |
|||
data: info.picture, |
|||
description: info.picture_desc || "Cover", |
|||
}) |
|||
const writer = new ID3Writer(audioData); |
|||
|
|||
// reserve original data
|
|||
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || []; |
|||
frames.forEach((frame) => { |
|||
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') { |
|||
try { |
|||
writer.setFrame(frame.id, frame.value); |
|||
} catch (e) {} |
|||
} |
|||
return writer.addTag(); |
|||
}); |
|||
|
|||
const old = original.common; |
|||
writer |
|||
.setFrame('TPE1', old?.artists || info.artists || []) |
|||
.setFrame('TIT2', old?.title || info.title) |
|||
.setFrame('TALB', old?.album || info.album || ''); |
|||
if (info.picture) { |
|||
writer.setFrame('APIC', { |
|||
type: 3, |
|||
data: info.picture, |
|||
description: info.picture_desc || 'Cover', |
|||
}); |
|||
} |
|||
return writer.addTag(); |
|||
} |
|||
|
|||
export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) { |
|||
const writer = new MetaFlac(audioData) |
|||
const old = original.common |
|||
if (!old.title && !old.album && old.artists) { |
|||
writer.setTag("TITLE=" + info.title) |
|||
writer.setTag("ALBUM=" + info.album) |
|||
if (info.artists) { |
|||
writer.removeTag("ARTIST") |
|||
info.artists.forEach(artist => writer.setTag("ARTIST=" + artist)) |
|||
} |
|||
const writer = new MetaFlac(audioData); |
|||
const old = original.common; |
|||
if (!old.title && !old.album && old.artists) { |
|||
writer.setTag('TITLE=' + info.title); |
|||
writer.setTag('ALBUM=' + info.album); |
|||
if (info.artists) { |
|||
writer.removeTag('ARTIST'); |
|||
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist)); |
|||
} |
|||
} |
|||
|
|||
if (info.picture) { |
|||
writer.importPictureFromBuffer(Buffer.from(info.picture)) |
|||
} |
|||
return writer.save() |
|||
if (info.picture) { |
|||
writer.importPictureFromBuffer(Buffer.from(info.picture)); |
|||
} |
|||
return writer.save(); |
|||
} |
|||
|
|||
export function SplitFilename(n: string): { name: string; ext: string } { |
|||
const pos = n.lastIndexOf(".") |
|||
return { |
|||
ext: n.substring(pos + 1).toLowerCase(), |
|||
name: n.substring(0, pos) |
|||
} |
|||
const pos = n.lastIndexOf('.'); |
|||
return { |
|||
ext: n.substring(pos + 1).toLowerCase(), |
|||
name: n.substring(0, pos), |
|||
}; |
|||
} |
|||
|
@ -1,5 +1,2 @@ |
|||
const bs = chrome || browser |
|||
bs.tabs.create({ |
|||
url: bs.runtime.getURL('./index.html') |
|||
}, tab => console.log(tab)) |
|||
|
|||
const bs = chrome || browser; |
|||
bs.tabs.create({ url: bs.runtime.getURL('./index.html') }, (tab) => console.log(tab)); |
|||
|
@ -1,31 +1,30 @@ |
|||
/* eslint-disable no-console */ |
|||
|
|||
import {register} from 'register-service-worker' |
|||
import { register } from 'register-service-worker'; |
|||
|
|||
if (process.env.NODE_ENV === 'production' && window.location.protocol === "https:") { |
|||
|
|||
register(`${process.env.BASE_URL}service-worker.js`, { |
|||
ready() { |
|||
console.log('App is being served from cache by a service worker.') |
|||
}, |
|||
registered() { |
|||
console.log('Service worker has been registered.') |
|||
}, |
|||
cached() { |
|||
console.log('Content has been cached for offline use.') |
|||
}, |
|||
updatefound() { |
|||
console.log('New content is downloading.') |
|||
}, |
|||
updated() { |
|||
console.log('New content is available.'); |
|||
window.location.reload(); |
|||
}, |
|||
offline() { |
|||
console.log('No internet connection found. App is running in offline mode.') |
|||
}, |
|||
error(error) { |
|||
console.error('Error during service worker registration:', error) |
|||
} |
|||
}) |
|||
if (process.env.NODE_ENV === 'production' && window.location.protocol === 'https:') { |
|||
register(`${process.env.BASE_URL}service-worker.js`, { |
|||
ready() { |
|||
console.log('App is being served from cache by a service worker.'); |
|||
}, |
|||
registered() { |
|||
console.log('Service worker has been registered.'); |
|||
}, |
|||
cached() { |
|||
console.log('Content has been cached for offline use.'); |
|||
}, |
|||
updatefound() { |
|||
console.log('New content is downloading.'); |
|||
}, |
|||
updated() { |
|||
console.log('New content is available.'); |
|||
window.location.reload(); |
|||
}, |
|||
offline() { |
|||
console.log('No internet connection found. App is running in offline mode.'); |
|||
}, |
|||
error(error) { |
|||
console.error('Error during service worker registration:', error); |
|||
}, |
|||
}); |
|||
} |
|||
|
@ -1,17 +1,15 @@ |
|||
import Vue, {VNode} from 'vue' |
|||
import Vue, { VNode } from 'vue'; |
|||
|
|||
declare global { |
|||
namespace JSX { |
|||
// tslint:disable no-empty-interface
|
|||
interface Element extends VNode { |
|||
} |
|||
interface Element extends VNode {} |
|||
|
|||
// tslint:disable no-empty-interface
|
|||
interface ElementClass extends Vue { |
|||
} |
|||
interface ElementClass extends Vue {} |
|||
|
|||
interface IntrinsicElements { |
|||
[elem: string]: any |
|||
[elem: string]: any; |
|||
} |
|||
} |
|||
} |
|||
|
@ -1,4 +1,4 @@ |
|||
declare module '*.vue' { |
|||
import Vue from 'vue' |
|||
export default Vue |
|||
import Vue from 'vue'; |
|||
export default Vue; |
|||
} |
|||
|
@ -1,56 +1,73 @@ |
|||
import {fromByteArray as Base64Encode} from "base64-js"; |
|||
import { fromByteArray as Base64Encode } from 'base64-js'; |
|||
|
|||
export const IXAREA_API_ENDPOINT = "https://um-api.ixarea.com" |
|||
export const IXAREA_API_ENDPOINT = 'https://um-api.ixarea.com'; |
|||
|
|||
export interface UpdateInfo { |
|||
Found: boolean |
|||
HttpsFound: boolean |
|||
Version: string |
|||
URL: string |
|||
Detail: string |
|||
Found: boolean; |
|||
HttpsFound: boolean; |
|||
Version: string; |
|||
URL: string; |
|||
Detail: string; |
|||
} |
|||
|
|||
export async function checkUpdate(version: string): Promise<UpdateInfo> { |
|||
const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", { |
|||
method: "POST", |
|||
headers: {"Content-Type": "application/json"}, |
|||
body: JSON.stringify({"Version": version}) |
|||
}); |
|||
return await resp.json(); |
|||
const resp = await fetch(IXAREA_API_ENDPOINT + '/music/app-version', { |
|||
method: 'POST', |
|||
headers: { 'Content-Type': 'application/json' }, |
|||
body: JSON.stringify({ Version: version }), |
|||
}); |
|||
return await resp.json(); |
|||
} |
|||
|
|||
export function reportKeyUsage(keyData: Uint8Array, maskData: number[], filename: string, format: string, title: string, artist?: string, album?: string) { |
|||
return fetch(IXAREA_API_ENDPOINT + "/qmcmask/usage", { |
|||
method: "POST", |
|||
headers: {"Content-Type": "application/json"}, |
|||
body: JSON.stringify({ |
|||
Mask: Base64Encode(new Uint8Array(maskData)), Key: Base64Encode(keyData), |
|||
Artist: artist, Title: title, Album: album, Filename: filename, Format: format |
|||
}), |
|||
}) |
|||
export function reportKeyUsage( |
|||
keyData: Uint8Array, |
|||
maskData: number[], |
|||
filename: string, |
|||
format: string, |
|||
title: string, |
|||
artist?: string, |
|||
album?: string, |
|||
) { |
|||
return fetch(IXAREA_API_ENDPOINT + '/qmcmask/usage', { |
|||
method: 'POST', |
|||
headers: { 'Content-Type': 'application/json' }, |
|||
body: JSON.stringify({ |
|||
Mask: Base64Encode(new Uint8Array(maskData)), |
|||
Key: Base64Encode(keyData), |
|||
Artist: artist, |
|||
Title: title, |
|||
Album: album, |
|||
Filename: filename, |
|||
Format: format, |
|||
}), |
|||
}); |
|||
} |
|||
|
|||
interface KeyInfo { |
|||
Matrix44: string |
|||
Matrix44: string; |
|||
} |
|||
|
|||
export async function queryKeyInfo(keyData: Uint8Array, filename: string, format: string): Promise<KeyInfo> { |
|||
const resp = await fetch(IXAREA_API_ENDPOINT + "/qmcmask/query", { |
|||
method: "POST", |
|||
headers: {"Content-Type": "application/json"}, |
|||
body: JSON.stringify({Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44}), |
|||
}); |
|||
return await resp.json(); |
|||
const resp = await fetch(IXAREA_API_ENDPOINT + '/qmcmask/query', { |
|||
method: 'POST', |
|||
headers: { 'Content-Type': 'application/json' }, |
|||
body: JSON.stringify({ Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44 }), |
|||
}); |
|||
return await resp.json(); |
|||
} |
|||
|
|||
export interface CoverInfo { |
|||
Id: string |
|||
Type: number |
|||
Id: string; |
|||
Type: number; |
|||
} |
|||
|
|||
export async function queryAlbumCover(title: string, artist?: string, album?: string): Promise<CoverInfo> { |
|||
const endpoint = IXAREA_API_ENDPOINT + "/music/qq-cover" |
|||
const params = new URLSearchParams([["Title", title], ["Artist", artist ?? ""], ["Album", album ?? ""]]) |
|||
const resp = await fetch(`${endpoint}?${params.toString()}`) |
|||
return await resp.json() |
|||
const endpoint = IXAREA_API_ENDPOINT + '/music/qq-cover'; |
|||
const params = new URLSearchParams([ |
|||
['Title', title], |
|||
['Artist', artist ?? ''], |
|||
['Album', album ?? ''], |
|||
]); |
|||
const resp = await fetch(`${endpoint}?${params.toString()}`); |
|||
return await resp.json(); |
|||
} |
|||
|
@ -1,79 +1,80 @@ |
|||
import {DecryptResult} from "@/decrypt/entity"; |
|||
import {FileSystemDirectoryHandle} from "@/shims-fs"; |
|||
import { DecryptResult } from '@/decrypt/entity'; |
|||
import { FileSystemDirectoryHandle } from '@/shims-fs'; |
|||
|
|||
export enum FilenamePolicy { |
|||
ArtistAndTitle, |
|||
TitleOnly, |
|||
TitleAndArtist, |
|||
SameAsOriginal, |
|||
ArtistAndTitle, |
|||
TitleOnly, |
|||
TitleAndArtist, |
|||
SameAsOriginal, |
|||
} |
|||
|
|||
export const FilenamePolicies: { key: FilenamePolicy, text: string }[] = [ |
|||
{key: FilenamePolicy.ArtistAndTitle, text: "歌手-歌曲名"}, |
|||
{key: FilenamePolicy.TitleOnly, text: "歌曲名"}, |
|||
{key: FilenamePolicy.TitleAndArtist, text: "歌曲名-歌手"}, |
|||
{key: FilenamePolicy.SameAsOriginal, text: "同源文件名"}, |
|||
] |
|||
export const FilenamePolicies: { key: FilenamePolicy; text: string }[] = [ |
|||
{ key: FilenamePolicy.ArtistAndTitle, text: '歌手-歌曲名' }, |
|||
{ key: FilenamePolicy.TitleOnly, text: '歌曲名' }, |
|||
{ key: FilenamePolicy.TitleAndArtist, text: '歌曲名-歌手' }, |
|||
{ key: FilenamePolicy.SameAsOriginal, text: '同源文件名' }, |
|||
]; |
|||
|
|||
export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy): string { |
|||
switch (policy) { |
|||
case FilenamePolicy.TitleOnly: |
|||
return `${data.title}.${data.ext}`; |
|||
case FilenamePolicy.TitleAndArtist: |
|||
return `${data.title} - ${data.artist}.${data.ext}`; |
|||
case FilenamePolicy.SameAsOriginal: |
|||
return `${data.rawFilename}.${data.ext}`; |
|||
default: |
|||
case FilenamePolicy.ArtistAndTitle: |
|||
return `${data.artist} - ${data.title}.${data.ext}`; |
|||
} |
|||
switch (policy) { |
|||
case FilenamePolicy.TitleOnly: |
|||
return `${data.title}.${data.ext}`; |
|||
case FilenamePolicy.TitleAndArtist: |
|||
return `${data.title} - ${data.artist}.${data.ext}`; |
|||
case FilenamePolicy.SameAsOriginal: |
|||
return `${data.rawFilename}.${data.ext}`; |
|||
default: |
|||
case FilenamePolicy.ArtistAndTitle: |
|||
return `${data.artist} - ${data.title}.${data.ext}`; |
|||
} |
|||
} |
|||
|
|||
export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) { |
|||
let filename = GetDownloadFilename(data, policy) |
|||
// prevent filename exist
|
|||
try { |
|||
await dir.getFileHandle(filename) |
|||
filename = `${new Date().getTime()} - ${filename}` |
|||
} catch (e) { |
|||
} |
|||
const file = await dir.getFileHandle(filename, {create: true}) |
|||
const w = await file.createWritable() |
|||
await w.write(data.blob) |
|||
await w.close() |
|||
|
|||
let filename = GetDownloadFilename(data, policy); |
|||
// prevent filename exist
|
|||
try { |
|||
await dir.getFileHandle(filename); |
|||
filename = `${new Date().getTime()} - ${filename}`; |
|||
} catch (e) {} |
|||
const file = await dir.getFileHandle(filename, { create: true }); |
|||
const w = await file.createWritable(); |
|||
await w.write(data.blob); |
|||
await w.close(); |
|||
} |
|||
|
|||
export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) { |
|||
const a = document.createElement('a'); |
|||
a.href = data.file; |
|||
a.download = GetDownloadFilename(data, policy) |
|||
document.body.append(a); |
|||
a.click(); |
|||
a.remove(); |
|||
const a = document.createElement('a'); |
|||
a.href = data.file; |
|||
a.download = GetDownloadFilename(data, policy); |
|||
document.body.append(a); |
|||
a.click(); |
|||
a.remove(); |
|||
} |
|||
|
|||
export function RemoveBlobMusic(data: DecryptResult) { |
|||
URL.revokeObjectURL(data.file); |
|||
if (data.picture?.startsWith("blob:")) { |
|||
URL.revokeObjectURL(data.picture); |
|||
} |
|||
URL.revokeObjectURL(data.file); |
|||
if (data.picture?.startsWith('blob:')) { |
|||
URL.revokeObjectURL(data.picture); |
|||
} |
|||
} |
|||
|
|||
export class DecryptQueue { |
|||
private readonly pending: (() => Promise<void>)[]; |
|||
private readonly pending: (() => Promise<void>)[]; |
|||
|
|||
constructor() { |
|||
this.pending = [] |
|||
} |
|||
constructor() { |
|||
this.pending = []; |
|||
} |
|||
|
|||
queue(fn: () => Promise<void>) { |
|||
this.pending.push(fn) |
|||
this.consume() |
|||
} |
|||
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) |
|||
} |
|||
private consume() { |
|||
const fn = this.pending.shift(); |
|||
if (fn) |
|||
fn() |
|||
.then(() => this.consume) |
|||
.catch(console.error); |
|||
} |
|||
} |
|||
|
@ -1,4 +1,4 @@ |
|||
import {expose} from "threads/worker"; |
|||
import {CommonDecrypt} from "@/decrypt/common"; |
|||
import { expose } from 'threads/worker'; |
|||
import { CommonDecrypt } from '@/decrypt/common'; |
|||
|
|||
expose(CommonDecrypt) |
|||
expose(CommonDecrypt); |
|||
|
Loading…
Reference in new issue