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
					
				
							
								
								
									
										150
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										150
									
								
								src/App.vue
									
									
									
									
									
								
							@ -1,85 +1,87 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <el-container id="app">
 | 
					  <el-container id="app">
 | 
				
			||||||
        <el-main>
 | 
					    <el-main>
 | 
				
			||||||
            <Home/>
 | 
					      <Home />
 | 
				
			||||||
        </el-main>
 | 
					    </el-main>
 | 
				
			||||||
        <el-footer id="app-footer">
 | 
					    <el-footer id="app-footer">
 | 
				
			||||||
            <el-row>
 | 
					      <el-row>
 | 
				
			||||||
                <a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }})
 | 
					        <a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }})
 | 
				
			||||||
                :移除已购音乐的加密保护。
 | 
					        :移除已购音乐的加密保护。
 | 
				
			||||||
                <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
 | 
					        <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
 | 
				
			||||||
            </el-row>
 | 
					      </el-row>
 | 
				
			||||||
            <el-row>
 | 
					      <el-row>
 | 
				
			||||||
                目前支持网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
 | 
					        目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
 | 
				
			||||||
                <a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。
 | 
					        <a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。
 | 
				
			||||||
            </el-row>
 | 
					      </el-row>
 | 
				
			||||||
            <el-row>
 | 
					      <el-row>
 | 
				
			||||||
                <!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
 | 
					        <!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
 | 
				
			||||||
                <span>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>
 | 
					        <a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
 | 
				
			||||||
                开放源代码
 | 
					        开放源代码
 | 
				
			||||||
            </el-row>
 | 
					      </el-row>
 | 
				
			||||||
        </el-footer>
 | 
					    </el-footer>
 | 
				
			||||||
    </el-container>
 | 
					  </el-container>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import FileSelector from '@/component/FileSelector';
 | 
				
			||||||
import FileSelector from "@/component/FileSelector"
 | 
					import PreviewTable from '@/component/PreviewTable';
 | 
				
			||||||
import PreviewTable from "@/component/PreviewTable"
 | 
					import config from '@/../package.json';
 | 
				
			||||||
import config from "@/../package.json"
 | 
					import Home from '@/view/Home';
 | 
				
			||||||
import Home from "@/view/Home";
 | 
					import { checkUpdate } from '@/utils/api';
 | 
				
			||||||
import {checkUpdate} from "@/utils/api";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    name: 'app',
 | 
					  name: 'app',
 | 
				
			||||||
    components: {
 | 
					  components: {
 | 
				
			||||||
        FileSelector,
 | 
					    FileSelector,
 | 
				
			||||||
        PreviewTable,
 | 
					    PreviewTable,
 | 
				
			||||||
        Home
 | 
					    Home,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      version: config.version,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  created() {
 | 
				
			||||||
 | 
					    this.$nextTick(() => this.finishLoad());
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    async finishLoad() {
 | 
				
			||||||
 | 
					      const mask = document.getElementById('loader-mask');
 | 
				
			||||||
 | 
					      if (!!mask) mask.remove();
 | 
				
			||||||
 | 
					      let updateInfo;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        updateInfo = await checkUpdate(this.version);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.warn('check version info failed', e);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        updateInfo &&
 | 
				
			||||||
 | 
					        process.env.NODE_ENV === 'production' &&
 | 
				
			||||||
 | 
					        (updateInfo.HttpsFound || (updateInfo.Found && window.location.protocol !== 'https:'))
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        this.$notify.warning({
 | 
				
			||||||
 | 
					          title: '发现更新',
 | 
				
			||||||
 | 
					          message: `发现新版本 v${updateInfo.Version}<br/>更新详情:${updateInfo.Detail}<br/> <a target="_blank" href="${updateInfo.URL}">获取更新</a>`,
 | 
				
			||||||
 | 
					          dangerouslyUseHTMLString: true,
 | 
				
			||||||
 | 
					          duration: 15000,
 | 
				
			||||||
 | 
					          position: 'top-left',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.$notify.info({
 | 
				
			||||||
 | 
					          title: '离线使用',
 | 
				
			||||||
 | 
					          message: `我们使用PWA技术,无网络也能使用<br/>最近更新:${config.updateInfo}<br/><a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>`,
 | 
				
			||||||
 | 
					          dangerouslyUseHTMLString: true,
 | 
				
			||||||
 | 
					          duration: 10000,
 | 
				
			||||||
 | 
					          position: 'top-left',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data() {
 | 
					  },
 | 
				
			||||||
        return {
 | 
					};
 | 
				
			||||||
            version: config.version,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    created() {
 | 
					 | 
				
			||||||
        this.$nextTick(() => this.finishLoad());
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    methods: {
 | 
					 | 
				
			||||||
        async finishLoad() {
 | 
					 | 
				
			||||||
            const mask = document.getElementById("loader-mask");
 | 
					 | 
				
			||||||
            if (!!mask) mask.remove();
 | 
					 | 
				
			||||||
            let updateInfo;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                updateInfo = await checkUpdate(this.version)
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
                console.warn("check version info failed", e)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if ((updateInfo && process.env.NODE_ENV === 'production') && (updateInfo.HttpsFound ||
 | 
					 | 
				
			||||||
                (updateInfo.Found && window.location.protocol !== "https:"))) {
 | 
					 | 
				
			||||||
                this.$notify.warning({
 | 
					 | 
				
			||||||
                    title: '发现更新',
 | 
					 | 
				
			||||||
                    message: `发现新版本 v${updateInfo.Version}<br/>更新详情:${updateInfo.Detail}<br/> <a target="_blank" href="${updateInfo.URL}">获取更新</a>`,
 | 
					 | 
				
			||||||
                    dangerouslyUseHTMLString: true,
 | 
					 | 
				
			||||||
                    duration: 15000,
 | 
					 | 
				
			||||||
                    position: 'top-left'
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                this.$notify.info({
 | 
					 | 
				
			||||||
                    title: '离线使用',
 | 
					 | 
				
			||||||
                    message: `我们使用PWA技术,无网络也能使用<br/>最近更新:${config.updateInfo}<br/><a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>`,
 | 
					 | 
				
			||||||
                    dangerouslyUseHTMLString: true,
 | 
					 | 
				
			||||||
                    duration: 10000,
 | 
					 | 
				
			||||||
                    position: 'top-left'
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss">
 | 
					<style lang="scss">
 | 
				
			||||||
@import "scss/unlock-music";
 | 
					@import 'scss/unlock-music';
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,99 +1,90 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <el-upload
 | 
					  <el-upload :auto-upload="false" :on-change="addFile" :show-file-list="false" action="" drag multiple>
 | 
				
			||||||
        :auto-upload="false"
 | 
					    <i class="el-icon-upload" />
 | 
				
			||||||
        :on-change="addFile"
 | 
					    <div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
 | 
				
			||||||
        :show-file-list="false"
 | 
					    <div slot="tip" class="el-upload__tip">
 | 
				
			||||||
        action=""
 | 
					      <div>
 | 
				
			||||||
        drag
 | 
					        仅在浏览器内对文件进行解锁,无需消耗流量
 | 
				
			||||||
        multiple>
 | 
					        <el-tooltip effect="dark" placement="top-start">
 | 
				
			||||||
        <i class="el-icon-upload"/>
 | 
					          <div slot="content">算法在源代码中已经提供,所有运算都发生在本地</div>
 | 
				
			||||||
        <div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
 | 
					          <i class="el-icon-info" style="font-size: 12px" />
 | 
				
			||||||
        <div slot="tip" class="el-upload__tip">
 | 
					        </el-tooltip>
 | 
				
			||||||
            <div>
 | 
					      </div>
 | 
				
			||||||
                仅在浏览器内对文件进行解锁,无需消耗流量
 | 
					      <div>
 | 
				
			||||||
                <el-tooltip effect="dark" placement="top-start">
 | 
					        工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
 | 
				
			||||||
                    <div slot="content">
 | 
					        <el-tooltip effect="dark" placement="top-start">
 | 
				
			||||||
                        算法在源代码中已经提供,所有运算都发生在本地
 | 
					          <div slot="content">
 | 
				
			||||||
                    </div>
 | 
					            将此工具部署在HTTPS环境下,可以启用Web Worker特性,<br />
 | 
				
			||||||
                    <i class="el-icon-info" style="font-size: 12px"/>
 | 
					            从而更快的利用并行处理完成解锁
 | 
				
			||||||
                </el-tooltip>
 | 
					          </div>
 | 
				
			||||||
            </div>
 | 
					          <i class="el-icon-info" style="font-size: 12px" />
 | 
				
			||||||
            <div>
 | 
					        </el-tooltip>
 | 
				
			||||||
                工作模式: {{ parallel ? "多线程 Worker" : "单线程 Queue" }}
 | 
					      </div>
 | 
				
			||||||
                <el-tooltip effect="dark" placement="top-start">
 | 
					    </div>
 | 
				
			||||||
                    <div slot="content">
 | 
					    <transition name="el-fade-in"
 | 
				
			||||||
                        将此工具部署在HTTPS环境下,可以启用Web Worker特性,<br/>
 | 
					      ><!--todo: add delay to animation-->
 | 
				
			||||||
                        从而更快的利用并行处理完成解锁
 | 
					      <el-progress
 | 
				
			||||||
                    </div>
 | 
					        v-show="progress_show"
 | 
				
			||||||
                    <i class="el-icon-info" style="font-size: 12px"/>
 | 
					        :format="progress_string"
 | 
				
			||||||
                </el-tooltip>
 | 
					        :percentage="progress_value"
 | 
				
			||||||
            </div>
 | 
					        :stroke-width="16"
 | 
				
			||||||
        </div>
 | 
					        :text-inside="true"
 | 
				
			||||||
        <transition name="el-fade-in"><!--todo: add delay to animation-->
 | 
					        style="margin: 16px 6px 0 6px"
 | 
				
			||||||
            <el-progress
 | 
					      ></el-progress>
 | 
				
			||||||
                v-show="progress_show" :format="progress_string" :percentage="progress_value"
 | 
					    </transition>
 | 
				
			||||||
                :stroke-width="16" :text-inside="true"
 | 
					  </el-upload>
 | 
				
			||||||
                style="margin: 16px 6px 0 6px"
 | 
					 | 
				
			||||||
            ></el-progress>
 | 
					 | 
				
			||||||
        </transition>
 | 
					 | 
				
			||||||
    </el-upload>
 | 
					 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import {spawn, Worker, Pool} from "threads"
 | 
					import { spawn, Worker, Pool } from 'threads';
 | 
				
			||||||
import {CommonDecrypt} from "@/decrypt/common.ts";
 | 
					import { CommonDecrypt } from '@/decrypt/common.ts';
 | 
				
			||||||
import {DecryptQueue} from "@/utils/utils";
 | 
					import { DecryptQueue } from '@/utils/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    name: "FileSelector",
 | 
					  name: 'FileSelector',
 | 
				
			||||||
    data() {
 | 
					  data() {
 | 
				
			||||||
        return {
 | 
					    return {
 | 
				
			||||||
            task_all: 0,
 | 
					      task_all: 0,
 | 
				
			||||||
            task_finished: 0,
 | 
					      task_finished: 0,
 | 
				
			||||||
            queue: new DecryptQueue(), // for http or file protocol
 | 
					      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;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    progress_show() {
 | 
				
			||||||
        progress_value() {
 | 
					      return this.task_all !== this.task_finished;
 | 
				
			||||||
            return this.task_all ? this.task_finished / this.task_all * 100 : 0
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        progress_show() {
 | 
					 | 
				
			||||||
            return this.task_all !== this.task_finished
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    mounted() {
 | 
					  },
 | 
				
			||||||
        if (window.Worker && window.location.protocol !== "file:" && process.env.NODE_ENV === 'production') {
 | 
					  mounted() {
 | 
				
			||||||
            console.log("Using Worker Pool")
 | 
					    if (window.Worker && window.location.protocol !== 'file:' && process.env.NODE_ENV === 'production') {
 | 
				
			||||||
            this.queue = Pool(
 | 
					      console.log('Using Worker Pool');
 | 
				
			||||||
                () => spawn(new Worker('@/utils/worker.ts')),
 | 
					      this.queue = Pool(() => spawn(new Worker('@/utils/worker.ts')), navigator.hardwareConcurrency || 1);
 | 
				
			||||||
                navigator.hardwareConcurrency || 1
 | 
					      this.parallel = true;
 | 
				
			||||||
            )
 | 
					    } else {
 | 
				
			||||||
            this.parallel = true
 | 
					      console.log('Using Queue in Main Thread');
 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            console.log("Using Queue in Main Thread")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    methods: {
 | 
					 | 
				
			||||||
        progress_string() {
 | 
					 | 
				
			||||||
            return `${this.task_finished} / ${this.task_all}`
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        async addFile(file) {
 | 
					 | 
				
			||||||
            this.task_all++
 | 
					 | 
				
			||||||
            this.queue.queue(async (dec = CommonDecrypt) => {
 | 
					 | 
				
			||||||
                console.log("start handling", file.name)
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    this.$emit("success", await dec(file));
 | 
					 | 
				
			||||||
                } catch (e) {
 | 
					 | 
				
			||||||
                    console.error(e)
 | 
					 | 
				
			||||||
                    this.$emit("error", e, file.name)
 | 
					 | 
				
			||||||
                } finally {
 | 
					 | 
				
			||||||
                    this.task_finished++
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    progress_string() {
 | 
				
			||||||
 | 
					      return `${this.task_finished} / ${this.task_all}`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async addFile(file) {
 | 
				
			||||||
 | 
					      this.task_all++;
 | 
				
			||||||
 | 
					      this.queue.queue(async (dec = CommonDecrypt) => {
 | 
				
			||||||
 | 
					        console.log('start handling', file.name);
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          this.$emit('success', await dec(file));
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          console.error(e);
 | 
				
			||||||
 | 
					          this.$emit('error', e, file.name);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					          this.task_finished++;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,71 +1,62 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <el-table :data="tableData" style="width: 100%">
 | 
					  <el-table :data="tableData" style="width: 100%">
 | 
				
			||||||
 | 
					    <el-table-column label="封面">
 | 
				
			||||||
        <el-table-column label="封面">
 | 
					      <template slot-scope="scope">
 | 
				
			||||||
            <template slot-scope="scope">
 | 
					        <el-image :src="scope.row.picture" style="width: 100px; height: 100px">
 | 
				
			||||||
                <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">
 | 
					        </el-image>
 | 
				
			||||||
                        暂无封面
 | 
					      </template>
 | 
				
			||||||
                    </div>
 | 
					    </el-table-column>
 | 
				
			||||||
                </el-image>
 | 
					    <el-table-column label="歌曲">
 | 
				
			||||||
            </template>
 | 
					      <template #default="scope">
 | 
				
			||||||
        </el-table-column>
 | 
					        <span>{{ scope.row.title }}</span>
 | 
				
			||||||
        <el-table-column label="歌曲">
 | 
					      </template>
 | 
				
			||||||
            <template #default="scope">
 | 
					    </el-table-column>
 | 
				
			||||||
                <span>{{ scope.row.title }}</span>
 | 
					    <el-table-column label="歌手">
 | 
				
			||||||
            </template>
 | 
					      <template #default="scope">
 | 
				
			||||||
        </el-table-column>
 | 
					        <p>{{ scope.row.artist }}</p>
 | 
				
			||||||
        <el-table-column label="歌手">
 | 
					      </template>
 | 
				
			||||||
            <template #default="scope">
 | 
					    </el-table-column>
 | 
				
			||||||
                <p>{{ scope.row.artist }}</p>
 | 
					    <el-table-column label="专辑">
 | 
				
			||||||
            </template>
 | 
					      <template #default="scope">
 | 
				
			||||||
        </el-table-column>
 | 
					        <p>{{ scope.row.album }}</p>
 | 
				
			||||||
        <el-table-column label="专辑">
 | 
					      </template>
 | 
				
			||||||
            <template #default="scope">
 | 
					    </el-table-column>
 | 
				
			||||||
                <p>{{ scope.row.album }}</p>
 | 
					    <el-table-column label="操作">
 | 
				
			||||||
            </template>
 | 
					      <template #default="scope">
 | 
				
			||||||
        </el-table-column>
 | 
					        <el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
 | 
				
			||||||
        <el-table-column label="操作">
 | 
					        </el-button>
 | 
				
			||||||
            <template #default="scope">
 | 
					        <el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button>
 | 
				
			||||||
                <el-button circle
 | 
					        <el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
 | 
				
			||||||
                           icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
 | 
					        </el-button>
 | 
				
			||||||
                </el-button>
 | 
					      </template>
 | 
				
			||||||
                <el-button circle
 | 
					    </el-table-column>
 | 
				
			||||||
                           icon="el-icon-download" @click="handleDownload(scope.row)">
 | 
					  </el-table>
 | 
				
			||||||
                </el-button>
 | 
					 | 
				
			||||||
                <el-button circle
 | 
					 | 
				
			||||||
                           icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
 | 
					 | 
				
			||||||
                </el-button>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
        </el-table-column>
 | 
					 | 
				
			||||||
    </el-table>
 | 
					 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import {RemoveBlobMusic} from '@/utils/utils'
 | 
					import { RemoveBlobMusic } from '@/utils/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    name: "PreviewTable",
 | 
					  name: 'PreviewTable',
 | 
				
			||||||
    props: {
 | 
					  props: {
 | 
				
			||||||
        tableData: {type: Array, required: true},
 | 
					    tableData: { type: Array, required: true },
 | 
				
			||||||
        policy: {type: Number, required: true}
 | 
					    policy: { type: Number, required: true },
 | 
				
			||||||
    },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    methods: {
 | 
					  methods: {
 | 
				
			||||||
        handlePlay(index, row) {
 | 
					    handlePlay(index, row) {
 | 
				
			||||||
            this.$emit("play", row.file);
 | 
					      this.$emit('play', row.file);
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        handleDelete(index, row) {
 | 
					    handleDelete(index, row) {
 | 
				
			||||||
            RemoveBlobMusic(row);
 | 
					      RemoveBlobMusic(row);
 | 
				
			||||||
            this.tableData.splice(index, 1);
 | 
					      this.tableData.splice(index, 1);
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        handleDownload(row) {
 | 
					    handleDownload(row) {
 | 
				
			||||||
            this.$emit("download", row)
 | 
					      this.$emit('download', row);
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
    }
 | 
					  },
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped></style>
 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,81 +1,79 @@
 | 
				
			|||||||
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
 | 
					import { Decrypt as NcmDecrypt } from '@/decrypt/ncm';
 | 
				
			||||||
import {Decrypt as NcmCacheDecrypt} from "@/decrypt/ncmcache";
 | 
					import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache';
 | 
				
			||||||
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
 | 
					import { Decrypt as XmDecrypt } from '@/decrypt/xm';
 | 
				
			||||||
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
 | 
					import { Decrypt as QmcDecrypt } from '@/decrypt/qmc';
 | 
				
			||||||
import {Decrypt as QmcCacheDecrypt} from "@/decrypt/qmccache";
 | 
					import { Decrypt as QmcCacheDecrypt } from '@/decrypt/qmccache';
 | 
				
			||||||
import {Decrypt as KgmDecrypt} from "@/decrypt/kgm";
 | 
					import { Decrypt as KgmDecrypt } from '@/decrypt/kgm';
 | 
				
			||||||
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
 | 
					import { Decrypt as KwmDecrypt } from '@/decrypt/kwm';
 | 
				
			||||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
 | 
					import { Decrypt as RawDecrypt } from '@/decrypt/raw';
 | 
				
			||||||
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
 | 
					import { Decrypt as TmDecrypt } from '@/decrypt/tm';
 | 
				
			||||||
import {DecryptResult, FileInfo} from "@/decrypt/entity";
 | 
					import { DecryptResult, FileInfo } from '@/decrypt/entity';
 | 
				
			||||||
import {SplitFilename} from "@/decrypt/utils";
 | 
					import { SplitFilename } from '@/decrypt/utils';
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
 | 
					export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
 | 
				
			||||||
    const raw = SplitFilename(file.name)
 | 
					  const raw = SplitFilename(file.name);
 | 
				
			||||||
    let rt_data: DecryptResult;
 | 
					  let rt_data: DecryptResult;
 | 
				
			||||||
    switch (raw.ext) {
 | 
					  switch (raw.ext) {
 | 
				
			||||||
        case "ncm":// Netease Mp3/Flac
 | 
					    case 'ncm': // Netease Mp3/Flac
 | 
				
			||||||
            rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "uc":// Netease Cache
 | 
					    case 'uc': // Netease Cache
 | 
				
			||||||
            rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "kwm":// Kuwo Mp3/Flac
 | 
					    case 'kwm': // Kuwo Mp3/Flac
 | 
				
			||||||
            rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break
 | 
					      break;
 | 
				
			||||||
        case "xm": // Xiami Wav/M4a/Mp3/Flac
 | 
					    case 'xm': // Xiami Wav/M4a/Mp3/Flac
 | 
				
			||||||
        case "wav":// Xiami/Raw Wav
 | 
					    case 'wav': // Xiami/Raw Wav
 | 
				
			||||||
        case "mp3":// Xiami/Raw Mp3
 | 
					    case 'mp3': // Xiami/Raw Mp3
 | 
				
			||||||
        case "flac":// Xiami/Raw Flac
 | 
					    case 'flac': // Xiami/Raw Flac
 | 
				
			||||||
        case "m4a":// Xiami/Raw M4a
 | 
					    case 'm4a': // Xiami/Raw M4a
 | 
				
			||||||
            rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "ogg":// Raw Ogg
 | 
					    case 'ogg': // Raw Ogg
 | 
				
			||||||
            rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "tm0":// QQ Music IOS Mp3
 | 
					    case 'tm0': // QQ Music IOS Mp3
 | 
				
			||||||
        case "tm3":// QQ Music IOS Mp3
 | 
					    case 'tm3': // QQ Music IOS Mp3
 | 
				
			||||||
            rt_data = await RawDecrypt(file.raw, raw.name, "mp3");
 | 
					      rt_data = await RawDecrypt(file.raw, raw.name, 'mp3');
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "qmc3"://QQ Music Android Mp3
 | 
					    case 'qmc3': //QQ Music Android Mp3
 | 
				
			||||||
        case "qmc2"://QQ Music Android Ogg
 | 
					    case 'qmc2': //QQ Music Android Ogg
 | 
				
			||||||
        case "qmc0"://QQ Music Android Mp3
 | 
					    case 'qmc0': //QQ Music Android Mp3
 | 
				
			||||||
        case "qmcflac"://QQ Music Android Flac
 | 
					    case 'qmcflac': //QQ Music Android Flac
 | 
				
			||||||
        case "qmcogg"://QQ Music Android Ogg
 | 
					    case 'qmcogg': //QQ Music Android Ogg
 | 
				
			||||||
        case "tkm"://QQ Music Accompaniment M4a
 | 
					    case 'tkm': //QQ Music Accompaniment M4a
 | 
				
			||||||
        case "bkcmp3"://Moo Music Mp3
 | 
					    case 'bkcmp3': //Moo Music Mp3
 | 
				
			||||||
        case "bkcflac"://Moo Music Flac
 | 
					    case 'bkcflac': //Moo Music Flac
 | 
				
			||||||
        case "mflac"://QQ Music New Flac
 | 
					    case 'mflac': //QQ Music New Flac
 | 
				
			||||||
        case "mflac0"://QQ Music New Flac
 | 
					    case 'mflac0': //QQ Music New Flac
 | 
				
			||||||
        case "mgg": //QQ Music New Ogg
 | 
					    case 'mgg': //QQ Music New Ogg
 | 
				
			||||||
        case "mgg1": //QQ Music New Ogg
 | 
					    case 'mgg1': //QQ Music New Ogg
 | 
				
			||||||
        case "666c6163"://QQ Music Weiyun Flac
 | 
					    case '666c6163': //QQ Music Weiyun Flac
 | 
				
			||||||
        case "6d7033"://QQ Music Weiyun Mp3
 | 
					    case '6d7033': //QQ Music Weiyun Mp3
 | 
				
			||||||
        case "6f6767"://QQ Music Weiyun Ogg
 | 
					    case '6f6767': //QQ Music Weiyun Ogg
 | 
				
			||||||
        case "6d3461"://QQ Music Weiyun M4a
 | 
					    case '6d3461': //QQ Music Weiyun M4a
 | 
				
			||||||
        case "776176"://QQ Music Weiyun Wav
 | 
					    case '776176': //QQ Music Weiyun Wav
 | 
				
			||||||
            rt_data = await QmcDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await QmcDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "tm2":// QQ Music IOS M4a
 | 
					    case 'tm2': // QQ Music IOS M4a
 | 
				
			||||||
        case "tm6":// QQ Music IOS M4a
 | 
					    case 'tm6': // QQ Music IOS M4a
 | 
				
			||||||
            rt_data = await TmDecrypt(file.raw, raw.name);
 | 
					      rt_data = await TmDecrypt(file.raw, raw.name);
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "cache"://QQ Music Cache
 | 
					    case 'cache': //QQ Music Cache
 | 
				
			||||||
            rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break;
 | 
					      break;
 | 
				
			||||||
        case "vpr":
 | 
					    case 'vpr':
 | 
				
			||||||
        case "kgm":
 | 
					    case 'kgm':
 | 
				
			||||||
        case "kgma":
 | 
					    case 'kgma':
 | 
				
			||||||
            rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
 | 
					      rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
 | 
				
			||||||
            break
 | 
					      break;
 | 
				
			||||||
        default:
 | 
					    default:
 | 
				
			||||||
            throw "不支持此文件格式"
 | 
					      throw '不支持此文件格式';
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!rt_data.rawExt) rt_data.rawExt = raw.ext;
 | 
					  if (!rt_data.rawExt) rt_data.rawExt = raw.ext;
 | 
				
			||||||
    if (!rt_data.rawFilename) rt_data.rawFilename = raw.name;
 | 
					  if (!rt_data.rawFilename) rt_data.rawFilename = raw.name;
 | 
				
			||||||
    console.log(rt_data);
 | 
					  console.log(rt_data);
 | 
				
			||||||
    return rt_data;
 | 
					  return rt_data;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,26 +1,25 @@
 | 
				
			|||||||
export interface DecryptResult {
 | 
					export interface DecryptResult {
 | 
				
			||||||
    title: string
 | 
					  title: string;
 | 
				
			||||||
    album?: string
 | 
					  album?: string;
 | 
				
			||||||
    artist?: string
 | 
					  artist?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mime: string
 | 
					  mime: string;
 | 
				
			||||||
    ext: string
 | 
					  ext: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    file: string
 | 
					  file: string;
 | 
				
			||||||
    blob: Blob
 | 
					  blob: Blob;
 | 
				
			||||||
    picture?: string
 | 
					  picture?: string;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    message?: string
 | 
					 | 
				
			||||||
    rawExt?: string
 | 
					 | 
				
			||||||
    rawFilename?: string
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  message?: string;
 | 
				
			||||||
 | 
					  rawExt?: string;
 | 
				
			||||||
 | 
					  rawFilename?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface FileInfo {
 | 
					export interface FileInfo {
 | 
				
			||||||
    status: string
 | 
					  status: string;
 | 
				
			||||||
    name: string,
 | 
					  name: string;
 | 
				
			||||||
    size: number,
 | 
					  size: number;
 | 
				
			||||||
    percentage: number,
 | 
					  percentage: number;
 | 
				
			||||||
    uid: number,
 | 
					  uid: number;
 | 
				
			||||||
    raw: File
 | 
					  raw: File;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,122 +1,125 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    AudioMimeType,
 | 
					  AudioMimeType,
 | 
				
			||||||
    BytesHasPrefix,
 | 
					  BytesHasPrefix,
 | 
				
			||||||
    GetArrayBuffer,
 | 
					  GetArrayBuffer,
 | 
				
			||||||
    GetCoverFromFile,
 | 
					  GetCoverFromFile,
 | 
				
			||||||
    GetMetaFromFile,
 | 
					  GetMetaFromFile,
 | 
				
			||||||
    SniffAudioExt
 | 
					  SniffAudioExt,
 | 
				
			||||||
} from "@/decrypt/utils";
 | 
					} from '@/decrypt/utils';
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
import config from "@/../package.json"
 | 
					import config from '@/../package.json';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//prettier-ignore
 | 
				
			||||||
const VprHeader = [
 | 
					const VprHeader = [
 | 
				
			||||||
    0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
 | 
					  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 = [
 | 
					const KgmHeader = [
 | 
				
			||||||
    0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
 | 
					  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 = [
 | 
					const VprMaskDiff = [
 | 
				
			||||||
    0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
 | 
					  0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
 | 
				
			||||||
    0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
 | 
					  0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
 | 
				
			||||||
    0x00]
 | 
					  0x00
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
					export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
				
			||||||
 | 
					  const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
 | 
					  if (raw_ext === 'vpr') {
 | 
				
			||||||
 | 
					    if (!BytesHasPrefix(oriData, VprHeader)) throw Error('Not a valid vpr file!');
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    if (!BytesHasPrefix(oriData, KgmHeader)) throw Error('Not a valid kgm(a) file!');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer);
 | 
				
			||||||
 | 
					  let headerLen = bHeaderLen.getUint32(0, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
					  let audioData = oriData.slice(headerLen);
 | 
				
			||||||
    if (raw_ext === "vpr") {
 | 
					  let dataLen = audioData.length;
 | 
				
			||||||
        if (!BytesHasPrefix(oriData, VprHeader)) throw Error("Not a valid vpr file!")
 | 
					  if (audioData.byteLength > 1 << 26) {
 | 
				
			||||||
    } else {
 | 
					    throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁");
 | 
				
			||||||
        if (!BytesHasPrefix(oriData, KgmHeader)) throw Error("Not a valid kgm(a) file!")
 | 
					  }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer)
 | 
					 | 
				
			||||||
    let headerLen = bHeaderLen.getUint32(0, true)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let audioData = oriData.slice(headerLen)
 | 
					  let key1 = new Uint8Array(17);
 | 
				
			||||||
    let dataLen = audioData.length
 | 
					  key1.set(oriData.slice(0x1c, 0x2c), 0);
 | 
				
			||||||
    if (audioData.byteLength > 1 << 26) {
 | 
					  if (MaskV2.length === 0) {
 | 
				
			||||||
        throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁")
 | 
					    if (!(await LoadMaskV2())) throw Error('加载Kgm/Vpr Mask数据失败');
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let key1 = new Uint8Array(17)
 | 
					  for (let i = 0; i < dataLen; i++) {
 | 
				
			||||||
    key1.set(oriData.slice(0x1c, 0x2c), 0)
 | 
					    let med8 = key1[i % 17] ^ audioData[i];
 | 
				
			||||||
    if (MaskV2.length === 0) {
 | 
					    med8 ^= (med8 & 0xf) << 4;
 | 
				
			||||||
        if (!await LoadMaskV2()) throw Error("加载Kgm/Vpr Mask数据失败")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (let i = 0; i < dataLen; i++) {
 | 
					    let msk8 = GetMask(i);
 | 
				
			||||||
        let med8 = key1[i % 17] ^ audioData[i]
 | 
					    msk8 ^= (msk8 & 0xf) << 4;
 | 
				
			||||||
        med8 ^= (med8 & 0xf) << 4
 | 
					    audioData[i] = med8 ^ msk8;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (raw_ext === 'vpr') {
 | 
				
			||||||
 | 
					    for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let msk8 = GetMask(i)
 | 
					  const ext = SniffAudioExt(audioData);
 | 
				
			||||||
        msk8 ^= (msk8 & 0xf) << 4
 | 
					  const mime = AudioMimeType[ext];
 | 
				
			||||||
        audioData[i] = med8 ^ msk8
 | 
					  let musicBlob = new Blob([audioData], { type: mime });
 | 
				
			||||||
    }
 | 
					  const musicMeta = await metaParseBlob(musicBlob);
 | 
				
			||||||
    if (raw_ext === "vpr") {
 | 
					  const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
 | 
				
			||||||
        for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17]
 | 
					  return {
 | 
				
			||||||
    }
 | 
					    album: musicMeta.common.album,
 | 
				
			||||||
 | 
					    picture: GetCoverFromFile(musicMeta),
 | 
				
			||||||
    const ext = SniffAudioExt(audioData);
 | 
					    file: URL.createObjectURL(musicBlob),
 | 
				
			||||||
    const mime = AudioMimeType[ext];
 | 
					    blob: musicBlob,
 | 
				
			||||||
    let musicBlob = new Blob([audioData], {type: mime});
 | 
					    ext,
 | 
				
			||||||
    const musicMeta = await metaParseBlob(musicBlob);
 | 
					    mime,
 | 
				
			||||||
    const {title, artist} = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
 | 
					    title,
 | 
				
			||||||
    return {
 | 
					    artist,
 | 
				
			||||||
        album: musicMeta.common.album,
 | 
					  };
 | 
				
			||||||
        picture: GetCoverFromFile(musicMeta),
 | 
					 | 
				
			||||||
        file: URL.createObjectURL(musicBlob),
 | 
					 | 
				
			||||||
        blob: musicBlob,
 | 
					 | 
				
			||||||
        ext,
 | 
					 | 
				
			||||||
        mime,
 | 
					 | 
				
			||||||
        title,
 | 
					 | 
				
			||||||
        artist
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
function GetMask(pos: number) {
 | 
					function GetMask(pos: number) {
 | 
				
			||||||
    return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]
 | 
					  return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let MaskV2: Uint8Array = new Uint8Array(0);
 | 
					let MaskV2: Uint8Array = new Uint8Array(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function LoadMaskV2(): Promise<boolean> {
 | 
					async function LoadMaskV2(): Promise<boolean> {
 | 
				
			||||||
    let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/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 (['http:', 'https:'].some((v) => v == self.location.protocol)) {
 | 
				
			||||||
        if (!!self.document) {// using Web Worker
 | 
					    if (!!self.document) {
 | 
				
			||||||
            mask_url = "./static/kgm.mask"
 | 
					      // using Web Worker
 | 
				
			||||||
        } else {// using Main thread
 | 
					      mask_url = './static/kgm.mask';
 | 
				
			||||||
            mask_url = "../static/kgm.mask"
 | 
					    } else {
 | 
				
			||||||
        }
 | 
					      // using Main thread
 | 
				
			||||||
    }
 | 
					      mask_url = '../static/kgm.mask';
 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        const resp = await fetch(mask_url, {method: "GET"})
 | 
					 | 
				
			||||||
        MaskV2 = new Uint8Array(await resp.arrayBuffer());
 | 
					 | 
				
			||||||
        return true
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
        console.error(e)
 | 
					 | 
				
			||||||
        return false
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const resp = await fetch(mask_url, { method: 'GET' });
 | 
				
			||||||
 | 
					    MaskV2 = new Uint8Array(await resp.arrayBuffer());
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error(e);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//prettier-ignore
 | 
				
			||||||
const MaskV2PreDef = [
 | 
					const MaskV2PreDef = [
 | 
				
			||||||
    0xB8, 0xD5, 0x3D, 0xB2, 0xE9, 0xAF, 0x78, 0x8C, 0x83, 0x33, 0x71, 0x51, 0x76, 0xA0, 0xCD, 0x37,
 | 
					  0xb8, 0xd5, 0x3d, 0xb2, 0xe9, 0xaf, 0x78, 0x8c, 0x83, 0x33, 0x71, 0x51, 0x76, 0xa0, 0xcd, 0x37, 0x2f, 0x3e, 0x35,
 | 
				
			||||||
    0x2F, 0x3E, 0x35, 0x8D, 0xA9, 0xBE, 0x98, 0xB7, 0xE7, 0x8C, 0x22, 0xCE, 0x5A, 0x61, 0xDF, 0x68,
 | 
					  0x8d, 0xa9, 0xbe, 0x98, 0xb7, 0xe7, 0x8c, 0x22, 0xce, 0x5a, 0x61, 0xdf, 0x68, 0x69, 0x89, 0xfe, 0xa5, 0xb6, 0xde,
 | 
				
			||||||
    0x69, 0x89, 0xFE, 0xA5, 0xB6, 0xDE, 0xA9, 0x77, 0xFC, 0xC8, 0xBD, 0xBD, 0xE5, 0x6D, 0x3E, 0x5A,
 | 
					  0xa9, 0x77, 0xfc, 0xc8, 0xbd, 0xbd, 0xe5, 0x6d, 0x3e, 0x5a, 0x36, 0xef, 0x69, 0x4e, 0xbe, 0xe1, 0xe9, 0x66, 0x1c,
 | 
				
			||||||
    0x36, 0xEF, 0x69, 0x4E, 0xBE, 0xE1, 0xE9, 0x66, 0x1C, 0xF3, 0xD9, 0x02, 0xB6, 0xF2, 0x12, 0x9B,
 | 
					  0xf3, 0xd9, 0x02, 0xb6, 0xf2, 0x12, 0x9b, 0x44, 0xd0, 0x6f, 0xb9, 0x35, 0x89, 0xb6, 0x46, 0x6d, 0x73, 0x82, 0x06,
 | 
				
			||||||
    0x44, 0xD0, 0x6F, 0xB9, 0x35, 0x89, 0xB6, 0x46, 0x6D, 0x73, 0x82, 0x06, 0x69, 0xC1, 0xED, 0xD7,
 | 
					  0x69, 0xc1, 0xed, 0xd7, 0x85, 0xc2, 0x30, 0xdf, 0xa2, 0x62, 0xbe, 0x79, 0x2d, 0x62, 0x62, 0x3d, 0x0d, 0x7e, 0xbe,
 | 
				
			||||||
    0x85, 0xC2, 0x30, 0xDF, 0xA2, 0x62, 0xBE, 0x79, 0x2D, 0x62, 0x62, 0x3D, 0x0D, 0x7E, 0xBE, 0x48,
 | 
					  0x48, 0x89, 0x23, 0x02, 0xa0, 0xe4, 0xd5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xfd, 0x16, 0x3a, 0x21, 0x3b, 0x16, 0x0f,
 | 
				
			||||||
    0x89, 0x23, 0x02, 0xA0, 0xE4, 0xD5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xFD, 0x16, 0x3A, 0x21, 0x3B,
 | 
					  0xc3, 0xb2, 0xbb, 0xb3, 0xe2, 0xba, 0x3a, 0x3d, 0x13, 0xec, 0xf6, 0x01, 0x45, 0x84, 0xa5, 0x70, 0x0f, 0x93, 0x49,
 | 
				
			||||||
    0x16, 0x0F, 0xC3, 0xB2, 0xBB, 0xB3, 0xE2, 0xBA, 0x3A, 0x3D, 0x13, 0xEC, 0xF6, 0x01, 0x45, 0x84,
 | 
					  0x0c, 0x64, 0xcd, 0x31, 0xd5, 0xcc, 0x4c, 0x07, 0x01, 0x9e, 0x00, 0x1a, 0x23, 0x90, 0xbf, 0x88, 0x1e, 0x3b, 0xab,
 | 
				
			||||||
    0xA5, 0x70, 0x0F, 0x93, 0x49, 0x0C, 0x64, 0xCD, 0x31, 0xD5, 0xCC, 0x4C, 0x07, 0x01, 0x9E, 0x00,
 | 
					  0xa6, 0x3e, 0xc4, 0x73, 0x47, 0x10, 0x7e, 0x3b, 0x5e, 0xbc, 0xe3, 0x00, 0x84, 0xff, 0x09, 0xd4, 0xe0, 0x89, 0x0f,
 | 
				
			||||||
    0x1A, 0x23, 0x90, 0xBF, 0x88, 0x1E, 0x3B, 0xAB, 0xA6, 0x3E, 0xC4, 0x73, 0x47, 0x10, 0x7E, 0x3B,
 | 
					  0x5b, 0x58, 0x70, 0x4f, 0xfb, 0x65, 0xd8, 0x5c, 0x53, 0x1b, 0xd3, 0xc8, 0xc6, 0xbf, 0xef, 0x98, 0xb0, 0x50, 0x4f,
 | 
				
			||||||
    0x5E, 0xBC, 0xE3, 0x00, 0x84, 0xFF, 0x09, 0xD4, 0xE0, 0x89, 0x0F, 0x5B, 0x58, 0x70, 0x4F, 0xFB,
 | 
					  0x0f, 0xea, 0xe5, 0x83, 0x58, 0x8c, 0x28, 0x2c, 0x84, 0x67, 0xcd, 0xd0, 0x9e, 0x47, 0xdb, 0x27, 0x50, 0xca, 0xf4,
 | 
				
			||||||
    0x65, 0xD8, 0x5C, 0x53, 0x1B, 0xD3, 0xC8, 0xC6, 0xBF, 0xEF, 0x98, 0xB0, 0x50, 0x4F, 0x0F, 0xEA,
 | 
					  0x63, 0x63, 0xe8, 0x97, 0x7f, 0x1b, 0x4b, 0x0c, 0xc2, 0xc1, 0x21, 0x4c, 0xcc, 0x58, 0xf5, 0x94, 0x52, 0xa3, 0xf3,
 | 
				
			||||||
    0xE5, 0x83, 0x58, 0x8C, 0x28, 0x2C, 0x84, 0x67, 0xCD, 0xD0, 0x9E, 0x47, 0xDB, 0x27, 0x50, 0xCA,
 | 
					  0xd3, 0xe0, 0x68, 0xf4, 0x00, 0x23, 0xf3, 0x5e, 0x0a, 0x7b, 0x93, 0xdd, 0xab, 0x12, 0xb2, 0x13, 0xe8, 0x84, 0xd7,
 | 
				
			||||||
    0xF4, 0x63, 0x63, 0xE8, 0x97, 0x7F, 0x1B, 0x4B, 0x0C, 0xC2, 0xC1, 0x21, 0x4C, 0xCC, 0x58, 0xF5,
 | 
					  0xa7, 0x9f, 0x0f, 0x32, 0x4c, 0x55, 0x1d, 0x04, 0x36, 0x52, 0xdc, 0x03, 0xf3, 0xf9, 0x4e, 0x42, 0xe9, 0x3d, 0x61,
 | 
				
			||||||
    0x94, 0x52, 0xA3, 0xF3, 0xD3, 0xE0, 0x68, 0xF4, 0x00, 0x23, 0xF3, 0x5E, 0x0A, 0x7B, 0x93, 0xDD,
 | 
					  0xef, 0x7c, 0xb6, 0xb3, 0x93, 0x50,
 | 
				
			||||||
    0xAB, 0x12, 0xB2, 0x13, 0xE8, 0x84, 0xD7, 0xA7, 0x9F, 0x0F, 0x32, 0x4C, 0x55, 0x1D, 0x04, 0x36,
 | 
					];
 | 
				
			||||||
    0x52, 0xDC, 0x03, 0xF3, 0xF9, 0x4E, 0x42, 0xE9, 0x3D, 0x61, 0xEF, 0x7C, 0xB6, 0xB3, 0x93, 0x50,
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,77 +1,74 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    AudioMimeType,
 | 
					  AudioMimeType,
 | 
				
			||||||
    BytesHasPrefix,
 | 
					  BytesHasPrefix,
 | 
				
			||||||
    GetArrayBuffer,
 | 
					  GetArrayBuffer,
 | 
				
			||||||
    GetCoverFromFile,
 | 
					  GetCoverFromFile,
 | 
				
			||||||
    GetMetaFromFile,
 | 
					  GetMetaFromFile,
 | 
				
			||||||
    SniffAudioExt
 | 
					  SniffAudioExt,
 | 
				
			||||||
} from "@/decrypt/utils";
 | 
					} from '@/decrypt/utils';
 | 
				
			||||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
 | 
					import { Decrypt as RawDecrypt } from '@/decrypt/raw';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//prettier-ignore
 | 
				
			||||||
const MagicHeader = [
 | 
					const MagicHeader = [
 | 
				
			||||||
    0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
 | 
					  0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
 | 
				
			||||||
    0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65,
 | 
					  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> {
 | 
					export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
 | 
				
			||||||
    const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
					  const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    if (!BytesHasPrefix(oriData, MagicHeader)) {
 | 
					  if (!BytesHasPrefix(oriData, MagicHeader)) {
 | 
				
			||||||
        if (SniffAudioExt(oriData) === "aac") {
 | 
					    if (SniffAudioExt(oriData) === 'aac') {
 | 
				
			||||||
            return await RawDecrypt(file, raw_filename, "aac", false)
 | 
					      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 fileKey = oriData.slice(0x18, 0x20);
 | 
				
			||||||
    let mask = createMaskFromKey(fileKey)
 | 
					  let mask = createMaskFromKey(fileKey);
 | 
				
			||||||
    let audioData = oriData.slice(0x400);
 | 
					  let audioData = oriData.slice(0x400);
 | 
				
			||||||
    let lenAudioData = audioData.length;
 | 
					  let lenAudioData = audioData.length;
 | 
				
			||||||
    for (let cur = 0; cur < lenAudioData; ++cur)
 | 
					  for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= mask[cur % 0x20];
 | 
				
			||||||
        audioData[cur] ^= mask[cur % 0x20];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ext = SniffAudioExt(audioData);
 | 
				
			||||||
 | 
					  const mime = AudioMimeType[ext];
 | 
				
			||||||
 | 
					  let musicBlob = new Blob([audioData], { type: mime });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ext = SniffAudioExt(audioData);
 | 
					  const musicMeta = await metaParseBlob(musicBlob);
 | 
				
			||||||
    const mime = AudioMimeType[ext];
 | 
					  const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
 | 
				
			||||||
    let musicBlob = new Blob([audioData], {type: mime});
 | 
					  return {
 | 
				
			||||||
 | 
					    album: musicMeta.common.album,
 | 
				
			||||||
    const musicMeta = await metaParseBlob(musicBlob);
 | 
					    picture: GetCoverFromFile(musicMeta),
 | 
				
			||||||
    const {title, artist} = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
 | 
					    file: URL.createObjectURL(musicBlob),
 | 
				
			||||||
    return {
 | 
					    blob: musicBlob,
 | 
				
			||||||
        album: musicMeta.common.album,
 | 
					    mime,
 | 
				
			||||||
        picture: GetCoverFromFile(musicMeta),
 | 
					    title,
 | 
				
			||||||
        file: URL.createObjectURL(musicBlob),
 | 
					    artist,
 | 
				
			||||||
        blob: musicBlob,
 | 
					    ext,
 | 
				
			||||||
        mime,
 | 
					  };
 | 
				
			||||||
        title,
 | 
					 | 
				
			||||||
        artist,
 | 
					 | 
				
			||||||
        ext
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
function createMaskFromKey(keyBytes: Uint8Array): Uint8Array {
 | 
					function createMaskFromKey(keyBytes: Uint8Array): Uint8Array {
 | 
				
			||||||
    let keyView = new DataView(keyBytes.buffer)
 | 
					  let keyView = new DataView(keyBytes.buffer);
 | 
				
			||||||
    let keyStr = keyView.getBigUint64(0, true).toString()
 | 
					  let keyStr = keyView.getBigUint64(0, true).toString();
 | 
				
			||||||
    let keyStrTrim = trimKey(keyStr)
 | 
					  let keyStrTrim = trimKey(keyStr);
 | 
				
			||||||
    let key = new Uint8Array(32)
 | 
					  let key = new Uint8Array(32);
 | 
				
			||||||
    for (let i = 0; i < 32; i++) {
 | 
					  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 {
 | 
					function trimKey(keyRaw: string): string {
 | 
				
			||||||
    let lenRaw = keyRaw.length;
 | 
					  let lenRaw = keyRaw.length;
 | 
				
			||||||
    let out = keyRaw;
 | 
					  let out = keyRaw;
 | 
				
			||||||
    if (lenRaw > 32) {
 | 
					  if (lenRaw > 32) {
 | 
				
			||||||
        out = keyRaw.slice(0, 32)
 | 
					    out = keyRaw.slice(0, 32);
 | 
				
			||||||
    } else if (lenRaw < 32) {
 | 
					  } else if (lenRaw < 32) {
 | 
				
			||||||
        out = keyRaw.padEnd(32, keyRaw)
 | 
					    out = keyRaw.padEnd(32, keyRaw);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
    return out
 | 
					  return out;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,244 +1,237 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    AudioMimeType,
 | 
					  AudioMimeType,
 | 
				
			||||||
    BytesHasPrefix,
 | 
					  BytesHasPrefix,
 | 
				
			||||||
    GetArrayBuffer,
 | 
					  GetArrayBuffer,
 | 
				
			||||||
    GetImageFromURL,
 | 
					  GetImageFromURL,
 | 
				
			||||||
    GetMetaFromFile,
 | 
					  GetMetaFromFile,
 | 
				
			||||||
    IMusicMeta,
 | 
					  IMusicMeta,
 | 
				
			||||||
    SniffAudioExt,
 | 
					  SniffAudioExt,
 | 
				
			||||||
    WriteMetaToFlac,
 | 
					  WriteMetaToFlac,
 | 
				
			||||||
    WriteMetaToMp3
 | 
					  WriteMetaToMp3,
 | 
				
			||||||
} from "@/decrypt/utils";
 | 
					} from '@/decrypt/utils';
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
				
			||||||
import jimp from 'jimp';
 | 
					import jimp from 'jimp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import AES from "crypto-js/aes";
 | 
					import AES from 'crypto-js/aes';
 | 
				
			||||||
import PKCS7 from "crypto-js/pad-pkcs7";
 | 
					import PKCS7 from 'crypto-js/pad-pkcs7';
 | 
				
			||||||
import ModeECB from "crypto-js/mode-ecb";
 | 
					import ModeECB from 'crypto-js/mode-ecb';
 | 
				
			||||||
import WordArray from "crypto-js/lib-typedarrays";
 | 
					import WordArray from 'crypto-js/lib-typedarrays';
 | 
				
			||||||
import Base64 from "crypto-js/enc-base64";
 | 
					import Base64 from 'crypto-js/enc-base64';
 | 
				
			||||||
import EncUTF8 from "crypto-js/enc-utf8";
 | 
					import EncUTF8 from 'crypto-js/enc-utf8';
 | 
				
			||||||
import EncHex from "crypto-js/enc-hex";
 | 
					import EncHex from 'crypto-js/enc-hex';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					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];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CORE_KEY = EncHex.parse('687a4852416d736f356b496e62617857');
 | 
				
			||||||
 | 
					const META_KEY = EncHex.parse('2331346C6A6B5F215C5D2630553C2728');
 | 
				
			||||||
 | 
					const MagicHeader = [0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
 | 
					export async function Decrypt(file: 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 {
 | 
					interface NcmMusicMeta {
 | 
				
			||||||
    //musicId: number
 | 
					  //musicId: number
 | 
				
			||||||
    musicName?: string
 | 
					  musicName?: string;
 | 
				
			||||||
    artist?: Array<string | number>[]
 | 
					  artist?: Array<string | number>[];
 | 
				
			||||||
    format?: string
 | 
					  format?: string;
 | 
				
			||||||
    album?: string
 | 
					  album?: string;
 | 
				
			||||||
    albumPic?: string
 | 
					  albumPic?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface NcmDjMeta {
 | 
					interface NcmDjMeta {
 | 
				
			||||||
    mainMusic: NcmMusicMeta
 | 
					  mainMusic: NcmMusicMeta;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class NcmDecrypt {
 | 
					class NcmDecrypt {
 | 
				
			||||||
    raw: ArrayBuffer
 | 
					  raw: ArrayBuffer;
 | 
				
			||||||
    view: DataView
 | 
					  view: DataView;
 | 
				
			||||||
    offset: number = 0
 | 
					  offset: number = 0;
 | 
				
			||||||
    filename: string
 | 
					  filename: string;
 | 
				
			||||||
    format: string = ""
 | 
					  format: string = '';
 | 
				
			||||||
    mime: string = ""
 | 
					  mime: string = '';
 | 
				
			||||||
    audio?: Uint8Array
 | 
					  audio?: Uint8Array;
 | 
				
			||||||
    blob?: Blob
 | 
					  blob?: Blob;
 | 
				
			||||||
    oriMeta?: NcmMusicMeta
 | 
					  oriMeta?: NcmMusicMeta;
 | 
				
			||||||
    newMeta?: IMusicMeta
 | 
					  newMeta?: IMusicMeta;
 | 
				
			||||||
    image?: { mime: string, buffer: ArrayBuffer, url: string }
 | 
					  image?: { mime: string; buffer: ArrayBuffer; url: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(buf: ArrayBuffer, filename: string) {
 | 
					  constructor(buf: ArrayBuffer, filename: string) {
 | 
				
			||||||
        const prefix = new Uint8Array(buf, 0, 8)
 | 
					    const prefix = new Uint8Array(buf, 0, 8);
 | 
				
			||||||
        if (!BytesHasPrefix(prefix, MagicHeader)) throw Error("此ncm文件已损坏")
 | 
					    if (!BytesHasPrefix(prefix, MagicHeader)) throw Error('此ncm文件已损坏');
 | 
				
			||||||
        this.offset = 10
 | 
					    this.offset = 10;
 | 
				
			||||||
        this.raw = buf
 | 
					    this.raw = buf;
 | 
				
			||||||
        this.view = new DataView(buf)
 | 
					    this.view = new DataView(buf);
 | 
				
			||||||
        this.filename = filename
 | 
					    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);
 | 
				
			||||||
 | 
					    this.offset += keyLen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const plainText = AES.decrypt(
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      { ciphertext: WordArray.create(cipherText) },
 | 
				
			||||||
 | 
					      CORE_KEY,
 | 
				
			||||||
 | 
					      { mode: ModeECB, padding: PKCS7 },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = new Uint8Array(plainText.sigBytes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const words = plainText.words;
 | 
				
			||||||
 | 
					    const sigBytes = plainText.sigBytes;
 | 
				
			||||||
 | 
					    for (let i = 0; i < sigBytes; i++) {
 | 
				
			||||||
 | 
					      result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _getKeyData(): Uint8Array {
 | 
					    return result.slice(17);
 | 
				
			||||||
        const keyLen = this.view.getUint32(this.offset, true);
 | 
					  }
 | 
				
			||||||
        this.offset += 4;
 | 
					 | 
				
			||||||
        const cipherText = new Uint8Array(this.raw, this.offset, keyLen)
 | 
					 | 
				
			||||||
            .map(uint8 => uint8 ^ 0x64);
 | 
					 | 
				
			||||||
        this.offset += keyLen;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const plainText = AES.decrypt(
 | 
					  _getKeyBox(): Uint8Array {
 | 
				
			||||||
            // @ts-ignore
 | 
					    const keyData = this._getKeyData();
 | 
				
			||||||
            {ciphertext: WordArray.create(cipherText)},
 | 
					    const box = new Uint8Array(Array(256).keys());
 | 
				
			||||||
            CORE_KEY,
 | 
					 | 
				
			||||||
            {mode: ModeECB, padding: PKCS7}
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const result = new Uint8Array(plainText.sigBytes);
 | 
					    const keyDataLen = keyData.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const words = plainText.words;
 | 
					    let j = 0;
 | 
				
			||||||
        const sigBytes = plainText.sigBytes;
 | 
					 | 
				
			||||||
        for (let i = 0; i < sigBytes; i++) {
 | 
					 | 
				
			||||||
            result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return result.slice(17)
 | 
					    for (let i = 0; i < 256; i++) {
 | 
				
			||||||
 | 
					      j = (box[i] + j + keyData[i % keyDataLen]) & 0xff;
 | 
				
			||||||
 | 
					      [box[i], box[j]] = [box[j], box[i]];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _getKeyBox(): Uint8Array {
 | 
					    return box.map((_, i, arr) => {
 | 
				
			||||||
        const keyData = this._getKeyData()
 | 
					      i = (i + 1) & 0xff;
 | 
				
			||||||
        const box = new Uint8Array(Array(256).keys());
 | 
					      const si = arr[i];
 | 
				
			||||||
 | 
					      const sj = arr[(i + si) & 0xff];
 | 
				
			||||||
 | 
					      return arr[(si + sj) & 0xff];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const keyDataLen = keyData.length;
 | 
					  _getMetaData(): NcmMusicMeta {
 | 
				
			||||||
 | 
					    const metaDataLen = this.view.getUint32(this.offset, true);
 | 
				
			||||||
 | 
					    this.offset += 4;
 | 
				
			||||||
 | 
					    if (metaDataLen === 0) return {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let j = 0;
 | 
					    const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen).map((data) => data ^ 0x63);
 | 
				
			||||||
 | 
					    this.offset += metaDataLen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (let i = 0; i < 256; i++) {
 | 
					    WordArray.create();
 | 
				
			||||||
            j = (box[i] + j + keyData[i % keyDataLen]) & 0xff;
 | 
					    const plainText = AES.decrypt(
 | 
				
			||||||
            [box[i], box[j]] = [box[j], box[i]];
 | 
					      // @ts-ignore
 | 
				
			||||||
        }
 | 
					      {
 | 
				
			||||||
 | 
					        ciphertext: Base64.parse(
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
 | 
					          WordArray.create(cipherText.slice(22)).toString(EncUTF8),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      META_KEY,
 | 
				
			||||||
 | 
					      { mode: ModeECB, padding: PKCS7 },
 | 
				
			||||||
 | 
					    ).toString(EncUTF8);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return box.map((_, i, arr) => {
 | 
					    const labelIndex = plainText.indexOf(':');
 | 
				
			||||||
            i = (i + 1) & 0xff;
 | 
					    let result: NcmMusicMeta;
 | 
				
			||||||
            const si = arr[i];
 | 
					    if (plainText.slice(0, labelIndex) === 'dj') {
 | 
				
			||||||
            const sj = arr[(i + si) & 0xff];
 | 
					      const tmp: NcmDjMeta = JSON.parse(plainText.slice(labelIndex + 1));
 | 
				
			||||||
            return arr[(si + sj) & 0xff];
 | 
					      result = tmp.mainMusic;
 | 
				
			||||||
        });
 | 
					    } else {
 | 
				
			||||||
 | 
					      result = JSON.parse(plainText.slice(labelIndex + 1));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!!result.albumPic) {
 | 
				
			||||||
 | 
					      result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async _buildMeta() {
 | 
				
			||||||
 | 
					    if (!this.oriMeta) throw Error('invalid sequence');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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]));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _getMetaData(): NcmMusicMeta {
 | 
					    if (artists.length === 0 && !!info.artist) {
 | 
				
			||||||
        const metaDataLen = this.view.getUint32(this.offset, true);
 | 
					      artists = info.artist
 | 
				
			||||||
        this.offset += 4;
 | 
					        .split(',')
 | 
				
			||||||
        if (metaDataLen === 0) return {};
 | 
					        .map((val) => val.trim())
 | 
				
			||||||
 | 
					        .filter((val) => val != '');
 | 
				
			||||||
        const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen)
 | 
					 | 
				
			||||||
            .map(data => data ^ 0x63);
 | 
					 | 
				
			||||||
        this.offset += metaDataLen;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        WordArray.create()
 | 
					 | 
				
			||||||
        const plainText = AES.decrypt(
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                ciphertext: Base64.parse(
 | 
					 | 
				
			||||||
                    // @ts-ignore
 | 
					 | 
				
			||||||
                    WordArray.create(cipherText.slice(22)).toString(EncUTF8)
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            META_KEY,
 | 
					 | 
				
			||||||
            {mode: ModeECB, padding: PKCS7}
 | 
					 | 
				
			||||||
        ).toString(EncUTF8);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const labelIndex = plainText.indexOf(":");
 | 
					 | 
				
			||||||
        let result: NcmMusicMeta;
 | 
					 | 
				
			||||||
        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"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return result
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _getAudio(keyBox: Uint8Array): Uint8Array {
 | 
					    if (this.oriMeta.albumPic)
 | 
				
			||||||
        this.offset += this.view.getUint32(this.offset + 5, true) + 13
 | 
					      try {
 | 
				
			||||||
        const audioData = new Uint8Array(this.raw, this.offset)
 | 
					        this.image = await GetImageFromURL(this.oriMeta.albumPic);
 | 
				
			||||||
        let lenAudioData = audioData.length
 | 
					        while (this.image && this.image.buffer.byteLength >= 1 << 24) {
 | 
				
			||||||
        for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff]
 | 
					          let img = await jimp.read(Buffer.from(this.image.buffer));
 | 
				
			||||||
        return audioData
 | 
					          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);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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.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;
 | 
				
			||||||
 | 
					    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);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        console.info(`writing meta for ${this.format} is not being supported for now`);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.blob = new Blob([this.audio], { type: this.mime });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async _buildMeta() {
 | 
					  gatherResult(): DecryptResult {
 | 
				
			||||||
        if (!this.oriMeta) throw Error("invalid sequence")
 | 
					    if (!this.newMeta || !this.blob) throw Error('bad sequence');
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      title: this.newMeta.title,
 | 
				
			||||||
 | 
					      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,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const info = GetMetaFromFile(this.filename, this.oriMeta.musicName)
 | 
					  async decrypt() {
 | 
				
			||||||
 | 
					    const keyBox = this._getKeyBox();
 | 
				
			||||||
        // build artists
 | 
					    this.oriMeta = this._getMetaData();
 | 
				
			||||||
        let artists: string[] = [];
 | 
					    this.audio = this._getAudio(keyBox);
 | 
				
			||||||
        if (!!this.oriMeta.artist) {
 | 
					    this.format = this.oriMeta.format || SniffAudioExt(this.audio);
 | 
				
			||||||
            this.oriMeta.artist.forEach(arr => artists.push(<string>arr[0]));
 | 
					    this.mime = AudioMimeType[this.format];
 | 
				
			||||||
        }
 | 
					    await this._buildMeta();
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
        if (artists.length === 0 && !!info.artist) {
 | 
					      await this._writeMeta();
 | 
				
			||||||
            artists = info.artist.split(',')
 | 
					    } catch (e) {
 | 
				
			||||||
                .map(val => val.trim()).filter(val => val != "");
 | 
					      console.warn('write meta data failed', e);
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
            console.log("get cover image failed", e)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.newMeta = {title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer}
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return this.gatherResult();
 | 
				
			||||||
    async _writeMeta() {
 | 
					  }
 | 
				
			||||||
        if (!this.audio || !this.newMeta) throw Error("invalid sequence")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                console.info(`writing meta for ${this.format} is not being supported for now`)
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            this.blob = new Blob([this.audio], {type: this.mime})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    gatherResult(): DecryptResult {
 | 
					 | 
				
			||||||
        if (!this.newMeta || !this.blob) throw Error("bad sequence")
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            title: this.newMeta.title,
 | 
					 | 
				
			||||||
            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
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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()
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            await this._writeMeta()
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
            console.warn("write meta data failed", e)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return this.gatherResult()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,29 +1,28 @@
 | 
				
			|||||||
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils";
 | 
					import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
					export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
				
			||||||
    : Promise<DecryptResult> {
 | 
					  const buffer = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    const buffer = new Uint8Array(await GetArrayBuffer(file));
 | 
					  let length = buffer.length;
 | 
				
			||||||
    let length = buffer.length
 | 
					  for (let i = 0; i < length; i++) {
 | 
				
			||||||
    for (let i = 0; i < length; i++) {
 | 
					    buffer[i] ^= 163;
 | 
				
			||||||
        buffer[i] ^= 163
 | 
					  }
 | 
				
			||||||
    }
 | 
					  const ext = SniffAudioExt(buffer, raw_ext);
 | 
				
			||||||
    const ext = SniffAudioExt(buffer, raw_ext);
 | 
					  if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
 | 
				
			||||||
    if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]})
 | 
					  const tag = await metaParseBlob(file);
 | 
				
			||||||
    const tag = await metaParseBlob(file);
 | 
					  const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
 | 
				
			||||||
    const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					  return {
 | 
				
			||||||
        title,
 | 
					    title,
 | 
				
			||||||
        artist,
 | 
					    artist,
 | 
				
			||||||
        ext,
 | 
					    ext,
 | 
				
			||||||
        album: tag.common.album,
 | 
					    album: tag.common.album,
 | 
				
			||||||
        picture: GetCoverFromFile(tag),
 | 
					    picture: GetCoverFromFile(tag),
 | 
				
			||||||
        file: URL.createObjectURL(file),
 | 
					    file: URL.createObjectURL(file),
 | 
				
			||||||
        blob: file,
 | 
					    blob: file,
 | 
				
			||||||
        mime: AudioMimeType[ext]
 | 
					    mime: AudioMimeType[ext],
 | 
				
			||||||
    }
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
import fs from "fs";
 | 
					import fs from 'fs';
 | 
				
			||||||
import {QmcDecoder} from "@/decrypt/qmc";
 | 
					import { QmcDecoder } from '@/decrypt/qmc';
 | 
				
			||||||
import {BytesEqual} from "@/decrypt/utils";
 | 
					import { BytesEqual } from '@/decrypt/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function loadTestDataDecoder(name: string): {
 | 
					function loadTestDataDecoder(name: string): {
 | 
				
			||||||
  cipherText: Uint8Array,
 | 
					  cipherText: Uint8Array;
 | 
				
			||||||
  clearText: Uint8Array
 | 
					  clearText: Uint8Array;
 | 
				
			||||||
} {
 | 
					} {
 | 
				
			||||||
  const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
 | 
					  const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
 | 
				
			||||||
  const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
 | 
					  const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
 | 
				
			||||||
@ -13,20 +13,17 @@ function loadTestDataDecoder(name: string): {
 | 
				
			|||||||
  cipherText.set(cipherSuffix, cipherBody.length);
 | 
					  cipherText.set(cipherSuffix, cipherBody.length);
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    cipherText,
 | 
					    cipherText,
 | 
				
			||||||
    clearText: fs.readFileSync(`testdata/${name}_target.bin`)
 | 
					    clearText: fs.readFileSync(`testdata/${name}_target.bin`),
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("qmc: real file", async () => {
 | 
					test('qmc: real file', async () => {
 | 
				
			||||||
  const cases = ["mflac0_rc4", "mflac_map", "mgg_map", "qmc0_static"]
 | 
					  const cases = ['mflac0_rc4', 'mflac_map', 'mgg_map', 'qmc0_static'];
 | 
				
			||||||
  for (const name of cases) {
 | 
					  for (const name of cases) {
 | 
				
			||||||
    const {clearText, cipherText} = loadTestDataDecoder(name)
 | 
					    const { clearText, cipherText } = loadTestDataDecoder(name);
 | 
				
			||||||
    const c = new QmcDecoder(cipherText)
 | 
					    const c = new QmcDecoder(cipherText);
 | 
				
			||||||
    const buf = c.decrypt()
 | 
					    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 {
 | 
					import {
 | 
				
			||||||
  AudioMimeType,
 | 
					  AudioMimeType,
 | 
				
			||||||
  GetArrayBuffer,
 | 
					  GetArrayBuffer,
 | 
				
			||||||
@ -7,56 +7,55 @@ import {
 | 
				
			|||||||
  GetMetaFromFile,
 | 
					  GetMetaFromFile,
 | 
				
			||||||
  SniffAudioExt,
 | 
					  SniffAudioExt,
 | 
				
			||||||
  WriteMetaToFlac,
 | 
					  WriteMetaToFlac,
 | 
				
			||||||
  WriteMetaToMp3
 | 
					  WriteMetaToMp3,
 | 
				
			||||||
} from "@/decrypt/utils";
 | 
					} from '@/decrypt/utils';
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
				
			||||||
import {DecryptQMCWasm} from "./qmc_wasm";
 | 
					import { DecryptQMCWasm } from './qmc_wasm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import iconv from 'iconv-lite';
 | 
				
			||||||
import iconv from "iconv-lite";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { queryAlbumCover } from '@/utils/api';
 | 
				
			||||||
import {queryAlbumCover} from "@/utils/api";
 | 
					import { QmcDeriveKey } from '@/decrypt/qmc_key';
 | 
				
			||||||
import {QmcDeriveKey} from "@/decrypt/qmc_key";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Handler {
 | 
					interface Handler {
 | 
				
			||||||
  ext: string
 | 
					  ext: string;
 | 
				
			||||||
  version: number
 | 
					  version: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const HandlerMap: { [key: string]: Handler } = {
 | 
					export const HandlerMap: { [key: string]: Handler } = {
 | 
				
			||||||
  "mgg": {ext: "ogg", version: 2},
 | 
					  mgg: { ext: 'ogg', version: 2 },
 | 
				
			||||||
  "mgg1": {ext: "ogg", version: 2},
 | 
					  mgg1: { ext: 'ogg', version: 2 },
 | 
				
			||||||
  "mflac": {ext: "flac", version: 2},
 | 
					  mflac: { ext: 'flac', version: 2 },
 | 
				
			||||||
  "mflac0": {ext: "flac", version: 2},
 | 
					  mflac0: { ext: 'flac', version: 2 },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // qmcflac / qmcogg:
 | 
					  // qmcflac / qmcogg:
 | 
				
			||||||
  // 有可能是 v2 加密但混用同一个后缀名。
 | 
					  // 有可能是 v2 加密但混用同一个后缀名。
 | 
				
			||||||
  "qmcflac": {ext: "flac", version: 2},
 | 
					  qmcflac: { ext: 'flac', version: 2 },
 | 
				
			||||||
  "qmcogg": {ext: "ogg", version: 2},
 | 
					  qmcogg: { ext: 'ogg', version: 2 },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  "qmc0": {ext: "mp3", version: 1},
 | 
					  qmc0: { ext: 'mp3', version: 1 },
 | 
				
			||||||
  "qmc2": {ext: "ogg", version: 1},
 | 
					  qmc2: { ext: 'ogg', version: 1 },
 | 
				
			||||||
  "qmc3": {ext: "mp3", version: 1},
 | 
					  qmc3: { ext: 'mp3', version: 1 },
 | 
				
			||||||
  "bkcmp3": {ext: "mp3", version: 1},
 | 
					  bkcmp3: { ext: 'mp3', version: 1 },
 | 
				
			||||||
  "bkcflac": {ext: "flac", version: 1},
 | 
					  bkcflac: { ext: 'flac', version: 1 },
 | 
				
			||||||
  "tkm": {ext: "m4a", version: 1},
 | 
					  tkm: { ext: 'm4a', version: 1 },
 | 
				
			||||||
  "666c6163": {ext: "flac", version: 1},
 | 
					  '666c6163': { ext: 'flac', version: 1 },
 | 
				
			||||||
  "6d7033": {ext: "mp3", version: 1},
 | 
					  '6d7033': { ext: 'mp3', version: 1 },
 | 
				
			||||||
  "6f6767": {ext: "ogg", version: 1},
 | 
					  '6f6767': { ext: 'ogg', version: 1 },
 | 
				
			||||||
  "6d3461": {ext: "m4a", version: 1},
 | 
					  '6d3461': { ext: 'm4a', version: 1 },
 | 
				
			||||||
  "776176": {ext: "wav", version: 1}
 | 
					  '776176': { ext: 'wav', version: 1 },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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> {
 | 
				
			||||||
  if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
 | 
					  if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
 | 
				
			||||||
  const handler = HandlerMap[raw_ext];
 | 
					  const handler = HandlerMap[raw_ext];
 | 
				
			||||||
  let {version} = handler;
 | 
					  let { version } = handler;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const fileBuffer = await GetArrayBuffer(file);
 | 
					  const fileBuffer = await GetArrayBuffer(file);
 | 
				
			||||||
  let musicDecoded: Uint8Array | undefined;
 | 
					  let musicDecoded: Uint8Array | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (version === 2 && globalThis.WebAssembly) {
 | 
					  if (version === 2 && globalThis.WebAssembly) {
 | 
				
			||||||
    console.log("qmc: using wasm decoder")
 | 
					    console.log('qmc: using wasm decoder');
 | 
				
			||||||
    const v2Decrypted = await DecryptQMCWasm(fileBuffer);
 | 
					    const v2Decrypted = await DecryptQMCWasm(fileBuffer);
 | 
				
			||||||
    // 如果 v2 检测失败,降级到 v1 再尝试一次
 | 
					    // 如果 v2 检测失败,降级到 v1 再尝试一次
 | 
				
			||||||
    if (v2Decrypted) {
 | 
					    if (v2Decrypted) {
 | 
				
			||||||
@ -65,28 +64,28 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  if (!musicDecoded) {
 | 
					  if (!musicDecoded) {
 | 
				
			||||||
    // may throw error
 | 
					    // may throw error
 | 
				
			||||||
    console.log("qmc: using js decoder")
 | 
					    console.log('qmc: using js decoder');
 | 
				
			||||||
    const d = new QmcDecoder(new Uint8Array(fileBuffer))
 | 
					    const d = new QmcDecoder(new Uint8Array(fileBuffer));
 | 
				
			||||||
    musicDecoded = d.decrypt()
 | 
					    musicDecoded = d.decrypt();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ext = SniffAudioExt(musicDecoded, handler.ext);
 | 
					  const ext = SniffAudioExt(musicDecoded, handler.ext);
 | 
				
			||||||
  const mime = AudioMimeType[ext];
 | 
					  const mime = AudioMimeType[ext];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let musicBlob = new Blob([musicDecoded], {type: mime});
 | 
					  let musicBlob = new Blob([musicDecoded], { type: mime });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const musicMeta = await metaParseBlob(musicBlob);
 | 
					  const musicMeta = await metaParseBlob(musicBlob);
 | 
				
			||||||
  for (let metaIdx in musicMeta.native) {
 | 
					  for (let metaIdx in musicMeta.native) {
 | 
				
			||||||
    if (!musicMeta.native.hasOwnProperty(metaIdx)) continue
 | 
					    if (!musicMeta.native.hasOwnProperty(metaIdx)) continue;
 | 
				
			||||||
    if (musicMeta.native[metaIdx].some(item => item.id === "TCON" && item.value === "(12)")) {
 | 
					    if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) {
 | 
				
			||||||
      console.warn("try using gbk encoding to decode meta")
 | 
					      console.warn('try using gbk encoding to decode meta');
 | 
				
			||||||
      musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ""), "gbk");
 | 
					      musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ''), 'gbk');
 | 
				
			||||||
      musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ""), "gbk");
 | 
					      musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ''), 'gbk');
 | 
				
			||||||
      musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ""), "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);
 | 
					  let imgUrl = GetCoverFromFile(musicMeta);
 | 
				
			||||||
  if (!imgUrl) {
 | 
					  if (!imgUrl) {
 | 
				
			||||||
@ -94,20 +93,20 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
 | 
				
			|||||||
    if (imgUrl) {
 | 
					    if (imgUrl) {
 | 
				
			||||||
      const imageInfo = await GetImageFromURL(imgUrl);
 | 
					      const imageInfo = await GetImageFromURL(imgUrl);
 | 
				
			||||||
      if (imageInfo) {
 | 
					      if (imageInfo) {
 | 
				
			||||||
        imgUrl = imageInfo.url
 | 
					        imgUrl = imageInfo.url;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          const newMeta = {picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(" _ ")}
 | 
					          const newMeta = { picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(' _ ') };
 | 
				
			||||||
          if (ext === "mp3") {
 | 
					          if (ext === 'mp3') {
 | 
				
			||||||
            musicDecoded = WriteMetaToMp3(Buffer.from(musicDecoded), newMeta, musicMeta)
 | 
					            musicDecoded = WriteMetaToMp3(Buffer.from(musicDecoded), newMeta, musicMeta);
 | 
				
			||||||
            musicBlob = new Blob([musicDecoded], {type: mime});
 | 
					            musicBlob = new Blob([musicDecoded], { type: mime });
 | 
				
			||||||
          } else if (ext === 'flac') {
 | 
					          } else if (ext === 'flac') {
 | 
				
			||||||
            musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta)
 | 
					            musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta);
 | 
				
			||||||
            musicBlob = new Blob([musicDecoded], {type: mime});
 | 
					            musicBlob = new Blob([musicDecoded], { type: mime });
 | 
				
			||||||
          } else {
 | 
					          } 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) {
 | 
					        } 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,
 | 
					    picture: imgUrl,
 | 
				
			||||||
    file: URL.createObjectURL(musicBlob),
 | 
					    file: URL.createObjectURL(musicBlob),
 | 
				
			||||||
    blob: musicBlob,
 | 
					    blob: musicBlob,
 | 
				
			||||||
    mime: mime
 | 
					    mime: mime,
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getCoverImage(title: string, artist?: string, album?: string): Promise<string> {
 | 
					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 {
 | 
					  try {
 | 
				
			||||||
    const data = await queryAlbumCover(title, artist, album)
 | 
					    const data = await queryAlbumCover(title, artist, album);
 | 
				
			||||||
    return `${song_query_url}/${data.Type}/${data.Id}`
 | 
					    return `${song_query_url}/${data.Type}/${data.Id}`;
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch (e) {
 | 
				
			||||||
    console.warn(e);
 | 
					    console.warn(e);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return ""
 | 
					  return '';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class QmcDecoder {
 | 
					export class QmcDecoder {
 | 
				
			||||||
  file: Uint8Array
 | 
					  private static readonly BYTE_COMMA = ','.charCodeAt(0);
 | 
				
			||||||
  size: number
 | 
					  file: Uint8Array;
 | 
				
			||||||
  decoded: boolean = false
 | 
					  size: number;
 | 
				
			||||||
  audioSize?: number
 | 
					  decoded: boolean = false;
 | 
				
			||||||
  private static readonly BYTE_COMMA = ','.charCodeAt(0)
 | 
					  audioSize?: number;
 | 
				
			||||||
  cipher?: QmcStreamCipher
 | 
					  cipher?: QmcStreamCipher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(file: Uint8Array) {
 | 
					  constructor(file: Uint8Array) {
 | 
				
			||||||
    this.file = file
 | 
					    this.file = file;
 | 
				
			||||||
    this.size = file.length
 | 
					    this.size = file.length;
 | 
				
			||||||
    this.searchKey()
 | 
					    this.searchKey();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  decrypt(): Uint8Array {
 | 
					  decrypt(): Uint8Array {
 | 
				
			||||||
    if (!this.cipher) {
 | 
					    if (!this.cipher) {
 | 
				
			||||||
      throw new Error("no cipher found")
 | 
					      throw new Error('no cipher found');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (!this.audioSize || this.audioSize <= 0) {
 | 
					    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) {
 | 
					    if (!this.decoded) {
 | 
				
			||||||
      this.cipher.decrypt(audioBuf, 0)
 | 
					      this.cipher.decrypt(audioBuf, 0);
 | 
				
			||||||
      this.decoded = true
 | 
					      this.decoded = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return audioBuf
 | 
					    return audioBuf;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private searchKey() {
 | 
					  private searchKey() {
 | 
				
			||||||
    const last4Byte = this.file.slice(-4);
 | 
					    const last4Byte = this.file.slice(-4);
 | 
				
			||||||
    const textEnc = new TextDecoder()
 | 
					    const textEnc = new TextDecoder();
 | 
				
			||||||
    if (textEnc.decode(last4Byte) === 'QTag') {
 | 
					    if (textEnc.decode(last4Byte) === 'QTag') {
 | 
				
			||||||
      const sizeBuf = this.file.slice(-8, -4)
 | 
					      const sizeBuf = this.file.slice(-8, -4);
 | 
				
			||||||
      const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset)
 | 
					      const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset);
 | 
				
			||||||
      const keySize = sizeView.getUint32(0, false)
 | 
					      const keySize = sizeView.getUint32(0, false);
 | 
				
			||||||
      this.audioSize = this.size - keySize - 8
 | 
					      this.audioSize = this.size - keySize - 8;
 | 
				
			||||||
      const rawKey = this.file.subarray(this.audioSize, this.size - 8)
 | 
					      const rawKey = this.file.subarray(this.audioSize, this.size - 8);
 | 
				
			||||||
      const keyEnd = rawKey.findIndex(v => v == QmcDecoder.BYTE_COMMA)
 | 
					      const keyEnd = rawKey.findIndex((v) => v == QmcDecoder.BYTE_COMMA);
 | 
				
			||||||
      this.setCipher(rawKey.subarray(0, keyEnd))
 | 
					      this.setCipher(rawKey.subarray(0, keyEnd));
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
 | 
					      const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
 | 
				
			||||||
      const keySize = sizeView.getUint32(0, true)
 | 
					      const keySize = sizeView.getUint32(0, true);
 | 
				
			||||||
      if (keySize < 0x300) {
 | 
					      if (keySize < 0x300) {
 | 
				
			||||||
        this.audioSize = this.size - keySize - 4
 | 
					        this.audioSize = this.size - keySize - 4;
 | 
				
			||||||
        const rawKey = this.file.subarray(this.audioSize, this.size - 4)
 | 
					        const rawKey = this.file.subarray(this.audioSize, this.size - 4);
 | 
				
			||||||
        this.setCipher(rawKey)
 | 
					        this.setCipher(rawKey);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        this.audioSize = this.size
 | 
					        this.audioSize = this.size;
 | 
				
			||||||
        this.cipher = new QmcStaticCipher()
 | 
					        this.cipher = new QmcStaticCipher();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private setCipher(keyRaw: Uint8Array) {
 | 
					  private setCipher(keyRaw: Uint8Array) {
 | 
				
			||||||
    const keyDec = QmcDeriveKey(keyRaw)
 | 
					    const keyDec = QmcDeriveKey(keyRaw);
 | 
				
			||||||
    if (keyDec.length > 300) {
 | 
					    if (keyDec.length > 300) {
 | 
				
			||||||
      this.cipher = new QmcRC4Cipher(keyDec)
 | 
					      this.cipher = new QmcRC4Cipher(keyDec);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.cipher = new QmcMapCipher(keyDec)
 | 
					      this.cipher = new QmcMapCipher(keyDec);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,115 +1,117 @@
 | 
				
			|||||||
import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher} from "@/decrypt/qmc_cipher";
 | 
					import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher';
 | 
				
			||||||
import fs from 'fs'
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("static cipher [0x7ff8,0x8000) ", () => {
 | 
					test('static cipher [0x7ff8,0x8000) ', () => {
 | 
				
			||||||
 | 
					  //prettier-ignore
 | 
				
			||||||
  const expected = new Uint8Array([
 | 
					  const expected = new Uint8Array([
 | 
				
			||||||
    0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A,
 | 
					    0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A,
 | 
				
			||||||
    0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8,
 | 
					    0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8,
 | 
				
			||||||
  ])
 | 
					  ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const c = new QmcStaticCipher()
 | 
					  const c = new QmcStaticCipher();
 | 
				
			||||||
  const buf = new Uint8Array(16)
 | 
					  const buf = new Uint8Array(16);
 | 
				
			||||||
  c.decrypt(buf, 0x7ff8)
 | 
					  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([
 | 
					  const expected = new Uint8Array([
 | 
				
			||||||
    0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52,
 | 
					    0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52,
 | 
				
			||||||
    0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00,
 | 
					    0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00,
 | 
				
			||||||
  ])
 | 
					  ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const c = new QmcStaticCipher()
 | 
					  const c = new QmcStaticCipher();
 | 
				
			||||||
  const buf = new Uint8Array(16)
 | 
					  const buf = new Uint8Array(16);
 | 
				
			||||||
  c.decrypt(buf, 0)
 | 
					  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([
 | 
					  const expected = new Uint8Array([
 | 
				
			||||||
    0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB,
 | 
					    0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB,
 | 
				
			||||||
    0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79,
 | 
					    0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79,
 | 
				
			||||||
  ])
 | 
					  ])
 | 
				
			||||||
  const key = new Uint8Array(256)
 | 
					  const key = new Uint8Array(256);
 | 
				
			||||||
  for (let i = 0; i < 256; i++) key[i] = i
 | 
					  for (let i = 0; i < 256; i++) key[i] = i;
 | 
				
			||||||
  const buf = new Uint8Array(16)
 | 
					  const buf = new Uint8Array(16);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const c = new QmcMapCipher(key)
 | 
					  const c = new QmcMapCipher(key);
 | 
				
			||||||
  c.decrypt(buf, 0)
 | 
					  c.decrypt(buf, 0);
 | 
				
			||||||
  expect(buf).toStrictEqual(expected)
 | 
					  expect(buf).toStrictEqual(expected);
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function loadTestDataCipher(name: string): {
 | 
					function loadTestDataCipher(name: string): {
 | 
				
			||||||
  key: Uint8Array,
 | 
					  key: Uint8Array;
 | 
				
			||||||
  cipherText: Uint8Array,
 | 
					  cipherText: Uint8Array;
 | 
				
			||||||
  clearText: Uint8Array
 | 
					  clearText: Uint8Array;
 | 
				
			||||||
} {
 | 
					} {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    key: fs.readFileSync(`testdata/${name}_key.bin`),
 | 
					    key: fs.readFileSync(`testdata/${name}_key.bin`),
 | 
				
			||||||
    cipherText: fs.readFileSync(`testdata/${name}_raw.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 () => {
 | 
					test('map cipher: real file', async () => {
 | 
				
			||||||
  const cases = ["mflac_map", "mgg_map"]
 | 
					  const cases = ['mflac_map', 'mgg_map'];
 | 
				
			||||||
  for (const name of cases) {
 | 
					  for (const name of cases) {
 | 
				
			||||||
    const {key, clearText, cipherText} = loadTestDataCipher(name)
 | 
					    const { key, clearText, cipherText } = loadTestDataCipher(name);
 | 
				
			||||||
    const c = new QmcMapCipher(key)
 | 
					    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 () => {
 | 
					test('rc4 cipher: real file', async () => {
 | 
				
			||||||
  const cases = ["mflac0_rc4"]
 | 
					  const cases = ['mflac0_rc4'];
 | 
				
			||||||
  for (const name of cases) {
 | 
					  for (const name of cases) {
 | 
				
			||||||
    const {key, clearText, cipherText} = loadTestDataCipher(name)
 | 
					    const { key, clearText, cipherText } = loadTestDataCipher(name);
 | 
				
			||||||
    const c = new QmcRC4Cipher(key)
 | 
					    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 () => {
 | 
					test('rc4 cipher: first segment', async () => {
 | 
				
			||||||
  const cases = ["mflac0_rc4"]
 | 
					  const cases = ['mflac0_rc4'];
 | 
				
			||||||
  for (const name of cases) {
 | 
					  for (const name of cases) {
 | 
				
			||||||
    const {key, clearText, cipherText} = loadTestDataCipher(name)
 | 
					    const { key, clearText, cipherText } = loadTestDataCipher(name);
 | 
				
			||||||
    const c = new QmcRC4Cipher(key)
 | 
					    const c = new QmcRC4Cipher(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const buf = cipherText.slice(0, 128)
 | 
					    const buf = cipherText.slice(0, 128);
 | 
				
			||||||
    c.decrypt(buf, 0)
 | 
					    c.decrypt(buf, 0);
 | 
				
			||||||
    expect(buf).toStrictEqual(clearText.slice(0, 128))
 | 
					    expect(buf).toStrictEqual(clearText.slice(0, 128));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("rc4 cipher: align block (128~5120)", async () => {
 | 
					test('rc4 cipher: align block (128~5120)', async () => {
 | 
				
			||||||
  const cases = ["mflac0_rc4"]
 | 
					  const cases = ['mflac0_rc4'];
 | 
				
			||||||
  for (const name of cases) {
 | 
					  for (const name of cases) {
 | 
				
			||||||
    const {key, clearText, cipherText} = loadTestDataCipher(name)
 | 
					    const { key, clearText, cipherText } = loadTestDataCipher(name);
 | 
				
			||||||
    const c = new QmcRC4Cipher(key)
 | 
					    const c = new QmcRC4Cipher(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const buf = cipherText.slice(128, 5120)
 | 
					    const buf = cipherText.slice(128, 5120);
 | 
				
			||||||
    c.decrypt(buf, 128)
 | 
					    c.decrypt(buf, 128);
 | 
				
			||||||
    expect(buf).toStrictEqual(clearText.slice(128, 5120))
 | 
					    expect(buf).toStrictEqual(clearText.slice(128, 5120));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("rc4 cipher: simple block (5120~10240)", async () => {
 | 
					test('rc4 cipher: simple block (5120~10240)', async () => {
 | 
				
			||||||
  const cases = ["mflac0_rc4"]
 | 
					  const cases = ['mflac0_rc4'];
 | 
				
			||||||
  for (const name of cases) {
 | 
					  for (const name of cases) {
 | 
				
			||||||
    const {key, clearText, cipherText} = loadTestDataCipher(name)
 | 
					    const { key, clearText, cipherText } = loadTestDataCipher(name);
 | 
				
			||||||
    const c = new QmcRC4Cipher(key)
 | 
					    const c = new QmcRC4Cipher(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const buf = cipherText.slice(5120, 10240)
 | 
					    const buf = cipherText.slice(5120, 10240);
 | 
				
			||||||
    c.decrypt(buf, 5120)
 | 
					    c.decrypt(buf, 5120);
 | 
				
			||||||
    expect(buf).toStrictEqual(clearText.slice(5120, 10240))
 | 
					    expect(buf).toStrictEqual(clearText.slice(5120, 10240));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
export interface QmcStreamCipher {
 | 
					export interface QmcStreamCipher {
 | 
				
			||||||
  decrypt(buf: Uint8Array, offset: number): void
 | 
					  decrypt(buf: Uint8Array, offset: number): void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export class QmcStaticCipher implements QmcStreamCipher {
 | 
					export class QmcStaticCipher implements QmcStreamCipher {
 | 
				
			||||||
 | 
					  //prettier-ignore
 | 
				
			||||||
  private static readonly staticCipherBox: Uint8Array = new Uint8Array([
 | 
					  private static readonly staticCipherBox: Uint8Array = new Uint8Array([
 | 
				
			||||||
    0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
 | 
					    0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
 | 
				
			||||||
    0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
 | 
					    0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
 | 
				
			||||||
@ -40,26 +40,26 @@ export class QmcStaticCipher implements QmcStreamCipher {
 | 
				
			|||||||
  ])
 | 
					  ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public getMask(offset: number) {
 | 
					  public getMask(offset: number) {
 | 
				
			||||||
    if (offset > 0x7FFF) offset %= 0x7FFF
 | 
					    if (offset > 0x7fff) offset %= 0x7fff;
 | 
				
			||||||
    return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff]
 | 
					    return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public decrypt(buf: Uint8Array, offset: number) {
 | 
					  public decrypt(buf: Uint8Array, offset: number) {
 | 
				
			||||||
    for (let i = 0; i < buf.length; i++) {
 | 
					    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 {
 | 
					export class QmcMapCipher implements QmcStreamCipher {
 | 
				
			||||||
  key: Uint8Array
 | 
					  key: Uint8Array;
 | 
				
			||||||
  n: number
 | 
					  n: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(key: Uint8Array) {
 | 
					  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.key = key;
 | 
				
			||||||
    this.n = key.length
 | 
					    this.n = key.length;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static rotate(value: number, bits: number) {
 | 
					  private static rotate(value: number, bits: number) {
 | 
				
			||||||
@ -71,7 +71,7 @@ export class QmcMapCipher implements QmcStreamCipher {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  decrypt(buf: Uint8Array, offset: number): void {
 | 
					  decrypt(buf: Uint8Array, offset: number): void {
 | 
				
			||||||
    for (let i = 0; i < buf.length; i++) {
 | 
					    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;
 | 
					    if (offset > 0x7fff) offset %= 0x7fff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const idx = (offset * offset + 71214) % this.n;
 | 
					    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 {
 | 
					export class QmcRC4Cipher implements QmcStreamCipher {
 | 
				
			||||||
  private static readonly FIRST_SEGMENT_SIZE = 0x80;
 | 
					  private static readonly FIRST_SEGMENT_SIZE = 0x80;
 | 
				
			||||||
  private static readonly SEGMENT_SIZE = 5120
 | 
					  private static readonly SEGMENT_SIZE = 5120;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  S: Uint8Array
 | 
					  S: Uint8Array;
 | 
				
			||||||
  N: number
 | 
					  N: number;
 | 
				
			||||||
  key: Uint8Array
 | 
					  key: Uint8Array;
 | 
				
			||||||
  hash: number
 | 
					  hash: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(key: Uint8Array) {
 | 
					  constructor(key: Uint8Array) {
 | 
				
			||||||
    if (key.length == 0) {
 | 
					    if (key.length == 0) {
 | 
				
			||||||
      throw Error("invalid key size")
 | 
					      throw Error('invalid key size');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.key = key
 | 
					    this.key = key;
 | 
				
			||||||
    this.N = key.length
 | 
					    this.N = key.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // init seed box
 | 
					    // init seed box
 | 
				
			||||||
    this.S = new Uint8Array(this.N);
 | 
					    this.S = new Uint8Array(this.N);
 | 
				
			||||||
@ -109,7 +108,7 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
				
			|||||||
    let j = 0;
 | 
					    let j = 0;
 | 
				
			||||||
    for (let i = 0; i < this.N; ++i) {
 | 
					    for (let i = 0; i < this.N; ++i) {
 | 
				
			||||||
      j = (this.S[i] + j + this.key[i % this.N]) % this.N;
 | 
					      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
 | 
					    // init hash base
 | 
				
			||||||
@ -125,7 +124,6 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      this.hash = next_hash;
 | 
					      this.hash = next_hash;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  decrypt(buf: Uint8Array, offset: number): void {
 | 
					  decrypt(buf: Uint8Array, offset: number): void {
 | 
				
			||||||
@ -133,52 +131,50 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
				
			|||||||
    let processed = 0;
 | 
					    let processed = 0;
 | 
				
			||||||
    const postProcess = (len: number): boolean => {
 | 
					    const postProcess = (len: number): boolean => {
 | 
				
			||||||
      toProcess -= len;
 | 
					      toProcess -= len;
 | 
				
			||||||
      processed += len
 | 
					      processed += len;
 | 
				
			||||||
      offset += len
 | 
					      offset += len;
 | 
				
			||||||
      return toProcess == 0
 | 
					      return toProcess == 0;
 | 
				
			||||||
    }
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Initial segment
 | 
					    // Initial segment
 | 
				
			||||||
    if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) {
 | 
					    if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) {
 | 
				
			||||||
      const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset);
 | 
					      const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset);
 | 
				
			||||||
      this.encFirstSegment(buf.subarray(0, len_segment), offset);
 | 
					      this.encFirstSegment(buf.subarray(0, len_segment), offset);
 | 
				
			||||||
      if (postProcess(len_segment)) return
 | 
					      if (postProcess(len_segment)) return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // align segment
 | 
					    // align segment
 | 
				
			||||||
    if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) {
 | 
					    if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) {
 | 
				
			||||||
      const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess);
 | 
					      const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess);
 | 
				
			||||||
      this.encASegment(buf.subarray(processed, processed + len_segment), offset);
 | 
					      this.encASegment(buf.subarray(processed, processed + len_segment), offset);
 | 
				
			||||||
      if (postProcess(len_segment)) return
 | 
					      if (postProcess(len_segment)) return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Batch process segments
 | 
					    // Batch process segments
 | 
				
			||||||
    while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
 | 
					    while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
 | 
				
			||||||
      this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
 | 
					      this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
 | 
				
			||||||
      postProcess(QmcRC4Cipher.SEGMENT_SIZE)
 | 
					      postProcess(QmcRC4Cipher.SEGMENT_SIZE);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Last segment (incomplete segment)
 | 
					    // Last segment (incomplete segment)
 | 
				
			||||||
    if (toProcess > 0) {
 | 
					    if (toProcess > 0) {
 | 
				
			||||||
      this.encASegment(buf.subarray(processed), offset);
 | 
					      this.encASegment(buf.subarray(processed), offset);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private encFirstSegment(buf: Uint8Array, offset: number) {
 | 
					  private encFirstSegment(buf: Uint8Array, offset: number) {
 | 
				
			||||||
    for (let i = 0; i < buf.length; i++) {
 | 
					    for (let i = 0; i < buf.length; i++) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
      buf[i] ^= this.key[this.getSegmentKey(offset + i)];
 | 
					      buf[i] ^= this.key[this.getSegmentKey(offset + i)];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private encASegment(buf: Uint8Array, offset: number) {
 | 
					  private encASegment(buf: Uint8Array, offset: number) {
 | 
				
			||||||
    // Initialise a new seed box
 | 
					    // Initialise a new seed box
 | 
				
			||||||
    const S = this.S.slice(0)
 | 
					    const S = this.S.slice(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Calculate the number of bytes to skip.
 | 
					    // Calculate the number of bytes to skip.
 | 
				
			||||||
    // The initial "key" derived from segment id, plus the current offset.
 | 
					    // 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
 | 
					    // decrypt the block
 | 
				
			||||||
    let j = 0;
 | 
					    let j = 0;
 | 
				
			||||||
@ -186,7 +182,7 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
				
			|||||||
    for (let i = -skipLen; i < buf.length; i++) {
 | 
					    for (let i = -skipLen; i < buf.length; i++) {
 | 
				
			||||||
      j = (j + 1) % this.N;
 | 
					      j = (j + 1) % this.N;
 | 
				
			||||||
      k = (S[j] + k) % 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) {
 | 
					      if (i >= 0) {
 | 
				
			||||||
        buf[i] ^= S[(S[j] + S[k]) % this.N];
 | 
					        buf[i] ^= S[(S[j] + S[k]) % this.N];
 | 
				
			||||||
@ -195,8 +191,8 @@ export class QmcRC4Cipher implements QmcStreamCipher {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private getSegmentKey(id: number): number {
 | 
					  private getSegmentKey(id: number): number {
 | 
				
			||||||
    const seed = this.key[id % this.N]
 | 
					    const seed = this.key[id % this.N];
 | 
				
			||||||
    const idx = (this.hash / ((id + 1) * seed) * 100.0) | 0;
 | 
					    const idx = ((this.hash / ((id + 1) * seed)) * 100.0) | 0;
 | 
				
			||||||
    return idx % this.N
 | 
					    return idx % this.N;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,30 +1,26 @@
 | 
				
			|||||||
import {QmcDeriveKey, simpleMakeKey} from "@/decrypt/qmc_key";
 | 
					import { QmcDeriveKey, simpleMakeKey } from '@/decrypt/qmc_key';
 | 
				
			||||||
import fs from "fs";
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("key dec: make simple key", () => {
 | 
					test('key dec: make simple key', () => {
 | 
				
			||||||
  expect(
 | 
					  expect(simpleMakeKey(106, 8)).toStrictEqual([0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]);
 | 
				
			||||||
    simpleMakeKey(106, 8)
 | 
					});
 | 
				
			||||||
  ).toStrictEqual(
 | 
					 | 
				
			||||||
    [0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function loadTestDataKeyDecrypt(name: string): {
 | 
					function loadTestDataKeyDecrypt(name: string): {
 | 
				
			||||||
  cipherText: Uint8Array,
 | 
					  cipherText: Uint8Array;
 | 
				
			||||||
  clearText: Uint8Array
 | 
					  clearText: Uint8Array;
 | 
				
			||||||
} {
 | 
					} {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`),
 | 
					    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 () => {
 | 
					test('key dec: real file', async () => {
 | 
				
			||||||
  const cases = ["mflac_map", "mgg_map", "mflac0_rc4"]
 | 
					  const cases = ['mflac_map', 'mgg_map', 'mflac0_rc4'];
 | 
				
			||||||
  for (const name of cases) {
 | 
					  for (const name of cases) {
 | 
				
			||||||
    const {clearText, cipherText} = loadTestDataKeyDecrypt(name)
 | 
					    const { clearText, cipherText } = loadTestDataKeyDecrypt(name);
 | 
				
			||||||
    const buf = QmcDeriveKey(cipherText)
 | 
					    const buf = QmcDeriveKey(cipherText);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(buf).toStrictEqual(clearText)
 | 
					    expect(buf).toStrictEqual(clearText);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,86 +1,83 @@
 | 
				
			|||||||
import {TeaCipher} from "@/utils/tea";
 | 
					import { TeaCipher } from '@/utils/tea';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SALT_LEN = 2
 | 
					const SALT_LEN = 2;
 | 
				
			||||||
const ZERO_LEN = 7
 | 
					const ZERO_LEN = 7;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
 | 
					export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
 | 
				
			||||||
  const textDec = new TextDecoder()
 | 
					  const textDec = new TextDecoder();
 | 
				
			||||||
  const rawDec = Buffer.from(textDec.decode(raw), 'base64')
 | 
					  const rawDec = Buffer.from(textDec.decode(raw), 'base64');
 | 
				
			||||||
  let n = rawDec.length;
 | 
					  let n = rawDec.length;
 | 
				
			||||||
  if (n < 16) {
 | 
					  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);
 | 
					  let teaKey = new Uint8Array(16);
 | 
				
			||||||
  for (let i = 0; i < 8; i++) {
 | 
					  for (let i = 0; i < 8; i++) {
 | 
				
			||||||
    teaKey[i << 1] = simpleKey[i];
 | 
					    teaKey[i << 1] = simpleKey[i];
 | 
				
			||||||
    teaKey[(i << 1) + 1] = rawDec[i];
 | 
					    teaKey[(i << 1) + 1] = rawDec[i];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const sub = decryptTencentTea(rawDec.subarray(8), teaKey)
 | 
					  const sub = decryptTencentTea(rawDec.subarray(8), teaKey);
 | 
				
			||||||
  rawDec.set(sub, 8)
 | 
					  rawDec.set(sub, 8);
 | 
				
			||||||
  return rawDec.subarray(0, 8 + sub.length)
 | 
					  return rawDec.subarray(0, 8 + sub.length);
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// simpleMakeKey exported only for unit test
 | 
					// simpleMakeKey exported only for unit test
 | 
				
			||||||
export function simpleMakeKey(salt: number, length: number): number[] {
 | 
					export function simpleMakeKey(salt: number, length: number): number[] {
 | 
				
			||||||
  const keyBuf: number[] = []
 | 
					  const keyBuf: number[] = [];
 | 
				
			||||||
  for (let i = 0; i < length; i++) {
 | 
					  for (let i = 0; i < length; i++) {
 | 
				
			||||||
    const tmp = Math.tan(salt + i * 0.1)
 | 
					    const tmp = Math.tan(salt + i * 0.1);
 | 
				
			||||||
    keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0)
 | 
					    keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return keyBuf
 | 
					  return keyBuf;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
 | 
					function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
 | 
				
			||||||
  if (inBuf.length % 8 != 0) {
 | 
					  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) {
 | 
					  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 tmpBuf = new Uint8Array(8);
 | 
				
			||||||
  const tmpView = new DataView(tmpBuf.buffer);
 | 
					  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;//只要最低三位
 | 
					  const nPadLen = tmpBuf[0] & 0x7; //只要最低三位
 | 
				
			||||||
  /*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
 | 
					  /*密文格式: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 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 ivPrev = new Uint8Array(8);
 | 
				
			||||||
  let ivCur = inBuf.slice(0, 8); // init iv
 | 
					  let ivCur = inBuf.slice(0, 8); // init iv
 | 
				
			||||||
  let inBufPos = 8;
 | 
					  let inBufPos = 8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 跳过 Padding Len 和 Padding
 | 
					  // 跳过 Padding Len 和 Padding
 | 
				
			||||||
  let tmpIdx = 1 + nPadLen;
 | 
					  let tmpIdx = 1 + nPadLen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // CBC IV 处理
 | 
					  // CBC IV 处理
 | 
				
			||||||
  const cryptBlock = () => {
 | 
					  const cryptBlock = () => {
 | 
				
			||||||
    ivPrev = ivCur;
 | 
					    ivPrev = ivCur;
 | 
				
			||||||
    ivCur = inBuf.slice(inBufPos, inBufPos + 8)
 | 
					    ivCur = inBuf.slice(inBufPos, inBufPos + 8);
 | 
				
			||||||
    for (let j = 0; j < 8; j++) {
 | 
					    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;
 | 
					    inBufPos += 8;
 | 
				
			||||||
    tmpIdx = 0;
 | 
					    tmpIdx = 0;
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 跳过 Salt
 | 
					  // 跳过 Salt
 | 
				
			||||||
  for (let i = 1; i <= SALT_LEN;) {
 | 
					  for (let i = 1; i <= SALT_LEN; ) {
 | 
				
			||||||
    if (tmpIdx < 8) {
 | 
					    if (tmpIdx < 8) {
 | 
				
			||||||
      tmpIdx++;
 | 
					      tmpIdx++;
 | 
				
			||||||
      i++;
 | 
					      i++;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      cryptBlock()
 | 
					      cryptBlock();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -89,19 +86,18 @@ function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
 | 
				
			|||||||
  while (outBufPos < outLen) {
 | 
					  while (outBufPos < outLen) {
 | 
				
			||||||
    if (tmpIdx < 8) {
 | 
					    if (tmpIdx < 8) {
 | 
				
			||||||
      outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
 | 
					      outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
 | 
				
			||||||
      outBufPos++
 | 
					      outBufPos++;
 | 
				
			||||||
      tmpIdx++;
 | 
					      tmpIdx++;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      cryptBlock()
 | 
					      cryptBlock();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 校验Zero
 | 
					  // 校验Zero
 | 
				
			||||||
  for (let i = 1; i <= ZERO_LEN; i++) {
 | 
					  for (let i = 1; i <= ZERO_LEN; i++) {
 | 
				
			||||||
    if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) {
 | 
					    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 {
 | 
					function MergeUint8Array(array: Uint8Array[]): Uint8Array {
 | 
				
			||||||
  let length = 0;
 | 
					  let length = 0;
 | 
				
			||||||
  array.forEach(item => {
 | 
					  array.forEach((item) => {
 | 
				
			||||||
    length += item.length;
 | 
					    length += item.length;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let mergedArray = new Uint8Array(length);
 | 
					  let mergedArray = new Uint8Array(length);
 | 
				
			||||||
  let offset = 0;
 | 
					  let offset = 0;
 | 
				
			||||||
  array.forEach(item => {
 | 
					  array.forEach((item) => {
 | 
				
			||||||
    mergedArray.set(item, offset);
 | 
					    mergedArray.set(item, offset);
 | 
				
			||||||
    offset += item.length;
 | 
					    offset += item.length;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -42,16 +42,12 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
 | 
				
			|||||||
  const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
 | 
					  const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 进行检测
 | 
					  // 进行检测
 | 
				
			||||||
  const detectOK = QMCCrypto.detectKeyEndPosition(
 | 
					  const detectOK = QMCCrypto.detectKeyEndPosition(pDetectionResult, pDetectionBuf, detectionBuf.length);
 | 
				
			||||||
    pDetectionResult,
 | 
					 | 
				
			||||||
    pDetectionBuf,
 | 
					 | 
				
			||||||
    detectionBuf.length
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 提取结构体内容:
 | 
					  // 提取结构体内容:
 | 
				
			||||||
  // (pos: i32; len: i32; error: char[??])
 | 
					  // (pos: i32; len: i32; error: char[??])
 | 
				
			||||||
  const position = QMCCrypto.getValue(pDetectionResult, "i32");
 | 
					  const position = QMCCrypto.getValue(pDetectionResult, 'i32');
 | 
				
			||||||
  const len = QMCCrypto.getValue(pDetectionResult + 4, "i32");
 | 
					  const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 释放内存
 | 
					  // 释放内存
 | 
				
			||||||
  QMCCrypto._free(pDetectionBuf);
 | 
					  QMCCrypto._free(pDetectionBuf);
 | 
				
			||||||
@ -66,9 +62,7 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
 | 
				
			|||||||
  const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
 | 
					  const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 提取嵌入到文件的 EKey
 | 
					  // 提取嵌入到文件的 EKey
 | 
				
			||||||
  const ekey = new Uint8Array(
 | 
					  const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
 | 
				
			||||||
    mggBlob.slice(decryptedSize, decryptedSize + len)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 解码 UTF-8 数据到 string
 | 
					  // 解码 UTF-8 数据到 string
 | 
				
			||||||
  const decoder = new TextDecoder();
 | 
					  const decoder = new TextDecoder();
 | 
				
			||||||
@ -85,9 +79,7 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
 | 
				
			|||||||
    const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
 | 
					    const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 解密一些片段
 | 
					    // 解密一些片段
 | 
				
			||||||
    const blockData = new Uint8Array(
 | 
					    const blockData = new Uint8Array(mggBlob.slice(offset, offset + blockSize));
 | 
				
			||||||
      mggBlob.slice(offset, offset + blockSize)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    QMCCrypto.writeArrayToMemory(blockData, buf);
 | 
					    QMCCrypto.writeArrayToMemory(blockData, buf);
 | 
				
			||||||
    QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize);
 | 
					    QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize);
 | 
				
			||||||
    decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize));
 | 
					    decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize));
 | 
				
			||||||
 | 
				
			|||||||
@ -1,51 +1,50 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    AudioMimeType,
 | 
					  AudioMimeType,
 | 
				
			||||||
    GetArrayBuffer,
 | 
					  GetArrayBuffer,
 | 
				
			||||||
    GetCoverFromFile,
 | 
					  GetCoverFromFile,
 | 
				
			||||||
    GetMetaFromFile,
 | 
					  GetMetaFromFile,
 | 
				
			||||||
    SniffAudioExt,
 | 
					  SniffAudioExt,
 | 
				
			||||||
    SplitFilename
 | 
					  SplitFilename,
 | 
				
			||||||
} from "@/decrypt/utils";
 | 
					} 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)
 | 
					export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
 | 
				
			||||||
    : Promise<DecryptResult> {
 | 
					  const buffer = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    const buffer = new Uint8Array(await GetArrayBuffer(file));
 | 
					  let length = buffer.length;
 | 
				
			||||||
    let length = buffer.length
 | 
					  for (let i = 0; i < length; i++) {
 | 
				
			||||||
    for (let i = 0; i < length; i++) {
 | 
					    buffer[i] ^= 0xf4;
 | 
				
			||||||
        buffer[i] ^= 0xf4
 | 
					    if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
 | 
				
			||||||
        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] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
 | 
					    else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
 | 
				
			||||||
        else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
 | 
					    else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
 | 
				
			||||||
        else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
 | 
					  }
 | 
				
			||||||
    }
 | 
					  let ext = SniffAudioExt(buffer, '');
 | 
				
			||||||
    let ext = SniffAudioExt(buffer, "");
 | 
					  const newName = SplitFilename(raw_filename);
 | 
				
			||||||
    const newName = SplitFilename(raw_filename)
 | 
					  let audioBlob: Blob;
 | 
				
			||||||
    let audioBlob: Blob
 | 
					  if (ext !== '' || newName.ext === 'mp3') {
 | 
				
			||||||
    if (ext !== "" || newName.ext === "mp3") {
 | 
					    audioBlob = new Blob([buffer], { type: AudioMimeType[ext] });
 | 
				
			||||||
        audioBlob = new Blob([buffer], {type: AudioMimeType[ext]})
 | 
					  } else if (newName.ext in HandlerMap) {
 | 
				
			||||||
    } 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);
 | 
				
			||||||
        return QmcDecrypt(audioBlob, newName.name, newName.ext);
 | 
					  } else {
 | 
				
			||||||
    } else {
 | 
					    throw '不支持的QQ音乐缓存格式';
 | 
				
			||||||
        throw "不支持的QQ音乐缓存格式"
 | 
					  }
 | 
				
			||||||
    }
 | 
					  const tag = await metaParseBlob(audioBlob);
 | 
				
			||||||
    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 {
 | 
					  return {
 | 
				
			||||||
        title,
 | 
					    title,
 | 
				
			||||||
        artist,
 | 
					    artist,
 | 
				
			||||||
        ext,
 | 
					    ext,
 | 
				
			||||||
        album: tag.common.album,
 | 
					    album: tag.common.album,
 | 
				
			||||||
        picture: GetCoverFromFile(tag),
 | 
					    picture: GetCoverFromFile(tag),
 | 
				
			||||||
        file: URL.createObjectURL(audioBlob),
 | 
					    file: URL.createObjectURL(audioBlob),
 | 
				
			||||||
        blob: audioBlob,
 | 
					    blob: audioBlob,
 | 
				
			||||||
        mime: AudioMimeType[ext]
 | 
					    mime: AudioMimeType[ext],
 | 
				
			||||||
    }
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,28 +1,32 @@
 | 
				
			|||||||
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils";
 | 
					import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import { parseBlob as metaParseBlob } from 'music-metadata-browser';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string, detect: boolean = true)
 | 
					export async function Decrypt(
 | 
				
			||||||
    : Promise<DecryptResult> {
 | 
					  file: Blob,
 | 
				
			||||||
    let ext = raw_ext;
 | 
					  raw_filename: string,
 | 
				
			||||||
    if (detect) {
 | 
					  raw_ext: string,
 | 
				
			||||||
        const buffer = new Uint8Array(await GetArrayBuffer(file));
 | 
					  detect: boolean = true,
 | 
				
			||||||
        ext = SniffAudioExt(buffer, raw_ext);
 | 
					): Promise<DecryptResult> {
 | 
				
			||||||
        if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]})
 | 
					  let ext = raw_ext;
 | 
				
			||||||
    }
 | 
					  if (detect) {
 | 
				
			||||||
    const tag = await metaParseBlob(file);
 | 
					    const buffer = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
 | 
					    ext = SniffAudioExt(buffer, raw_ext);
 | 
				
			||||||
 | 
					    if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const tag = await metaParseBlob(file);
 | 
				
			||||||
 | 
					  const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					  return {
 | 
				
			||||||
        title,
 | 
					    title,
 | 
				
			||||||
        artist,
 | 
					    artist,
 | 
				
			||||||
        ext,
 | 
					    ext,
 | 
				
			||||||
        album: tag.common.album,
 | 
					    album: tag.common.album,
 | 
				
			||||||
        picture: GetCoverFromFile(tag),
 | 
					    picture: GetCoverFromFile(tag),
 | 
				
			||||||
        file: URL.createObjectURL(file),
 | 
					    file: URL.createObjectURL(file),
 | 
				
			||||||
        blob: file,
 | 
					    blob: file,
 | 
				
			||||||
        mime: AudioMimeType[ext]
 | 
					    mime: AudioMimeType[ext],
 | 
				
			||||||
    }
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,14 @@
 | 
				
			|||||||
import {Decrypt as RawDecrypt} from "./raw";
 | 
					import { Decrypt as RawDecrypt } from './raw';
 | 
				
			||||||
import {GetArrayBuffer} from "@/decrypt/utils";
 | 
					import { GetArrayBuffer } from '@/decrypt/utils';
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TM_HEADER = [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70];
 | 
					const TM_HEADER = [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
 | 
					export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
 | 
				
			||||||
    const audioData = new Uint8Array(await GetArrayBuffer(file));
 | 
					  const audioData = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    for (let cur = 0; cur < 8; ++cur) {
 | 
					  for (let cur = 0; cur < 8; ++cur) {
 | 
				
			||||||
        audioData[cur] = TM_HEADER[cur];
 | 
					    audioData[cur] = TM_HEADER[cur];
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
    const musicData = new Blob([audioData], {type: "audio/mp4"});
 | 
					  const musicData = new Blob([audioData], { type: 'audio/mp4' });
 | 
				
			||||||
    return await RawDecrypt(musicData, raw_filename, "m4a", false)
 | 
					  return await RawDecrypt(musicData, raw_filename, 'm4a', false);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,177 +1,176 @@
 | 
				
			|||||||
import {IAudioMetadata} from "music-metadata-browser";
 | 
					import { IAudioMetadata } from 'music-metadata-browser';
 | 
				
			||||||
import ID3Writer from "browser-id3-writer";
 | 
					import ID3Writer from 'browser-id3-writer';
 | 
				
			||||||
import MetaFlac from "metaflac-js";
 | 
					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 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 M4A_HEADER = [0x66, 0x74, 0x79, 0x70];
 | 
				
			||||||
export const WMA_HEADER = [
 | 
					export const WMA_HEADER = [
 | 
				
			||||||
    0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
 | 
					  0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c,
 | 
				
			||||||
    0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C,
 | 
					];
 | 
				
			||||||
]
 | 
					export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46];
 | 
				
			||||||
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46]
 | 
					export const AAC_HEADER = [0xff, 0xf1];
 | 
				
			||||||
export const AAC_HEADER = [0xFF, 0xF1]
 | 
					export const DFF_HEADER = [0x46, 0x52, 0x4d, 0x38];
 | 
				
			||||||
export const DFF_HEADER = [0x46, 0x52, 0x4D, 0x38]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AudioMimeType: { [key: string]: string } = {
 | 
					export const AudioMimeType: { [key: string]: string } = {
 | 
				
			||||||
    mp3: "audio/mpeg",
 | 
					  mp3: 'audio/mpeg',
 | 
				
			||||||
    flac: "audio/flac",
 | 
					  flac: 'audio/flac',
 | 
				
			||||||
    m4a: "audio/mp4",
 | 
					  m4a: 'audio/mp4',
 | 
				
			||||||
    ogg: "audio/ogg",
 | 
					  ogg: 'audio/ogg',
 | 
				
			||||||
    wma: "audio/x-ms-wma",
 | 
					  wma: 'audio/x-ms-wma',
 | 
				
			||||||
    wav: "audio/x-wav",
 | 
					  wav: 'audio/x-wav',
 | 
				
			||||||
    dff: "audio/x-dff"
 | 
					  dff: 'audio/x-dff',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean {
 | 
					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 prefix.every((val, idx) => {
 | 
				
			||||||
        return val === data[idx];
 | 
					    return val === data[idx];
 | 
				
			||||||
    })
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function BytesEqual(a: Uint8Array, b: Uint8Array,): boolean {
 | 
					export function BytesEqual(a: Uint8Array, b: Uint8Array): boolean {
 | 
				
			||||||
    if (a.length !== b.length) return false
 | 
					  if (a.length !== b.length) return false;
 | 
				
			||||||
    return a.every((val, idx) => {
 | 
					  return a.every((val, idx) => {
 | 
				
			||||||
        return val === b[idx];
 | 
					    return val === b[idx];
 | 
				
			||||||
    })
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SniffAudioExt(data: Uint8Array, fallback_ext: string = 'mp3'): string {
 | 
				
			||||||
export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): string {
 | 
					  if (BytesHasPrefix(data, MP3_HEADER)) return 'mp3';
 | 
				
			||||||
    if (BytesHasPrefix(data, MP3_HEADER)) return "mp3"
 | 
					  if (BytesHasPrefix(data, FLAC_HEADER)) return 'flac';
 | 
				
			||||||
    if (BytesHasPrefix(data, FLAC_HEADER)) return "flac"
 | 
					  if (BytesHasPrefix(data, OGG_HEADER)) return 'ogg';
 | 
				
			||||||
    if (BytesHasPrefix(data, OGG_HEADER)) return "ogg"
 | 
					  if (data.length >= 4 + M4A_HEADER.length && BytesHasPrefix(data.slice(4), M4A_HEADER)) return 'm4a';
 | 
				
			||||||
    if (data.length >= 4 + M4A_HEADER.length &&
 | 
					  if (BytesHasPrefix(data, WAV_HEADER)) return 'wav';
 | 
				
			||||||
      BytesHasPrefix(data.slice(4), M4A_HEADER)) return "m4a"
 | 
					  if (BytesHasPrefix(data, WMA_HEADER)) return 'wma';
 | 
				
			||||||
    if (BytesHasPrefix(data, WAV_HEADER)) return "wav"
 | 
					  if (BytesHasPrefix(data, AAC_HEADER)) return 'aac';
 | 
				
			||||||
    if (BytesHasPrefix(data, WMA_HEADER)) return "wma"
 | 
					  if (BytesHasPrefix(data, DFF_HEADER)) return 'dff';
 | 
				
			||||||
    if (BytesHasPrefix(data, AAC_HEADER)) return "aac"
 | 
					  return fallback_ext;
 | 
				
			||||||
    if (BytesHasPrefix(data, DFF_HEADER)) return "dff"
 | 
					 | 
				
			||||||
    return fallback_ext;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function GetArrayBuffer(obj: Blob): Promise<ArrayBuffer> {
 | 
					export function GetArrayBuffer(obj: Blob): Promise<ArrayBuffer> {
 | 
				
			||||||
    if (!!obj.arrayBuffer) return obj.arrayBuffer()
 | 
					  if (!!obj.arrayBuffer) return obj.arrayBuffer();
 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
        const reader = new FileReader();
 | 
					    const reader = new FileReader();
 | 
				
			||||||
        reader.onload = (e) => {
 | 
					    reader.onload = (e) => {
 | 
				
			||||||
            const rs = e.target?.result
 | 
					      const rs = e.target?.result;
 | 
				
			||||||
            if (!rs) {
 | 
					      if (!rs) {
 | 
				
			||||||
                reject("read file failed")
 | 
					        reject('read file failed');
 | 
				
			||||||
            } else {
 | 
					      } else {
 | 
				
			||||||
                resolve(rs as ArrayBuffer)
 | 
					        resolve(rs as ArrayBuffer);
 | 
				
			||||||
            }
 | 
					      }
 | 
				
			||||||
        };
 | 
					    };
 | 
				
			||||||
        reader.readAsArrayBuffer(obj);
 | 
					    reader.readAsArrayBuffer(obj);
 | 
				
			||||||
    });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function GetCoverFromFile(metadata: IAudioMetadata): string {
 | 
					export function GetCoverFromFile(metadata: IAudioMetadata): string {
 | 
				
			||||||
    if (metadata.common?.picture && metadata.common.picture.length > 0) {
 | 
					  if (metadata.common?.picture && metadata.common.picture.length > 0) {
 | 
				
			||||||
        return URL.createObjectURL(new Blob(
 | 
					    return URL.createObjectURL(
 | 
				
			||||||
            [metadata.common.picture[0].data],
 | 
					      new Blob([metadata.common.picture[0].data], { type: metadata.common.picture[0].format }),
 | 
				
			||||||
            {type: metadata.common.picture[0].format}
 | 
					    );
 | 
				
			||||||
        ));
 | 
					  }
 | 
				
			||||||
    }
 | 
					  return '';
 | 
				
			||||||
    return "";
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IMusicMetaBasic {
 | 
					export interface IMusicMetaBasic {
 | 
				
			||||||
    title: string
 | 
					  title: string;
 | 
				
			||||||
    artist?: string
 | 
					  artist?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function GetMetaFromFile(filename: string, exist_title?: string, exist_artist?: string, separator = "-")
 | 
					export function GetMetaFromFile(
 | 
				
			||||||
    : IMusicMetaBasic {
 | 
					  filename: string,
 | 
				
			||||||
    const meta: IMusicMetaBasic = {title: exist_title ?? "", artist: exist_artist}
 | 
					  exist_title?: string,
 | 
				
			||||||
 | 
					  exist_artist?: string,
 | 
				
			||||||
 | 
					  separator = '-',
 | 
				
			||||||
 | 
					): IMusicMetaBasic {
 | 
				
			||||||
 | 
					  const meta: IMusicMetaBasic = { title: exist_title ?? '', artist: exist_artist };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const items = filename.split(separator);
 | 
					  const items = filename.split(separator);
 | 
				
			||||||
    if (items.length > 1) {
 | 
					  if (items.length > 1) {
 | 
				
			||||||
        if (!meta.artist) meta.artist = items[0].trim();
 | 
					    if (!meta.artist) meta.artist = items[0].trim();
 | 
				
			||||||
        if (!meta.title) meta.title = items[1].trim();
 | 
					    if (!meta.title) meta.title = items[1].trim();
 | 
				
			||||||
    } else if (items.length === 1) {
 | 
					  } else if (items.length === 1) {
 | 
				
			||||||
        if (!meta.title) meta.title = items[0].trim();
 | 
					    if (!meta.title) meta.title = items[0].trim();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return meta;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GetImageFromURL(
 | 
				
			||||||
 | 
					  src: string,
 | 
				
			||||||
 | 
					): Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const resp = await fetch(src);
 | 
				
			||||||
 | 
					    const mime = resp.headers.get('Content-Type');
 | 
				
			||||||
 | 
					    if (mime?.startsWith('image/')) {
 | 
				
			||||||
 | 
					      const buffer = await resp.arrayBuffer();
 | 
				
			||||||
 | 
					      const url = URL.createObjectURL(new Blob([buffer], { type: mime }));
 | 
				
			||||||
 | 
					      return { buffer, url, mime };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return meta
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.warn(e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function GetImageFromURL(src: string):
 | 
					 | 
				
			||||||
    Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        const resp = await fetch(src);
 | 
					 | 
				
			||||||
        const mime = resp.headers.get("Content-Type");
 | 
					 | 
				
			||||||
        if (mime?.startsWith("image/")) {
 | 
					 | 
				
			||||||
            const buffer = await resp.arrayBuffer();
 | 
					 | 
				
			||||||
            const url = URL.createObjectURL(new Blob([buffer], {type: mime}))
 | 
					 | 
				
			||||||
            return {buffer, url, mime}
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
        console.warn(e)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IMusicMeta {
 | 
					export interface IMusicMeta {
 | 
				
			||||||
    title: string
 | 
					  title: string;
 | 
				
			||||||
    artists?: string[]
 | 
					  artists?: string[];
 | 
				
			||||||
    album?: string
 | 
					  album?: string;
 | 
				
			||||||
    picture?: ArrayBuffer
 | 
					  picture?: ArrayBuffer;
 | 
				
			||||||
    picture_desc?: string
 | 
					  picture_desc?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
 | 
					export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
 | 
				
			||||||
    const writer = new ID3Writer(audioData);
 | 
					  const writer = new ID3Writer(audioData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // reserve original data
 | 
					  // reserve original data
 | 
				
			||||||
    const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || []
 | 
					  const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
 | 
				
			||||||
    frames.forEach(frame => {
 | 
					  frames.forEach((frame) => {
 | 
				
			||||||
        if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
 | 
					    if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
 | 
				
			||||||
            try {
 | 
					      try {
 | 
				
			||||||
                writer.setFrame(frame.id, frame.value)
 | 
					        writer.setFrame(frame.id, frame.value);
 | 
				
			||||||
            } catch (e) {
 | 
					      } catch (e) {}
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const old = original.common
 | 
					 | 
				
			||||||
    writer.setFrame('TPE1', old?.artists || info.artists || [])
 | 
					 | 
				
			||||||
        .setFrame('TIT2', old?.title || info.title)
 | 
					 | 
				
			||||||
        .setFrame('TALB', old?.album || info.album || "");
 | 
					 | 
				
			||||||
    if (info.picture) {
 | 
					 | 
				
			||||||
        writer.setFrame('APIC', {
 | 
					 | 
				
			||||||
            type: 3,
 | 
					 | 
				
			||||||
            data: info.picture,
 | 
					 | 
				
			||||||
            description: info.picture_desc || "Cover",
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return writer.addTag();
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const old = original.common;
 | 
				
			||||||
 | 
					  writer
 | 
				
			||||||
 | 
					    .setFrame('TPE1', old?.artists || info.artists || [])
 | 
				
			||||||
 | 
					    .setFrame('TIT2', old?.title || info.title)
 | 
				
			||||||
 | 
					    .setFrame('TALB', old?.album || info.album || '');
 | 
				
			||||||
 | 
					  if (info.picture) {
 | 
				
			||||||
 | 
					    writer.setFrame('APIC', {
 | 
				
			||||||
 | 
					      type: 3,
 | 
				
			||||||
 | 
					      data: info.picture,
 | 
				
			||||||
 | 
					      description: info.picture_desc || 'Cover',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return writer.addTag();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
 | 
					export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
 | 
				
			||||||
    const writer = new MetaFlac(audioData)
 | 
					  const writer = new MetaFlac(audioData);
 | 
				
			||||||
    const old = original.common
 | 
					  const old = original.common;
 | 
				
			||||||
    if (!old.title && !old.album && old.artists) {
 | 
					  if (!old.title && !old.album && old.artists) {
 | 
				
			||||||
        writer.setTag("TITLE=" + info.title)
 | 
					    writer.setTag('TITLE=' + info.title);
 | 
				
			||||||
        writer.setTag("ALBUM=" + info.album)
 | 
					    writer.setTag('ALBUM=' + info.album);
 | 
				
			||||||
        if (info.artists) {
 | 
					    if (info.artists) {
 | 
				
			||||||
            writer.removeTag("ARTIST")
 | 
					      writer.removeTag('ARTIST');
 | 
				
			||||||
            info.artists.forEach(artist => writer.setTag("ARTIST=" + artist))
 | 
					      info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (info.picture) {
 | 
					  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 } {
 | 
					export function SplitFilename(n: string): { name: string; ext: string } {
 | 
				
			||||||
    const pos = n.lastIndexOf(".")
 | 
					  const pos = n.lastIndexOf('.');
 | 
				
			||||||
    return {
 | 
					  return {
 | 
				
			||||||
        ext: n.substring(pos + 1).toLowerCase(),
 | 
					    ext: n.substring(pos + 1).toLowerCase(),
 | 
				
			||||||
        name: n.substring(0, pos)
 | 
					    name: n.substring(0, pos),
 | 
				
			||||||
    }
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,66 +1,67 @@
 | 
				
			|||||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
 | 
					import { Decrypt as RawDecrypt } from '@/decrypt/raw';
 | 
				
			||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile} from "@/decrypt/utils";
 | 
					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 MagicHeader = [0x69, 0x66, 0x6d, 0x74];
 | 
				
			||||||
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe]
 | 
					const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe];
 | 
				
			||||||
const FileTypeMap: { [key: string]: string } = {
 | 
					const FileTypeMap: { [key: string]: string } = {
 | 
				
			||||||
    " WAV": ".wav",
 | 
					  ' WAV': '.wav',
 | 
				
			||||||
    "FLAC": ".flac",
 | 
					  FLAC: '.flac',
 | 
				
			||||||
    " MP3": ".mp3",
 | 
					  ' MP3': '.mp3',
 | 
				
			||||||
    " A4M": ".m4a",
 | 
					  ' A4M': '.m4a',
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
					export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
				
			||||||
    const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
					  const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
 | 
					  if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
 | 
				
			||||||
        if (raw_ext === "xm") {
 | 
					    if (raw_ext === 'xm') {
 | 
				
			||||||
            throw Error("此xm文件已损坏")
 | 
					      throw Error('此xm文件已损坏');
 | 
				
			||||||
        } else {
 | 
					    } 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)) {
 | 
					  if (!FileTypeMap.hasOwnProperty(typeText)) {
 | 
				
			||||||
        throw Error("未知的.xm文件类型")
 | 
					    throw Error('未知的.xm文件类型');
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let key = oriData[0xf]
 | 
					  let key = oriData[0xf];
 | 
				
			||||||
    let dataOffset = oriData[0xc] | oriData[0xd] << 8 | oriData[0xe] << 16
 | 
					  let dataOffset = oriData[0xc] | (oriData[0xd] << 8) | (oriData[0xe] << 16);
 | 
				
			||||||
    let audioData = oriData.slice(0x10);
 | 
					  let audioData = oriData.slice(0x10);
 | 
				
			||||||
    let lenAudioData = audioData.length;
 | 
					  let lenAudioData = audioData.length;
 | 
				
			||||||
    for (let cur = dataOffset; cur < lenAudioData; ++cur)
 | 
					  for (let cur = dataOffset; cur < lenAudioData; ++cur) audioData[cur] = (audioData[cur] - key) ^ 0xff;
 | 
				
			||||||
        audioData[cur] = (audioData[cur] - key) ^ 0xff;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ext = FileTypeMap[typeText];
 | 
					  const ext = FileTypeMap[typeText];
 | 
				
			||||||
    const mime = AudioMimeType[ext];
 | 
					  const mime = AudioMimeType[ext];
 | 
				
			||||||
    let musicBlob = new Blob([audioData], {type: mime});
 | 
					  let musicBlob = new Blob([audioData], { type: mime });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const musicMeta = await metaParseBlob(musicBlob);
 | 
					  const musicMeta = await metaParseBlob(musicBlob);
 | 
				
			||||||
    if (ext === "wav") {
 | 
					  if (ext === 'wav') {
 | 
				
			||||||
        //todo:未知的编码方式
 | 
					    //todo:未知的编码方式
 | 
				
			||||||
        console.info(musicMeta.common)
 | 
					    console.info(musicMeta.common);
 | 
				
			||||||
        musicMeta.common.album = "";
 | 
					    musicMeta.common.album = '';
 | 
				
			||||||
        musicMeta.common.artist = "";
 | 
					    musicMeta.common.artist = '';
 | 
				
			||||||
        musicMeta.common.title = "";
 | 
					    musicMeta.common.title = '';
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
    const {title, artist} = GetMetaFromFile(raw_filename,
 | 
					  const { title, artist } = GetMetaFromFile(
 | 
				
			||||||
        musicMeta.common.title, musicMeta.common.artist,
 | 
					    raw_filename,
 | 
				
			||||||
        raw_filename.indexOf("_") === -1 ? "-" : "_")
 | 
					    musicMeta.common.title,
 | 
				
			||||||
 | 
					    musicMeta.common.artist,
 | 
				
			||||||
 | 
					    raw_filename.indexOf('_') === -1 ? '-' : '_',
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					  return {
 | 
				
			||||||
        title,
 | 
					    title,
 | 
				
			||||||
        artist,
 | 
					    artist,
 | 
				
			||||||
        ext,
 | 
					    ext,
 | 
				
			||||||
        mime,
 | 
					    mime,
 | 
				
			||||||
        album: musicMeta.common.album,
 | 
					    album: musicMeta.common.album,
 | 
				
			||||||
        picture: GetCoverFromFile(musicMeta),
 | 
					    picture: GetCoverFromFile(musicMeta),
 | 
				
			||||||
        file: URL.createObjectURL(musicBlob),
 | 
					    file: URL.createObjectURL(musicBlob),
 | 
				
			||||||
        blob: musicBlob,
 | 
					    blob: musicBlob,
 | 
				
			||||||
        rawExt: "xm"
 | 
					    rawExt: 'xm',
 | 
				
			||||||
    }
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,2 @@
 | 
				
			|||||||
const bs = chrome || browser
 | 
					const bs = chrome || browser;
 | 
				
			||||||
bs.tabs.create({
 | 
					bs.tabs.create({ url: bs.runtime.getURL('./index.html') }, (tab) => console.log(tab));
 | 
				
			||||||
    url: bs.runtime.getURL('./index.html')
 | 
					 | 
				
			||||||
}, tab => console.log(tab))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										44
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								src/main.ts
									
									
									
									
									
								
							@ -1,25 +1,25 @@
 | 
				
			|||||||
import Vue from 'vue'
 | 
					import Vue from 'vue';
 | 
				
			||||||
import App from '@/App.vue'
 | 
					import App from '@/App.vue';
 | 
				
			||||||
import '@/registerServiceWorker'
 | 
					import '@/registerServiceWorker';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    Button,
 | 
					  Button,
 | 
				
			||||||
    Checkbox,
 | 
					  Checkbox,
 | 
				
			||||||
    Col,
 | 
					  Col,
 | 
				
			||||||
    Container,
 | 
					  Container,
 | 
				
			||||||
    Footer,
 | 
					  Footer,
 | 
				
			||||||
    Icon,
 | 
					  Icon,
 | 
				
			||||||
    Image,
 | 
					  Image,
 | 
				
			||||||
    Link,
 | 
					  Link,
 | 
				
			||||||
    Main,
 | 
					  Main,
 | 
				
			||||||
    Notification,
 | 
					  Notification,
 | 
				
			||||||
    Progress,
 | 
					  Progress,
 | 
				
			||||||
    Radio,
 | 
					  Radio,
 | 
				
			||||||
    Row,
 | 
					  Row,
 | 
				
			||||||
    Table,
 | 
					  Table,
 | 
				
			||||||
    TableColumn,
 | 
					  TableColumn,
 | 
				
			||||||
    Tooltip,
 | 
					  Tooltip,
 | 
				
			||||||
    Upload,
 | 
					  Upload,
 | 
				
			||||||
    MessageBox
 | 
					  MessageBox,
 | 
				
			||||||
} from 'element-ui';
 | 
					} from 'element-ui';
 | 
				
			||||||
import 'element-ui/lib/theme-chalk/base.css';
 | 
					import 'element-ui/lib/theme-chalk/base.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,5 +44,5 @@ Vue.prototype.$confirm = MessageBox.confirm;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Vue.config.productionTip = false;
 | 
					Vue.config.productionTip = false;
 | 
				
			||||||
new Vue({
 | 
					new Vue({
 | 
				
			||||||
    render: h => h(App),
 | 
					  render: (h) => h(App),
 | 
				
			||||||
}).$mount('#app');
 | 
					}).$mount('#app');
 | 
				
			||||||
 | 
				
			|||||||
@ -1,31 +1,30 @@
 | 
				
			|||||||
/* eslint-disable no-console */
 | 
					/* eslint-disable no-console */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {register} from 'register-service-worker'
 | 
					import { register } from 'register-service-worker';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (process.env.NODE_ENV === 'production' && window.location.protocol === "https:") {
 | 
					if (process.env.NODE_ENV === 'production' && window.location.protocol === 'https:') {
 | 
				
			||||||
 | 
					  register(`${process.env.BASE_URL}service-worker.js`, {
 | 
				
			||||||
    register(`${process.env.BASE_URL}service-worker.js`, {
 | 
					    ready() {
 | 
				
			||||||
        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() {
 | 
				
			||||||
        registered() {
 | 
					      console.log('Service worker has been registered.');
 | 
				
			||||||
            console.log('Service worker has been registered.')
 | 
					    },
 | 
				
			||||||
        },
 | 
					    cached() {
 | 
				
			||||||
        cached() {
 | 
					      console.log('Content has been cached for offline use.');
 | 
				
			||||||
            console.log('Content has been cached for offline use.')
 | 
					    },
 | 
				
			||||||
        },
 | 
					    updatefound() {
 | 
				
			||||||
        updatefound() {
 | 
					      console.log('New content is downloading.');
 | 
				
			||||||
            console.log('New content is downloading.')
 | 
					    },
 | 
				
			||||||
        },
 | 
					    updated() {
 | 
				
			||||||
        updated() {
 | 
					      console.log('New content is available.');
 | 
				
			||||||
            console.log('New content is available.');
 | 
					      window.location.reload();
 | 
				
			||||||
            window.location.reload();
 | 
					    },
 | 
				
			||||||
        },
 | 
					    offline() {
 | 
				
			||||||
        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) {
 | 
				
			||||||
        error(error) {
 | 
					      console.error('Error during service worker registration:', error);
 | 
				
			||||||
            console.error('Error during service worker registration:', error)
 | 
					    },
 | 
				
			||||||
        }
 | 
					  });
 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								src/shims-browser-id3-writer.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								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 {
 | 
					  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 {
 | 
					  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;
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										48
									
								
								src/shims-fs.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								src/shims-fs.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1,58 +1,54 @@
 | 
				
			|||||||
export interface FileSystemGetFileOptions {
 | 
					export interface FileSystemGetFileOptions {
 | 
				
			||||||
    create?: boolean
 | 
					  create?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FileSystemCreateWritableOptions {
 | 
					interface FileSystemCreateWritableOptions {
 | 
				
			||||||
    keepExistingData?: boolean
 | 
					  keepExistingData?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FileSystemRemoveOptions {
 | 
					interface FileSystemRemoveOptions {
 | 
				
			||||||
    recursive?: boolean
 | 
					  recursive?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FileSystemFileHandle {
 | 
					interface FileSystemFileHandle {
 | 
				
			||||||
    getFile(): Promise<File>;
 | 
					  getFile(): Promise<File>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>
 | 
					  createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum WriteCommandType {
 | 
					enum WriteCommandType {
 | 
				
			||||||
    write = "write",
 | 
					  write = 'write',
 | 
				
			||||||
    seek = "seek",
 | 
					  seek = 'seek',
 | 
				
			||||||
    truncate = "truncate",
 | 
					  truncate = 'truncate',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface WriteParams {
 | 
					interface WriteParams {
 | 
				
			||||||
    type: WriteCommandType
 | 
					  type: WriteCommandType;
 | 
				
			||||||
    size?: number
 | 
					  size?: number;
 | 
				
			||||||
    position?: number
 | 
					  position?: number;
 | 
				
			||||||
    data: BufferSource | Blob | string
 | 
					  data: BufferSource | Blob | string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams
 | 
					type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FileSystemWritableFileStream extends WritableStream {
 | 
					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 {
 | 
					export declare interface FileSystemDirectoryHandle {
 | 
				
			||||||
    getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>
 | 
					  getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<undefined>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<undefined>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
    interface Window {
 | 
					  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 {
 | 
					declare global {
 | 
				
			||||||
  namespace JSX {
 | 
					  namespace JSX {
 | 
				
			||||||
    // tslint:disable no-empty-interface
 | 
					    // tslint:disable no-empty-interface
 | 
				
			||||||
    interface Element extends VNode {
 | 
					    interface Element extends VNode {}
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // tslint:disable no-empty-interface
 | 
					    // tslint:disable no-empty-interface
 | 
				
			||||||
    interface ElementClass extends Vue {
 | 
					    interface ElementClass extends Vue {}
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    interface IntrinsicElements {
 | 
					    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' {
 | 
					declare module '*.vue' {
 | 
				
			||||||
  import Vue from 'vue'
 | 
					  import Vue from 'vue';
 | 
				
			||||||
  export default 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 {
 | 
					export interface UpdateInfo {
 | 
				
			||||||
    Found: boolean
 | 
					  Found: boolean;
 | 
				
			||||||
    HttpsFound: boolean
 | 
					  HttpsFound: boolean;
 | 
				
			||||||
    Version: string
 | 
					  Version: string;
 | 
				
			||||||
    URL: string
 | 
					  URL: string;
 | 
				
			||||||
    Detail: string
 | 
					  Detail: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function checkUpdate(version: string): Promise<UpdateInfo> {
 | 
					export async function checkUpdate(version: string): Promise<UpdateInfo> {
 | 
				
			||||||
    const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", {
 | 
					  const resp = await fetch(IXAREA_API_ENDPOINT + '/music/app-version', {
 | 
				
			||||||
        method: "POST",
 | 
					    method: 'POST',
 | 
				
			||||||
        headers: {"Content-Type": "application/json"},
 | 
					    headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
        body: JSON.stringify({"Version": version})
 | 
					    body: JSON.stringify({ Version: version }),
 | 
				
			||||||
    });
 | 
					  });
 | 
				
			||||||
    return await resp.json();
 | 
					  return await resp.json();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function reportKeyUsage(keyData: Uint8Array, maskData: number[], filename: string, format: string, title: string, artist?: string, album?: string) {
 | 
					export function reportKeyUsage(
 | 
				
			||||||
    return fetch(IXAREA_API_ENDPOINT + "/qmcmask/usage", {
 | 
					  keyData: Uint8Array,
 | 
				
			||||||
        method: "POST",
 | 
					  maskData: number[],
 | 
				
			||||||
        headers: {"Content-Type": "application/json"},
 | 
					  filename: string,
 | 
				
			||||||
        body: JSON.stringify({
 | 
					  format: string,
 | 
				
			||||||
            Mask: Base64Encode(new Uint8Array(maskData)), Key: Base64Encode(keyData),
 | 
					  title: string,
 | 
				
			||||||
            Artist: artist, Title: title, Album: album, Filename: filename, Format: format
 | 
					  artist?: string,
 | 
				
			||||||
        }),
 | 
					  album?: string,
 | 
				
			||||||
    })
 | 
					) {
 | 
				
			||||||
 | 
					  return fetch(IXAREA_API_ENDPOINT + '/qmcmask/usage', {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					    headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
 | 
					    body: JSON.stringify({
 | 
				
			||||||
 | 
					      Mask: Base64Encode(new Uint8Array(maskData)),
 | 
				
			||||||
 | 
					      Key: Base64Encode(keyData),
 | 
				
			||||||
 | 
					      Artist: artist,
 | 
				
			||||||
 | 
					      Title: title,
 | 
				
			||||||
 | 
					      Album: album,
 | 
				
			||||||
 | 
					      Filename: filename,
 | 
				
			||||||
 | 
					      Format: format,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface KeyInfo {
 | 
					interface KeyInfo {
 | 
				
			||||||
    Matrix44: string
 | 
					  Matrix44: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function queryKeyInfo(keyData: Uint8Array, filename: string, format: string): Promise<KeyInfo> {
 | 
					export async function queryKeyInfo(keyData: Uint8Array, filename: string, format: string): Promise<KeyInfo> {
 | 
				
			||||||
    const resp = await fetch(IXAREA_API_ENDPOINT + "/qmcmask/query", {
 | 
					  const resp = await fetch(IXAREA_API_ENDPOINT + '/qmcmask/query', {
 | 
				
			||||||
        method: "POST",
 | 
					    method: 'POST',
 | 
				
			||||||
        headers: {"Content-Type": "application/json"},
 | 
					    headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
        body: JSON.stringify({Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44}),
 | 
					    body: JSON.stringify({ Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44 }),
 | 
				
			||||||
    });
 | 
					  });
 | 
				
			||||||
    return await resp.json();
 | 
					  return await resp.json();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface CoverInfo {
 | 
					export interface CoverInfo {
 | 
				
			||||||
    Id: string
 | 
					  Id: string;
 | 
				
			||||||
    Type: number
 | 
					  Type: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function queryAlbumCover(title: string, artist?: string, album?: string): Promise<CoverInfo> {
 | 
					export async function queryAlbumCover(title: string, artist?: string, album?: string): Promise<CoverInfo> {
 | 
				
			||||||
    const endpoint = IXAREA_API_ENDPOINT + "/music/qq-cover"
 | 
					  const endpoint = IXAREA_API_ENDPOINT + '/music/qq-cover';
 | 
				
			||||||
    const params = new URLSearchParams([["Title", title], ["Artist", artist ?? ""], ["Album", album ?? ""]])
 | 
					  const params = new URLSearchParams([
 | 
				
			||||||
    const resp = await fetch(`${endpoint}?${params.toString()}`)
 | 
					    ['Title', title],
 | 
				
			||||||
    return await resp.json()
 | 
					    ['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
 | 
					// Use of this source code is governed by a BSD-style
 | 
				
			||||||
// license that can be found in https://go.dev/LICENSE.
 | 
					// license that can be found in https://go.dev/LICENSE.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {TeaCipher} from "@/utils/tea";
 | 
					import { TeaCipher } from '@/utils/tea';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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,
 | 
				
			||||||
 | 
					  ])
 | 
				
			||||||
 | 
					  expect(() => new TeaCipher(testKey.slice(0, 16))).not.toThrow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("key size", () => {
 | 
					  expect(() => new TeaCipher(testKey)).toThrow();
 | 
				
			||||||
    const testKey = new Uint8Array([
 | 
					 | 
				
			||||||
        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
 | 
					 | 
				
			||||||
        0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
 | 
					 | 
				
			||||||
        0x00
 | 
					 | 
				
			||||||
    ])
 | 
					 | 
				
			||||||
    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.slice(0, 15))).toThrow();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// prettier-ignore
 | 
				
			||||||
const teaTests = [
 | 
					const teaTests = [
 | 
				
			||||||
    // These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
 | 
					  // These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
 | 
				
			||||||
    {
 | 
					  {
 | 
				
			||||||
        rounds: TeaCipher.numRounds,
 | 
					    rounds: TeaCipher.numRounds,
 | 
				
			||||||
        key: new Uint8Array([
 | 
					    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]),
 | 
					    plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
 | 
				
			||||||
        cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
 | 
					    cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
 | 
				
			||||||
    },
 | 
					  },
 | 
				
			||||||
    {
 | 
					  {
 | 
				
			||||||
        rounds: TeaCipher.numRounds,
 | 
					    rounds: TeaCipher.numRounds,
 | 
				
			||||||
        key: new Uint8Array([
 | 
					    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]),
 | 
					    plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
 | 
				
			||||||
        cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
 | 
					    cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
 | 
				
			||||||
    },
 | 
					  },
 | 
				
			||||||
    {
 | 
					  {
 | 
				
			||||||
        rounds: 16,
 | 
					    rounds: 16,
 | 
				
			||||||
        key: new Uint8Array([
 | 
					    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]),
 | 
					    plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
 | 
				
			||||||
        cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
 | 
					    cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
 | 
				
			||||||
    },
 | 
					  },
 | 
				
			||||||
]
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("rounds", () => {
 | 
					test('rounds', () => {
 | 
				
			||||||
    const tt = teaTests[0];
 | 
					  const tt = teaTests[0];
 | 
				
			||||||
    expect(() => new TeaCipher(tt.key, tt.rounds - 1))
 | 
					  expect(() => new TeaCipher(tt.key, tt.rounds - 1)).toThrow();
 | 
				
			||||||
        .toThrow()
 | 
					});
 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('encrypt & decrypt', () => {
 | 
				
			||||||
 | 
					  for (const tt of teaTests) {
 | 
				
			||||||
 | 
					    const c = new TeaCipher(tt.key, tt.rounds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("encrypt & decrypt", () => {
 | 
					    const buf = new Uint8Array(8);
 | 
				
			||||||
    for (const tt of teaTests) {
 | 
					    const bufView = new DataView(buf.buffer);
 | 
				
			||||||
        const c = new TeaCipher(tt.key, tt.rounds)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const buf = new Uint8Array(8)
 | 
					    c.encrypt(bufView, new DataView(tt.plainText.buffer));
 | 
				
			||||||
        const bufView = new DataView(buf.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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										100
									
								
								src/utils/tea.ts
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								src/utils/tea.ts
									
									
									
									
									
								
							@ -15,68 +15,66 @@
 | 
				
			|||||||
// where compatibility with legacy systems, not security, is the goal.
 | 
					// where compatibility with legacy systems, not security, is the goal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TeaCipher {
 | 
					export class TeaCipher {
 | 
				
			||||||
    // BlockSize is the size of a TEA block, in bytes.
 | 
					  // BlockSize is the size of a TEA block, in bytes.
 | 
				
			||||||
    static readonly BlockSize = 8;
 | 
					  static readonly BlockSize = 8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // KeySize is the size of a TEA key, in bytes.
 | 
					  // KeySize is the size of a TEA key, in bytes.
 | 
				
			||||||
    static readonly KeySize = 16;
 | 
					  static readonly KeySize = 16;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // delta is the TEA key schedule constant.
 | 
					  // delta is the TEA key schedule constant.
 | 
				
			||||||
    static readonly delta = 0x9e3779b9;
 | 
					  static readonly delta = 0x9e3779b9;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // numRounds 64 is the standard number of rounds in TEA.
 | 
					  // numRounds 64 is the standard number of rounds in TEA.
 | 
				
			||||||
    static readonly numRounds = 64;
 | 
					  static readonly numRounds = 64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    k0: number
 | 
					  k0: number;
 | 
				
			||||||
    k1: number
 | 
					  k1: number;
 | 
				
			||||||
    k2: number
 | 
					  k2: number;
 | 
				
			||||||
    k3: number
 | 
					  k3: number;
 | 
				
			||||||
    rounds: number
 | 
					  rounds: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
 | 
					  constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
 | 
				
			||||||
        if (key.length != 16) {
 | 
					    if (key.length != 16) {
 | 
				
			||||||
            throw Error("incorrect key size")
 | 
					      throw Error('incorrect key size');
 | 
				
			||||||
        }
 | 
					    }
 | 
				
			||||||
        if ((rounds & 1) != 0) {
 | 
					    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) {
 | 
					  encrypt(dst: DataView, src: DataView) {
 | 
				
			||||||
 | 
					    let v0 = src.getUint32(0, false);
 | 
				
			||||||
 | 
					    let v1 = src.getUint32(4, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let v0 = src.getUint32(0, false)
 | 
					    let sum = 0;
 | 
				
			||||||
        let v1 = src.getUint32(4, false)
 | 
					    for (let i = 0; i < this.rounds / 2; i++) {
 | 
				
			||||||
 | 
					      sum = sum + TeaCipher.delta;
 | 
				
			||||||
        let sum = 0
 | 
					      v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
 | 
				
			||||||
        for (let i = 0; i < this.rounds / 2; i++) {
 | 
					      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)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    decrypt(dst: DataView, src: DataView) {
 | 
					    dst.setUint32(0, v0, false);
 | 
				
			||||||
        let v0 = src.getUint32(0, false)
 | 
					    dst.setUint32(4, v1, false);
 | 
				
			||||||
        let v1 = src.getUint32(4, false)
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let sum = TeaCipher.delta * this.rounds / 2
 | 
					  decrypt(dst: DataView, src: DataView) {
 | 
				
			||||||
        for (let i = 0; i < this.rounds / 2; i++) {
 | 
					    let v0 = src.getUint32(0, false);
 | 
				
			||||||
            v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3)
 | 
					    let v1 = src.getUint32(4, false);
 | 
				
			||||||
            v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1)
 | 
					
 | 
				
			||||||
            sum -= TeaCipher.delta
 | 
					    let sum = (TeaCipher.delta * this.rounds) / 2;
 | 
				
			||||||
        }
 | 
					    for (let i = 0; i < this.rounds / 2; i++) {
 | 
				
			||||||
        dst.setUint32(0, v0, false)
 | 
					      v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
 | 
				
			||||||
        dst.setUint32(4, v1, false)
 | 
					      v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
 | 
				
			||||||
 | 
					      sum -= TeaCipher.delta;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    dst.setUint32(0, v0, false);
 | 
				
			||||||
 | 
					    dst.setUint32(4, v1, false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,79 +1,80 @@
 | 
				
			|||||||
import {DecryptResult} from "@/decrypt/entity";
 | 
					import { DecryptResult } from '@/decrypt/entity';
 | 
				
			||||||
import {FileSystemDirectoryHandle} from "@/shims-fs";
 | 
					import { FileSystemDirectoryHandle } from '@/shims-fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum FilenamePolicy {
 | 
					export enum FilenamePolicy {
 | 
				
			||||||
    ArtistAndTitle,
 | 
					  ArtistAndTitle,
 | 
				
			||||||
    TitleOnly,
 | 
					  TitleOnly,
 | 
				
			||||||
    TitleAndArtist,
 | 
					  TitleAndArtist,
 | 
				
			||||||
    SameAsOriginal,
 | 
					  SameAsOriginal,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const FilenamePolicies: { key: FilenamePolicy, text: string }[] = [
 | 
					export const FilenamePolicies: { key: FilenamePolicy; text: string }[] = [
 | 
				
			||||||
    {key: FilenamePolicy.ArtistAndTitle, text: "歌手-歌曲名"},
 | 
					  { key: FilenamePolicy.ArtistAndTitle, text: '歌手-歌曲名' },
 | 
				
			||||||
    {key: FilenamePolicy.TitleOnly, text: "歌曲名"},
 | 
					  { key: FilenamePolicy.TitleOnly, text: '歌曲名' },
 | 
				
			||||||
    {key: FilenamePolicy.TitleAndArtist, text: "歌曲名-歌手"},
 | 
					  { key: FilenamePolicy.TitleAndArtist, text: '歌曲名-歌手' },
 | 
				
			||||||
    {key: FilenamePolicy.SameAsOriginal, text: "同源文件名"},
 | 
					  { key: FilenamePolicy.SameAsOriginal, text: '同源文件名' },
 | 
				
			||||||
]
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy): string {
 | 
					export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy): string {
 | 
				
			||||||
    switch (policy) {
 | 
					  switch (policy) {
 | 
				
			||||||
        case FilenamePolicy.TitleOnly:
 | 
					    case FilenamePolicy.TitleOnly:
 | 
				
			||||||
            return `${data.title}.${data.ext}`;
 | 
					      return `${data.title}.${data.ext}`;
 | 
				
			||||||
        case FilenamePolicy.TitleAndArtist:
 | 
					    case FilenamePolicy.TitleAndArtist:
 | 
				
			||||||
            return `${data.title} - ${data.artist}.${data.ext}`;
 | 
					      return `${data.title} - ${data.artist}.${data.ext}`;
 | 
				
			||||||
        case FilenamePolicy.SameAsOriginal:
 | 
					    case FilenamePolicy.SameAsOriginal:
 | 
				
			||||||
            return `${data.rawFilename}.${data.ext}`;
 | 
					      return `${data.rawFilename}.${data.ext}`;
 | 
				
			||||||
        default:
 | 
					    default:
 | 
				
			||||||
        case FilenamePolicy.ArtistAndTitle:
 | 
					    case FilenamePolicy.ArtistAndTitle:
 | 
				
			||||||
            return `${data.artist} - ${data.title}.${data.ext}`;
 | 
					      return `${data.artist} - ${data.title}.${data.ext}`;
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) {
 | 
					export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) {
 | 
				
			||||||
    let filename = GetDownloadFilename(data, policy)
 | 
					  let filename = GetDownloadFilename(data, policy);
 | 
				
			||||||
    // prevent filename exist
 | 
					  // prevent filename exist
 | 
				
			||||||
    try {
 | 
					  try {
 | 
				
			||||||
        await dir.getFileHandle(filename)
 | 
					    await dir.getFileHandle(filename);
 | 
				
			||||||
        filename = `${new Date().getTime()} - ${filename}`
 | 
					    filename = `${new Date().getTime()} - ${filename}`;
 | 
				
			||||||
    } catch (e) {
 | 
					  } catch (e) {}
 | 
				
			||||||
    }
 | 
					  const file = await dir.getFileHandle(filename, { create: true });
 | 
				
			||||||
    const file = await dir.getFileHandle(filename, {create: true})
 | 
					  const w = await file.createWritable();
 | 
				
			||||||
    const w = await file.createWritable()
 | 
					  await w.write(data.blob);
 | 
				
			||||||
    await w.write(data.blob)
 | 
					  await w.close();
 | 
				
			||||||
    await w.close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) {
 | 
					export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) {
 | 
				
			||||||
    const a = document.createElement('a');
 | 
					  const a = document.createElement('a');
 | 
				
			||||||
    a.href = data.file;
 | 
					  a.href = data.file;
 | 
				
			||||||
    a.download = GetDownloadFilename(data, policy)
 | 
					  a.download = GetDownloadFilename(data, policy);
 | 
				
			||||||
    document.body.append(a);
 | 
					  document.body.append(a);
 | 
				
			||||||
    a.click();
 | 
					  a.click();
 | 
				
			||||||
    a.remove();
 | 
					  a.remove();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function RemoveBlobMusic(data: DecryptResult) {
 | 
					export function RemoveBlobMusic(data: DecryptResult) {
 | 
				
			||||||
    URL.revokeObjectURL(data.file);
 | 
					  URL.revokeObjectURL(data.file);
 | 
				
			||||||
    if (data.picture?.startsWith("blob:")) {
 | 
					  if (data.picture?.startsWith('blob:')) {
 | 
				
			||||||
        URL.revokeObjectURL(data.picture);
 | 
					    URL.revokeObjectURL(data.picture);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class DecryptQueue {
 | 
					export class DecryptQueue {
 | 
				
			||||||
    private readonly pending: (() => Promise<void>)[];
 | 
					  private readonly pending: (() => Promise<void>)[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					  constructor() {
 | 
				
			||||||
        this.pending = []
 | 
					    this.pending = [];
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queue(fn: () => Promise<void>) {
 | 
					  queue(fn: () => Promise<void>) {
 | 
				
			||||||
        this.pending.push(fn)
 | 
					    this.pending.push(fn);
 | 
				
			||||||
        this.consume()
 | 
					    this.consume();
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private consume() {
 | 
					  private consume() {
 | 
				
			||||||
        const fn = this.pending.shift()
 | 
					    const fn = this.pending.shift();
 | 
				
			||||||
        if (fn) fn().then(() => this.consume).catch(console.error)
 | 
					    if (fn)
 | 
				
			||||||
    }
 | 
					      fn()
 | 
				
			||||||
 | 
					        .then(() => this.consume)
 | 
				
			||||||
 | 
					        .catch(console.error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import {expose} from "threads/worker";
 | 
					import { expose } from 'threads/worker';
 | 
				
			||||||
import {CommonDecrypt} from "@/decrypt/common";
 | 
					import { CommonDecrypt } from '@/decrypt/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
expose(CommonDecrypt)
 | 
					expose(CommonDecrypt);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,157 +1,156 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					  <div>
 | 
				
			||||||
        <file-selector @error="showFail" @success="showSuccess"/>
 | 
					    <file-selector @error="showFail" @success="showSuccess" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div id="app-control">
 | 
					    <div id="app-control">
 | 
				
			||||||
            <el-row class="mb-3">
 | 
					      <el-row class="mb-3">
 | 
				
			||||||
                <span>歌曲命名格式:</span>
 | 
					        <span>歌曲命名格式:</span>
 | 
				
			||||||
                <el-radio v-for="k in FilenamePolicies" :key="k.key"
 | 
					        <el-radio v-for="k in FilenamePolicies" :key="k.key" v-model="filename_policy" :label="k.key">
 | 
				
			||||||
                          v-model="filename_policy" :label="k.key">
 | 
					          {{ k.text }}
 | 
				
			||||||
                    {{ k.text }}
 | 
					        </el-radio>
 | 
				
			||||||
                </el-radio>
 | 
					      </el-row>
 | 
				
			||||||
            </el-row>
 | 
					      <el-row>
 | 
				
			||||||
            <el-row>
 | 
					        <el-button icon="el-icon-download" plain @click="handleDownloadAll">下载全部</el-button>
 | 
				
			||||||
                <el-button icon="el-icon-download" plain @click="handleDownloadAll">下载全部</el-button>
 | 
					        <el-button icon="el-icon-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
 | 
				
			||||||
                <el-button icon="el-icon-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <el-tooltip class="item" effect="dark" placement="top-start">
 | 
					        <el-tooltip class="item" effect="dark" placement="top-start">
 | 
				
			||||||
                    <div slot="content">
 | 
					          <div slot="content">
 | 
				
			||||||
                        <span v-if="instant_save">工作模式: {{ dir ? "写入本地文件系统" : "调用浏览器下载" }}</span>
 | 
					            <span v-if="instant_save">工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}</span>
 | 
				
			||||||
                        <span v-else>
 | 
					            <span v-else>
 | 
				
			||||||
                            当您使用此工具进行大量文件解锁的时候,建议开启此选项。<br/>
 | 
					              当您使用此工具进行大量文件解锁的时候,建议开启此选项。<br />
 | 
				
			||||||
                            开启后,解锁结果将不会存留于浏览器中,防止内存不足。
 | 
					              开启后,解锁结果将不会存留于浏览器中,防止内存不足。
 | 
				
			||||||
                        </span>
 | 
					            </span>
 | 
				
			||||||
                    </div>
 | 
					          </div>
 | 
				
			||||||
                    <el-checkbox v-model="instant_save" border class="ml-2">立即保存</el-checkbox>
 | 
					          <el-checkbox v-model="instant_save" border class="ml-2">立即保存</el-checkbox>
 | 
				
			||||||
                </el-tooltip>
 | 
					        </el-tooltip>
 | 
				
			||||||
            </el-row>
 | 
					      </el-row>
 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <audio :autoplay="playing_auto" :src="playing_url" controls/>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @play="changePlaying"/>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <audio :autoplay="playing_auto" :src="playing_url" controls />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @play="changePlaying" />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import FileSelector from '@/component/FileSelector';
 | 
				
			||||||
import FileSelector from "@/component/FileSelector"
 | 
					import PreviewTable from '@/component/PreviewTable';
 | 
				
			||||||
import PreviewTable from "@/component/PreviewTable"
 | 
					import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
 | 
				
			||||||
import {DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile} from "@/utils/utils"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    name: 'Home',
 | 
					  name: 'Home',
 | 
				
			||||||
    components: {
 | 
					  components: {
 | 
				
			||||||
        FileSelector,
 | 
					    FileSelector,
 | 
				
			||||||
        PreviewTable
 | 
					    PreviewTable,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      tableData: [],
 | 
				
			||||||
 | 
					      playing_url: '',
 | 
				
			||||||
 | 
					      playing_auto: false,
 | 
				
			||||||
 | 
					      filename_policy: FilenamePolicy.ArtistAndTitle,
 | 
				
			||||||
 | 
					      instant_save: false,
 | 
				
			||||||
 | 
					      FilenamePolicies,
 | 
				
			||||||
 | 
					      dir: null,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    instant_save(val) {
 | 
				
			||||||
 | 
					      if (val) this.showDirectlySave();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data() {
 | 
					  },
 | 
				
			||||||
        return {
 | 
					  methods: {
 | 
				
			||||||
            tableData: [],
 | 
					    async showSuccess(data) {
 | 
				
			||||||
            playing_url: "",
 | 
					      if (this.instant_save) {
 | 
				
			||||||
            playing_auto: false,
 | 
					        await this.saveFile(data);
 | 
				
			||||||
            filename_policy: FilenamePolicy.ArtistAndTitle,
 | 
					        RemoveBlobMusic(data);
 | 
				
			||||||
            instant_save: false,
 | 
					      } else {
 | 
				
			||||||
            FilenamePolicies,
 | 
					        this.tableData.push(data);
 | 
				
			||||||
            dir: null
 | 
					        this.$notify.success({
 | 
				
			||||||
 | 
					          title: '解锁成功',
 | 
				
			||||||
 | 
					          message: '成功解锁 ' + data.title,
 | 
				
			||||||
 | 
					          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)]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showFail(errInfo, filename) {
 | 
				
			||||||
 | 
					      console.error(errInfo, filename);
 | 
				
			||||||
 | 
					      this.$notify.error({
 | 
				
			||||||
 | 
					        title: '出现问题',
 | 
				
			||||||
 | 
					        message:
 | 
				
			||||||
 | 
					          errInfo +
 | 
				
			||||||
 | 
					          ',' +
 | 
				
			||||||
 | 
					          filename +
 | 
				
			||||||
 | 
					          ',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
 | 
				
			||||||
 | 
					        dangerouslyUseHTMLString: true,
 | 
				
			||||||
 | 
					        duration: 6000,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (process.env.NODE_ENV === 'production') {
 | 
				
			||||||
 | 
					        window._paq.push(['trackEvent', 'Error', String(errInfo), filename]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    changePlaying(url) {
 | 
				
			||||||
 | 
					      this.playing_url = url;
 | 
				
			||||||
 | 
					      this.playing_auto = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    handleDeleteAll() {
 | 
				
			||||||
 | 
					      this.tableData.forEach((value) => {
 | 
				
			||||||
 | 
					        RemoveBlobMusic(value);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      this.tableData = [];
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    handleDownloadAll() {
 | 
				
			||||||
 | 
					      let index = 0;
 | 
				
			||||||
 | 
					      let c = setInterval(() => {
 | 
				
			||||||
 | 
					        if (index < this.tableData.length) {
 | 
				
			||||||
 | 
					          this.saveFile(this.tableData[index]);
 | 
				
			||||||
 | 
					          index++;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          clearInterval(c);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      }, 300);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    watch: {
 | 
					 | 
				
			||||||
        instant_save(val) {
 | 
					 | 
				
			||||||
            if (val) this.showDirectlySave()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    methods: {
 | 
					 | 
				
			||||||
        async showSuccess(data) {
 | 
					 | 
				
			||||||
            if (this.instant_save) {
 | 
					 | 
				
			||||||
                await this.saveFile(data)
 | 
					 | 
				
			||||||
                RemoveBlobMusic(data);
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                this.tableData.push(data);
 | 
					 | 
				
			||||||
                this.$notify.success({
 | 
					 | 
				
			||||||
                    title: '解锁成功',
 | 
					 | 
				
			||||||
                    message: '成功解锁 ' + data.title,
 | 
					 | 
				
			||||||
                    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)]);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        showFail(errInfo, filename) {
 | 
					 | 
				
			||||||
            console.error(errInfo, filename)
 | 
					 | 
				
			||||||
            this.$notify.error({
 | 
					 | 
				
			||||||
                title: '出现问题',
 | 
					 | 
				
			||||||
                message: errInfo + "," + filename +
 | 
					 | 
				
			||||||
                    ',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
 | 
					 | 
				
			||||||
                dangerouslyUseHTMLString: true,
 | 
					 | 
				
			||||||
                duration: 6000
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            if (process.env.NODE_ENV === 'production') {
 | 
					 | 
				
			||||||
                window._paq.push(["trackEvent", "Error", String(errInfo), filename]);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        changePlaying(url) {
 | 
					 | 
				
			||||||
            this.playing_url = url;
 | 
					 | 
				
			||||||
            this.playing_auto = true;
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        handleDeleteAll() {
 | 
					 | 
				
			||||||
            this.tableData.forEach(value => {
 | 
					 | 
				
			||||||
                RemoveBlobMusic(value);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            this.tableData = [];
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        handleDownloadAll() {
 | 
					 | 
				
			||||||
            let index = 0;
 | 
					 | 
				
			||||||
            let c = setInterval(() => {
 | 
					 | 
				
			||||||
                if (index < this.tableData.length) {
 | 
					 | 
				
			||||||
                    this.saveFile(this.tableData[index])
 | 
					 | 
				
			||||||
                    index++;
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    clearInterval(c);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }, 300);
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async saveFile(data) {
 | 
					    async saveFile(data) {
 | 
				
			||||||
            if (this.dir) {
 | 
					      if (this.dir) {
 | 
				
			||||||
                await DirectlyWriteFile(data, this.filename_policy, this.dir)
 | 
					        await DirectlyWriteFile(data, this.filename_policy, this.dir);
 | 
				
			||||||
                this.$notify({
 | 
					        this.$notify({
 | 
				
			||||||
                    title: "保存成功",
 | 
					          title: '保存成功',
 | 
				
			||||||
                    message: data.title,
 | 
					          message: data.title,
 | 
				
			||||||
                    position: "top-left",
 | 
					          position: 'top-left',
 | 
				
			||||||
                    type: "success",
 | 
					          type: 'success',
 | 
				
			||||||
                    duration: 3000
 | 
					          duration: 3000,
 | 
				
			||||||
                })
 | 
					        });
 | 
				
			||||||
            } else {
 | 
					      } else {
 | 
				
			||||||
                DownloadBlobMusic(data, this.filename_policy)
 | 
					        DownloadBlobMusic(data, this.filename_policy);
 | 
				
			||||||
            }
 | 
					      }
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        async showDirectlySave() {
 | 
					 | 
				
			||||||
            if (!window.showDirectoryPicker) return
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                await this.$confirm("您的浏览器支持文件直接保存到磁盘,是否使用?",
 | 
					 | 
				
			||||||
                    "新特性提示", {
 | 
					 | 
				
			||||||
                        confirmButtonText: "使用",
 | 
					 | 
				
			||||||
                        cancelButtonText: "不使用",
 | 
					 | 
				
			||||||
                        type: "warning",
 | 
					 | 
				
			||||||
                        center: true
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
                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)
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
                console.error(e)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
}
 | 
					    async showDirectlySave() {
 | 
				
			||||||
 | 
					      if (!window.showDirectoryPicker) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await this.$confirm('您的浏览器支持文件直接保存到磁盘,是否使用?', '新特性提示', {
 | 
				
			||||||
 | 
					          confirmButtonText: '使用',
 | 
				
			||||||
 | 
					          cancelButtonText: '不使用',
 | 
				
			||||||
 | 
					          type: 'warning',
 | 
				
			||||||
 | 
					          center: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        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);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error(e);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user