WIP: error fix & improvements #60

Draft
nullptr-0 wants to merge 5 commits from nullptr-0/web:pr into master
21 changed files with 94 additions and 62 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "unlock-music",
"version": "1.10.6",
"version": "1.10.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "unlock-music",
"version": "1.10.6",
"version": "1.10.7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@ -1,8 +1,8 @@
{
"name": "unlock-music",
"version": "1.10.6",
"version": "1.10.7",
"ext_build": 0,
"updateInfo": "修正文件过小的情况下酷狗 / QQ解密错误问题",
"updateInfo": "修正文件未加密时出现错误的问题",
"license": "MIT",
"description": "Unlock encrypted music file in browser.",
"repository": {

View File

@ -77,6 +77,7 @@
</template>
<script>
import { split_regex } from '@/decrypt/utils';
import Ruby from './Ruby';
export default {
@ -152,10 +153,10 @@ export default {
this.$emit('ok', {
picture: this.imgFile.blob,
title: this.title,
artist: this.artist,
artist: this.artist.split(split_regex),
album: this.album,
albumartist: this.albumartist,
genre: this.genre,
genre: this.genre.split(split_regex),
});
},
},

View File

@ -14,7 +14,7 @@
</el-table-column>
<el-table-column label="歌手">
<template #default="scope">
<p>{{ scope.row.artist }}</p>
<p>{{ scope.row.artist.join('/') }}</p>
</template>
</el-table-column>
<el-table-column label="专辑">
@ -27,7 +27,7 @@
<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-edit" @click="handleEdit(scope.row)"></el-button>
<el-button circle icon="el-icon-edit" @click="handleEdit(scope.$index, scope.row)"></el-button>
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
</el-button>
</template>
@ -56,8 +56,8 @@ export default {
handleDownload(row) {
this.$emit('download', row);
},
handleEdit(row) {
this.$emit('edit', row);
handleEdit(index, row) {
this.$emit('edit', index, row);
},
},
};

View File

@ -21,7 +21,7 @@ describe('decrypt/joox', () => {
title: 'unused',
album: 'unused',
blob: blob,
artist: 'unused',
artist: ['unused'],
imgUrl: 'https://example.unlock-music.dev/',
};
});

View File

@ -1,7 +1,7 @@
export interface DecryptResult {
title: string;
album?: string;
artist?: string;
artist?: string[];
mime: string;
ext: string;

View File

@ -1,3 +1,4 @@
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
import { Decrypt as Mg3dDecrypt } from '@/decrypt/mg3d';
import { Decrypt as NcmDecrypt } from '@/decrypt/ncm';
import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache';
@ -23,32 +24,38 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
const raw = SplitFilename(file.name);
let rt_data: DecryptResult;
switch (raw.ext) {
let raw_file: Blob;
raw_file = file.raw;
let ext = raw.ext;
const buffer = new Uint8Array(await GetArrayBuffer(raw_file));
ext = SniffAudioExt(buffer, raw.ext);
if (ext !== raw.ext) raw_file = new Blob([buffer], { type: AudioMimeType[ext] });
switch (ext) {
case 'mg3d': // Migu Wav
rt_data = await Mg3dDecrypt(file.raw, raw.name);
rt_data = await Mg3dDecrypt(raw_file, raw.name);
break;
case 'ncm': // Netease Mp3/Flac
rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
rt_data = await NcmDecrypt(raw_file, raw.name, raw.ext);
break;
case 'uc': // Netease Cache
rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext);
rt_data = await NcmCacheDecrypt(raw_file, raw.name, raw.ext);
break;
case 'kwm': // Kuwo Mp3/Flac
rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
rt_data = await KwmDecrypt(raw_file, raw.name, raw.ext);
break;
case 'xm': // Xiami Wav/M4a/Mp3/Flac
case 'wav': // Xiami/Raw Wav
case 'mp3': // Xiami/Raw Mp3
case 'flac': // Xiami/Raw Flac
case 'm4a': // Xiami/Raw M4a
rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
rt_data = await XmDecrypt(raw_file, raw.name, raw.ext);
break;
case 'ogg': // Raw Ogg
rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
rt_data = await RawDecrypt(raw_file, raw.name, raw.ext, false);
break;
case 'tm0': // QQ Music IOS Mp3
case 'tm3': // QQ Music IOS Mp3
rt_data = await RawDecrypt(file.raw, raw.name, 'mp3');
rt_data = await RawDecrypt(raw_file, raw.name, 'mp3', false);
break;
case 'qmc0': //QQ Music Android Mp3
case 'qmc3': //QQ Music Android Mp3
@ -81,26 +88,26 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
case '6f6767': //QQ Music Weiyun Ogg
case '6d3461': //QQ Music Weiyun M4a
case '776176': //QQ Music Weiyun Wav
rt_data = await QmcDecrypt(file.raw, raw.name, raw.ext);
rt_data = await QmcDecrypt(raw_file, raw.name, raw.ext);
break;
case 'tm2': // QQ Music IOS M4a
case 'tm6': // QQ Music IOS M4a
rt_data = await TmDecrypt(file.raw, raw.name);
rt_data = await TmDecrypt(raw_file, raw.name);
break;
case 'cache': //QQ Music Cache
rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
rt_data = await QmcCacheDecrypt(raw_file, raw.name, raw.ext);
break;
case 'vpr':
case 'kgm':
case 'kgma':
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
rt_data = await KgmDecrypt(raw_file, raw.name, raw.ext);
break;
case 'ofl_en':
rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext);
rt_data = await JooxDecrypt(raw_file, raw.name, raw.ext);
break;
case 'x2m':
case 'x3m':
rt_data = await XimalayaDecrypt(file.raw, raw.name, raw.ext);
rt_data = await XimalayaDecrypt(raw_file, raw.name, raw.ext);
break;
case 'mflach': //QQ Music New Flac
throw '网页版无法解锁,请使用<a target="_blank" href="https://git.unlock-music.dev/um/cli">CLI版本</a>'

View File

@ -21,7 +21,7 @@ const KgmHeader = [
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14
]
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const oriData = await GetArrayBuffer(file);
if (raw_ext === 'vpr') {
if (!BytesHasPrefix(new Uint8Array(oriData), VprHeader)) throw Error('Not a valid vpr file!');
@ -43,7 +43,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], { type: mime });
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, musicMeta.common.artists || [musicMeta.common.artist || ""]);
return {
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),

View File

@ -22,7 +22,7 @@ const MagicHeader2 = [
];
const PreDefinedKey = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (!BytesHasPrefix(oriData, MagicHeader) && !BytesHasPrefix(oriData, MagicHeader2)) {
if (SniffAudioExt(oriData) === 'aac') {
@ -42,7 +42,7 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
let musicBlob = new Blob([audioData], { type: mime });
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, musicMeta.common.artists || [musicMeta.common.artist || ""]);
return {
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),

View File

@ -25,7 +25,7 @@ function decryptSegment(data: Uint8Array, key: Uint8Array) {
return Buffer.from(data);
}
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
export async function Decrypt(file: Blob, raw_filename: string): Promise<DecryptResult> {
const buf = new Uint8Array(await GetArrayBuffer(file));
// 咪咕编码的 WAV 文件有很多“空洞”内容,尝试密钥。

View File

@ -26,7 +26,7 @@ const CORE_KEY = EncHex.parse('687a4852416d736f356b496e62617857');
const META_KEY = EncHex.parse('2331346C6A6B5F215C5D2630553C2728');
const MagicHeader = [0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d];
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt();
}
@ -175,7 +175,6 @@ class NcmDecrypt {
if (artists.length === 0 && info.artist) {
artists = info.artist
.split(',')
.map((val) => val.trim())
.filter((val) => val != '');
}
@ -219,7 +218,7 @@ class NcmDecrypt {
if (!this.newMeta || !this.blob) throw Error('bad sequence');
return {
title: this.newMeta.title,
artist: this.newMeta.artists?.join('; '),
artist: this.newMeta.artists,
ext: this.format,
album: this.newMeta.album,
picture: this.image?.url,

View File

@ -13,7 +13,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
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, String(tag.common.artists || tag.common.artist || ""));
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artists || [tag.common.artist || ""]);
return {
title,

View File

@ -43,7 +43,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
throw '不支持的QQ音乐缓存格式';
}
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, tag.common.artists || [tag.common.artist || ""]);
return {
title,

View File

@ -17,7 +17,7 @@ export async function Decrypt(
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, String(tag.common.artists || tag.common.artist || ''));
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artists || [tag.common.artist || '']);
return {
title,

View File

@ -4,7 +4,7 @@ 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> {
export async function Decrypt(file: Blob, raw_filename: string): Promise<DecryptResult> {
const audioData = new Uint8Array(await GetArrayBuffer(file));
for (let cur = 0; cur < 8; ++cur) {
audioData[cur] = TM_HEADER[cur];

View File

@ -80,13 +80,13 @@ export function GetCoverFromFile(metadata: IAudioMetadata): string {
export interface IMusicMetaBasic {
title: string;
artist?: string;
artist?: string[];
}
export function GetMetaFromFile(
filename: string,
exist_title?: string,
exist_artist?: string,
exist_artist?: string[],
separator = '-',
): IMusicMetaBasic {
const meta: IMusicMetaBasic = { title: exist_title ?? '', artist: exist_artist };
@ -94,7 +94,7 @@ export function GetMetaFromFile(
const items = filename.split(separator);
if (items.length > 1) {
//由文件名和原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.length < items[0].trim().split(split_regex).length) meta.artist = items[0].trim().split(split_regex);
if (!meta.title) meta.title = items[1].trim();
} else if (items.length === 1) {
if (!meta.title) meta.title = items[0].trim();

View File

@ -7,7 +7,7 @@ const HandlerMap: Map<string, (data: Uint8Array) => Uint8Array> = new Map([
['x3m', ProcessX3M],
]);
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
const handler = HandlerMap.get(raw_ext);
if (!handler) throw 'File type is incorrect!';
@ -20,7 +20,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
const musicMeta = await metaParseBlob(musicBlob);
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artists || [musicMeta.common.artist || '']);
return {
picture: '',

View File

@ -13,7 +13,7 @@ const FileTypeMap: { [key: string]: string } = {
' A4M': '.m4a',
};
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
if (raw_ext === 'xm') {
@ -49,7 +49,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
const { title, artist } = GetMetaFromFile(
raw_filename,
musicMeta.common.title,
String(musicMeta.common.artists || musicMeta.common.artist || ""),
musicMeta.common.artists || [musicMeta.common.artist || ""],
raw_filename.indexOf('_') === -1 ? '-' : '_',
);

View File

@ -14,7 +14,7 @@ import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/uti
interface MetaResult {
title: string;
artist: string;
artist: string[];
album: string;
imgUrl: string;
blob: Blob;
@ -44,6 +44,7 @@ export async function extractQQMusicMeta(
musicMeta.common.artist = '';
if (!musicMeta.common.artists) {
musicMeta.common.artist = fromGBK(musicMeta.common.artist);
musicMeta.common.artists = [musicMeta.common.artist];
}
else {
musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join();
@ -61,8 +62,8 @@ export async function extractQQMusicMeta(
}
}
const info = GetMetaFromFile(name, musicMeta.common.title, musicMeta.common.artist);
info.artist = info.artist || '';
const info = GetMetaFromFile(name, musicMeta.common.title, musicMeta.common.artists);
info.artist = info.artist || [''];
let imageURL = GetCoverFromFile(musicMeta);
if (!imageURL) {
@ -76,7 +77,7 @@ export async function extractQQMusicMeta(
imgUrl: imageURL,
blob: await writeMetaToAudioFile({
title: info.title,
artists: info.artist.split(split_regex),
artists: info.artist,
ext,
imageURL,
musicMeta,
@ -97,7 +98,7 @@ async function fetchMetadataFromSongId(
return {
title: info.track_info.title,
artist: artists.join(','),
artist: artists,
album: info.track_info.album.name,
imgUrl: imageURL,
@ -112,9 +113,9 @@ async function fetchMetadataFromSongId(
};
}
async function getCoverImage(title: string, artist?: string, album?: string): Promise<string> {
async function getCoverImage(title: string, artist?: string[], album?: string): Promise<string> {
try {
const data = await queryAlbumCover(title, artist, album);
const data = await queryAlbumCover(title, artist?.join(), album);
return getQMImageURLFromPMID(data.Id, data.Type);
} catch (e) {
console.warn(e);

View File

@ -14,10 +14,10 @@
:show="showEditDialog"
:picture="editing_data.picture"
:title="editing_data.title"
:artist="editing_data.artist"
:artist="joinedArtist"
:album="editing_data.album"
:albumartist="editing_data.albumartist"
:genre="editing_data.genre"
:genre="joinedGenre"
@cancel="showEditDialog = false"
@ok="handleEdit"
></edit-dialog>
@ -78,7 +78,19 @@ export default {
return {
showConfigDialog: false,
showEditDialog: false,
editing_data: { picture: '', title: '', artist: '', album: '', albumartist: '', genre: '' },
editing_data: {
picture: '',
title: '',
artist: [''],
album: '',
albumartist: '',
genre: [''],
ext: '',
blob: Blob,
index: 0
},
joinedArtist: "",
joinedGenre: "",
tableData: [],
playing_url: '',
playing_auto: false,
@ -160,7 +172,11 @@ export default {
}
this.editing_data.title = data.title;
this.editing_data.artist = data.artist;
this.joinedArtist = data.artist.join();
this.editing_data.album = data.album;
this.editing_data.albumartist = data.albumartist;
this.editing_data.genre = data.genre;
this.joinedGenre = data.genre.join();
let writeSuccess = true;
let notifyMsg = '成功修改 ' + this.editing_data.title;
try {
@ -175,10 +191,10 @@ export default {
const newMeta = {
picture: imageInfo?.buffer,
title: data.title,
artists: data.artist.split(split_regex),
artists: data.artist,
album: data.album,
albumartist: data.albumartist,
genre: data.genre.split(split_regex),
genre: data.genre,
};
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
@ -195,6 +211,9 @@ export default {
notifyMsg = '修改' + this.editing_data.title + '未能完成。在写入新的元数据时发生错误:' + e;
}
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
let dataIndex = this.editing_data.index;
delete this.editing_data.index;
this.tableData.splice(dataIndex, 1, this.editing_data)
if (writeSuccess === true) {
this.$notify.success({
title: '修改成功',
@ -216,11 +235,14 @@ export default {
}
},
async editFile(data) {
async editFile(index, data) {
this.editing_data = data;
const musicMeta = await metaParseBlob(this.editing_data.blob);
const musicMeta = await metaParseBlob(data.blob);
this.editing_data.albumartist = musicMeta.common.albumartist || '';
this.editing_data.genre = musicMeta.common.genre?.toString() || '';
this.editing_data.genre = musicMeta.common.genre || [''];
this.joinedArtist = this.editing_data.artist.join();
this.joinedGenre = this.editing_data.genre.join();
this.editing_data.index = index;
this.showEditDialog = true;
},
async saveFile(data) {

View File

@ -28,7 +28,8 @@
"dom.iterable",
"scripthost"
],
"resolveJsonModule": true
"resolveJsonModule": true,
"outDir": "build"
},
"include": [
"src/**/*.ts",
@ -38,6 +39,7 @@
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
"node_modules",
"build/**/*"
]
}