mirror of
				https://git.unlock-music.dev/um/web.git
				synced 2025-11-04 14:43:28 +08:00 
			
		
		
		
	all: format with prettier
(cherry picked from commit cad5b4d7deba4fbe4a40a17306ce49d3b2f13139)
This commit is contained in:
		
							parent
							
								
									19486d4d34
								
							
						
					
					
						commit
						76dd78130a
					
				
							
								
								
									
										40
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								src/App.vue
									
									
									
									
									
								
							@ -15,7 +15,7 @@
 | 
			
		||||
      </el-row>
 | 
			
		||||
      <el-row>
 | 
			
		||||
        <!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
 | 
			
		||||
                <span>Copyright © 2019 - {{ (new Date()).getFullYear() }} MengYX</span>
 | 
			
		||||
        <span>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span>
 | 
			
		||||
        音乐解锁使用
 | 
			
		||||
        <a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
 | 
			
		||||
        开放源代码
 | 
			
		||||
@ -25,46 +25,48 @@
 | 
			
		||||
</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
 | 
			
		||||
    Home,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      version: config.version,
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.$nextTick(() => this.finishLoad());
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async finishLoad() {
 | 
			
		||||
            const mask = document.getElementById("loader-mask");
 | 
			
		||||
      const mask = document.getElementById('loader-mask');
 | 
			
		||||
      if (!!mask) mask.remove();
 | 
			
		||||
      let updateInfo;
 | 
			
		||||
      try {
 | 
			
		||||
                updateInfo = await checkUpdate(this.version)
 | 
			
		||||
        updateInfo = await checkUpdate(this.version);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
                console.warn("check version info failed", e)
 | 
			
		||||
        console.warn('check version info failed', e);
 | 
			
		||||
      }
 | 
			
		||||
            if ((updateInfo && process.env.NODE_ENV === 'production') && (updateInfo.HttpsFound ||
 | 
			
		||||
                (updateInfo.Found && window.location.protocol !== "https:"))) {
 | 
			
		||||
      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'
 | 
			
		||||
          position: 'top-left',
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$notify.info({
 | 
			
		||||
@ -72,14 +74,14 @@ export default {
 | 
			
		||||
          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'
 | 
			
		||||
          position: 'top-left',
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
@import "scss/unlock-music";
 | 
			
		||||
@import 'scss/unlock-music';
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,17 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <el-upload
 | 
			
		||||
        :auto-upload="false"
 | 
			
		||||
        :on-change="addFile"
 | 
			
		||||
        :show-file-list="false"
 | 
			
		||||
        action=""
 | 
			
		||||
        drag
 | 
			
		||||
        multiple>
 | 
			
		||||
  <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>
 | 
			
		||||
        仅在浏览器内对文件进行解锁,无需消耗流量
 | 
			
		||||
        <el-tooltip effect="dark" placement="top-start">
 | 
			
		||||
                    <div slot="content">
 | 
			
		||||
                        算法在源代码中已经提供,所有运算都发生在本地
 | 
			
		||||
                    </div>
 | 
			
		||||
          <div slot="content">算法在源代码中已经提供,所有运算都发生在本地</div>
 | 
			
		||||
          <i class="el-icon-info" style="font-size: 12px" />
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
                工作模式: {{ parallel ? "多线程 Worker" : "单线程 Queue" }}
 | 
			
		||||
        工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
 | 
			
		||||
        <el-tooltip effect="dark" placement="top-start">
 | 
			
		||||
          <div slot="content">
 | 
			
		||||
            将此工具部署在HTTPS环境下,可以启用Web Worker特性,<br />
 | 
			
		||||
@ -29,10 +21,14 @@
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
        <transition name="el-fade-in"><!--todo: add delay to animation-->
 | 
			
		||||
    <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"
 | 
			
		||||
        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>
 | 
			
		||||
@ -40,60 +36,55 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {spawn, Worker, Pool} from "threads"
 | 
			
		||||
import {CommonDecrypt} from "@/decrypt/common.ts";
 | 
			
		||||
import {DecryptQueue} from "@/utils/utils";
 | 
			
		||||
import { spawn, Worker, Pool } from 'threads';
 | 
			
		||||
import { CommonDecrypt } from '@/decrypt/common.ts';
 | 
			
		||||
import { DecryptQueue } from '@/utils/utils';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    name: "FileSelector",
 | 
			
		||||
  name: 'FileSelector',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      task_all: 0,
 | 
			
		||||
      task_finished: 0,
 | 
			
		||||
      queue: new DecryptQueue(), // for http or file protocol
 | 
			
		||||
            parallel: false
 | 
			
		||||
        }
 | 
			
		||||
      parallel: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    progress_value() {
 | 
			
		||||
            return this.task_all ? this.task_finished / this.task_all * 100 : 0
 | 
			
		||||
      return this.task_all ? (this.task_finished / this.task_all) * 100 : 0;
 | 
			
		||||
    },
 | 
			
		||||
    progress_show() {
 | 
			
		||||
            return this.task_all !== this.task_finished
 | 
			
		||||
        }
 | 
			
		||||
      return this.task_all !== this.task_finished;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
        if (window.Worker && window.location.protocol !== "file:" && process.env.NODE_ENV === 'production') {
 | 
			
		||||
            console.log("Using Worker Pool")
 | 
			
		||||
            this.queue = Pool(
 | 
			
		||||
                () => spawn(new Worker('@/utils/worker.ts')),
 | 
			
		||||
                navigator.hardwareConcurrency || 1
 | 
			
		||||
            )
 | 
			
		||||
            this.parallel = true
 | 
			
		||||
    if (window.Worker && window.location.protocol !== 'file:' && process.env.NODE_ENV === 'production') {
 | 
			
		||||
      console.log('Using Worker Pool');
 | 
			
		||||
      this.queue = Pool(() => spawn(new Worker('@/utils/worker.ts')), navigator.hardwareConcurrency || 1);
 | 
			
		||||
      this.parallel = true;
 | 
			
		||||
    } else {
 | 
			
		||||
            console.log("Using Queue in Main Thread")
 | 
			
		||||
      console.log('Using Queue in Main Thread');
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    progress_string() {
 | 
			
		||||
            return `${this.task_finished} / ${this.task_all}`
 | 
			
		||||
      return `${this.task_finished} / ${this.task_all}`;
 | 
			
		||||
    },
 | 
			
		||||
    async addFile(file) {
 | 
			
		||||
            this.task_all++
 | 
			
		||||
      this.task_all++;
 | 
			
		||||
      this.queue.queue(async (dec = CommonDecrypt) => {
 | 
			
		||||
                console.log("start handling", file.name)
 | 
			
		||||
        console.log('start handling', file.name);
 | 
			
		||||
        try {
 | 
			
		||||
                    this.$emit("success", await dec(file));
 | 
			
		||||
          this.$emit('success', await dec(file));
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
                    console.error(e)
 | 
			
		||||
                    this.$emit("error", e, file.name)
 | 
			
		||||
          console.error(e);
 | 
			
		||||
          this.$emit('error', e, file.name);
 | 
			
		||||
        } finally {
 | 
			
		||||
                    this.task_finished++
 | 
			
		||||
          this.task_finished++;
 | 
			
		||||
        }
 | 
			
		||||
            })
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,9 @@
 | 
			
		||||
<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>
 | 
			
		||||
          <div slot="error" class="image-slot el-image__error">暂无封面</div>
 | 
			
		||||
        </el-image>
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-table-column>
 | 
			
		||||
@ -27,14 +24,10 @@
 | 
			
		||||
    </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 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 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>
 | 
			
		||||
@ -42,30 +35,28 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {RemoveBlobMusic} from '@/utils/utils'
 | 
			
		||||
import { RemoveBlobMusic } from '@/utils/utils';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    name: "PreviewTable",
 | 
			
		||||
  name: 'PreviewTable',
 | 
			
		||||
  props: {
 | 
			
		||||
    tableData: { type: Array, required: true },
 | 
			
		||||
        policy: {type: Number, required: true}
 | 
			
		||||
    policy: { type: Number, required: true },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
    handlePlay(index, row) {
 | 
			
		||||
            this.$emit("play", row.file);
 | 
			
		||||
      this.$emit('play', row.file);
 | 
			
		||||
    },
 | 
			
		||||
    handleDelete(index, row) {
 | 
			
		||||
      RemoveBlobMusic(row);
 | 
			
		||||
      this.tableData.splice(index, 1);
 | 
			
		||||
    },
 | 
			
		||||
    handleDownload(row) {
 | 
			
		||||
            this.$emit("download", row)
 | 
			
		||||
      this.$emit('download', row);
 | 
			
		||||
    },
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 | 
			
		||||
@ -1,76 +1,75 @@
 | 
			
		||||
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
 | 
			
		||||
import {Decrypt as NcmCacheDecrypt} from "@/decrypt/ncmcache";
 | 
			
		||||
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
 | 
			
		||||
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
 | 
			
		||||
import {Decrypt as QmcCacheDecrypt} from "@/decrypt/qmccache";
 | 
			
		||||
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, FileInfo} from "@/decrypt/entity";
 | 
			
		||||
import {SplitFilename} from "@/decrypt/utils";
 | 
			
		||||
 | 
			
		||||
import { Decrypt as NcmDecrypt } from '@/decrypt/ncm';
 | 
			
		||||
import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache';
 | 
			
		||||
import { Decrypt as XmDecrypt } from '@/decrypt/xm';
 | 
			
		||||
import { Decrypt as QmcDecrypt } from '@/decrypt/qmc';
 | 
			
		||||
import { Decrypt as QmcCacheDecrypt } from '@/decrypt/qmccache';
 | 
			
		||||
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, FileInfo } from '@/decrypt/entity';
 | 
			
		||||
import { SplitFilename } from '@/decrypt/utils';
 | 
			
		||||
 | 
			
		||||
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
 | 
			
		||||
    const raw = SplitFilename(file.name)
 | 
			
		||||
  const raw = SplitFilename(file.name);
 | 
			
		||||
  let rt_data: DecryptResult;
 | 
			
		||||
  switch (raw.ext) {
 | 
			
		||||
        case "ncm":// Netease Mp3/Flac
 | 
			
		||||
    case 'ncm': // Netease Mp3/Flac
 | 
			
		||||
      rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
 | 
			
		||||
      break;
 | 
			
		||||
        case "uc":// Netease Cache
 | 
			
		||||
    case 'uc': // Netease Cache
 | 
			
		||||
      rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext);
 | 
			
		||||
      break;
 | 
			
		||||
        case "kwm":// Kuwo Mp3/Flac
 | 
			
		||||
    case 'kwm': // Kuwo Mp3/Flac
 | 
			
		||||
      rt_data = await KwmDecrypt(file.raw, 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
 | 
			
		||||
      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);
 | 
			
		||||
      break;
 | 
			
		||||
        case "ogg":// Raw Ogg
 | 
			
		||||
    case 'ogg': // Raw Ogg
 | 
			
		||||
      rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
 | 
			
		||||
      break;
 | 
			
		||||
        case "tm0":// QQ Music IOS Mp3
 | 
			
		||||
        case "tm3":// QQ Music IOS Mp3
 | 
			
		||||
            rt_data = await RawDecrypt(file.raw, raw.name, "mp3");
 | 
			
		||||
    case 'tm0': // QQ Music IOS Mp3
 | 
			
		||||
    case 'tm3': // QQ Music IOS Mp3
 | 
			
		||||
      rt_data = await RawDecrypt(file.raw, raw.name, 'mp3');
 | 
			
		||||
      break;
 | 
			
		||||
        case "qmc3"://QQ Music Android Mp3
 | 
			
		||||
        case "qmc2"://QQ Music Android Ogg
 | 
			
		||||
        case "qmc0"://QQ Music Android Mp3
 | 
			
		||||
        case "qmcflac"://QQ Music Android Flac
 | 
			
		||||
        case "qmcogg"://QQ Music Android Ogg
 | 
			
		||||
        case "tkm"://QQ Music Accompaniment M4a
 | 
			
		||||
        case "bkcmp3"://Moo Music Mp3
 | 
			
		||||
        case "bkcflac"://Moo Music Flac
 | 
			
		||||
        case "mflac"://QQ Music New Flac
 | 
			
		||||
        case "mflac0"://QQ Music New Flac
 | 
			
		||||
        case "mgg": //QQ Music New Ogg
 | 
			
		||||
        case "mgg1": //QQ Music New Ogg
 | 
			
		||||
        case "666c6163"://QQ Music Weiyun Flac
 | 
			
		||||
        case "6d7033"://QQ Music Weiyun Mp3
 | 
			
		||||
        case "6f6767"://QQ Music Weiyun Ogg
 | 
			
		||||
        case "6d3461"://QQ Music Weiyun M4a
 | 
			
		||||
        case "776176"://QQ Music Weiyun Wav
 | 
			
		||||
    case 'qmc3': //QQ Music Android Mp3
 | 
			
		||||
    case 'qmc2': //QQ Music Android Ogg
 | 
			
		||||
    case 'qmc0': //QQ Music Android Mp3
 | 
			
		||||
    case 'qmcflac': //QQ Music Android Flac
 | 
			
		||||
    case 'qmcogg': //QQ Music Android Ogg
 | 
			
		||||
    case 'tkm': //QQ Music Accompaniment M4a
 | 
			
		||||
    case 'bkcmp3': //Moo Music Mp3
 | 
			
		||||
    case 'bkcflac': //Moo Music Flac
 | 
			
		||||
    case 'mflac': //QQ Music New Flac
 | 
			
		||||
    case 'mflac0': //QQ Music New Flac
 | 
			
		||||
    case 'mgg': //QQ Music New Ogg
 | 
			
		||||
    case 'mgg1': //QQ Music New Ogg
 | 
			
		||||
    case '666c6163': //QQ Music Weiyun Flac
 | 
			
		||||
    case '6d7033': //QQ Music Weiyun Mp3
 | 
			
		||||
    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);
 | 
			
		||||
      break;
 | 
			
		||||
        case "tm2":// QQ Music IOS M4a
 | 
			
		||||
        case "tm6":// QQ Music IOS M4a
 | 
			
		||||
    case 'tm2': // QQ Music IOS M4a
 | 
			
		||||
    case 'tm6': // QQ Music IOS M4a
 | 
			
		||||
      rt_data = await TmDecrypt(file.raw, raw.name);
 | 
			
		||||
      break;
 | 
			
		||||
        case "cache"://QQ Music Cache
 | 
			
		||||
    case 'cache': //QQ Music Cache
 | 
			
		||||
      rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
 | 
			
		||||
      break;
 | 
			
		||||
        case "vpr":
 | 
			
		||||
        case "kgm":
 | 
			
		||||
        case "kgma":
 | 
			
		||||
    case 'vpr':
 | 
			
		||||
    case 'kgm':
 | 
			
		||||
    case 'kgma':
 | 
			
		||||
      rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
 | 
			
		||||
            break
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
            throw "不支持此文件格式"
 | 
			
		||||
      throw '不支持此文件格式';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!rt_data.rawExt) rt_data.rawExt = raw.ext;
 | 
			
		||||
@ -78,4 +77,3 @@ export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
 | 
			
		||||
  console.log(rt_data);
 | 
			
		||||
  return rt_data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,64 +4,68 @@ import {
 | 
			
		||||
  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"
 | 
			
		||||
  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]
 | 
			
		||||
  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]
 | 
			
		||||
  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]
 | 
			
		||||
 | 
			
		||||
  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!")
 | 
			
		||||
  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!")
 | 
			
		||||
    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 bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer);
 | 
			
		||||
  let headerLen = bHeaderLen.getUint32(0, true);
 | 
			
		||||
 | 
			
		||||
    let audioData = oriData.slice(headerLen)
 | 
			
		||||
    let dataLen = audioData.length
 | 
			
		||||
  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> 进行解锁")
 | 
			
		||||
    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)
 | 
			
		||||
  let key1 = new Uint8Array(17);
 | 
			
		||||
  key1.set(oriData.slice(0x1c, 0x2c), 0);
 | 
			
		||||
  if (MaskV2.length === 0) {
 | 
			
		||||
        if (!await LoadMaskV2()) throw Error("加载Kgm/Vpr Mask数据失败")
 | 
			
		||||
    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
 | 
			
		||||
    let med8 = key1[i % 17] ^ audioData[i];
 | 
			
		||||
    med8 ^= (med8 & 0xf) << 4;
 | 
			
		||||
 | 
			
		||||
        let msk8 = GetMask(i)
 | 
			
		||||
        msk8 ^= (msk8 & 0xf) << 4
 | 
			
		||||
        audioData[i] = med8 ^ msk8
 | 
			
		||||
    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]
 | 
			
		||||
  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)
 | 
			
		||||
  const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
 | 
			
		||||
  return {
 | 
			
		||||
    album: musicMeta.common.album,
 | 
			
		||||
    picture: GetCoverFromFile(musicMeta),
 | 
			
		||||
@ -70,53 +74,52 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
 | 
			
		||||
    ext,
 | 
			
		||||
    mime,
 | 
			
		||||
    title,
 | 
			
		||||
        artist
 | 
			
		||||
    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"
 | 
			
		||||
  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"})
 | 
			
		||||
    const resp = await fetch(mask_url, { method: 'GET' });
 | 
			
		||||
    MaskV2 = new Uint8Array(await resp.arrayBuffer());
 | 
			
		||||
        return true
 | 
			
		||||
    return true;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
        console.error(e)
 | 
			
		||||
        return false
 | 
			
		||||
    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,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -4,42 +4,41 @@ import {
 | 
			
		||||
  GetArrayBuffer,
 | 
			
		||||
  GetCoverFromFile,
 | 
			
		||||
  GetMetaFromFile,
 | 
			
		||||
    SniffAudioExt
 | 
			
		||||
} from "@/decrypt/utils";
 | 
			
		||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
 | 
			
		||||
  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,
 | 
			
		||||
]
 | 
			
		||||
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)
 | 
			
		||||
    if (SniffAudioExt(oriData) === 'aac') {
 | 
			
		||||
      return await RawDecrypt(file, raw_filename, 'aac', false);
 | 
			
		||||
    }
 | 
			
		||||
        throw Error("not a valid kwm file")
 | 
			
		||||
    throw Error('not a valid kwm file');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    let fileKey = oriData.slice(0x18, 0x20)
 | 
			
		||||
    let mask = createMaskFromKey(fileKey)
 | 
			
		||||
  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];
 | 
			
		||||
 | 
			
		||||
  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 musicMeta = await metaParseBlob(musicBlob);
 | 
			
		||||
    const {title, artist} = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
 | 
			
		||||
  const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
 | 
			
		||||
  return {
 | 
			
		||||
    album: musicMeta.common.album,
 | 
			
		||||
    picture: GetCoverFromFile(musicMeta),
 | 
			
		||||
@ -48,30 +47,28 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
 | 
			
		||||
    mime,
 | 
			
		||||
    title,
 | 
			
		||||
    artist,
 | 
			
		||||
        ext
 | 
			
		||||
    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)
 | 
			
		||||
  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)
 | 
			
		||||
    key[i] = PreDefinedKey.charCodeAt(i) ^ keyStrTrim.charCodeAt(i);
 | 
			
		||||
  }
 | 
			
		||||
    return key
 | 
			
		||||
  return key;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function trimKey(keyRaw: string): string {
 | 
			
		||||
  let lenRaw = keyRaw.length;
 | 
			
		||||
  let out = keyRaw;
 | 
			
		||||
  if (lenRaw > 32) {
 | 
			
		||||
        out = keyRaw.slice(0, 32)
 | 
			
		||||
    out = keyRaw.slice(0, 32);
 | 
			
		||||
  } else if (lenRaw < 32) {
 | 
			
		||||
        out = keyRaw.padEnd(32, keyRaw)
 | 
			
		||||
    out = keyRaw.padEnd(32, keyRaw);
 | 
			
		||||
  }
 | 
			
		||||
    return out
 | 
			
		||||
  return out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,79 +7,75 @@ import {
 | 
			
		||||
  IMusicMeta,
 | 
			
		||||
  SniffAudioExt,
 | 
			
		||||
  WriteMetaToFlac,
 | 
			
		||||
    WriteMetaToMp3
 | 
			
		||||
} from "@/decrypt/utils";
 | 
			
		||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
			
		||||
  WriteMetaToMp3,
 | 
			
		||||
} from '@/decrypt/utils';
 | 
			
		||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
			
		||||
import jimp from 'jimp';
 | 
			
		||||
 | 
			
		||||
import AES from "crypto-js/aes";
 | 
			
		||||
import PKCS7 from "crypto-js/pad-pkcs7";
 | 
			
		||||
import ModeECB from "crypto-js/mode-ecb";
 | 
			
		||||
import WordArray from "crypto-js/lib-typedarrays";
 | 
			
		||||
import Base64 from "crypto-js/enc-base64";
 | 
			
		||||
import EncUTF8 from "crypto-js/enc-utf8";
 | 
			
		||||
import EncHex from "crypto-js/enc-hex";
 | 
			
		||||
import AES from 'crypto-js/aes';
 | 
			
		||||
import PKCS7 from 'crypto-js/pad-pkcs7';
 | 
			
		||||
import ModeECB from 'crypto-js/mode-ecb';
 | 
			
		||||
import WordArray from 'crypto-js/lib-typedarrays';
 | 
			
		||||
import Base64 from 'crypto-js/enc-base64';
 | 
			
		||||
import EncUTF8 from 'crypto-js/enc-utf8';
 | 
			
		||||
import EncHex from 'crypto-js/enc-hex';
 | 
			
		||||
 | 
			
		||||
import {DecryptResult} from "@/decrypt/entity";
 | 
			
		||||
 | 
			
		||||
const CORE_KEY = EncHex.parse("687a4852416d736f356b496e62617857");
 | 
			
		||||
const META_KEY = EncHex.parse("2331346C6A6B5F215C5D2630553C2728");
 | 
			
		||||
const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D];
 | 
			
		||||
import { DecryptResult } from '@/decrypt/entity';
 | 
			
		||||
 | 
			
		||||
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> {
 | 
			
		||||
    return (new NcmDecrypt(await GetArrayBuffer(file), raw_filename)).decrypt()
 | 
			
		||||
  return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface NcmMusicMeta {
 | 
			
		||||
  //musicId: number
 | 
			
		||||
    musicName?: string
 | 
			
		||||
    artist?: Array<string | number>[]
 | 
			
		||||
    format?: string
 | 
			
		||||
    album?: string
 | 
			
		||||
    albumPic?: string
 | 
			
		||||
  musicName?: string;
 | 
			
		||||
  artist?: Array<string | number>[];
 | 
			
		||||
  format?: string;
 | 
			
		||||
  album?: string;
 | 
			
		||||
  albumPic?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface NcmDjMeta {
 | 
			
		||||
    mainMusic: NcmMusicMeta
 | 
			
		||||
  mainMusic: NcmMusicMeta;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NcmDecrypt {
 | 
			
		||||
    raw: ArrayBuffer
 | 
			
		||||
    view: DataView
 | 
			
		||||
    offset: number = 0
 | 
			
		||||
    filename: string
 | 
			
		||||
    format: string = ""
 | 
			
		||||
    mime: string = ""
 | 
			
		||||
    audio?: Uint8Array
 | 
			
		||||
    blob?: Blob
 | 
			
		||||
    oriMeta?: NcmMusicMeta
 | 
			
		||||
    newMeta?: IMusicMeta
 | 
			
		||||
    image?: { mime: string, buffer: ArrayBuffer, url: string }
 | 
			
		||||
  raw: ArrayBuffer;
 | 
			
		||||
  view: DataView;
 | 
			
		||||
  offset: number = 0;
 | 
			
		||||
  filename: string;
 | 
			
		||||
  format: string = '';
 | 
			
		||||
  mime: string = '';
 | 
			
		||||
  audio?: Uint8Array;
 | 
			
		||||
  blob?: Blob;
 | 
			
		||||
  oriMeta?: NcmMusicMeta;
 | 
			
		||||
  newMeta?: IMusicMeta;
 | 
			
		||||
  image?: { mime: string; buffer: ArrayBuffer; url: string };
 | 
			
		||||
 | 
			
		||||
  constructor(buf: ArrayBuffer, filename: string) {
 | 
			
		||||
        const prefix = new Uint8Array(buf, 0, 8)
 | 
			
		||||
        if (!BytesHasPrefix(prefix, MagicHeader)) throw Error("此ncm文件已损坏")
 | 
			
		||||
        this.offset = 10
 | 
			
		||||
        this.raw = buf
 | 
			
		||||
        this.view = new DataView(buf)
 | 
			
		||||
        this.filename = filename
 | 
			
		||||
    const prefix = new Uint8Array(buf, 0, 8);
 | 
			
		||||
    if (!BytesHasPrefix(prefix, MagicHeader)) throw Error('此ncm文件已损坏');
 | 
			
		||||
    this.offset = 10;
 | 
			
		||||
    this.raw = buf;
 | 
			
		||||
    this.view = new DataView(buf);
 | 
			
		||||
    this.filename = filename;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getKeyData(): Uint8Array {
 | 
			
		||||
    const keyLen = this.view.getUint32(this.offset, true);
 | 
			
		||||
    this.offset += 4;
 | 
			
		||||
        const cipherText = new Uint8Array(this.raw, this.offset, keyLen)
 | 
			
		||||
            .map(uint8 => uint8 ^ 0x64);
 | 
			
		||||
    const cipherText = new Uint8Array(this.raw, this.offset, keyLen).map((uint8) => uint8 ^ 0x64);
 | 
			
		||||
    this.offset += keyLen;
 | 
			
		||||
 | 
			
		||||
    const plainText = AES.decrypt(
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      { ciphertext: WordArray.create(cipherText) },
 | 
			
		||||
      CORE_KEY,
 | 
			
		||||
            {mode: ModeECB, padding: PKCS7}
 | 
			
		||||
      { mode: ModeECB, padding: PKCS7 },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const result = new Uint8Array(plainText.sigBytes);
 | 
			
		||||
@ -90,11 +86,11 @@ class NcmDecrypt {
 | 
			
		||||
      result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        return result.slice(17)
 | 
			
		||||
    return result.slice(17);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getKeyBox(): Uint8Array {
 | 
			
		||||
        const keyData = this._getKeyData()
 | 
			
		||||
    const keyData = this._getKeyData();
 | 
			
		||||
    const box = new Uint8Array(Array(256).keys());
 | 
			
		||||
 | 
			
		||||
    const keyDataLen = keyData.length;
 | 
			
		||||
@ -119,126 +115,123 @@ class NcmDecrypt {
 | 
			
		||||
    this.offset += 4;
 | 
			
		||||
    if (metaDataLen === 0) return {};
 | 
			
		||||
 | 
			
		||||
        const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen)
 | 
			
		||||
            .map(data => data ^ 0x63);
 | 
			
		||||
    const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen).map((data) => data ^ 0x63);
 | 
			
		||||
    this.offset += metaDataLen;
 | 
			
		||||
 | 
			
		||||
        WordArray.create()
 | 
			
		||||
    WordArray.create();
 | 
			
		||||
    const plainText = AES.decrypt(
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      {
 | 
			
		||||
        ciphertext: Base64.parse(
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
                    WordArray.create(cipherText.slice(22)).toString(EncUTF8)
 | 
			
		||||
                )
 | 
			
		||||
          WordArray.create(cipherText.slice(22)).toString(EncUTF8),
 | 
			
		||||
        ),
 | 
			
		||||
      },
 | 
			
		||||
      META_KEY,
 | 
			
		||||
            {mode: ModeECB, padding: PKCS7}
 | 
			
		||||
      { mode: ModeECB, padding: PKCS7 },
 | 
			
		||||
    ).toString(EncUTF8);
 | 
			
		||||
 | 
			
		||||
        const labelIndex = plainText.indexOf(":");
 | 
			
		||||
    const labelIndex = plainText.indexOf(':');
 | 
			
		||||
    let result: NcmMusicMeta;
 | 
			
		||||
        if (plainText.slice(0, labelIndex) === "dj") {
 | 
			
		||||
    if (plainText.slice(0, labelIndex) === 'dj') {
 | 
			
		||||
      const tmp: NcmDjMeta = JSON.parse(plainText.slice(labelIndex + 1));
 | 
			
		||||
      result = tmp.mainMusic;
 | 
			
		||||
    } else {
 | 
			
		||||
      result = JSON.parse(plainText.slice(labelIndex + 1));
 | 
			
		||||
    }
 | 
			
		||||
    if (!!result.albumPic) {
 | 
			
		||||
            result.albumPic = result.albumPic.replace("http://", "https://") + "?param=500y500"
 | 
			
		||||
      result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500';
 | 
			
		||||
    }
 | 
			
		||||
        return result
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getAudio(keyBox: Uint8Array): Uint8Array {
 | 
			
		||||
        this.offset += this.view.getUint32(this.offset + 5, true) + 13
 | 
			
		||||
        const audioData = new Uint8Array(this.raw, this.offset)
 | 
			
		||||
        let lenAudioData = audioData.length
 | 
			
		||||
        for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff]
 | 
			
		||||
        return audioData
 | 
			
		||||
    this.offset += this.view.getUint32(this.offset + 5, true) + 13;
 | 
			
		||||
    const audioData = new Uint8Array(this.raw, this.offset);
 | 
			
		||||
    let lenAudioData = audioData.length;
 | 
			
		||||
    for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff];
 | 
			
		||||
    return audioData;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async _buildMeta() {
 | 
			
		||||
        if (!this.oriMeta) throw Error("invalid sequence")
 | 
			
		||||
    if (!this.oriMeta) throw Error('invalid sequence');
 | 
			
		||||
 | 
			
		||||
        const info = GetMetaFromFile(this.filename, this.oriMeta.musicName)
 | 
			
		||||
    const info = GetMetaFromFile(this.filename, this.oriMeta.musicName);
 | 
			
		||||
 | 
			
		||||
    // build artists
 | 
			
		||||
    let artists: string[] = [];
 | 
			
		||||
    if (!!this.oriMeta.artist) {
 | 
			
		||||
            this.oriMeta.artist.forEach(arr => artists.push(<string>arr[0]));
 | 
			
		||||
      this.oriMeta.artist.forEach((arr) => artists.push(<string>arr[0]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (artists.length === 0 && !!info.artist) {
 | 
			
		||||
            artists = info.artist.split(',')
 | 
			
		||||
                .map(val => val.trim()).filter(val => val != "");
 | 
			
		||||
      artists = info.artist
 | 
			
		||||
        .split(',')
 | 
			
		||||
        .map((val) => val.trim())
 | 
			
		||||
        .filter((val) => val != '');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (this.oriMeta.albumPic) try {
 | 
			
		||||
            this.image = await GetImageFromURL(this.oriMeta.albumPic)
 | 
			
		||||
    if (this.oriMeta.albumPic)
 | 
			
		||||
      try {
 | 
			
		||||
        this.image = await GetImageFromURL(this.oriMeta.albumPic);
 | 
			
		||||
        while (this.image && this.image.buffer.byteLength >= 1 << 24) {
 | 
			
		||||
                let img = await jimp.read(Buffer.from(this.image.buffer))
 | 
			
		||||
                await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO)
 | 
			
		||||
                this.image.buffer = await img.getBufferAsync("image/jpeg")
 | 
			
		||||
          let img = await jimp.read(Buffer.from(this.image.buffer));
 | 
			
		||||
          await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
 | 
			
		||||
          this.image.buffer = await img.getBufferAsync('image/jpeg');
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
            console.log("get cover image failed", e)
 | 
			
		||||
        console.log('get cover image failed', e);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        this.newMeta = {title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer}
 | 
			
		||||
    this.newMeta = { title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async _writeMeta() {
 | 
			
		||||
        if (!this.audio || !this.newMeta) throw Error("invalid sequence")
 | 
			
		||||
    if (!this.audio || !this.newMeta) throw Error('invalid sequence');
 | 
			
		||||
 | 
			
		||||
        if (!this.blob) this.blob = new Blob([this.audio], {type: this.mime})
 | 
			
		||||
    if (!this.blob) this.blob = new Blob([this.audio], { type: this.mime });
 | 
			
		||||
    const ori = await metaParseBlob(this.blob);
 | 
			
		||||
 | 
			
		||||
        let shouldWrite = !ori.common.album && !ori.common.artists && !ori.common.title
 | 
			
		||||
    let shouldWrite = !ori.common.album && !ori.common.artists && !ori.common.title;
 | 
			
		||||
    if (shouldWrite || this.newMeta.picture) {
 | 
			
		||||
            if (this.format === "mp3") {
 | 
			
		||||
                this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori)
 | 
			
		||||
            } else if (this.format === "flac") {
 | 
			
		||||
                this.audio = WriteMetaToFlac(Buffer.from(this.audio), this.newMeta, ori)
 | 
			
		||||
      if (this.format === 'mp3') {
 | 
			
		||||
        this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori);
 | 
			
		||||
      } else if (this.format === 'flac') {
 | 
			
		||||
        this.audio = WriteMetaToFlac(Buffer.from(this.audio), this.newMeta, ori);
 | 
			
		||||
      } else {
 | 
			
		||||
                console.info(`writing meta for ${this.format} is not being supported for now`)
 | 
			
		||||
                return
 | 
			
		||||
        console.info(`writing meta for ${this.format} is not being supported for now`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
            this.blob = new Blob([this.audio], {type: this.mime})
 | 
			
		||||
      this.blob = new Blob([this.audio], { type: this.mime });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  gatherResult(): DecryptResult {
 | 
			
		||||
        if (!this.newMeta || !this.blob) throw Error("bad sequence")
 | 
			
		||||
    if (!this.newMeta || !this.blob) throw Error('bad sequence');
 | 
			
		||||
    return {
 | 
			
		||||
      title: this.newMeta.title,
 | 
			
		||||
            artist: this.newMeta.artists?.join("; "),
 | 
			
		||||
      artist: this.newMeta.artists?.join('; '),
 | 
			
		||||
      ext: this.format,
 | 
			
		||||
      album: this.newMeta.album,
 | 
			
		||||
      picture: this.image?.url,
 | 
			
		||||
      file: URL.createObjectURL(this.blob),
 | 
			
		||||
      blob: this.blob,
 | 
			
		||||
            mime: this.mime
 | 
			
		||||
        }
 | 
			
		||||
      mime: this.mime,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async decrypt() {
 | 
			
		||||
        const keyBox = this._getKeyBox()
 | 
			
		||||
        this.oriMeta = this._getMetaData()
 | 
			
		||||
        this.audio = this._getAudio(keyBox)
 | 
			
		||||
        this.format = this.oriMeta.format || SniffAudioExt(this.audio)
 | 
			
		||||
        this.mime = AudioMimeType[this.format]
 | 
			
		||||
        await this._buildMeta()
 | 
			
		||||
    const keyBox = this._getKeyBox();
 | 
			
		||||
    this.oriMeta = this._getMetaData();
 | 
			
		||||
    this.audio = this._getAudio(keyBox);
 | 
			
		||||
    this.format = this.oriMeta.format || SniffAudioExt(this.audio);
 | 
			
		||||
    this.mime = AudioMimeType[this.format];
 | 
			
		||||
    await this._buildMeta();
 | 
			
		||||
    try {
 | 
			
		||||
            await this._writeMeta()
 | 
			
		||||
      await this._writeMeta();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
            console.warn("write meta data failed", e)
 | 
			
		||||
      console.warn('write meta data failed', e);
 | 
			
		||||
    }
 | 
			
		||||
        return this.gatherResult()
 | 
			
		||||
    return this.gatherResult();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,19 @@
 | 
			
		||||
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> {
 | 
			
		||||
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
 | 
			
		||||
  let length = buffer.length;
 | 
			
		||||
  for (let i = 0; i < length; i++) {
 | 
			
		||||
        buffer[i] ^= 163
 | 
			
		||||
    buffer[i] ^= 163;
 | 
			
		||||
  }
 | 
			
		||||
  const ext = SniffAudioExt(buffer, raw_ext);
 | 
			
		||||
    if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]})
 | 
			
		||||
  if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
 | 
			
		||||
  const tag = await metaParseBlob(file);
 | 
			
		||||
    const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
 | 
			
		||||
  const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    title,
 | 
			
		||||
@ -24,6 +23,6 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
			
		||||
    picture: GetCoverFromFile(tag),
 | 
			
		||||
    file: URL.createObjectURL(file),
 | 
			
		||||
    blob: file,
 | 
			
		||||
        mime: AudioMimeType[ext]
 | 
			
		||||
    }
 | 
			
		||||
    mime: AudioMimeType[ext],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import {QmcDecoder} from "@/decrypt/qmc";
 | 
			
		||||
import {BytesEqual} from "@/decrypt/utils";
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import { QmcDecoder } from '@/decrypt/qmc';
 | 
			
		||||
import { BytesEqual } from '@/decrypt/utils';
 | 
			
		||||
 | 
			
		||||
function loadTestDataDecoder(name: string): {
 | 
			
		||||
  cipherText: Uint8Array,
 | 
			
		||||
  clearText: Uint8Array
 | 
			
		||||
  cipherText: Uint8Array;
 | 
			
		||||
  clearText: Uint8Array;
 | 
			
		||||
} {
 | 
			
		||||
  const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
 | 
			
		||||
  const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
 | 
			
		||||
@ -13,20 +13,17 @@ function loadTestDataDecoder(name: string): {
 | 
			
		||||
  cipherText.set(cipherSuffix, cipherBody.length);
 | 
			
		||||
  return {
 | 
			
		||||
    cipherText,
 | 
			
		||||
    clearText: fs.readFileSync(`testdata/${name}_target.bin`)
 | 
			
		||||
  }
 | 
			
		||||
    clearText: fs.readFileSync(`testdata/${name}_target.bin`),
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test("qmc: real file", async () => {
 | 
			
		||||
  const cases = ["mflac0_rc4", "mflac_map", "mgg_map", "qmc0_static"]
 | 
			
		||||
test('qmc: real file', async () => {
 | 
			
		||||
  const cases = ['mflac0_rc4', 'mflac_map', 'mgg_map', 'qmc0_static'];
 | 
			
		||||
  for (const name of cases) {
 | 
			
		||||
    const {clearText, cipherText} = loadTestDataDecoder(name)
 | 
			
		||||
    const c = new QmcDecoder(cipherText)
 | 
			
		||||
    const buf = c.decrypt()
 | 
			
		||||
    const { clearText, cipherText } = loadTestDataDecoder(name);
 | 
			
		||||
    const c = new QmcDecoder(cipherText);
 | 
			
		||||
    const buf = c.decrypt();
 | 
			
		||||
 | 
			
		||||
    expect(BytesEqual(buf, clearText)).toBeTruthy()
 | 
			
		||||
    expect(BytesEqual(buf, clearText)).toBeTruthy();
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher} from "./qmc_cipher";
 | 
			
		||||
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher';
 | 
			
		||||
import {
 | 
			
		||||
  AudioMimeType,
 | 
			
		||||
  GetArrayBuffer,
 | 
			
		||||
@ -7,44 +7,43 @@ import {
 | 
			
		||||
  GetMetaFromFile,
 | 
			
		||||
  SniffAudioExt,
 | 
			
		||||
  WriteMetaToFlac,
 | 
			
		||||
  WriteMetaToMp3
 | 
			
		||||
} from "@/decrypt/utils";
 | 
			
		||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
			
		||||
import {DecryptQMCWasm} from "./qmc_wasm";
 | 
			
		||||
  WriteMetaToMp3,
 | 
			
		||||
} from '@/decrypt/utils';
 | 
			
		||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
			
		||||
import { DecryptQMCWasm } from './qmc_wasm';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import iconv from "iconv-lite";
 | 
			
		||||
import {DecryptResult} from "@/decrypt/entity";
 | 
			
		||||
import {queryAlbumCover} from "@/utils/api";
 | 
			
		||||
import {QmcDeriveKey} from "@/decrypt/qmc_key";
 | 
			
		||||
import iconv from 'iconv-lite';
 | 
			
		||||
import { DecryptResult } from '@/decrypt/entity';
 | 
			
		||||
import { queryAlbumCover } from '@/utils/api';
 | 
			
		||||
import { QmcDeriveKey } from '@/decrypt/qmc_key';
 | 
			
		||||
 | 
			
		||||
interface Handler {
 | 
			
		||||
  ext: string
 | 
			
		||||
  version: number
 | 
			
		||||
  ext: string;
 | 
			
		||||
  version: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const HandlerMap: { [key: string]: Handler } = {
 | 
			
		||||
  "mgg": {ext: "ogg", version: 2},
 | 
			
		||||
  "mgg1": {ext: "ogg", version: 2},
 | 
			
		||||
  "mflac": {ext: "flac", version: 2},
 | 
			
		||||
  "mflac0": {ext: "flac", version: 2},
 | 
			
		||||
  mgg: { ext: 'ogg', version: 2 },
 | 
			
		||||
  mgg1: { ext: 'ogg', version: 2 },
 | 
			
		||||
  mflac: { ext: 'flac', version: 2 },
 | 
			
		||||
  mflac0: { ext: 'flac', version: 2 },
 | 
			
		||||
 | 
			
		||||
  // qmcflac / qmcogg:
 | 
			
		||||
  // 有可能是 v2 加密但混用同一个后缀名。
 | 
			
		||||
  "qmcflac": {ext: "flac", version: 2},
 | 
			
		||||
  "qmcogg": {ext: "ogg", version: 2},
 | 
			
		||||
  qmcflac: { ext: 'flac', version: 2 },
 | 
			
		||||
  qmcogg: { ext: 'ogg', version: 2 },
 | 
			
		||||
 | 
			
		||||
  "qmc0": {ext: "mp3", version: 1},
 | 
			
		||||
  "qmc2": {ext: "ogg", version: 1},
 | 
			
		||||
  "qmc3": {ext: "mp3", version: 1},
 | 
			
		||||
  "bkcmp3": {ext: "mp3", version: 1},
 | 
			
		||||
  "bkcflac": {ext: "flac", version: 1},
 | 
			
		||||
  "tkm": {ext: "m4a", version: 1},
 | 
			
		||||
  "666c6163": {ext: "flac", version: 1},
 | 
			
		||||
  "6d7033": {ext: "mp3", version: 1},
 | 
			
		||||
  "6f6767": {ext: "ogg", version: 1},
 | 
			
		||||
  "6d3461": {ext: "m4a", version: 1},
 | 
			
		||||
  "776176": {ext: "wav", version: 1}
 | 
			
		||||
  qmc0: { ext: 'mp3', version: 1 },
 | 
			
		||||
  qmc2: { ext: 'ogg', version: 1 },
 | 
			
		||||
  qmc3: { ext: 'mp3', version: 1 },
 | 
			
		||||
  bkcmp3: { ext: 'mp3', version: 1 },
 | 
			
		||||
  bkcflac: { ext: 'flac', version: 1 },
 | 
			
		||||
  tkm: { ext: 'm4a', version: 1 },
 | 
			
		||||
  '666c6163': { ext: 'flac', version: 1 },
 | 
			
		||||
  '6d7033': { ext: 'mp3', version: 1 },
 | 
			
		||||
  '6f6767': { ext: 'ogg', version: 1 },
 | 
			
		||||
  '6d3461': { ext: 'm4a', version: 1 },
 | 
			
		||||
  '776176': { ext: 'wav', version: 1 },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
			
		||||
@ -56,7 +55,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
			
		||||
  let musicDecoded: Uint8Array | undefined;
 | 
			
		||||
 | 
			
		||||
  if (version === 2 && globalThis.WebAssembly) {
 | 
			
		||||
    console.log("qmc: using wasm decoder")
 | 
			
		||||
    console.log('qmc: using wasm decoder');
 | 
			
		||||
    const v2Decrypted = await DecryptQMCWasm(fileBuffer);
 | 
			
		||||
    // 如果 v2 检测失败,降级到 v1 再尝试一次
 | 
			
		||||
    if (v2Decrypted) {
 | 
			
		||||
@ -65,9 +64,9 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
			
		||||
  }
 | 
			
		||||
  if (!musicDecoded) {
 | 
			
		||||
    // may throw error
 | 
			
		||||
    console.log("qmc: using js decoder")
 | 
			
		||||
    const d = new QmcDecoder(new Uint8Array(fileBuffer))
 | 
			
		||||
    musicDecoded = d.decrypt()
 | 
			
		||||
    console.log('qmc: using js decoder');
 | 
			
		||||
    const d = new QmcDecoder(new Uint8Array(fileBuffer));
 | 
			
		||||
    musicDecoded = d.decrypt();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ext = SniffAudioExt(musicDecoded, handler.ext);
 | 
			
		||||
@ -77,16 +76,16 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
			
		||||
 | 
			
		||||
  const musicMeta = await metaParseBlob(musicBlob);
 | 
			
		||||
  for (let metaIdx in musicMeta.native) {
 | 
			
		||||
    if (!musicMeta.native.hasOwnProperty(metaIdx)) continue
 | 
			
		||||
    if (musicMeta.native[metaIdx].some(item => item.id === "TCON" && item.value === "(12)")) {
 | 
			
		||||
      console.warn("try using gbk encoding to decode meta")
 | 
			
		||||
      musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ""), "gbk");
 | 
			
		||||
      musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ""), "gbk");
 | 
			
		||||
      musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ""), "gbk");
 | 
			
		||||
    if (!musicMeta.native.hasOwnProperty(metaIdx)) continue;
 | 
			
		||||
    if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) {
 | 
			
		||||
      console.warn('try using gbk encoding to decode meta');
 | 
			
		||||
      musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ''), 'gbk');
 | 
			
		||||
      musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ''), 'gbk');
 | 
			
		||||
      musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ''), 'gbk');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
 | 
			
		||||
  const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
 | 
			
		||||
 | 
			
		||||
  let imgUrl = GetCoverFromFile(musicMeta);
 | 
			
		||||
  if (!imgUrl) {
 | 
			
		||||
@ -94,20 +93,20 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
			
		||||
    if (imgUrl) {
 | 
			
		||||
      const imageInfo = await GetImageFromURL(imgUrl);
 | 
			
		||||
      if (imageInfo) {
 | 
			
		||||
        imgUrl = imageInfo.url
 | 
			
		||||
        imgUrl = imageInfo.url;
 | 
			
		||||
        try {
 | 
			
		||||
          const newMeta = {picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(" _ ")}
 | 
			
		||||
          if (ext === "mp3") {
 | 
			
		||||
            musicDecoded = WriteMetaToMp3(Buffer.from(musicDecoded), newMeta, musicMeta)
 | 
			
		||||
          const newMeta = { picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(' _ ') };
 | 
			
		||||
          if (ext === 'mp3') {
 | 
			
		||||
            musicDecoded = WriteMetaToMp3(Buffer.from(musicDecoded), newMeta, musicMeta);
 | 
			
		||||
            musicBlob = new Blob([musicDecoded], { type: mime });
 | 
			
		||||
          } else if (ext === 'flac') {
 | 
			
		||||
            musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta)
 | 
			
		||||
            musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta);
 | 
			
		||||
            musicBlob = new Blob([musicDecoded], { type: mime });
 | 
			
		||||
          } else {
 | 
			
		||||
            console.info("writing metadata for " + ext + " is not being supported for now")
 | 
			
		||||
            console.info('writing metadata for ' + ext + ' is not being supported for now');
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.warn("Error while appending cover image to file " + e)
 | 
			
		||||
          console.warn('Error while appending cover image to file ' + e);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -120,86 +119,83 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
			
		||||
    picture: imgUrl,
 | 
			
		||||
    file: URL.createObjectURL(musicBlob),
 | 
			
		||||
    blob: musicBlob,
 | 
			
		||||
    mime: mime
 | 
			
		||||
    mime: mime,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async function getCoverImage(title: string, artist?: string, album?: string): Promise<string> {
 | 
			
		||||
  const song_query_url = "https://stats.ixarea.com/apis" + "/music/qq-cover"
 | 
			
		||||
  const song_query_url = 'https://stats.ixarea.com/apis' + '/music/qq-cover';
 | 
			
		||||
  try {
 | 
			
		||||
    const data = await queryAlbumCover(title, artist, album)
 | 
			
		||||
    return `${song_query_url}/${data.Type}/${data.Id}`
 | 
			
		||||
    const data = await queryAlbumCover(title, artist, album);
 | 
			
		||||
    return `${song_query_url}/${data.Type}/${data.Id}`;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.warn(e);
 | 
			
		||||
  }
 | 
			
		||||
  return ""
 | 
			
		||||
  return '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class QmcDecoder {
 | 
			
		||||
  file: Uint8Array
 | 
			
		||||
  size: number
 | 
			
		||||
  decoded: boolean = false
 | 
			
		||||
  audioSize?: number
 | 
			
		||||
  private static readonly BYTE_COMMA = ','.charCodeAt(0)
 | 
			
		||||
  cipher?: QmcStreamCipher
 | 
			
		||||
  private static readonly BYTE_COMMA = ','.charCodeAt(0);
 | 
			
		||||
  file: Uint8Array;
 | 
			
		||||
  size: number;
 | 
			
		||||
  decoded: boolean = false;
 | 
			
		||||
  audioSize?: number;
 | 
			
		||||
  cipher?: QmcStreamCipher;
 | 
			
		||||
 | 
			
		||||
  constructor(file: Uint8Array) {
 | 
			
		||||
    this.file = file
 | 
			
		||||
    this.size = file.length
 | 
			
		||||
    this.searchKey()
 | 
			
		||||
    this.file = file;
 | 
			
		||||
    this.size = file.length;
 | 
			
		||||
    this.searchKey();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  decrypt(): Uint8Array {
 | 
			
		||||
    if (!this.cipher) {
 | 
			
		||||
      throw new Error("no cipher found")
 | 
			
		||||
      throw new Error('no cipher found');
 | 
			
		||||
    }
 | 
			
		||||
    if (!this.audioSize || this.audioSize <= 0) {
 | 
			
		||||
      throw new Error("invalid audio size")
 | 
			
		||||
      throw new Error('invalid audio size');
 | 
			
		||||
    }
 | 
			
		||||
    const audioBuf = this.file.subarray(0, this.audioSize)
 | 
			
		||||
    const audioBuf = this.file.subarray(0, this.audioSize);
 | 
			
		||||
 | 
			
		||||
    if (!this.decoded) {
 | 
			
		||||
      this.cipher.decrypt(audioBuf, 0)
 | 
			
		||||
      this.decoded = true
 | 
			
		||||
      this.cipher.decrypt(audioBuf, 0);
 | 
			
		||||
      this.decoded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return audioBuf
 | 
			
		||||
    return audioBuf;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private searchKey() {
 | 
			
		||||
    const last4Byte = this.file.slice(-4);
 | 
			
		||||
    const textEnc = new TextDecoder()
 | 
			
		||||
    const textEnc = new TextDecoder();
 | 
			
		||||
    if (textEnc.decode(last4Byte) === 'QTag') {
 | 
			
		||||
      const sizeBuf = this.file.slice(-8, -4)
 | 
			
		||||
      const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset)
 | 
			
		||||
      const keySize = sizeView.getUint32(0, false)
 | 
			
		||||
      this.audioSize = this.size - keySize - 8
 | 
			
		||||
      const rawKey = this.file.subarray(this.audioSize, this.size - 8)
 | 
			
		||||
      const keyEnd = rawKey.findIndex(v => v == QmcDecoder.BYTE_COMMA)
 | 
			
		||||
      this.setCipher(rawKey.subarray(0, keyEnd))
 | 
			
		||||
      const sizeBuf = this.file.slice(-8, -4);
 | 
			
		||||
      const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset);
 | 
			
		||||
      const keySize = sizeView.getUint32(0, false);
 | 
			
		||||
      this.audioSize = this.size - keySize - 8;
 | 
			
		||||
      const rawKey = this.file.subarray(this.audioSize, this.size - 8);
 | 
			
		||||
      const keyEnd = rawKey.findIndex((v) => v == QmcDecoder.BYTE_COMMA);
 | 
			
		||||
      this.setCipher(rawKey.subarray(0, keyEnd));
 | 
			
		||||
    } else {
 | 
			
		||||
      const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
 | 
			
		||||
      const keySize = sizeView.getUint32(0, true)
 | 
			
		||||
      const keySize = sizeView.getUint32(0, true);
 | 
			
		||||
      if (keySize < 0x300) {
 | 
			
		||||
        this.audioSize = this.size - keySize - 4
 | 
			
		||||
        const rawKey = this.file.subarray(this.audioSize, this.size - 4)
 | 
			
		||||
        this.setCipher(rawKey)
 | 
			
		||||
        this.audioSize = this.size - keySize - 4;
 | 
			
		||||
        const rawKey = this.file.subarray(this.audioSize, this.size - 4);
 | 
			
		||||
        this.setCipher(rawKey);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.audioSize = this.size
 | 
			
		||||
        this.cipher = new QmcStaticCipher()
 | 
			
		||||
        this.audioSize = this.size;
 | 
			
		||||
        this.cipher = new QmcStaticCipher();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setCipher(keyRaw: Uint8Array) {
 | 
			
		||||
    const keyDec = QmcDeriveKey(keyRaw)
 | 
			
		||||
    const keyDec = QmcDeriveKey(keyRaw);
 | 
			
		||||
    if (keyDec.length > 300) {
 | 
			
		||||
      this.cipher = new QmcRC4Cipher(keyDec)
 | 
			
		||||
      this.cipher = new QmcRC4Cipher(keyDec);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.cipher = new QmcMapCipher(keyDec)
 | 
			
		||||
      this.cipher = new QmcMapCipher(keyDec);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,9 +1,9 @@
 | 
			
		||||
export interface QmcStreamCipher {
 | 
			
		||||
  decrypt(buf: Uint8Array, offset: number): void
 | 
			
		||||
  decrypt(buf: Uint8Array, offset: number): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export class QmcStaticCipher implements QmcStreamCipher {
 | 
			
		||||
  //prettier-ignore
 | 
			
		||||
  private static readonly staticCipherBox: Uint8Array = new Uint8Array([
 | 
			
		||||
    0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
 | 
			
		||||
    0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
 | 
			
		||||
@ -40,26 +40,26 @@ export class QmcStaticCipher implements QmcStreamCipher {
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  public getMask(offset: number) {
 | 
			
		||||
    if (offset > 0x7FFF) offset %= 0x7FFF
 | 
			
		||||
    return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff]
 | 
			
		||||
    if (offset > 0x7fff) offset %= 0x7fff;
 | 
			
		||||
    return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public decrypt(buf: Uint8Array, offset: number) {
 | 
			
		||||
    for (let i = 0; i < buf.length; i++) {
 | 
			
		||||
      buf[i] ^= this.getMask(offset + i)
 | 
			
		||||
      buf[i] ^= this.getMask(offset + i);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class QmcMapCipher implements QmcStreamCipher {
 | 
			
		||||
  key: Uint8Array
 | 
			
		||||
  n: number
 | 
			
		||||
  key: Uint8Array;
 | 
			
		||||
  n: number;
 | 
			
		||||
 | 
			
		||||
  constructor(key: Uint8Array) {
 | 
			
		||||
    if (key.length == 0) throw Error("qmc/cipher_map: invalid key size")
 | 
			
		||||
    if (key.length == 0) throw Error('qmc/cipher_map: invalid key size');
 | 
			
		||||
 | 
			
		||||
    this.key = key
 | 
			
		||||
    this.n = key.length
 | 
			
		||||
    this.key = key;
 | 
			
		||||
    this.n = key.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static rotate(value: number, bits: number) {
 | 
			
		||||
@ -71,7 +71,7 @@ export class QmcMapCipher implements QmcStreamCipher {
 | 
			
		||||
 | 
			
		||||
  decrypt(buf: Uint8Array, offset: number): void {
 | 
			
		||||
    for (let i = 0; i < buf.length; i++) {
 | 
			
		||||
      buf[i] ^= this.getMask(offset + i)
 | 
			
		||||
      buf[i] ^= this.getMask(offset + i);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -79,27 +79,26 @@ export class QmcMapCipher implements QmcStreamCipher {
 | 
			
		||||
    if (offset > 0x7fff) offset %= 0x7fff;
 | 
			
		||||
 | 
			
		||||
    const idx = (offset * offset + 71214) % this.n;
 | 
			
		||||
    return QmcMapCipher.rotate(this.key[idx], idx & 0x7)
 | 
			
		||||
    return QmcMapCipher.rotate(this.key[idx], idx & 0x7);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class QmcRC4Cipher implements QmcStreamCipher {
 | 
			
		||||
  private static readonly FIRST_SEGMENT_SIZE = 0x80;
 | 
			
		||||
  private static readonly SEGMENT_SIZE = 5120
 | 
			
		||||
  private static readonly SEGMENT_SIZE = 5120;
 | 
			
		||||
 | 
			
		||||
  S: Uint8Array
 | 
			
		||||
  N: number
 | 
			
		||||
  key: Uint8Array
 | 
			
		||||
  hash: number
 | 
			
		||||
  S: Uint8Array;
 | 
			
		||||
  N: number;
 | 
			
		||||
  key: Uint8Array;
 | 
			
		||||
  hash: number;
 | 
			
		||||
 | 
			
		||||
  constructor(key: Uint8Array) {
 | 
			
		||||
    if (key.length == 0) {
 | 
			
		||||
      throw Error("invalid key size")
 | 
			
		||||
      throw Error('invalid key size');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.key = key
 | 
			
		||||
    this.N = key.length
 | 
			
		||||
    this.key = key;
 | 
			
		||||
    this.N = key.length;
 | 
			
		||||
 | 
			
		||||
    // init seed box
 | 
			
		||||
    this.S = new Uint8Array(this.N);
 | 
			
		||||
@ -109,7 +108,7 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
			
		||||
    let j = 0;
 | 
			
		||||
    for (let i = 0; i < this.N; ++i) {
 | 
			
		||||
      j = (this.S[i] + j + this.key[i % this.N]) % this.N;
 | 
			
		||||
      [this.S[i], this.S[j]] = [this.S[j], this.S[i]]
 | 
			
		||||
      [this.S[i], this.S[j]] = [this.S[j], this.S[i]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // init hash base
 | 
			
		||||
@ -125,7 +124,6 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
			
		||||
 | 
			
		||||
      this.hash = next_hash;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  decrypt(buf: Uint8Array, offset: number): void {
 | 
			
		||||
@ -133,52 +131,50 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
			
		||||
    let processed = 0;
 | 
			
		||||
    const postProcess = (len: number): boolean => {
 | 
			
		||||
      toProcess -= len;
 | 
			
		||||
      processed += len
 | 
			
		||||
      offset += len
 | 
			
		||||
      return toProcess == 0
 | 
			
		||||
    }
 | 
			
		||||
      processed += len;
 | 
			
		||||
      offset += len;
 | 
			
		||||
      return toProcess == 0;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Initial segment
 | 
			
		||||
    if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) {
 | 
			
		||||
      const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset);
 | 
			
		||||
      this.encFirstSegment(buf.subarray(0, len_segment), offset);
 | 
			
		||||
      if (postProcess(len_segment)) return
 | 
			
		||||
      if (postProcess(len_segment)) return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // align segment
 | 
			
		||||
    if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) {
 | 
			
		||||
      const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess);
 | 
			
		||||
      this.encASegment(buf.subarray(processed, processed + len_segment), offset);
 | 
			
		||||
      if (postProcess(len_segment)) return
 | 
			
		||||
      if (postProcess(len_segment)) return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Batch process segments
 | 
			
		||||
    while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
 | 
			
		||||
      this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
 | 
			
		||||
      postProcess(QmcRC4Cipher.SEGMENT_SIZE)
 | 
			
		||||
      postProcess(QmcRC4Cipher.SEGMENT_SIZE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Last segment (incomplete segment)
 | 
			
		||||
    if (toProcess > 0) {
 | 
			
		||||
      this.encASegment(buf.subarray(processed), offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private encFirstSegment(buf: Uint8Array, offset: number) {
 | 
			
		||||
    for (let i = 0; i < buf.length; i++) {
 | 
			
		||||
 | 
			
		||||
      buf[i] ^= this.key[this.getSegmentKey(offset + i)];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private encASegment(buf: Uint8Array, offset: number) {
 | 
			
		||||
    // Initialise a new seed box
 | 
			
		||||
    const S = this.S.slice(0)
 | 
			
		||||
    const S = this.S.slice(0);
 | 
			
		||||
 | 
			
		||||
    // Calculate the number of bytes to skip.
 | 
			
		||||
    // The initial "key" derived from segment id, plus the current offset.
 | 
			
		||||
    const skipLen = (offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(offset / QmcRC4Cipher.SEGMENT_SIZE)
 | 
			
		||||
    const skipLen = (offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(offset / QmcRC4Cipher.SEGMENT_SIZE);
 | 
			
		||||
 | 
			
		||||
    // decrypt the block
 | 
			
		||||
    let j = 0;
 | 
			
		||||
@ -186,7 +182,7 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
			
		||||
    for (let i = -skipLen; i < buf.length; i++) {
 | 
			
		||||
      j = (j + 1) % this.N;
 | 
			
		||||
      k = (S[j] + k) % this.N;
 | 
			
		||||
      [S[k], S[j]] = [S[j], S[k]]
 | 
			
		||||
      [S[k], S[j]] = [S[j], S[k]];
 | 
			
		||||
 | 
			
		||||
      if (i >= 0) {
 | 
			
		||||
        buf[i] ^= S[(S[j] + S[k]) % this.N];
 | 
			
		||||
@ -195,8 +191,8 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getSegmentKey(id: number): number {
 | 
			
		||||
    const seed = this.key[id % this.N]
 | 
			
		||||
    const idx = (this.hash / ((id + 1) * seed) * 100.0) | 0;
 | 
			
		||||
    return idx % this.N
 | 
			
		||||
    const seed = this.key[id % this.N];
 | 
			
		||||
    const idx = ((this.hash / ((id + 1) * seed)) * 100.0) | 0;
 | 
			
		||||
    return idx % this.N;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,78 +1,75 @@
 | 
			
		||||
import {TeaCipher} from "@/utils/tea";
 | 
			
		||||
import { TeaCipher } from '@/utils/tea';
 | 
			
		||||
 | 
			
		||||
const SALT_LEN = 2
 | 
			
		||||
const ZERO_LEN = 7
 | 
			
		||||
const SALT_LEN = 2;
 | 
			
		||||
const ZERO_LEN = 7;
 | 
			
		||||
 | 
			
		||||
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
 | 
			
		||||
  const textDec = new TextDecoder()
 | 
			
		||||
  const rawDec = Buffer.from(textDec.decode(raw), 'base64')
 | 
			
		||||
  const textDec = new TextDecoder();
 | 
			
		||||
  const rawDec = Buffer.from(textDec.decode(raw), 'base64');
 | 
			
		||||
  let n = rawDec.length;
 | 
			
		||||
  if (n < 16) {
 | 
			
		||||
    throw Error("key length is too short")
 | 
			
		||||
    throw Error('key length is too short');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const simpleKey = simpleMakeKey(106, 8)
 | 
			
		||||
  const simpleKey = simpleMakeKey(106, 8);
 | 
			
		||||
  let teaKey = new Uint8Array(16);
 | 
			
		||||
  for (let i = 0; i < 8; i++) {
 | 
			
		||||
    teaKey[i << 1] = simpleKey[i];
 | 
			
		||||
    teaKey[(i << 1) + 1] = rawDec[i];
 | 
			
		||||
  }
 | 
			
		||||
  const sub = decryptTencentTea(rawDec.subarray(8), teaKey)
 | 
			
		||||
  rawDec.set(sub, 8)
 | 
			
		||||
  return rawDec.subarray(0, 8 + sub.length)
 | 
			
		||||
 | 
			
		||||
  const sub = decryptTencentTea(rawDec.subarray(8), teaKey);
 | 
			
		||||
  rawDec.set(sub, 8);
 | 
			
		||||
  return rawDec.subarray(0, 8 + sub.length);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// simpleMakeKey exported only for unit test
 | 
			
		||||
export function simpleMakeKey(salt: number, length: number): number[] {
 | 
			
		||||
  const keyBuf: number[] = []
 | 
			
		||||
  const keyBuf: number[] = [];
 | 
			
		||||
  for (let i = 0; i < length; i++) {
 | 
			
		||||
    const tmp = Math.tan(salt + i * 0.1)
 | 
			
		||||
    keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0)
 | 
			
		||||
    const tmp = Math.tan(salt + i * 0.1);
 | 
			
		||||
    keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0);
 | 
			
		||||
  }
 | 
			
		||||
  return keyBuf
 | 
			
		||||
  return keyBuf;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
 | 
			
		||||
  if (inBuf.length % 8 != 0) {
 | 
			
		||||
    throw Error("inBuf size not a multiple of the block size")
 | 
			
		||||
    throw Error('inBuf size not a multiple of the block size');
 | 
			
		||||
  }
 | 
			
		||||
  if (inBuf.length < 16) {
 | 
			
		||||
    throw Error("inBuf size too small")
 | 
			
		||||
    throw Error('inBuf size too small');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const blk = new TeaCipher(key, 32)
 | 
			
		||||
  const blk = new TeaCipher(key, 32);
 | 
			
		||||
 | 
			
		||||
  const tmpBuf = new Uint8Array(8);
 | 
			
		||||
  const tmpView = new DataView(tmpBuf.buffer);
 | 
			
		||||
 | 
			
		||||
  blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8))
 | 
			
		||||
  blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8));
 | 
			
		||||
 | 
			
		||||
  const nPadLen = tmpBuf[0] & 0x7; //只要最低三位
 | 
			
		||||
  /*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
 | 
			
		||||
  const outLen = inBuf.length - 1 /*PadLen*/ - nPadLen - SALT_LEN - ZERO_LEN;
 | 
			
		||||
  const outBuf = new Uint8Array(outLen)
 | 
			
		||||
  const outBuf = new Uint8Array(outLen);
 | 
			
		||||
 | 
			
		||||
  let ivPrev = new Uint8Array(8);
 | 
			
		||||
  let ivCur = inBuf.slice(0, 8); // init iv
 | 
			
		||||
  let inBufPos = 8;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  // 跳过 Padding Len 和 Padding
 | 
			
		||||
  let tmpIdx = 1 + nPadLen;
 | 
			
		||||
 | 
			
		||||
  // CBC IV 处理
 | 
			
		||||
  const cryptBlock = () => {
 | 
			
		||||
    ivPrev = ivCur;
 | 
			
		||||
    ivCur = inBuf.slice(inBufPos, inBufPos + 8)
 | 
			
		||||
    ivCur = inBuf.slice(inBufPos, inBufPos + 8);
 | 
			
		||||
    for (let j = 0; j < 8; j++) {
 | 
			
		||||
      tmpBuf[j] ^= ivCur[j]
 | 
			
		||||
      tmpBuf[j] ^= ivCur[j];
 | 
			
		||||
    }
 | 
			
		||||
    blk.decrypt(tmpView, tmpView)
 | 
			
		||||
    blk.decrypt(tmpView, tmpView);
 | 
			
		||||
    inBufPos += 8;
 | 
			
		||||
    tmpIdx = 0;
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 跳过 Salt
 | 
			
		||||
  for (let i = 1; i <= SALT_LEN; ) {
 | 
			
		||||
@ -80,7 +77,7 @@ function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
 | 
			
		||||
      tmpIdx++;
 | 
			
		||||
      i++;
 | 
			
		||||
    } else {
 | 
			
		||||
      cryptBlock()
 | 
			
		||||
      cryptBlock();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -89,19 +86,18 @@ function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
 | 
			
		||||
  while (outBufPos < outLen) {
 | 
			
		||||
    if (tmpIdx < 8) {
 | 
			
		||||
      outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
 | 
			
		||||
      outBufPos++
 | 
			
		||||
      outBufPos++;
 | 
			
		||||
      tmpIdx++;
 | 
			
		||||
    } else {
 | 
			
		||||
      cryptBlock()
 | 
			
		||||
      cryptBlock();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 校验Zero
 | 
			
		||||
  for (let i = 1; i <= ZERO_LEN; i++) {
 | 
			
		||||
    if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) {
 | 
			
		||||
      throw Error("zero check failed")
 | 
			
		||||
      throw Error('zero check failed');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return outBuf
 | 
			
		||||
  return outBuf;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,13 +8,13 @@ const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
 | 
			
		||||
 | 
			
		||||
function MergeUint8Array(array: Uint8Array[]): Uint8Array {
 | 
			
		||||
  let length = 0;
 | 
			
		||||
  array.forEach(item => {
 | 
			
		||||
  array.forEach((item) => {
 | 
			
		||||
    length += item.length;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  let mergedArray = new Uint8Array(length);
 | 
			
		||||
  let offset = 0;
 | 
			
		||||
  array.forEach(item => {
 | 
			
		||||
  array.forEach((item) => {
 | 
			
		||||
    mergedArray.set(item, offset);
 | 
			
		||||
    offset += item.length;
 | 
			
		||||
  });
 | 
			
		||||
@ -42,16 +42,12 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
 | 
			
		||||
  const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
 | 
			
		||||
 | 
			
		||||
  // 进行检测
 | 
			
		||||
  const detectOK = QMCCrypto.detectKeyEndPosition(
 | 
			
		||||
    pDetectionResult,
 | 
			
		||||
    pDetectionBuf,
 | 
			
		||||
    detectionBuf.length
 | 
			
		||||
  );
 | 
			
		||||
  const detectOK = QMCCrypto.detectKeyEndPosition(pDetectionResult, pDetectionBuf, detectionBuf.length);
 | 
			
		||||
 | 
			
		||||
  // 提取结构体内容:
 | 
			
		||||
  // (pos: i32; len: i32; error: char[??])
 | 
			
		||||
  const position = QMCCrypto.getValue(pDetectionResult, "i32");
 | 
			
		||||
  const len = QMCCrypto.getValue(pDetectionResult + 4, "i32");
 | 
			
		||||
  const position = QMCCrypto.getValue(pDetectionResult, 'i32');
 | 
			
		||||
  const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32');
 | 
			
		||||
 | 
			
		||||
  // 释放内存
 | 
			
		||||
  QMCCrypto._free(pDetectionBuf);
 | 
			
		||||
@ -66,9 +62,7 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
 | 
			
		||||
  const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
 | 
			
		||||
 | 
			
		||||
  // 提取嵌入到文件的 EKey
 | 
			
		||||
  const ekey = new Uint8Array(
 | 
			
		||||
    mggBlob.slice(decryptedSize, decryptedSize + len)
 | 
			
		||||
  );
 | 
			
		||||
  const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
 | 
			
		||||
 | 
			
		||||
  // 解码 UTF-8 数据到 string
 | 
			
		||||
  const decoder = new TextDecoder();
 | 
			
		||||
@ -85,9 +79,7 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
 | 
			
		||||
    const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
 | 
			
		||||
 | 
			
		||||
    // 解密一些片段
 | 
			
		||||
    const blockData = new Uint8Array(
 | 
			
		||||
      mggBlob.slice(offset, offset + blockSize)
 | 
			
		||||
    );
 | 
			
		||||
    const blockData = new Uint8Array(mggBlob.slice(offset, offset + blockSize));
 | 
			
		||||
    QMCCrypto.writeArrayToMemory(blockData, buf);
 | 
			
		||||
    QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize);
 | 
			
		||||
    decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize));
 | 
			
		||||
 | 
			
		||||
@ -4,39 +4,38 @@ import {
 | 
			
		||||
  GetCoverFromFile,
 | 
			
		||||
  GetMetaFromFile,
 | 
			
		||||
  SniffAudioExt,
 | 
			
		||||
    SplitFilename
 | 
			
		||||
} from "@/decrypt/utils";
 | 
			
		||||
  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> {
 | 
			
		||||
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
 | 
			
		||||
  const buffer = new Uint8Array(await GetArrayBuffer(file));
 | 
			
		||||
    let length = buffer.length
 | 
			
		||||
  let length = buffer.length;
 | 
			
		||||
  for (let i = 0; i < length; i++) {
 | 
			
		||||
        buffer[i] ^= 0xf4
 | 
			
		||||
    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]})
 | 
			
		||||
  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"})
 | 
			
		||||
    audioBlob = new Blob([buffer], { type: 'application/octet-stream' });
 | 
			
		||||
    return QmcDecrypt(audioBlob, newName.name, newName.ext);
 | 
			
		||||
  } else {
 | 
			
		||||
        throw "不支持的QQ音乐缓存格式"
 | 
			
		||||
    throw '不支持的QQ音乐缓存格式';
 | 
			
		||||
  }
 | 
			
		||||
  const tag = await metaParseBlob(audioBlob);
 | 
			
		||||
    const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
 | 
			
		||||
  const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    title,
 | 
			
		||||
@ -46,6 +45,6 @@ export async function Decrypt(file: Blob, raw_filename: string, _: string)
 | 
			
		||||
    picture: GetCoverFromFile(tag),
 | 
			
		||||
    file: URL.createObjectURL(audioBlob),
 | 
			
		||||
    blob: audioBlob,
 | 
			
		||||
        mime: AudioMimeType[ext]
 | 
			
		||||
    }
 | 
			
		||||
    mime: AudioMimeType[ext],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,23 @@
 | 
			
		||||
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> {
 | 
			
		||||
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]})
 | 
			
		||||
    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)
 | 
			
		||||
  const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    title,
 | 
			
		||||
@ -23,6 +27,6 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string,
 | 
			
		||||
    picture: GetCoverFromFile(tag),
 | 
			
		||||
    file: URL.createObjectURL(file),
 | 
			
		||||
    blob: file,
 | 
			
		||||
        mime: AudioMimeType[ext]
 | 
			
		||||
    }
 | 
			
		||||
    mime: AudioMimeType[ext],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
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];
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,6 @@ export async function Decrypt(file: File, raw_filename: string): Promise<Decrypt
 | 
			
		||||
  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 musicData = new Blob([audioData], { type: 'audio/mp4' });
 | 
			
		||||
  return await RawDecrypt(musicData, raw_filename, 'm4a', false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,68 +1,64 @@
 | 
			
		||||
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
 | 
			
		||||
  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
 | 
			
		||||
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"
 | 
			
		||||
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()
 | 
			
		||||
  if (!!obj.arrayBuffer) return obj.arrayBuffer();
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    const reader = new FileReader();
 | 
			
		||||
    reader.onload = (e) => {
 | 
			
		||||
            const rs = e.target?.result
 | 
			
		||||
      const rs = e.target?.result;
 | 
			
		||||
      if (!rs) {
 | 
			
		||||
                reject("read file failed")
 | 
			
		||||
        reject('read file failed');
 | 
			
		||||
      } else {
 | 
			
		||||
                resolve(rs as ArrayBuffer)
 | 
			
		||||
        resolve(rs as ArrayBuffer);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    reader.readAsArrayBuffer(obj);
 | 
			
		||||
@ -71,22 +67,25 @@ export function GetArrayBuffer(obj: Blob): Promise<ArrayBuffer> {
 | 
			
		||||
 | 
			
		||||
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 URL.createObjectURL(
 | 
			
		||||
      new Blob([metadata.common.picture[0].data], { type: metadata.common.picture[0].format }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
    return "";
 | 
			
		||||
  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}
 | 
			
		||||
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) {
 | 
			
		||||
@ -95,83 +94,83 @@ export function GetMetaFromFile(filename: string, exist_title?: string, exist_ar
 | 
			
		||||
  } else if (items.length === 1) {
 | 
			
		||||
    if (!meta.title) meta.title = items[0].trim();
 | 
			
		||||
  }
 | 
			
		||||
    return meta
 | 
			
		||||
  return meta;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function GetImageFromURL(src: string):
 | 
			
		||||
    Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
 | 
			
		||||
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 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}
 | 
			
		||||
      const url = URL.createObjectURL(new Blob([buffer], { type: mime }));
 | 
			
		||||
      return { buffer, url, mime };
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
        console.warn(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 => {
 | 
			
		||||
  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) {
 | 
			
		||||
        writer.setFrame(frame.id, frame.value);
 | 
			
		||||
      } catch (e) {}
 | 
			
		||||
    }
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
    const old = original.common
 | 
			
		||||
    writer.setFrame('TPE1', old?.artists || info.artists || [])
 | 
			
		||||
  const old = original.common;
 | 
			
		||||
  writer
 | 
			
		||||
    .setFrame('TPE1', old?.artists || info.artists || [])
 | 
			
		||||
    .setFrame('TIT2', old?.title || info.title)
 | 
			
		||||
        .setFrame('TALB', old?.album || info.album || "");
 | 
			
		||||
    .setFrame('TALB', old?.album || info.album || '');
 | 
			
		||||
  if (info.picture) {
 | 
			
		||||
    writer.setFrame('APIC', {
 | 
			
		||||
      type: 3,
 | 
			
		||||
      data: info.picture,
 | 
			
		||||
            description: info.picture_desc || "Cover",
 | 
			
		||||
        })
 | 
			
		||||
      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
 | 
			
		||||
  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)
 | 
			
		||||
    writer.setTag('TITLE=' + info.title);
 | 
			
		||||
    writer.setTag('ALBUM=' + info.album);
 | 
			
		||||
    if (info.artists) {
 | 
			
		||||
            writer.removeTag("ARTIST")
 | 
			
		||||
            info.artists.forEach(artist => writer.setTag("ARTIST=" + artist))
 | 
			
		||||
      writer.removeTag('ARTIST');
 | 
			
		||||
      info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (info.picture) {
 | 
			
		||||
        writer.importPictureFromBuffer(Buffer.from(info.picture))
 | 
			
		||||
    writer.importPictureFromBuffer(Buffer.from(info.picture));
 | 
			
		||||
  }
 | 
			
		||||
    return writer.save()
 | 
			
		||||
  return writer.save();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function SplitFilename(n: string): { name: string; ext: string } {
 | 
			
		||||
    const pos = n.lastIndexOf(".")
 | 
			
		||||
  const pos = n.lastIndexOf('.');
 | 
			
		||||
  return {
 | 
			
		||||
    ext: n.substring(pos + 1).toLowerCase(),
 | 
			
		||||
        name: n.substring(0, pos)
 | 
			
		||||
    }
 | 
			
		||||
    name: n.substring(0, pos),
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,55 +1,57 @@
 | 
			
		||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
 | 
			
		||||
import {DecryptResult} from "@/decrypt/entity";
 | 
			
		||||
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile} from "@/decrypt/utils";
 | 
			
		||||
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
 | 
			
		||||
import { DecryptResult } from '@/decrypt/entity';
 | 
			
		||||
import { AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile } from '@/decrypt/utils';
 | 
			
		||||
 | 
			
		||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
			
		||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
			
		||||
 | 
			
		||||
const MagicHeader = [0x69, 0x66, 0x6D, 0x74]
 | 
			
		||||
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe]
 | 
			
		||||
const MagicHeader = [0x69, 0x66, 0x6d, 0x74];
 | 
			
		||||
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe];
 | 
			
		||||
const FileTypeMap: { [key: string]: string } = {
 | 
			
		||||
    " WAV": ".wav",
 | 
			
		||||
    "FLAC": ".flac",
 | 
			
		||||
    " MP3": ".mp3",
 | 
			
		||||
    " A4M": ".m4a",
 | 
			
		||||
}
 | 
			
		||||
  ' WAV': '.wav',
 | 
			
		||||
  FLAC: '.flac',
 | 
			
		||||
  ' MP3': '.mp3',
 | 
			
		||||
  ' A4M': '.m4a',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function Decrypt(file: File, 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") {
 | 
			
		||||
            throw Error("此xm文件已损坏")
 | 
			
		||||
    if (raw_ext === 'xm') {
 | 
			
		||||
      throw Error('此xm文件已损坏');
 | 
			
		||||
    } else {
 | 
			
		||||
            return await RawDecrypt(file, raw_filename, raw_ext, true)
 | 
			
		||||
      return await RawDecrypt(file, raw_filename, raw_ext, true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    let typeText = (new TextDecoder()).decode(oriData.slice(4, 8))
 | 
			
		||||
  let typeText = new TextDecoder().decode(oriData.slice(4, 8));
 | 
			
		||||
  if (!FileTypeMap.hasOwnProperty(typeText)) {
 | 
			
		||||
        throw Error("未知的.xm文件类型")
 | 
			
		||||
    throw Error('未知的.xm文件类型');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    let key = oriData[0xf]
 | 
			
		||||
    let dataOffset = oriData[0xc] | oriData[0xd] << 8 | oriData[0xe] << 16
 | 
			
		||||
  let key = oriData[0xf];
 | 
			
		||||
  let dataOffset = oriData[0xc] | (oriData[0xd] << 8) | (oriData[0xe] << 16);
 | 
			
		||||
  let audioData = oriData.slice(0x10);
 | 
			
		||||
  let lenAudioData = audioData.length;
 | 
			
		||||
    for (let cur = dataOffset; cur < lenAudioData; ++cur)
 | 
			
		||||
        audioData[cur] = (audioData[cur] - key) ^ 0xff;
 | 
			
		||||
  for (let cur = dataOffset; cur < lenAudioData; ++cur) audioData[cur] = (audioData[cur] - key) ^ 0xff;
 | 
			
		||||
 | 
			
		||||
  const ext = FileTypeMap[typeText];
 | 
			
		||||
  const mime = AudioMimeType[ext];
 | 
			
		||||
  let musicBlob = new Blob([audioData], { type: mime });
 | 
			
		||||
 | 
			
		||||
  const musicMeta = await metaParseBlob(musicBlob);
 | 
			
		||||
    if (ext === "wav") {
 | 
			
		||||
  if (ext === 'wav') {
 | 
			
		||||
    //todo:未知的编码方式
 | 
			
		||||
        console.info(musicMeta.common)
 | 
			
		||||
        musicMeta.common.album = "";
 | 
			
		||||
        musicMeta.common.artist = "";
 | 
			
		||||
        musicMeta.common.title = "";
 | 
			
		||||
    console.info(musicMeta.common);
 | 
			
		||||
    musicMeta.common.album = '';
 | 
			
		||||
    musicMeta.common.artist = '';
 | 
			
		||||
    musicMeta.common.title = '';
 | 
			
		||||
  }
 | 
			
		||||
    const {title, artist} = GetMetaFromFile(raw_filename,
 | 
			
		||||
        musicMeta.common.title, musicMeta.common.artist,
 | 
			
		||||
        raw_filename.indexOf("_") === -1 ? "-" : "_")
 | 
			
		||||
  const { title, artist } = GetMetaFromFile(
 | 
			
		||||
    raw_filename,
 | 
			
		||||
    musicMeta.common.title,
 | 
			
		||||
    musicMeta.common.artist,
 | 
			
		||||
    raw_filename.indexOf('_') === -1 ? '-' : '_',
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    title,
 | 
			
		||||
@ -60,7 +62,6 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
 | 
			
		||||
    picture: GetCoverFromFile(musicMeta),
 | 
			
		||||
    file: URL.createObjectURL(musicBlob),
 | 
			
		||||
    blob: musicBlob,
 | 
			
		||||
        rawExt: "xm"
 | 
			
		||||
    rawExt: 'xm',
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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));
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/main.ts
									
									
									
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
import Vue from 'vue'
 | 
			
		||||
import App from '@/App.vue'
 | 
			
		||||
import '@/registerServiceWorker'
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import App from '@/App.vue';
 | 
			
		||||
import '@/registerServiceWorker';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Checkbox,
 | 
			
		||||
@ -19,7 +19,7 @@ import {
 | 
			
		||||
  TableColumn,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Upload,
 | 
			
		||||
    MessageBox
 | 
			
		||||
  MessageBox,
 | 
			
		||||
} from 'element-ui';
 | 
			
		||||
import 'element-ui/lib/theme-chalk/base.css';
 | 
			
		||||
 | 
			
		||||
@ -44,5 +44,5 @@ Vue.prototype.$confirm = MessageBox.confirm;
 | 
			
		||||
 | 
			
		||||
Vue.config.productionTip = false;
 | 
			
		||||
new Vue({
 | 
			
		||||
    render: h => h(App),
 | 
			
		||||
  render: (h) => h(App),
 | 
			
		||||
}).$mount('#app');
 | 
			
		||||
 | 
			
		||||
@ -1,31 +1,30 @@
 | 
			
		||||
/* eslint-disable no-console */
 | 
			
		||||
 | 
			
		||||
import {register} from 'register-service-worker'
 | 
			
		||||
 | 
			
		||||
if (process.env.NODE_ENV === 'production' && window.location.protocol === "https:") {
 | 
			
		||||
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.')
 | 
			
		||||
      console.log('App is being served from cache by a service worker.');
 | 
			
		||||
    },
 | 
			
		||||
    registered() {
 | 
			
		||||
            console.log('Service worker has been registered.')
 | 
			
		||||
      console.log('Service worker has been registered.');
 | 
			
		||||
    },
 | 
			
		||||
    cached() {
 | 
			
		||||
            console.log('Content has been cached for offline use.')
 | 
			
		||||
      console.log('Content has been cached for offline use.');
 | 
			
		||||
    },
 | 
			
		||||
    updatefound() {
 | 
			
		||||
            console.log('New content is downloading.')
 | 
			
		||||
      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.')
 | 
			
		||||
      console.log('No internet connection found. App is running in offline mode.');
 | 
			
		||||
    },
 | 
			
		||||
    error(error) {
 | 
			
		||||
            console.error('Error during service worker registration:', error)
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
      console.error('Error during service worker registration:', error);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								src/shims-browser-id3-writer.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								src/shims-browser-id3-writer.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1,25 +1,23 @@
 | 
			
		||||
declare module "browser-id3-writer" {
 | 
			
		||||
declare module 'browser-id3-writer' {
 | 
			
		||||
  export default class ID3Writer {
 | 
			
		||||
        constructor(buffer: Buffer | ArrayBuffer)
 | 
			
		||||
    constructor(buffer: Buffer | ArrayBuffer);
 | 
			
		||||
 | 
			
		||||
        setFrame(name: string, value: string | object | string[])
 | 
			
		||||
    setFrame(name: string, value: string | object | string[]);
 | 
			
		||||
 | 
			
		||||
        addTag(): Uint8Array
 | 
			
		||||
    addTag(): Uint8Array;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module "metaflac-js" {
 | 
			
		||||
declare module 'metaflac-js' {
 | 
			
		||||
  export default class Metaflac {
 | 
			
		||||
        constructor(buffer: Buffer)
 | 
			
		||||
    constructor(buffer: Buffer);
 | 
			
		||||
 | 
			
		||||
        setTag(field: string)
 | 
			
		||||
    setTag(field: string);
 | 
			
		||||
 | 
			
		||||
        removeTag(name: string)
 | 
			
		||||
    removeTag(name: string);
 | 
			
		||||
 | 
			
		||||
        importPictureFromBuffer(picture: Buffer)
 | 
			
		||||
    importPictureFromBuffer(picture: Buffer);
 | 
			
		||||
 | 
			
		||||
        save(): Buffer
 | 
			
		||||
    save(): Buffer;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								src/shims-fs.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								src/shims-fs.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1,58 +1,54 @@
 | 
			
		||||
export interface FileSystemGetFileOptions {
 | 
			
		||||
    create?: boolean
 | 
			
		||||
  create?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FileSystemCreateWritableOptions {
 | 
			
		||||
    keepExistingData?: boolean
 | 
			
		||||
  keepExistingData?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FileSystemRemoveOptions {
 | 
			
		||||
    recursive?: boolean
 | 
			
		||||
  recursive?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FileSystemFileHandle {
 | 
			
		||||
  getFile(): Promise<File>;
 | 
			
		||||
 | 
			
		||||
    createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>
 | 
			
		||||
  createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum WriteCommandType {
 | 
			
		||||
    write = "write",
 | 
			
		||||
    seek = "seek",
 | 
			
		||||
    truncate = "truncate",
 | 
			
		||||
  write = 'write',
 | 
			
		||||
  seek = 'seek',
 | 
			
		||||
  truncate = 'truncate',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface WriteParams {
 | 
			
		||||
    type: WriteCommandType
 | 
			
		||||
    size?: number
 | 
			
		||||
    position?: number
 | 
			
		||||
    data: BufferSource | Blob | string
 | 
			
		||||
  type: WriteCommandType;
 | 
			
		||||
  size?: number;
 | 
			
		||||
  position?: number;
 | 
			
		||||
  data: BufferSource | Blob | string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams
 | 
			
		||||
type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams;
 | 
			
		||||
 | 
			
		||||
interface FileSystemWritableFileStream extends WritableStream {
 | 
			
		||||
    write(data: FileSystemWriteChunkType): Promise<undefined>
 | 
			
		||||
  write(data: FileSystemWriteChunkType): Promise<undefined>;
 | 
			
		||||
 | 
			
		||||
    seek(position: number): Promise<undefined>
 | 
			
		||||
  seek(position: number): Promise<undefined>;
 | 
			
		||||
 | 
			
		||||
    truncate(size: number): Promise<undefined>
 | 
			
		||||
  truncate(size: number): Promise<undefined>;
 | 
			
		||||
 | 
			
		||||
    close(): Promise<undefined> // should be implemented in WritableStream
 | 
			
		||||
  close(): Promise<undefined>; // should be implemented in WritableStream
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export declare interface FileSystemDirectoryHandle {
 | 
			
		||||
    getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>
 | 
			
		||||
 | 
			
		||||
    removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<undefined>
 | 
			
		||||
  getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>;
 | 
			
		||||
 | 
			
		||||
  removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<undefined>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
 | 
			
		||||
        showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>
 | 
			
		||||
    showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								src/shims-tsx.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								src/shims-tsx.d.ts
									
									
									
									
										vendored
									
									
								
							@ -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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								src/shims-vue.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/shims-vue.d.ts
									
									
									
									
										vendored
									
									
								
							@ -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})
 | 
			
		||||
  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"},
 | 
			
		||||
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
 | 
			
		||||
      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"},
 | 
			
		||||
  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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,74 +4,67 @@
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in https://go.dev/LICENSE.
 | 
			
		||||
 | 
			
		||||
import {TeaCipher} from "@/utils/tea";
 | 
			
		||||
import { TeaCipher } from '@/utils/tea';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
test("key size", () => {
 | 
			
		||||
test('key size', () => {
 | 
			
		||||
  // prettier-ignore
 | 
			
		||||
  const testKey = new Uint8Array([
 | 
			
		||||
    0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
 | 
			
		||||
        0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
 | 
			
		||||
        0x00
 | 
			
		||||
    0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
 | 
			
		||||
    0x00,
 | 
			
		||||
  ])
 | 
			
		||||
    expect(() => new TeaCipher(testKey.slice(0, 16)))
 | 
			
		||||
        .not.toThrow()
 | 
			
		||||
  expect(() => new TeaCipher(testKey.slice(0, 16))).not.toThrow();
 | 
			
		||||
 | 
			
		||||
    expect(() => new TeaCipher(testKey))
 | 
			
		||||
        .toThrow()
 | 
			
		||||
 | 
			
		||||
    expect(() => new TeaCipher(testKey.slice(0, 15)))
 | 
			
		||||
        .toThrow()
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
  expect(() => new TeaCipher(testKey)).toThrow();
 | 
			
		||||
 | 
			
		||||
  expect(() => new TeaCipher(testKey.slice(0, 15))).toThrow();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
const teaTests = [
 | 
			
		||||
  // These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
 | 
			
		||||
  {
 | 
			
		||||
    rounds: TeaCipher.numRounds,
 | 
			
		||||
    key: new Uint8Array([
 | 
			
		||||
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
 | 
			
		||||
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
    ]),
 | 
			
		||||
    plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
 | 
			
		||||
    cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    rounds: TeaCipher.numRounds,
 | 
			
		||||
    key: new Uint8Array([
 | 
			
		||||
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
 | 
			
		||||
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
 | 
			
		||||
      0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
 | 
			
		||||
    ]),
 | 
			
		||||
    plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
 | 
			
		||||
    cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    rounds: 16,
 | 
			
		||||
    key: new Uint8Array([
 | 
			
		||||
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
 | 
			
		||||
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
    ]),
 | 
			
		||||
    plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
 | 
			
		||||
    cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
 | 
			
		||||
  },
 | 
			
		||||
]
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
test("rounds", () => {
 | 
			
		||||
test('rounds', () => {
 | 
			
		||||
  const tt = teaTests[0];
 | 
			
		||||
    expect(() => new TeaCipher(tt.key, tt.rounds - 1))
 | 
			
		||||
        .toThrow()
 | 
			
		||||
})
 | 
			
		||||
  expect(() => new TeaCipher(tt.key, tt.rounds - 1)).toThrow();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
test("encrypt & decrypt", () => {
 | 
			
		||||
test('encrypt & decrypt', () => {
 | 
			
		||||
  for (const tt of teaTests) {
 | 
			
		||||
        const c = new TeaCipher(tt.key, tt.rounds)
 | 
			
		||||
    const c = new TeaCipher(tt.key, tt.rounds);
 | 
			
		||||
 | 
			
		||||
        const buf = new Uint8Array(8)
 | 
			
		||||
        const bufView = new DataView(buf.buffer)
 | 
			
		||||
    const buf = new Uint8Array(8);
 | 
			
		||||
    const bufView = new DataView(buf.buffer);
 | 
			
		||||
 | 
			
		||||
        c.encrypt(bufView, new DataView(tt.plainText.buffer))
 | 
			
		||||
        expect(buf).toStrictEqual(tt.cipherText)
 | 
			
		||||
    c.encrypt(bufView, new DataView(tt.plainText.buffer));
 | 
			
		||||
    expect(buf).toStrictEqual(tt.cipherText);
 | 
			
		||||
 | 
			
		||||
        c.decrypt(bufView, new DataView(tt.cipherText.buffer))
 | 
			
		||||
        expect(buf).toStrictEqual(tt.plainText)
 | 
			
		||||
    c.decrypt(bufView, new DataView(tt.cipherText.buffer));
 | 
			
		||||
    expect(buf).toStrictEqual(tt.plainText);
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -27,56 +27,54 @@ export class TeaCipher {
 | 
			
		||||
  // numRounds 64 is the standard number of rounds in TEA.
 | 
			
		||||
  static readonly numRounds = 64;
 | 
			
		||||
 | 
			
		||||
    k0: number
 | 
			
		||||
    k1: number
 | 
			
		||||
    k2: number
 | 
			
		||||
    k3: number
 | 
			
		||||
    rounds: number
 | 
			
		||||
  k0: number;
 | 
			
		||||
  k1: number;
 | 
			
		||||
  k2: number;
 | 
			
		||||
  k3: number;
 | 
			
		||||
  rounds: number;
 | 
			
		||||
 | 
			
		||||
  constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
 | 
			
		||||
    if (key.length != 16) {
 | 
			
		||||
            throw Error("incorrect key size")
 | 
			
		||||
      throw Error('incorrect key size');
 | 
			
		||||
    }
 | 
			
		||||
    if ((rounds & 1) != 0) {
 | 
			
		||||
            throw Error("odd number of rounds specified")
 | 
			
		||||
      throw Error('odd number of rounds specified');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        const k = new DataView(key.buffer)
 | 
			
		||||
        this.k0 = k.getUint32(0, false)
 | 
			
		||||
        this.k1 = k.getUint32(4, false)
 | 
			
		||||
        this.k2 = k.getUint32(8, false)
 | 
			
		||||
        this.k3 = k.getUint32(12, false)
 | 
			
		||||
        this.rounds = rounds
 | 
			
		||||
    const k = new DataView(key.buffer);
 | 
			
		||||
    this.k0 = k.getUint32(0, false);
 | 
			
		||||
    this.k1 = k.getUint32(4, false);
 | 
			
		||||
    this.k2 = k.getUint32(8, false);
 | 
			
		||||
    this.k3 = k.getUint32(12, false);
 | 
			
		||||
    this.rounds = rounds;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  encrypt(dst: DataView, src: DataView) {
 | 
			
		||||
    let v0 = src.getUint32(0, false);
 | 
			
		||||
    let v1 = src.getUint32(4, false);
 | 
			
		||||
 | 
			
		||||
        let v0 = src.getUint32(0, false)
 | 
			
		||||
        let v1 = src.getUint32(4, false)
 | 
			
		||||
 | 
			
		||||
        let sum = 0
 | 
			
		||||
    let sum = 0;
 | 
			
		||||
    for (let i = 0; i < this.rounds / 2; i++) {
 | 
			
		||||
            sum = sum + TeaCipher.delta
 | 
			
		||||
            v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1)
 | 
			
		||||
            v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3)
 | 
			
		||||
      sum = sum + TeaCipher.delta;
 | 
			
		||||
      v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
 | 
			
		||||
      v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        dst.setUint32(0, v0, false)
 | 
			
		||||
        dst.setUint32(4, v1, false)
 | 
			
		||||
    dst.setUint32(0, v0, false);
 | 
			
		||||
    dst.setUint32(4, v1, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  decrypt(dst: DataView, src: DataView) {
 | 
			
		||||
        let v0 = src.getUint32(0, false)
 | 
			
		||||
        let v1 = src.getUint32(4, false)
 | 
			
		||||
    let v0 = src.getUint32(0, false);
 | 
			
		||||
    let v1 = src.getUint32(4, false);
 | 
			
		||||
 | 
			
		||||
        let sum = TeaCipher.delta * this.rounds / 2
 | 
			
		||||
    let sum = (TeaCipher.delta * this.rounds) / 2;
 | 
			
		||||
    for (let i = 0; i < this.rounds / 2; i++) {
 | 
			
		||||
            v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3)
 | 
			
		||||
            v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1)
 | 
			
		||||
            sum -= TeaCipher.delta
 | 
			
		||||
      v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
 | 
			
		||||
      v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
 | 
			
		||||
      sum -= TeaCipher.delta;
 | 
			
		||||
    }
 | 
			
		||||
        dst.setUint32(0, v0, false)
 | 
			
		||||
        dst.setUint32(4, v1, false)
 | 
			
		||||
    dst.setUint32(0, v0, false);
 | 
			
		||||
    dst.setUint32(4, v1, false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import {DecryptResult} from "@/decrypt/entity";
 | 
			
		||||
import {FileSystemDirectoryHandle} from "@/shims-fs";
 | 
			
		||||
import { DecryptResult } from '@/decrypt/entity';
 | 
			
		||||
import { FileSystemDirectoryHandle } from '@/shims-fs';
 | 
			
		||||
 | 
			
		||||
export enum FilenamePolicy {
 | 
			
		||||
  ArtistAndTitle,
 | 
			
		||||
@ -8,12 +8,12 @@ export enum FilenamePolicy {
 | 
			
		||||
  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) {
 | 
			
		||||
@ -30,24 +30,22 @@ export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) {
 | 
			
		||||
    let filename = GetDownloadFilename(data, policy)
 | 
			
		||||
  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()
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
  a.download = GetDownloadFilename(data, policy);
 | 
			
		||||
  document.body.append(a);
 | 
			
		||||
  a.click();
 | 
			
		||||
  a.remove();
 | 
			
		||||
@ -55,7 +53,7 @@ export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) {
 | 
			
		||||
 | 
			
		||||
export function RemoveBlobMusic(data: DecryptResult) {
 | 
			
		||||
  URL.revokeObjectURL(data.file);
 | 
			
		||||
    if (data.picture?.startsWith("blob:")) {
 | 
			
		||||
  if (data.picture?.startsWith('blob:')) {
 | 
			
		||||
    URL.revokeObjectURL(data.picture);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -64,16 +62,19 @@ export class DecryptQueue {
 | 
			
		||||
  private readonly pending: (() => Promise<void>)[];
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
        this.pending = []
 | 
			
		||||
    this.pending = [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  queue(fn: () => Promise<void>) {
 | 
			
		||||
        this.pending.push(fn)
 | 
			
		||||
        this.consume()
 | 
			
		||||
    this.pending.push(fn);
 | 
			
		||||
    this.consume();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private consume() {
 | 
			
		||||
        const fn = this.pending.shift()
 | 
			
		||||
        if (fn) fn().then(() => this.consume).catch(console.error)
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,7 @@
 | 
			
		||||
    <div id="app-control">
 | 
			
		||||
      <el-row class="mb-3">
 | 
			
		||||
        <span>歌曲命名格式:</span>
 | 
			
		||||
                <el-radio v-for="k in FilenamePolicies" :key="k.key"
 | 
			
		||||
                          v-model="filename_policy" :label="k.key">
 | 
			
		||||
        <el-radio v-for="k in FilenamePolicies" :key="k.key" v-model="filename_policy" :label="k.key">
 | 
			
		||||
          {{ k.text }}
 | 
			
		||||
        </el-radio>
 | 
			
		||||
      </el-row>
 | 
			
		||||
@ -16,7 +15,7 @@
 | 
			
		||||
 | 
			
		||||
        <el-tooltip class="item" effect="dark" placement="top-start">
 | 
			
		||||
          <div slot="content">
 | 
			
		||||
                        <span v-if="instant_save">工作模式: {{ dir ? "写入本地文件系统" : "调用浏览器下载" }}</span>
 | 
			
		||||
            <span v-if="instant_save">工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}</span>
 | 
			
		||||
            <span v-else>
 | 
			
		||||
              当您使用此工具进行大量文件解锁的时候,建议开启此选项。<br />
 | 
			
		||||
              开启后,解锁结果将不会存留于浏览器中,防止内存不足。
 | 
			
		||||
@ -34,62 +33,64 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
import FileSelector from "@/component/FileSelector"
 | 
			
		||||
import PreviewTable from "@/component/PreviewTable"
 | 
			
		||||
import {DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile} from "@/utils/utils"
 | 
			
		||||
import FileSelector from '@/component/FileSelector';
 | 
			
		||||
import PreviewTable from '@/component/PreviewTable';
 | 
			
		||||
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Home',
 | 
			
		||||
  components: {
 | 
			
		||||
    FileSelector,
 | 
			
		||||
        PreviewTable
 | 
			
		||||
    PreviewTable,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      tableData: [],
 | 
			
		||||
            playing_url: "",
 | 
			
		||||
      playing_url: '',
 | 
			
		||||
      playing_auto: false,
 | 
			
		||||
      filename_policy: FilenamePolicy.ArtistAndTitle,
 | 
			
		||||
      instant_save: false,
 | 
			
		||||
      FilenamePolicies,
 | 
			
		||||
            dir: null
 | 
			
		||||
        }
 | 
			
		||||
      dir: null,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    instant_save(val) {
 | 
			
		||||
            if (val) this.showDirectlySave()
 | 
			
		||||
        }
 | 
			
		||||
      if (val) this.showDirectlySave();
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async showSuccess(data) {
 | 
			
		||||
      if (this.instant_save) {
 | 
			
		||||
                await this.saveFile(data)
 | 
			
		||||
        await this.saveFile(data);
 | 
			
		||||
        RemoveBlobMusic(data);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.tableData.push(data);
 | 
			
		||||
        this.$notify.success({
 | 
			
		||||
          title: '解锁成功',
 | 
			
		||||
          message: '成功解锁 ' + data.title,
 | 
			
		||||
                    duration: 3000
 | 
			
		||||
          duration: 3000,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
        let _rp_data = [data.title, data.artist, data.album];
 | 
			
		||||
                window._paq.push(["trackEvent", "Unlock", data.rawExt + "," + data.mime, JSON.stringify(_rp_data)]);
 | 
			
		||||
        window._paq.push(['trackEvent', 'Unlock', data.rawExt + ',' + data.mime, JSON.stringify(_rp_data)]);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    showFail(errInfo, filename) {
 | 
			
		||||
            console.error(errInfo, filename)
 | 
			
		||||
      console.error(errInfo, filename);
 | 
			
		||||
      this.$notify.error({
 | 
			
		||||
        title: '出现问题',
 | 
			
		||||
                message: errInfo + "," + filename +
 | 
			
		||||
        message:
 | 
			
		||||
          errInfo +
 | 
			
		||||
          ',' +
 | 
			
		||||
          filename +
 | 
			
		||||
          ',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
 | 
			
		||||
        dangerouslyUseHTMLString: true,
 | 
			
		||||
                duration: 6000
 | 
			
		||||
        duration: 6000,
 | 
			
		||||
      });
 | 
			
		||||
      if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
                window._paq.push(["trackEvent", "Error", String(errInfo), filename]);
 | 
			
		||||
        window._paq.push(['trackEvent', 'Error', String(errInfo), filename]);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    changePlaying(url) {
 | 
			
		||||
@ -97,7 +98,7 @@ export default {
 | 
			
		||||
      this.playing_auto = true;
 | 
			
		||||
    },
 | 
			
		||||
    handleDeleteAll() {
 | 
			
		||||
            this.tableData.forEach(value => {
 | 
			
		||||
      this.tableData.forEach((value) => {
 | 
			
		||||
        RemoveBlobMusic(value);
 | 
			
		||||
      });
 | 
			
		||||
      this.tableData = [];
 | 
			
		||||
@ -106,7 +107,7 @@ export default {
 | 
			
		||||
      let index = 0;
 | 
			
		||||
      let c = setInterval(() => {
 | 
			
		||||
        if (index < this.tableData.length) {
 | 
			
		||||
                    this.saveFile(this.tableData[index])
 | 
			
		||||
          this.saveFile(this.tableData[index]);
 | 
			
		||||
          index++;
 | 
			
		||||
        } else {
 | 
			
		||||
          clearInterval(c);
 | 
			
		||||
@ -116,42 +117,40 @@ export default {
 | 
			
		||||
 | 
			
		||||
    async saveFile(data) {
 | 
			
		||||
      if (this.dir) {
 | 
			
		||||
                await DirectlyWriteFile(data, this.filename_policy, this.dir)
 | 
			
		||||
        await DirectlyWriteFile(data, this.filename_policy, this.dir);
 | 
			
		||||
        this.$notify({
 | 
			
		||||
                    title: "保存成功",
 | 
			
		||||
          title: '保存成功',
 | 
			
		||||
          message: data.title,
 | 
			
		||||
                    position: "top-left",
 | 
			
		||||
                    type: "success",
 | 
			
		||||
                    duration: 3000
 | 
			
		||||
                })
 | 
			
		||||
          position: 'top-left',
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          duration: 3000,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
                DownloadBlobMusic(data, this.filename_policy)
 | 
			
		||||
        DownloadBlobMusic(data, this.filename_policy);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    async showDirectlySave() {
 | 
			
		||||
            if (!window.showDirectoryPicker) return
 | 
			
		||||
      if (!window.showDirectoryPicker) return;
 | 
			
		||||
      try {
 | 
			
		||||
                await this.$confirm("您的浏览器支持文件直接保存到磁盘,是否使用?",
 | 
			
		||||
                    "新特性提示", {
 | 
			
		||||
                        confirmButtonText: "使用",
 | 
			
		||||
                        cancelButtonText: "不使用",
 | 
			
		||||
                        type: "warning",
 | 
			
		||||
                        center: true
 | 
			
		||||
                    })
 | 
			
		||||
        await this.$confirm('您的浏览器支持文件直接保存到磁盘,是否使用?', '新特性提示', {
 | 
			
		||||
          confirmButtonText: '使用',
 | 
			
		||||
          cancelButtonText: '不使用',
 | 
			
		||||
          type: 'warning',
 | 
			
		||||
          center: true,
 | 
			
		||||
        });
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
                console.log(e)
 | 
			
		||||
                return
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      try {
 | 
			
		||||
                this.dir = await window.showDirectoryPicker()
 | 
			
		||||
                const test_filename = "__unlock_music_write_test.txt"
 | 
			
		||||
                await this.dir.getFileHandle(test_filename, {create: true})
 | 
			
		||||
                await this.dir.removeEntry(test_filename)
 | 
			
		||||
        this.dir = await window.showDirectoryPicker();
 | 
			
		||||
        const test_filename = '__unlock_music_write_test.txt';
 | 
			
		||||
        await this.dir.getFileHandle(test_filename, { create: true });
 | 
			
		||||
        await this.dir.removeEntry(test_filename);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
                console.error(e)
 | 
			
		||||
            }
 | 
			
		||||
        console.error(e);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user